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
|
||||||
|
|
||||||
|
@ -38,14 +38,14 @@ The example strategy, example prediction model, and example config can be found
|
|||||||
The user provides FreqAI with a set of custom *base* indicators (the same way as in a typical Freqtrade strategy) as well as target values (*labels*).
|
The user provides FreqAI with a set of custom *base* indicators (the same way as in a typical Freqtrade strategy) as well as target values (*labels*).
|
||||||
FreqAI trains a model to predict the target values based on the input of custom indicators, for each pair in the whitelist. These models are consistently retrained to adapt to market conditions. FreqAI offers the ability to both backtest strategies (emulating reality with periodic retraining) and deploy dry/live runs. In dry/live conditions, FreqAI can be set to constant retraining in a background thread in an effort to keep models as up to date as possible.
|
FreqAI trains a model to predict the target values based on the input of custom indicators, for each pair in the whitelist. These models are consistently retrained to adapt to market conditions. FreqAI offers the ability to both backtest strategies (emulating reality with periodic retraining) and deploy dry/live runs. In dry/live conditions, FreqAI can be set to constant retraining in a background thread in an effort to keep models as up to date as possible.
|
||||||
|
|
||||||
An overview of the algorithm is shown below, explaining the data processing pipeline and the model usage.
|
An overview of the algorithm is shown below, explaining the data processing pipeline and the model usage.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### Important machine learning vocabulary
|
### Important machine learning vocabulary
|
||||||
|
|
||||||
**Features** - the quantities with which a model is trained. All features for a single candle is stored as a vector. In FreqAI, the user
|
**Features** - the quantities with which a model is trained. All features for a single candle is stored as a vector. In FreqAI, the user
|
||||||
builds the feature sets from anything they can construct in the strategy.
|
builds the feature sets from anything they can construct in the strategy.
|
||||||
|
|
||||||
**Labels** - the target values that a model is trained
|
**Labels** - the target values that a model is trained
|
||||||
toward. Each set of features is associated with a single label that is
|
toward. Each set of features is associated with a single label that is
|
||||||
@ -53,12 +53,12 @@ defined by the user within the strategy. These labels intentionally look into th
|
|||||||
future, and are not available to the model during dry/live/backtesting.
|
future, and are not available to the model during dry/live/backtesting.
|
||||||
|
|
||||||
**Training** - the process of feeding individual feature sets, composed of historic data, with associated labels into the
|
**Training** - the process of feeding individual feature sets, composed of historic data, with associated labels into the
|
||||||
model with the goal of matching input feature sets to associated labels.
|
model with the goal of matching input feature sets to associated labels.
|
||||||
|
|
||||||
**Train data** - a subset of the historic data that is fed to the model during
|
**Train data** - a subset of the historic data that is fed to the model during
|
||||||
training. This data directly influences weight connections in the model.
|
training. This data directly influences weight connections in the model.
|
||||||
|
|
||||||
**Test data** - a subset of the historic data that is used to evaluate the performance of the model after training. This data does not influence nodal weights within the model.
|
**Test data** - a subset of the historic data that is used to evaluate the performance of the model after training. This data does not influence nodal weights within the model.
|
||||||
|
|
||||||
## Install prerequisites
|
## Install prerequisites
|
||||||
|
|
||||||
@ -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.
|
||||||
@ -104,11 +104,11 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
| `include_corr_pairlist` | A list of correlated coins that FreqAI will add as additional features to all `pair_whitelist` coins. All indicators set in `populate_any_indicators` during feature engineering (see details [here](#feature-engineering)) will be created for each coin in this list, and that set of features is added to the base asset feature set. <br> **Datatype:** List of assets (strings).
|
| `include_corr_pairlist` | A list of correlated coins that FreqAI will add as additional features to all `pair_whitelist` coins. All indicators set in `populate_any_indicators` during feature engineering (see details [here](#feature-engineering)) will be created for each coin in this list, and that set of features is added to the base asset feature set. <br> **Datatype:** List of assets (strings).
|
||||||
| `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.
|
||||||
| `DI_threshold` | Activates the Dissimilarity Index for outlier detection when > 0. See details about how it works [here](#removing-outliers-with-the-dissimilarity-index). <br> **Datatype:** Positive float (typically < 1).
|
| `DI_threshold` | Activates the Dissimilarity Index for outlier detection when > 0. See details about how it works [here](#removing-outliers-with-the-dissimilarity-index). <br> **Datatype:** Positive float (typically < 1).
|
||||||
| `use_SVM_to_remove_outliers` | Train a support vector machine to detect and remove outliers from the training data set, as well as from incoming data points. See details about how it works [here](#removing-outliers-using-a-support-vector-machine-svm). <br> **Datatype:** Boolean.
|
| `use_SVM_to_remove_outliers` | Train a support vector machine to detect and remove outliers from the training data set, as well as from incoming data points. See details about how it works [here](#removing-outliers-using-a-support-vector-machine-svm). <br> **Datatype:** Boolean.
|
||||||
| `svm_params` | All parameters available in Sklearn's `SGDOneClassSVM()`. See details about some select parameters [here](#removing-outliers-using-a-support-vector-machine-svm). <br> **Datatype:** Dictionary.
|
| `svm_params` | All parameters available in Sklearn's `SGDOneClassSVM()`. See details about some select parameters [here](#removing-outliers-using-a-support-vector-machine-svm). <br> **Datatype:** Dictionary.
|
||||||
@ -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()
|
||||||
@ -200,9 +202,9 @@ The FreqAI strategy requires the user to include the following lines of code in
|
|||||||
|
|
||||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
|
||||||
# the model will return all labels created by user in `populate_any_indicators`
|
# the model will return all labels created by user in `populate_any_indicators`
|
||||||
# (& appended targets), an indication of whether or not the prediction should be accepted,
|
# (& appended targets), an indication of whether or not the prediction should be accepted,
|
||||||
# the target mean/std values for each of the labels created by user in
|
# the target mean/std values for each of the labels created by user in
|
||||||
# `populate_any_indicators()` for each training period.
|
# `populate_any_indicators()` for each training period.
|
||||||
|
|
||||||
dataframe = self.freqai.start(dataframe, metadata, self)
|
dataframe = self.freqai.start(dataframe, metadata, self)
|
||||||
@ -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.
|
||||||
@ -310,7 +323,7 @@ The user is encouraged to inherit `train()` and `predict()` to let them customiz
|
|||||||
## Feature engineering
|
## Feature engineering
|
||||||
|
|
||||||
Features are added by the user inside the `populate_any_indicators()` method of the strategy
|
Features are added by the user inside the `populate_any_indicators()` method of the strategy
|
||||||
by prepending indicators with `%`, and labels with `&`.
|
by prepending indicators with `%`, and labels with `&`.
|
||||||
|
|
||||||
There are some important components/structures that the user *must* include when building their feature set; the use of these is shown below:
|
There are some important components/structures that the user *must* include when building their feature set; the use of these is shown below:
|
||||||
|
|
||||||
@ -419,13 +432,13 @@ In total, the number of features the user of the presented example strat has cre
|
|||||||
length of `include_timeframes` * no. features in `populate_any_indicators()` * length of `include_corr_pairlist` * no. `include_shifted_candles` * length of `indicator_periods_candles`
|
length of `include_timeframes` * no. features in `populate_any_indicators()` * length of `include_corr_pairlist` * no. `include_shifted_candles` * length of `indicator_periods_candles`
|
||||||
$= 3 * 3 * 3 * 2 * 2 = 108$.
|
$= 3 * 3 * 3 * 2 * 2 = 108$.
|
||||||
|
|
||||||
Another structure to consider is the location of the labels at the bottom of the example function (below `if set_generalized_indicators:`).
|
Another structure to consider is the location of the labels at the bottom of the example function (below `if set_generalized_indicators:`).
|
||||||
This is where the user will add single features and labels to their feature set to avoid duplication of them from
|
This is where the user will add single features and labels to their feature set to avoid duplication of them from
|
||||||
various configuration parameters that multiply the feature set, such as `include_timeframes`.
|
various configuration parameters that multiply the feature set, such as `include_timeframes`.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Features **must** be defined in `populate_any_indicators()`. Definining FreqAI features in `populate_indicators()`
|
Features **must** be defined in `populate_any_indicators()`. Definining FreqAI features in `populate_indicators()`
|
||||||
will cause the algorithm to fail in live/dry mode. If the user wishes to add generalized features that are not associated with
|
will cause the algorithm to fail in live/dry mode. If the user wishes to add generalized features that are not associated with
|
||||||
a specific pair or timeframe, they should use the following structure inside `populate_any_indicators()`
|
a specific pair or timeframe, they should use the following structure inside `populate_any_indicators()`
|
||||||
(as exemplified in `freqtrade/templates/FreqaiExampleStrategy.py`):
|
(as exemplified in `freqtrade/templates/FreqaiExampleStrategy.py`):
|
||||||
|
|
||||||
@ -434,7 +447,7 @@ various configuration parameters that multiply the feature set, such as `include
|
|||||||
|
|
||||||
...
|
...
|
||||||
|
|
||||||
# Add generalized indicators here (because in live, it will call only this function to populate
|
# Add generalized indicators here (because in live, it will call only this function to populate
|
||||||
# indicators for retraining). Notice how we ensure not to add them multiple times by associating
|
# indicators for retraining). Notice how we ensure not to add them multiple times by associating
|
||||||
# these generalized indicators to the basepair/timeframe
|
# these generalized indicators to the basepair/timeframe
|
||||||
if set_generalized_indicators:
|
if set_generalized_indicators:
|
||||||
@ -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,23 +542,17 @@ 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
|
||||||
a certain number of hours old by setting the `expiration_hours` in the config file:
|
a certain number of hours old by setting the `expiration_hours` in the config file:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@ -632,15 +639,15 @@ The user can stratify (group) the training/testing data using:
|
|||||||
|
|
||||||
This will split the data chronologically so that every Xth data point is used to test the model after training. In the
|
This will split the data chronologically so that every Xth data point is used to test the model after training. In the
|
||||||
example above, the user is asking for every third data point in the dataframe to be used for
|
example above, the user is asking for every third data point in the dataframe to be used for
|
||||||
testing; the other points are used for training.
|
testing; the other points are used for training.
|
||||||
|
|
||||||
The test data is used to evaluate the performance of the model after training. If the test score is high, the model is able to capture the behavior of the data well. If the test score is low, either the model either does not capture the complexity of the data, the test data is significantly different from the train data, or a different model should be used.
|
The test data is used to evaluate the performance of the model after training. If the test score is high, the model is able to capture the behavior of the data well. If the test score is low, either the model either does not capture the complexity of the data, the test data is significantly different from the train data, or a different model should be used.
|
||||||
|
|
||||||
### Controlling the model learning process
|
### Controlling the model learning process
|
||||||
|
|
||||||
Model training parameters are unique to the machine learning library selected by the user. FreqAI allows the user to set any parameter for any library using the `model_training_parameters` dictionary in the user configuration file. The example configuration file (found in `config_examples/config_freqai.example.json`) show some of the example parameters associated with `Catboost` and `LightGBM`, but the user can add any parameters available in those libraries.
|
Model training parameters are unique to the machine learning library selected by the user. FreqAI allows the user to set any parameter for any library using the `model_training_parameters` dictionary in the user configuration file. The example configuration file (found in `config_examples/config_freqai.example.json`) show some of the example parameters associated with `Catboost` and `LightGBM`, but the user can add any parameters available in those libraries.
|
||||||
|
|
||||||
Data split parameters are defined in `data_split_parameters` which can be any parameters associated with `Sklearn`'s `train_test_split()` function.
|
Data split parameters are defined in `data_split_parameters` which can be any parameters associated with `Sklearn`'s `train_test_split()` function.
|
||||||
|
|
||||||
FreqAI includes some additional parameters such as `weight_factor`, which allows the user to weight more recent data more strongly
|
FreqAI includes some additional parameters such as `weight_factor`, which allows the user to weight more recent data more strongly
|
||||||
than past data via an exponential function:
|
than past data via an exponential function:
|
||||||
@ -670,7 +677,7 @@ The user can tell FreqAI to remove outlier data points from the training/test da
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Equity and crypto markets suffer from a high level of non-patterned noise in the form of outlier data points. The Dissimilarity Index (DI) aims to quantify the uncertainty associated with each prediction made by the model. The DI allows predictions which are outliers (not existent in the model feature space) to be thrown out due to low levels of certainty.
|
Equity and crypto markets suffer from a high level of non-patterned noise in the form of outlier data points. The Dissimilarity Index (DI) aims to quantify the uncertainty associated with each prediction made by the model. The DI allows predictions which are outliers (not existent in the model feature space) to be thrown out due to low levels of certainty.
|
||||||
|
|
||||||
To do so, FreqAI measures the distance between each training data point (feature vector), $X_{a}$, and all other training data points:
|
To do so, FreqAI measures the distance between each training data point (feature vector), $X_{a}$, and all other training data points:
|
||||||
|
|
||||||
@ -688,7 +695,7 @@ which enables the estimation of the Dissimilarity Index as:
|
|||||||
|
|
||||||
$$ DI_k = d_k/\overline{d} $$
|
$$ DI_k = d_k/\overline{d} $$
|
||||||
|
|
||||||
The user can tweak the DI through the `DI_threshold` to increase or decrease the extrapolation of the trained model.
|
The user can tweak the DI through the `DI_threshold` to increase or decrease the extrapolation of the trained model.
|
||||||
|
|
||||||
Below is a figure that describes the DI for a 3D data set.
|
Below is a figure that describes the DI for a 3D data set.
|
||||||
|
|
||||||
@ -707,11 +714,11 @@ The user can tell FreqAI to remove outlier data points from the training/test da
|
|||||||
```
|
```
|
||||||
|
|
||||||
FreqAI will train an SVM on the training data (or components of it if the user activated
|
FreqAI will train an SVM on the training data (or components of it if the user activated
|
||||||
`principal_component_analysis`) and remove any data point that the SVM deems to be beyond the feature space.
|
`principal_component_analysis`) and remove any data point that the SVM deems to be beyond the feature space.
|
||||||
|
|
||||||
The parameter `shuffle` is by default set to `False` to ensure consistent results. If it is set to `True`, running the SVM multiple times on the same data set might result in different outcomes due to `max_iter` being to low for the algorithm to reach the demanded `tol`. Increasing `max_iter` solves this issue but causes the procedure to take longer time.
|
The parameter `shuffle` is by default set to `False` to ensure consistent results. If it is set to `True`, running the SVM multiple times on the same data set might result in different outcomes due to `max_iter` being to low for the algorithm to reach the demanded `tol`. Increasing `max_iter` solves this issue but causes the procedure to take longer time.
|
||||||
|
|
||||||
The parameter `nu`, *very* broadly, is the amount of data points that should be considered outliers.
|
The parameter `nu`, *very* broadly, is the amount of data points that should be considered outliers.
|
||||||
|
|
||||||
#### Removing outliers with DBSCAN
|
#### Removing outliers with DBSCAN
|
||||||
|
|
||||||
|
@ -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,10 +127,9 @@ 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"]
|
)
|
||||||
)
|
|
||||||
dk = self.start_backtesting(dataframe, metadata, self.dk)
|
dk = self.start_backtesting(dataframe, metadata, self.dk)
|
||||||
|
|
||||||
dataframe = dk.remove_features_from_df(dk.return_dataframe)
|
dataframe = dk.remove_features_from_df(dk.return_dataframe)
|
||||||
@ -224,28 +226,39 @@ 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}"
|
||||||
)
|
)
|
||||||
if not self.model_exists(
|
|
||||||
metadata["pair"], dk, trained_timestamp=int(trained_timestamp.stopts)
|
dk.set_new_model_names(metadata["pair"], trained_timestamp)
|
||||||
):
|
|
||||||
dk.find_features(dataframe_train)
|
if dk.check_if_backtest_prediction_exists():
|
||||||
self.model = self.train(dataframe_train, metadata["pair"], dk)
|
append_df = dk.get_backtesting_prediction()
|
||||||
self.dd.pair_dict[metadata["pair"]]["trained_timestamp"] = int(
|
dk.append_predictions(append_df)
|
||||||
trained_timestamp.stopts)
|
|
||||||
dk.set_new_model_names(metadata["pair"], trained_timestamp)
|
|
||||||
self.dd.save_data(self.model, metadata["pair"], dk)
|
|
||||||
else:
|
else:
|
||||||
self.model = self.dd.load_data(metadata["pair"], dk)
|
if not self.model_exists(
|
||||||
|
metadata["pair"], dk, trained_timestamp=trained_timestamp_int
|
||||||
|
):
|
||||||
|
dk.find_features(dataframe_train)
|
||||||
|
self.model = self.train(dataframe_train, metadata["pair"], dk)
|
||||||
|
self.dd.pair_dict[metadata["pair"]]["trained_timestamp"] = int(
|
||||||
|
trained_timestamp.stopts)
|
||||||
|
|
||||||
self.check_if_feature_list_matches_strategy(dataframe_train, dk)
|
if self.save_backtest_models:
|
||||||
|
logger.info('Saving backtest model to disk.')
|
||||||
|
self.dd.save_data(self.model, metadata["pair"], dk)
|
||||||
|
else:
|
||||||
|
self.model = self.dd.load_data(metadata["pair"], dk)
|
||||||
|
|
||||||
pred_df, do_preds = self.predict(dataframe_backtest, dk)
|
self.check_if_feature_list_matches_strategy(dataframe_train, dk)
|
||||||
|
|
||||||
dk.append_predictions(pred_df, do_preds)
|
pred_df, do_preds = self.predict(dataframe_backtest, dk)
|
||||||
|
append_df = dk.get_predictions_to_append(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