Merge branch 'develop' into feat/freqai-rl-dev
This commit is contained in:
commit
52b774b5eb
@ -1,10 +1,10 @@
|
|||||||
# Configuration
|
# Configuration
|
||||||
|
|
||||||
`FreqAI` is configured through the typical [Freqtrade config file](configuration.md) and the standard [Freqtrade strategy](strategy-customization.md). Examples of `FreqAI` config and strategy files can be found in `config_examples/config_freqai.example.json` and `freqtrade/templates/FreqaiExampleStrategy.py`, respectively.
|
FreqAI is configured through the typical [Freqtrade config file](configuration.md) and the standard [Freqtrade strategy](strategy-customization.md). Examples of FreqAI config and strategy files can be found in `config_examples/config_freqai.example.json` and `freqtrade/templates/FreqaiExampleStrategy.py`, respectively.
|
||||||
|
|
||||||
## Setting up the configuration file
|
## Setting up the configuration file
|
||||||
|
|
||||||
Although there are plenty of additional parameters to choose from, as highlighted in the [parameter table](freqai-parameter-table.md#parameter-table), a `FreqAI` config must at minimum include the following parameters (the parameter values are only examples):
|
Although there are plenty of additional parameters to choose from, as highlighted in the [parameter table](freqai-parameter-table.md#parameter-table), a FreqAI config must at minimum include the following parameters (the parameter values are only examples):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"freqai": {
|
"freqai": {
|
||||||
@ -35,9 +35,9 @@
|
|||||||
|
|
||||||
A full example config is available in `config_examples/config_freqai.example.json`.
|
A full example config is available in `config_examples/config_freqai.example.json`.
|
||||||
|
|
||||||
## Building a `FreqAI` strategy
|
## Building a FreqAI strategy
|
||||||
|
|
||||||
The `FreqAI` strategy requires including the following lines of code in the standard [Freqtrade strategy](strategy-customization.md):
|
The FreqAI strategy requires including the following lines of code in the standard [Freqtrade strategy](strategy-customization.md):
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# user should define the maximum startup candle count (the largest number of candles
|
# user should define the maximum startup candle count (the largest number of candles
|
||||||
@ -129,7 +129,7 @@ Notice also the location of the labels under `if set_generalized_indicators:` at
|
|||||||
The `self.freqai.start()` function cannot be called outside the `populate_indicators()`.
|
The `self.freqai.start()` function cannot be called outside the `populate_indicators()`.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Features **must** be defined in `populate_any_indicators()`. Defining `FreqAI` features in `populate_indicators()`
|
Features **must** be defined in `populate_any_indicators()`. Defining FreqAI features in `populate_indicators()`
|
||||||
will cause the algorithm to fail in live/dry mode. In order to add generalized features that are not associated with a specific pair or timeframe, the following structure inside `populate_any_indicators()` should be used
|
will cause the algorithm to fail in live/dry mode. In order to add generalized features that are not associated with a specific pair or timeframe, the following structure inside `populate_any_indicators()` should be used
|
||||||
(as exemplified in `freqtrade/templates/FreqaiExampleStrategy.py`):
|
(as exemplified in `freqtrade/templates/FreqaiExampleStrategy.py`):
|
||||||
|
|
||||||
@ -166,15 +166,15 @@ Below are the values you can expect to include/use inside a typical strategy dat
|
|||||||
|
|
||||||
| DataFrame Key | Description |
|
| DataFrame Key | Description |
|
||||||
|------------|-------------|
|
|------------|-------------|
|
||||||
| `df['&*']` | Any dataframe column prepended with `&` in `populate_any_indicators()` is treated as a training target (label) inside `FreqAI` (typically following the naming convention `&-s*`). The names of these dataframe columns are fed back as the predictions. For example, to predict the price change in the next 40 candles (similar to `templates/FreqaiExampleStrategy.py`), you would set `df['&-s_close']`. `FreqAI` makes the predictions and gives them back under the same key (`df['&-s_close']`) to be used in `populate_entry/exit_trend()`. <br> **Datatype:** Depends on the output of the model.
|
| `df['&*']` | Any dataframe column prepended with `&` in `populate_any_indicators()` is treated as a training target (label) inside FreqAI (typically following the naming convention `&-s*`). For example, to predict the close price 40 candles into the future, you would set `df['&-s_close'] = df['close'].shift(-self.freqai_info["feature_parameters"]["label_period_candles"])` with `"label_period_candles": 40` in the config. FreqAI makes the predictions and gives them back under the same key (`df['&-s_close']`) to be used in `populate_entry/exit_trend()`. <br> **Datatype:** Depends on the output of the model.
|
||||||
| `df['&*_std/mean']` | Standard deviation and mean values of the defined labels during training (or live tracking with `fit_live_predictions_candles`). Commonly used to understand the rarity of a prediction (use the z-score as shown in `templates/FreqaiExampleStrategy.py` and explained [here](#creating-a-dynamic-target-threshold) to evaluate how often a particular prediction was observed during training or historically with `fit_live_predictions_candles`). <br> **Datatype:** Float.
|
| `df['&*_std/mean']` | Standard deviation and mean values of the defined labels during training (or live tracking with `fit_live_predictions_candles`). Commonly used to understand the rarity of a prediction (use the z-score as shown in `templates/FreqaiExampleStrategy.py` and explained [here](#creating-a-dynamic-target-threshold) to evaluate how often a particular prediction was observed during training or historically with `fit_live_predictions_candles`). <br> **Datatype:** Float.
|
||||||
| `df['do_predict']` | Indication of an outlier data point. The return value is integer between -1 and 2, which lets you know if the prediction is trustworthy or not. `do_predict==1` means that the prediction is trustworthy. If the Dissimilarity Index (DI, see details [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di)) of the input data point is above the threshold defined in the config, `FreqAI` will subtract 1 from `do_predict`, resulting in `do_predict==0`. If `use_SVM_to_remove_outliers()` is active, the Support Vector Machine (SVM, see details [here](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm)) may also detect outliers in training and prediction data. In this case, the SVM will also subtract 1 from `do_predict`. If the input data point was considered an outlier by the SVM but not by the DI, or vice versa, the result will be `do_predict==0`. If both the DI and the SVM considers the input data point to be an outlier, the result will be `do_predict==-1`. A particular case is when `do_predict == 2`, which means that the model has expired due to exceeding `expired_hours`. <br> **Datatype:** Integer between -1 and 2.
|
| `df['do_predict']` | Indication of an outlier data point. The return value is integer between -2 and 2, which lets you know if the prediction is trustworthy or not. `do_predict==1` means that the prediction is trustworthy. If the Dissimilarity Index (DI, see details [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di)) of the input data point is above the threshold defined in the config, FreqAI will subtract 1 from `do_predict`, resulting in `do_predict==0`. If `use_SVM_to_remove_outliers()` is active, the Support Vector Machine (SVM, see details [here](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm)) may also detect outliers in training and prediction data. In this case, the SVM will also subtract 1 from `do_predict`. If the input data point was considered an outlier by the SVM but not by the DI, or vice versa, the result will be `do_predict==0`. If both the DI and the SVM considers the input data point to be an outlier, the result will be `do_predict==-1`. As with the SVM, if `use_DBSCAN_to_remove_outliers` is active, DBSCAN (see details [here](freqai-feature-engineering.md#identifying-outliers-with-dbscan)) may also detect outliers and subtract 1 from `do_predict`. Hence, if both the SVM and DBSCAN are active and identify a datapoint that was above the DI threshold as an outlier, the result will be `do_predict==-2`. A particular case is when `do_predict == 2`, which means that the model has expired due to exceeding `expired_hours`. <br> **Datatype:** Integer between -2 and 2.
|
||||||
| `df['DI_values']` | Dissimilarity Index (DI) values are proxies for the level of confidence `FreqAI` has in the prediction. A lower DI means the prediction is close to the training data, i.e., higher prediction confidence. See details about the DI [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di). <br> **Datatype:** Float.
|
| `df['DI_values']` | Dissimilarity Index (DI) values are proxies for the level of confidence FreqAI has in the prediction. A lower DI means the prediction is close to the training data, i.e., higher prediction confidence. See details about the DI [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di). <br> **Datatype:** Float.
|
||||||
| `df['%*']` | Any dataframe column prepended with `%` in `populate_any_indicators()` is treated as a training feature. For example, you can include the RSI in the training feature set (similar to in `templates/FreqaiExampleStrategy.py`) by setting `df['%-rsi']`. See more details on how this is done [here](freqai-feature-engineering.md). <br> **Note:** Since the number of features prepended with `%` can multiply very quickly (10s of thousands of features is easily engineered using the multiplictative functionality described in the `feature_parameters` table shown above), these features are removed from the dataframe upon return from `FreqAI`. To keep a particular type of feature for plotting purposes, you would prepend it with `%%`. <br> **Datatype:** Depends on the output of the model.
|
| `df['%*']` | Any dataframe column prepended with `%` in `populate_any_indicators()` is treated as a training feature. For example, you can include the RSI in the training feature set (similar to in `templates/FreqaiExampleStrategy.py`) by setting `df['%-rsi']`. See more details on how this is done [here](freqai-feature-engineering.md). <br> **Note:** Since the number of features prepended with `%` can multiply very quickly (10s of thousands of features are easily engineered using the multiplictative functionality of, e.g., `include_shifted_candles` and `include_timeframes` as described in the [parameter table](freqai-parameter-table.md)), these features are removed from the dataframe that is returned from FreqAI to the strategy. To keep a particular type of feature for plotting purposes, you would prepend it with `%%`. <br> **Datatype:** Depends on the output of the model.
|
||||||
|
|
||||||
## Setting the `startup_candle_count`
|
## Setting the `startup_candle_count`
|
||||||
|
|
||||||
The `startup_candle_count` in the `FreqAI` strategy needs to be set up in the same way as in the standard 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 the `dataprovider`, to avoid any NaNs at the beginning of the first training. You can easily set this value by identifying the longest period (in candle units) which is passed to the indicator creation functions (e.g., Ta-Lib functions). In the presented example, `startup_candle_count` is 20 since this is the maximum value in `indicators_periods_candles`.
|
The `startup_candle_count` in the FreqAI strategy needs to be set up in the same way as in the standard 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 the `dataprovider`, to avoid any NaNs at the beginning of the first training. You can easily set this value by identifying the longest period (in candle units) which is passed to the indicator creation functions (e.g., Ta-Lib functions). In the presented example, `startup_candle_count` is 20 since this is the maximum value in `indicators_periods_candles`.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
There are instances where the Ta-Lib functions actually require more data than just the passed `period` or else the feature dataset gets populated with NaNs. Anecdotally, multiplying the `startup_candle_count` by 2 always leads to a fully NaN free training dataset. Hence, it is typically safest to multiply the expected `startup_candle_count` by 2. Look out for this log message to confirm that the data is clean:
|
There are instances where the Ta-Lib functions actually require more data than just the passed `period` or else the feature dataset gets populated with NaNs. Anecdotally, multiplying the `startup_candle_count` by 2 always leads to a fully NaN free training dataset. Hence, it is typically safest to multiply the expected `startup_candle_count` by 2. Look out for this log message to confirm that the data is clean:
|
||||||
@ -185,7 +185,7 @@ The `startup_candle_count` in the `FreqAI` strategy needs to be set up in the sa
|
|||||||
|
|
||||||
## Creating a dynamic target threshold
|
## Creating a dynamic target threshold
|
||||||
|
|
||||||
Deciding when to enter or exit a trade can be done in a dynamic way to reflect current market conditions. `FreqAI` allows you to return additional information from the training of a model (more info [here](freqai-feature-engineering.md#returning-additional-info-from-training)). For example, the `&*_std/mean` return values describe the statistical distribution of the target/label *during the most recent training*. Comparing a given prediction to these values allows you to know the rarity of the prediction. In `templates/FreqaiExampleStrategy.py`, the `target_roi` and `sell_roi` are defined to be 1.25 z-scores away from the mean which causes predictions that are closer to the mean to be filtered out.
|
Deciding when to enter or exit a trade can be done in a dynamic way to reflect current market conditions. FreqAI allows you to return additional information from the training of a model (more info [here](freqai-feature-engineering.md#returning-additional-info-from-training)). For example, the `&*_std/mean` return values describe the statistical distribution of the target/label *during the most recent training*. Comparing a given prediction to these values allows you to know the rarity of the prediction. In `templates/FreqaiExampleStrategy.py`, the `target_roi` and `sell_roi` are defined to be 1.25 z-scores away from the mean which causes predictions that are closer to the mean to be filtered out.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
dataframe["target_roi"] = dataframe["&-s_close_mean"] + dataframe["&-s_close_std"] * 1.25
|
dataframe["target_roi"] = dataframe["&-s_close_mean"] + dataframe["&-s_close_std"] * 1.25
|
||||||
@ -200,15 +200,15 @@ To consider the population of *historical predictions* for creating the dynamic
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
If this value is set, `FreqAI` will initially use the predictions from the training data and subsequently begin introducing real prediction data as it is generated. `FreqAI` will save this historical data to be reloaded if you stop and restart a model with the same `identifier`.
|
If this value is set, FreqAI will initially use the predictions from the training data and subsequently begin introducing real prediction data as it is generated. FreqAI will save this historical data to be reloaded if you stop and restart a model with the same `identifier`.
|
||||||
|
|
||||||
## Using different prediction models
|
## Using different prediction models
|
||||||
|
|
||||||
`FreqAI` has multiple example prediction model libraries that are ready to be used as is via the flag `--freqaimodel`. These libraries include `Catboost`, `LightGBM`, and `XGBoost` regression, classification, and multi-target models, and can be found in `freqai/prediction_models/`. However, it is possible to customize and create your own prediction models using the `IFreqaiModel` class. You are encouraged to inherit `fit()`, `train()`, and `predict()` to let these customize various aspects of the training procedures.
|
FreqAI has multiple example prediction model libraries that are ready to be used as is via the flag `--freqaimodel`. These libraries include `Catboost`, `LightGBM`, and `XGBoost` regression, classification, and multi-target models, and can be found in `freqai/prediction_models/`. However, it is possible to customize and create your own prediction models using the `IFreqaiModel` class. You are encouraged to inherit `fit()`, `train()`, and `predict()` to let these customize various aspects of the training procedures.
|
||||||
|
|
||||||
### Setting classifier targets
|
### Setting classifier targets
|
||||||
|
|
||||||
`FreqAI` includes a variety of classifiers, such as the `CatboostClassifier` via the flag `--freqaimodel CatboostClassifier`. If you elects to use a classifier, the classes need to be set using strings. For example:
|
FreqAI includes a variety of classifiers, such as the `CatboostClassifier` via the flag `--freqaimodel CatboostClassifier`. If you elects to use a classifier, the classes need to be set using strings. For example:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
df['&s-up_or_down'] = np.where( df["close"].shift(-100) > df["close"], 'up', 'down')
|
df['&s-up_or_down'] = np.where( df["close"].shift(-100) > df["close"], 'up', 'down')
|
||||||
|
@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
## Project architecture
|
## Project architecture
|
||||||
|
|
||||||
The architecture and functions of `FreqAI` are generalized to encourages development of unique features, functions, models, etc.
|
The architecture and functions of FreqAI are generalized to encourages development of unique features, functions, models, etc.
|
||||||
|
|
||||||
The class structure and a detailed algorithmic overview is depicted in the following diagram:
|
The class structure and a detailed algorithmic overview is depicted in the following diagram:
|
||||||
|
|
||||||
![image](assets/freqai_algorithm-diagram.jpg)
|
![image](assets/freqai_algorithm-diagram.jpg)
|
||||||
|
|
||||||
As shown, there are three distinct objects comprising `FreqAI`:
|
As shown, there are three distinct objects comprising FreqAI:
|
||||||
|
|
||||||
* **IFreqaiModel** - A singular persistent object containing all the necessary logic to collect, store, and process data, engineer features, run training, and inference models.
|
* **IFreqaiModel** - A singular persistent object containing all the necessary logic to collect, store, and process data, engineer features, run training, and inference models.
|
||||||
* **FreqaiDataKitchen** - A non-persistent object which is created uniquely for each unique asset/model. Beyond metadata, it also contains a variety of data processing tools.
|
* **FreqaiDataKitchen** - A non-persistent object which is created uniquely for each unique asset/model. Beyond metadata, it also contains a variety of data processing tools.
|
||||||
@ -18,7 +18,7 @@ There are a variety of built-in [prediction models](freqai-configuration.md#usin
|
|||||||
|
|
||||||
## Data handling
|
## Data handling
|
||||||
|
|
||||||
`FreqAI` aims to organize model files, prediction data, and meta data in a way that simplifies post-processing and enhances crash resilience by automatic data reloading. The data is saved in a file structure,`user_data_dir/models/`, which contains all the data associated with the trainings and backtests. The `FreqaiDataKitchen()` relies heavily on the file structure for proper training and inferencing and should therefore not be manually modified.
|
FreqAI aims to organize model files, prediction data, and meta data in a way that simplifies post-processing and enhances crash resilience by automatic data reloading. The data is saved in a file structure,`user_data_dir/models/`, which contains all the data associated with the trainings and backtests. The `FreqaiDataKitchen()` relies heavily on the file structure for proper training and inferencing and should therefore not be manually modified.
|
||||||
|
|
||||||
### File structure
|
### File structure
|
||||||
|
|
||||||
@ -27,13 +27,13 @@ The file structure is automatically generated based on the model `identifier` se
|
|||||||
| Structure | Description |
|
| Structure | Description |
|
||||||
|-----------|-------------|
|
|-----------|-------------|
|
||||||
| `config_*.json` | A copy of the model specific configuration file. |
|
| `config_*.json` | A copy of the model specific configuration file. |
|
||||||
| `historic_predictions.pkl` | A file containing all historic predictions generated during the lifetime of the `identifier` model during live deployment. `historic_predictions.pkl` is used to reload the model after a crash or a config change. A backup file is always held incase of corruption on the main file. **`FreqAI` automatically detects corruption and replaces the corrupted file with the backup**. |
|
| `historic_predictions.pkl` | A file containing all historic predictions generated during the lifetime of the `identifier` model during live deployment. `historic_predictions.pkl` is used to reload the model after a crash or a config change. A backup file is always held in case of corruption on the main file. FreqAI **automatically** detects corruption and replaces the corrupted file with the backup. |
|
||||||
| `pair_dictionary.json` | A file containing the training queue as well as the on disk location of the most recently trained model. |
|
| `pair_dictionary.json` | A file containing the training queue as well as the on disk location of the most recently trained model. |
|
||||||
| `sub-train-*_TIMESTAMP` | A folder containing all the files associated with a single model, such as: <br>
|
| `sub-train-*_TIMESTAMP` | A folder containing all the files associated with a single model, such as: <br>
|
||||||
|| `*_metadata.json` - Metadata for the model, such as normalization max/mins, expected training feature list, etc. <br>
|
|| `*_metadata.json` - Metadata for the model, such as normalization max/min, expected training feature list, etc. <br>
|
||||||
|| `*_model.*` - The model file saved to disk for reloading from a crash. Can be `joblib` (typical boosting libs), `zip` (stable_baselines), `hd5` (keras type), etc. <br>
|
|| `*_model.*` - The model file saved to disk for reloading from a crash. Can be `joblib` (typical boosting libs), `zip` (stable_baselines), `hd5` (keras type), etc. <br>
|
||||||
|| `*_pca_object.pkl` - The [Principal component analysis (PCA)](freqai-feature-engineering.md#data-dimensionality-reduction-with-principal-component-analysis) transform (if `principal_component_analysis: true` is set in the config) which will be used to transform unseen prediction features. <br>
|
|| `*_pca_object.pkl` - The [Principal component analysis (PCA)](freqai-feature-engineering.md#data-dimensionality-reduction-with-principal-component-analysis) transform (if `principal_component_analysis: True` is set in the config) which will be used to transform unseen prediction features. <br>
|
||||||
|| `*_svm_model.pkl` - The [Support Vector Machine (SVM)](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm) model which is used to detect outliers in unseen prediction features. <br>
|
|| `*_svm_model.pkl` - The [Support Vector Machine (SVM)](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm) model (if `use_SVM_to_remove_outliers: True` is set in the config) which is used to detect outliers in unseen prediction features. <br>
|
||||||
|| `*_trained_df.pkl` - The dataframe containing all the training features used to train the `identifier` model. This is used for computing the [Dissimilarity Index (DI)](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di) and can also be used for post-processing. <br>
|
|| `*_trained_df.pkl` - The dataframe containing all the training features used to train the `identifier` model. This is used for computing the [Dissimilarity Index (DI)](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di) and can also be used for post-processing. <br>
|
||||||
|| `*_trained_dates.df.pkl` - The dates associated with the `trained_df.pkl`, which is useful for post-processing. |
|
|| `*_trained_dates.df.pkl` - The dates associated with the `trained_df.pkl`, which is useful for post-processing. |
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
Low level feature engineering is performed in the user strategy within a function called `populate_any_indicators()`. That function sets the `base features` such as, `RSI`, `MFI`, `EMA`, `SMA`, time of day, volume, etc. The `base features` can be custom indicators or they can be imported from any technical-analysis library that you can find. One important syntax rule is that all `base features` string names are prepended with `%`, while labels/targets are prepended with `&`.
|
Low level feature engineering is performed in the user strategy within a function called `populate_any_indicators()`. That function sets the `base features` such as, `RSI`, `MFI`, `EMA`, `SMA`, time of day, volume, etc. The `base features` can be custom indicators or they can be imported from any technical-analysis library that you can find. One important syntax rule is that all `base features` string names are prepended with `%`, while labels/targets are prepended with `&`.
|
||||||
|
|
||||||
Meanwhile, high level feature engineering is handled within `"feature_parameters":{}` in the `FreqAI` config. Within this file, it is possible to decide large scale feature expansions on top of the `base_features` such as "including correlated pairs" or "including informative timeframes" or even "including recent candles."
|
Meanwhile, high level feature engineering is handled within `"feature_parameters":{}` in the FreqAI config. Within this file, it is possible to decide large scale feature expansions on top of the `base_features` such as "including correlated pairs" or "including informative timeframes" or even "including recent candles."
|
||||||
|
|
||||||
It is advisable to start from the template `populate_any_indicators()` in the source provided example strategy (found in `templates/FreqaiExampleStrategy.py`) to ensure that the feature definitions are following the correct conventions. Here is an example of how to set the indicators and labels in the strategy:
|
It is advisable to start from the template `populate_any_indicators()` in the source provided example strategy (found in `templates/FreqaiExampleStrategy.py`) to ensure that the feature definitions are following the correct conventions. Here is an example of how to set the indicators and labels in the strategy:
|
||||||
|
|
||||||
@ -122,7 +122,7 @@ The `include_timeframes` in the config above are the timeframes (`tf`) of each c
|
|||||||
|
|
||||||
You can ask for each of the defined features to be included also for informative pairs using the `include_corr_pairlist`. This means that the feature set will include all the features from `populate_any_indicators` on all the `include_timeframes` for each of the correlated pairs defined in the config (`ETH/USD`, `LINK/USD`, and `BNB/USD` in the presented example).
|
You can ask for each of the defined features to be included also for informative pairs using the `include_corr_pairlist`. This means that the feature set will include all the features from `populate_any_indicators` on all the `include_timeframes` for each of the correlated pairs defined in the config (`ETH/USD`, `LINK/USD`, and `BNB/USD` in the presented example).
|
||||||
|
|
||||||
`include_shifted_candles` indicates the number of previous candles to include in the feature set. For example, `include_shifted_candles: 2` tells `FreqAI` to include the past 2 candles for each of the features in the feature set.
|
`include_shifted_candles` indicates the number of previous candles to include in the feature set. For example, `include_shifted_candles: 2` tells FreqAI to include the past 2 candles for each of the features in the feature set.
|
||||||
|
|
||||||
In total, the number of features the user of the presented example strat has created is: length of `include_timeframes` * no. features in `populate_any_indicators()` * length of `include_corr_pairlist` * no. `include_shifted_candles` * length of `indicator_periods_candles`
|
In total, the number of features the user of the presented example strat has created is: 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$.
|
||||||
@ -131,7 +131,7 @@ In total, the number of features the user of the presented example strat has cre
|
|||||||
|
|
||||||
Important metrics can be returned to the strategy at the end of each model training by assigning them to `dk.data['extra_returns_per_train']['my_new_value'] = XYZ` inside the custom prediction model class.
|
Important metrics can be returned to the strategy at the end of each model training by assigning them to `dk.data['extra_returns_per_train']['my_new_value'] = XYZ` inside the custom prediction model class.
|
||||||
|
|
||||||
`FreqAI` takes the `my_new_value` assigned in this dictionary and expands it to fit the dataframe that is returned to the strategy. You can then use the returned metrics in your strategy through `dataframe['my_new_value']`. An example of how return values can be used in `FreqAI` are the `&*_mean` and `&*_std` values that are used to [created a dynamic target threshold](freqai-configuration.md#creating-a-dynamic-target-threshold).
|
FreqAI takes the `my_new_value` assigned in this dictionary and expands it to fit the dataframe that is returned to the strategy. You can then use the returned metrics in your strategy through `dataframe['my_new_value']`. An example of how return values can be used in FreqAI are the `&*_mean` and `&*_std` values that are used to [created a dynamic target threshold](freqai-configuration.md#creating-a-dynamic-target-threshold).
|
||||||
|
|
||||||
Another example, where the user wants to use live metrics from the trade database, is shown below:
|
Another example, where the user wants to use live metrics from the trade database, is shown below:
|
||||||
|
|
||||||
@ -141,15 +141,15 @@ Another example, where the user wants to use live metrics from the trade databas
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
You need to set the standard dictionary in the config so that `FreqAI` can return proper dataframe shapes. These values will likely be overridden by the prediction model, but in the case where the model has yet to set them, or needs a default initial value, the preset values are what will be returned.
|
You need to set the standard dictionary in the config so that FreqAI can return proper dataframe shapes. These values will likely be overridden by the prediction model, but in the case where the model has yet to set them, or needs a default initial value, the pre-set values are what will be returned.
|
||||||
|
|
||||||
## Feature normalization
|
## Feature normalization
|
||||||
|
|
||||||
`FreqAI` is strict when it comes to data normalization. The train features, $X^{train}$, are always normalized to [-1, 1] using a shifted min-max normalization:
|
FreqAI is strict when it comes to data normalization. The train features, $X^{train}$, are always normalized to [-1, 1] using a shifted min-max normalization:
|
||||||
|
|
||||||
$$X^{train}_{norm} = 2 * \frac{X^{train} - X^{train}.min()}{X^{train}.max() - X^{train}.min()} - 1$$
|
$$X^{train}_{norm} = 2 * \frac{X^{train} - X^{train}.min()}{X^{train}.max() - X^{train}.min()} - 1$$
|
||||||
|
|
||||||
All other data (test data and unseen prediction data in dry/live/backtest) is always automatically normalized to the training feature space according to industry standards. `FreqAI` stores all the metadata required to ensure that test and prediction features will be properly normalized and that predictions are properly denormalized. For this reason, it is not recommended to eschew industry standards and modify `FreqAI` internals - however - advanced users can do so by inheriting `train()` in their custom `IFreqaiModel` and using their own normalization functions.
|
All other data (test data and unseen prediction data in dry/live/backtest) is always automatically normalized to the training feature space according to industry standards. FreqAI stores all the metadata required to ensure that test and prediction features will be properly normalized and that predictions are properly denormalized. For this reason, it is not recommended to eschew industry standards and modify FreqAI internals - however - advanced users can do so by inheriting `train()` in their custom `IFreqaiModel` and using their own normalization functions.
|
||||||
|
|
||||||
## Data dimensionality reduction with Principal Component Analysis
|
## Data dimensionality reduction with Principal Component Analysis
|
||||||
|
|
||||||
@ -169,17 +169,17 @@ This will perform PCA on the features and reduce their dimensionality so that th
|
|||||||
|
|
||||||
The `inlier_metric` is a metric aimed at quantifying how similar a the features of a data point are to the most recent historic data points.
|
The `inlier_metric` is a metric aimed at quantifying how similar a the features of a data point are to the most recent historic data points.
|
||||||
|
|
||||||
You define the lookback window by setting `inlier_metric_window` and `FreqAI` computes the distance between the present time point and each of the previous `inlier_metric_window` lookback points. A Weibull function is fit to each of the lookback distributions and its cumulative distribution function (CDF) is used to produce a quantile for each lookback point. The `inlier_metric` is then computed for each time point as the average of the corresponding lookback quantiles. The figure below explains the concept for an `inlier_metric_window` of 5.
|
You define the lookback window by setting `inlier_metric_window` and FreqAI computes the distance between the present time point and each of the previous `inlier_metric_window` lookback points. A Weibull function is fit to each of the lookback distributions and its cumulative distribution function (CDF) is used to produce a quantile for each lookback point. The `inlier_metric` is then computed for each time point as the average of the corresponding lookback quantiles. The figure below explains the concept for an `inlier_metric_window` of 5.
|
||||||
|
|
||||||
![inlier-metric](assets/freqai_inlier-metric.jpg)
|
![inlier-metric](assets/freqai_inlier-metric.jpg)
|
||||||
|
|
||||||
`FreqAI` adds the `inlier_metric` to the training features and hence gives the model access to a novel type of temporal information.
|
FreqAI adds the `inlier_metric` to the training features and hence gives the model access to a novel type of temporal information.
|
||||||
|
|
||||||
This function does **not** remove outliers from the data set.
|
This function does **not** remove outliers from the data set.
|
||||||
|
|
||||||
## Weighting features for temporal importance
|
## Weighting features for temporal importance
|
||||||
|
|
||||||
`FreqAI` allows you to set a `weight_factor` to weight recent data more strongly than past data via an exponential function:
|
FreqAI allows you to set a `weight_factor` to weight recent data more strongly than past data via an exponential function:
|
||||||
|
|
||||||
$$ W_i = \exp(\frac{-i}{\alpha*n}) $$
|
$$ W_i = \exp(\frac{-i}{\alpha*n}) $$
|
||||||
|
|
||||||
@ -189,13 +189,13 @@ where $W_i$ is the weight of data point $i$ in a total set of $n$ data points. B
|
|||||||
|
|
||||||
## Outlier detection
|
## Outlier detection
|
||||||
|
|
||||||
Equity and crypto markets suffer from a high level of non-patterned noise in the form of outlier data points. `FreqAI` implements a variety of methods to identify such outliers and hence mitigate risk.
|
Equity and crypto markets suffer from a high level of non-patterned noise in the form of outlier data points. FreqAI implements a variety of methods to identify such outliers and hence mitigate risk.
|
||||||
|
|
||||||
### Identifying outliers with the Dissimilarity Index (DI)
|
### Identifying outliers with the Dissimilarity Index (DI)
|
||||||
|
|
||||||
The Dissimilarity Index (DI) aims to quantify the uncertainty associated with each prediction made by the model.
|
The Dissimilarity Index (DI) aims to quantify the uncertainty associated with each prediction made by the model.
|
||||||
|
|
||||||
You can tell `FreqAI` to remove outlier data points from the training/test data sets using the DI by including the following statement in the config:
|
You can tell FreqAI to remove outlier data points from the training/test data sets using the DI by including the following statement in the config:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"freqai": {
|
"freqai": {
|
||||||
@ -205,7 +205,7 @@ You can tell `FreqAI` to remove outlier data points from the training/test data
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
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:
|
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:
|
||||||
|
|
||||||
$$ d_{ab} = \sqrt{\sum_{j=1}^p(X_{a,j}-X_{b,j})^2} $$
|
$$ d_{ab} = \sqrt{\sum_{j=1}^p(X_{a,j}-X_{b,j})^2} $$
|
||||||
|
|
||||||
@ -229,7 +229,7 @@ Below is a figure that describes the DI for a 3D data set.
|
|||||||
|
|
||||||
### Identifying outliers using a Support Vector Machine (SVM)
|
### Identifying outliers using a Support Vector Machine (SVM)
|
||||||
|
|
||||||
You can tell `FreqAI` to remove outlier data points from the training/test data sets using a Support Vector Machine (SVM) by including the following statement in the config:
|
You can tell FreqAI to remove outlier data points from the training/test data sets using a Support Vector Machine (SVM) by including the following statement in the config:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"freqai": {
|
"freqai": {
|
||||||
@ -241,7 +241,7 @@ You can tell `FreqAI` to remove outlier data points from the training/test data
|
|||||||
|
|
||||||
The SVM will be trained on the training data and any data point that the SVM deems to be beyond the feature space will be removed.
|
The SVM will be trained on the training data and any data point that the SVM deems to be beyond the feature space will be removed.
|
||||||
|
|
||||||
`FreqAI` uses `sklearn.linear_model.SGDOneClassSVM` (details are available on scikit-learn's webpage [here](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.SGDOneClassSVM.html) (external website)) and you can elect to provide additional parameters for the SVM, such as `shuffle`, and `nu`.
|
FreqAI uses `sklearn.linear_model.SGDOneClassSVM` (details are available on scikit-learn's webpage [here](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.SGDOneClassSVM.html) (external website)) and you can elect to provide additional parameters for the SVM, such as `shuffle`, and `nu`.
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
@ -249,7 +249,7 @@ The parameter `nu`, *very* broadly, is the amount of data points that should be
|
|||||||
|
|
||||||
### Identifying outliers with DBSCAN
|
### Identifying outliers with DBSCAN
|
||||||
|
|
||||||
You can configure `FreqAI` to use DBSCAN to cluster and remove outliers from the training/test data set or incoming outliers from predictions, by activating `use_DBSCAN_to_remove_outliers` in the config:
|
You can configure FreqAI to use DBSCAN to cluster and remove outliers from the training/test data set or incoming outliers from predictions, by activating `use_DBSCAN_to_remove_outliers` in the config:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"freqai": {
|
"freqai": {
|
||||||
@ -265,4 +265,4 @@ Given a number of data points $N$, and a distance $\varepsilon$, DBSCAN clusters
|
|||||||
|
|
||||||
![dbscan](assets/freqai_dbscan.jpg)
|
![dbscan](assets/freqai_dbscan.jpg)
|
||||||
|
|
||||||
`FreqAI` uses `sklearn.cluster.DBSCAN` (details are available on scikit-learn's webpage [here](https://scikit-learn.org/stable/modules/generated/sklearn.cluster.DBSCAN.html) (external website)) with `min_samples` ($N$) taken as 1/4 of the no. of time points in the feature set. `eps` ($\varepsilon$) is computed automatically as the elbow point in the *k-distance graph* computed from the nearest neighbors in the pairwise distances of all data points in the feature set.
|
FreqAI uses `sklearn.cluster.DBSCAN` (details are available on scikit-learn's webpage [here](https://scikit-learn.org/stable/modules/generated/sklearn.cluster.DBSCAN.html) (external website)) with `min_samples` ($N$) taken as 1/4 of the no. of time points (candles) in the feature set. `eps` ($\varepsilon$) is computed automatically as the elbow point in the *k-distance graph* computed from the nearest neighbors in the pairwise distances of all data points in the feature set.
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
# Parameter table
|
# Parameter table
|
||||||
|
|
||||||
The table below will list all configuration parameters available for `FreqAI`. Some of the parameters are exemplified in `config_examples/config_freqai.example.json`.
|
The table below will list all configuration parameters available for FreqAI. Some of the parameters are exemplified in `config_examples/config_freqai.example.json`.
|
||||||
|
|
||||||
Mandatory parameters are marked as **Required** and have to be set in one of the suggested ways.
|
Mandatory parameters are marked as **Required** and have to be set in one of the suggested ways.
|
||||||
|
|
||||||
| Parameter | Description |
|
| Parameter | Description |
|
||||||
|------------|-------------|
|
|------------|-------------|
|
||||||
| | **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.
|
||||||
| `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 `train_period_days` window defined above, and retraining the model during backtesting (more info [here](freqai-running.md#backtesting)). This can be fractional days, but beware that the 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 `train_period_days` window defined above, and retraining the model during backtesting (more info [here](freqai-running.md#backtesting)). This can be fractional days, but beware that the provided `timerange` will be divided by this number to yield the number of trainings necessary to complete the backtest. <br> **Datatype:** Float.
|
||||||
| `identifier` | **Required.** <br> A unique ID for the current model. If models are saved to disk, the `identifier` allows for reloading specific pre-trained models/data. <br> **Datatype:** String.
|
| `identifier` | **Required.** <br> A unique ID for the current model. If models are saved to disk, the `identifier` allows for reloading specific pre-trained models/data. <br> **Datatype:** String.
|
||||||
| `live_retrain_hours` | Frequency of retraining during dry/live runs. <br> **Datatype:** Float > 0. <br> Default: 0 (models retrain as often as possible).
|
| `live_retrain_hours` | Frequency of retraining during dry/live runs. <br> **Datatype:** Float > 0. <br> Default: `0` (models retrain as often as possible).
|
||||||
| `expiration_hours` | Avoid making predictions if a model is more than `expiration_hours` old. <br> **Datatype:** Positive integer. <br> Default: 0 (models never expire).
|
| `expiration_hours` | Avoid making predictions if a model is more than `expiration_hours` old. <br> **Datatype:** Positive integer. <br> Default: `0` (models never expire).
|
||||||
| `purge_old_models` | Delete obsolete models. <br> **Datatype:** Boolean. <br> Default: `False` (all historic models remain on disk).
|
| `purge_old_models` | Delete obsolete models. <br> **Datatype:** Boolean. <br> Default: `False` (all historic models remain on disk).
|
||||||
| `save_backtest_models` | Save models to disk when running backtesting. Backtesting operates most efficiently by saving the prediction data and reusing them directly for subsequent runs (when you wish to tune entry/exit parameters). Saving backtesting models to disk also allows to use the same model files for starting a dry/live instance with the same model `identifier`. <br> **Datatype:** Boolean. <br> Default: `False` (no models are saved).
|
| `save_backtest_models` | Save models to disk when running backtesting. Backtesting operates most efficiently by saving the prediction data and reusing them directly for subsequent runs (when you wish to tune entry/exit parameters). Saving backtesting models to disk also allows to use the same model files for starting a dry/live instance with the same model `identifier`. <br> **Datatype:** Boolean. <br> Default: `False` (no models are saved).
|
||||||
| `fit_live_predictions_candles` | Number of historical candles to use for computing target (label) statistics from prediction data, instead of from the training dataset (more information can be found [here](freqai-configuration.md#creating-a-dynamic-target-threshold)). <br> **Datatype:** Positive integer.
|
| `fit_live_predictions_candles` | Number of historical candles to use for computing target (label) statistics from prediction data, instead of from the training dataset (more information can be found [here](freqai-configuration.md#creating-a-dynamic-target-threshold)). <br> **Datatype:** Positive integer.
|
||||||
@ -21,21 +21,21 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
|||||||
| | **Feature parameters**
|
| | **Feature parameters**
|
||||||
| `feature_parameters` | A dictionary containing the parameters used to engineer the feature set. Details and examples are shown [here](freqai-feature-engineering.md). <br> **Datatype:** Dictionary.
|
| `feature_parameters` | A dictionary containing the parameters used to engineer the feature set. Details and examples are shown [here](freqai-feature-engineering.md). <br> **Datatype:** Dictionary.
|
||||||
| `include_timeframes` | A list of timeframes that all indicators in `populate_any_indicators` will be created for. The list is added as features to the base indicators dataset. <br> **Datatype:** List of timeframes (strings).
|
| `include_timeframes` | A list of timeframes that all indicators in `populate_any_indicators` will be created for. The list is added as features to the base indicators dataset. <br> **Datatype:** List of timeframes (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](freqai-feature-engineering.md)) will be created for each correlated coin. The correlated coins features are added to the base indicators dataset. <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](freqai-feature-engineering.md)) will be created for each correlated coin. The correlated coins features are added to the base indicators dataset. <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). You can create custom labels and choose whether to make 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). You can create custom labels and choose whether to make use of this parameter or not. <br> **Datatype:** Positive integer.
|
||||||
| `include_shifted_candles` | Add features from previous candles to subsequent candles with the intent of adding historical information. If used, `FreqAI` will duplicate and shift all features from the `include_shifted_candles` previous candles 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 with the intent of adding historical information. If used, FreqAI will duplicate and shift all features from the `include_shifted_candles` previous candles so that the information is available for the subsequent candle. <br> **Datatype:** Positive integer.
|
||||||
| `weight_factor` | Weight training data points according to their recency (see details [here](freqai-feature-engineering.md#weighting-features-for-temporal-importance)). <br> **Datatype:** Positive float (typically < 1).
|
| `weight_factor` | Weight training data points according to their recency (see details [here](freqai-feature-engineering.md#weighting-features-for-temporal-importance)). <br> **Datatype:** Positive float (typically < 1).
|
||||||
| `indicator_max_period_candles` | **No longer used (#7325)**. Replaced by `startup_candle_count` which is set in the [strategy](freqai-configuration.md#building-a-freqai-strategy). `startup_candle_count` is timeframe independent and defines the maximum *period* used in `populate_any_indicators()` for indicator creation. `FreqAI` uses this parameter together with the maximum timeframe in `include_time_frames` to calculate how many data points to download such that the first data point does not include a NaN <br> **Datatype:** Positive integer.
|
| `indicator_max_period_candles` | **No longer used (#7325)**. Replaced by `startup_candle_count` which is set in the [strategy](freqai-configuration.md#building-a-freqai-strategy). `startup_candle_count` is timeframe independent and defines the maximum *period* used in `populate_any_indicators()` for indicator creation. FreqAI uses this parameter together with the maximum timeframe in `include_time_frames` to calculate how many data points to download such that the first data point does not include a NaN. <br> **Datatype:** Positive integer.
|
||||||
| `indicator_periods_candles` | Time periods to calculate indicators for. The indicators are added to the base indicator dataset. <br> **Datatype:** List of positive integers.
|
| `indicator_periods_candles` | Time periods to calculate indicators for. The indicators are added to the base indicator dataset. <br> **Datatype:** List of positive integers.
|
||||||
| `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. defaults to `False`.
|
| `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. <br> Default: `False`.
|
||||||
| `plot_feature_importances` | Create a feature importance plot for each model for the top/bottom `plot_feature_importances` number of features.<br> **Datatype:** Integer, defaults to `0`.
|
| `plot_feature_importances` | Create a feature importance plot for each model for the top/bottom `plot_feature_importances` number of features. <br> **Datatype:** Integer. <br> Default: `0`.
|
||||||
| `DI_threshold` | Activates the use of the Dissimilarity Index for outlier detection when set to > 0. See details about how it works [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di). <br> **Datatype:** Positive float (typically < 1).
|
| `DI_threshold` | Activates the use of the Dissimilarity Index for outlier detection when set to > 0. See details about how it works [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di). <br> **Datatype:** Positive float (typically < 1).
|
||||||
| `use_SVM_to_remove_outliers` | Train a support vector machine to detect and remove outliers from the training dataset, as well as from incoming data points. See details about how it works [here](freqai-feature-engineering.md#identifying-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 dataset, as well as from incoming data points. See details about how it works [here](freqai-feature-engineering.md#identifying-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](freqai-feature-engineering.md#identifying-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](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm). <br> **Datatype:** Dictionary.
|
||||||
| `use_DBSCAN_to_remove_outliers` | Cluster data using the DBSCAN algorithm to identify and remove outliers from training and prediction data. See details about how it works [here](freqai-feature-engineering.md#identifying-outliers-with-dbscan). <br> **Datatype:** Boolean.
|
| `use_DBSCAN_to_remove_outliers` | Cluster data using the DBSCAN algorithm to identify and remove outliers from training and prediction data. See details about how it works [here](freqai-feature-engineering.md#identifying-outliers-with-dbscan). <br> **Datatype:** Boolean.
|
||||||
| `inlier_metric_window` | If set, `FreqAI` adds an `inlier_metric` to the training feature set and set the lookback to be the `inlier_metric_window`, i.e., the number of previous time points to compare the current candle to. Details of how the `inlier_metric` is computed can be found [here](freqai-feature-engineering.md#inlier-metric). <br> **Datatype:** Integer. <br> Default: 0.
|
| `inlier_metric_window` | If set, FreqAI adds an `inlier_metric` to the training feature set and set the lookback to be the `inlier_metric_window`, i.e., the number of previous time points to compare the current candle to. Details of how the `inlier_metric` is computed can be found [here](freqai-feature-engineering.md#inlier-metric). <br> **Datatype:** Integer. <br> Default: `0`.
|
||||||
| `noise_standard_deviation` | If set, `FreqAI` adds noise to the training features with the aim of preventing overfitting. `FreqAI` generates random deviates from a gaussian distribution with a standard deviation of `noise_standard_deviation` and adds them to all data points. `noise_standard_deviation` should be kept relative to the normalized space, i.e., between -1 and 1. In other words, since data in `FreqAI` is always normalized to be between -1 and 1, `noise_standard_deviation: 0.05` would result in 32% of the data being randomly increased/decreased by more than 2.5% (i.e., the percent of data falling within the first standard deviation). <br> **Datatype:** Integer. <br> Default: 0.
|
| `noise_standard_deviation` | If set, FreqAI adds noise to the training features with the aim of preventing overfitting. FreqAI generates random deviates from a gaussian distribution with a standard deviation of `noise_standard_deviation` and adds them to all data points. `noise_standard_deviation` should be kept relative to the normalized space, i.e., between -1 and 1. In other words, since data in FreqAI is always normalized to be between -1 and 1, `noise_standard_deviation: 0.05` would result in 32% of the data being randomly increased/decreased by more than 2.5% (i.e., the percent of data falling within the first standard deviation). <br> **Datatype:** Integer. <br> Default: `0`.
|
||||||
| `outlier_protection_percentage` | Enable to prevent outlier detection methods from discarding too much data. If more than `outlier_protection_percentage` % of points are detected as outliers by the SVM or DBSCAN, `FreqAI` will log a warning message and ignore outlier detection, i.e., the original dataset will be kept intact. If the outlier protection is triggered, no predictions will be made based on the training dataset. <br> **Datatype:** Float. <br> Default: `30`.
|
| `outlier_protection_percentage` | Enable to prevent outlier detection methods from discarding too much data. If more than `outlier_protection_percentage` % of points are detected as outliers by the SVM or DBSCAN, FreqAI will log a warning message and ignore outlier detection, i.e., the original dataset will be kept intact. If the outlier protection is triggered, no predictions will be made based on the training dataset. <br> **Datatype:** Float. <br> Default: `30`.
|
||||||
| `reverse_train_test_order` | Split the feature dataset (see below) and use the latest data split for training and test on historical split of the data. This allows the model to be trained up to the most recent data point, while avoiding overfitting. However, you should be careful to understand the unorthodox nature of this parameter before employing it. <br> **Datatype:** Boolean. <br> Default: `False` (no reversal).
|
| `reverse_train_test_order` | Split the feature dataset (see below) and use the latest data split for training and test on historical split of the data. This allows the model to be trained up to the most recent data point, while avoiding overfitting. However, you should be careful to understand the unorthodox nature of this parameter before employing it. <br> **Datatype:** Boolean. <br> Default: `False` (no reversal).
|
||||||
| | **Data split parameters**
|
| | **Data split parameters**
|
||||||
| `data_split_parameters` | Include any additional parameters available from Scikit-learn `test_train_split()`, which are shown [here](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) (external website). <br> **Datatype:** Dictionary.
|
| `data_split_parameters` | Include any additional parameters available from Scikit-learn `test_train_split()`, which are shown [here](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) (external website). <br> **Datatype:** Dictionary.
|
||||||
@ -43,8 +43,8 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
|||||||
| `shuffle` | Shuffle the training data points during training. Typically, to not remove the chronological order of data in time-series forecasting, this is set to `False`. <br> **Datatype:** Boolean. <br> Defaut: `False`.
|
| `shuffle` | Shuffle the training data points during training. Typically, to not remove the chronological order of data in time-series forecasting, this is set to `False`. <br> **Datatype:** Boolean. <br> Defaut: `False`.
|
||||||
| | **Model training parameters**
|
| | **Model training parameters**
|
||||||
| `model_training_parameters` | A flexible dictionary that includes all parameters available by the selected model library. For example, if you use `LightGBMRegressor`, this dictionary can contain any parameter available by the `LightGBMRegressor` [here](https://lightgbm.readthedocs.io/en/latest/pythonapi/lightgbm.LGBMRegressor.html) (external website). If you select a different model, this dictionary can contain any parameter from that model. <br> **Datatype:** Dictionary.
|
| `model_training_parameters` | A flexible dictionary that includes all parameters available by the selected model library. For example, if you use `LightGBMRegressor`, this dictionary can contain any parameter available by the `LightGBMRegressor` [here](https://lightgbm.readthedocs.io/en/latest/pythonapi/lightgbm.LGBMRegressor.html) (external website). If you select a different model, this dictionary can contain any parameter from that model. <br> **Datatype:** Dictionary.
|
||||||
| `n_estimators` | The number of boosted trees to fit in regression. <br> **Datatype:** Integer.
|
| `n_estimators` | The number of boosted trees to fit in the training of the model. <br> **Datatype:** Integer.
|
||||||
| `learning_rate` | Boosting learning rate during regression. <br> **Datatype:** Float.
|
| `learning_rate` | Boosting learning rate during training of the model. <br> **Datatype:** Float.
|
||||||
| `n_jobs`, `thread_count`, `task_type` | Set the number of threads for parallel processing and the `task_type` (`gpu` or `cpu`). Different model libraries use different parameter names. <br> **Datatype:** Float.
|
| `n_jobs`, `thread_count`, `task_type` | Set the number of threads for parallel processing and the `task_type` (`gpu` or `cpu`). Different model libraries use different parameter names. <br> **Datatype:** Float.
|
||||||
| | *Reinforcement Learning Parameters**
|
| | *Reinforcement Learning Parameters**
|
||||||
| `rl_config` | A dictionary containing the control parameters for a Reinforcement Learning model. <br> **Datatype:** Dictionary.
|
| `rl_config` | A dictionary containing the control parameters for a Reinforcement Learning model. <br> **Datatype:** Dictionary.
|
||||||
@ -58,4 +58,4 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
|||||||
| `model_reward_parameters` | Parameters used inside the user customizable `calculate_reward()` function in `ReinforcementLearner.py` <br> **Datatype:** int.
|
| `model_reward_parameters` | Parameters used inside the user customizable `calculate_reward()` function in `ReinforcementLearner.py` <br> **Datatype:** int.
|
||||||
| | **Extraneous parameters**
|
| | **Extraneous parameters**
|
||||||
| `keras` | If the selected model makes use of Keras (typical for Tensorflow-based prediction models), this flag needs to be activated so that the model save/loading follows Keras standards. <br> **Datatype:** Boolean. <br> Default: `False`.
|
| `keras` | If the selected model makes use of Keras (typical for Tensorflow-based prediction models), this flag needs to be activated so that the model save/loading follows Keras standards. <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||||
| `conv_width` | The width of a convolutional neural network input tensor. This replaces the need for shifting candles (`include_shifted_candles`) by feeding in historical data points as the second dimension of the tensor. Technically, this parameter can also be used for regressors, but it only adds computational overhead and does not change the model training/prediction. <br> **Datatype:** Integer. <br> Default: 2.
|
| `conv_width` | The width of a convolutional neural network input tensor. This replaces the need for shifting candles (`include_shifted_candles`) by feeding in historical data points as the second dimension of the tensor. Technically, this parameter can also be used for regressors, but it only adds computational overhead and does not change the model training/prediction. <br> **Datatype:** Integer. <br> Default: `2`.
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Running FreqAI
|
# Running FreqAI
|
||||||
|
|
||||||
There are two ways to train and deploy an adaptive machine learning model - live deployment and historical backtesting. In both cases, `FreqAI` runs/simulates periodic retraining of models as shown in the following figure:
|
There are two ways to train and deploy an adaptive machine learning model - live deployment and historical backtesting. In both cases, FreqAI runs/simulates periodic retraining of models as shown in the following figure:
|
||||||
|
|
||||||
![freqai-window](assets/freqai_moving-window.jpg)
|
![freqai-window](assets/freqai_moving-window.jpg)
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ FreqAI automatically downloads the proper amount of data needed to ensure traini
|
|||||||
|
|
||||||
### Saving prediction data
|
### Saving prediction data
|
||||||
|
|
||||||
All predictions made during the lifetime of a specific `identifier` model are stored in `historical_predictions.pkl` to allow for reloading after a crash or changes made to the config.
|
All predictions made during the lifetime of a specific `identifier` model are stored in `historic_predictions.pkl` to allow for reloading after a crash or changes made to the config.
|
||||||
|
|
||||||
### Purging old model data
|
### Purging old model data
|
||||||
|
|
||||||
@ -75,7 +75,7 @@ To allow for tweaking your strategy (**not** the features!), FreqAI will automat
|
|||||||
|
|
||||||
An additional directory called `predictions`, which contains all the predictions stored in `hdf` format, will be created in the `unique-id` folder.
|
An additional directory called `predictions`, which contains all the predictions stored in `hdf` format, will be created in the `unique-id` folder.
|
||||||
|
|
||||||
To change your **features**, you **must** set a new `identifier` in the config to signal to `FreqAI` to train new models.
|
To change your **features**, you **must** set a new `identifier` in the config to signal to FreqAI to train new models.
|
||||||
|
|
||||||
To save the models generated during a particular backtest so that you can start a live deployment from one of them instead of training a new model, you must set `save_backtest_models` to `True` in the config.
|
To save the models generated during a particular backtest so that you can start a live deployment from one of them instead of training a new model, you must set `save_backtest_models` to `True` in the config.
|
||||||
|
|
||||||
@ -115,7 +115,7 @@ The FreqAI specific parameter `label_period_candles` defines the offset (number
|
|||||||
|
|
||||||
## Continual learning
|
## Continual learning
|
||||||
|
|
||||||
You can choose to adopt a continual learning scheme by setting `"continual_learning": true` in the config. By enabling `continual_learning`, after training an initial model from scratch, subsequent trainings will start from the final model state of the preceding training. This gives the new model a "memory" of the previous state. By default, this is set to `false` which means that all new models are trained from scratch, without input from previous models.
|
You can choose to adopt a continual learning scheme by setting `"continual_learning": true` in the config. By enabling `continual_learning`, after training an initial model from scratch, subsequent trainings will start from the final model state of the preceding training. This gives the new model a "memory" of the previous state. By default, this is set to `False` which means that all new models are trained from scratch, without input from previous models.
|
||||||
|
|
||||||
## Hyperopt
|
## Hyperopt
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
![freqai-logo](assets/freqai_doc_logo.svg)
|
![freqai-logo](assets/freqai_doc_logo.svg)
|
||||||
|
|
||||||
# `FreqAI`
|
# FreqAI
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
`FreqAI` is a software designed to automate a variety of tasks associated with training a predictive machine learning model to generate market forecasts given a set of input features.
|
FreqAI is a software designed to automate a variety of tasks associated with training a predictive machine learning model to generate market forecasts given a set of input features.
|
||||||
|
|
||||||
Features include:
|
Features include:
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ Features include:
|
|||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
The easiest way to quickly test `FreqAI` is to run it in dry mode with the following command:
|
The easiest way to quickly test FreqAI is to run it in dry mode with the following command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
freqtrade trade --config config_examples/config_freqai.example.json --strategy FreqaiExampleStrategy --freqaimodel LightGBMRegressor --strategy-path freqtrade/templates
|
freqtrade trade --config config_examples/config_freqai.example.json --strategy FreqaiExampleStrategy --freqaimodel LightGBMRegressor --strategy-path freqtrade/templates
|
||||||
@ -37,7 +37,7 @@ An example strategy, prediction model, and config to use as a starting points ca
|
|||||||
|
|
||||||
## General approach
|
## General approach
|
||||||
|
|
||||||
You provide `FreqAI` with a set of custom *base indicators* (the same way as in a [typical Freqtrade strategy](strategy-customization.md)) as well as target values (*labels*). For each pair in the whitelist, `FreqAI` trains a model to predict the target values based on the input of custom indicators. The models are then consistently retrained, with a predetermined frequency, to adapt to market conditions. `FreqAI` offers the ability to both backtest strategies (emulating reality with periodic retraining on historic data) and deploy dry/live runs. In dry/live conditions, `FreqAI` can be set to constant retraining in a background thread to keep models as up to date as possible.
|
You provide FreqAI with a set of custom *base indicators* (the same way as in a [typical Freqtrade strategy](strategy-customization.md)) as well as target values (*labels*). For each pair in the whitelist, FreqAI trains a model to predict the target values based on the input of custom indicators. The models are then consistently retrained, with a predetermined frequency, to adapt to market conditions. FreqAI offers the ability to both backtest strategies (emulating reality with periodic retraining on historic data) and deploy dry/live runs. In dry/live conditions, FreqAI can be set to constant retraining in a background thread to keep models as up to date as possible.
|
||||||
|
|
||||||
An overview of the algorithm, explaining the data processing pipeline and model usage, is shown below.
|
An overview of the algorithm, explaining the data processing pipeline and model usage, is shown below.
|
||||||
|
|
||||||
@ -45,21 +45,21 @@ An overview of the algorithm, explaining the data processing pipeline and model
|
|||||||
|
|
||||||
### Important machine learning vocabulary
|
### Important machine learning vocabulary
|
||||||
|
|
||||||
**Features** - the parameters, based on historic data, on which a model is trained. All features for a single candle is stored as a vector. In `FreqAI`, you build a feature data sets from anything you can construct in the strategy.
|
**Features** - the parameters, based on historic data, on which a model is trained. All features for a single candle are stored as a vector. In FreqAI, you build a feature data set from anything you can construct in the strategy.
|
||||||
|
|
||||||
**Labels** - the target values that a model is trained toward. Each feature vector is associated with a single label that is defined by you within the strategy. These labels intentionally look into the future, and are not available to the model during dry/live/backtesting.
|
**Labels** - the target values that the model is trained toward. Each feature vector is associated with a single label that is defined by you within the strategy. These labels intentionally look into the future and are what you are training the model to be able to predict.
|
||||||
|
|
||||||
**Training** - the process of "teaching" the model to match the feature sets to the associated labels. Different types of models "learn" in different ways. More information about the different models can be found [here](freqai-configuration.md#using-different-prediction-models).
|
**Training** - the process of "teaching" the model to match the feature sets to the associated labels. Different types of models "learn" in different ways which means that one might be better than another for a specific application. More information about the different models that are already implemented in FreqAI can be found [here](freqai-configuration.md#using-different-prediction-models).
|
||||||
|
|
||||||
**Train data** - a subset of the feature data set that is fed to the model during training. This data directly influences weight connections in the model.
|
**Train data** - a subset of the feature data set that is fed to the model during training to "teach" the model how to predict the targets. This data directly influences weight connections in the model.
|
||||||
|
|
||||||
**Test data** - a subset of the feature data set 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 feature data set that is used to evaluate the performance of the model after training. This data does not influence nodal weights within the model.
|
||||||
|
|
||||||
**Inferencing** - the process of feeding a trained model new data on which it will make a prediction.
|
**Inferencing** - the process of feeding a trained model new unseen data on which it will make a prediction.
|
||||||
|
|
||||||
## Install prerequisites
|
## Install prerequisites
|
||||||
|
|
||||||
The normal Freqtrade install process will ask if you wish to install `FreqAI` dependencies. You should reply "yes" to this question if you wish to use `FreqAI`. If you did not reply yes, you can manually install these dependencies after the install with:
|
The normal Freqtrade install process will ask if you wish to install FreqAI dependencies. You should reply "yes" to this question if you wish to use FreqAI. If you did not reply yes, you can manually install these dependencies after the install with:
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
pip install -r requirements-freqai.txt
|
pip install -r requirements-freqai.txt
|
||||||
@ -70,24 +70,19 @@ pip install -r requirements-freqai.txt
|
|||||||
|
|
||||||
### Usage with docker
|
### Usage with docker
|
||||||
|
|
||||||
For docker users, a dedicated tag with freqAI dependencies is available as `:freqai`.
|
If you are using docker, a dedicated tag with FreqAI dependencies is available as `:freqai`. As such - you can replace the image line in your docker-compose file with `image: freqtradeorg/freqtrade:develop_freqai`. This image contains the regular FreqAI dependencies. Similar to native installs, Catboost will not be available on ARM based devices.
|
||||||
As such - you can replace the image line in your docker-compose file with `image: freqtradeorg/freqtrade:develop_freqai`.
|
|
||||||
This image contains the regular freqAI dependencies. Similar to native installs, Catboost will not be available on ARM based devices.
|
|
||||||
|
|
||||||
## Setting up FreqAI
|
|
||||||
|
|
||||||
### Parameter table
|
|
||||||
|
|
||||||
## Common pitfalls
|
## Common pitfalls
|
||||||
|
|
||||||
`FreqAI` cannot be combined with dynamic `VolumePairlists` (or any pairlist filter that adds and removes pairs dynamically).
|
FreqAI cannot be combined with dynamic `VolumePairlists` (or any pairlist filter that adds and removes pairs dynamically).
|
||||||
This is for performance reasons - `FreqAI` relies on making quick predictions/retrains. To do this effectively,
|
This is for performance reasons - FreqAI relies on making quick predictions/retrains. To do this effectively,
|
||||||
it needs to download all the training data at the beginning of a dry/live instance. `FreqAI` stores and appends
|
it needs to download all the training data at the beginning of a dry/live instance. FreqAI stores and appends
|
||||||
new candles automatically for future retrains. This means that if new pairs arrive later in the dry run due to a volume pairlist, it will not have the data ready. However, `FreqAI` does work with the `ShufflePairlist` or a `VolumePairlist` which keeps the total pairlist constant (but reorders the pairs according to volume).
|
new candles automatically for future retrains. This means that if new pairs arrive later in the dry run due to a volume pairlist, it will not have the data ready. However, FreqAI does work with the `ShufflePairlist` or a `VolumePairlist` which keeps the total pairlist constant (but reorders the pairs according to volume).
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
`FreqAI` is developed by a group of individuals who all contribute specific skillsets to the project.
|
FreqAI is developed by a group of individuals who all contribute specific skillsets to the project.
|
||||||
|
|
||||||
Conception and software development:
|
Conception and software development:
|
||||||
Robert Caulk @robcaulk
|
Robert Caulk @robcaulk
|
||||||
@ -102,5 +97,4 @@ Software development:
|
|||||||
Wagner Costa @wagnercosta
|
Wagner Costa @wagnercosta
|
||||||
|
|
||||||
Beta testing and bug reporting:
|
Beta testing and bug reporting:
|
||||||
Stefan Gehring @bloodhunter4rc, @longyu, Andrew Robert Lawless @paranoidandy, Pascal Schmidt @smidelis, Ryan McMullan @smarmau,
|
Stefan Gehring @bloodhunter4rc, @longyu, Andrew Lawless @paranoidandy, Pascal Schmidt @smidelis, Ryan McMullan @smarmau, Juha Nykänen @suikula, Johan van der Vlugt @jooopiert, Richárd Józsa @richardjosza, Timothy Pogue @wizrds
|
||||||
Juha Nykänen @suikula, Johan van der Vlugt @jooopiert, Richárd Józsa @richardjosza
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
markdown==3.3.7
|
markdown==3.3.7
|
||||||
mkdocs==1.3.1
|
mkdocs==1.4.0
|
||||||
mkdocs-material==8.5.3
|
mkdocs-material==8.5.6
|
||||||
mdx_truly_sane_lists==1.3
|
mdx_truly_sane_lists==1.3
|
||||||
pymdown-extensions==9.5
|
pymdown-extensions==9.6
|
||||||
jinja2==3.1.2
|
jinja2==3.1.2
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
# flake8: noqa: F401
|
# flake8: noqa: F401
|
||||||
|
|
||||||
from freqtrade.configuration.check_exchange import check_exchange
|
|
||||||
from freqtrade.configuration.config_setup import setup_utils_configuration
|
from freqtrade.configuration.config_setup import setup_utils_configuration
|
||||||
from freqtrade.configuration.config_validation import validate_config_consistency
|
from freqtrade.configuration.config_validation import validate_config_consistency
|
||||||
from freqtrade.configuration.configuration import Configuration
|
from freqtrade.configuration.configuration import Configuration
|
||||||
|
@ -8,7 +8,6 @@ from pathlib import Path
|
|||||||
from typing import Any, Callable, Dict, List, Optional
|
from typing import Any, Callable, Dict, List, Optional
|
||||||
|
|
||||||
from freqtrade import constants
|
from freqtrade import constants
|
||||||
from freqtrade.configuration.check_exchange import check_exchange
|
|
||||||
from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings
|
from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings
|
||||||
from freqtrade.configuration.directory_operations import create_datadir, create_userdata_dir
|
from freqtrade.configuration.directory_operations import create_datadir, create_userdata_dir
|
||||||
from freqtrade.configuration.environment_vars import enironment_vars_to_dict
|
from freqtrade.configuration.environment_vars import enironment_vars_to_dict
|
||||||
@ -100,6 +99,9 @@ class Configuration:
|
|||||||
|
|
||||||
self._process_freqai_options(config)
|
self._process_freqai_options(config)
|
||||||
|
|
||||||
|
# Import check_exchange here to avoid import cycle problems
|
||||||
|
from freqtrade.exchange.check_exchange import check_exchange
|
||||||
|
|
||||||
# Check if the exchange set by the user is supported
|
# Check if the exchange set by the user is supported
|
||||||
check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True))
|
check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True))
|
||||||
|
|
||||||
|
@ -12,8 +12,8 @@ from freqtrade.exchange.coinbasepro import Coinbasepro
|
|||||||
from freqtrade.exchange.exchange import (amount_to_contract_precision, amount_to_contracts,
|
from freqtrade.exchange.exchange import (amount_to_contract_precision, amount_to_contracts,
|
||||||
amount_to_precision, available_exchanges, ccxt_exchanges,
|
amount_to_precision, available_exchanges, ccxt_exchanges,
|
||||||
contracts_to_amount, date_minus_candles,
|
contracts_to_amount, date_minus_candles,
|
||||||
is_exchange_known_ccxt, is_exchange_officially_supported,
|
is_exchange_known_ccxt, market_is_active,
|
||||||
market_is_active, price_to_precision, timeframe_to_minutes,
|
price_to_precision, timeframe_to_minutes,
|
||||||
timeframe_to_msecs, timeframe_to_next_date,
|
timeframe_to_msecs, timeframe_to_next_date,
|
||||||
timeframe_to_prev_date, timeframe_to_seconds,
|
timeframe_to_prev_date, timeframe_to_seconds,
|
||||||
validate_exchange, validate_exchanges)
|
validate_exchange, validate_exchanges)
|
||||||
|
@ -3,8 +3,8 @@ import logging
|
|||||||
from freqtrade.constants import Config
|
from freqtrade.constants import Config
|
||||||
from freqtrade.enums import RunMode
|
from freqtrade.enums import RunMode
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange import (available_exchanges, is_exchange_known_ccxt,
|
from freqtrade.exchange import available_exchanges, is_exchange_known_ccxt, validate_exchange
|
||||||
is_exchange_officially_supported, validate_exchange)
|
from freqtrade.exchange.common import MAP_EXCHANGE_CHILDCLASS, SUPPORTED_EXCHANGES
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -52,7 +52,7 @@ def check_exchange(config: Config, check_for_bad: bool = True) -> bool:
|
|||||||
else:
|
else:
|
||||||
logger.warning(f'Exchange "{exchange}" will not work with Freqtrade. Reason: {reason}')
|
logger.warning(f'Exchange "{exchange}" will not work with Freqtrade. Reason: {reason}')
|
||||||
|
|
||||||
if is_exchange_officially_supported(exchange):
|
if MAP_EXCHANGE_CHILDCLASS.get(exchange, exchange) in SUPPORTED_EXCHANGES:
|
||||||
logger.info(f'Exchange "{exchange}" is officially supported '
|
logger.info(f'Exchange "{exchange}" is officially supported '
|
||||||
f'by the Freqtrade development team.')
|
f'by the Freqtrade development team.')
|
||||||
else:
|
else:
|
@ -30,8 +30,7 @@ from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFun
|
|||||||
RetryableOrderError, TemporaryError)
|
RetryableOrderError, TemporaryError)
|
||||||
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGES,
|
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGES,
|
||||||
EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED,
|
EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED,
|
||||||
SUPPORTED_EXCHANGES, remove_credentials, retrier,
|
remove_credentials, retrier, retrier_async)
|
||||||
retrier_async)
|
|
||||||
from freqtrade.misc import (chunks, deep_merge_dicts, file_dump_json, file_load_json,
|
from freqtrade.misc import (chunks, deep_merge_dicts, file_dump_json, file_load_json,
|
||||||
safe_value_fallback2)
|
safe_value_fallback2)
|
||||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||||
@ -1292,7 +1291,14 @@ class Exchange:
|
|||||||
order = self.fetch_order(order_id, pair)
|
order = self.fetch_order(order_id, pair)
|
||||||
except InvalidOrderException:
|
except InvalidOrderException:
|
||||||
logger.warning(f"Could not fetch cancelled order {order_id}.")
|
logger.warning(f"Could not fetch cancelled order {order_id}.")
|
||||||
order = {'fee': {}, 'status': 'canceled', 'amount': amount, 'info': {}}
|
order = {
|
||||||
|
'id': order_id,
|
||||||
|
'status': 'canceled',
|
||||||
|
'amount': amount,
|
||||||
|
'filled': 0.0,
|
||||||
|
'fee': {},
|
||||||
|
'info': {}
|
||||||
|
}
|
||||||
|
|
||||||
return order
|
return order
|
||||||
|
|
||||||
@ -1863,6 +1869,38 @@ class Exchange:
|
|||||||
return self._async_get_candle_history(
|
return self._async_get_candle_history(
|
||||||
pair, timeframe, since_ms=since_ms, candle_type=candle_type)
|
pair, timeframe, since_ms=since_ms, candle_type=candle_type)
|
||||||
|
|
||||||
|
def _build_ohlcv_dl_jobs(
|
||||||
|
self, pair_list: ListPairsWithTimeframes, since_ms: Optional[int],
|
||||||
|
cache: bool) -> Tuple[List[Coroutine], List[Tuple[str, str, CandleType]]]:
|
||||||
|
"""
|
||||||
|
Build Coroutines to execute as part of refresh_latest_ohlcv
|
||||||
|
"""
|
||||||
|
input_coroutines = []
|
||||||
|
cached_pairs = []
|
||||||
|
for pair, timeframe, candle_type in set(pair_list):
|
||||||
|
if (
|
||||||
|
timeframe not in self.timeframes
|
||||||
|
and candle_type in (CandleType.SPOT, CandleType.FUTURES)
|
||||||
|
):
|
||||||
|
logger.warning(
|
||||||
|
f"Cannot download ({pair}, {timeframe}) combination as this timeframe is "
|
||||||
|
f"not available on {self.name}. Available timeframes are "
|
||||||
|
f"{', '.join(self.timeframes)}.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if ((pair, timeframe, candle_type) not in self._klines or not cache
|
||||||
|
or self._now_is_time_to_refresh(pair, timeframe, candle_type)):
|
||||||
|
input_coroutines.append(self._build_coroutine(
|
||||||
|
pair, timeframe, candle_type=candle_type, since_ms=since_ms))
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.debug(
|
||||||
|
f"Using cached candle (OHLCV) data for {pair}, {timeframe}, {candle_type} ..."
|
||||||
|
)
|
||||||
|
cached_pairs.append((pair, timeframe, candle_type))
|
||||||
|
|
||||||
|
return input_coroutines, cached_pairs
|
||||||
|
|
||||||
def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *,
|
def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *,
|
||||||
since_ms: Optional[int] = None, cache: bool = True,
|
since_ms: Optional[int] = None, cache: bool = True,
|
||||||
drop_incomplete: Optional[bool] = None
|
drop_incomplete: Optional[bool] = None
|
||||||
@ -1880,27 +1918,9 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
logger.debug("Refreshing candle (OHLCV) data for %d pairs", len(pair_list))
|
logger.debug("Refreshing candle (OHLCV) data for %d pairs", len(pair_list))
|
||||||
drop_incomplete = self._ohlcv_partial_candle if drop_incomplete is None else drop_incomplete
|
drop_incomplete = self._ohlcv_partial_candle if drop_incomplete is None else drop_incomplete
|
||||||
input_coroutines = []
|
|
||||||
cached_pairs = []
|
|
||||||
# Gather coroutines to run
|
|
||||||
for pair, timeframe, candle_type in set(pair_list):
|
|
||||||
if (timeframe not in self.timeframes
|
|
||||||
and candle_type in (CandleType.SPOT, CandleType.FUTURES)):
|
|
||||||
logger.warning(
|
|
||||||
f"Cannot download ({pair}, {timeframe}) combination as this timeframe is "
|
|
||||||
f"not available on {self.name}. Available timeframes are "
|
|
||||||
f"{', '.join(self.timeframes)}.")
|
|
||||||
continue
|
|
||||||
if ((pair, timeframe, candle_type) not in self._klines or not cache
|
|
||||||
or self._now_is_time_to_refresh(pair, timeframe, candle_type)):
|
|
||||||
input_coroutines.append(self._build_coroutine(
|
|
||||||
pair, timeframe, candle_type=candle_type, since_ms=since_ms))
|
|
||||||
|
|
||||||
else:
|
# Gather coroutines to run
|
||||||
logger.debug(
|
input_coroutines, cached_pairs = self._build_ohlcv_dl_jobs(pair_list, since_ms, cache)
|
||||||
f"Using cached candle (OHLCV) data for {pair}, {timeframe}, {candle_type} ..."
|
|
||||||
)
|
|
||||||
cached_pairs.append((pair, timeframe, candle_type))
|
|
||||||
|
|
||||||
results_df = {}
|
results_df = {}
|
||||||
# Chunk requests into batches of 100 to avoid overwelming ccxt Throttling
|
# Chunk requests into batches of 100 to avoid overwelming ccxt Throttling
|
||||||
@ -1941,10 +1961,8 @@ class Exchange:
|
|||||||
interval_in_sec = timeframe_to_seconds(timeframe)
|
interval_in_sec = timeframe_to_seconds(timeframe)
|
||||||
|
|
||||||
return not (
|
return not (
|
||||||
(self._pairs_last_refresh_time.get(
|
(self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0)
|
||||||
(pair, timeframe, candle_type),
|
+ interval_in_sec) >= arrow.utcnow().int_timestamp
|
||||||
0
|
|
||||||
) + interval_in_sec) >= arrow.utcnow().int_timestamp
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@retrier_async
|
@retrier_async
|
||||||
@ -2754,10 +2772,6 @@ def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = Non
|
|||||||
return exchange_name in ccxt_exchanges(ccxt_module)
|
return exchange_name in ccxt_exchanges(ccxt_module)
|
||||||
|
|
||||||
|
|
||||||
def is_exchange_officially_supported(exchange_name: str) -> bool:
|
|
||||||
return exchange_name in SUPPORTED_EXCHANGES
|
|
||||||
|
|
||||||
|
|
||||||
def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:
|
def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Return the list of all exchanges known to ccxt
|
Return the list of all exchanges known to ccxt
|
||||||
|
@ -264,7 +264,7 @@ class FreqaiDataDrawer:
|
|||||||
|
|
||||||
def append_model_predictions(self, pair: str, predictions: DataFrame,
|
def append_model_predictions(self, pair: str, predictions: DataFrame,
|
||||||
do_preds: NDArray[np.int_],
|
do_preds: NDArray[np.int_],
|
||||||
dk: FreqaiDataKitchen, len_df: int) -> None:
|
dk: FreqaiDataKitchen, strat_df: DataFrame) -> None:
|
||||||
"""
|
"""
|
||||||
Append model predictions to historic predictions dataframe, then set the
|
Append model predictions to historic predictions dataframe, then set the
|
||||||
strategy return dataframe to the tail of the historic predictions. The length of
|
strategy return dataframe to the tail of the historic predictions. The length of
|
||||||
@ -273,6 +273,7 @@ class FreqaiDataDrawer:
|
|||||||
historic predictions.
|
historic predictions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
len_df = len(strat_df)
|
||||||
index = self.historic_predictions[pair].index[-1:]
|
index = self.historic_predictions[pair].index[-1:]
|
||||||
columns = self.historic_predictions[pair].columns
|
columns = self.historic_predictions[pair].columns
|
||||||
|
|
||||||
@ -300,6 +301,15 @@ class FreqaiDataDrawer:
|
|||||||
for return_str in rets:
|
for return_str in rets:
|
||||||
df[return_str].iloc[-1] = rets[return_str]
|
df[return_str].iloc[-1] = rets[return_str]
|
||||||
|
|
||||||
|
# this logic carries users between version without needing to
|
||||||
|
# change their identifier
|
||||||
|
if 'close_price' not in df.columns:
|
||||||
|
df['close_price'] = np.nan
|
||||||
|
df['date_pred'] = np.nan
|
||||||
|
|
||||||
|
df['close_price'].iloc[-1] = strat_df['close'].iloc[-1]
|
||||||
|
df['date_pred'].iloc[-1] = strat_df['date'].iloc[-1]
|
||||||
|
|
||||||
self.model_return_values[pair] = df.tail(len_df).reset_index(drop=True)
|
self.model_return_values[pair] = df.tail(len_df).reset_index(drop=True)
|
||||||
|
|
||||||
def attach_return_values_to_return_dataframe(
|
def attach_return_values_to_return_dataframe(
|
||||||
|
@ -407,7 +407,7 @@ class IFreqaiModel(ABC):
|
|||||||
# allows FreqUI to show full return values.
|
# allows FreqUI to show full return values.
|
||||||
pred_df, do_preds = self.predict(dataframe, dk)
|
pred_df, do_preds = self.predict(dataframe, dk)
|
||||||
if pair not in self.dd.historic_predictions:
|
if pair not in self.dd.historic_predictions:
|
||||||
self.set_initial_historic_predictions(pred_df, dk, pair)
|
self.set_initial_historic_predictions(pred_df, dk, pair, dataframe)
|
||||||
self.dd.set_initial_return_values(pair, pred_df)
|
self.dd.set_initial_return_values(pair, pred_df)
|
||||||
|
|
||||||
dk.return_dataframe = self.dd.attach_return_values_to_return_dataframe(pair, dataframe)
|
dk.return_dataframe = self.dd.attach_return_values_to_return_dataframe(pair, dataframe)
|
||||||
@ -428,7 +428,7 @@ class IFreqaiModel(ABC):
|
|||||||
|
|
||||||
if self.freqai_info.get('fit_live_predictions_candles', 0) and self.live:
|
if self.freqai_info.get('fit_live_predictions_candles', 0) and self.live:
|
||||||
self.fit_live_predictions(dk, pair)
|
self.fit_live_predictions(dk, pair)
|
||||||
self.dd.append_model_predictions(pair, pred_df, do_preds, dk, len(dataframe))
|
self.dd.append_model_predictions(pair, pred_df, do_preds, dk, dataframe)
|
||||||
dk.return_dataframe = self.dd.attach_return_values_to_return_dataframe(pair, dataframe)
|
dk.return_dataframe = self.dd.attach_return_values_to_return_dataframe(pair, dataframe)
|
||||||
|
|
||||||
return
|
return
|
||||||
@ -597,7 +597,7 @@ class IFreqaiModel(ABC):
|
|||||||
self.dd.purge_old_models()
|
self.dd.purge_old_models()
|
||||||
|
|
||||||
def set_initial_historic_predictions(
|
def set_initial_historic_predictions(
|
||||||
self, pred_df: DataFrame, dk: FreqaiDataKitchen, pair: str
|
self, pred_df: DataFrame, dk: FreqaiDataKitchen, pair: str, strat_df: DataFrame
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
This function is called only if the datadrawer failed to load an
|
This function is called only if the datadrawer failed to load an
|
||||||
@ -640,6 +640,9 @@ class IFreqaiModel(ABC):
|
|||||||
for return_str in dk.data['extra_returns_per_train']:
|
for return_str in dk.data['extra_returns_per_train']:
|
||||||
hist_preds_df[return_str] = 0
|
hist_preds_df[return_str] = 0
|
||||||
|
|
||||||
|
hist_preds_df['close_price'] = strat_df['close']
|
||||||
|
hist_preds_df['date_pred'] = strat_df['date']
|
||||||
|
|
||||||
# # for keras type models, the conv_window needs to be prepended so
|
# # for keras type models, the conv_window needs to be prepended so
|
||||||
# # viewing is correct in frequi
|
# # viewing is correct in frequi
|
||||||
if self.freqai_info.get('keras', False) or self.ft_params.get('inlier_metric_window', 0):
|
if self.freqai_info.get('keras', False) or self.ft_params.get('inlier_metric_window', 0):
|
||||||
|
@ -1311,7 +1311,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# place new order only if new price is supplied
|
# place new order only if new price is supplied
|
||||||
self.execute_entry(
|
self.execute_entry(
|
||||||
pair=trade.pair,
|
pair=trade.pair,
|
||||||
stake_amount=(order_obj.remaining * order_obj.price),
|
stake_amount=(order_obj.remaining * order_obj.price / trade.leverage),
|
||||||
price=adjusted_entry_price,
|
price=adjusted_entry_price,
|
||||||
trade=trade,
|
trade=trade,
|
||||||
is_short=trade.is_short,
|
is_short=trade.is_short,
|
||||||
@ -1389,11 +1389,13 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
reason += f", {constants.CANCEL_REASON['FULLY_CANCELLED']}"
|
reason += f", {constants.CANCEL_REASON['FULLY_CANCELLED']}"
|
||||||
else:
|
else:
|
||||||
self.update_trade_state(trade, trade.open_order_id, corder)
|
self.update_trade_state(trade, trade.open_order_id, corder)
|
||||||
|
trade.open_order_id = None
|
||||||
logger.info(f'{side} Order timeout for {trade}.')
|
logger.info(f'{side} Order timeout for {trade}.')
|
||||||
else:
|
else:
|
||||||
# update_trade_state (and subsequently recalc_trade_from_orders) will handle updates
|
# update_trade_state (and subsequently recalc_trade_from_orders) will handle updates
|
||||||
# to the trade object
|
# to the trade object
|
||||||
self.update_trade_state(trade, trade.open_order_id, corder)
|
self.update_trade_state(trade, trade.open_order_id, corder)
|
||||||
|
trade.open_order_id = None
|
||||||
|
|
||||||
logger.info(f'Partial {trade.entry_side} order timeout for {trade}.')
|
logger.info(f'Partial {trade.entry_side} order timeout for {trade}.')
|
||||||
reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}"
|
reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}"
|
||||||
@ -1409,47 +1411,63 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
:return: True if exit order was cancelled, false otherwise
|
:return: True if exit order was cancelled, false otherwise
|
||||||
"""
|
"""
|
||||||
cancelled = False
|
cancelled = False
|
||||||
# if trade is not partially completed, just cancel the order
|
# Cancelled orders may have the status of 'canceled' or 'closed'
|
||||||
if order['remaining'] == order['amount'] or order.get('filled') == 0.0:
|
if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES:
|
||||||
if not self.exchange.check_order_canceled_empty(order):
|
filled_val: float = order.get('filled', 0.0) or 0.0
|
||||||
|
filled_rem_stake = trade.stake_amount - filled_val * trade.open_rate
|
||||||
|
minstake = self.exchange.get_min_pair_stake_amount(
|
||||||
|
trade.pair, trade.open_rate, self.strategy.stoploss)
|
||||||
|
# Double-check remaining amount
|
||||||
|
if filled_val > 0:
|
||||||
|
reason = constants.CANCEL_REASON['PARTIALLY_FILLED']
|
||||||
|
if minstake and filled_rem_stake < minstake:
|
||||||
|
logger.warning(
|
||||||
|
f"Order {trade.open_order_id} for {trade.pair} not cancelled, as "
|
||||||
|
f"the filled amount of {filled_val} would result in an unexitable trade.")
|
||||||
|
reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
||||||
|
|
||||||
|
self._notify_exit_cancel(
|
||||||
|
trade,
|
||||||
|
order_type=self.strategy.order_types['exit'],
|
||||||
|
reason=reason, order_id=order['id'],
|
||||||
|
sub_trade=trade.amount != order['amount']
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# if trade is not partially completed, just delete the order
|
|
||||||
co = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair,
|
co = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair,
|
||||||
trade.amount)
|
trade.amount)
|
||||||
trade.update_order(co)
|
|
||||||
except InvalidOrderException:
|
except InvalidOrderException:
|
||||||
logger.exception(
|
logger.exception(
|
||||||
f"Could not cancel {trade.exit_side} order {trade.open_order_id}")
|
f"Could not cancel {trade.exit_side} order {trade.open_order_id}")
|
||||||
return False
|
return False
|
||||||
logger.info('%s order %s for %s.', trade.exit_side.capitalize(), reason, trade)
|
|
||||||
else:
|
|
||||||
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
|
|
||||||
logger.info('%s order %s for %s.', trade.exit_side.capitalize(), reason, trade)
|
|
||||||
trade.update_order(order)
|
|
||||||
|
|
||||||
trade.close_rate = None
|
trade.close_rate = None
|
||||||
trade.close_rate_requested = None
|
trade.close_rate_requested = None
|
||||||
trade.close_profit = None
|
trade.close_profit = None
|
||||||
trade.close_profit_abs = None
|
trade.close_profit_abs = None
|
||||||
trade.open_order_id = None
|
# Set exit_reason for fill message
|
||||||
|
exit_reason_prev = trade.exit_reason
|
||||||
|
trade.exit_reason = trade.exit_reason + f", {reason}" if trade.exit_reason else reason
|
||||||
|
self.update_trade_state(trade, trade.open_order_id, co)
|
||||||
|
# Order might be filled above in odd timing issues.
|
||||||
|
if co.get('status') in ('canceled', 'cancelled'):
|
||||||
trade.exit_reason = None
|
trade.exit_reason = None
|
||||||
cancelled = True
|
trade.open_order_id = None
|
||||||
self.wallets.update()
|
|
||||||
else:
|
else:
|
||||||
# TODO: figure out how to handle partially complete sell orders
|
trade.exit_reason = exit_reason_prev
|
||||||
reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
|
||||||
cancelled = False
|
|
||||||
|
|
||||||
order_obj = trade.select_order_by_order_id(order['id'])
|
logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.')
|
||||||
if not order_obj:
|
cancelled = True
|
||||||
raise DependencyException(
|
else:
|
||||||
f"Order_obj not found for {order['id']}. This should not have happened.")
|
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
|
||||||
|
logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.')
|
||||||
|
self.update_trade_state(trade, trade.open_order_id, order)
|
||||||
|
trade.open_order_id = None
|
||||||
|
|
||||||
sub_trade = order_obj.amount != trade.amount
|
|
||||||
self._notify_exit_cancel(
|
self._notify_exit_cancel(
|
||||||
trade,
|
trade,
|
||||||
order_type=self.strategy.order_types['exit'],
|
order_type=self.strategy.order_types['exit'],
|
||||||
reason=reason, order=order_obj, sub_trade=sub_trade
|
reason=reason, order_id=order['id'], sub_trade=trade.amount != order['amount']
|
||||||
)
|
)
|
||||||
return cancelled
|
return cancelled
|
||||||
|
|
||||||
@ -1646,7 +1664,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
self.rpc.send_msg(msg)
|
self.rpc.send_msg(msg)
|
||||||
|
|
||||||
def _notify_exit_cancel(self, trade: Trade, order_type: str, reason: str,
|
def _notify_exit_cancel(self, trade: Trade, order_type: str, reason: str,
|
||||||
order: Order, sub_trade: bool = False) -> None:
|
order_id: str, sub_trade: bool = False) -> None:
|
||||||
"""
|
"""
|
||||||
Sends rpc notification when a sell cancel occurred.
|
Sends rpc notification when a sell cancel occurred.
|
||||||
"""
|
"""
|
||||||
@ -1655,6 +1673,11 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
else:
|
else:
|
||||||
trade.exit_order_status = reason
|
trade.exit_order_status = reason
|
||||||
|
|
||||||
|
order = trade.select_order_by_order_id(order_id)
|
||||||
|
if not order:
|
||||||
|
raise DependencyException(
|
||||||
|
f"Order_obj not found for {order_id}. This should not have happened.")
|
||||||
|
|
||||||
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
|
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
|
||||||
profit_trade = trade.calc_profit(rate=profit_rate)
|
profit_trade = trade.calc_profit(rate=profit_rate)
|
||||||
current_rate = self.exchange.get_rate(
|
current_rate = self.exchange.get_rate(
|
||||||
|
@ -1045,7 +1045,7 @@ class Backtesting:
|
|||||||
if requested_rate:
|
if requested_rate:
|
||||||
self._enter_trade(pair=trade.pair, row=row, trade=trade,
|
self._enter_trade(pair=trade.pair, row=row, trade=trade,
|
||||||
requested_rate=requested_rate,
|
requested_rate=requested_rate,
|
||||||
requested_stake=(order.remaining * order.price),
|
requested_stake=(order.remaining * order.price / trade.leverage),
|
||||||
direction='short' if trade.is_short else 'long')
|
direction='short' if trade.is_short else 'long')
|
||||||
self.replaced_entry_orders += 1
|
self.replaced_entry_orders += 1
|
||||||
else:
|
else:
|
||||||
|
@ -24,6 +24,7 @@ from pandas import DataFrame
|
|||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, FTHYPT_FILEVERSION, LAST_BT_RESULT_FN, Config
|
from freqtrade.constants import DATETIME_PRINT_FORMAT, FTHYPT_FILEVERSION, LAST_BT_RESULT_FN, Config
|
||||||
from freqtrade.data.converter import trim_dataframes
|
from freqtrade.data.converter import trim_dataframes
|
||||||
from freqtrade.data.history import get_timerange
|
from freqtrade.data.history import get_timerange
|
||||||
|
from freqtrade.data.metrics import calculate_market_change
|
||||||
from freqtrade.enums import HyperoptState
|
from freqtrade.enums import HyperoptState
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.misc import deep_merge_dicts, file_dump_json, plural
|
from freqtrade.misc import deep_merge_dicts, file_dump_json, plural
|
||||||
@ -111,6 +112,7 @@ class Hyperopt:
|
|||||||
|
|
||||||
self.clean_hyperopt()
|
self.clean_hyperopt()
|
||||||
|
|
||||||
|
self.market_change = 0.0
|
||||||
self.num_epochs_saved = 0
|
self.num_epochs_saved = 0
|
||||||
self.current_best_epoch: Optional[Dict[str, Any]] = None
|
self.current_best_epoch: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
@ -357,7 +359,7 @@ class Hyperopt:
|
|||||||
|
|
||||||
strat_stats = generate_strategy_stats(
|
strat_stats = generate_strategy_stats(
|
||||||
self.pairlist, self.backtesting.strategy.get_strategy_name(),
|
self.pairlist, self.backtesting.strategy.get_strategy_name(),
|
||||||
backtesting_results, min_date, max_date, market_change=0
|
backtesting_results, min_date, max_date, market_change=self.market_change
|
||||||
)
|
)
|
||||||
results_explanation = HyperoptTools.format_results_explanation_string(
|
results_explanation = HyperoptTools.format_results_explanation_string(
|
||||||
strat_stats, self.config['stake_currency'])
|
strat_stats, self.config['stake_currency'])
|
||||||
@ -425,6 +427,9 @@ class Hyperopt:
|
|||||||
# Trim startup period from analyzed dataframe to get correct dates for output.
|
# Trim startup period from analyzed dataframe to get correct dates for output.
|
||||||
trimmed = trim_dataframes(preprocessed, self.timerange, self.backtesting.required_startup)
|
trimmed = trim_dataframes(preprocessed, self.timerange, self.backtesting.required_startup)
|
||||||
self.min_date, self.max_date = get_timerange(trimmed)
|
self.min_date, self.max_date = get_timerange(trimmed)
|
||||||
|
if not self.market_change:
|
||||||
|
self.market_change = calculate_market_change(trimmed, 'close')
|
||||||
|
|
||||||
# Real trimming will happen as part of backtesting.
|
# Real trimming will happen as part of backtesting.
|
||||||
return preprocessed
|
return preprocessed
|
||||||
|
|
||||||
|
@ -198,8 +198,10 @@ class ApiServer(RPCHandler):
|
|||||||
logger.debug(f"Found message of type: {message.get('type')}")
|
logger.debug(f"Found message of type: {message.get('type')}")
|
||||||
# Broadcast it
|
# Broadcast it
|
||||||
await self._ws_channel_manager.broadcast(message)
|
await self._ws_channel_manager.broadcast(message)
|
||||||
# Sleep, make this configurable?
|
# Limit messages per sec.
|
||||||
await asyncio.sleep(0.1)
|
# Could cause problems with queue size if too low, and
|
||||||
|
# problems with network traffik if too high.
|
||||||
|
await asyncio.sleep(0.001)
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -8,16 +8,16 @@
|
|||||||
coveralls==3.3.1
|
coveralls==3.3.1
|
||||||
flake8==5.0.4
|
flake8==5.0.4
|
||||||
flake8-tidy-imports==4.8.0
|
flake8-tidy-imports==4.8.0
|
||||||
mypy==0.971
|
mypy==0.981
|
||||||
pre-commit==2.20.0
|
pre-commit==2.20.0
|
||||||
pytest==7.1.3
|
pytest==7.1.3
|
||||||
pytest-asyncio==0.19.0
|
pytest-asyncio==0.19.0
|
||||||
pytest-cov==3.0.0
|
pytest-cov==4.0.0
|
||||||
pytest-mock==3.8.2
|
pytest-mock==3.9.0
|
||||||
pytest-random-order==1.0.4
|
pytest-random-order==1.0.4
|
||||||
isort==5.10.1
|
isort==5.10.1
|
||||||
# For datetime mocking
|
# For datetime mocking
|
||||||
time-machine==2.8.1
|
time-machine==2.8.2
|
||||||
|
|
||||||
# Convert jupyter notebooks to markdown documents
|
# Convert jupyter notebooks to markdown documents
|
||||||
nbconvert==7.0.0
|
nbconvert==7.0.0
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
# Required for freqai
|
# Required for freqai
|
||||||
scikit-learn==1.1.2
|
scikit-learn==1.1.2
|
||||||
joblib==1.2.0
|
joblib==1.2.0
|
||||||
catboost==1.0.6; platform_machine != 'aarch64'
|
catboost==1.1; platform_machine != 'aarch64'
|
||||||
lightgbm==3.3.2
|
lightgbm==3.3.2
|
||||||
xgboost==1.6.2
|
xgboost==1.6.2
|
||||||
torch==1.12.1
|
torch==1.12.1
|
||||||
|
@ -4,7 +4,7 @@ pandas==1.5.0; platform_machine != 'armv7l'
|
|||||||
pandas==1.4.3; platform_machine == 'armv7l'
|
pandas==1.4.3; platform_machine == 'armv7l'
|
||||||
pandas-ta==0.3.14b
|
pandas-ta==0.3.14b
|
||||||
|
|
||||||
ccxt==1.93.98
|
ccxt==1.95.2
|
||||||
# Pin cryptography for now due to rust build errors with piwheels
|
# Pin cryptography for now due to rust build errors with piwheels
|
||||||
cryptography==38.0.1
|
cryptography==38.0.1
|
||||||
aiohttp==3.8.3
|
aiohttp==3.8.3
|
||||||
|
@ -480,7 +480,7 @@ def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> No
|
|||||||
default_conf.update({'strategy': CURRENT_TEST_STRATEGY})
|
default_conf.update({'strategy': CURRENT_TEST_STRATEGY})
|
||||||
strategy = StrategyResolver.load_strategy(default_conf)
|
strategy = StrategyResolver.load_strategy(default_conf)
|
||||||
|
|
||||||
timerange = TimeRange('index', 'index', 200, 250)
|
timerange = TimeRange()
|
||||||
data = strategy.advise_all_indicators(
|
data = strategy.advise_all_indicators(
|
||||||
load_data(
|
load_data(
|
||||||
datadir=testdatadir,
|
datadir=testdatadir,
|
||||||
|
85
tests/exchange/test_exchange_utils.py
Normal file
85
tests/exchange/test_exchange_utils.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring, protected-access, invalid-name
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from freqtrade.enums import RunMode
|
||||||
|
from freqtrade.exceptions import OperationalException
|
||||||
|
from freqtrade.exchange.check_exchange import check_exchange
|
||||||
|
from tests.conftest import log_has_re
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_exchange(default_conf, caplog) -> None:
|
||||||
|
# Test an officially supported by Freqtrade team exchange
|
||||||
|
default_conf['runmode'] = RunMode.DRY_RUN
|
||||||
|
default_conf.get('exchange').update({'name': 'BITTREX'})
|
||||||
|
assert check_exchange(default_conf)
|
||||||
|
assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.",
|
||||||
|
caplog)
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
|
# Test an officially supported by Freqtrade team exchange
|
||||||
|
default_conf.get('exchange').update({'name': 'binance'})
|
||||||
|
assert check_exchange(default_conf)
|
||||||
|
assert log_has_re(
|
||||||
|
r"Exchange \"binance\" is officially supported by the Freqtrade development team\.",
|
||||||
|
caplog)
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
|
# Test an officially supported by Freqtrade team exchange
|
||||||
|
default_conf.get('exchange').update({'name': 'binanceus'})
|
||||||
|
assert check_exchange(default_conf)
|
||||||
|
assert log_has_re(
|
||||||
|
r"Exchange \"binanceus\" is officially supported by the Freqtrade development team\.",
|
||||||
|
caplog)
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
|
# Test an officially supported by Freqtrade team exchange - with remapping
|
||||||
|
default_conf.get('exchange').update({'name': 'okex'})
|
||||||
|
assert check_exchange(default_conf)
|
||||||
|
assert log_has_re(
|
||||||
|
r"Exchange \"okex\" is officially supported by the Freqtrade development team\.",
|
||||||
|
caplog)
|
||||||
|
caplog.clear()
|
||||||
|
# Test an available exchange, supported by ccxt
|
||||||
|
default_conf.get('exchange').update({'name': 'huobipro'})
|
||||||
|
assert check_exchange(default_conf)
|
||||||
|
assert log_has_re(r"Exchange .* is known to the the ccxt library, available for the bot, "
|
||||||
|
r"but not officially supported "
|
||||||
|
r"by the Freqtrade development team\. .*", caplog)
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
|
# Test a 'bad' exchange, which known to have serious problems
|
||||||
|
default_conf.get('exchange').update({'name': 'bitmex'})
|
||||||
|
with pytest.raises(OperationalException,
|
||||||
|
match=r"Exchange .* will not work with Freqtrade\..*"):
|
||||||
|
check_exchange(default_conf)
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
|
# Test a 'bad' exchange with check_for_bad=False
|
||||||
|
default_conf.get('exchange').update({'name': 'bitmex'})
|
||||||
|
assert check_exchange(default_conf, False)
|
||||||
|
assert log_has_re(r"Exchange .* is known to the the ccxt library, available for the bot, "
|
||||||
|
r"but not officially supported "
|
||||||
|
r"by the Freqtrade development team\. .*", caplog)
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
|
# Test an invalid exchange
|
||||||
|
default_conf.get('exchange').update({'name': 'unknown_exchange'})
|
||||||
|
with pytest.raises(
|
||||||
|
OperationalException,
|
||||||
|
match=r'Exchange "unknown_exchange" is not known to the ccxt library '
|
||||||
|
r'and therefore not available for the bot.*'
|
||||||
|
):
|
||||||
|
check_exchange(default_conf)
|
||||||
|
|
||||||
|
# Test no exchange...
|
||||||
|
default_conf.get('exchange').update({'name': ''})
|
||||||
|
default_conf['runmode'] = RunMode.PLOT
|
||||||
|
assert check_exchange(default_conf)
|
||||||
|
|
||||||
|
# Test no exchange...
|
||||||
|
default_conf.get('exchange').update({'name': ''})
|
||||||
|
default_conf['runmode'] = RunMode.UTIL_EXCHANGE
|
||||||
|
with pytest.raises(OperationalException,
|
||||||
|
match=r'This command requires a configured exchange.*'):
|
||||||
|
check_exchange(default_conf)
|
@ -297,6 +297,7 @@ def test_params_no_optimize_details(hyperopt) -> None:
|
|||||||
def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None:
|
def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None:
|
||||||
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
|
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
|
||||||
dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
|
dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.calculate_market_change', return_value=1.5)
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
|
mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
|
||||||
|
|
||||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
|
||||||
@ -530,6 +531,7 @@ def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None:
|
|||||||
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
|
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
|
||||||
dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
|
dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
|
mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.calculate_market_change', return_value=1.5)
|
||||||
|
|
||||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
|
||||||
MagicMock(return_value=(MagicMock(), None)))
|
MagicMock(return_value=(MagicMock(), None)))
|
||||||
@ -581,6 +583,7 @@ def test_print_json_spaces_default(mocker, hyperopt_conf, capsys) -> None:
|
|||||||
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
|
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
|
||||||
dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
|
dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
|
mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.calculate_market_change', return_value=1.5)
|
||||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
|
||||||
MagicMock(return_value=(MagicMock(), None)))
|
MagicMock(return_value=(MagicMock(), None)))
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
@ -622,6 +625,7 @@ def test_print_json_spaces_default(mocker, hyperopt_conf, capsys) -> None:
|
|||||||
def test_print_json_spaces_roi_stoploss(mocker, hyperopt_conf, capsys) -> None:
|
def test_print_json_spaces_roi_stoploss(mocker, hyperopt_conf, capsys) -> None:
|
||||||
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
|
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
|
||||||
dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
|
dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.calculate_market_change', return_value=1.5)
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
|
mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
|
||||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
|
||||||
MagicMock(return_value=(MagicMock(), None)))
|
MagicMock(return_value=(MagicMock(), None)))
|
||||||
@ -663,6 +667,7 @@ def test_print_json_spaces_roi_stoploss(mocker, hyperopt_conf, capsys) -> None:
|
|||||||
def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> None:
|
def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> None:
|
||||||
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
|
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
|
||||||
dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
|
dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.calculate_market_change', return_value=1.5)
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
|
mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
|
||||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
|
||||||
MagicMock(return_value=(MagicMock(), None)))
|
MagicMock(return_value=(MagicMock(), None)))
|
||||||
@ -736,6 +741,7 @@ def test_simplified_interface_all_failed(mocker, hyperopt_conf, caplog) -> None:
|
|||||||
def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None:
|
def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None:
|
||||||
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
|
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
|
||||||
dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
|
dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.calculate_market_change', return_value=1.5)
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
|
mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
|
||||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
|
||||||
MagicMock(return_value=(MagicMock(), None)))
|
MagicMock(return_value=(MagicMock(), None)))
|
||||||
@ -778,6 +784,7 @@ def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None:
|
|||||||
def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None:
|
def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None:
|
||||||
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
|
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
|
||||||
dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
|
dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.calculate_market_change', return_value=1.5)
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
|
mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
|
||||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
|
||||||
MagicMock(return_value=(MagicMock(), None)))
|
MagicMock(return_value=(MagicMock(), None)))
|
||||||
|
@ -188,15 +188,19 @@ async def test_emc_create_connection_success(default_conf, caplog, mocker):
|
|||||||
emc.shutdown()
|
emc.shutdown()
|
||||||
|
|
||||||
|
|
||||||
async def test_emc_create_connection_invalid_port(default_conf, caplog, mocker):
|
@pytest.mark.parametrize('host,port', [
|
||||||
|
(_TEST_WS_HOST, -1),
|
||||||
|
("10000.1241..2121/", _TEST_WS_PORT),
|
||||||
|
])
|
||||||
|
async def test_emc_create_connection_invalid_url(default_conf, caplog, mocker, host, port):
|
||||||
default_conf.update({
|
default_conf.update({
|
||||||
"external_message_consumer": {
|
"external_message_consumer": {
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"producers": [
|
"producers": [
|
||||||
{
|
{
|
||||||
"name": "default",
|
"name": "default",
|
||||||
"host": _TEST_WS_HOST,
|
"host": host,
|
||||||
"port": -1,
|
"port": port,
|
||||||
"ws_token": _TEST_WS_TOKEN
|
"ws_token": _TEST_WS_TOKEN
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -207,38 +211,13 @@ async def test_emc_create_connection_invalid_port(default_conf, caplog, mocker):
|
|||||||
})
|
})
|
||||||
|
|
||||||
dp = DataProvider(default_conf, None, None, None)
|
dp = DataProvider(default_conf, None, None, None)
|
||||||
|
# Handle start explicitly to avoid messing with threading in tests
|
||||||
|
mocker.patch("freqtrade.rpc.external_message_consumer.ExternalMessageConsumer.start",)
|
||||||
emc = ExternalMessageConsumer(default_conf, dp)
|
emc = ExternalMessageConsumer(default_conf, dp)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await asyncio.sleep(0.01)
|
emc._running = True
|
||||||
assert log_has_re(r".+ is an invalid WebSocket URL .+", caplog)
|
await emc._create_connection(emc.producers[0], asyncio.Lock())
|
||||||
finally:
|
|
||||||
emc.shutdown()
|
|
||||||
|
|
||||||
|
|
||||||
async def test_emc_create_connection_invalid_host(default_conf, caplog, mocker):
|
|
||||||
default_conf.update({
|
|
||||||
"external_message_consumer": {
|
|
||||||
"enabled": True,
|
|
||||||
"producers": [
|
|
||||||
{
|
|
||||||
"name": "default",
|
|
||||||
"host": "10000.1241..2121/",
|
|
||||||
"port": _TEST_WS_PORT,
|
|
||||||
"ws_token": _TEST_WS_TOKEN
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"wait_timeout": 60,
|
|
||||||
"ping_timeout": 60,
|
|
||||||
"sleep_timeout": 60
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
dp = DataProvider(default_conf, None, None, None)
|
|
||||||
emc = ExternalMessageConsumer(default_conf, dp)
|
|
||||||
|
|
||||||
try:
|
|
||||||
await asyncio.sleep(0.01)
|
|
||||||
assert log_has_re(r".+ is an invalid WebSocket URL .+", caplog)
|
assert log_has_re(r".+ is an invalid WebSocket URL .+", caplog)
|
||||||
finally:
|
finally:
|
||||||
emc.shutdown()
|
emc.shutdown()
|
||||||
|
@ -11,7 +11,7 @@ import pytest
|
|||||||
from jsonschema import ValidationError
|
from jsonschema import ValidationError
|
||||||
|
|
||||||
from freqtrade.commands import Arguments
|
from freqtrade.commands import Arguments
|
||||||
from freqtrade.configuration import Configuration, check_exchange, validate_config_consistency
|
from freqtrade.configuration import Configuration, validate_config_consistency
|
||||||
from freqtrade.configuration.config_validation import validate_config_schema
|
from freqtrade.configuration.config_validation import validate_config_schema
|
||||||
from freqtrade.configuration.deprecated_settings import (check_conflicting_settings,
|
from freqtrade.configuration.deprecated_settings import (check_conflicting_settings,
|
||||||
process_deprecated_setting,
|
process_deprecated_setting,
|
||||||
@ -584,67 +584,6 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
|
|||||||
assert config['runmode'] == RunMode.HYPEROPT
|
assert config['runmode'] == RunMode.HYPEROPT
|
||||||
|
|
||||||
|
|
||||||
def test_check_exchange(default_conf, caplog) -> None:
|
|
||||||
# Test an officially supported by Freqtrade team exchange
|
|
||||||
default_conf['runmode'] = RunMode.DRY_RUN
|
|
||||||
default_conf.get('exchange').update({'name': 'BITTREX'})
|
|
||||||
assert check_exchange(default_conf)
|
|
||||||
assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.",
|
|
||||||
caplog)
|
|
||||||
caplog.clear()
|
|
||||||
|
|
||||||
# Test an officially supported by Freqtrade team exchange
|
|
||||||
default_conf.get('exchange').update({'name': 'binance'})
|
|
||||||
assert check_exchange(default_conf)
|
|
||||||
assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.",
|
|
||||||
caplog)
|
|
||||||
caplog.clear()
|
|
||||||
|
|
||||||
# Test an available exchange, supported by ccxt
|
|
||||||
default_conf.get('exchange').update({'name': 'huobipro'})
|
|
||||||
assert check_exchange(default_conf)
|
|
||||||
assert log_has_re(r"Exchange .* is known to the the ccxt library, available for the bot, "
|
|
||||||
r"but not officially supported "
|
|
||||||
r"by the Freqtrade development team\. .*", caplog)
|
|
||||||
caplog.clear()
|
|
||||||
|
|
||||||
# Test a 'bad' exchange, which known to have serious problems
|
|
||||||
default_conf.get('exchange').update({'name': 'bitmex'})
|
|
||||||
with pytest.raises(OperationalException,
|
|
||||||
match=r"Exchange .* will not work with Freqtrade\..*"):
|
|
||||||
check_exchange(default_conf)
|
|
||||||
caplog.clear()
|
|
||||||
|
|
||||||
# Test a 'bad' exchange with check_for_bad=False
|
|
||||||
default_conf.get('exchange').update({'name': 'bitmex'})
|
|
||||||
assert check_exchange(default_conf, False)
|
|
||||||
assert log_has_re(r"Exchange .* is known to the the ccxt library, available for the bot, "
|
|
||||||
r"but not officially supported "
|
|
||||||
r"by the Freqtrade development team\. .*", caplog)
|
|
||||||
caplog.clear()
|
|
||||||
|
|
||||||
# Test an invalid exchange
|
|
||||||
default_conf.get('exchange').update({'name': 'unknown_exchange'})
|
|
||||||
with pytest.raises(
|
|
||||||
OperationalException,
|
|
||||||
match=r'Exchange "unknown_exchange" is not known to the ccxt library '
|
|
||||||
r'and therefore not available for the bot.*'
|
|
||||||
):
|
|
||||||
check_exchange(default_conf)
|
|
||||||
|
|
||||||
# Test no exchange...
|
|
||||||
default_conf.get('exchange').update({'name': ''})
|
|
||||||
default_conf['runmode'] = RunMode.PLOT
|
|
||||||
assert check_exchange(default_conf)
|
|
||||||
|
|
||||||
# Test no exchange...
|
|
||||||
default_conf.get('exchange').update({'name': ''})
|
|
||||||
default_conf['runmode'] = RunMode.UTIL_EXCHANGE
|
|
||||||
with pytest.raises(OperationalException,
|
|
||||||
match=r'This command requires a configured exchange.*'):
|
|
||||||
check_exchange(default_conf)
|
|
||||||
|
|
||||||
|
|
||||||
def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None:
|
def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None:
|
||||||
patched_configuration_load_config_file(mocker, default_conf)
|
patched_configuration_load_config_file(mocker, default_conf)
|
||||||
|
|
||||||
|
@ -28,6 +28,7 @@ from tests.conftest import (create_mock_trades, create_mock_trades_usdt, get_pat
|
|||||||
from tests.conftest_trades import (MOCK_TRADE_COUNT, entry_side, exit_side, mock_order_1,
|
from tests.conftest_trades import (MOCK_TRADE_COUNT, entry_side, exit_side, mock_order_1,
|
||||||
mock_order_2, mock_order_2_sell, mock_order_3, mock_order_3_sell,
|
mock_order_2, mock_order_2_sell, mock_order_3, mock_order_3_sell,
|
||||||
mock_order_4, mock_order_5_stoploss, mock_order_6_sell)
|
mock_order_4, mock_order_5_stoploss, mock_order_6_sell)
|
||||||
|
from tests.conftest_trades_usdt import mock_trade_usdt_4
|
||||||
|
|
||||||
|
|
||||||
def patch_RPCManager(mocker) -> MagicMock:
|
def patch_RPCManager(mocker) -> MagicMock:
|
||||||
@ -1060,6 +1061,7 @@ def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_sho
|
|||||||
freqtrade = FreqtradeBot(default_conf_usdt)
|
freqtrade = FreqtradeBot(default_conf_usdt)
|
||||||
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
||||||
|
|
||||||
|
# TODO: should not be magicmock
|
||||||
trade = MagicMock()
|
trade = MagicMock()
|
||||||
trade.is_short = is_short
|
trade.is_short = is_short
|
||||||
trade.open_order_id = None
|
trade.open_order_id = None
|
||||||
@ -1101,6 +1103,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
|
|||||||
# First case: when stoploss is not yet set but the order is open
|
# First case: when stoploss is not yet set but the order is open
|
||||||
# should get the stoploss order id immediately
|
# should get the stoploss order id immediately
|
||||||
# and should return false as no trade actually happened
|
# and should return false as no trade actually happened
|
||||||
|
# TODO: should not be magicmock
|
||||||
trade = MagicMock()
|
trade = MagicMock()
|
||||||
trade.is_short = is_short
|
trade.is_short = is_short
|
||||||
trade.is_open = True
|
trade.is_open = True
|
||||||
@ -1879,6 +1882,7 @@ def test_exit_positions(mocker, default_conf_usdt, limit_order, is_short, caplog
|
|||||||
return_value=limit_order[entry_side(is_short)])
|
return_value=limit_order[entry_side(is_short)])
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[])
|
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[])
|
||||||
|
|
||||||
|
# TODO: should not be magicmock
|
||||||
trade = MagicMock()
|
trade = MagicMock()
|
||||||
trade.is_short = is_short
|
trade.is_short = is_short
|
||||||
trade.open_order_id = '123'
|
trade.open_order_id = '123'
|
||||||
@ -1902,6 +1906,7 @@ def test_exit_positions_exception(mocker, default_conf_usdt, limit_order, caplog
|
|||||||
order = limit_order[entry_side(is_short)]
|
order = limit_order[entry_side(is_short)]
|
||||||
mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order)
|
mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order)
|
||||||
|
|
||||||
|
# TODO: should not be magicmock
|
||||||
trade = MagicMock()
|
trade = MagicMock()
|
||||||
trade.is_short = is_short
|
trade.is_short = is_short
|
||||||
trade.open_order_id = None
|
trade.open_order_id = None
|
||||||
@ -2042,6 +2047,7 @@ def test_update_trade_state_exception(mocker, default_conf_usdt, is_short, limit
|
|||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order)
|
mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order)
|
||||||
|
|
||||||
|
# TODO: should not be magicmock
|
||||||
trade = MagicMock()
|
trade = MagicMock()
|
||||||
trade.open_order_id = '123'
|
trade.open_order_id = '123'
|
||||||
trade.amount = 123
|
trade.amount = 123
|
||||||
@ -2060,6 +2066,7 @@ def test_update_trade_state_orderexception(mocker, default_conf_usdt, caplog) ->
|
|||||||
mocker.patch('freqtrade.exchange.Exchange.fetch_order',
|
mocker.patch('freqtrade.exchange.Exchange.fetch_order',
|
||||||
MagicMock(side_effect=InvalidOrderException))
|
MagicMock(side_effect=InvalidOrderException))
|
||||||
|
|
||||||
|
# TODO: should not be magicmock
|
||||||
trade = MagicMock()
|
trade = MagicMock()
|
||||||
trade.open_order_id = '123'
|
trade.open_order_id = '123'
|
||||||
|
|
||||||
@ -2980,7 +2987,7 @@ def test_manage_open_orders_exception(default_conf_usdt, ticker_usdt, open_trade
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("is_short", [False, True])
|
@pytest.mark.parametrize("is_short", [False, True])
|
||||||
def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order, is_short) -> None:
|
def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order, is_short, fee) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
l_order = limit_order[entry_side(is_short)]
|
l_order = limit_order[entry_side(is_short)]
|
||||||
@ -2994,16 +3001,12 @@ def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order, is_
|
|||||||
freqtrade = FreqtradeBot(default_conf_usdt)
|
freqtrade = FreqtradeBot(default_conf_usdt)
|
||||||
freqtrade._notify_enter_cancel = MagicMock()
|
freqtrade._notify_enter_cancel = MagicMock()
|
||||||
|
|
||||||
# TODO: Convert to real trade
|
trade = mock_trade_usdt_4(fee, is_short)
|
||||||
trade = MagicMock()
|
Trade.query.session.add(trade)
|
||||||
trade.pair = 'LTC/USDT'
|
Trade.commit()
|
||||||
trade.open_rate = 200
|
|
||||||
trade.is_short = False
|
|
||||||
trade.entry_side = "buy"
|
|
||||||
trade.amount = 100
|
|
||||||
l_order['filled'] = 0.0
|
l_order['filled'] = 0.0
|
||||||
l_order['status'] = 'open'
|
l_order['status'] = 'open'
|
||||||
trade.nr_of_successful_entries = 0
|
|
||||||
reason = CANCEL_REASON['TIMEOUT']
|
reason = CANCEL_REASON['TIMEOUT']
|
||||||
assert freqtrade.handle_cancel_enter(trade, l_order, reason)
|
assert freqtrade.handle_cancel_enter(trade, l_order, reason)
|
||||||
assert cancel_order_mock.call_count == 1
|
assert cancel_order_mock.call_count == 1
|
||||||
@ -3035,7 +3038,7 @@ def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order, is_
|
|||||||
@pytest.mark.parametrize("is_short", [False, True])
|
@pytest.mark.parametrize("is_short", [False, True])
|
||||||
@pytest.mark.parametrize("limit_buy_order_canceled_empty", ['binance', 'ftx', 'kraken', 'bittrex'],
|
@pytest.mark.parametrize("limit_buy_order_canceled_empty", ['binance', 'ftx', 'kraken', 'bittrex'],
|
||||||
indirect=['limit_buy_order_canceled_empty'])
|
indirect=['limit_buy_order_canceled_empty'])
|
||||||
def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf_usdt, is_short,
|
def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf_usdt, is_short, fee,
|
||||||
limit_buy_order_canceled_empty) -> None:
|
limit_buy_order_canceled_empty) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
@ -3046,11 +3049,10 @@ def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf_usdt, is_sho
|
|||||||
freqtrade = FreqtradeBot(default_conf_usdt)
|
freqtrade = FreqtradeBot(default_conf_usdt)
|
||||||
|
|
||||||
reason = CANCEL_REASON['TIMEOUT']
|
reason = CANCEL_REASON['TIMEOUT']
|
||||||
# TODO: Convert to real trade
|
|
||||||
trade = MagicMock()
|
trade = mock_trade_usdt_4(fee, is_short)
|
||||||
trade.nr_of_successful_entries = 0
|
Trade.query.session.add(trade)
|
||||||
trade.pair = 'LTC/ETH'
|
Trade.commit()
|
||||||
trade.entry_side = "sell" if is_short else "buy"
|
|
||||||
assert freqtrade.handle_cancel_enter(trade, limit_buy_order_canceled_empty, reason)
|
assert freqtrade.handle_cancel_enter(trade, limit_buy_order_canceled_empty, reason)
|
||||||
assert cancel_order_mock.call_count == 0
|
assert cancel_order_mock.call_count == 0
|
||||||
assert log_has_re(
|
assert log_has_re(
|
||||||
@ -3068,7 +3070,7 @@ def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf_usdt, is_sho
|
|||||||
'String Return value',
|
'String Return value',
|
||||||
123
|
123
|
||||||
])
|
])
|
||||||
def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_order, is_short,
|
def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_order, is_short, fee,
|
||||||
cancelorder) -> None:
|
cancelorder) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
@ -3076,20 +3078,15 @@ def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_order
|
|||||||
cancel_order_mock = MagicMock(return_value=cancelorder)
|
cancel_order_mock = MagicMock(return_value=cancelorder)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
cancel_order=cancel_order_mock
|
cancel_order=cancel_order_mock,
|
||||||
|
fetch_order=MagicMock(side_effect=InvalidOrderException)
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf_usdt)
|
freqtrade = FreqtradeBot(default_conf_usdt)
|
||||||
freqtrade._notify_enter_cancel = MagicMock()
|
freqtrade._notify_enter_cancel = MagicMock()
|
||||||
# TODO: Convert to real trade
|
trade = mock_trade_usdt_4(fee, is_short)
|
||||||
trade = MagicMock()
|
Trade.query.session.add(trade)
|
||||||
trade.pair = 'LTC/USDT'
|
Trade.commit()
|
||||||
trade.entry_side = "buy"
|
|
||||||
trade.open_rate = 200
|
|
||||||
trade.entry_side = "buy"
|
|
||||||
trade.open_order_id = "open_order_noop"
|
|
||||||
trade.nr_of_successful_entries = 0
|
|
||||||
trade.amount = 100
|
|
||||||
l_order['filled'] = 0.0
|
l_order['filled'] = 0.0
|
||||||
l_order['status'] = 'open'
|
l_order['status'] = 'open'
|
||||||
reason = CANCEL_REASON['TIMEOUT']
|
reason = CANCEL_REASON['TIMEOUT']
|
||||||
@ -3098,6 +3095,9 @@ def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_order
|
|||||||
|
|
||||||
cancel_order_mock.reset_mock()
|
cancel_order_mock.reset_mock()
|
||||||
l_order['filled'] = 1.0
|
l_order['filled'] = 1.0
|
||||||
|
order = deepcopy(l_order)
|
||||||
|
order['status'] = 'canceled'
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order)
|
||||||
assert not freqtrade.handle_cancel_enter(trade, l_order, reason)
|
assert not freqtrade.handle_cancel_enter(trade, l_order, reason)
|
||||||
assert cancel_order_mock.call_count == 1
|
assert cancel_order_mock.call_count == 1
|
||||||
|
|
||||||
@ -3111,6 +3111,9 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None:
|
|||||||
cancel_order=cancel_order_mock,
|
cancel_order=cancel_order_mock,
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_rate', return_value=0.245441)
|
mocker.patch('freqtrade.exchange.Exchange.get_rate', return_value=0.245441)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_min_pair_stake_amount', return_value=0.2)
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_order_fee')
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf_usdt)
|
freqtrade = FreqtradeBot(default_conf_usdt)
|
||||||
|
|
||||||
@ -3175,10 +3178,13 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None:
|
|||||||
assert send_msg_mock.call_count == 1
|
assert send_msg_mock.call_count == 1
|
||||||
assert trade.close_rate is None
|
assert trade.close_rate is None
|
||||||
assert trade.exit_reason is None
|
assert trade.exit_reason is None
|
||||||
|
assert trade.open_order_id is None
|
||||||
|
|
||||||
send_msg_mock.reset_mock()
|
send_msg_mock.reset_mock()
|
||||||
|
|
||||||
|
# Partial exit - below exit threshold
|
||||||
order['amount'] = 2
|
order['amount'] = 2
|
||||||
|
order['filled'] = 1.9
|
||||||
assert not freqtrade.handle_cancel_exit(trade, order, reason)
|
assert not freqtrade.handle_cancel_exit(trade, order, reason)
|
||||||
# Assert cancel_order was not called (callcount remains unchanged)
|
# Assert cancel_order was not called (callcount remains unchanged)
|
||||||
assert cancel_order_mock.call_count == 1
|
assert cancel_order_mock.call_count == 1
|
||||||
@ -3188,12 +3194,21 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None:
|
|||||||
|
|
||||||
assert not freqtrade.handle_cancel_exit(trade, order, reason)
|
assert not freqtrade.handle_cancel_exit(trade, order, reason)
|
||||||
|
|
||||||
send_msg_mock.call_args_list[0][0][0]['reason'] = CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
assert (send_msg_mock.call_args_list[0][0][0]['reason']
|
||||||
|
== CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'])
|
||||||
|
|
||||||
# Message should not be iterated again
|
# Message should not be iterated again
|
||||||
assert trade.exit_order_status == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
assert trade.exit_order_status == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
||||||
assert send_msg_mock.call_count == 1
|
assert send_msg_mock.call_count == 1
|
||||||
|
|
||||||
|
send_msg_mock.reset_mock()
|
||||||
|
|
||||||
|
order['filled'] = 1
|
||||||
|
assert freqtrade.handle_cancel_exit(trade, order, reason)
|
||||||
|
assert send_msg_mock.call_count == 1
|
||||||
|
assert (send_msg_mock.call_args_list[0][0][0]['reason']
|
||||||
|
== CANCEL_REASON['PARTIALLY_FILLED'])
|
||||||
|
|
||||||
|
|
||||||
def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None:
|
def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
@ -3204,6 +3219,7 @@ def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None:
|
|||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf_usdt)
|
freqtrade = FreqtradeBot(default_conf_usdt)
|
||||||
|
|
||||||
|
# TODO: should not be magicmock
|
||||||
trade = MagicMock()
|
trade = MagicMock()
|
||||||
reason = CANCEL_REASON['TIMEOUT']
|
reason = CANCEL_REASON['TIMEOUT']
|
||||||
order = {'remaining': 1,
|
order = {'remaining': 1,
|
||||||
|
@ -351,8 +351,13 @@ def test_dca_short(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
|
|||||||
assert trade.nr_of_successful_exits == 1
|
assert trade.nr_of_successful_exits == 1
|
||||||
|
|
||||||
|
|
||||||
def test_dca_order_adjust(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
|
@pytest.mark.parametrize('leverage', [
|
||||||
|
1, 2
|
||||||
|
])
|
||||||
|
def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker) -> None:
|
||||||
default_conf_usdt['position_adjustment_enable'] = True
|
default_conf_usdt['position_adjustment_enable'] = True
|
||||||
|
default_conf_usdt['trading_mode'] = 'futures'
|
||||||
|
default_conf_usdt['margin_mode'] = 'isolated'
|
||||||
|
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -363,9 +368,14 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
|
|||||||
price_to_precision=lambda s, x, y: y,
|
price_to_precision=lambda s, x, y: y,
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled', return_value=False)
|
mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled', return_value=False)
|
||||||
|
mocker.patch("freqtrade.exchange.Exchange.get_max_leverage", return_value=10)
|
||||||
|
mocker.patch("freqtrade.exchange.Exchange.get_funding_fees", return_value=0)
|
||||||
|
mocker.patch("freqtrade.exchange.Exchange.get_maintenance_ratio_and_amt", return_value=(0, 0))
|
||||||
|
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
freqtrade.strategy.custom_entry_price = lambda **kwargs: ticker_usdt['ask'] * 0.96
|
freqtrade.strategy.custom_entry_price = lambda **kwargs: ticker_usdt['ask'] * 0.96
|
||||||
|
freqtrade.strategy.leverage = MagicMock(return_value=leverage)
|
||||||
|
freqtrade.strategy.minimal_roi = {0: 0.2}
|
||||||
|
|
||||||
freqtrade.enter_positions()
|
freqtrade.enter_positions()
|
||||||
|
|
||||||
@ -377,6 +387,8 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
|
|||||||
assert trade.open_rate == 1.96
|
assert trade.open_rate == 1.96
|
||||||
assert trade.stop_loss_pct is None
|
assert trade.stop_loss_pct is None
|
||||||
assert trade.stop_loss == 0.0
|
assert trade.stop_loss == 0.0
|
||||||
|
assert trade.leverage == leverage
|
||||||
|
assert trade.stake_amount == 60
|
||||||
assert trade.initial_stop_loss == 0.0
|
assert trade.initial_stop_loss == 0.0
|
||||||
assert trade.initial_stop_loss_pct is None
|
assert trade.initial_stop_loss_pct is None
|
||||||
# No adjustment
|
# No adjustment
|
||||||
@ -396,6 +408,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
|
|||||||
assert trade.open_rate == 1.96
|
assert trade.open_rate == 1.96
|
||||||
assert trade.stop_loss_pct is None
|
assert trade.stop_loss_pct is None
|
||||||
assert trade.stop_loss == 0.0
|
assert trade.stop_loss == 0.0
|
||||||
|
assert trade.stake_amount == 60
|
||||||
assert trade.initial_stop_loss == 0.0
|
assert trade.initial_stop_loss == 0.0
|
||||||
assert trade.initial_stop_loss_pct is None
|
assert trade.initial_stop_loss_pct is None
|
||||||
|
|
||||||
@ -407,9 +420,10 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
|
|||||||
assert trade.open_order_id is None
|
assert trade.open_order_id is None
|
||||||
# Open rate is not adjusted yet
|
# Open rate is not adjusted yet
|
||||||
assert trade.open_rate == 1.99
|
assert trade.open_rate == 1.99
|
||||||
|
assert trade.stake_amount == 60
|
||||||
assert trade.stop_loss_pct == -0.1
|
assert trade.stop_loss_pct == -0.1
|
||||||
assert trade.stop_loss == 1.99 * 0.9
|
assert pytest.approx(trade.stop_loss) == 1.99 * (1 - 0.1 / leverage)
|
||||||
assert trade.initial_stop_loss == 1.99 * 0.9
|
assert pytest.approx(trade.initial_stop_loss) == 1.99 * (1 - 0.1 / leverage)
|
||||||
assert trade.initial_stop_loss_pct == -0.1
|
assert trade.initial_stop_loss_pct == -0.1
|
||||||
|
|
||||||
# 2nd order - not filling
|
# 2nd order - not filling
|
||||||
@ -422,7 +436,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
|
|||||||
assert trade.open_order_id is not None
|
assert trade.open_order_id is not None
|
||||||
assert trade.open_rate == 1.99
|
assert trade.open_rate == 1.99
|
||||||
assert trade.orders[-1].price == 1.96
|
assert trade.orders[-1].price == 1.96
|
||||||
assert trade.orders[-1].cost == 120
|
assert trade.orders[-1].cost == 120 * leverage
|
||||||
|
|
||||||
# Replace new order with diff. order at a lower price
|
# Replace new order with diff. order at a lower price
|
||||||
freqtrade.strategy.adjust_entry_price = MagicMock(return_value=1.95)
|
freqtrade.strategy.adjust_entry_price = MagicMock(return_value=1.95)
|
||||||
@ -432,8 +446,9 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
|
|||||||
assert len(trade.orders) == 4
|
assert len(trade.orders) == 4
|
||||||
assert trade.open_order_id is not None
|
assert trade.open_order_id is not None
|
||||||
assert trade.open_rate == 1.99
|
assert trade.open_rate == 1.99
|
||||||
|
assert trade.stake_amount == 60
|
||||||
assert trade.orders[-1].price == 1.95
|
assert trade.orders[-1].price == 1.95
|
||||||
assert pytest.approx(trade.orders[-1].cost) == 120
|
assert pytest.approx(trade.orders[-1].cost) == 120 * leverage
|
||||||
|
|
||||||
# Fill DCA order
|
# Fill DCA order
|
||||||
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=None)
|
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=None)
|
||||||
@ -446,13 +461,13 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
|
|||||||
assert trade.open_order_id is None
|
assert trade.open_order_id is None
|
||||||
assert pytest.approx(trade.open_rate) == 1.963153456
|
assert pytest.approx(trade.open_rate) == 1.963153456
|
||||||
assert trade.orders[-1].price == 1.95
|
assert trade.orders[-1].price == 1.95
|
||||||
assert pytest.approx(trade.orders[-1].cost) == 120
|
assert pytest.approx(trade.orders[-1].cost) == 120 * leverage
|
||||||
assert trade.orders[-1].status == 'closed'
|
assert trade.orders[-1].status == 'closed'
|
||||||
|
|
||||||
assert pytest.approx(trade.amount) == 91.689215
|
assert pytest.approx(trade.amount) == 91.689215 * leverage
|
||||||
# Check the 2 filled orders equal the above amount
|
# Check the 2 filled orders equal the above amount
|
||||||
assert pytest.approx(trade.orders[1].amount) == 30.150753768
|
assert pytest.approx(trade.orders[1].amount) == 30.150753768 * leverage
|
||||||
assert pytest.approx(trade.orders[-1].amount) == 61.538461232
|
assert pytest.approx(trade.orders[-1].amount) == 61.538461232 * leverage
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('leverage', [1, 2])
|
@pytest.mark.parametrize('leverage', [1, 2])
|
||||||
|
@ -63,7 +63,7 @@ def test_init_plotscript(default_conf, mocker, testdatadir):
|
|||||||
|
|
||||||
def test_add_indicators(default_conf, testdatadir, caplog):
|
def test_add_indicators(default_conf, testdatadir, caplog):
|
||||||
pair = "UNITTEST/BTC"
|
pair = "UNITTEST/BTC"
|
||||||
timerange = TimeRange(None, 'line', 0, -1000)
|
timerange = TimeRange()
|
||||||
|
|
||||||
data = history.load_pair_history(pair=pair, timeframe='1m',
|
data = history.load_pair_history(pair=pair, timeframe='1m',
|
||||||
datadir=testdatadir, timerange=timerange)
|
datadir=testdatadir, timerange=timerange)
|
||||||
|
Loading…
Reference in New Issue
Block a user