Merge branch 'develop' into improve-doc
This commit is contained in:
commit
89e0e552bb
@ -53,7 +53,6 @@
|
|||||||
],
|
],
|
||||||
"freqai": {
|
"freqai": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"startup_candles": 10000,
|
|
||||||
"purge_old_models": true,
|
"purge_old_models": true,
|
||||||
"train_period_days": 15,
|
"train_period_days": 15,
|
||||||
"backtest_period_days": 7,
|
"backtest_period_days": 7,
|
||||||
@ -75,8 +74,10 @@
|
|||||||
"weight_factor": 0.9,
|
"weight_factor": 0.9,
|
||||||
"principal_component_analysis": false,
|
"principal_component_analysis": false,
|
||||||
"use_SVM_to_remove_outliers": true,
|
"use_SVM_to_remove_outliers": true,
|
||||||
"indicator_max_period_candles": 20,
|
"indicator_periods_candles": [
|
||||||
"indicator_periods_candles": [10, 20]
|
10,
|
||||||
|
20
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"data_split_parameters": {
|
"data_split_parameters": {
|
||||||
"test_size": 0.33,
|
"test_size": 0.33,
|
||||||
|
@ -61,8 +61,8 @@ Binance supports [time_in_force](configuration.md#understand-order_time_in_force
|
|||||||
|
|
||||||
### Binance Blacklist
|
### Binance Blacklist
|
||||||
|
|
||||||
For Binance, please add `"BNB/<STAKE>"` to your blacklist to avoid issues.
|
For Binance, it is suggested to add `"BNB/<STAKE>"` to your blacklist to avoid issues, unless you are willing to maintain enough extra `BNB` on the account or unless you're willing to disable using `BNB` for fees.
|
||||||
Accounts having BNB accounts use this to pay for fees - if your first trade happens to be on `BNB`, further trades will consume this position and make the initial BNB trade unsellable as the expected amount is not there anymore.
|
Binance accounts may use `BNB` for fees, and if a trade happens to be on `BNB`, further trades may consume this position and make the initial BNB trade unsellable as the expected amount is not there anymore.
|
||||||
|
|
||||||
### Binance Futures
|
### Binance Futures
|
||||||
|
|
||||||
@ -205,8 +205,8 @@ Kucoin supports [time_in_force](configuration.md#understand-order_time_in_force)
|
|||||||
|
|
||||||
### Kucoin Blacklists
|
### Kucoin Blacklists
|
||||||
|
|
||||||
For Kucoin, please add `"KCS/<STAKE>"` to your blacklist to avoid issues.
|
For Kucoin, it is suggested to add `"KCS/<STAKE>"` to your blacklist to avoid issues, unless you are willing to maintain enough extra `KCS` on the account or unless you're willing to disable using `KCS` for fees.
|
||||||
Accounts having KCS accounts use this to pay for fees - if your first trade happens to be on `KCS`, further trades will consume this position and make the initial KCS trade unsellable as the expected amount is not there anymore.
|
Kucoin accounts may use `KCS` for fees, and if a trade happens to be on `KCS`, further trades may consume this position and make the initial `KCS` trade unsellable as the expected amount is not there anymore.
|
||||||
|
|
||||||
## Huobi
|
## Huobi
|
||||||
|
|
||||||
|
@ -89,10 +89,10 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
|------------|-------------|
|
|------------|-------------|
|
||||||
| | **General configuration parameters**
|
| | **General configuration parameters**
|
||||||
| `freqai` | **Required.** <br> The parent dictionary containing all the parameters for controlling FreqAI. <br> **Datatype:** Dictionary.
|
| `freqai` | **Required.** <br> The parent dictionary containing all the parameters for controlling FreqAI. <br> **Datatype:** Dictionary.
|
||||||
| `startup_candles` | Number of candles needed for *backtesting only* to ensure all indicators are non NaNs at the start of the first train period. <br> **Datatype:** Positive integer.
|
|
||||||
| `purge_old_models` | Delete obsolete models (otherwise, all historic models will remain on disk). <br> **Datatype:** Boolean. Default: `False`.
|
| `purge_old_models` | Delete obsolete models (otherwise, all historic models will remain on disk). <br> **Datatype:** Boolean. Default: `False`.
|
||||||
| `train_period_days` | **Required.** <br> Number of days to use for the training data (width of the sliding window). <br> **Datatype:** Positive integer.
|
| `train_period_days` | **Required.** <br> Number of days to use for the training data (width of the sliding window). <br> **Datatype:** Positive integer.
|
||||||
| `backtest_period_days` | **Required.** <br> Number of days to inference from the trained model before sliding the window defined above, and retraining the model. This can be fractional days, but beware that the user-provided `timerange` will be divided by this number to yield the number of trainings necessary to complete the backtest. <br> **Datatype:** Float.
|
| `backtest_period_days` | **Required.** <br> Number of days to inference from the trained model before sliding the window defined above, and retraining the model. This can be fractional days, but beware that the user-provided `timerange` will be divided by this number to yield the number of trainings necessary to complete the backtest. <br> **Datatype:** Float.
|
||||||
|
| `save_backtest_models` | Backtesting operates most efficiently by saving the prediction data and reusing them directly for subsequent runs (when users wish to tune entry/exit parameters). If a user wishes to save models to disk when running backtesting, they should activate `save_backtest_models`. A user may wish to do this if they plan to use the same model files for starting a dry/live instance with the same `identifier`. <br> **Datatype:** Boolean. Default: `False`.
|
||||||
| `identifier` | **Required.** <br> A unique name for the current model. This can be reused to reload pre-trained models/data. <br> **Datatype:** String.
|
| `identifier` | **Required.** <br> A unique name for the current model. This can be reused to reload pre-trained models/data. <br> **Datatype:** String.
|
||||||
| `live_retrain_hours` | Frequency of retraining during dry/live runs. <br> Default set to 0, which means the model will retrain as often as possible. <br> **Datatype:** Float > 0.
|
| `live_retrain_hours` | Frequency of retraining during dry/live runs. <br> Default set to 0, which means the model will retrain as often as possible. <br> **Datatype:** Float > 0.
|
||||||
| `expiration_hours` | Avoid making predictions if a model is more than `expiration_hours` old. <br> Defaults set to 0, which means models never expire. <br> **Datatype:** Positive integer.
|
| `expiration_hours` | Avoid making predictions if a model is more than `expiration_hours` old. <br> Defaults set to 0, which means models never expire. <br> **Datatype:** Positive integer.
|
||||||
@ -105,7 +105,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
| `label_period_candles` | Number of candles into the future that the labels are created for. This is used in `populate_any_indicators` (see `templates/FreqaiExampleStrategy.py` for detailed usage). The user can create custom labels, making use of this parameter or not. <br> **Datatype:** Positive integer.
|
| `label_period_candles` | Number of candles into the future that the labels are created for. This is used in `populate_any_indicators` (see `templates/FreqaiExampleStrategy.py` for detailed usage). The user can create custom labels, making use of this parameter or not. <br> **Datatype:** Positive integer.
|
||||||
| `include_shifted_candles` | Add features from previous candles to subsequent candles to add historical information. FreqAI takes all features from the `include_shifted_candles` previous candles, duplicates and shifts them so that the information is available for the subsequent candle. <br> **Datatype:** Positive integer.
|
| `include_shifted_candles` | Add features from previous candles to subsequent candles to add historical information. FreqAI takes all features from the `include_shifted_candles` previous candles, duplicates and shifts them so that the information is available for the subsequent candle. <br> **Datatype:** Positive integer.
|
||||||
| `weight_factor` | Used to set weights for training data points according to their recency. See details about how it works [here](#controlling-the-model-learning-process). <br> **Datatype:** Positive float (typically < 1).
|
| `weight_factor` | Used to set weights for training data points according to their recency. See details about how it works [here](#controlling-the-model-learning-process). <br> **Datatype:** Positive float (typically < 1).
|
||||||
| `indicator_max_period_candles` | The maximum period used in `populate_any_indicators()` for indicator creation. FreqAI uses this information in combination with the maximum timeframe to calculate how many data points that should be downloaded so that the first data point does not have a NaN. <br> **Datatype:** Positive integer.
|
| `indicator_max_period_candles` | **No longer used**. User must use the strategy set `startup_candle_count` which defines the maximum *period* used in `populate_any_indicators()` for indicator creation (timeframe independent). FreqAI uses this information in combination with the maximum timeframe to calculate how many data points it should download so that the first data point does not have a NaN <br> **Datatype:** positive integer.
|
||||||
| `indicator_periods_candles` | Calculate indicators for `indicator_periods_candles` time periods and add them to the feature set. <br> **Datatype:** List of positive integers.
|
| `indicator_periods_candles` | Calculate indicators for `indicator_periods_candles` time periods and add them to the feature set. <br> **Datatype:** List of positive integers.
|
||||||
| `stratify_training_data` | This value is used to indicate the grouping of the data. For example, 2 would set every 2nd data point into a separate dataset to be pulled from during training/testing. See details about how it works [here](#stratifying-the-data-for-training-and-testing-the-model) <br> **Datatype:** Positive integer.
|
| `stratify_training_data` | This value is used to indicate the grouping of the data. For example, 2 would set every 2nd data point into a separate dataset to be pulled from during training/testing. See details about how it works [here](#stratifying-the-data-for-training-and-testing-the-model) <br> **Datatype:** Positive integer.
|
||||||
| `principal_component_analysis` | Automatically reduce the dimensionality of the data set using Principal Component Analysis. See details about how it works [here](#reducing-data-dimensionality-with-principal-component-analysis) <br> **Datatype:** Boolean.
|
| `principal_component_analysis` | Automatically reduce the dimensionality of the data set using Principal Component Analysis. See details about how it works [here](#reducing-data-dimensionality-with-principal-component-analysis) <br> **Datatype:** Boolean.
|
||||||
@ -167,7 +167,6 @@ The user interface is isolated to the typical Freqtrade config file. A FreqAI co
|
|||||||
],
|
],
|
||||||
"label_period_candles": 24,
|
"label_period_candles": 24,
|
||||||
"include_shifted_candles": 2,
|
"include_shifted_candles": 2,
|
||||||
"indicator_max_period_candles": 20,
|
|
||||||
"indicator_periods_candles": [10, 20]
|
"indicator_periods_candles": [10, 20]
|
||||||
},
|
},
|
||||||
"data_split_parameters" : {
|
"data_split_parameters" : {
|
||||||
@ -184,6 +183,9 @@ The user interface is isolated to the typical Freqtrade config file. A FreqAI co
|
|||||||
The FreqAI strategy requires the user to include the following lines of code in the standard Freqtrade strategy:
|
The FreqAI strategy requires the user to include the following lines of code in the standard Freqtrade strategy:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
# user should define the maximum startup candle count (the largest number of candles
|
||||||
|
# passed to any single indicator)
|
||||||
|
startup_candle_count: int = 20
|
||||||
|
|
||||||
def informative_pairs(self):
|
def informative_pairs(self):
|
||||||
whitelist_pairs = self.dp.current_whitelist()
|
whitelist_pairs = self.dp.current_whitelist()
|
||||||
@ -277,6 +279,17 @@ The FreqAI strategy requires the user to include the following lines of code in
|
|||||||
|
|
||||||
Notice how the `populate_any_indicators()` is where the user adds their own features ([more information](#feature-engineering)) and labels ([more information](#setting-classifier-targets)). See a full example at `templates/FreqaiExampleStrategy.py`.
|
Notice how the `populate_any_indicators()` is where the user adds their own features ([more information](#feature-engineering)) and labels ([more information](#setting-classifier-targets)). See a full example at `templates/FreqaiExampleStrategy.py`.
|
||||||
|
|
||||||
|
### Setting the `startup_candle_count`
|
||||||
|
Users need to take care to set the `startup_candle_count` in their strategy the same way they would for any normal Freqtrade strategy (see details [here](strategy-customization.md/#strategy-startup-period)). This value is used by Freqtrade to ensure that a sufficient amount of data is provided when calling on the `dataprovider` to avoid any NaNs at the beginning of the first training. Users can easily set this value by identifying the longest period (in candle units) that they pass to their indicator creation functions (e.g. talib functions). In the present example, the user would pass 20 to as this value (since it is the maximum value in their `indicators_periods_candles`).
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
Typically it is best for users to be safe and multiply their expected `startup_candle_count` by 2. There are instances where the talib functions actually require more data than just the passed `period`. Anecdotally, multiplying the `startup_candle_count` by 2 always leads to a fully NaN free training dataset. Look out for this log message to confirm that your data is clean:
|
||||||
|
|
||||||
|
```
|
||||||
|
2022-08-31 15:14:04 - freqtrade.freqai.data_kitchen - INFO - dropped 0 training points due to NaNs in populated dataset 4319.
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Creating a dynamic target
|
## Creating a dynamic target
|
||||||
|
|
||||||
The `&*_std/mean` return values describe the statistical fit of the user defined label *during the most recent training*. This value allows the user to know the rarity of a given prediction. For example, `templates/FreqaiExampleStrategy.py`, creates a `target_roi` which is based on filtering out predictions that are below a given z-score of 1.25.
|
The `&*_std/mean` return values describe the statistical fit of the user defined label *during the most recent training*. This value allows the user to know the rarity of a given prediction. For example, `templates/FreqaiExampleStrategy.py`, creates a `target_roi` which is based on filtering out predictions that are below a given z-score of 1.25.
|
||||||
@ -502,7 +515,7 @@ and if a full `live_retrain_hours` has elapsed since the end of the loaded model
|
|||||||
The FreqAI backtesting module can be executed with the following command:
|
The FreqAI backtesting module can be executed with the following command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
freqtrade backtesting --strategy FreqaiExampleStrategy --config config_freqai.example.json --freqaimodel LightGBMRegressor --timerange 20210501-20210701
|
freqtrade backtesting --strategy FreqaiExampleStrategy --config config_examples/config_freqai.example.json --freqaimodel LightGBMRegressor --timerange 20210501-20210701
|
||||||
```
|
```
|
||||||
|
|
||||||
Backtesting mode requires the user to have the data pre-downloaded (unlike in dry/live mode where FreqAI automatically downloads the necessary data). The user should be careful to consider that the time range of the downloaded data is more than the backtesting time range. This is because FreqAI needs data prior to the desired backtesting time range in order to train a model to be ready to make predictions on the first candle of the user-set backtesting time range. More details on how to calculate the data to download can be found [here](#deciding-the-sliding-training-window-and-backtesting-duration).
|
Backtesting mode requires the user to have the data pre-downloaded (unlike in dry/live mode where FreqAI automatically downloads the necessary data). The user should be careful to consider that the time range of the downloaded data is more than the backtesting time range. This is because FreqAI needs data prior to the desired backtesting time range in order to train a model to be ready to make predictions on the first candle of the user-set backtesting time range. More details on how to calculate the data to download can be found [here](#deciding-the-sliding-training-window-and-backtesting-duration).
|
||||||
@ -529,20 +542,14 @@ the user is asking FreqAI to use a training period of 30 days and backtest on th
|
|||||||
This means that if the user sets `--timerange 20210501-20210701`,
|
This means that if the user sets `--timerange 20210501-20210701`,
|
||||||
FreqAI will train have trained 8 separate models at the end of `--timerange` (because the full range comprises 8 weeks). After the training of the model, FreqAI will backtest the subsequent 7 days. The "sliding window" then moves one week forward (emulating FreqAI retraining once per week in live mode) and the new model uses the previous 30 days (including the 7 days used for backtesting by the previous model) to train. This is repeated until the end of `--timerange`.
|
FreqAI will train have trained 8 separate models at the end of `--timerange` (because the full range comprises 8 weeks). After the training of the model, FreqAI will backtest the subsequent 7 days. The "sliding window" then moves one week forward (emulating FreqAI retraining once per week in live mode) and the new model uses the previous 30 days (including the 7 days used for backtesting by the previous model) to train. This is repeated until the end of `--timerange`.
|
||||||
|
|
||||||
In live mode, the required training data is automatically computed and downloaded. However, in backtesting mode,
|
|
||||||
the user must manually enter the required number of `startup_candles` in the config. This value
|
|
||||||
is used to increase the data to FreqAI, which should be sufficient to enable all indicators
|
|
||||||
to be NaN free at the beginning of the first training. This is done by identifying the
|
|
||||||
longest timeframe (`4h` in presented example config) and the longest indicator period (`20` days in presented example config)
|
|
||||||
and adding this to the `train_period_days`. The units need to be in the base candle time frame:
|
|
||||||
`startup_candles` = ( 4 hours * 20 max period * 60 minutes/hour + 30 day train_period_days * 1440 minutes per day ) / 5 min (base time frame) = 9360.
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
In dry/live mode, this is all precomputed and handled automatically. Thus, `startup_candle` has no influence on dry/live mode.
|
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Although fractional `backtest_period_days` is allowed, the user should be aware that the `--timerange` is divided by this value to determine the number of models that FreqAI will need to train in order to backtest the full range. For example, if the user wants to set a `--timerange` of 10 days, and asks for a `backtest_period_days` of 0.1, FreqAI will need to train 100 models per pair to complete the full backtest. Because of this, a true backtest of FreqAI adaptive training would take a *very* long time. The best way to fully test a model is to run it dry and let it constantly train. In this case, backtesting would take the exact same amount of time as a dry run.
|
Although fractional `backtest_period_days` is allowed, the user should be aware that the `--timerange` is divided by this value to determine the number of models that FreqAI will need to train in order to backtest the full range. For example, if the user wants to set a `--timerange` of 10 days, and asks for a `backtest_period_days` of 0.1, FreqAI will need to train 100 models per pair to complete the full backtest. Because of this, a true backtest of FreqAI adaptive training would take a *very* long time. The best way to fully test a model is to run it dry and let it constantly train. In this case, backtesting would take the exact same amount of time as a dry run.
|
||||||
|
|
||||||
|
### Downloading data for backtesting
|
||||||
|
Live/dry instances will download the data automatically for the user, but users who wish to use backtesting functionality still need to download the necessary data using `download-data` (details [here](data-download/#data-downloading)). FreqAI users need to pay careful attention to understanding how much *additional* data needs to be downloaded to ensure that they have a sufficient amount of training data *before* the start of their backtesting timerange. The amount of additional data can be roughly estimated by taking subtracting `train_period_days` and the `startup_candle_count` ([details](#setting-the-startupcandlecount)) from the beginning of the desired backtesting timerange.
|
||||||
|
|
||||||
|
As an example, if we wish to backtest the `--timerange` above of `20210501-20210701`, and we use the example config which sets `train_period_days` to 15. The startup candle count is 40 on a maximum `include_timeframes` of 1h. We would need 20210501 - 15 days - 40 * 1h / 24 hours = 20210414 (16.7 days earlier than the start of the desired training timerange).
|
||||||
|
|
||||||
### Defining model expirations
|
### Defining model expirations
|
||||||
|
|
||||||
During dry/live mode, FreqAI trains each coin pair sequentially (on separate threads/GPU from the main Freqtrade bot). This means that there is always an age discrepancy between models. If a user is training on 50 pairs, and each pair requires 5 minutes to train, the oldest model will be over 4 hours old. This may be undesirable if the characteristic time scale (the trade duration target) for a strategy is less than 4 hours. The user can decide to only make trade entries if the model is less than
|
During dry/live mode, FreqAI trains each coin pair sequentially (on separate threads/GPU from the main Freqtrade bot). This means that there is always an age discrepancy between models. If a user is training on 50 pairs, and each pair requires 5 minutes to train, the oldest model will be over 4 hours old. This may be undesirable if the characteristic time scale (the trade duration target) for a strategy is less than 4 hours. The user can decide to only make trade entries if the model is less than
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
Please only use advanced trading modes when you know how freqtrade (and your strategy) works.
|
Please only use advanced trading modes when you know how freqtrade (and your strategy) works.
|
||||||
Also, never risk more than what you can afford to lose.
|
Also, never risk more than what you can afford to lose.
|
||||||
|
|
||||||
Please read the [strategy migration guide](strategy_migration.md#strategy-migration-between-v2-and-v3) to migrate your strategy from a freqtrade v2 strategy, to v3 strategy that can short and trade futures.
|
If you already have an existing strategy, please read the [strategy migration guide](strategy_migration.md#strategy-migration-between-v2-and-v3) to migrate your strategy from a freqtrade v2 strategy, to strategy of version 3 which can short and trade futures.
|
||||||
|
|
||||||
## Shorting
|
## Shorting
|
||||||
|
|
||||||
@ -62,6 +62,13 @@ You will also have to pick a "margin mode" (explanation below) - with freqtrade
|
|||||||
"margin_mode": "isolated"
|
"margin_mode": "isolated"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
##### Pair namings
|
||||||
|
|
||||||
|
Freqtrade follows the [ccxt naming conventions for futures](https://docs.ccxt.com/en/latest/manual.html?#perpetual-swap-perpetual-future).
|
||||||
|
A futures pair will therefore have the naming of `base/quote:settle` (e.g. `ETH/USDT:USDT`).
|
||||||
|
|
||||||
|
Binance is currently still an exception to this naming scheme, where pairs are named `ETH/USDT` also for futures markets, but will be aligned as soon as CCXT is ready.
|
||||||
|
|
||||||
### Margin mode
|
### Margin mode
|
||||||
|
|
||||||
On top of `trading_mode` - you will also have to configure your `margin_mode`.
|
On top of `trading_mode` - you will also have to configure your `margin_mode`.
|
||||||
|
@ -166,7 +166,7 @@ Additional technical libraries can be installed as necessary, or custom indicato
|
|||||||
|
|
||||||
Most indicators have an instable startup period, in which they are either not available (NaN), or the calculation is incorrect. This can lead to inconsistencies, since Freqtrade does not know how long this instable period should be.
|
Most indicators have an instable startup period, in which they are either not available (NaN), or the calculation is incorrect. This can lead to inconsistencies, since Freqtrade does not know how long this instable period should be.
|
||||||
To account for this, the strategy can be assigned the `startup_candle_count` attribute.
|
To account for this, the strategy can be assigned the `startup_candle_count` attribute.
|
||||||
This should be set to the maximum number of candles that the strategy requires to calculate stable indicators.
|
This should be set to the maximum number of candles that the strategy requires to calculate stable indicators. In the case where a user includes higher timeframes with informative pairs, the `startup_candle_count` does not necessarily change. The value is the maximum period (in candles) that any of the informatives timeframes need to compute stable indicators.
|
||||||
|
|
||||||
In this example strategy, this should be set to 100 (`startup_candle_count = 100`), since the longest needed history is 100 candles.
|
In this example strategy, this should be set to 100 (`startup_candle_count = 100`), since the longest needed history is 100 candles.
|
||||||
|
|
||||||
|
@ -91,9 +91,9 @@ class DataProvider:
|
|||||||
timerange = TimeRange.parse_timerange(None if self._config.get(
|
timerange = TimeRange.parse_timerange(None if self._config.get(
|
||||||
'timerange') is None else str(self._config.get('timerange')))
|
'timerange') is None else str(self._config.get('timerange')))
|
||||||
# Move informative start time respecting startup_candle_count
|
# Move informative start time respecting startup_candle_count
|
||||||
timerange.subtract_start(
|
startup_candles = self.get_required_startup(str(timeframe))
|
||||||
timeframe_to_seconds(str(timeframe)) * self._config.get('startup_candle_count', 0)
|
tf_seconds = timeframe_to_seconds(str(timeframe))
|
||||||
)
|
timerange.subtract_start(tf_seconds * startup_candles)
|
||||||
self.__cached_pairs_backtesting[saved_pair] = load_pair_history(
|
self.__cached_pairs_backtesting[saved_pair] = load_pair_history(
|
||||||
pair=pair,
|
pair=pair,
|
||||||
timeframe=timeframe or self._config['timeframe'],
|
timeframe=timeframe or self._config['timeframe'],
|
||||||
@ -105,6 +105,21 @@ class DataProvider:
|
|||||||
)
|
)
|
||||||
return self.__cached_pairs_backtesting[saved_pair].copy()
|
return self.__cached_pairs_backtesting[saved_pair].copy()
|
||||||
|
|
||||||
|
def get_required_startup(self, timeframe: str) -> int:
|
||||||
|
freqai_config = self._config.get('freqai', {})
|
||||||
|
if not freqai_config.get('enabled', False):
|
||||||
|
return self._config.get('startup_candle_count', 0)
|
||||||
|
else:
|
||||||
|
startup_candles = self._config.get('startup_candle_count', 0)
|
||||||
|
indicator_periods = freqai_config['feature_parameters']['indicator_periods_candles']
|
||||||
|
# make sure the startupcandles is at least the set maximum indicator periods
|
||||||
|
self._config['startup_candle_count'] = max(startup_candles, max(indicator_periods))
|
||||||
|
tf_seconds = timeframe_to_seconds(timeframe)
|
||||||
|
train_candles = freqai_config['train_period_days'] * 86400 / tf_seconds
|
||||||
|
total_candles = int(self._config['startup_candle_count'] + train_candles)
|
||||||
|
logger.info(f'Increasing startup_candle_count for freqai to {total_candles}')
|
||||||
|
return total_candles
|
||||||
|
|
||||||
def get_pair_dataframe(
|
def get_pair_dataframe(
|
||||||
self,
|
self,
|
||||||
pair: str,
|
pair: str,
|
||||||
|
@ -2600,7 +2600,7 @@ class Exchange:
|
|||||||
is_short: bool,
|
is_short: bool,
|
||||||
amount: float, # Absolute value of position size
|
amount: float, # Absolute value of position size
|
||||||
stake_amount: float,
|
stake_amount: float,
|
||||||
wallet_balance: float = 0.0,
|
wallet_balance: float,
|
||||||
mm_ex_1: float = 0.0, # (Binance) Cross only
|
mm_ex_1: float = 0.0, # (Binance) Cross only
|
||||||
upnl_ex_1: float = 0.0, # (Binance) Cross only
|
upnl_ex_1: float = 0.0, # (Binance) Cross only
|
||||||
) -> Optional[float]:
|
) -> Optional[float]:
|
||||||
|
@ -16,8 +16,6 @@ from sklearn.model_selection import train_test_split
|
|||||||
from sklearn.neighbors import NearestNeighbors
|
from sklearn.neighbors import NearestNeighbors
|
||||||
|
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.data.dataprovider import DataProvider
|
|
||||||
from freqtrade.data.history.history_utils import refresh_backtest_ohlcv_data
|
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange import timeframe_to_seconds
|
from freqtrade.exchange import timeframe_to_seconds
|
||||||
from freqtrade.strategy.interface import IStrategy
|
from freqtrade.strategy.interface import IStrategy
|
||||||
@ -71,6 +69,8 @@ class FreqaiDataKitchen:
|
|||||||
self.label_list: List = []
|
self.label_list: List = []
|
||||||
self.training_features_list: List = []
|
self.training_features_list: List = []
|
||||||
self.model_filename: str = ""
|
self.model_filename: str = ""
|
||||||
|
self.backtesting_results_path = Path()
|
||||||
|
self.backtest_predictions_folder: str = "backtesting_predictions"
|
||||||
self.live = live
|
self.live = live
|
||||||
self.pair = pair
|
self.pair = pair
|
||||||
|
|
||||||
@ -780,9 +780,10 @@ class FreqaiDataKitchen:
|
|||||||
weights = np.exp(-np.arange(num_weights) / (wfactor * num_weights))[::-1]
|
weights = np.exp(-np.arange(num_weights) / (wfactor * num_weights))[::-1]
|
||||||
return weights
|
return weights
|
||||||
|
|
||||||
def append_predictions(self, predictions: DataFrame, do_predict: npt.ArrayLike) -> None:
|
def get_predictions_to_append(self, predictions: DataFrame,
|
||||||
|
do_predict: npt.ArrayLike) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Append backtest prediction from current backtest period to all previous periods
|
Get backtest prediction from current backtest period
|
||||||
"""
|
"""
|
||||||
|
|
||||||
append_df = DataFrame()
|
append_df = DataFrame()
|
||||||
@ -797,13 +798,18 @@ class FreqaiDataKitchen:
|
|||||||
if self.freqai_config["feature_parameters"].get("DI_threshold", 0) > 0:
|
if self.freqai_config["feature_parameters"].get("DI_threshold", 0) > 0:
|
||||||
append_df["DI_values"] = self.DI_values
|
append_df["DI_values"] = self.DI_values
|
||||||
|
|
||||||
|
return append_df
|
||||||
|
|
||||||
|
def append_predictions(self, append_df: DataFrame) -> None:
|
||||||
|
"""
|
||||||
|
Append backtest prediction from current backtest period to all previous periods
|
||||||
|
"""
|
||||||
|
|
||||||
if self.full_df.empty:
|
if self.full_df.empty:
|
||||||
self.full_df = append_df
|
self.full_df = append_df
|
||||||
else:
|
else:
|
||||||
self.full_df = pd.concat([self.full_df, append_df], axis=0)
|
self.full_df = pd.concat([self.full_df, append_df], axis=0)
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
def fill_predictions(self, dataframe):
|
def fill_predictions(self, dataframe):
|
||||||
"""
|
"""
|
||||||
Back fill values to before the backtesting range so that the dataframe matches size
|
Back fill values to before the backtesting range so that the dataframe matches size
|
||||||
@ -903,9 +909,7 @@ class FreqaiDataKitchen:
|
|||||||
# We notice that users like to use exotic indicators where
|
# We notice that users like to use exotic indicators where
|
||||||
# they do not know the required timeperiod. Here we include a factor
|
# they do not know the required timeperiod. Here we include a factor
|
||||||
# of safety by multiplying the user considered "max" by 2.
|
# of safety by multiplying the user considered "max" by 2.
|
||||||
max_period = self.freqai_config["feature_parameters"].get(
|
max_period = self.config.get('startup_candle_count', 20) * 2
|
||||||
"indicator_max_period_candles", 20
|
|
||||||
) * 2
|
|
||||||
additional_seconds = max_period * max_tf_seconds
|
additional_seconds = max_period * max_tf_seconds
|
||||||
|
|
||||||
if trained_timestamp != 0:
|
if trained_timestamp != 0:
|
||||||
@ -951,31 +955,6 @@ class FreqaiDataKitchen:
|
|||||||
|
|
||||||
self.model_filename = f"cb_{coin.lower()}_{int(trained_timerange.stopts)}"
|
self.model_filename = f"cb_{coin.lower()}_{int(trained_timerange.stopts)}"
|
||||||
|
|
||||||
def download_all_data_for_training(self, timerange: TimeRange, dp: DataProvider) -> None:
|
|
||||||
"""
|
|
||||||
Called only once upon start of bot to download the necessary data for
|
|
||||||
populating indicators and training the model.
|
|
||||||
:param timerange: TimeRange = The full data timerange for populating the indicators
|
|
||||||
and training the model.
|
|
||||||
:param dp: DataProvider instance attached to the strategy
|
|
||||||
"""
|
|
||||||
new_pairs_days = int((timerange.stopts - timerange.startts) / SECONDS_IN_DAY)
|
|
||||||
if not dp._exchange:
|
|
||||||
# Not realistic - this is only called in live mode.
|
|
||||||
raise OperationalException("Dataprovider did not have an exchange attached.")
|
|
||||||
refresh_backtest_ohlcv_data(
|
|
||||||
dp._exchange,
|
|
||||||
pairs=self.all_pairs,
|
|
||||||
timeframes=self.freqai_config["feature_parameters"].get("include_timeframes"),
|
|
||||||
datadir=self.config["datadir"],
|
|
||||||
timerange=timerange,
|
|
||||||
new_pairs_days=new_pairs_days,
|
|
||||||
erase=False,
|
|
||||||
data_format=self.config.get("dataformat_ohlcv", "json"),
|
|
||||||
trading_mode=self.config.get("trading_mode", "spot"),
|
|
||||||
prepend=self.config.get("prepend_data", False),
|
|
||||||
)
|
|
||||||
|
|
||||||
def set_all_pairs(self) -> None:
|
def set_all_pairs(self) -> None:
|
||||||
|
|
||||||
self.all_pairs = copy.deepcopy(
|
self.all_pairs = copy.deepcopy(
|
||||||
@ -1089,3 +1068,50 @@ class FreqaiDataKitchen:
|
|||||||
if self.unique_classes:
|
if self.unique_classes:
|
||||||
for label in self.unique_classes:
|
for label in self.unique_classes:
|
||||||
self.unique_class_list += list(self.unique_classes[label])
|
self.unique_class_list += list(self.unique_classes[label])
|
||||||
|
|
||||||
|
def save_backtesting_prediction(
|
||||||
|
self, append_df: DataFrame
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
"""
|
||||||
|
Save prediction dataframe from backtesting to h5 file format
|
||||||
|
:param append_df: dataframe for backtesting period
|
||||||
|
"""
|
||||||
|
full_predictions_folder = Path(self.full_path / self.backtest_predictions_folder)
|
||||||
|
if not full_predictions_folder.is_dir():
|
||||||
|
full_predictions_folder.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
append_df.to_hdf(self.backtesting_results_path, key='append_df', mode='w')
|
||||||
|
|
||||||
|
def get_backtesting_prediction(
|
||||||
|
self
|
||||||
|
) -> DataFrame:
|
||||||
|
|
||||||
|
"""
|
||||||
|
Get prediction dataframe from h5 file format
|
||||||
|
"""
|
||||||
|
append_df = pd.read_hdf(self.backtesting_results_path)
|
||||||
|
return append_df
|
||||||
|
|
||||||
|
def check_if_backtest_prediction_exists(
|
||||||
|
self
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Check if a backtesting prediction already exists
|
||||||
|
:param dk: FreqaiDataKitchen
|
||||||
|
:return:
|
||||||
|
:boolean: whether the prediction file exists or not.
|
||||||
|
"""
|
||||||
|
path_to_predictionfile = Path(self.full_path /
|
||||||
|
self.backtest_predictions_folder /
|
||||||
|
f"{self.model_filename}_prediction.h5")
|
||||||
|
self.backtesting_results_path = path_to_predictionfile
|
||||||
|
|
||||||
|
file_exists = path_to_predictionfile.is_file()
|
||||||
|
if file_exists:
|
||||||
|
logger.info(f"Found backtesting prediction file at {path_to_predictionfile}")
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
f"Could not find backtesting prediction file at {path_to_predictionfile}"
|
||||||
|
)
|
||||||
|
return file_exists
|
||||||
|
@ -71,6 +71,9 @@ class IFreqaiModel(ABC):
|
|||||||
self.first = True
|
self.first = True
|
||||||
self.set_full_path()
|
self.set_full_path()
|
||||||
self.follow_mode: bool = self.freqai_info.get("follow_mode", False)
|
self.follow_mode: bool = self.freqai_info.get("follow_mode", False)
|
||||||
|
self.save_backtest_models: bool = self.freqai_info.get("save_backtest_models", False)
|
||||||
|
if self.save_backtest_models:
|
||||||
|
logger.info('Backtesting module configured to save all models.')
|
||||||
self.dd = FreqaiDataDrawer(Path(self.full_path), self.config, self.follow_mode)
|
self.dd = FreqaiDataDrawer(Path(self.full_path), self.config, self.follow_mode)
|
||||||
self.identifier: str = self.freqai_info.get("identifier", "no_id_provided")
|
self.identifier: str = self.freqai_info.get("identifier", "no_id_provided")
|
||||||
self.scanning = False
|
self.scanning = False
|
||||||
@ -124,7 +127,6 @@ class IFreqaiModel(ABC):
|
|||||||
elif not self.follow_mode:
|
elif not self.follow_mode:
|
||||||
self.dk = FreqaiDataKitchen(self.config, self.live, metadata["pair"])
|
self.dk = FreqaiDataKitchen(self.config, self.live, metadata["pair"])
|
||||||
logger.info(f"Training {len(self.dk.training_timeranges)} timeranges")
|
logger.info(f"Training {len(self.dk.training_timeranges)} timeranges")
|
||||||
with self.analysis_lock:
|
|
||||||
dataframe = self.dk.use_strategy_to_populate_indicators(
|
dataframe = self.dk.use_strategy_to_populate_indicators(
|
||||||
strategy, prediction_dataframe=dataframe, pair=metadata["pair"]
|
strategy, prediction_dataframe=dataframe, pair=metadata["pair"]
|
||||||
)
|
)
|
||||||
@ -224,19 +226,29 @@ class IFreqaiModel(ABC):
|
|||||||
"trains"
|
"trains"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
trained_timestamp_int = int(trained_timestamp.stopts)
|
||||||
dk.data_path = Path(
|
dk.data_path = Path(
|
||||||
dk.full_path
|
dk.full_path
|
||||||
/
|
/
|
||||||
f"sub-train-{metadata['pair'].split('/')[0]}_{int(trained_timestamp.stopts)}"
|
f"sub-train-{metadata['pair'].split('/')[0]}_{trained_timestamp_int}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
dk.set_new_model_names(metadata["pair"], trained_timestamp)
|
||||||
|
|
||||||
|
if dk.check_if_backtest_prediction_exists():
|
||||||
|
append_df = dk.get_backtesting_prediction()
|
||||||
|
dk.append_predictions(append_df)
|
||||||
|
else:
|
||||||
if not self.model_exists(
|
if not self.model_exists(
|
||||||
metadata["pair"], dk, trained_timestamp=int(trained_timestamp.stopts)
|
metadata["pair"], dk, trained_timestamp=trained_timestamp_int
|
||||||
):
|
):
|
||||||
dk.find_features(dataframe_train)
|
dk.find_features(dataframe_train)
|
||||||
self.model = self.train(dataframe_train, metadata["pair"], dk)
|
self.model = self.train(dataframe_train, metadata["pair"], dk)
|
||||||
self.dd.pair_dict[metadata["pair"]]["trained_timestamp"] = int(
|
self.dd.pair_dict[metadata["pair"]]["trained_timestamp"] = int(
|
||||||
trained_timestamp.stopts)
|
trained_timestamp.stopts)
|
||||||
dk.set_new_model_names(metadata["pair"], trained_timestamp)
|
|
||||||
|
if self.save_backtest_models:
|
||||||
|
logger.info('Saving backtest model to disk.')
|
||||||
self.dd.save_data(self.model, metadata["pair"], dk)
|
self.dd.save_data(self.model, metadata["pair"], dk)
|
||||||
else:
|
else:
|
||||||
self.model = self.dd.load_data(metadata["pair"], dk)
|
self.model = self.dd.load_data(metadata["pair"], dk)
|
||||||
@ -244,8 +256,9 @@ class IFreqaiModel(ABC):
|
|||||||
self.check_if_feature_list_matches_strategy(dataframe_train, dk)
|
self.check_if_feature_list_matches_strategy(dataframe_train, dk)
|
||||||
|
|
||||||
pred_df, do_preds = self.predict(dataframe_backtest, dk)
|
pred_df, do_preds = self.predict(dataframe_backtest, dk)
|
||||||
|
append_df = dk.get_predictions_to_append(pred_df, do_preds)
|
||||||
dk.append_predictions(pred_df, do_preds)
|
dk.append_predictions(append_df)
|
||||||
|
dk.save_backtesting_prediction(append_df)
|
||||||
|
|
||||||
dk.fill_predictions(dataframe)
|
dk.fill_predictions(dataframe)
|
||||||
|
|
||||||
@ -290,14 +303,8 @@ class IFreqaiModel(ABC):
|
|||||||
)
|
)
|
||||||
dk.set_paths(metadata["pair"], new_trained_timerange.stopts)
|
dk.set_paths(metadata["pair"], new_trained_timerange.stopts)
|
||||||
|
|
||||||
# download candle history if it is not already in memory
|
# load candle history into memory if it is not yet.
|
||||||
if not self.dd.historic_data:
|
if not self.dd.historic_data:
|
||||||
logger.info(
|
|
||||||
"Downloading all training data for all pairs in whitelist and "
|
|
||||||
"corr_pairlist, this may take a while if you do not have the "
|
|
||||||
"data saved"
|
|
||||||
)
|
|
||||||
dk.download_all_data_for_training(data_load_timerange, strategy.dp)
|
|
||||||
self.dd.load_all_pair_histories(data_load_timerange, dk)
|
self.dd.load_all_pair_histories(data_load_timerange, dk)
|
||||||
|
|
||||||
if not self.scanning:
|
if not self.scanning:
|
||||||
@ -462,11 +469,6 @@ class IFreqaiModel(ABC):
|
|||||||
:return:
|
:return:
|
||||||
:boolean: whether the model file exists or not.
|
:boolean: whether the model file exists or not.
|
||||||
"""
|
"""
|
||||||
coin, _ = pair.split("/")
|
|
||||||
|
|
||||||
if not self.live:
|
|
||||||
dk.model_filename = model_filename = f"cb_{coin.lower()}_{trained_timestamp}"
|
|
||||||
|
|
||||||
path_to_modelfile = Path(dk.data_path / f"{model_filename}_model.joblib")
|
path_to_modelfile = Path(dk.data_path / f"{model_filename}_model.joblib")
|
||||||
file_exists = path_to_modelfile.is_file()
|
file_exists = path_to_modelfile.is_file()
|
||||||
if file_exists and not scanning:
|
if file_exists and not scanning:
|
||||||
@ -619,8 +621,8 @@ class IFreqaiModel(ABC):
|
|||||||
logger.info(
|
logger.info(
|
||||||
f'Total time spent inferencing pairlist {self.inference_time:.2f} seconds')
|
f'Total time spent inferencing pairlist {self.inference_time:.2f} seconds')
|
||||||
if self.inference_time > 0.25 * self.base_tf_seconds:
|
if self.inference_time > 0.25 * self.base_tf_seconds:
|
||||||
logger.warning('Inference took over 25/% of the candle time. Reduce pairlist to'
|
logger.warning("Inference took over 25% of the candle time. Reduce pairlist to"
|
||||||
' avoid blinding open trades and degrading performance.')
|
" avoid blinding open trades and degrading performance.")
|
||||||
self.pair_it = 0
|
self.pair_it = 0
|
||||||
self.inference_time = 0
|
self.inference_time = 0
|
||||||
return
|
return
|
||||||
|
134
freqtrade/freqai/utils.py
Normal file
134
freqtrade/freqai/utils.py
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import logging
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from freqtrade.configuration import TimeRange
|
||||||
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
|
from freqtrade.data.history.history_utils import refresh_backtest_ohlcv_data
|
||||||
|
from freqtrade.exceptions import OperationalException
|
||||||
|
from freqtrade.exchange import timeframe_to_seconds
|
||||||
|
from freqtrade.exchange.exchange import market_is_active
|
||||||
|
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def download_all_data_for_training(dp: DataProvider, config: dict) -> None:
|
||||||
|
"""
|
||||||
|
Called only once upon start of bot to download the necessary data for
|
||||||
|
populating indicators and training the model.
|
||||||
|
:param timerange: TimeRange = The full data timerange for populating the indicators
|
||||||
|
and training the model.
|
||||||
|
:param dp: DataProvider instance attached to the strategy
|
||||||
|
"""
|
||||||
|
|
||||||
|
if dp._exchange is None:
|
||||||
|
raise OperationalException('No exchange object found.')
|
||||||
|
markets = [p for p, m in dp._exchange.markets.items() if market_is_active(m)
|
||||||
|
or config.get('include_inactive')]
|
||||||
|
|
||||||
|
all_pairs = dynamic_expand_pairlist(config, markets)
|
||||||
|
|
||||||
|
timerange = get_required_data_timerange(config)
|
||||||
|
|
||||||
|
new_pairs_days = int((timerange.stopts - timerange.startts) / 86400)
|
||||||
|
|
||||||
|
refresh_backtest_ohlcv_data(
|
||||||
|
dp._exchange,
|
||||||
|
pairs=all_pairs,
|
||||||
|
timeframes=config["freqai"]["feature_parameters"].get("include_timeframes"),
|
||||||
|
datadir=config["datadir"],
|
||||||
|
timerange=timerange,
|
||||||
|
new_pairs_days=new_pairs_days,
|
||||||
|
erase=False,
|
||||||
|
data_format=config.get("dataformat_ohlcv", "json"),
|
||||||
|
trading_mode=config.get("trading_mode", "spot"),
|
||||||
|
prepend=config.get("prepend_data", False),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_required_data_timerange(
|
||||||
|
config: dict
|
||||||
|
) -> TimeRange:
|
||||||
|
"""
|
||||||
|
Used to compute the required data download time range
|
||||||
|
for auto data-download in FreqAI
|
||||||
|
"""
|
||||||
|
time = datetime.now(tz=timezone.utc).timestamp()
|
||||||
|
|
||||||
|
timeframes = config["freqai"]["feature_parameters"].get("include_timeframes")
|
||||||
|
|
||||||
|
max_tf_seconds = 0
|
||||||
|
for tf in timeframes:
|
||||||
|
secs = timeframe_to_seconds(tf)
|
||||||
|
if secs > max_tf_seconds:
|
||||||
|
max_tf_seconds = secs
|
||||||
|
|
||||||
|
startup_candles = config.get('startup_candle_count', 0)
|
||||||
|
indicator_periods = config["freqai"]["feature_parameters"]["indicator_periods_candles"]
|
||||||
|
|
||||||
|
# factor the max_period as a factor of safety.
|
||||||
|
max_period = int(max(startup_candles, max(indicator_periods)) * 1.5)
|
||||||
|
config['startup_candle_count'] = max_period
|
||||||
|
logger.info(f'FreqAI auto-downloader using {max_period} startup candles.')
|
||||||
|
|
||||||
|
additional_seconds = max_period * max_tf_seconds
|
||||||
|
|
||||||
|
startts = int(
|
||||||
|
time
|
||||||
|
- config["freqai"].get("train_period_days", 0) * 86400
|
||||||
|
- additional_seconds
|
||||||
|
)
|
||||||
|
stopts = int(time)
|
||||||
|
data_load_timerange = TimeRange('date', 'date', startts, stopts)
|
||||||
|
|
||||||
|
return data_load_timerange
|
||||||
|
|
||||||
|
|
||||||
|
# Keep below for when we wish to download heterogeneously lengthed data for FreqAI.
|
||||||
|
# def download_all_data_for_training(dp: DataProvider, config: dict) -> None:
|
||||||
|
# """
|
||||||
|
# Called only once upon start of bot to download the necessary data for
|
||||||
|
# populating indicators and training a FreqAI model.
|
||||||
|
# :param timerange: TimeRange = The full data timerange for populating the indicators
|
||||||
|
# and training the model.
|
||||||
|
# :param dp: DataProvider instance attached to the strategy
|
||||||
|
# """
|
||||||
|
|
||||||
|
# if dp._exchange is not None:
|
||||||
|
# markets = [p for p, m in dp._exchange.markets.items() if market_is_active(m)
|
||||||
|
# or config.get('include_inactive')]
|
||||||
|
# else:
|
||||||
|
# # This should not occur:
|
||||||
|
# raise OperationalException('No exchange object found.')
|
||||||
|
|
||||||
|
# all_pairs = dynamic_expand_pairlist(config, markets)
|
||||||
|
|
||||||
|
# if not dp._exchange:
|
||||||
|
# # Not realistic - this is only called in live mode.
|
||||||
|
# raise OperationalException("Dataprovider did not have an exchange attached.")
|
||||||
|
|
||||||
|
# time = datetime.now(tz=timezone.utc).timestamp()
|
||||||
|
|
||||||
|
# for tf in config["freqai"]["feature_parameters"].get("include_timeframes"):
|
||||||
|
# timerange = TimeRange()
|
||||||
|
# timerange.startts = int(time)
|
||||||
|
# timerange.stopts = int(time)
|
||||||
|
# startup_candles = dp.get_required_startup(str(tf))
|
||||||
|
# tf_seconds = timeframe_to_seconds(str(tf))
|
||||||
|
# timerange.subtract_start(tf_seconds * startup_candles)
|
||||||
|
# new_pairs_days = int((timerange.stopts - timerange.startts) / 86400)
|
||||||
|
# # FIXME: now that we are looping on `refresh_backtest_ohlcv_data`, the function
|
||||||
|
# # redownloads the funding rate for each pair.
|
||||||
|
# refresh_backtest_ohlcv_data(
|
||||||
|
# dp._exchange,
|
||||||
|
# pairs=all_pairs,
|
||||||
|
# timeframes=[tf],
|
||||||
|
# datadir=config["datadir"],
|
||||||
|
# timerange=timerange,
|
||||||
|
# new_pairs_days=new_pairs_days,
|
||||||
|
# erase=False,
|
||||||
|
# data_format=config.get("dataformat_ohlcv", "json"),
|
||||||
|
# trading_mode=config.get("trading_mode", "spot"),
|
||||||
|
# prepend=config.get("prepend_data", False),
|
||||||
|
# )
|
@ -1778,7 +1778,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
self.rpc.send_msg(msg)
|
self.rpc.send_msg(msg)
|
||||||
|
|
||||||
def apply_fee_conditional(self, trade: Trade, trade_base_currency: str,
|
def apply_fee_conditional(self, trade: Trade, trade_base_currency: str,
|
||||||
amount: float, fee_abs: float) -> Optional[float]:
|
amount: float, fee_abs: float, order_obj: Order) -> Optional[float]:
|
||||||
"""
|
"""
|
||||||
Applies the fee to amount (either from Order or from Trades).
|
Applies the fee to amount (either from Order or from Trades).
|
||||||
Can eat into dust if more than the required asset is available.
|
Can eat into dust if more than the required asset is available.
|
||||||
@ -1786,7 +1786,12 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
never in base currency.
|
never in base currency.
|
||||||
"""
|
"""
|
||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
if fee_abs != 0 and self.wallets.get_free(trade_base_currency) >= amount:
|
amount_ = amount
|
||||||
|
if order_obj.ft_order_side == trade.exit_side or order_obj.ft_order_side == 'stoploss':
|
||||||
|
# check against remaining amount!
|
||||||
|
amount_ = trade.amount - amount
|
||||||
|
|
||||||
|
if fee_abs != 0 and self.wallets.get_free(trade_base_currency) >= amount_:
|
||||||
# Eat into dust if we own more than base currency
|
# Eat into dust if we own more than base currency
|
||||||
logger.info(f"Fee amount for {trade} was in base currency - "
|
logger.info(f"Fee amount for {trade} was in base currency - "
|
||||||
f"Eating Fee {fee_abs} into dust.")
|
f"Eating Fee {fee_abs} into dust.")
|
||||||
@ -1833,7 +1838,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
if trade_base_currency == fee_currency:
|
if trade_base_currency == fee_currency:
|
||||||
# Apply fee to amount
|
# Apply fee to amount
|
||||||
return self.apply_fee_conditional(trade, trade_base_currency,
|
return self.apply_fee_conditional(trade, trade_base_currency,
|
||||||
amount=order_amount, fee_abs=fee_cost)
|
amount=order_amount, fee_abs=fee_cost,
|
||||||
|
order_obj=order_obj)
|
||||||
return None
|
return None
|
||||||
return self.fee_detection_from_trades(
|
return self.fee_detection_from_trades(
|
||||||
trade, order, order_obj, order_amount, order.get('trades', []))
|
trade, order, order_obj, order_amount, order.get('trades', []))
|
||||||
@ -1892,8 +1898,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
raise DependencyException("Half bought? Amounts don't match")
|
raise DependencyException("Half bought? Amounts don't match")
|
||||||
|
|
||||||
if fee_abs != 0:
|
if fee_abs != 0:
|
||||||
return self.apply_fee_conditional(trade, trade_base_currency,
|
return self.apply_fee_conditional(
|
||||||
amount=amount, fee_abs=fee_abs)
|
trade, trade_base_currency, amount=amount, fee_abs=fee_abs, order_obj=order_obj)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_valid_price(self, custom_price: float, proposed_price: float) -> float:
|
def get_valid_price(self, custom_price: float, proposed_price: float) -> float:
|
||||||
|
@ -212,21 +212,12 @@ class Backtesting:
|
|||||||
"""
|
"""
|
||||||
self.progress.init_step(BacktestState.DATALOAD, 1)
|
self.progress.init_step(BacktestState.DATALOAD, 1)
|
||||||
|
|
||||||
if self.config.get('freqai', {}).get('enabled', False):
|
|
||||||
startup_candles = int(self.config.get('freqai', {}).get('startup_candles', 0))
|
|
||||||
if not startup_candles:
|
|
||||||
raise OperationalException('FreqAI backtesting module requires user set '
|
|
||||||
'startup_candles in config.')
|
|
||||||
self.required_startup += int(self.config.get('freqai', {}).get('startup_candles', 0))
|
|
||||||
logger.info(f'Increasing startup_candle_count for freqai to {self.required_startup}')
|
|
||||||
self.config['startup_candle_count'] = self.required_startup
|
|
||||||
|
|
||||||
data = history.load_data(
|
data = history.load_data(
|
||||||
datadir=self.config['datadir'],
|
datadir=self.config['datadir'],
|
||||||
pairs=self.pairlists.whitelist,
|
pairs=self.pairlists.whitelist,
|
||||||
timeframe=self.timeframe,
|
timeframe=self.timeframe,
|
||||||
timerange=self.timerange,
|
timerange=self.timerange,
|
||||||
startup_candles=self.required_startup,
|
startup_candles=self.dataprovider.get_required_startup(self.timeframe),
|
||||||
fail_without_data=True,
|
fail_without_data=True,
|
||||||
data_format=self.config.get('dataformat_ohlcv', 'json'),
|
data_format=self.config.get('dataformat_ohlcv', 'json'),
|
||||||
candle_type=self.config.get('candle_type_def', CandleType.SPOT)
|
candle_type=self.config.get('candle_type_def', CandleType.SPOT)
|
||||||
|
@ -648,7 +648,6 @@ class LocalTrade():
|
|||||||
"""
|
"""
|
||||||
self.close_rate = rate
|
self.close_rate = rate
|
||||||
self.close_date = self.close_date or datetime.utcnow()
|
self.close_date = self.close_date or datetime.utcnow()
|
||||||
self.close_profit_abs = self.calc_profit(rate) + self.realized_profit
|
|
||||||
self.is_open = False
|
self.is_open = False
|
||||||
self.exit_order_status = 'closed'
|
self.exit_order_status = 'closed'
|
||||||
self.open_order_id = None
|
self.open_order_id = None
|
||||||
|
@ -52,7 +52,7 @@ class PrecisionFilter(IPairList):
|
|||||||
:return: True if the pair can stay, false if it should be removed
|
:return: True if the pair can stay, false if it should be removed
|
||||||
"""
|
"""
|
||||||
if ticker.get('last', None) is None:
|
if ticker.get('last', None) is None:
|
||||||
self.log_once(f"Removed {ticker['symbol']} from whitelist, because "
|
self.log_once(f"Removed {pair} from whitelist, because "
|
||||||
"ticker['last'] is empty (Usually no trade in the last 24h).",
|
"ticker['last'] is empty (Usually no trade in the last 24h).",
|
||||||
logger.info)
|
logger.info)
|
||||||
return False
|
return False
|
||||||
@ -62,10 +62,10 @@ class PrecisionFilter(IPairList):
|
|||||||
sp = self._exchange.price_to_precision(pair, stop_price)
|
sp = self._exchange.price_to_precision(pair, stop_price)
|
||||||
|
|
||||||
stop_gap_price = self._exchange.price_to_precision(pair, stop_price * 0.99)
|
stop_gap_price = self._exchange.price_to_precision(pair, stop_price * 0.99)
|
||||||
logger.debug(f"{ticker['symbol']} - {sp} : {stop_gap_price}")
|
logger.debug(f"{pair} - {sp} : {stop_gap_price}")
|
||||||
|
|
||||||
if sp <= stop_gap_price:
|
if sp <= stop_gap_price:
|
||||||
self.log_once(f"Removed {ticker['symbol']} from whitelist, because "
|
self.log_once(f"Removed {pair} from whitelist, because "
|
||||||
f"stop price {sp} would be <= stop limit {stop_gap_price}", logger.info)
|
f"stop price {sp} would be <= stop limit {stop_gap_price}", logger.info)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -186,6 +186,7 @@ class VolumePairList(IPairList):
|
|||||||
needed_pairs, since_ms=since_ms, cache=False
|
needed_pairs, since_ms=since_ms, cache=False
|
||||||
)
|
)
|
||||||
for i, p in enumerate(filtered_tickers):
|
for i, p in enumerate(filtered_tickers):
|
||||||
|
contract_size = self._exchange.markets[p['symbol']].get('contractSize', 1.0) or 1.0
|
||||||
pair_candles = candles[
|
pair_candles = candles[
|
||||||
(p['symbol'], self._lookback_timeframe, self._def_candletype)
|
(p['symbol'], self._lookback_timeframe, self._def_candletype)
|
||||||
] if (
|
] if (
|
||||||
@ -199,6 +200,7 @@ class VolumePairList(IPairList):
|
|||||||
|
|
||||||
pair_candles['quoteVolume'] = (
|
pair_candles['quoteVolume'] = (
|
||||||
pair_candles['volume'] * pair_candles['typical_price']
|
pair_candles['volume'] * pair_candles['typical_price']
|
||||||
|
* contract_size
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Exchange ohlcv data is in quote volume already.
|
# Exchange ohlcv data is in quote volume already.
|
||||||
|
@ -148,10 +148,19 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
def load_freqAI_model(self) -> None:
|
def load_freqAI_model(self) -> None:
|
||||||
if self.config.get('freqai', {}).get('enabled', False):
|
if self.config.get('freqai', {}).get('enabled', False):
|
||||||
# Import here to avoid importing this if freqAI is disabled
|
# Import here to avoid importing this if freqAI is disabled
|
||||||
|
from freqtrade.freqai.utils import download_all_data_for_training
|
||||||
from freqtrade.resolvers.freqaimodel_resolver import FreqaiModelResolver
|
from freqtrade.resolvers.freqaimodel_resolver import FreqaiModelResolver
|
||||||
|
|
||||||
self.freqai = FreqaiModelResolver.load_freqaimodel(self.config)
|
self.freqai = FreqaiModelResolver.load_freqaimodel(self.config)
|
||||||
self.freqai_info = self.config["freqai"]
|
self.freqai_info = self.config["freqai"]
|
||||||
|
|
||||||
|
# download the desired data in dry/live
|
||||||
|
if self.config.get('runmode') in (RunMode.DRY_RUN, RunMode.LIVE):
|
||||||
|
logger.info(
|
||||||
|
"Downloading all training data for all pairs in whitelist and "
|
||||||
|
"corr_pairlist, this may take a while if the data is not "
|
||||||
|
"already on disk."
|
||||||
|
)
|
||||||
|
download_all_data_for_training(self.dp, self.config)
|
||||||
else:
|
else:
|
||||||
# Gracious failures if freqAI is disabled but "start" is called.
|
# Gracious failures if freqAI is disabled but "start" is called.
|
||||||
class DummyClass():
|
class DummyClass():
|
||||||
|
@ -43,7 +43,8 @@ class FreqaiExampleStrategy(IStrategy):
|
|||||||
process_only_new_candles = True
|
process_only_new_candles = True
|
||||||
stoploss = -0.05
|
stoploss = -0.05
|
||||||
use_exit_signal = True
|
use_exit_signal = True
|
||||||
startup_candle_count: int = 300
|
# this is the maximum period fed to talib (timeframe independent)
|
||||||
|
startup_candle_count: int = 40
|
||||||
can_short = False
|
can_short = False
|
||||||
|
|
||||||
linear_roi_offset = DecimalParameter(
|
linear_roi_offset = DecimalParameter(
|
||||||
|
@ -45,7 +45,6 @@ class FreqaiExampleHybridStrategy(IStrategy):
|
|||||||
"weight_factor": 0.9,
|
"weight_factor": 0.9,
|
||||||
"principal_component_analysis": false,
|
"principal_component_analysis": false,
|
||||||
"use_SVM_to_remove_outliers": true,
|
"use_SVM_to_remove_outliers": true,
|
||||||
"indicator_max_period_candles": 20,
|
|
||||||
"indicator_periods_candles": [10, 20]
|
"indicator_periods_candles": [10, 20]
|
||||||
},
|
},
|
||||||
"data_split_parameters": {
|
"data_split_parameters": {
|
||||||
|
@ -4985,6 +4985,7 @@ def test_get_liquidation_price1(mocker, default_conf):
|
|||||||
is_short=False,
|
is_short=False,
|
||||||
amount=0.8,
|
amount=0.8,
|
||||||
stake_amount=18.884 * 0.8,
|
stake_amount=18.884 * 0.8,
|
||||||
|
wallet_balance=18.884 * 0.8,
|
||||||
)
|
)
|
||||||
assert liq_price == 17.47
|
assert liq_price == 17.47
|
||||||
|
|
||||||
@ -4996,6 +4997,7 @@ def test_get_liquidation_price1(mocker, default_conf):
|
|||||||
is_short=False,
|
is_short=False,
|
||||||
amount=0.8,
|
amount=0.8,
|
||||||
stake_amount=18.884 * 0.8,
|
stake_amount=18.884 * 0.8,
|
||||||
|
wallet_balance=18.884 * 0.8,
|
||||||
)
|
)
|
||||||
assert liq_price == 17.540699999999998
|
assert liq_price == 17.540699999999998
|
||||||
|
|
||||||
@ -5007,6 +5009,7 @@ def test_get_liquidation_price1(mocker, default_conf):
|
|||||||
is_short=False,
|
is_short=False,
|
||||||
amount=0.8,
|
amount=0.8,
|
||||||
stake_amount=18.884 * 0.8,
|
stake_amount=18.884 * 0.8,
|
||||||
|
wallet_balance=18.884 * 0.8,
|
||||||
)
|
)
|
||||||
assert liq_price is None
|
assert liq_price is None
|
||||||
default_conf['trading_mode'] = 'margin'
|
default_conf['trading_mode'] = 'margin'
|
||||||
@ -5019,6 +5022,7 @@ def test_get_liquidation_price1(mocker, default_conf):
|
|||||||
is_short=False,
|
is_short=False,
|
||||||
amount=0.8,
|
amount=0.8,
|
||||||
stake_amount=18.884 * 0.8,
|
stake_amount=18.884 * 0.8,
|
||||||
|
wallet_balance=18.884 * 0.8,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -45,7 +45,6 @@ def freqai_conf(default_conf, tmpdir):
|
|||||||
"principal_component_analysis": False,
|
"principal_component_analysis": False,
|
||||||
"use_SVM_to_remove_outliers": True,
|
"use_SVM_to_remove_outliers": True,
|
||||||
"stratify_training_data": 0,
|
"stratify_training_data": 0,
|
||||||
"indicator_max_period_candles": 10,
|
|
||||||
"indicator_periods_candles": [10],
|
"indicator_periods_candles": [10],
|
||||||
},
|
},
|
||||||
"data_split_parameters": {"test_size": 0.33, "random_state": 1},
|
"data_split_parameters": {"test_size": 0.33, "random_state": 1},
|
||||||
|
@ -48,10 +48,4 @@ def test_freqai_backtest_load_data(freqai_conf, mocker, caplog):
|
|||||||
|
|
||||||
assert log_has_re('Increasing startup_candle_count for freqai to.*', caplog)
|
assert log_has_re('Increasing startup_candle_count for freqai to.*', caplog)
|
||||||
|
|
||||||
del freqai_conf['freqai']['startup_candles']
|
|
||||||
backtesting = Backtesting(freqai_conf)
|
|
||||||
with pytest.raises(OperationalException,
|
|
||||||
match=r'FreqAI backtesting module.*startup_candles in config.'):
|
|
||||||
backtesting.load_bt_data()
|
|
||||||
|
|
||||||
Backtesting.cleanup()
|
Backtesting.cleanup()
|
||||||
|
@ -174,6 +174,7 @@ def test_train_model_in_series_LightGBMClassifier(mocker, freqai_conf):
|
|||||||
|
|
||||||
def test_start_backtesting(mocker, freqai_conf):
|
def test_start_backtesting(mocker, freqai_conf):
|
||||||
freqai_conf.update({"timerange": "20180120-20180130"})
|
freqai_conf.update({"timerange": "20180120-20180130"})
|
||||||
|
freqai_conf.get("freqai", {}).update({"save_backtest_models": True})
|
||||||
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
|
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
|
||||||
exchange = get_patched_exchange(mocker, freqai_conf)
|
exchange = get_patched_exchange(mocker, freqai_conf)
|
||||||
strategy.dp = DataProvider(freqai_conf, exchange)
|
strategy.dp = DataProvider(freqai_conf, exchange)
|
||||||
@ -192,7 +193,7 @@ def test_start_backtesting(mocker, freqai_conf):
|
|||||||
freqai.start_backtesting(df, metadata, freqai.dk)
|
freqai.start_backtesting(df, metadata, freqai.dk)
|
||||||
model_folders = [x for x in freqai.dd.full_path.iterdir() if x.is_dir()]
|
model_folders = [x for x in freqai.dd.full_path.iterdir() if x.is_dir()]
|
||||||
|
|
||||||
assert len(model_folders) == 5
|
assert len(model_folders) == 6
|
||||||
|
|
||||||
shutil.rmtree(Path(freqai.dk.full_path))
|
shutil.rmtree(Path(freqai.dk.full_path))
|
||||||
|
|
||||||
@ -200,6 +201,7 @@ def test_start_backtesting(mocker, freqai_conf):
|
|||||||
def test_start_backtesting_subdaily_backtest_period(mocker, freqai_conf):
|
def test_start_backtesting_subdaily_backtest_period(mocker, freqai_conf):
|
||||||
freqai_conf.update({"timerange": "20180120-20180124"})
|
freqai_conf.update({"timerange": "20180120-20180124"})
|
||||||
freqai_conf.get("freqai", {}).update({"backtest_period_days": 0.5})
|
freqai_conf.get("freqai", {}).update({"backtest_period_days": 0.5})
|
||||||
|
freqai_conf.get("freqai", {}).update({"save_backtest_models": True})
|
||||||
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
|
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
|
||||||
exchange = get_patched_exchange(mocker, freqai_conf)
|
exchange = get_patched_exchange(mocker, freqai_conf)
|
||||||
strategy.dp = DataProvider(freqai_conf, exchange)
|
strategy.dp = DataProvider(freqai_conf, exchange)
|
||||||
@ -217,13 +219,14 @@ def test_start_backtesting_subdaily_backtest_period(mocker, freqai_conf):
|
|||||||
metadata = {"pair": "LTC/BTC"}
|
metadata = {"pair": "LTC/BTC"}
|
||||||
freqai.start_backtesting(df, metadata, freqai.dk)
|
freqai.start_backtesting(df, metadata, freqai.dk)
|
||||||
model_folders = [x for x in freqai.dd.full_path.iterdir() if x.is_dir()]
|
model_folders = [x for x in freqai.dd.full_path.iterdir() if x.is_dir()]
|
||||||
assert len(model_folders) == 8
|
assert len(model_folders) == 9
|
||||||
|
|
||||||
shutil.rmtree(Path(freqai.dk.full_path))
|
shutil.rmtree(Path(freqai.dk.full_path))
|
||||||
|
|
||||||
|
|
||||||
def test_start_backtesting_from_existing_folder(mocker, freqai_conf, caplog):
|
def test_start_backtesting_from_existing_folder(mocker, freqai_conf, caplog):
|
||||||
freqai_conf.update({"timerange": "20180120-20180130"})
|
freqai_conf.update({"timerange": "20180120-20180130"})
|
||||||
|
freqai_conf.get("freqai", {}).update({"save_backtest_models": True})
|
||||||
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
|
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
|
||||||
exchange = get_patched_exchange(mocker, freqai_conf)
|
exchange = get_patched_exchange(mocker, freqai_conf)
|
||||||
strategy.dp = DataProvider(freqai_conf, exchange)
|
strategy.dp = DataProvider(freqai_conf, exchange)
|
||||||
@ -242,7 +245,7 @@ def test_start_backtesting_from_existing_folder(mocker, freqai_conf, caplog):
|
|||||||
freqai.start_backtesting(df, metadata, freqai.dk)
|
freqai.start_backtesting(df, metadata, freqai.dk)
|
||||||
model_folders = [x for x in freqai.dd.full_path.iterdir() if x.is_dir()]
|
model_folders = [x for x in freqai.dd.full_path.iterdir() if x.is_dir()]
|
||||||
|
|
||||||
assert len(model_folders) == 5
|
assert len(model_folders) == 6
|
||||||
|
|
||||||
# without deleting the exiting folder structure, re-run
|
# without deleting the exiting folder structure, re-run
|
||||||
|
|
||||||
@ -263,10 +266,14 @@ def test_start_backtesting_from_existing_folder(mocker, freqai_conf, caplog):
|
|||||||
freqai.start_backtesting(df, metadata, freqai.dk)
|
freqai.start_backtesting(df, metadata, freqai.dk)
|
||||||
|
|
||||||
assert log_has_re(
|
assert log_has_re(
|
||||||
"Found model at ",
|
"Found backtesting prediction file ",
|
||||||
caplog,
|
caplog,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
path = (freqai.dd.full_path / freqai.dk.backtest_predictions_folder)
|
||||||
|
prediction_files = [x for x in path.iterdir() if x.is_file()]
|
||||||
|
assert len(prediction_files) == 5
|
||||||
|
|
||||||
shutil.rmtree(Path(freqai.dk.full_path))
|
shutil.rmtree(Path(freqai.dk.full_path))
|
||||||
|
|
||||||
|
|
||||||
|
@ -37,6 +37,7 @@ def generate_mock_trade(pair: str, fee: float, is_open: bool,
|
|||||||
trade.orders.append(Order(
|
trade.orders.append(Order(
|
||||||
ft_order_side=trade.entry_side,
|
ft_order_side=trade.entry_side,
|
||||||
order_id=f'{pair}-{trade.entry_side}-{trade.open_date}',
|
order_id=f'{pair}-{trade.entry_side}-{trade.open_date}',
|
||||||
|
ft_is_open=False,
|
||||||
ft_pair=pair,
|
ft_pair=pair,
|
||||||
amount=trade.amount,
|
amount=trade.amount,
|
||||||
filled=trade.amount,
|
filled=trade.amount,
|
||||||
@ -51,6 +52,7 @@ def generate_mock_trade(pair: str, fee: float, is_open: bool,
|
|||||||
trade.orders.append(Order(
|
trade.orders.append(Order(
|
||||||
ft_order_side=trade.exit_side,
|
ft_order_side=trade.exit_side,
|
||||||
order_id=f'{pair}-{trade.exit_side}-{trade.close_date}',
|
order_id=f'{pair}-{trade.exit_side}-{trade.close_date}',
|
||||||
|
ft_is_open=False,
|
||||||
ft_pair=pair,
|
ft_pair=pair,
|
||||||
amount=trade.amount,
|
amount=trade.amount,
|
||||||
filled=trade.amount,
|
filled=trade.amount,
|
||||||
|
@ -4650,11 +4650,17 @@ def test_apply_fee_conditional(default_conf_usdt, fee, mocker,
|
|||||||
fee_close=fee.return_value,
|
fee_close=fee.return_value,
|
||||||
open_order_id="123456"
|
open_order_id="123456"
|
||||||
)
|
)
|
||||||
|
order = Order(
|
||||||
|
ft_order_side='buy',
|
||||||
|
order_id='100',
|
||||||
|
ft_pair=trade.pair,
|
||||||
|
ft_is_open=True,
|
||||||
|
)
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||||
|
|
||||||
walletmock.reset_mock()
|
walletmock.reset_mock()
|
||||||
# Amount is kept as is
|
# Amount is kept as is
|
||||||
assert freqtrade.apply_fee_conditional(trade, 'LTC', amount, fee_abs) == amount_exp
|
assert freqtrade.apply_fee_conditional(trade, 'LTC', amount, fee_abs, order) == amount_exp
|
||||||
assert walletmock.call_count == 1
|
assert walletmock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
@ -581,25 +581,25 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee,
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'exchange,is_short,lev,open_value,close_value,profit,profit_ratio,trading_mode,funding_fees', [
|
'exchange,is_short,lev,open_value,close_value,profit,profit_ratio,trading_mode,funding_fees', [
|
||||||
("binance", False, 1, 60.15, 65.835, 5.685, 0.09451371, spot, 0.0),
|
("binance", False, 1, 60.15, 65.835, 5.685, 0.09451371, spot, 0.0),
|
||||||
("binance", True, 1, 59.850, 66.1663784375, -6.3163784375, -0.1055368, margin, 0.0),
|
("binance", True, 1, 65.835, 60.151253125, 5.68374687, 0.08633321, margin, 0.0),
|
||||||
("binance", False, 3, 60.15, 65.83416667, 5.68416667, 0.28349958, margin, 0.0),
|
("binance", False, 3, 60.15, 65.83416667, 5.68416667, 0.28349958, margin, 0.0),
|
||||||
("binance", True, 3, 59.85, 66.1663784375, -6.3163784375, -0.31661044, margin, 0.0),
|
("binance", True, 3, 65.835, 60.151253125, 5.68374687, 0.25899963, margin, 0.0),
|
||||||
|
|
||||||
("kraken", False, 1, 60.15, 65.835, 5.685, 0.09451371, spot, 0.0),
|
("kraken", False, 1, 60.15, 65.835, 5.685, 0.09451371, spot, 0.0),
|
||||||
("kraken", True, 1, 59.850, 66.231165, -6.381165, -0.1066192, margin, 0.0),
|
("kraken", True, 1, 65.835, 60.21015, 5.62485, 0.0854386, margin, 0.0),
|
||||||
("kraken", False, 3, 60.15, 65.795, 5.645, 0.28154613, margin, 0.0),
|
("kraken", False, 3, 60.15, 65.795, 5.645, 0.28154613, margin, 0.0),
|
||||||
("kraken", True, 3, 59.850, 66.231165, -6.381165, -0.3198578, margin, 0.0),
|
("kraken", True, 3, 65.835, 60.21015, 5.62485, 0.25631579, margin, 0.0),
|
||||||
|
|
||||||
("binance", False, 1, 60.15, 65.835, 5.685, 0.09451371, futures, 0.0),
|
("binance", False, 1, 60.15, 65.835, 5.685, 0.09451371, futures, 0.0),
|
||||||
("binance", False, 1, 60.15, 66.835, 6.685, 0.11113881, futures, 1.0),
|
("binance", False, 1, 60.15, 66.835, 6.685, 0.11113881, futures, 1.0),
|
||||||
("binance", True, 1, 59.85, 66.165, -6.315, -0.10551378, futures, 0.0),
|
("binance", True, 1, 65.835, 60.15, 5.685, 0.08635224, futures, 0.0),
|
||||||
("binance", True, 1, 59.85, 67.165, -7.315, -0.12222222, futures, -1.0),
|
("binance", True, 1, 65.835, 61.15, 4.685, 0.07116276, futures, -1.0),
|
||||||
|
("binance", True, 3, 65.835, 59.15, 6.685, 0.3046252, futures, 1.0),
|
||||||
("binance", False, 3, 60.15, 64.835, 4.685, 0.23366583, futures, -1.0),
|
("binance", False, 3, 60.15, 64.835, 4.685, 0.23366583, futures, -1.0),
|
||||||
("binance", True, 3, 59.85, 65.165, -5.315, -0.26641604, futures, 1.0),
|
|
||||||
])
|
])
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_calc_open_close_trade_price(
|
def test_calc_open_close_trade_price(
|
||||||
limit_buy_order_usdt, limit_sell_order_usdt, fee, exchange, is_short, lev,
|
limit_order, fee, exchange, is_short, lev,
|
||||||
open_value, close_value, profit, profit_ratio, trading_mode, funding_fees
|
open_value, close_value, profit, profit_ratio, trading_mode, funding_fees
|
||||||
):
|
):
|
||||||
trade: Trade = Trade(
|
trade: Trade = Trade(
|
||||||
@ -617,22 +617,24 @@ def test_calc_open_close_trade_price(
|
|||||||
trading_mode=trading_mode,
|
trading_mode=trading_mode,
|
||||||
funding_fees=funding_fees
|
funding_fees=funding_fees
|
||||||
)
|
)
|
||||||
|
entry_order = limit_order[trade.entry_side]
|
||||||
|
exit_order = limit_order[trade.exit_side]
|
||||||
trade.open_order_id = f'something-{is_short}-{lev}-{exchange}'
|
trade.open_order_id = f'something-{is_short}-{lev}-{exchange}'
|
||||||
|
|
||||||
oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy')
|
oobj = Order.parse_from_ccxt_object(entry_order, 'ADA/USDT', trade.entry_side)
|
||||||
|
trade.orders.append(oobj)
|
||||||
trade.update_trade(oobj)
|
trade.update_trade(oobj)
|
||||||
|
|
||||||
oobj = Order.parse_from_ccxt_object(limit_sell_order_usdt, 'ADA/USDT', 'sell')
|
oobj = Order.parse_from_ccxt_object(exit_order, 'ADA/USDT', trade.exit_side)
|
||||||
|
trade.orders.append(oobj)
|
||||||
trade.update_trade(oobj)
|
trade.update_trade(oobj)
|
||||||
|
|
||||||
trade.open_rate = 2.0
|
assert trade.is_open is False
|
||||||
trade.close_rate = 2.2
|
|
||||||
trade.recalc_open_trade_value()
|
|
||||||
assert pytest.approx(trade._calc_open_trade_value(trade.amount, trade.open_rate)) == open_value
|
assert pytest.approx(trade._calc_open_trade_value(trade.amount, trade.open_rate)) == open_value
|
||||||
assert pytest.approx(trade.calc_close_trade_value(trade.close_rate)) == close_value
|
assert pytest.approx(trade.calc_close_trade_value(trade.close_rate)) == close_value
|
||||||
assert pytest.approx(trade.calc_profit(trade.close_rate)) == round(profit, 8)
|
assert pytest.approx(trade.close_profit_abs) == profit
|
||||||
assert pytest.approx(trade.calc_profit_ratio(trade.close_rate)) == profit_ratio
|
assert pytest.approx(trade.close_profit) == profit_ratio
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
@ -654,6 +656,7 @@ def test_trade_close(fee):
|
|||||||
trade.orders.append(Order(
|
trade.orders.append(Order(
|
||||||
ft_order_side=trade.entry_side,
|
ft_order_side=trade.entry_side,
|
||||||
order_id=f'{trade.pair}-{trade.entry_side}-{trade.open_date}',
|
order_id=f'{trade.pair}-{trade.entry_side}-{trade.open_date}',
|
||||||
|
ft_is_open=False,
|
||||||
ft_pair=trade.pair,
|
ft_pair=trade.pair,
|
||||||
amount=trade.amount,
|
amount=trade.amount,
|
||||||
filled=trade.amount,
|
filled=trade.amount,
|
||||||
@ -667,6 +670,7 @@ def test_trade_close(fee):
|
|||||||
trade.orders.append(Order(
|
trade.orders.append(Order(
|
||||||
ft_order_side=trade.exit_side,
|
ft_order_side=trade.exit_side,
|
||||||
order_id=f'{trade.pair}-{trade.exit_side}-{trade.open_date}',
|
order_id=f'{trade.pair}-{trade.exit_side}-{trade.open_date}',
|
||||||
|
ft_is_open=False,
|
||||||
ft_pair=trade.pair,
|
ft_pair=trade.pair,
|
||||||
amount=trade.amount,
|
amount=trade.amount,
|
||||||
filled=trade.amount,
|
filled=trade.amount,
|
||||||
|
Loading…
Reference in New Issue
Block a user