Merge pull request #4454 from freqtrade/backtest_compound_speed
Backtest compound, wallet, ...
This commit is contained in:
commit
0db5c9746f
@ -16,6 +16,7 @@ usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
|||||||
[--max-open-trades INT]
|
[--max-open-trades INT]
|
||||||
[--stake-amount STAKE_AMOUNT] [--fee FLOAT]
|
[--stake-amount STAKE_AMOUNT] [--fee FLOAT]
|
||||||
[--eps] [--dmmp] [--enable-protections]
|
[--eps] [--dmmp] [--enable-protections]
|
||||||
|
[--dry-run-wallet DRY_RUN_WALLET]
|
||||||
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
|
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
|
||||||
[--export EXPORT] [--export-filename PATH]
|
[--export EXPORT] [--export-filename PATH]
|
||||||
|
|
||||||
@ -48,6 +49,9 @@ optional arguments:
|
|||||||
Enable protections for backtesting.Will slow
|
Enable protections for backtesting.Will slow
|
||||||
backtesting down by a considerable amount, but will
|
backtesting down by a considerable amount, but will
|
||||||
include configured protections
|
include configured protections
|
||||||
|
--dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET
|
||||||
|
Starting balance, used for backtesting / hyperopt and
|
||||||
|
dry-runs.
|
||||||
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
||||||
Provide a space-separated list of strategies to
|
Provide a space-separated list of strategies to
|
||||||
backtest. Please note that ticker-interval needs to be
|
backtest. Please note that ticker-interval needs to be
|
||||||
@ -91,8 +95,7 @@ Strategy arguments:
|
|||||||
## Test your strategy with Backtesting
|
## Test your strategy with Backtesting
|
||||||
|
|
||||||
Now you have good Buy and Sell strategies and some historic data, you want to test it against
|
Now you have good Buy and Sell strategies and some historic data, you want to test it against
|
||||||
real data. This is what we call
|
real data. This is what we call [backtesting](https://en.wikipedia.org/wiki/Backtesting).
|
||||||
[backtesting](https://en.wikipedia.org/wiki/Backtesting).
|
|
||||||
|
|
||||||
Backtesting will use the crypto-currencies (pairs) from your config file and load historical candle (OHCLV) data from `user_data/data/<exchange>` by default.
|
Backtesting will use the crypto-currencies (pairs) from your config file and load historical candle (OHCLV) data from `user_data/data/<exchange>` by default.
|
||||||
If no data is available for the exchange / pair / timeframe combination, backtesting will ask you to download them first using `freqtrade download-data`.
|
If no data is available for the exchange / pair / timeframe combination, backtesting will ask you to download them first using `freqtrade download-data`.
|
||||||
@ -100,6 +103,8 @@ For details on downloading, please refer to the [Data Downloading](data-download
|
|||||||
|
|
||||||
The result of backtesting will confirm if your bot has better odds of making a profit than a loss.
|
The result of backtesting will confirm if your bot has better odds of making a profit than a loss.
|
||||||
|
|
||||||
|
All profit calculations include fees, and freqtrade will use the exchange's default fees for the calculation.
|
||||||
|
|
||||||
!!! Warning "Using dynamic pairlists for backtesting"
|
!!! Warning "Using dynamic pairlists for backtesting"
|
||||||
Using dynamic pairlists is possible, however it relies on the current market conditions - which will not reflect the historic status of the pairlist.
|
Using dynamic pairlists is possible, however it relies on the current market conditions - which will not reflect the historic status of the pairlist.
|
||||||
Also, when using pairlists other than StaticPairlist, reproducability of backtesting-results cannot be guaranteed.
|
Also, when using pairlists other than StaticPairlist, reproducability of backtesting-results cannot be guaranteed.
|
||||||
@ -107,38 +112,56 @@ The result of backtesting will confirm if your bot has better odds of making a p
|
|||||||
|
|
||||||
To achieve reproducible results, best generate a pairlist via the [`test-pairlist`](utils.md#test-pairlist) command and use that as static pairlist.
|
To achieve reproducible results, best generate a pairlist via the [`test-pairlist`](utils.md#test-pairlist) command and use that as static pairlist.
|
||||||
|
|
||||||
### Run a backtesting against the currencies listed in your config file
|
### Starting balance
|
||||||
|
|
||||||
#### With 5 min candle (OHLCV) data (per default)
|
Backtesting will require a starting balance, which can be provided as `--dry-run-wallet <balance>` or `--starting-balance <balance>` command line argument, or via `dry_run_wallet` configuration setting.
|
||||||
|
This amount must be higher than `stake_amount`, otherwise the bot will not be able to simulate any trade.
|
||||||
|
|
||||||
|
### Dynamic stake amount
|
||||||
|
|
||||||
|
Backtesting supports [dynamic stake amount](configuration.md#dynamic-stake-amount) by configuring `stake_amount` as `"unlimited"`, which will split the starting balance into `max_open_trades` pieces.
|
||||||
|
Profits from early trades will result in subsequent higher stake amounts, resulting in compounding of profits over the backtesting period.
|
||||||
|
|
||||||
|
### Example backtesting commands
|
||||||
|
|
||||||
|
With 5 min candle (OHLCV) data (per default)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
freqtrade backtesting
|
freqtrade backtesting --strategy AwesomeStrategy
|
||||||
```
|
```
|
||||||
|
|
||||||
#### With 1 min candle (OHLCV) data
|
Where `--strategy AwesomeStrategy` / `-s AwesomeStrategy` refers to the class name of the strategy, which is within a python file in the `user_data/strategies` directory.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
With 1 min candle (OHLCV) data
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
freqtrade backtesting --timeframe 1m
|
freqtrade backtesting --strategy AwesomeStrategy --timeframe 1m
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Using a different on-disk historical candle (OHLCV) data source
|
---
|
||||||
|
|
||||||
|
Providing a custom starting balance of 1000 (in stake currency)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
freqtrade backtesting --strategy AwesomeStrategy --dry-run-wallet 1000
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Using a different on-disk historical candle (OHLCV) data source
|
||||||
|
|
||||||
Assume you downloaded the history data from the Bittrex exchange and kept it in the `user_data/data/bittrex-20180101` directory.
|
Assume you downloaded the history data from the Bittrex exchange and kept it in the `user_data/data/bittrex-20180101` directory.
|
||||||
You can then use this data for backtesting as follows:
|
You can then use this data for backtesting as follows:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
freqtrade --datadir user_data/data/bittrex-20180101 backtesting
|
freqtrade backtesting --strategy AwesomeStrategy --datadir user_data/data/bittrex-20180101
|
||||||
```
|
```
|
||||||
|
|
||||||
#### With a (custom) strategy file
|
---
|
||||||
|
|
||||||
```bash
|
Comparing multiple Strategies
|
||||||
freqtrade backtesting -s SampleStrategy
|
|
||||||
```
|
|
||||||
|
|
||||||
Where `-s SampleStrategy` refers to the class name within the strategy file `sample_strategy.py` found in the `freqtrade/user_data/strategies` directory.
|
|
||||||
|
|
||||||
#### Comparing multiple Strategies
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
freqtrade backtesting --strategy-list SampleStrategy1 AwesomeStrategy --timeframe 5m
|
freqtrade backtesting --strategy-list SampleStrategy1 AwesomeStrategy --timeframe 5m
|
||||||
@ -146,23 +169,29 @@ freqtrade backtesting --strategy-list SampleStrategy1 AwesomeStrategy --timefram
|
|||||||
|
|
||||||
Where `SampleStrategy1` and `AwesomeStrategy` refer to class names of strategies.
|
Where `SampleStrategy1` and `AwesomeStrategy` refer to class names of strategies.
|
||||||
|
|
||||||
#### Exporting trades to file
|
---
|
||||||
|
|
||||||
|
Exporting trades to file
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
freqtrade backtesting --export trades --config config.json --strategy SampleStrategy
|
freqtrade backtesting --strategy backtesting --export trades --config config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
The exported trades can be used for [further analysis](#further-backtest-result-analysis), or can be used by the plotting script `plot_dataframe.py` in the scripts directory.
|
The exported trades can be used for [further analysis](#further-backtest-result-analysis), or can be used by the plotting script `plot_dataframe.py` in the scripts directory.
|
||||||
|
|
||||||
#### Exporting trades to file specifying a custom filename
|
---
|
||||||
|
|
||||||
|
Exporting trades to file specifying a custom filename
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
freqtrade backtesting --export trades --export-filename=backtest_samplestrategy.json
|
freqtrade backtesting --strategy backtesting --export trades --export-filename=backtest_samplestrategy.json
|
||||||
```
|
```
|
||||||
|
|
||||||
Please also read about the [strategy startup period](strategy-customization.md#strategy-startup-period).
|
Please also read about the [strategy startup period](strategy-customization.md#strategy-startup-period).
|
||||||
|
|
||||||
#### Supplying custom fee value
|
---
|
||||||
|
|
||||||
|
Supplying custom fee value
|
||||||
|
|
||||||
Sometimes your account has certain fee rebates (fee reductions starting with a certain account size or monthly volume), which are not visible to ccxt.
|
Sometimes your account has certain fee rebates (fee reductions starting with a certain account size or monthly volume), which are not visible to ccxt.
|
||||||
To account for this in backtesting, you can use the `--fee` command line option to supply this value to backtesting.
|
To account for this in backtesting, you can use the `--fee` command line option to supply this value to backtesting.
|
||||||
@ -177,26 +206,26 @@ freqtrade backtesting --fee 0.001
|
|||||||
!!! Note
|
!!! Note
|
||||||
Only supply this option (or the corresponding configuration parameter) if you want to experiment with different fee values. By default, Backtesting fetches the default fee from the exchange pair/market info.
|
Only supply this option (or the corresponding configuration parameter) if you want to experiment with different fee values. By default, Backtesting fetches the default fee from the exchange pair/market info.
|
||||||
|
|
||||||
#### Running backtest with smaller testset by using timerange
|
---
|
||||||
|
|
||||||
Use the `--timerange` argument to change how much of the testset you want to use.
|
Running backtest with smaller test-set by using timerange
|
||||||
|
|
||||||
|
Use the `--timerange` argument to change how much of the test-set you want to use.
|
||||||
|
|
||||||
For example, running backtesting with the `--timerange=20190501-` option will use all available data starting with May 1st, 2019 from your inputdata.
|
For example, running backtesting with the `--timerange=20190501-` option will use all available data starting with May 1st, 2019 from your input data.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
freqtrade backtesting --timerange=20190501-
|
freqtrade backtesting --timerange=20190501-
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also specify particular dates or a range span indexed by start and stop.
|
You can also specify particular date ranges.
|
||||||
|
|
||||||
The full timerange specification:
|
The full timerange specification:
|
||||||
|
|
||||||
- Use tickframes till 2018/01/31: `--timerange=-20180131`
|
- Use data until 2018/01/31: `--timerange=-20180131`
|
||||||
- Use tickframes since 2018/01/31: `--timerange=20180131-`
|
- Use data since 2018/01/31: `--timerange=20180131-`
|
||||||
- Use tickframes since 2018/01/31 till 2018/03/01 : `--timerange=20180131-20180301`
|
- Use data since 2018/01/31 till 2018/03/01 : `--timerange=20180131-20180301`
|
||||||
- Use tickframes between POSIX timestamps 1527595200 1527618600:
|
- Use data between POSIX / epoch timestamps 1527595200 1527618600: `--timerange=1527595200-1527618600`
|
||||||
`--timerange=1527595200-1527618600`
|
|
||||||
|
|
||||||
## Understand the backtesting result
|
## Understand the backtesting result
|
||||||
|
|
||||||
@ -248,19 +277,30 @@ A backtesting result will look like that:
|
|||||||
| Max open trades | 3 |
|
| Max open trades | 3 |
|
||||||
| | |
|
| | |
|
||||||
| Total trades | 429 |
|
| Total trades | 429 |
|
||||||
| Total Profit % | 152.41% |
|
| Starting balance | 0.01000000 BTC |
|
||||||
|
| Final balance | 0.01762792 BTC |
|
||||||
|
| Absolute profit | 0.00762792 BTC |
|
||||||
|
| Total profit % | 76.2% |
|
||||||
| Trades per day | 3.575 |
|
| Trades per day | 3.575 |
|
||||||
|
| Avg. stake amount | 0.001 BTC |
|
||||||
|
| Total trade volume | 0.429 BTC |
|
||||||
| | |
|
| | |
|
||||||
| Best Pair | LSK/BTC 26.26% |
|
| Best Pair | LSK/BTC 26.26% |
|
||||||
| Worst Pair | ZEC/BTC -10.18% |
|
| Worst Pair | ZEC/BTC -10.18% |
|
||||||
| Best Trade | LSK/BTC 4.25% |
|
| Best Trade | LSK/BTC 4.25% |
|
||||||
| Worst Trade | ZEC/BTC -10.25% |
|
| Worst Trade | ZEC/BTC -10.25% |
|
||||||
| Best day | 25.27% |
|
| Best day | 0.00076 BTC |
|
||||||
| Worst day | -30.67% |
|
| Worst day | -0.00036 BTC |
|
||||||
|
| Days win/draw/lose | 12 / 82 / 25 |
|
||||||
| Avg. Duration Winners | 4:23:00 |
|
| Avg. Duration Winners | 4:23:00 |
|
||||||
| Avg. Duration Loser | 6:55:00 |
|
| Avg. Duration Loser | 6:55:00 |
|
||||||
| | |
|
| | |
|
||||||
| Max Drawdown | 50.63% |
|
| Min balance | 0.00945123 BTC |
|
||||||
|
| Max balance | 0.01846651 BTC |
|
||||||
|
| Drawdown | 50.63% |
|
||||||
|
| Drawdown | 0.0015 BTC |
|
||||||
|
| Drawdown high | 0.0013 BTC |
|
||||||
|
| Drawdown low | -0.0002 BTC |
|
||||||
| Drawdown Start | 2019-02-15 14:10:00 |
|
| Drawdown Start | 2019-02-15 14:10:00 |
|
||||||
| Drawdown End | 2019-04-11 18:15:00 |
|
| Drawdown End | 2019-04-11 18:15:00 |
|
||||||
| Market change | -5.88% |
|
| Market change | -5.88% |
|
||||||
@ -281,9 +321,9 @@ here:
|
|||||||
The bot has made `429` trades for an average duration of `4:12:00`, with a performance of `76.20%` (profit), that means it has
|
The bot has made `429` trades for an average duration of `4:12:00`, with a performance of `76.20%` (profit), that means it has
|
||||||
earned a total of `0.00762792 BTC` starting with a capital of 0.01 BTC.
|
earned a total of `0.00762792 BTC` starting with a capital of 0.01 BTC.
|
||||||
|
|
||||||
The column `avg profit %` shows the average profit for all trades made while the column `cum profit %` sums up all the profits/losses.
|
The column `Avg Profit %` shows the average profit for all trades made while the column `Cum Profit %` sums up all the profits/losses.
|
||||||
The column `tot profit %` shows instead the total profit % in relation to allocated capital (`max_open_trades * stake_amount`).
|
The column `Tot Profit %` shows instead the total profit % in relation to the starting balance.
|
||||||
In the above results we have `max_open_trades=2` and `stake_amount=0.005` in config so `tot_profit %` will be `(76.20/100) * (0.005 * 2) =~ 0.00762792 BTC`.
|
In the above results, we have a starting balance of 0.01 BTC and an absolute profit of 0.00762792 BTC - so the `Tot Profit %` will be `(0.00762792 / 0.01) * 100 ~= 76.2%`.
|
||||||
|
|
||||||
Your strategy performance is influenced by your buy strategy, your sell strategy, and also by the `minimal_roi` and `stop_loss` you have set.
|
Your strategy performance is influenced by your buy strategy, your sell strategy, and also by the `minimal_roi` and `stop_loss` you have set.
|
||||||
|
|
||||||
@ -324,19 +364,30 @@ It contains some useful key metrics about performance of your strategy on backte
|
|||||||
| Max open trades | 3 |
|
| Max open trades | 3 |
|
||||||
| | |
|
| | |
|
||||||
| Total trades | 429 |
|
| Total trades | 429 |
|
||||||
| Total Profit % | 152.41% |
|
| Starting balance | 0.01000000 BTC |
|
||||||
|
| Final balance | 0.01762792 BTC |
|
||||||
|
| Absolute profit | 0.00762792 BTC |
|
||||||
|
| Total profit % | 76.2% |
|
||||||
| Trades per day | 3.575 |
|
| Trades per day | 3.575 |
|
||||||
|
| Avg. stake amount | 0.001 BTC |
|
||||||
|
| Total trade volume | 0.429 BTC |
|
||||||
| | |
|
| | |
|
||||||
| Best Pair | LSK/BTC 26.26% |
|
| Best Pair | LSK/BTC 26.26% |
|
||||||
| Worst Pair | ZEC/BTC -10.18% |
|
| Worst Pair | ZEC/BTC -10.18% |
|
||||||
| Best Trade | LSK/BTC 4.25% |
|
| Best Trade | LSK/BTC 4.25% |
|
||||||
| Worst Trade | ZEC/BTC -10.25% |
|
| Worst Trade | ZEC/BTC -10.25% |
|
||||||
| Best day | 25.27% |
|
| Best day | 0.00076 BTC |
|
||||||
| Worst day | -30.67% |
|
| Worst day | -0.00036 BTC |
|
||||||
|
| Days win/draw/lose | 12 / 82 / 25 |
|
||||||
| Avg. Duration Winners | 4:23:00 |
|
| Avg. Duration Winners | 4:23:00 |
|
||||||
| Avg. Duration Loser | 6:55:00 |
|
| Avg. Duration Loser | 6:55:00 |
|
||||||
| | |
|
| | |
|
||||||
| Max Drawdown | 50.63% |
|
| Min balance | 0.00945123 BTC |
|
||||||
|
| Max balance | 0.01846651 BTC |
|
||||||
|
| Drawdown | 50.63% |
|
||||||
|
| Drawdown | 0.0015 BTC |
|
||||||
|
| Drawdown high | 0.0013 BTC |
|
||||||
|
| Drawdown low | -0.0002 BTC |
|
||||||
| Drawdown Start | 2019-02-15 14:10:00 |
|
| Drawdown Start | 2019-02-15 14:10:00 |
|
||||||
| Drawdown End | 2019-04-11 18:15:00 |
|
| Drawdown End | 2019-04-11 18:15:00 |
|
||||||
| Market change | -5.88% |
|
| Market change | -5.88% |
|
||||||
@ -347,13 +398,21 @@ It contains some useful key metrics about performance of your strategy on backte
|
|||||||
- `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option).
|
- `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option).
|
||||||
- `Max open trades`: Setting of `max_open_trades` (or `--max-open-trades`) - or number of pairs in the pairlist (whatever is lower).
|
- `Max open trades`: Setting of `max_open_trades` (or `--max-open-trades`) - or number of pairs in the pairlist (whatever is lower).
|
||||||
- `Total trades`: Identical to the total trades of the backtest output table.
|
- `Total trades`: Identical to the total trades of the backtest output table.
|
||||||
- `Total Profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table.
|
- `Starting balance`: Start balance - as given by dry-run-wallet (config or command line).
|
||||||
|
- `Final balance`: Final balance - starting balance + absolute profit.
|
||||||
|
- `Absolute profit`: Profit made in stake currency.
|
||||||
|
- `Total profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. Calculated as `(End capital − Starting capital) / Starting capital`.
|
||||||
- `Trades per day`: Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy).
|
- `Trades per day`: Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy).
|
||||||
|
- `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount.
|
||||||
|
- `Total trade volume`: Volume generated on the exchange to reach the above profit.
|
||||||
- `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Cum Profit %`.
|
- `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Cum Profit %`.
|
||||||
- `Best Trade` / `Worst Trade`: Biggest winning trade and biggest losing trade
|
- `Best Trade` / `Worst Trade`: Biggest single winning trade and biggest single losing trade.
|
||||||
- `Best day` / `Worst day`: Best and worst day based on daily profit.
|
- `Best day` / `Worst day`: Best and worst day based on daily profit.
|
||||||
|
- `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade).
|
||||||
- `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades.
|
- `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades.
|
||||||
- `Max Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced).
|
- `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period.
|
||||||
|
- `Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced).
|
||||||
|
- `Drawdown high` / `Drawdown low`: Profit at the beginning and end of the largest drawdown period. A negative low value means initial capital lost.
|
||||||
- `Drawdown Start` / `Drawdown End`: Start and end datetime for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command).
|
- `Drawdown Start` / `Drawdown End`: Start and end datetime for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command).
|
||||||
- `Market change`: Change of the market during the backtest period. Calculated as average of all pairs changes from the first to the last candle using the "close" column.
|
- `Market change`: Change of the market during the backtest period. Calculated as average of all pairs changes from the first to the last candle using the "close" column.
|
||||||
|
|
||||||
@ -418,6 +477,5 @@ Detailed output for all strategies one after the other will be available, so mak
|
|||||||
|
|
||||||
## Next step
|
## Next step
|
||||||
|
|
||||||
Great, your strategy is profitable. What if the bot can give your the
|
Great, your strategy is profitable. What if the bot can give your the optimal parameters to use for your strategy?
|
||||||
optimal parameters to use for your strategy?
|
|
||||||
Your next step is to learn [how to find optimal parameters with Hyperopt](hyperopt.md)
|
Your next step is to learn [how to find optimal parameters with Hyperopt](hyperopt.md)
|
||||||
|
@ -56,6 +56,7 @@ optional arguments:
|
|||||||
usage: freqtrade trade [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
usage: freqtrade trade [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
||||||
[--userdir PATH] [-s NAME] [--strategy-path PATH]
|
[--userdir PATH] [-s NAME] [--strategy-path PATH]
|
||||||
[--db-url PATH] [--sd-notify] [--dry-run]
|
[--db-url PATH] [--sd-notify] [--dry-run]
|
||||||
|
[--dry-run-wallet DRY_RUN_WALLET]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
@ -66,6 +67,9 @@ optional arguments:
|
|||||||
--sd-notify Notify systemd service manager.
|
--sd-notify Notify systemd service manager.
|
||||||
--dry-run Enforce dry-run for trading (removes Exchange secrets
|
--dry-run Enforce dry-run for trading (removes Exchange secrets
|
||||||
and simulates trades).
|
and simulates trades).
|
||||||
|
--dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET
|
||||||
|
Starting balance, used for backtesting / hyperopt and
|
||||||
|
dry-runs.
|
||||||
|
|
||||||
Common arguments:
|
Common arguments:
|
||||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||||
|
@ -49,7 +49,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
| `timeframe` | The timeframe (former ticker interval) to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** String
|
| `timeframe` | The timeframe (former ticker interval) to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** String
|
||||||
| `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency). <br> **Datatype:** String
|
| `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency). <br> **Datatype:** String
|
||||||
| `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
| `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
||||||
| `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in the Dry Run mode.<br>*Defaults to `1000`.* <br> **Datatype:** Float
|
| `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in Dry Run mode.<br>*Defaults to `1000`.* <br> **Datatype:** Float
|
||||||
| `cancel_open_orders_on_exit` | Cancel open orders when the `/stop` RPC command is issued, `Ctrl+C` is pressed or the bot dies unexpectedly. When set to `true`, this allows you to use `/stop` to cancel unfilled and partially filled orders in the event of a market crash. It does not impact open positions. <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
| `cancel_open_orders_on_exit` | Cancel open orders when the `/stop` RPC command is issued, `Ctrl+C` is pressed or the bot dies unexpectedly. When set to `true`, this allows you to use `/stop` to cancel unfilled and partially filled orders in the event of a market crash. It does not impact open positions. <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||||
| `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
| `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||||
| `minimal_roi` | **Required.** Set the threshold as ratio the bot will use to sell a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Dict
|
| `minimal_roi` | **Required.** Set the threshold as ratio the bot will use to sell a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Dict
|
||||||
@ -219,11 +219,12 @@ To allow the bot to trade all the available `stake_currency` in your account (mi
|
|||||||
"tradable_balance_ratio": 0.99,
|
"tradable_balance_ratio": 0.99,
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Note
|
!!! Tip "Compounding profits"
|
||||||
This configuration will allow increasing / decreasing stakes depending on the performance of the bot (lower stake if bot is loosing, higher stakes if the bot has a winning record, since higher balances are available).
|
This configuration will allow increasing / decreasing stakes depending on the performance of the bot (lower stake if bot is loosing, higher stakes if the bot has a winning record, since higher balances are available), and will result in profit compounding.
|
||||||
|
|
||||||
!!! Note "When using Dry-Run Mode"
|
!!! Note "When using Dry-Run Mode"
|
||||||
When using `"stake_amount" : "unlimited",` in combination with Dry-Run, the balance will be simulated starting with a stake of `dry_run_wallet` which will evolve over time. It is therefore important to set `dry_run_wallet` to a sensible value (like 0.05 or 0.01 for BTC and 1000 or 100 for USDT, for example), otherwise it may simulate trades with 100 BTC (or more) or 0.05 USDT (or less) at once - which may not correspond to your real available balance or is less than the exchange minimal limit for the order amount for the stake currency.
|
When using `"stake_amount" : "unlimited",` in combination with Dry-Run, Backtesting or Hyperopt, the balance will be simulated starting with a stake of `dry_run_wallet` which will evolve over time.
|
||||||
|
It is therefore important to set `dry_run_wallet` to a sensible value (like 0.05 or 0.01 for BTC and 1000 or 100 for USDT, for example), otherwise it may simulate trades with 100 BTC (or more) or 0.05 USDT (or less) at once - which may not correspond to your real available balance or is less than the exchange minimal limit for the order amount for the stake currency.
|
||||||
|
|
||||||
--8<-- "includes/pricing.md"
|
--8<-- "includes/pricing.md"
|
||||||
|
|
||||||
|
@ -43,7 +43,8 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
|||||||
[--max-open-trades INT]
|
[--max-open-trades INT]
|
||||||
[--stake-amount STAKE_AMOUNT] [--fee FLOAT]
|
[--stake-amount STAKE_AMOUNT] [--fee FLOAT]
|
||||||
[--hyperopt NAME] [--hyperopt-path PATH] [--eps]
|
[--hyperopt NAME] [--hyperopt-path PATH] [--eps]
|
||||||
[--dmmp] [--enable-protections] [-e INT]
|
[--dmmp] [--enable-protections]
|
||||||
|
[--dry-run-wallet DRY_RUN_WALLET] [-e INT]
|
||||||
[--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]]
|
[--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]]
|
||||||
[--print-all] [--no-color] [--print-json] [-j JOBS]
|
[--print-all] [--no-color] [--print-json] [-j JOBS]
|
||||||
[--random-state INT] [--min-trades INT]
|
[--random-state INT] [--min-trades INT]
|
||||||
@ -82,6 +83,9 @@ optional arguments:
|
|||||||
Enable protections for backtesting.Will slow
|
Enable protections for backtesting.Will slow
|
||||||
backtesting down by a considerable amount, but will
|
backtesting down by a considerable amount, but will
|
||||||
include configured protections
|
include configured protections
|
||||||
|
--dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET
|
||||||
|
Starting balance, used for backtesting / hyperopt and
|
||||||
|
dry-runs.
|
||||||
-e INT, --epochs INT Specify number of epochs (default: 100).
|
-e INT, --epochs INT Specify number of epochs (default: 100).
|
||||||
--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]
|
--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]
|
||||||
Specify which parameters to hyperopt. Space-separated
|
Specify which parameters to hyperopt. Space-separated
|
||||||
|
@ -678,7 +678,7 @@ To verify if a pair is currently locked, use `self.is_pair_locked(pair)`.
|
|||||||
Locked pairs will always be rounded up to the next candle. So assuming a `5m` timeframe, a lock with `until` set to 10:18 will lock the pair until the candle from 10:15-10:20 will be finished.
|
Locked pairs will always be rounded up to the next candle. So assuming a `5m` timeframe, a lock with `until` set to 10:18 will lock the pair until the candle from 10:15-10:20 will be finished.
|
||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
Locking pairs is not available during backtesting.
|
Manually locking pairs is not available during backtesting, only locks via Protections are allowed.
|
||||||
|
|
||||||
#### Pair locking example
|
#### Pair locking example
|
||||||
|
|
||||||
|
@ -14,18 +14,18 @@ ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_dat
|
|||||||
|
|
||||||
ARGS_STRATEGY = ["strategy", "strategy_path"]
|
ARGS_STRATEGY = ["strategy", "strategy_path"]
|
||||||
|
|
||||||
ARGS_TRADE = ["db_url", "sd_notify", "dry_run"]
|
ARGS_TRADE = ["db_url", "sd_notify", "dry_run", "dry_run_wallet", ]
|
||||||
|
|
||||||
ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv",
|
ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv",
|
||||||
"max_open_trades", "stake_amount", "fee"]
|
"max_open_trades", "stake_amount", "fee"]
|
||||||
|
|
||||||
ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions",
|
ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions",
|
||||||
"enable_protections",
|
"enable_protections", "dry_run_wallet",
|
||||||
"strategy_list", "export", "exportfilename"]
|
"strategy_list", "export", "exportfilename"]
|
||||||
|
|
||||||
ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
|
ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
|
||||||
"position_stacking", "use_max_market_positions",
|
"position_stacking", "use_max_market_positions",
|
||||||
"enable_protections",
|
"enable_protections", "dry_run_wallet",
|
||||||
"epochs", "spaces", "print_all",
|
"epochs", "spaces", "print_all",
|
||||||
"print_colorized", "print_json", "hyperopt_jobs",
|
"print_colorized", "print_json", "hyperopt_jobs",
|
||||||
"hyperopt_random_state", "hyperopt_min_trades",
|
"hyperopt_random_state", "hyperopt_min_trades",
|
||||||
|
@ -110,6 +110,11 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
help='Enforce dry-run for trading (removes Exchange secrets and simulates trades).',
|
help='Enforce dry-run for trading (removes Exchange secrets and simulates trades).',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
),
|
),
|
||||||
|
"dry_run_wallet": Arg(
|
||||||
|
'--dry-run-wallet', '--starting-balance',
|
||||||
|
help='Starting balance, used for backtesting / hyperopt and dry-runs.',
|
||||||
|
type=float,
|
||||||
|
),
|
||||||
# Optimize common
|
# Optimize common
|
||||||
"timeframe": Arg(
|
"timeframe": Arg(
|
||||||
'-i', '--timeframe', '--ticker-interval',
|
'-i', '--timeframe', '--ticker-interval',
|
||||||
@ -128,7 +133,6 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
"stake_amount": Arg(
|
"stake_amount": Arg(
|
||||||
'--stake-amount',
|
'--stake-amount',
|
||||||
help='Override the value of the `stake_amount` configuration setting.',
|
help='Override the value of the `stake_amount` configuration setting.',
|
||||||
type=float,
|
|
||||||
),
|
),
|
||||||
# Backtesting
|
# Backtesting
|
||||||
"position_stacking": Arg(
|
"position_stacking": Arg(
|
||||||
|
@ -3,7 +3,8 @@ from typing import Any, Dict
|
|||||||
|
|
||||||
from freqtrade import constants
|
from freqtrade import constants
|
||||||
from freqtrade.configuration import setup_utils_configuration
|
from freqtrade.configuration import setup_utils_configuration
|
||||||
from freqtrade.exceptions import DependencyException, OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
|
from freqtrade.misc import round_coin_value
|
||||||
from freqtrade.state import RunMode
|
from freqtrade.state import RunMode
|
||||||
|
|
||||||
|
|
||||||
@ -22,11 +23,13 @@ def setup_optimize_configuration(args: Dict[str, Any], method: RunMode) -> Dict[
|
|||||||
RunMode.BACKTEST: 'backtesting',
|
RunMode.BACKTEST: 'backtesting',
|
||||||
RunMode.HYPEROPT: 'hyperoptimization',
|
RunMode.HYPEROPT: 'hyperoptimization',
|
||||||
}
|
}
|
||||||
if (method in no_unlimited_runmodes.keys() and
|
if method in no_unlimited_runmodes.keys():
|
||||||
config['stake_amount'] == constants.UNLIMITED_STAKE_AMOUNT):
|
if (config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT
|
||||||
raise DependencyException(
|
and config['stake_amount'] > config['dry_run_wallet']):
|
||||||
f'The value of `stake_amount` cannot be set as "{constants.UNLIMITED_STAKE_AMOUNT}" '
|
wallet = round_coin_value(config['dry_run_wallet'], config['stake_currency'])
|
||||||
f'for {no_unlimited_runmodes[method]}')
|
stake = round_coin_value(config['stake_amount'], config['stake_currency'])
|
||||||
|
raise OperationalException(f"Starting balance ({wallet}) "
|
||||||
|
f"is smaller than stake_amount {stake}.")
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
@ -214,9 +214,6 @@ class Configuration:
|
|||||||
self._args_to_config(
|
self._args_to_config(
|
||||||
config, argname='enable_protections',
|
config, argname='enable_protections',
|
||||||
logstring='Parameter --enable-protections detected, enabling Protections. ...')
|
logstring='Parameter --enable-protections detected, enabling Protections. ...')
|
||||||
# Setting max_open_trades to infinite if -1
|
|
||||||
if config.get('max_open_trades') == -1:
|
|
||||||
config['max_open_trades'] = float('inf')
|
|
||||||
|
|
||||||
if 'use_max_market_positions' in self.args and not self.args["use_max_market_positions"]:
|
if 'use_max_market_positions' in self.args and not self.args["use_max_market_positions"]:
|
||||||
config.update({'use_max_market_positions': False})
|
config.update({'use_max_market_positions': False})
|
||||||
@ -228,11 +225,23 @@ class Configuration:
|
|||||||
'overriding max_open_trades to: %s ...', config.get('max_open_trades'))
|
'overriding max_open_trades to: %s ...', config.get('max_open_trades'))
|
||||||
elif config['runmode'] in NON_UTIL_MODES:
|
elif config['runmode'] in NON_UTIL_MODES:
|
||||||
logger.info('Using max_open_trades: %s ...', config.get('max_open_trades'))
|
logger.info('Using max_open_trades: %s ...', config.get('max_open_trades'))
|
||||||
|
# Setting max_open_trades to infinite if -1
|
||||||
|
if config.get('max_open_trades') == -1:
|
||||||
|
config['max_open_trades'] = float('inf')
|
||||||
|
|
||||||
|
if self.args.get('stake_amount', None):
|
||||||
|
# Convert explicitly to float to support CLI argument for both unlimited and value
|
||||||
|
try:
|
||||||
|
self.args['stake_amount'] = float(self.args['stake_amount'])
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
self._args_to_config(config, argname='stake_amount',
|
self._args_to_config(config, argname='stake_amount',
|
||||||
logstring='Parameter --stake-amount detected, '
|
logstring='Parameter --stake-amount detected, '
|
||||||
'overriding stake_amount to: {} ...')
|
'overriding stake_amount to: {} ...')
|
||||||
|
self._args_to_config(config, argname='dry_run_wallet',
|
||||||
|
logstring='Parameter --dry-run-wallet detected, '
|
||||||
|
'overriding dry_run_wallet to: {} ...')
|
||||||
self._args_to_config(config, argname='fee',
|
self._args_to_config(config, argname='fee',
|
||||||
logstring='Parameter --fee detected, '
|
logstring='Parameter --fee detected, '
|
||||||
'setting fee to: {} ...')
|
'setting fee to: {} ...')
|
||||||
|
@ -10,7 +10,7 @@ import pandas as pd
|
|||||||
|
|
||||||
from freqtrade.constants import LAST_BT_RESULT_FN
|
from freqtrade.constants import LAST_BT_RESULT_FN
|
||||||
from freqtrade.misc import json_load
|
from freqtrade.misc import json_load
|
||||||
from freqtrade.persistence import Trade, init_db
|
from freqtrade.persistence import LocalTrade, Trade, init_db
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -224,7 +224,7 @@ def evaluate_result_multi(results: pd.DataFrame, timeframe: str,
|
|||||||
return df_final[df_final['open_trades'] > max_open_trades]
|
return df_final[df_final['open_trades'] > max_open_trades]
|
||||||
|
|
||||||
|
|
||||||
def trade_list_to_dataframe(trades: List[Trade]) -> pd.DataFrame:
|
def trade_list_to_dataframe(trades: List[LocalTrade]) -> pd.DataFrame:
|
||||||
"""
|
"""
|
||||||
Convert list of Trade objects to pandas Dataframe
|
Convert list of Trade objects to pandas Dataframe
|
||||||
:param trades: List of trade objects
|
:param trades: List of trade objects
|
||||||
@ -360,13 +360,14 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str,
|
|||||||
|
|
||||||
def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date',
|
def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date',
|
||||||
value_col: str = 'profit_ratio'
|
value_col: str = 'profit_ratio'
|
||||||
) -> Tuple[float, pd.Timestamp, pd.Timestamp]:
|
) -> Tuple[float, pd.Timestamp, pd.Timestamp, float, float]:
|
||||||
"""
|
"""
|
||||||
Calculate max drawdown and the corresponding close dates
|
Calculate max drawdown and the corresponding close dates
|
||||||
:param trades: DataFrame containing trades (requires columns close_date and profit_ratio)
|
:param trades: DataFrame containing trades (requires columns close_date and profit_ratio)
|
||||||
:param date_col: Column in DataFrame to use for dates (defaults to 'close_date')
|
:param date_col: Column in DataFrame to use for dates (defaults to 'close_date')
|
||||||
:param value_col: Column in DataFrame to use for values (defaults to 'profit_ratio')
|
:param value_col: Column in DataFrame to use for values (defaults to 'profit_ratio')
|
||||||
:return: Tuple (float, highdate, lowdate) with absolute max drawdown, high and low time
|
:return: Tuple (float, highdate, lowdate, highvalue, lowvalue) with absolute max drawdown,
|
||||||
|
high and low time and high and low value.
|
||||||
:raise: ValueError if trade-dataframe was found empty.
|
:raise: ValueError if trade-dataframe was found empty.
|
||||||
"""
|
"""
|
||||||
if len(trades) == 0:
|
if len(trades) == 0:
|
||||||
@ -382,13 +383,17 @@ def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date'
|
|||||||
raise ValueError("No losing trade, therefore no drawdown.")
|
raise ValueError("No losing trade, therefore no drawdown.")
|
||||||
high_date = profit_results.loc[max_drawdown_df.iloc[:idxmin]['high_value'].idxmax(), date_col]
|
high_date = profit_results.loc[max_drawdown_df.iloc[:idxmin]['high_value'].idxmax(), date_col]
|
||||||
low_date = profit_results.loc[idxmin, date_col]
|
low_date = profit_results.loc[idxmin, date_col]
|
||||||
return abs(min(max_drawdown_df['drawdown'])), high_date, low_date
|
high_val = max_drawdown_df.loc[max_drawdown_df.iloc[:idxmin]
|
||||||
|
['high_value'].idxmax(), 'cumulative']
|
||||||
|
low_val = max_drawdown_df.loc[idxmin, 'cumulative']
|
||||||
|
return abs(min(max_drawdown_df['drawdown'])), high_date, low_date, high_val, low_val
|
||||||
|
|
||||||
|
|
||||||
def calculate_csum(trades: pd.DataFrame) -> Tuple[float, float]:
|
def calculate_csum(trades: pd.DataFrame, starting_balance: float = 0) -> Tuple[float, float]:
|
||||||
"""
|
"""
|
||||||
Calculate min/max cumsum of trades, to show if the wallet/stake amount ratio is sane
|
Calculate min/max cumsum of trades, to show if the wallet/stake amount ratio is sane
|
||||||
:param trades: DataFrame containing trades (requires columns close_date and profit_percent)
|
:param trades: DataFrame containing trades (requires columns close_date and profit_percent)
|
||||||
|
:param starting_balance: Add starting balance to results, to show the wallets high / low points
|
||||||
:return: Tuple (float, float) with cumsum of profit_abs
|
:return: Tuple (float, float) with cumsum of profit_abs
|
||||||
:raise: ValueError if trade-dataframe was found empty.
|
:raise: ValueError if trade-dataframe was found empty.
|
||||||
"""
|
"""
|
||||||
@ -397,7 +402,7 @@ def calculate_csum(trades: pd.DataFrame) -> Tuple[float, float]:
|
|||||||
|
|
||||||
csum_df = pd.DataFrame()
|
csum_df = pd.DataFrame()
|
||||||
csum_df['sum'] = trades['profit_abs'].cumsum()
|
csum_df['sum'] = trades['profit_abs'].cumsum()
|
||||||
csum_min = csum_df['sum'].min()
|
csum_min = csum_df['sum'].min() + starting_balance
|
||||||
csum_max = csum_df['sum'].max()
|
csum_max = csum_df['sum'].max() + starting_balance
|
||||||
|
|
||||||
return csum_min, csum_max
|
return csum_min, csum_max
|
||||||
|
@ -147,6 +147,9 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
Destructor - clean up async stuff
|
Destructor - clean up async stuff
|
||||||
"""
|
"""
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
logger.debug("Exchange object destroyed, closing async loop")
|
logger.debug("Exchange object destroyed, closing async loop")
|
||||||
if self._api_async and inspect.iscoroutinefunction(self._api_async.close):
|
if self._api_async and inspect.iscoroutinefunction(self._api_async.close):
|
||||||
asyncio.get_event_loop().run_until_complete(self._api_async.close())
|
asyncio.get_event_loop().run_until_complete(self._api_async.close())
|
||||||
|
@ -937,7 +937,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
Check and execute sell
|
Check and execute sell
|
||||||
"""
|
"""
|
||||||
should_sell = self.strategy.should_sell(
|
should_sell = self.strategy.should_sell(
|
||||||
trade, sell_rate, datetime.utcnow(), buy, sell,
|
trade, sell_rate, datetime.now(timezone.utc), buy, sell,
|
||||||
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
|
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -17,17 +17,18 @@ from freqtrade.data import history
|
|||||||
from freqtrade.data.btanalysis import trade_list_to_dataframe
|
from freqtrade.data.btanalysis import trade_list_to_dataframe
|
||||||
from freqtrade.data.converter import trim_dataframe
|
from freqtrade.data.converter import trim_dataframe
|
||||||
from freqtrade.data.dataprovider import DataProvider
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import DependencyException, OperationalException
|
||||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
|
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
|
||||||
from freqtrade.mixins import LoggingMixin
|
from freqtrade.mixins import LoggingMixin
|
||||||
from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results,
|
from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results,
|
||||||
store_backtest_stats)
|
store_backtest_stats)
|
||||||
from freqtrade.persistence import PairLocks, Trade
|
from freqtrade.persistence import LocalTrade, PairLocks, Trade
|
||||||
from freqtrade.plugins.pairlistmanager import PairListManager
|
from freqtrade.plugins.pairlistmanager import PairListManager
|
||||||
from freqtrade.plugins.protectionmanager import ProtectionManager
|
from freqtrade.plugins.protectionmanager import ProtectionManager
|
||||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||||
from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType
|
from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType
|
||||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||||
|
from freqtrade.wallets import Wallets
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -114,6 +115,8 @@ class Backtesting:
|
|||||||
if self.config.get('enable_protections', False):
|
if self.config.get('enable_protections', False):
|
||||||
self.protections = ProtectionManager(self.config)
|
self.protections = ProtectionManager(self.config)
|
||||||
|
|
||||||
|
self.wallets = Wallets(self.config, self.exchange, log=False)
|
||||||
|
|
||||||
# Get maximum required startup period
|
# Get maximum required startup period
|
||||||
self.required_startup = max([strat.startup_candle_count for strat in self.strategylist])
|
self.required_startup = max([strat.startup_candle_count for strat in self.strategylist])
|
||||||
# Load one (first) strategy
|
# Load one (first) strategy
|
||||||
@ -124,7 +127,7 @@ class Backtesting:
|
|||||||
PairLocks.use_db = True
|
PairLocks.use_db = True
|
||||||
Trade.use_db = True
|
Trade.use_db = True
|
||||||
|
|
||||||
def _set_strategy(self, strategy):
|
def _set_strategy(self, strategy: IStrategy):
|
||||||
"""
|
"""
|
||||||
Load strategy into backtesting
|
Load strategy into backtesting
|
||||||
"""
|
"""
|
||||||
@ -171,8 +174,6 @@ class Backtesting:
|
|||||||
PairLocks.use_db = False
|
PairLocks.use_db = False
|
||||||
PairLocks.timeframe = self.config['timeframe']
|
PairLocks.timeframe = self.config['timeframe']
|
||||||
Trade.use_db = False
|
Trade.use_db = False
|
||||||
if enable_protections:
|
|
||||||
# Reset persisted data - used for protections only
|
|
||||||
PairLocks.reset_locks()
|
PairLocks.reset_locks()
|
||||||
Trade.reset_trades()
|
Trade.reset_trades()
|
||||||
|
|
||||||
@ -203,10 +204,10 @@ class Backtesting:
|
|||||||
|
|
||||||
# Convert from Pandas to list for performance reasons
|
# Convert from Pandas to list for performance reasons
|
||||||
# (Looping Pandas is slow.)
|
# (Looping Pandas is slow.)
|
||||||
data[pair] = [x for x in df_analyzed.itertuples(index=False, name=None)]
|
data[pair] = df_analyzed.values.tolist()
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def _get_close_rate(self, sell_row: Tuple, trade: Trade, sell: SellCheckTuple,
|
def _get_close_rate(self, sell_row: Tuple, trade: LocalTrade, sell: SellCheckTuple,
|
||||||
trade_dur: int) -> float:
|
trade_dur: int) -> float:
|
||||||
"""
|
"""
|
||||||
Get close rate for backtesting result
|
Get close rate for backtesting result
|
||||||
@ -246,24 +247,48 @@ class Backtesting:
|
|||||||
else:
|
else:
|
||||||
return sell_row[OPEN_IDX]
|
return sell_row[OPEN_IDX]
|
||||||
|
|
||||||
def _get_sell_trade_entry(self, trade: Trade, sell_row: Tuple) -> Optional[Trade]:
|
def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]:
|
||||||
|
|
||||||
sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], sell_row[DATE_IDX],
|
sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore
|
||||||
sell_row[BUY_IDX], sell_row[SELL_IDX],
|
sell_row[DATE_IDX], sell_row[BUY_IDX], sell_row[SELL_IDX],
|
||||||
low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX])
|
low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX])
|
||||||
if sell.sell_flag:
|
if sell.sell_flag:
|
||||||
trade_dur = int((sell_row[DATE_IDX] - trade.open_date).total_seconds() // 60)
|
|
||||||
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
|
|
||||||
|
|
||||||
trade.close_date = sell_row[DATE_IDX]
|
trade.close_date = sell_row[DATE_IDX]
|
||||||
trade.sell_reason = sell.sell_type
|
trade.sell_reason = sell.sell_type.value
|
||||||
|
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
|
||||||
|
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
|
||||||
trade.close(closerate, show_msg=False)
|
trade.close(closerate, show_msg=False)
|
||||||
return trade
|
return trade
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def handle_left_open(self, open_trades: Dict[str, List[Trade]],
|
def _enter_trade(self, pair: str, row: List, max_open_trades: int,
|
||||||
data: Dict[str, List[Tuple]]) -> List[Trade]:
|
open_trade_count: int) -> Optional[LocalTrade]:
|
||||||
|
try:
|
||||||
|
stake_amount = self.wallets.get_trade_stake_amount(
|
||||||
|
pair, max_open_trades - open_trade_count, None)
|
||||||
|
except DependencyException:
|
||||||
|
return None
|
||||||
|
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, row[OPEN_IDX], -0.05)
|
||||||
|
if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount):
|
||||||
|
# Enter trade
|
||||||
|
trade = LocalTrade(
|
||||||
|
pair=pair,
|
||||||
|
open_rate=row[OPEN_IDX],
|
||||||
|
open_date=row[DATE_IDX],
|
||||||
|
stake_amount=stake_amount,
|
||||||
|
amount=round(stake_amount / row[OPEN_IDX], 8),
|
||||||
|
fee_open=self.fee,
|
||||||
|
fee_close=self.fee,
|
||||||
|
is_open=True,
|
||||||
|
exchange='backtesting',
|
||||||
|
)
|
||||||
|
return trade
|
||||||
|
return None
|
||||||
|
|
||||||
|
def handle_left_open(self, open_trades: Dict[str, List[LocalTrade]],
|
||||||
|
data: Dict[str, List[Tuple]]) -> List[LocalTrade]:
|
||||||
"""
|
"""
|
||||||
Handling of left open trades at the end of backtesting
|
Handling of left open trades at the end of backtesting
|
||||||
"""
|
"""
|
||||||
@ -274,13 +299,15 @@ class Backtesting:
|
|||||||
sell_row = data[pair][-1]
|
sell_row = data[pair][-1]
|
||||||
|
|
||||||
trade.close_date = sell_row[DATE_IDX]
|
trade.close_date = sell_row[DATE_IDX]
|
||||||
trade.sell_reason = SellType.FORCE_SELL
|
trade.sell_reason = SellType.FORCE_SELL.value
|
||||||
trade.close(sell_row[OPEN_IDX], show_msg=False)
|
trade.close(sell_row[OPEN_IDX], show_msg=False)
|
||||||
trade.is_open = True
|
# Deepcopy object to have wallets update correctly
|
||||||
trades.append(trade)
|
trade1 = deepcopy(trade)
|
||||||
|
trade1.is_open = True
|
||||||
|
trades.append(trade1)
|
||||||
return trades
|
return trades
|
||||||
|
|
||||||
def backtest(self, processed: Dict, stake_amount: float,
|
def backtest(self, processed: Dict,
|
||||||
start_date: datetime, end_date: datetime,
|
start_date: datetime, end_date: datetime,
|
||||||
max_open_trades: int = 0, position_stacking: bool = False,
|
max_open_trades: int = 0, position_stacking: bool = False,
|
||||||
enable_protections: bool = False) -> DataFrame:
|
enable_protections: bool = False) -> DataFrame:
|
||||||
@ -292,7 +319,6 @@ class Backtesting:
|
|||||||
Avoid extensive logging in this method and functions it calls.
|
Avoid extensive logging in this method and functions it calls.
|
||||||
|
|
||||||
:param processed: a processed dictionary with format {pair, data}
|
:param processed: a processed dictionary with format {pair, data}
|
||||||
:param stake_amount: amount to use for each trade
|
|
||||||
:param start_date: backtesting timerange start datetime
|
:param start_date: backtesting timerange start datetime
|
||||||
:param end_date: backtesting timerange end datetime
|
:param end_date: backtesting timerange end datetime
|
||||||
:param max_open_trades: maximum number of concurrent trades, <= 0 means unlimited
|
:param max_open_trades: maximum number of concurrent trades, <= 0 means unlimited
|
||||||
@ -300,11 +326,7 @@ class Backtesting:
|
|||||||
:param enable_protections: Should protections be enabled?
|
:param enable_protections: Should protections be enabled?
|
||||||
:return: DataFrame with trades (results of backtesting)
|
:return: DataFrame with trades (results of backtesting)
|
||||||
"""
|
"""
|
||||||
logger.debug(f"Run backtest, stake_amount: {stake_amount}, "
|
trades: List[LocalTrade] = []
|
||||||
f"start_date: {start_date}, end_date: {end_date}, "
|
|
||||||
f"max_open_trades: {max_open_trades}, position_stacking: {position_stacking}"
|
|
||||||
)
|
|
||||||
trades: List[Trade] = []
|
|
||||||
self.prepare_backtest(enable_protections)
|
self.prepare_backtest(enable_protections)
|
||||||
|
|
||||||
# Use dict of lists with data for performance
|
# Use dict of lists with data for performance
|
||||||
@ -315,7 +337,7 @@ class Backtesting:
|
|||||||
indexes: Dict = {}
|
indexes: Dict = {}
|
||||||
tmp = start_date + timedelta(minutes=self.timeframe_min)
|
tmp = start_date + timedelta(minutes=self.timeframe_min)
|
||||||
|
|
||||||
open_trades: Dict[str, List] = defaultdict(list)
|
open_trades: Dict[str, List[LocalTrade]] = defaultdict(list)
|
||||||
open_trade_count = 0
|
open_trade_count = 0
|
||||||
|
|
||||||
# Loop timerange and get candle for each pair at that point in time
|
# Loop timerange and get candle for each pair at that point in time
|
||||||
@ -346,28 +368,18 @@ class Backtesting:
|
|||||||
and tmp != end_date
|
and tmp != end_date
|
||||||
and row[BUY_IDX] == 1 and row[SELL_IDX] != 1
|
and row[BUY_IDX] == 1 and row[SELL_IDX] != 1
|
||||||
and not PairLocks.is_pair_locked(pair, row[DATE_IDX])):
|
and not PairLocks.is_pair_locked(pair, row[DATE_IDX])):
|
||||||
# Enter trade
|
trade = self._enter_trade(pair, row, max_open_trades, open_trade_count_start)
|
||||||
trade = Trade(
|
if trade:
|
||||||
pair=pair,
|
|
||||||
open_rate=row[OPEN_IDX],
|
|
||||||
open_date=row[DATE_IDX],
|
|
||||||
stake_amount=stake_amount,
|
|
||||||
amount=round(stake_amount / row[OPEN_IDX], 8),
|
|
||||||
fee_open=self.fee,
|
|
||||||
fee_close=self.fee,
|
|
||||||
is_open=True,
|
|
||||||
)
|
|
||||||
# TODO: hacky workaround to avoid opening > max_open_trades
|
# TODO: hacky workaround to avoid opening > max_open_trades
|
||||||
# This emulates previous behaviour - not sure if this is correct
|
# This emulates previous behaviour - not sure if this is correct
|
||||||
# Prevents buying if the trade-slot was freed in this candle
|
# Prevents buying if the trade-slot was freed in this candle
|
||||||
open_trade_count_start += 1
|
open_trade_count_start += 1
|
||||||
open_trade_count += 1
|
open_trade_count += 1
|
||||||
# logger.debug(f"{pair} - Backtesting emulates creation of new trade: {trade}.")
|
# logger.debug(f"{pair} - Emulate creation of new trade: {trade}.")
|
||||||
open_trades[pair].append(trade)
|
open_trades[pair].append(trade)
|
||||||
Trade.trades.append(trade)
|
LocalTrade.trades.append(trade)
|
||||||
|
|
||||||
for trade in open_trades[pair]:
|
for trade in open_trades[pair]:
|
||||||
# since indexes has been incremented before, we need to go one step back to
|
|
||||||
# also check the buying candle for sell conditions.
|
# also check the buying candle for sell conditions.
|
||||||
trade_entry = self._get_sell_trade_entry(trade, row)
|
trade_entry = self._get_sell_trade_entry(trade, row)
|
||||||
# Sell occured
|
# Sell occured
|
||||||
@ -384,6 +396,7 @@ class Backtesting:
|
|||||||
tmp += timedelta(minutes=self.timeframe_min)
|
tmp += timedelta(minutes=self.timeframe_min)
|
||||||
|
|
||||||
trades += self.handle_left_open(open_trades, data=data)
|
trades += self.handle_left_open(open_trades, data=data)
|
||||||
|
self.wallets.update()
|
||||||
|
|
||||||
return trade_list_to_dataframe(trades)
|
return trade_list_to_dataframe(trades)
|
||||||
|
|
||||||
@ -417,7 +430,6 @@ class Backtesting:
|
|||||||
# Execute backtest and store results
|
# Execute backtest and store results
|
||||||
results = self.backtest(
|
results = self.backtest(
|
||||||
processed=preprocessed,
|
processed=preprocessed,
|
||||||
stake_amount=self.config['stake_amount'],
|
|
||||||
start_date=min_date.datetime,
|
start_date=min_date.datetime,
|
||||||
end_date=max_date.datetime,
|
end_date=max_date.datetime,
|
||||||
max_open_trades=max_open_trades,
|
max_open_trades=max_open_trades,
|
||||||
@ -428,7 +440,8 @@ class Backtesting:
|
|||||||
self.all_results[self.strategy.get_strategy_name()] = {
|
self.all_results[self.strategy.get_strategy_name()] = {
|
||||||
'results': results,
|
'results': results,
|
||||||
'config': self.strategy.config,
|
'config': self.strategy.config,
|
||||||
'locks': PairLocks.locks,
|
'locks': PairLocks.get_all_locks(),
|
||||||
|
'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']),
|
||||||
'backtest_start_time': int(backtest_start_time.timestamp()),
|
'backtest_start_time': int(backtest_start_time.timestamp()),
|
||||||
'backtest_end_time': int(backtest_end_time.timestamp()),
|
'backtest_end_time': int(backtest_end_time.timestamp()),
|
||||||
}
|
}
|
||||||
|
@ -541,7 +541,6 @@ class Hyperopt:
|
|||||||
|
|
||||||
backtesting_results = self.backtesting.backtest(
|
backtesting_results = self.backtesting.backtest(
|
||||||
processed=processed,
|
processed=processed,
|
||||||
stake_amount=self.config['stake_amount'],
|
|
||||||
start_date=min_date.datetime,
|
start_date=min_date.datetime,
|
||||||
end_date=max_date.datetime,
|
end_date=max_date.datetime,
|
||||||
max_open_trades=self.max_open_trades,
|
max_open_trades=self.max_open_trades,
|
||||||
@ -665,7 +664,10 @@ class Hyperopt:
|
|||||||
dump(preprocessed, self.data_pickle_file)
|
dump(preprocessed, self.data_pickle_file)
|
||||||
|
|
||||||
# We don't need exchange instance anymore while running hyperopt
|
# We don't need exchange instance anymore while running hyperopt
|
||||||
self.backtesting.exchange = None # type: ignore
|
self.backtesting.exchange.close()
|
||||||
|
self.backtesting.exchange._api = None # type: ignore
|
||||||
|
self.backtesting.exchange._api_async = None # type: ignore
|
||||||
|
# self.backtesting.exchange = None # type: ignore
|
||||||
self.backtesting.pairlists = None # type: ignore
|
self.backtesting.pairlists = None # type: ignore
|
||||||
self.backtesting.strategy.dp = None # type: ignore
|
self.backtesting.strategy.dp = None # type: ignore
|
||||||
IStrategy.dp = None # type: ignore
|
IStrategy.dp = None # type: ignore
|
||||||
|
@ -8,7 +8,7 @@ from numpy import int64
|
|||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
|
|
||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN
|
from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT
|
||||||
from freqtrade.data.btanalysis import (calculate_csum, calculate_market_change,
|
from freqtrade.data.btanalysis import (calculate_csum, calculate_market_change,
|
||||||
calculate_max_drawdown)
|
calculate_max_drawdown)
|
||||||
from freqtrade.misc import decimals_per_coin, file_dump_json, round_coin_value
|
from freqtrade.misc import decimals_per_coin, file_dump_json, round_coin_value
|
||||||
@ -56,12 +56,13 @@ def _get_line_header(first_column: str, stake_currency: str) -> List[str]:
|
|||||||
'Wins', 'Draws', 'Losses']
|
'Wins', 'Draws', 'Losses']
|
||||||
|
|
||||||
|
|
||||||
def _generate_result_line(result: DataFrame, max_open_trades: int, first_column: str) -> Dict:
|
def _generate_result_line(result: DataFrame, starting_balance: int, first_column: str) -> Dict:
|
||||||
"""
|
"""
|
||||||
Generate one result dict, with "first_column" as key.
|
Generate one result dict, with "first_column" as key.
|
||||||
"""
|
"""
|
||||||
profit_sum = result['profit_ratio'].sum()
|
profit_sum = result['profit_ratio'].sum()
|
||||||
profit_total = profit_sum / max_open_trades
|
# (end-capital - starting capital) / starting capital
|
||||||
|
profit_total = result['profit_abs'].sum() / starting_balance
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'key': first_column,
|
'key': first_column,
|
||||||
@ -88,13 +89,13 @@ def _generate_result_line(result: DataFrame, max_open_trades: int, first_column:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, max_open_trades: int,
|
def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, starting_balance: int,
|
||||||
results: DataFrame, skip_nan: bool = False) -> List[Dict]:
|
results: DataFrame, skip_nan: bool = False) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
Generates and returns a list for the given backtest data and the results dataframe
|
Generates and returns a list for the given backtest data and the results dataframe
|
||||||
:param data: Dict of <pair: dataframe> containing data that was used during backtesting.
|
:param data: Dict of <pair: dataframe> containing data that was used during backtesting.
|
||||||
:param stake_currency: stake-currency - used to correctly name headers
|
:param stake_currency: stake-currency - used to correctly name headers
|
||||||
:param max_open_trades: Maximum allowed open trades
|
:param starting_balance: Starting balance
|
||||||
:param results: Dataframe containing the backtest results
|
:param results: Dataframe containing the backtest results
|
||||||
:param skip_nan: Print "left open" open trades
|
:param skip_nan: Print "left open" open trades
|
||||||
:return: List of Dicts containing the metrics per pair
|
:return: List of Dicts containing the metrics per pair
|
||||||
@ -107,10 +108,10 @@ def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, max_open_t
|
|||||||
if skip_nan and result['profit_abs'].isnull().all():
|
if skip_nan and result['profit_abs'].isnull().all():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
tabular_data.append(_generate_result_line(result, max_open_trades, pair))
|
tabular_data.append(_generate_result_line(result, starting_balance, pair))
|
||||||
|
|
||||||
# Append Total
|
# Append Total
|
||||||
tabular_data.append(_generate_result_line(results, max_open_trades, 'TOTAL'))
|
tabular_data.append(_generate_result_line(results, starting_balance, 'TOTAL'))
|
||||||
return tabular_data
|
return tabular_data
|
||||||
|
|
||||||
|
|
||||||
@ -132,7 +133,7 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List
|
|||||||
|
|
||||||
tabular_data.append(
|
tabular_data.append(
|
||||||
{
|
{
|
||||||
'sell_reason': reason.value,
|
'sell_reason': reason,
|
||||||
'trades': count,
|
'trades': count,
|
||||||
'wins': len(result[result['profit_abs'] > 0]),
|
'wins': len(result[result['profit_abs'] > 0]),
|
||||||
'draws': len(result[result['profit_abs'] == 0]),
|
'draws': len(result[result['profit_abs'] == 0]),
|
||||||
@ -159,7 +160,7 @@ def generate_strategy_metrics(all_results: Dict) -> List[Dict]:
|
|||||||
tabular_data = []
|
tabular_data = []
|
||||||
for strategy, results in all_results.items():
|
for strategy, results in all_results.items():
|
||||||
tabular_data.append(_generate_result_line(
|
tabular_data.append(_generate_result_line(
|
||||||
results['results'], results['config']['max_open_trades'], strategy)
|
results['results'], results['config']['dry_run_wallet'], strategy)
|
||||||
)
|
)
|
||||||
return tabular_data
|
return tabular_data
|
||||||
|
|
||||||
@ -195,13 +196,18 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]:
|
|||||||
return {
|
return {
|
||||||
'backtest_best_day': 0,
|
'backtest_best_day': 0,
|
||||||
'backtest_worst_day': 0,
|
'backtest_worst_day': 0,
|
||||||
|
'backtest_best_day_abs': 0,
|
||||||
|
'backtest_worst_day_abs': 0,
|
||||||
'winning_days': 0,
|
'winning_days': 0,
|
||||||
'draw_days': 0,
|
'draw_days': 0,
|
||||||
'losing_days': 0,
|
'losing_days': 0,
|
||||||
'winner_holding_avg': timedelta(),
|
'winner_holding_avg': timedelta(),
|
||||||
'loser_holding_avg': timedelta(),
|
'loser_holding_avg': timedelta(),
|
||||||
}
|
}
|
||||||
daily_profit = results.resample('1d', on='close_date')['profit_ratio'].sum()
|
daily_profit_rel = results.resample('1d', on='close_date')['profit_ratio'].sum()
|
||||||
|
daily_profit = results.resample('1d', on='close_date')['profit_abs'].sum().round(10)
|
||||||
|
worst_rel = min(daily_profit_rel)
|
||||||
|
best_rel = max(daily_profit_rel)
|
||||||
worst = min(daily_profit)
|
worst = min(daily_profit)
|
||||||
best = max(daily_profit)
|
best = max(daily_profit)
|
||||||
winning_days = sum(daily_profit > 0)
|
winning_days = sum(daily_profit > 0)
|
||||||
@ -212,8 +218,10 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]:
|
|||||||
losing_trades = results.loc[results['profit_ratio'] < 0]
|
losing_trades = results.loc[results['profit_ratio'] < 0]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'backtest_best_day': best,
|
'backtest_best_day': best_rel,
|
||||||
'backtest_worst_day': worst,
|
'backtest_worst_day': worst_rel,
|
||||||
|
'backtest_best_day_abs': best,
|
||||||
|
'backtest_worst_day_abs': worst,
|
||||||
'winning_days': winning_days,
|
'winning_days': winning_days,
|
||||||
'draw_days': draw_days,
|
'draw_days': draw_days,
|
||||||
'losing_days': losing_days,
|
'losing_days': losing_days,
|
||||||
@ -246,15 +254,16 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
|
|||||||
continue
|
continue
|
||||||
config = content['config']
|
config = content['config']
|
||||||
max_open_trades = min(config['max_open_trades'], len(btdata.keys()))
|
max_open_trades = min(config['max_open_trades'], len(btdata.keys()))
|
||||||
|
starting_balance = config['dry_run_wallet']
|
||||||
stake_currency = config['stake_currency']
|
stake_currency = config['stake_currency']
|
||||||
|
|
||||||
pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
|
pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
|
||||||
max_open_trades=max_open_trades,
|
starting_balance=starting_balance,
|
||||||
results=results, skip_nan=False)
|
results=results, skip_nan=False)
|
||||||
sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades,
|
sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades,
|
||||||
results=results)
|
results=results)
|
||||||
left_open_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
|
left_open_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
|
||||||
max_open_trades=max_open_trades,
|
starting_balance=starting_balance,
|
||||||
results=results.loc[results['is_open']],
|
results=results.loc[results['is_open']],
|
||||||
skip_nan=True)
|
skip_nan=True)
|
||||||
daily_stats = generate_daily_stats(results)
|
daily_stats = generate_daily_stats(results)
|
||||||
@ -275,8 +284,10 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
|
|||||||
'sell_reason_summary': sell_reason_stats,
|
'sell_reason_summary': sell_reason_stats,
|
||||||
'left_open_trades': left_open_results,
|
'left_open_trades': left_open_results,
|
||||||
'total_trades': len(results),
|
'total_trades': len(results),
|
||||||
|
'total_volume': float(results['stake_amount'].sum()),
|
||||||
|
'avg_stake_amount': results['stake_amount'].mean() if len(results) > 0 else 0,
|
||||||
'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0,
|
'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0,
|
||||||
'profit_total': results['profit_ratio'].sum() / max_open_trades,
|
'profit_total': results['profit_abs'].sum() / starting_balance,
|
||||||
'profit_total_abs': results['profit_abs'].sum(),
|
'profit_total_abs': results['profit_abs'].sum(),
|
||||||
'backtest_start': min_date.datetime,
|
'backtest_start': min_date.datetime,
|
||||||
'backtest_start_ts': min_date.int_timestamp * 1000,
|
'backtest_start_ts': min_date.int_timestamp * 1000,
|
||||||
@ -292,6 +303,10 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
|
|||||||
'pairlist': list(btdata.keys()),
|
'pairlist': list(btdata.keys()),
|
||||||
'stake_amount': config['stake_amount'],
|
'stake_amount': config['stake_amount'],
|
||||||
'stake_currency': config['stake_currency'],
|
'stake_currency': config['stake_currency'],
|
||||||
|
'stake_currency_decimals': decimals_per_coin(config['stake_currency']),
|
||||||
|
'starting_balance': starting_balance,
|
||||||
|
'dry_run_wallet': starting_balance,
|
||||||
|
'final_balance': content['final_balance'],
|
||||||
'max_open_trades': max_open_trades,
|
'max_open_trades': max_open_trades,
|
||||||
'max_open_trades_setting': (config['max_open_trades']
|
'max_open_trades_setting': (config['max_open_trades']
|
||||||
if config['max_open_trades'] != float('inf') else -1),
|
if config['max_open_trades'] != float('inf') else -1),
|
||||||
@ -316,17 +331,23 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
|
|||||||
result['strategy'][strategy] = strat_stats
|
result['strategy'][strategy] = strat_stats
|
||||||
|
|
||||||
try:
|
try:
|
||||||
max_drawdown, drawdown_start, drawdown_end = calculate_max_drawdown(
|
max_drawdown, _, _, _, _ = calculate_max_drawdown(
|
||||||
results, value_col='profit_ratio')
|
results, value_col='profit_ratio')
|
||||||
|
drawdown_abs, drawdown_start, drawdown_end, high_val, low_val = calculate_max_drawdown(
|
||||||
|
results, value_col='profit_abs')
|
||||||
strat_stats.update({
|
strat_stats.update({
|
||||||
'max_drawdown': max_drawdown,
|
'max_drawdown': max_drawdown,
|
||||||
|
'max_drawdown_abs': drawdown_abs,
|
||||||
'drawdown_start': drawdown_start,
|
'drawdown_start': drawdown_start,
|
||||||
'drawdown_start_ts': drawdown_start.timestamp() * 1000,
|
'drawdown_start_ts': drawdown_start.timestamp() * 1000,
|
||||||
'drawdown_end': drawdown_end,
|
'drawdown_end': drawdown_end,
|
||||||
'drawdown_end_ts': drawdown_end.timestamp() * 1000,
|
'drawdown_end_ts': drawdown_end.timestamp() * 1000,
|
||||||
|
|
||||||
|
'max_drawdown_low': low_val,
|
||||||
|
'max_drawdown_high': high_val,
|
||||||
})
|
})
|
||||||
|
|
||||||
csum_min, csum_max = calculate_csum(results)
|
csum_min, csum_max = calculate_csum(results, starting_balance)
|
||||||
strat_stats.update({
|
strat_stats.update({
|
||||||
'csum_min': csum_min,
|
'csum_min': csum_min,
|
||||||
'csum_max': csum_max
|
'csum_max': csum_max
|
||||||
@ -335,6 +356,9 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
strat_stats.update({
|
strat_stats.update({
|
||||||
'max_drawdown': 0.0,
|
'max_drawdown': 0.0,
|
||||||
|
'max_drawdown_abs': 0.0,
|
||||||
|
'max_drawdown_low': 0.0,
|
||||||
|
'max_drawdown_high': 0.0,
|
||||||
'drawdown_start': datetime(1970, 1, 1, tzinfo=timezone.utc),
|
'drawdown_start': datetime(1970, 1, 1, tzinfo=timezone.utc),
|
||||||
'drawdown_start_ts': 0,
|
'drawdown_start_ts': 0,
|
||||||
'drawdown_end': datetime(1970, 1, 1, tzinfo=timezone.utc),
|
'drawdown_end': datetime(1970, 1, 1, tzinfo=timezone.utc),
|
||||||
@ -431,8 +455,19 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
|||||||
('Max open trades', strat_results['max_open_trades']),
|
('Max open trades', strat_results['max_open_trades']),
|
||||||
('', ''), # Empty line to improve readability
|
('', ''), # Empty line to improve readability
|
||||||
('Total trades', strat_results['total_trades']),
|
('Total trades', strat_results['total_trades']),
|
||||||
('Total Profit %', f"{round(strat_results['profit_total'] * 100, 2)}%"),
|
('Starting balance', round_coin_value(strat_results['starting_balance'],
|
||||||
|
strat_results['stake_currency'])),
|
||||||
|
('Final balance', round_coin_value(strat_results['final_balance'],
|
||||||
|
strat_results['stake_currency'])),
|
||||||
|
('Absolute profit ', round_coin_value(strat_results['profit_total_abs'],
|
||||||
|
strat_results['stake_currency'])),
|
||||||
|
('Total profit %', f"{round(strat_results['profit_total'] * 100, 2)}%"),
|
||||||
('Trades per day', strat_results['trades_per_day']),
|
('Trades per day', strat_results['trades_per_day']),
|
||||||
|
('Avg. stake amount', round_coin_value(strat_results['avg_stake_amount'],
|
||||||
|
strat_results['stake_currency'])),
|
||||||
|
('Total trade volume', round_coin_value(strat_results['total_volume'],
|
||||||
|
strat_results['stake_currency'])),
|
||||||
|
|
||||||
('', ''), # Empty line to improve readability
|
('', ''), # Empty line to improve readability
|
||||||
('Best Pair', f"{strat_results['best_pair']['key']} "
|
('Best Pair', f"{strat_results['best_pair']['key']} "
|
||||||
f"{round(strat_results['best_pair']['profit_sum_pct'], 2)}%"),
|
f"{round(strat_results['best_pair']['profit_sum_pct'], 2)}%"),
|
||||||
@ -442,20 +477,28 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
|||||||
('Worst trade', f"{worst_trade['pair']} "
|
('Worst trade', f"{worst_trade['pair']} "
|
||||||
f"{round(worst_trade['profit_ratio'] * 100, 2)}%"),
|
f"{round(worst_trade['profit_ratio'] * 100, 2)}%"),
|
||||||
|
|
||||||
('Best day', f"{round(strat_results['backtest_best_day'] * 100, 2)}%"),
|
('Best day', round_coin_value(strat_results['backtest_best_day_abs'],
|
||||||
('Worst day', f"{round(strat_results['backtest_worst_day'] * 100, 2)}%"),
|
strat_results['stake_currency'])),
|
||||||
|
('Worst day', round_coin_value(strat_results['backtest_worst_day_abs'],
|
||||||
|
strat_results['stake_currency'])),
|
||||||
('Days win/draw/lose', f"{strat_results['winning_days']} / "
|
('Days win/draw/lose', f"{strat_results['winning_days']} / "
|
||||||
f"{strat_results['draw_days']} / {strat_results['losing_days']}"),
|
f"{strat_results['draw_days']} / {strat_results['losing_days']}"),
|
||||||
('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"),
|
('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"),
|
||||||
('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"),
|
('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"),
|
||||||
('', ''), # Empty line to improve readability
|
('', ''), # Empty line to improve readability
|
||||||
|
|
||||||
('Abs Profit Min', round_coin_value(strat_results['csum_min'],
|
('Min balance', round_coin_value(strat_results['csum_min'],
|
||||||
strat_results['stake_currency'])),
|
strat_results['stake_currency'])),
|
||||||
('Abs Profit Max', round_coin_value(strat_results['csum_max'],
|
('Max balance', round_coin_value(strat_results['csum_max'],
|
||||||
strat_results['stake_currency'])),
|
strat_results['stake_currency'])),
|
||||||
|
|
||||||
('Max Drawdown', f"{round(strat_results['max_drawdown'] * 100, 2)}%"),
|
('Drawdown', f"{round(strat_results['max_drawdown'] * 100, 2)}%"),
|
||||||
|
('Drawdown', round_coin_value(strat_results['max_drawdown_abs'],
|
||||||
|
strat_results['stake_currency'])),
|
||||||
|
('Drawdown high', round_coin_value(strat_results['max_drawdown_high'],
|
||||||
|
strat_results['stake_currency'])),
|
||||||
|
('Drawdown low', round_coin_value(strat_results['max_drawdown_low'],
|
||||||
|
strat_results['stake_currency'])),
|
||||||
('Drawdown Start', strat_results['drawdown_start'].strftime(DATETIME_PRINT_FORMAT)),
|
('Drawdown Start', strat_results['drawdown_start'].strftime(DATETIME_PRINT_FORMAT)),
|
||||||
('Drawdown End', strat_results['drawdown_end'].strftime(DATETIME_PRINT_FORMAT)),
|
('Drawdown End', strat_results['drawdown_end'].strftime(DATETIME_PRINT_FORMAT)),
|
||||||
('Market change', f"{round(strat_results['market_change'] * 100, 2)}%"),
|
('Market change', f"{round(strat_results['market_change'] * 100, 2)}%"),
|
||||||
@ -463,7 +506,17 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
|||||||
|
|
||||||
return tabulate(metrics, headers=["Metric", "Value"], tablefmt="orgtbl")
|
return tabulate(metrics, headers=["Metric", "Value"], tablefmt="orgtbl")
|
||||||
else:
|
else:
|
||||||
return ''
|
start_balance = round_coin_value(strat_results['starting_balance'],
|
||||||
|
strat_results['stake_currency'])
|
||||||
|
stake_amount = round_coin_value(
|
||||||
|
strat_results['stake_amount'], strat_results['stake_currency']
|
||||||
|
) if strat_results['stake_amount'] != UNLIMITED_STAKE_AMOUNT else 'unlimited'
|
||||||
|
|
||||||
|
message = ("No trades made. "
|
||||||
|
f"Your starting balance was {start_balance}, "
|
||||||
|
f"and your stake was {stake_amount}."
|
||||||
|
)
|
||||||
|
return message
|
||||||
|
|
||||||
|
|
||||||
def show_backtest_results(config: Dict, backtest_stats: Dict):
|
def show_backtest_results(config: Dict, backtest_stats: Dict):
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
# flake8: noqa: F401
|
# flake8: noqa: F401
|
||||||
|
|
||||||
from freqtrade.persistence.models import Order, Trade, clean_dry_run_db, cleanup_db, init_db
|
from freqtrade.persistence.models import (LocalTrade, Order, Trade, clean_dry_run_db, cleanup_db,
|
||||||
|
init_db)
|
||||||
from freqtrade.persistence.pairlock_middleware import PairLocks
|
from freqtrade.persistence.pairlock_middleware import PairLocks
|
||||||
|
@ -141,7 +141,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
|
|||||||
inspector = inspect(engine)
|
inspector = inspect(engine)
|
||||||
cols = inspector.get_columns('trades')
|
cols = inspector.get_columns('trades')
|
||||||
|
|
||||||
if 'orders' not in previous_tables:
|
if 'orders' not in previous_tables and 'trades' in previous_tables:
|
||||||
logger.info('Moving open orders to Orders table.')
|
logger.info('Moving open orders to Orders table.')
|
||||||
migrate_open_orders_to_trades(engine)
|
migrate_open_orders_to_trades(engine)
|
||||||
else:
|
else:
|
||||||
|
@ -199,67 +199,67 @@ class Order(_DECL_BASE):
|
|||||||
return Order.query.filter(Order.ft_is_open.is_(True)).all()
|
return Order.query.filter(Order.ft_is_open.is_(True)).all()
|
||||||
|
|
||||||
|
|
||||||
class Trade(_DECL_BASE):
|
class LocalTrade():
|
||||||
"""
|
"""
|
||||||
Trade database model.
|
Trade database model.
|
||||||
Also handles updating and querying trades
|
Used in backtesting - must be aligned to Trade model!
|
||||||
|
|
||||||
"""
|
"""
|
||||||
__tablename__ = 'trades'
|
use_db: bool = False
|
||||||
|
|
||||||
use_db: bool = True
|
|
||||||
# Trades container for backtesting
|
# Trades container for backtesting
|
||||||
trades: List['Trade'] = []
|
trades: List['LocalTrade'] = []
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id: int = 0
|
||||||
|
|
||||||
orders = relationship("Order", order_by="Order.id", cascade="all, delete-orphan")
|
orders: List[Order] = []
|
||||||
|
|
||||||
exchange = Column(String, nullable=False)
|
exchange: str = ''
|
||||||
pair = Column(String, nullable=False, index=True)
|
pair: str = ''
|
||||||
is_open = Column(Boolean, nullable=False, default=True, index=True)
|
is_open: bool = True
|
||||||
fee_open = Column(Float, nullable=False, default=0.0)
|
fee_open: float = 0.0
|
||||||
fee_open_cost = Column(Float, nullable=True)
|
fee_open_cost: Optional[float] = None
|
||||||
fee_open_currency = Column(String, nullable=True)
|
fee_open_currency: str = ''
|
||||||
fee_close = Column(Float, nullable=False, default=0.0)
|
fee_close: float = 0.0
|
||||||
fee_close_cost = Column(Float, nullable=True)
|
fee_close_cost: Optional[float] = None
|
||||||
fee_close_currency = Column(String, nullable=True)
|
fee_close_currency: str = ''
|
||||||
open_rate = Column(Float)
|
open_rate: float = 0.0
|
||||||
open_rate_requested = Column(Float)
|
open_rate_requested: Optional[float] = None
|
||||||
# open_trade_value - calculated via _calc_open_trade_value
|
# open_trade_value - calculated via _calc_open_trade_value
|
||||||
open_trade_value = Column(Float)
|
open_trade_value: float = 0.0
|
||||||
close_rate = Column(Float)
|
close_rate: Optional[float] = None
|
||||||
close_rate_requested = Column(Float)
|
close_rate_requested: Optional[float] = None
|
||||||
close_profit = Column(Float)
|
close_profit: Optional[float] = None
|
||||||
close_profit_abs = Column(Float)
|
close_profit_abs: Optional[float] = None
|
||||||
stake_amount = Column(Float, nullable=False)
|
stake_amount: float = 0.0
|
||||||
amount = Column(Float)
|
amount: float = 0.0
|
||||||
amount_requested = Column(Float)
|
amount_requested: Optional[float] = None
|
||||||
open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
|
open_date: datetime
|
||||||
close_date = Column(DateTime)
|
close_date: Optional[datetime] = None
|
||||||
open_order_id = Column(String)
|
open_order_id: Optional[str] = None
|
||||||
# absolute value of the stop loss
|
# absolute value of the stop loss
|
||||||
stop_loss = Column(Float, nullable=True, default=0.0)
|
stop_loss: float = 0.0
|
||||||
# percentage value of the stop loss
|
# percentage value of the stop loss
|
||||||
stop_loss_pct = Column(Float, nullable=True)
|
stop_loss_pct: float = 0.0
|
||||||
# absolute value of the initial stop loss
|
# absolute value of the initial stop loss
|
||||||
initial_stop_loss = Column(Float, nullable=True, default=0.0)
|
initial_stop_loss: float = 0.0
|
||||||
# percentage value of the initial stop loss
|
# percentage value of the initial stop loss
|
||||||
initial_stop_loss_pct = Column(Float, nullable=True)
|
initial_stop_loss_pct: float = 0.0
|
||||||
# stoploss order id which is on exchange
|
# stoploss order id which is on exchange
|
||||||
stoploss_order_id = Column(String, nullable=True, index=True)
|
stoploss_order_id: Optional[str] = None
|
||||||
# last update time of the stoploss order on exchange
|
# last update time of the stoploss order on exchange
|
||||||
stoploss_last_update = Column(DateTime, nullable=True)
|
stoploss_last_update: Optional[datetime] = None
|
||||||
# absolute value of the highest reached price
|
# absolute value of the highest reached price
|
||||||
max_rate = Column(Float, nullable=True, default=0.0)
|
max_rate: float = 0.0
|
||||||
# Lowest price reached
|
# Lowest price reached
|
||||||
min_rate = Column(Float, nullable=True)
|
min_rate: float = 0.0
|
||||||
sell_reason = Column(String, nullable=True)
|
sell_reason: str = ''
|
||||||
sell_order_status = Column(String, nullable=True)
|
sell_order_status: str = ''
|
||||||
strategy = Column(String, nullable=True)
|
strategy: str = ''
|
||||||
timeframe = Column(Integer, nullable=True)
|
timeframe: Optional[int] = None
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**kwargs)
|
for key in kwargs:
|
||||||
|
setattr(self, key, kwargs[key])
|
||||||
self.recalc_open_trade_value()
|
self.recalc_open_trade_value()
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
@ -268,6 +268,14 @@ class Trade(_DECL_BASE):
|
|||||||
return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, '
|
return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, '
|
||||||
f'open_rate={self.open_rate:.8f}, open_since={open_since})')
|
f'open_rate={self.open_rate:.8f}, open_since={open_since})')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def open_date_utc(self):
|
||||||
|
return self.open_date.replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def close_date_utc(self):
|
||||||
|
return self.close_date.replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
def to_json(self) -> Dict[str, Any]:
|
def to_json(self) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
'trade_id': self.id,
|
'trade_id': self.id,
|
||||||
@ -306,9 +314,9 @@ class Trade(_DECL_BASE):
|
|||||||
'close_profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None,
|
'close_profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None,
|
||||||
'close_profit_abs': self.close_profit_abs, # Deprecated
|
'close_profit_abs': self.close_profit_abs, # Deprecated
|
||||||
|
|
||||||
'trade_duration_s': (int((self.close_date - self.open_date).total_seconds())
|
'trade_duration_s': (int((self.close_date_utc - self.open_date_utc).total_seconds())
|
||||||
if self.close_date else None),
|
if self.close_date else None),
|
||||||
'trade_duration': (int((self.close_date - self.open_date).total_seconds() // 60)
|
'trade_duration': (int((self.close_date_utc - self.open_date_utc).total_seconds() // 60)
|
||||||
if self.close_date else None),
|
if self.close_date else None),
|
||||||
|
|
||||||
'profit_ratio': self.close_profit,
|
'profit_ratio': self.close_profit,
|
||||||
@ -341,8 +349,7 @@ class Trade(_DECL_BASE):
|
|||||||
"""
|
"""
|
||||||
Resets all trades. Only active for backtesting mode.
|
Resets all trades. Only active for backtesting mode.
|
||||||
"""
|
"""
|
||||||
if not Trade.use_db:
|
LocalTrade.trades = []
|
||||||
Trade.trades = []
|
|
||||||
|
|
||||||
def adjust_min_max_rates(self, current_price: float) -> None:
|
def adjust_min_max_rates(self, current_price: float) -> None:
|
||||||
"""
|
"""
|
||||||
@ -410,8 +417,8 @@ class Trade(_DECL_BASE):
|
|||||||
|
|
||||||
if order_type in ('market', 'limit') and order['side'] == 'buy':
|
if order_type in ('market', 'limit') and order['side'] == 'buy':
|
||||||
# Update open rate and actual amount
|
# Update open rate and actual amount
|
||||||
self.open_rate = Decimal(safe_value_fallback(order, 'average', 'price'))
|
self.open_rate = float(safe_value_fallback(order, 'average', 'price'))
|
||||||
self.amount = Decimal(safe_value_fallback(order, 'filled', 'amount'))
|
self.amount = float(safe_value_fallback(order, 'filled', 'amount'))
|
||||||
self.recalc_open_trade_value()
|
self.recalc_open_trade_value()
|
||||||
if self.is_open:
|
if self.is_open:
|
||||||
logger.info(f'{order_type.upper()}_BUY has been fulfilled for {self}.')
|
logger.info(f'{order_type.upper()}_BUY has been fulfilled for {self}.')
|
||||||
@ -435,7 +442,7 @@ class Trade(_DECL_BASE):
|
|||||||
Sets close_rate to the given rate, calculates total profit
|
Sets close_rate to the given rate, calculates total profit
|
||||||
and marks trade as closed
|
and marks trade as closed
|
||||||
"""
|
"""
|
||||||
self.close_rate = Decimal(rate)
|
self.close_rate = rate
|
||||||
self.close_profit = self.calc_profit_ratio()
|
self.close_profit = self.calc_profit_ratio()
|
||||||
self.close_profit_abs = self.calc_profit()
|
self.close_profit_abs = self.calc_profit()
|
||||||
self.close_date = self.close_date or datetime.utcnow()
|
self.close_date = self.close_date or datetime.utcnow()
|
||||||
@ -480,14 +487,6 @@ class Trade(_DECL_BASE):
|
|||||||
def update_order(self, order: Dict) -> None:
|
def update_order(self, order: Dict) -> None:
|
||||||
Order.update_orders(self.orders, order)
|
Order.update_orders(self.orders, order)
|
||||||
|
|
||||||
def delete(self) -> None:
|
|
||||||
|
|
||||||
for order in self.orders:
|
|
||||||
Order.session.delete(order)
|
|
||||||
|
|
||||||
Trade.session.delete(self)
|
|
||||||
Trade.session.flush()
|
|
||||||
|
|
||||||
def _calc_open_trade_value(self) -> float:
|
def _calc_open_trade_value(self) -> float:
|
||||||
"""
|
"""
|
||||||
Calculate the open_rate including open_fee.
|
Calculate the open_rate including open_fee.
|
||||||
@ -517,7 +516,7 @@ class Trade(_DECL_BASE):
|
|||||||
if rate is None and not self.close_rate:
|
if rate is None and not self.close_rate:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
sell_trade = Decimal(self.amount) * Decimal(rate or self.close_rate)
|
sell_trade = Decimal(self.amount) * Decimal(rate or self.close_rate) # type: ignore
|
||||||
fees = sell_trade * Decimal(fee or self.fee_close)
|
fees = sell_trade * Decimal(fee or self.fee_close)
|
||||||
return float(sell_trade - fees)
|
return float(sell_trade - fees)
|
||||||
|
|
||||||
@ -589,7 +588,7 @@ class Trade(_DECL_BASE):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_trades_proxy(*, pair: str = None, is_open: bool = None,
|
def get_trades_proxy(*, pair: str = None, is_open: bool = None,
|
||||||
open_date: datetime = None, close_date: datetime = None,
|
open_date: datetime = None, close_date: datetime = None,
|
||||||
) -> List['Trade']:
|
) -> List['LocalTrade']:
|
||||||
"""
|
"""
|
||||||
Helper function to query Trades.
|
Helper function to query Trades.
|
||||||
Returns a List of trades, filtered on the parameters given.
|
Returns a List of trades, filtered on the parameters given.
|
||||||
@ -598,20 +597,9 @@ class Trade(_DECL_BASE):
|
|||||||
|
|
||||||
:return: unsorted List[Trade]
|
:return: unsorted List[Trade]
|
||||||
"""
|
"""
|
||||||
if Trade.use_db:
|
|
||||||
trade_filter = []
|
|
||||||
if pair:
|
|
||||||
trade_filter.append(Trade.pair == pair)
|
|
||||||
if open_date:
|
|
||||||
trade_filter.append(Trade.open_date > open_date)
|
|
||||||
if close_date:
|
|
||||||
trade_filter.append(Trade.close_date > close_date)
|
|
||||||
if is_open is not None:
|
|
||||||
trade_filter.append(Trade.is_open.is_(is_open))
|
|
||||||
return Trade.get_trades(trade_filter).all()
|
|
||||||
else:
|
|
||||||
# Offline mode - without database
|
# Offline mode - without database
|
||||||
sel_trades = [trade for trade in Trade.trades]
|
sel_trades = [trade for trade in LocalTrade.trades]
|
||||||
if pair:
|
if pair:
|
||||||
sel_trades = [trade for trade in sel_trades if trade.pair == pair]
|
sel_trades = [trade for trade in sel_trades if trade.pair == pair]
|
||||||
if open_date:
|
if open_date:
|
||||||
@ -663,9 +651,12 @@ class Trade(_DECL_BASE):
|
|||||||
Calculates total invested amount in open trades
|
Calculates total invested amount in open trades
|
||||||
in stake currency
|
in stake currency
|
||||||
"""
|
"""
|
||||||
total_open_stake_amount = Trade.session.query(func.sum(Trade.stake_amount))\
|
if Trade.use_db:
|
||||||
.filter(Trade.is_open.is_(True))\
|
total_open_stake_amount = Trade.session.query(
|
||||||
.scalar()
|
func.sum(Trade.stake_amount)).filter(Trade.is_open.is_(True)).scalar()
|
||||||
|
else:
|
||||||
|
total_open_stake_amount = sum(
|
||||||
|
t.stake_amount for t in Trade.get_trades_proxy(is_open=True))
|
||||||
return total_open_stake_amount or 0
|
return total_open_stake_amount or 0
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -723,6 +714,108 @@ class Trade(_DECL_BASE):
|
|||||||
logger.info(f"New stoploss: {trade.stop_loss}.")
|
logger.info(f"New stoploss: {trade.stop_loss}.")
|
||||||
|
|
||||||
|
|
||||||
|
class Trade(_DECL_BASE, LocalTrade):
|
||||||
|
"""
|
||||||
|
Trade database model.
|
||||||
|
Also handles updating and querying trades
|
||||||
|
|
||||||
|
Note: Fields must be aligned with LocalTrade class
|
||||||
|
"""
|
||||||
|
__tablename__ = 'trades'
|
||||||
|
|
||||||
|
use_db: bool = True
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
|
||||||
|
orders = relationship("Order", order_by="Order.id", cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
exchange = Column(String, nullable=False)
|
||||||
|
pair = Column(String, nullable=False, index=True)
|
||||||
|
is_open = Column(Boolean, nullable=False, default=True, index=True)
|
||||||
|
fee_open = Column(Float, nullable=False, default=0.0)
|
||||||
|
fee_open_cost = Column(Float, nullable=True)
|
||||||
|
fee_open_currency = Column(String, nullable=True)
|
||||||
|
fee_close = Column(Float, nullable=False, default=0.0)
|
||||||
|
fee_close_cost = Column(Float, nullable=True)
|
||||||
|
fee_close_currency = Column(String, nullable=True)
|
||||||
|
open_rate = Column(Float)
|
||||||
|
open_rate_requested = Column(Float)
|
||||||
|
# open_trade_value - calculated via _calc_open_trade_value
|
||||||
|
open_trade_value = Column(Float)
|
||||||
|
close_rate = Column(Float)
|
||||||
|
close_rate_requested = Column(Float)
|
||||||
|
close_profit = Column(Float)
|
||||||
|
close_profit_abs = Column(Float)
|
||||||
|
stake_amount = Column(Float, nullable=False)
|
||||||
|
amount = Column(Float)
|
||||||
|
amount_requested = Column(Float)
|
||||||
|
open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||||
|
close_date = Column(DateTime)
|
||||||
|
open_order_id = Column(String)
|
||||||
|
# absolute value of the stop loss
|
||||||
|
stop_loss = Column(Float, nullable=True, default=0.0)
|
||||||
|
# percentage value of the stop loss
|
||||||
|
stop_loss_pct = Column(Float, nullable=True)
|
||||||
|
# absolute value of the initial stop loss
|
||||||
|
initial_stop_loss = Column(Float, nullable=True, default=0.0)
|
||||||
|
# percentage value of the initial stop loss
|
||||||
|
initial_stop_loss_pct = Column(Float, nullable=True)
|
||||||
|
# stoploss order id which is on exchange
|
||||||
|
stoploss_order_id = Column(String, nullable=True, index=True)
|
||||||
|
# last update time of the stoploss order on exchange
|
||||||
|
stoploss_last_update = Column(DateTime, nullable=True)
|
||||||
|
# absolute value of the highest reached price
|
||||||
|
max_rate = Column(Float, nullable=True, default=0.0)
|
||||||
|
# Lowest price reached
|
||||||
|
min_rate = Column(Float, nullable=True)
|
||||||
|
sell_reason = Column(String, nullable=True)
|
||||||
|
sell_order_status = Column(String, nullable=True)
|
||||||
|
strategy = Column(String, nullable=True)
|
||||||
|
timeframe = Column(Integer, nullable=True)
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.recalc_open_trade_value()
|
||||||
|
|
||||||
|
def delete(self) -> None:
|
||||||
|
|
||||||
|
for order in self.orders:
|
||||||
|
Order.session.delete(order)
|
||||||
|
|
||||||
|
Trade.session.delete(self)
|
||||||
|
Trade.session.flush()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_trades_proxy(*, pair: str = None, is_open: bool = None,
|
||||||
|
open_date: datetime = None, close_date: datetime = None,
|
||||||
|
) -> List['LocalTrade']:
|
||||||
|
"""
|
||||||
|
Helper function to query Trades.
|
||||||
|
Returns a List of trades, filtered on the parameters given.
|
||||||
|
In live mode, converts the filter to a database query and returns all rows
|
||||||
|
In Backtest mode, uses filters on Trade.trades to get the result.
|
||||||
|
|
||||||
|
:return: unsorted List[Trade]
|
||||||
|
"""
|
||||||
|
if Trade.use_db:
|
||||||
|
trade_filter = []
|
||||||
|
if pair:
|
||||||
|
trade_filter.append(Trade.pair == pair)
|
||||||
|
if open_date:
|
||||||
|
trade_filter.append(Trade.open_date > open_date)
|
||||||
|
if close_date:
|
||||||
|
trade_filter.append(Trade.close_date > close_date)
|
||||||
|
if is_open is not None:
|
||||||
|
trade_filter.append(Trade.is_open.is_(is_open))
|
||||||
|
return Trade.get_trades(trade_filter).all()
|
||||||
|
else:
|
||||||
|
return LocalTrade.get_trades_proxy(
|
||||||
|
pair=pair, is_open=is_open,
|
||||||
|
open_date=open_date,
|
||||||
|
close_date=close_date
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PairLock(_DECL_BASE):
|
class PairLock(_DECL_BASE):
|
||||||
"""
|
"""
|
||||||
Pair Locks database model.
|
Pair Locks database model.
|
||||||
|
@ -123,3 +123,11 @@ class PairLocks():
|
|||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
|
|
||||||
return len(PairLocks.get_pair_locks(pair, now)) > 0 or PairLocks.is_global_lock(now)
|
return len(PairLocks.get_pair_locks(pair, now)) > 0 or PairLocks.is_global_lock(now)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_all_locks() -> List[PairLock]:
|
||||||
|
|
||||||
|
if PairLocks.use_db:
|
||||||
|
return PairLock.query.all()
|
||||||
|
else:
|
||||||
|
return PairLocks.locks
|
||||||
|
@ -145,7 +145,7 @@ def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame,
|
|||||||
Add scatter points indicating max drawdown
|
Add scatter points indicating max drawdown
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
max_drawdown, highdate, lowdate = calculate_max_drawdown(trades)
|
max_drawdown, highdate, lowdate, _, _ = calculate_max_drawdown(trades)
|
||||||
|
|
||||||
drawdown = go.Scatter(
|
drawdown = go.Scatter(
|
||||||
x=[highdate, lowdate],
|
x=[highdate, lowdate],
|
||||||
|
@ -44,7 +44,8 @@ class CooldownPeriod(IProtection):
|
|||||||
trades = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until)
|
trades = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until)
|
||||||
if trades:
|
if trades:
|
||||||
# Get latest trade
|
# Get latest trade
|
||||||
trade = sorted(trades, key=lambda t: t.close_date)[-1]
|
# Ignore type error as we know we only get closed trades.
|
||||||
|
trade = sorted(trades, key=lambda t: t.close_date)[-1] # type: ignore
|
||||||
self.log_once(f"Cooldown for {pair} for {self.stop_duration_str}.", logger.info)
|
self.log_once(f"Cooldown for {pair} for {self.stop_duration_str}.", logger.info)
|
||||||
until = self.calculate_lock_end([trade], self._stop_duration)
|
until = self.calculate_lock_end([trade], self._stop_duration)
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ from typing import Any, Dict, List, Optional, Tuple
|
|||||||
from freqtrade.exchange import timeframe_to_minutes
|
from freqtrade.exchange import timeframe_to_minutes
|
||||||
from freqtrade.misc import plural
|
from freqtrade.misc import plural
|
||||||
from freqtrade.mixins import LoggingMixin
|
from freqtrade.mixins import LoggingMixin
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import LocalTrade
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -93,11 +93,11 @@ class IProtection(LoggingMixin, ABC):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def calculate_lock_end(trades: List[Trade], stop_minutes: int) -> datetime:
|
def calculate_lock_end(trades: List[LocalTrade], stop_minutes: int) -> datetime:
|
||||||
"""
|
"""
|
||||||
Get lock end time
|
Get lock end time
|
||||||
"""
|
"""
|
||||||
max_date: datetime = max([trade.close_date for trade in trades])
|
max_date: datetime = max([trade.close_date for trade in trades if trade.close_date])
|
||||||
# comming from Database, tzinfo is not set.
|
# comming from Database, tzinfo is not set.
|
||||||
if max_date.tzinfo is None:
|
if max_date.tzinfo is None:
|
||||||
max_date = max_date.replace(tzinfo=timezone.utc)
|
max_date = max_date.replace(tzinfo=timezone.utc)
|
||||||
|
@ -53,7 +53,7 @@ class LowProfitPairs(IProtection):
|
|||||||
# Not enough trades in the relevant period
|
# Not enough trades in the relevant period
|
||||||
return False, None, None
|
return False, None, None
|
||||||
|
|
||||||
profit = sum(trade.close_profit for trade in trades)
|
profit = sum(trade.close_profit for trade in trades if trade.close_profit)
|
||||||
if profit < self._required_profit:
|
if profit < self._required_profit:
|
||||||
self.log_once(
|
self.log_once(
|
||||||
f"Trading for {pair} stopped due to {profit:.2f} < {self._required_profit} "
|
f"Trading for {pair} stopped due to {profit:.2f} < {self._required_profit} "
|
||||||
|
@ -55,7 +55,7 @@ class MaxDrawdown(IProtection):
|
|||||||
|
|
||||||
# Drawdown is always positive
|
# Drawdown is always positive
|
||||||
try:
|
try:
|
||||||
drawdown, _, _ = calculate_max_drawdown(trades_df, value_col='close_profit')
|
drawdown, _, _, _, _ = calculate_max_drawdown(trades_df, value_col='close_profit')
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return False, None, None
|
return False, None, None
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ class StoplossGuard(IProtection):
|
|||||||
trades = [trade for trade in trades1 if (str(trade.sell_reason) in (
|
trades = [trade for trade in trades1 if (str(trade.sell_reason) in (
|
||||||
SellType.TRAILING_STOP_LOSS.value, SellType.STOP_LOSS.value,
|
SellType.TRAILING_STOP_LOSS.value, SellType.STOP_LOSS.value,
|
||||||
SellType.STOPLOSS_ON_EXCHANGE.value)
|
SellType.STOPLOSS_ON_EXCHANGE.value)
|
||||||
and trade.close_profit < 0)]
|
and trade.close_profit and trade.close_profit < 0)]
|
||||||
|
|
||||||
if len(trades) < self._trade_limit:
|
if len(trades) < self._trade_limit:
|
||||||
return False, None, None
|
return False, None, None
|
||||||
|
@ -649,7 +649,7 @@ class IStrategy(ABC):
|
|||||||
:return: True if bot should sell at current rate
|
:return: True if bot should sell at current rate
|
||||||
"""
|
"""
|
||||||
# Check if time matches and current rate is above threshold
|
# Check if time matches and current rate is above threshold
|
||||||
trade_dur = int((current_time.timestamp() - trade.open_date.timestamp()) // 60)
|
trade_dur = int((current_time.timestamp() - trade.open_date_utc.timestamp()) // 60)
|
||||||
_, roi = self.min_roi_reached_entry(trade_dur)
|
_, roi = self.min_roi_reached_entry(trade_dur)
|
||||||
if roi is None:
|
if roi is None:
|
||||||
return False
|
return False
|
||||||
|
@ -11,6 +11,7 @@ from freqtrade.constants import UNLIMITED_STAKE_AMOUNT
|
|||||||
from freqtrade.exceptions import DependencyException
|
from freqtrade.exceptions import DependencyException
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.state import RunMode
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -26,8 +27,9 @@ class Wallet(NamedTuple):
|
|||||||
|
|
||||||
class Wallets:
|
class Wallets:
|
||||||
|
|
||||||
def __init__(self, config: dict, exchange: Exchange) -> None:
|
def __init__(self, config: dict, exchange: Exchange, log: bool = True) -> None:
|
||||||
self._config = config
|
self._config = config
|
||||||
|
self._log = log
|
||||||
self._exchange = exchange
|
self._exchange = exchange
|
||||||
self._wallets: Dict[str, Wallet] = {}
|
self._wallets: Dict[str, Wallet] = {}
|
||||||
self.start_cap = config['dry_run_wallet']
|
self.start_cap = config['dry_run_wallet']
|
||||||
@ -64,9 +66,9 @@ class Wallets:
|
|||||||
"""
|
"""
|
||||||
# Recreate _wallets to reset closed trade balances
|
# Recreate _wallets to reset closed trade balances
|
||||||
_wallets = {}
|
_wallets = {}
|
||||||
closed_trades = Trade.get_trades(Trade.is_open.is_(False)).all()
|
closed_trades = Trade.get_trades_proxy(is_open=False)
|
||||||
open_trades = Trade.get_trades(Trade.is_open.is_(True)).all()
|
open_trades = Trade.get_trades_proxy(is_open=True)
|
||||||
tot_profit = sum([trade.calc_profit() for trade in closed_trades])
|
tot_profit = sum([trade.close_profit_abs for trade in closed_trades])
|
||||||
tot_in_trades = sum([trade.stake_amount for trade in open_trades])
|
tot_in_trades = sum([trade.stake_amount for trade in open_trades])
|
||||||
|
|
||||||
current_stake = self.start_cap + tot_profit - tot_in_trades
|
current_stake = self.start_cap + tot_profit - tot_in_trades
|
||||||
@ -111,10 +113,11 @@ class Wallets:
|
|||||||
:param require_update: Allow skipping an update if balances were recently refreshed
|
:param require_update: Allow skipping an update if balances were recently refreshed
|
||||||
"""
|
"""
|
||||||
if (require_update or (self._last_wallet_refresh + 3600 < arrow.utcnow().int_timestamp)):
|
if (require_update or (self._last_wallet_refresh + 3600 < arrow.utcnow().int_timestamp)):
|
||||||
if self._config['dry_run']:
|
if (not self._config['dry_run'] or self._config.get('runmode') == RunMode.LIVE):
|
||||||
self._update_dry()
|
|
||||||
else:
|
|
||||||
self._update_live()
|
self._update_live()
|
||||||
|
else:
|
||||||
|
self._update_dry()
|
||||||
|
if self._log:
|
||||||
logger.info('Wallets synced.')
|
logger.info('Wallets synced.')
|
||||||
self._last_wallet_refresh = arrow.utcnow().int_timestamp
|
self._last_wallet_refresh = arrow.utcnow().int_timestamp
|
||||||
|
|
||||||
@ -154,6 +157,7 @@ class Wallets:
|
|||||||
Check if stake amount can be fulfilled with the available balance
|
Check if stake amount can be fulfilled with the available balance
|
||||||
for the stake currency
|
for the stake currency
|
||||||
:return: float: Stake amount
|
:return: float: Stake amount
|
||||||
|
:raise: DependencyException if balance is lower than stake-amount
|
||||||
"""
|
"""
|
||||||
available_amount = self._get_available_stake_amount()
|
available_amount = self._get_available_stake_amount()
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ from freqtrade.data.converter import ohlcv_to_dataframe
|
|||||||
from freqtrade.edge import Edge, PairInfo
|
from freqtrade.edge import Edge, PairInfo
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.freqtradebot import FreqtradeBot
|
from freqtrade.freqtradebot import FreqtradeBot
|
||||||
from freqtrade.persistence import Trade, init_db
|
from freqtrade.persistence import LocalTrade, Trade, init_db
|
||||||
from freqtrade.resolvers import ExchangeResolver
|
from freqtrade.resolvers import ExchangeResolver
|
||||||
from freqtrade.worker import Worker
|
from freqtrade.worker import Worker
|
||||||
from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4,
|
from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4,
|
||||||
@ -183,28 +183,34 @@ def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None:
|
|||||||
freqtrade.exchange.refresh_latest_ohlcv = lambda p: None
|
freqtrade.exchange.refresh_latest_ohlcv = lambda p: None
|
||||||
|
|
||||||
|
|
||||||
def create_mock_trades(fee):
|
def create_mock_trades(fee, use_db: bool = True):
|
||||||
"""
|
"""
|
||||||
Create some fake trades ...
|
Create some fake trades ...
|
||||||
"""
|
"""
|
||||||
|
def add_trade(trade):
|
||||||
|
if use_db:
|
||||||
|
Trade.session.add(trade)
|
||||||
|
else:
|
||||||
|
LocalTrade.trades.append(trade)
|
||||||
|
|
||||||
# Simulate dry_run entries
|
# Simulate dry_run entries
|
||||||
trade = mock_trade_1(fee)
|
trade = mock_trade_1(fee)
|
||||||
Trade.session.add(trade)
|
add_trade(trade)
|
||||||
|
|
||||||
trade = mock_trade_2(fee)
|
trade = mock_trade_2(fee)
|
||||||
Trade.session.add(trade)
|
add_trade(trade)
|
||||||
|
|
||||||
trade = mock_trade_3(fee)
|
trade = mock_trade_3(fee)
|
||||||
Trade.session.add(trade)
|
add_trade(trade)
|
||||||
|
|
||||||
trade = mock_trade_4(fee)
|
trade = mock_trade_4(fee)
|
||||||
Trade.session.add(trade)
|
add_trade(trade)
|
||||||
|
|
||||||
trade = mock_trade_5(fee)
|
trade = mock_trade_5(fee)
|
||||||
Trade.session.add(trade)
|
add_trade(trade)
|
||||||
|
|
||||||
trade = mock_trade_6(fee)
|
trade = mock_trade_6(fee)
|
||||||
Trade.session.add(trade)
|
add_trade(trade)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
@ -255,6 +261,7 @@ def get_default_conf(testdatadir):
|
|||||||
"20": 0.02,
|
"20": 0.02,
|
||||||
"0": 0.04
|
"0": 0.04
|
||||||
},
|
},
|
||||||
|
"dry_run_wallet": 1000,
|
||||||
"stoploss": -0.10,
|
"stoploss": -0.10,
|
||||||
"unfilledtimeout": {
|
"unfilledtimeout": {
|
||||||
"buy": 10,
|
"buy": 10,
|
||||||
|
@ -28,6 +28,7 @@ def mock_trade_1(fee):
|
|||||||
amount_requested=123.0,
|
amount_requested=123.0,
|
||||||
fee_open=fee.return_value,
|
fee_open=fee.return_value,
|
||||||
fee_close=fee.return_value,
|
fee_close=fee.return_value,
|
||||||
|
is_open=True,
|
||||||
open_rate=0.123,
|
open_rate=0.123,
|
||||||
exchange='bittrex',
|
exchange='bittrex',
|
||||||
open_order_id='dry_run_buy_12345',
|
open_order_id='dry_run_buy_12345',
|
||||||
@ -81,6 +82,7 @@ def mock_trade_2(fee):
|
|||||||
open_rate=0.123,
|
open_rate=0.123,
|
||||||
close_rate=0.128,
|
close_rate=0.128,
|
||||||
close_profit=0.005,
|
close_profit=0.005,
|
||||||
|
close_profit_abs=0.000584127,
|
||||||
exchange='bittrex',
|
exchange='bittrex',
|
||||||
is_open=False,
|
is_open=False,
|
||||||
open_order_id='dry_run_sell_12345',
|
open_order_id='dry_run_sell_12345',
|
||||||
@ -140,6 +142,7 @@ def mock_trade_3(fee):
|
|||||||
open_rate=0.05,
|
open_rate=0.05,
|
||||||
close_rate=0.06,
|
close_rate=0.06,
|
||||||
close_profit=0.01,
|
close_profit=0.01,
|
||||||
|
close_profit_abs=0.000155,
|
||||||
exchange='bittrex',
|
exchange='bittrex',
|
||||||
is_open=False,
|
is_open=False,
|
||||||
strategy='DefaultStrategy',
|
strategy='DefaultStrategy',
|
||||||
@ -180,6 +183,7 @@ def mock_trade_4(fee):
|
|||||||
amount_requested=124.0,
|
amount_requested=124.0,
|
||||||
fee_open=fee.return_value,
|
fee_open=fee.return_value,
|
||||||
fee_close=fee.return_value,
|
fee_close=fee.return_value,
|
||||||
|
is_open=True,
|
||||||
open_rate=0.123,
|
open_rate=0.123,
|
||||||
exchange='bittrex',
|
exchange='bittrex',
|
||||||
open_order_id='prod_buy_12345',
|
open_order_id='prod_buy_12345',
|
||||||
@ -230,6 +234,7 @@ def mock_trade_5(fee):
|
|||||||
amount_requested=124.0,
|
amount_requested=124.0,
|
||||||
fee_open=fee.return_value,
|
fee_open=fee.return_value,
|
||||||
fee_close=fee.return_value,
|
fee_close=fee.return_value,
|
||||||
|
is_open=True,
|
||||||
open_rate=0.123,
|
open_rate=0.123,
|
||||||
exchange='bittrex',
|
exchange='bittrex',
|
||||||
strategy='SampleStrategy',
|
strategy='SampleStrategy',
|
||||||
@ -281,6 +286,7 @@ def mock_trade_6(fee):
|
|||||||
amount_requested=2.0,
|
amount_requested=2.0,
|
||||||
fee_open=fee.return_value,
|
fee_open=fee.return_value,
|
||||||
fee_close=fee.return_value,
|
fee_close=fee.return_value,
|
||||||
|
is_open=True,
|
||||||
open_rate=0.15,
|
open_rate=0.15,
|
||||||
exchange='bittrex',
|
exchange='bittrex',
|
||||||
strategy='SampleStrategy',
|
strategy='SampleStrategy',
|
||||||
|
@ -274,15 +274,17 @@ def test_create_cum_profit1(testdatadir):
|
|||||||
def test_calculate_max_drawdown(testdatadir):
|
def test_calculate_max_drawdown(testdatadir):
|
||||||
filename = testdatadir / "backtest-result_test.json"
|
filename = testdatadir / "backtest-result_test.json"
|
||||||
bt_data = load_backtest_data(filename)
|
bt_data = load_backtest_data(filename)
|
||||||
drawdown, h, low = calculate_max_drawdown(bt_data)
|
drawdown, hdate, lowdate, hval, lval = calculate_max_drawdown(bt_data)
|
||||||
assert isinstance(drawdown, float)
|
assert isinstance(drawdown, float)
|
||||||
assert pytest.approx(drawdown) == 0.21142322
|
assert pytest.approx(drawdown) == 0.21142322
|
||||||
assert isinstance(h, Timestamp)
|
assert isinstance(hdate, Timestamp)
|
||||||
assert isinstance(low, Timestamp)
|
assert isinstance(lowdate, Timestamp)
|
||||||
assert h == Timestamp('2018-01-24 14:25:00', tz='UTC')
|
assert isinstance(hval, float)
|
||||||
assert low == Timestamp('2018-01-30 04:45:00', tz='UTC')
|
assert isinstance(lval, float)
|
||||||
|
assert hdate == Timestamp('2018-01-24 14:25:00', tz='UTC')
|
||||||
|
assert lowdate == Timestamp('2018-01-30 04:45:00', tz='UTC')
|
||||||
with pytest.raises(ValueError, match='Trade dataframe empty.'):
|
with pytest.raises(ValueError, match='Trade dataframe empty.'):
|
||||||
drawdown, h, low = calculate_max_drawdown(DataFrame())
|
drawdown, hdate, lowdate, hval, lval = calculate_max_drawdown(DataFrame())
|
||||||
|
|
||||||
|
|
||||||
def test_calculate_csum(testdatadir):
|
def test_calculate_csum(testdatadir):
|
||||||
@ -294,6 +296,10 @@ def test_calculate_csum(testdatadir):
|
|||||||
assert isinstance(csum_max, float)
|
assert isinstance(csum_max, float)
|
||||||
assert csum_min < 0.01
|
assert csum_min < 0.01
|
||||||
assert csum_max > 0.02
|
assert csum_max > 0.02
|
||||||
|
csum_min1, csum_max1 = calculate_csum(bt_data, 5)
|
||||||
|
|
||||||
|
assert csum_min1 == csum_min + 5
|
||||||
|
assert csum_max1 == csum_max + 5
|
||||||
|
|
||||||
with pytest.raises(ValueError, match='Trade dataframe empty.'):
|
with pytest.raises(ValueError, match='Trade dataframe empty.'):
|
||||||
csum_min, csum_max = calculate_csum(DataFrame())
|
csum_min, csum_max = calculate_csum(DataFrame())
|
||||||
@ -310,13 +316,16 @@ def test_calculate_max_drawdown2():
|
|||||||
# sort by profit and reset index
|
# sort by profit and reset index
|
||||||
df = df.sort_values('profit').reset_index(drop=True)
|
df = df.sort_values('profit').reset_index(drop=True)
|
||||||
df1 = df.copy()
|
df1 = df.copy()
|
||||||
drawdown, h, low = calculate_max_drawdown(df, date_col='open_date', value_col='profit')
|
drawdown, hdate, ldate, hval, lval = calculate_max_drawdown(
|
||||||
|
df, date_col='open_date', value_col='profit')
|
||||||
# Ensure df has not been altered.
|
# Ensure df has not been altered.
|
||||||
assert df.equals(df1)
|
assert df.equals(df1)
|
||||||
|
|
||||||
assert isinstance(drawdown, float)
|
assert isinstance(drawdown, float)
|
||||||
# High must be before low
|
# High must be before low
|
||||||
assert h < low
|
assert hdate < ldate
|
||||||
|
# High value must be higher than low value
|
||||||
|
assert hval > lval
|
||||||
assert drawdown == 0.091755
|
assert drawdown == 0.091755
|
||||||
|
|
||||||
df = DataFrame(zip(values[:5], dates[:5]), columns=['profit', 'open_date'])
|
df = DataFrame(zip(values[:5], dates[:5]), columns=['profit', 'open_date'])
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
# pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, C0330, unused-argument
|
# pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, C0330, unused-argument
|
||||||
import logging
|
import logging
|
||||||
from unittest.mock import MagicMock
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@ -489,7 +488,8 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
|
|||||||
default_conf["trailing_stop_positive_offset"] = data.trailing_stop_positive_offset
|
default_conf["trailing_stop_positive_offset"] = data.trailing_stop_positive_offset
|
||||||
default_conf["ask_strategy"] = {"use_sell_signal": data.use_sell_signal}
|
default_conf["ask_strategy"] = {"use_sell_signal": data.use_sell_signal}
|
||||||
|
|
||||||
mocker.patch("freqtrade.exchange.Exchange.get_fee", MagicMock(return_value=0.0))
|
mocker.patch("freqtrade.exchange.Exchange.get_fee", return_value=0.0)
|
||||||
|
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
frame = _build_backtest_dataframe(data.data)
|
frame = _build_backtest_dataframe(data.data)
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
@ -503,7 +503,6 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
|
|||||||
min_date, max_date = get_timerange({pair: frame})
|
min_date, max_date = get_timerange({pair: frame})
|
||||||
results = backtesting.backtest(
|
results = backtesting.backtest(
|
||||||
processed=data_processed,
|
processed=data_processed,
|
||||||
stake_amount=default_conf['stake_amount'],
|
|
||||||
start_date=min_date,
|
start_date=min_date,
|
||||||
end_date=max_date,
|
end_date=max_date,
|
||||||
max_open_trades=10,
|
max_open_trades=10,
|
||||||
@ -514,6 +513,6 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
|
|||||||
|
|
||||||
for c, trade in enumerate(data.trades):
|
for c, trade in enumerate(data.trades):
|
||||||
res = results.iloc[c]
|
res = results.iloc[c]
|
||||||
assert res.sell_reason == trade.sell_reason
|
assert res.sell_reason == trade.sell_reason.value
|
||||||
assert res.open_date == _get_frame_time_from_offset(trade.open_tick)
|
assert res.open_date == _get_frame_time_from_offset(trade.open_tick)
|
||||||
assert res.close_date == _get_frame_time_from_offset(trade.close_tick)
|
assert res.close_date == _get_frame_time_from_offset(trade.close_tick)
|
||||||
|
@ -9,7 +9,6 @@ import pandas as pd
|
|||||||
import pytest
|
import pytest
|
||||||
from arrow import Arrow
|
from arrow import Arrow
|
||||||
|
|
||||||
from freqtrade import constants
|
|
||||||
from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_backtesting
|
from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_backtesting
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.data import history
|
from freqtrade.data import history
|
||||||
@ -19,6 +18,7 @@ from freqtrade.data.dataprovider import DataProvider
|
|||||||
from freqtrade.data.history import get_timerange
|
from freqtrade.data.history import get_timerange
|
||||||
from freqtrade.exceptions import DependencyException, OperationalException
|
from freqtrade.exceptions import DependencyException, OperationalException
|
||||||
from freqtrade.optimize.backtesting import Backtesting
|
from freqtrade.optimize.backtesting import Backtesting
|
||||||
|
from freqtrade.persistence import LocalTrade
|
||||||
from freqtrade.resolvers import StrategyResolver
|
from freqtrade.resolvers import StrategyResolver
|
||||||
from freqtrade.state import RunMode
|
from freqtrade.state import RunMode
|
||||||
from freqtrade.strategy.interface import SellType
|
from freqtrade.strategy.interface import SellType
|
||||||
@ -90,7 +90,6 @@ def simple_backtest(config, contour, mocker, testdatadir) -> None:
|
|||||||
assert isinstance(processed, dict)
|
assert isinstance(processed, dict)
|
||||||
results = backtesting.backtest(
|
results = backtesting.backtest(
|
||||||
processed=processed,
|
processed=processed,
|
||||||
stake_amount=config['stake_amount'],
|
|
||||||
start_date=min_date,
|
start_date=min_date,
|
||||||
end_date=max_date,
|
end_date=max_date,
|
||||||
max_open_trades=1,
|
max_open_trades=1,
|
||||||
@ -111,7 +110,6 @@ def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC'):
|
|||||||
min_date, max_date = get_timerange(processed)
|
min_date, max_date = get_timerange(processed)
|
||||||
return {
|
return {
|
||||||
'processed': processed,
|
'processed': processed,
|
||||||
'stake_amount': conf['stake_amount'],
|
|
||||||
'start_date': min_date,
|
'start_date': min_date,
|
||||||
'end_date': max_date,
|
'end_date': max_date,
|
||||||
'max_open_trades': 10,
|
'max_open_trades': 10,
|
||||||
@ -233,8 +231,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) ->
|
|||||||
assert log_has('Parameter --fee detected, setting fee to: {} ...'.format(config['fee']), caplog)
|
assert log_has('Parameter --fee detected, setting fee to: {} ...'.format(config['fee']), caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_setup_optimize_configuration_unlimited_stake_amount(mocker, default_conf, caplog) -> None:
|
def test_setup_optimize_configuration_stake_amount(mocker, default_conf, caplog) -> None:
|
||||||
default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
|
|
||||||
|
|
||||||
patched_configuration_load_config_file(mocker, default_conf)
|
patched_configuration_load_config_file(mocker, default_conf)
|
||||||
|
|
||||||
@ -242,9 +239,21 @@ def test_setup_optimize_configuration_unlimited_stake_amount(mocker, default_con
|
|||||||
'backtesting',
|
'backtesting',
|
||||||
'--config', 'config.json',
|
'--config', 'config.json',
|
||||||
'--strategy', 'DefaultStrategy',
|
'--strategy', 'DefaultStrategy',
|
||||||
|
'--stake-amount', '1',
|
||||||
|
'--starting-balance', '2'
|
||||||
]
|
]
|
||||||
|
|
||||||
with pytest.raises(DependencyException, match=r'.`stake_amount`.*'):
|
conf = setup_optimize_configuration(get_args(args), RunMode.BACKTEST)
|
||||||
|
assert isinstance(conf, dict)
|
||||||
|
|
||||||
|
args = [
|
||||||
|
'backtesting',
|
||||||
|
'--config', 'config.json',
|
||||||
|
'--strategy', 'DefaultStrategy',
|
||||||
|
'--stake-amount', '1',
|
||||||
|
'--starting-balance', '0.5'
|
||||||
|
]
|
||||||
|
with pytest.raises(OperationalException, match=r"Starting balance .* smaller .*"):
|
||||||
setup_optimize_configuration(get_args(args), RunMode.BACKTEST)
|
setup_optimize_configuration(get_args(args), RunMode.BACKTEST)
|
||||||
|
|
||||||
|
|
||||||
@ -448,9 +457,48 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti
|
|||||||
Backtesting(default_conf)
|
Backtesting(default_conf)
|
||||||
|
|
||||||
|
|
||||||
|
def test_backtest__enter_trade(default_conf, fee, mocker, testdatadir) -> None:
|
||||||
|
default_conf['ask_strategy']['use_sell_signal'] = False
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
|
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
default_conf['stake_amount'] = 'unlimited'
|
||||||
|
backtesting = Backtesting(default_conf)
|
||||||
|
pair = 'UNITTEST/BTC'
|
||||||
|
row = [
|
||||||
|
pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0),
|
||||||
|
1, # Sell
|
||||||
|
0.001, # Open
|
||||||
|
0.0011, # Close
|
||||||
|
0, # Sell
|
||||||
|
0.00099, # Low
|
||||||
|
0.0012, # High
|
||||||
|
]
|
||||||
|
trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=0)
|
||||||
|
assert isinstance(trade, LocalTrade)
|
||||||
|
assert trade.stake_amount == 495
|
||||||
|
|
||||||
|
trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=2)
|
||||||
|
assert trade is None
|
||||||
|
|
||||||
|
# Stake-amount too high!
|
||||||
|
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=600.0)
|
||||||
|
|
||||||
|
trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=0)
|
||||||
|
assert trade is None
|
||||||
|
|
||||||
|
# Stake-amount too high!
|
||||||
|
mocker.patch("freqtrade.wallets.Wallets.get_trade_stake_amount",
|
||||||
|
side_effect=DependencyException)
|
||||||
|
|
||||||
|
trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=0)
|
||||||
|
assert trade is None
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
|
def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
|
||||||
default_conf['ask_strategy']['use_sell_signal'] = False
|
default_conf['ask_strategy']['use_sell_signal'] = False
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
|
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
pair = 'UNITTEST/BTC'
|
pair = 'UNITTEST/BTC'
|
||||||
@ -461,7 +509,6 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
|
|||||||
min_date, max_date = get_timerange(processed)
|
min_date, max_date = get_timerange(processed)
|
||||||
results = backtesting.backtest(
|
results = backtesting.backtest(
|
||||||
processed=processed,
|
processed=processed,
|
||||||
stake_amount=default_conf['stake_amount'],
|
|
||||||
start_date=min_date,
|
start_date=min_date,
|
||||||
end_date=max_date,
|
end_date=max_date,
|
||||||
max_open_trades=10,
|
max_open_trades=10,
|
||||||
@ -486,7 +533,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
|
|||||||
'trade_duration': [235, 40],
|
'trade_duration': [235, 40],
|
||||||
'profit_ratio': [0.0, 0.0],
|
'profit_ratio': [0.0, 0.0],
|
||||||
'profit_abs': [0.0, 0.0],
|
'profit_abs': [0.0, 0.0],
|
||||||
'sell_reason': [SellType.ROI, SellType.ROI],
|
'sell_reason': [SellType.ROI.value, SellType.ROI.value],
|
||||||
'initial_stop_loss_abs': [0.0940005, 0.09272236],
|
'initial_stop_loss_abs': [0.0940005, 0.09272236],
|
||||||
'initial_stop_loss_ratio': [-0.1, -0.1],
|
'initial_stop_loss_ratio': [-0.1, -0.1],
|
||||||
'stop_loss_abs': [0.0940005, 0.09272236],
|
'stop_loss_abs': [0.0940005, 0.09272236],
|
||||||
@ -512,6 +559,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
|
|||||||
def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None:
|
def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None:
|
||||||
default_conf['ask_strategy']['use_sell_signal'] = False
|
default_conf['ask_strategy']['use_sell_signal'] = False
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
|
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
|
|
||||||
@ -523,7 +571,6 @@ def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None
|
|||||||
min_date, max_date = get_timerange(processed)
|
min_date, max_date = get_timerange(processed)
|
||||||
results = backtesting.backtest(
|
results = backtesting.backtest(
|
||||||
processed=processed,
|
processed=processed,
|
||||||
stake_amount=default_conf['stake_amount'],
|
|
||||||
start_date=min_date,
|
start_date=min_date,
|
||||||
end_date=max_date,
|
end_date=max_date,
|
||||||
max_open_trades=1,
|
max_open_trades=1,
|
||||||
@ -558,6 +605,7 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad
|
|||||||
|
|
||||||
default_conf['enable_protections'] = True
|
default_conf['enable_protections'] = True
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
|
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||||
tests = [
|
tests = [
|
||||||
['sine', 9],
|
['sine', 9],
|
||||||
['raise', 10],
|
['raise', 10],
|
||||||
@ -589,6 +637,7 @@ def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir,
|
|||||||
default_conf['protections'] = protections
|
default_conf['protections'] = protections
|
||||||
default_conf['enable_protections'] = True
|
default_conf['enable_protections'] = True
|
||||||
|
|
||||||
|
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
# While buy-signals are unrealistic, running backtesting
|
# While buy-signals are unrealistic, running backtesting
|
||||||
# over and over again should not cause different results
|
# over and over again should not cause different results
|
||||||
@ -626,6 +675,7 @@ def test_backtest_only_sell(mocker, default_conf, testdatadir):
|
|||||||
|
|
||||||
|
|
||||||
def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir):
|
def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir):
|
||||||
|
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
backtest_conf = _make_backtest_conf(mocker, conf=default_conf,
|
backtest_conf = _make_backtest_conf(mocker, conf=default_conf,
|
||||||
pair='UNITTEST/BTC', datadir=testdatadir)
|
pair='UNITTEST/BTC', datadir=testdatadir)
|
||||||
@ -658,6 +708,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
|
|||||||
dataframe['sell'] = np.where((dataframe.index + multi - 2) % multi == 0, 1, 0)
|
dataframe['sell'] = np.where((dataframe.index + multi - 2) % multi == 0, 1, 0)
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
|
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
|
|
||||||
@ -678,7 +729,6 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
|
|||||||
min_date, max_date = get_timerange(processed)
|
min_date, max_date = get_timerange(processed)
|
||||||
backtest_conf = {
|
backtest_conf = {
|
||||||
'processed': processed,
|
'processed': processed,
|
||||||
'stake_amount': default_conf['stake_amount'],
|
|
||||||
'start_date': min_date,
|
'start_date': min_date,
|
||||||
'end_date': max_date,
|
'end_date': max_date,
|
||||||
'max_open_trades': 3,
|
'max_open_trades': 3,
|
||||||
@ -694,7 +744,6 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
|
|||||||
|
|
||||||
backtest_conf = {
|
backtest_conf = {
|
||||||
'processed': processed,
|
'processed': processed,
|
||||||
'stake_amount': default_conf['stake_amount'],
|
|
||||||
'start_date': min_date,
|
'start_date': min_date,
|
||||||
'end_date': max_date,
|
'end_date': max_date,
|
||||||
'max_open_trades': 1,
|
'max_open_trades': 1,
|
||||||
@ -822,6 +871,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
|
|||||||
'2018-01-30 05:35:00', ], utc=True),
|
'2018-01-30 05:35:00', ], utc=True),
|
||||||
'trade_duration': [235, 40],
|
'trade_duration': [235, 40],
|
||||||
'is_open': [False, False],
|
'is_open': [False, False],
|
||||||
|
'stake_amount': [0.01, 0.01],
|
||||||
'open_rate': [0.104445, 0.10302485],
|
'open_rate': [0.104445, 0.10302485],
|
||||||
'close_rate': [0.104969, 0.103541],
|
'close_rate': [0.104969, 0.103541],
|
||||||
'sell_reason': [SellType.ROI, SellType.ROI]
|
'sell_reason': [SellType.ROI, SellType.ROI]
|
||||||
@ -838,6 +888,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
|
|||||||
'2018-01-30 08:30:00'], utc=True),
|
'2018-01-30 08:30:00'], utc=True),
|
||||||
'trade_duration': [47, 40, 20],
|
'trade_duration': [47, 40, 20],
|
||||||
'is_open': [False, False, False],
|
'is_open': [False, False, False],
|
||||||
|
'stake_amount': [0.01, 0.01, 0.01],
|
||||||
'open_rate': [0.104445, 0.10302485, 0.122541],
|
'open_rate': [0.104445, 0.10302485, 0.122541],
|
||||||
'close_rate': [0.104969, 0.103541, 0.123541],
|
'close_rate': [0.104969, 0.103541, 0.123541],
|
||||||
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]
|
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]
|
||||||
|
@ -12,10 +12,9 @@ import pytest
|
|||||||
from arrow import Arrow
|
from arrow import Arrow
|
||||||
from filelock import Timeout
|
from filelock import Timeout
|
||||||
|
|
||||||
from freqtrade import constants
|
|
||||||
from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt
|
from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt
|
||||||
from freqtrade.data.history import load_data
|
from freqtrade.data.history import load_data
|
||||||
from freqtrade.exceptions import DependencyException, OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.optimize.hyperopt import Hyperopt
|
from freqtrade.optimize.hyperopt import Hyperopt
|
||||||
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver
|
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver
|
||||||
from freqtrade.state import RunMode
|
from freqtrade.state import RunMode
|
||||||
@ -130,8 +129,7 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo
|
|||||||
assert log_has('Parameter --print-all detected ...', caplog)
|
assert log_has('Parameter --print-all detected ...', caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_setup_hyperopt_configuration_unlimited_stake_amount(mocker, default_conf) -> None:
|
def test_setup_hyperopt_configuration_stake_amount(mocker, default_conf) -> None:
|
||||||
default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
|
|
||||||
|
|
||||||
patched_configuration_load_config_file(mocker, default_conf)
|
patched_configuration_load_config_file(mocker, default_conf)
|
||||||
|
|
||||||
@ -139,9 +137,20 @@ def test_setup_hyperopt_configuration_unlimited_stake_amount(mocker, default_con
|
|||||||
'hyperopt',
|
'hyperopt',
|
||||||
'--config', 'config.json',
|
'--config', 'config.json',
|
||||||
'--hyperopt', 'DefaultHyperOpt',
|
'--hyperopt', 'DefaultHyperOpt',
|
||||||
|
'--stake-amount', '1',
|
||||||
|
'--starting-balance', '2'
|
||||||
]
|
]
|
||||||
|
conf = setup_optimize_configuration(get_args(args), RunMode.HYPEROPT)
|
||||||
|
assert isinstance(conf, dict)
|
||||||
|
|
||||||
with pytest.raises(DependencyException, match=r'.`stake_amount`.*'):
|
args = [
|
||||||
|
'hyperopt',
|
||||||
|
'--config', 'config.json',
|
||||||
|
'--strategy', 'DefaultStrategy',
|
||||||
|
'--stake-amount', '1',
|
||||||
|
'--starting-balance', '0.5'
|
||||||
|
]
|
||||||
|
with pytest.raises(OperationalException, match=r"Starting balance .* smaller .*"):
|
||||||
setup_optimize_configuration(get_args(args), RunMode.HYPEROPT)
|
setup_optimize_configuration(get_args(args), RunMode.HYPEROPT)
|
||||||
|
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ def test_text_table_bt_results():
|
|||||||
)
|
)
|
||||||
|
|
||||||
pair_results = generate_pair_metrics(data={'ETH/BTC': {}}, stake_currency='BTC',
|
pair_results = generate_pair_metrics(data={'ETH/BTC': {}}, stake_currency='BTC',
|
||||||
max_open_trades=2, results=results)
|
starting_balance=4, results=results)
|
||||||
assert text_table_bt_results(pair_results, stake_currency='BTC') == result_str
|
assert text_table_bt_results(pair_results, stake_currency='BTC') == result_str
|
||||||
|
|
||||||
|
|
||||||
@ -73,11 +73,13 @@ def test_generate_backtest_stats(default_conf, testdatadir):
|
|||||||
"close_rate": [0.002546, 0.003014, 0.003103, 0.003217],
|
"close_rate": [0.002546, 0.003014, 0.003103, 0.003217],
|
||||||
"trade_duration": [123, 34, 31, 14],
|
"trade_duration": [123, 34, 31, 14],
|
||||||
"is_open": [False, False, False, True],
|
"is_open": [False, False, False, True],
|
||||||
|
"stake_amount": [0.01, 0.01, 0.01, 0.01],
|
||||||
"sell_reason": [SellType.ROI, SellType.STOP_LOSS,
|
"sell_reason": [SellType.ROI, SellType.STOP_LOSS,
|
||||||
SellType.ROI, SellType.FORCE_SELL]
|
SellType.ROI, SellType.FORCE_SELL]
|
||||||
}),
|
}),
|
||||||
'config': default_conf,
|
'config': default_conf,
|
||||||
'locks': [],
|
'locks': [],
|
||||||
|
'final_balance': 1000.02,
|
||||||
'backtest_start_time': Arrow.utcnow().int_timestamp,
|
'backtest_start_time': Arrow.utcnow().int_timestamp,
|
||||||
'backtest_end_time': Arrow.utcnow().int_timestamp,
|
'backtest_end_time': Arrow.utcnow().int_timestamp,
|
||||||
}
|
}
|
||||||
@ -100,6 +102,7 @@ def test_generate_backtest_stats(default_conf, testdatadir):
|
|||||||
# Above sample had no loosing trade
|
# Above sample had no loosing trade
|
||||||
assert strat_stats['max_drawdown'] == 0.0
|
assert strat_stats['max_drawdown'] == 0.0
|
||||||
|
|
||||||
|
# Retry with losing trade
|
||||||
results = {'DefStrat': {
|
results = {'DefStrat': {
|
||||||
'results': pd.DataFrame(
|
'results': pd.DataFrame(
|
||||||
{"pair": ["UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC"],
|
{"pair": ["UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC"],
|
||||||
@ -116,18 +119,31 @@ def test_generate_backtest_stats(default_conf, testdatadir):
|
|||||||
"open_rate": [0.002543, 0.003003, 0.003089, 0.003214],
|
"open_rate": [0.002543, 0.003003, 0.003089, 0.003214],
|
||||||
"close_rate": [0.002546, 0.003014, 0.0032903, 0.003217],
|
"close_rate": [0.002546, 0.003014, 0.0032903, 0.003217],
|
||||||
"trade_duration": [123, 34, 31, 14],
|
"trade_duration": [123, 34, 31, 14],
|
||||||
"open_at_end": [False, False, False, True],
|
"is_open": [False, False, False, True],
|
||||||
"sell_reason": [SellType.ROI, SellType.STOP_LOSS,
|
"stake_amount": [0.01, 0.01, 0.01, 0.01],
|
||||||
SellType.ROI, SellType.FORCE_SELL]
|
"sell_reason": [SellType.ROI, SellType.ROI,
|
||||||
|
SellType.STOP_LOSS, SellType.FORCE_SELL]
|
||||||
}),
|
}),
|
||||||
'config': default_conf}
|
'config': default_conf,
|
||||||
|
'locks': [],
|
||||||
|
'final_balance': 1000.02,
|
||||||
|
'backtest_start_time': Arrow.utcnow().int_timestamp,
|
||||||
|
'backtest_end_time': Arrow.utcnow().int_timestamp,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assert strat_stats['max_drawdown'] == 0.0
|
stats = generate_backtest_stats(btdata, results, min_date, max_date)
|
||||||
assert strat_stats['drawdown_start'] == datetime(1970, 1, 1, tzinfo=timezone.utc)
|
assert isinstance(stats, dict)
|
||||||
assert strat_stats['drawdown_end'] == datetime(1970, 1, 1, tzinfo=timezone.utc)
|
assert 'strategy' in stats
|
||||||
assert strat_stats['drawdown_end_ts'] == 0
|
assert 'DefStrat' in stats['strategy']
|
||||||
assert strat_stats['drawdown_start_ts'] == 0
|
assert 'strategy_comparison' in stats
|
||||||
|
strat_stats = stats['strategy']['DefStrat']
|
||||||
|
|
||||||
|
assert strat_stats['max_drawdown'] == 0.013803
|
||||||
|
assert strat_stats['drawdown_start'] == datetime(2017, 11, 14, 22, 10, tzinfo=timezone.utc)
|
||||||
|
assert strat_stats['drawdown_end'] == datetime(2017, 11, 14, 22, 43, tzinfo=timezone.utc)
|
||||||
|
assert strat_stats['drawdown_end_ts'] == 1510699380000
|
||||||
|
assert strat_stats['drawdown_start_ts'] == 1510697400000
|
||||||
assert strat_stats['pairlist'] == ['UNITTEST/BTC']
|
assert strat_stats['pairlist'] == ['UNITTEST/BTC']
|
||||||
|
|
||||||
# Test storing stats
|
# Test storing stats
|
||||||
@ -189,7 +205,7 @@ def test_generate_pair_metrics():
|
|||||||
)
|
)
|
||||||
|
|
||||||
pair_results = generate_pair_metrics(data={'ETH/BTC': {}}, stake_currency='BTC',
|
pair_results = generate_pair_metrics(data={'ETH/BTC': {}}, stake_currency='BTC',
|
||||||
max_open_trades=2, results=results)
|
starting_balance=2, results=results)
|
||||||
assert isinstance(pair_results, list)
|
assert isinstance(pair_results, list)
|
||||||
assert len(pair_results) == 2
|
assert len(pair_results) == 2
|
||||||
assert pair_results[-1]['key'] == 'TOTAL'
|
assert pair_results[-1]['key'] == 'TOTAL'
|
||||||
@ -265,7 +281,7 @@ def test_generate_sell_reason_stats():
|
|||||||
'wins': [2, 0, 0],
|
'wins': [2, 0, 0],
|
||||||
'draws': [0, 0, 0],
|
'draws': [0, 0, 0],
|
||||||
'losses': [0, 0, 1],
|
'losses': [0, 0, 1],
|
||||||
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]
|
'sell_reason': [SellType.ROI.value, SellType.ROI.value, SellType.STOP_LOSS.value]
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -291,6 +307,7 @@ def test_generate_sell_reason_stats():
|
|||||||
|
|
||||||
def test_text_table_strategy(default_conf):
|
def test_text_table_strategy(default_conf):
|
||||||
default_conf['max_open_trades'] = 2
|
default_conf['max_open_trades'] = 2
|
||||||
|
default_conf['dry_run_wallet'] = 3
|
||||||
results = {}
|
results = {}
|
||||||
results['TestStrategy1'] = {'results': pd.DataFrame(
|
results['TestStrategy1'] = {'results': pd.DataFrame(
|
||||||
{
|
{
|
||||||
@ -323,9 +340,9 @@ def test_text_table_strategy(default_conf):
|
|||||||
'|---------------+--------+----------------+----------------+------------------+'
|
'|---------------+--------+----------------+----------------+------------------+'
|
||||||
'----------------+----------------+--------+---------+----------|\n'
|
'----------------+----------------+--------+---------+----------|\n'
|
||||||
'| TestStrategy1 | 3 | 20.00 | 60.00 | 1.10000000 |'
|
'| TestStrategy1 | 3 | 20.00 | 60.00 | 1.10000000 |'
|
||||||
' 30.00 | 0:17:00 | 3 | 0 | 0 |\n'
|
' 36.67 | 0:17:00 | 3 | 0 | 0 |\n'
|
||||||
'| TestStrategy2 | 3 | 30.00 | 90.00 | 1.30000000 |'
|
'| TestStrategy2 | 3 | 30.00 | 90.00 | 1.30000000 |'
|
||||||
' 45.00 | 0:20:00 | 3 | 0 | 0 |'
|
' 43.33 | 0:20:00 | 3 | 0 | 0 |'
|
||||||
)
|
)
|
||||||
|
|
||||||
strategy_results = generate_strategy_metrics(all_results=results)
|
strategy_results = generate_strategy_metrics(all_results=results)
|
||||||
|
@ -73,9 +73,13 @@ def test_PairLocks(use_db):
|
|||||||
assert PairLocks.is_pair_locked('XRP/USDT', lock_time + timedelta(minutes=-50))
|
assert PairLocks.is_pair_locked('XRP/USDT', lock_time + timedelta(minutes=-50))
|
||||||
|
|
||||||
if use_db:
|
if use_db:
|
||||||
assert len(PairLock.query.all()) > 0
|
locks = PairLocks.get_all_locks()
|
||||||
|
locks_db = PairLock.query.all()
|
||||||
|
assert len(locks) == len(locks_db)
|
||||||
|
assert len(locks_db) > 0
|
||||||
else:
|
else:
|
||||||
# Nothing was pushed to the database
|
# Nothing was pushed to the database
|
||||||
|
assert len(PairLocks.get_all_locks()) > 0
|
||||||
assert len(PairLock.query.all()) == 0
|
assert len(PairLock.query.all()) == 0
|
||||||
# Reset use-db variable
|
# Reset use-db variable
|
||||||
PairLocks.reset_locks()
|
PairLocks.reset_locks()
|
||||||
|
@ -430,7 +430,8 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
|||||||
'--enable-position-stacking',
|
'--enable-position-stacking',
|
||||||
'--disable-max-market-positions',
|
'--disable-max-market-positions',
|
||||||
'--timerange', ':100',
|
'--timerange', ':100',
|
||||||
'--export', '/bar/foo'
|
'--export', '/bar/foo',
|
||||||
|
'--stake-amount', 'unlimited'
|
||||||
]
|
]
|
||||||
|
|
||||||
args = Arguments(arglist).get_parsed_arg()
|
args = Arguments(arglist).get_parsed_arg()
|
||||||
@ -463,6 +464,8 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
|||||||
|
|
||||||
assert 'export' in config
|
assert 'export' in config
|
||||||
assert log_has('Parameter --export detected: {} ...'.format(config['export']), caplog)
|
assert log_has('Parameter --export detected: {} ...'.format(config['export']), caplog)
|
||||||
|
assert 'stake_amount' in config
|
||||||
|
assert config['stake_amount'] == 'unlimited'
|
||||||
|
|
||||||
|
|
||||||
def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> None:
|
def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> None:
|
||||||
|
@ -2243,6 +2243,7 @@ def test_check_handle_timedout_sell_usercustom(default_conf, ticker, limit_sell_
|
|||||||
|
|
||||||
open_trade.open_date = arrow.utcnow().shift(hours=-5).datetime
|
open_trade.open_date = arrow.utcnow().shift(hours=-5).datetime
|
||||||
open_trade.close_date = arrow.utcnow().shift(minutes=-601).datetime
|
open_trade.close_date = arrow.utcnow().shift(minutes=-601).datetime
|
||||||
|
open_trade.close_profit_abs = 0.001
|
||||||
open_trade.is_open = False
|
open_trade.is_open = False
|
||||||
|
|
||||||
Trade.session.add(open_trade)
|
Trade.session.add(open_trade)
|
||||||
@ -2290,6 +2291,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old,
|
|||||||
|
|
||||||
open_trade.open_date = arrow.utcnow().shift(hours=-5).datetime
|
open_trade.open_date = arrow.utcnow().shift(hours=-5).datetime
|
||||||
open_trade.close_date = arrow.utcnow().shift(minutes=-601).datetime
|
open_trade.close_date = arrow.utcnow().shift(minutes=-601).datetime
|
||||||
|
open_trade.close_profit_abs = 0.001
|
||||||
open_trade.is_open = False
|
open_trade.is_open = False
|
||||||
|
|
||||||
Trade.session.add(open_trade)
|
Trade.session.add(open_trade)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
# pragma pylint: disable=missing-docstring, C0103
|
# pragma pylint: disable=missing-docstring, C0103
|
||||||
import logging
|
import logging
|
||||||
|
from types import FunctionType
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
@ -8,7 +9,7 @@ from sqlalchemy import create_engine
|
|||||||
|
|
||||||
from freqtrade import constants
|
from freqtrade import constants
|
||||||
from freqtrade.exceptions import DependencyException, OperationalException
|
from freqtrade.exceptions import DependencyException, OperationalException
|
||||||
from freqtrade.persistence import Order, Trade, clean_dry_run_db, init_db
|
from freqtrade.persistence import LocalTrade, Order, Trade, clean_dry_run_db, init_db
|
||||||
from tests.conftest import create_mock_trades, log_has, log_has_re
|
from tests.conftest import create_mock_trades, log_has, log_has_re
|
||||||
|
|
||||||
|
|
||||||
@ -1039,14 +1040,18 @@ def test_fee_updated(fee):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_total_open_trades_stakes(fee):
|
@pytest.mark.parametrize('use_db', [True, False])
|
||||||
|
def test_total_open_trades_stakes(fee, use_db):
|
||||||
|
|
||||||
|
Trade.use_db = use_db
|
||||||
res = Trade.total_open_trades_stakes()
|
res = Trade.total_open_trades_stakes()
|
||||||
assert res == 0
|
assert res == 0
|
||||||
create_mock_trades(fee)
|
create_mock_trades(fee, use_db)
|
||||||
res = Trade.total_open_trades_stakes()
|
res = Trade.total_open_trades_stakes()
|
||||||
assert res == 0.004
|
assert res == 0.004
|
||||||
|
|
||||||
|
Trade.use_db = True
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_get_overall_performance(fee):
|
def test_get_overall_performance(fee):
|
||||||
@ -1172,3 +1177,25 @@ def test_select_order(fee):
|
|||||||
assert order.ft_order_side == 'stoploss'
|
assert order.ft_order_side == 'stoploss'
|
||||||
order = trades[4].select_order('sell', False)
|
order = trades[4].select_order('sell', False)
|
||||||
assert order is None
|
assert order is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_Trade_object_idem():
|
||||||
|
|
||||||
|
assert issubclass(Trade, LocalTrade)
|
||||||
|
|
||||||
|
trade = vars(Trade)
|
||||||
|
localtrade = vars(LocalTrade)
|
||||||
|
|
||||||
|
# Parent (LocalTrade) should have the same attributes
|
||||||
|
for item in trade:
|
||||||
|
# Exclude private attributes and open_date (as it's not assigned a default)
|
||||||
|
if (not item.startswith('_')
|
||||||
|
and item not in ('delete', 'session', 'query', 'open_date')):
|
||||||
|
assert item in localtrade
|
||||||
|
|
||||||
|
# Fails if only a column is added without corresponding parent field
|
||||||
|
for item in localtrade:
|
||||||
|
if (not item.startswith('__')
|
||||||
|
and item not in ('trades', )
|
||||||
|
and type(getattr(LocalTrade, item)) not in (property, FunctionType)):
|
||||||
|
assert item in trade
|
||||||
|
Loading…
Reference in New Issue
Block a user