Compare commits

..

13 Commits

Author SHA1 Message Date
robcaulk
6c96a2464f Merge remote-tracking branch 'origin/develop' into feat/convolutional-neural-net 2022-12-16 12:24:35 +01:00
robcaulk
2c3a310ce2 allow DI with CNN 2022-12-07 20:30:13 +01:00
robcaulk
71c6ff18c4 try to avoid possible memory leaks 2022-12-07 20:08:31 +01:00
robcaulk
b438cd4b3f add newline to end of freqai-configuration.md 2022-12-07 19:52:31 +01:00
robcaulk
6343fbf9e3 remove verbose from CNNPredictionModel 2022-12-07 00:02:02 +01:00
robcaulk
389ab7e44b add test for CNNPredictionModel 2022-12-06 23:50:34 +01:00
robcaulk
665eed3906 add documentation for CNN, allow it to interact with model_training_parameters 2022-12-06 23:26:07 +01:00
robcaulk
9ce8255f24 isort. 2022-12-05 21:03:05 +01:00
robcaulk
72b1d1c9ae allow users to pass 0 test data 2022-12-05 20:55:05 +01:00
robcaulk
5826fae8ee Merge remote-tracking branch 'origin/develop' into feat/convolutional-neural-net 2022-12-05 20:40:19 +01:00
robcaulk
43c0d305a3 fix tensorflow version 2022-12-05 20:36:08 +01:00
Emre
ad7729e5d8 Fix function signature 2022-12-03 17:43:59 +03:00
robcaulk
57aaa390d0 start convolution neural network plugin 2022-11-27 17:42:03 +01:00
158 changed files with 7204 additions and 9871 deletions

View File

@@ -148,19 +148,6 @@ jobs:
if: runner.os == 'macOS' if: runner.os == 'macOS'
run: | run: |
brew update brew update
# homebrew fails to update python due to unlinking failures
# https://github.com/actions/runner-images/issues/6817
rm /usr/local/bin/2to3 || true
rm /usr/local/bin/2to3-3.11 || true
rm /usr/local/bin/idle3 || true
rm /usr/local/bin/idle3.11 || true
rm /usr/local/bin/pydoc3 || true
rm /usr/local/bin/pydoc3.11 || true
rm /usr/local/bin/python3 || true
rm /usr/local/bin/python3.11 || true
rm /usr/local/bin/python3-config || true
rm /usr/local/bin/python3.11-config || true
brew install hdf5 c-blosc brew install hdf5 c-blosc
python -m pip install --upgrade pip wheel python -m pip install --upgrade pip wheel
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
@@ -360,8 +347,6 @@ jobs:
pip install -e . pip install -e .
- name: Tests incl. ccxt compatibility tests - name: Tests incl. ccxt compatibility tests
env:
CI_WEB_PROXY: http://152.67.78.211:13128
run: | run: |
pytest --random-order --cov=freqtrade --cov-config=.coveragerc --longrun pytest --random-order --cov=freqtrade --cov-config=.coveragerc --longrun

View File

@@ -8,16 +8,16 @@ repos:
# stages: [push] # stages: [push]
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: "v0.991" rev: "v0.942"
hooks: hooks:
- id: mypy - id: mypy
exclude: build_helpers exclude: build_helpers
additional_dependencies: additional_dependencies:
- types-cachetools==5.2.1 - types-cachetools==5.2.1
- types-filelock==3.2.7 - types-filelock==3.2.7
- types-requests==2.28.11.8 - types-requests==2.28.11.5
- types-tabulate==0.9.0.0 - types-tabulate==0.9.0.0
- types-python-dateutil==2.8.19.6 - types-python-dateutil==2.8.19.4
# stages: [push] # stages: [push]
- repo: https://github.com/pycqa/isort - repo: https://github.com/pycqa/isort

View File

@@ -1,7 +1,6 @@
# ![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade_poweredby.svg) # ![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade_poweredby.svg)
[![Freqtrade CI](https://github.com/freqtrade/freqtrade/workflows/Freqtrade%20CI/badge.svg)](https://github.com/freqtrade/freqtrade/actions/) [![Freqtrade CI](https://github.com/freqtrade/freqtrade/workflows/Freqtrade%20CI/badge.svg)](https://github.com/freqtrade/freqtrade/actions/)
[![DOI](https://joss.theoj.org/papers/10.21105/joss.04864/status.svg)](https://doi.org/10.21105/joss.04864)
[![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop) [![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
[![Documentation](https://readthedocs.org/projects/freqtrade/badge/)](https://www.freqtrade.io) [![Documentation](https://readthedocs.org/projects/freqtrade/badge/)](https://www.freqtrade.io)
[![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability) [![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)

View File

@@ -70,21 +70,20 @@ docker push ${CACHE_IMAGE}:$TAG_ARM
# Otherwise installation might fail. # Otherwise installation might fail.
echo "create manifests" echo "create manifests"
docker manifest create ${IMAGE_NAME}:${TAG} ${CACHE_IMAGE}:${TAG} ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI} docker manifest create --amend ${IMAGE_NAME}:${TAG} ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI} ${CACHE_IMAGE}:${TAG}
docker manifest push -p ${IMAGE_NAME}:${TAG} docker manifest push -p ${IMAGE_NAME}:${TAG}
docker manifest create ${IMAGE_NAME}:${TAG_PLOT} ${CACHE_IMAGE}:${TAG_PLOT} ${CACHE_IMAGE}:${TAG_PLOT_ARM} docker manifest create ${IMAGE_NAME}:${TAG_PLOT} ${CACHE_IMAGE}:${TAG_PLOT_ARM} ${CACHE_IMAGE}:${TAG_PLOT}
docker manifest push -p ${IMAGE_NAME}:${TAG_PLOT} docker manifest push -p ${IMAGE_NAME}:${TAG_PLOT}
docker manifest create ${IMAGE_NAME}:${TAG_FREQAI} ${CACHE_IMAGE}:${TAG_FREQAI} ${CACHE_IMAGE}:${TAG_FREQAI_ARM} docker manifest create ${IMAGE_NAME}:${TAG_FREQAI} ${CACHE_IMAGE}:${TAG_FREQAI_ARM} ${CACHE_IMAGE}:${TAG_FREQAI}
docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI} docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI}
docker manifest create ${IMAGE_NAME}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL_ARM} docker manifest create ${IMAGE_NAME}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL_ARM} ${CACHE_IMAGE}:${TAG_FREQAI_RL}
docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI_RL} docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI_RL}
# Tag as latest for develop builds # Tag as latest for develop builds
if [ "${TAG}" = "develop" ]; then if [ "${TAG}" = "develop" ]; then
echo 'Tagging image as latest'
docker manifest create ${IMAGE_NAME}:latest ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI} ${CACHE_IMAGE}:${TAG} docker manifest create ${IMAGE_NAME}:latest ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI} ${CACHE_IMAGE}:${TAG}
docker manifest push -p ${IMAGE_NAME}:latest docker manifest push -p ${IMAGE_NAME}:latest
fi fi

View File

@@ -26,10 +26,7 @@ if [ "${GITHUB_EVENT_NAME}" = "schedule" ]; then
--cache-to=type=registry,ref=${CACHE_TAG} \ --cache-to=type=registry,ref=${CACHE_TAG} \
-f docker/Dockerfile.armhf \ -f docker/Dockerfile.armhf \
--platform ${PI_PLATFORM} \ --platform ${PI_PLATFORM} \
-t ${IMAGE_NAME}:${TAG_PI} \ -t ${IMAGE_NAME}:${TAG_PI} --push .
--push \
--provenance=false \
.
else else
echo "event ${GITHUB_EVENT_NAME}: building with cache" echo "event ${GITHUB_EVENT_NAME}: building with cache"
# Build regular image # Build regular image
@@ -38,16 +35,12 @@ else
# Pull last build to avoid rebuilding the whole image # Pull last build to avoid rebuilding the whole image
# docker pull --platform ${PI_PLATFORM} ${IMAGE_NAME}:${TAG} # docker pull --platform ${PI_PLATFORM} ${IMAGE_NAME}:${TAG}
# disable provenance due to https://github.com/docker/buildx/issues/1509
docker buildx build \ docker buildx build \
--cache-from=type=registry,ref=${CACHE_TAG} \ --cache-from=type=registry,ref=${CACHE_TAG} \
--cache-to=type=registry,ref=${CACHE_TAG} \ --cache-to=type=registry,ref=${CACHE_TAG} \
-f docker/Dockerfile.armhf \ -f docker/Dockerfile.armhf \
--platform ${PI_PLATFORM} \ --platform ${PI_PLATFORM} \
-t ${IMAGE_NAME}:${TAG_PI} \ -t ${IMAGE_NAME}:${TAG_PI} --push .
--push \
--provenance=false \
.
fi fi
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
@@ -75,10 +68,12 @@ fi
docker images docker images
docker push ${CACHE_IMAGE}:$TAG docker push ${CACHE_IMAGE}
docker push ${CACHE_IMAGE}:$TAG_PLOT docker push ${CACHE_IMAGE}:$TAG_PLOT
docker push ${CACHE_IMAGE}:$TAG_FREQAI docker push ${CACHE_IMAGE}:$TAG_FREQAI
docker push ${CACHE_IMAGE}:$TAG_FREQAI_RL docker push ${CACHE_IMAGE}:$TAG_FREQAI_RL
docker push ${CACHE_IMAGE}:$TAG
docker images docker images

View File

@@ -59,6 +59,20 @@
"pairlists": [ "pairlists": [
{"method": "StaticPairList"} {"method": "StaticPairList"}
], ],
"edge": {
"enabled": false,
"process_throttle_secs": 3600,
"calculate_since_number_of_days": 7,
"allowed_risk": 0.01,
"stoploss_range_min": -0.01,
"stoploss_range_max": -0.1,
"stoploss_range_step": -0.01,
"minimum_winrate": 0.60,
"minimum_expectancy": 0.20,
"min_trade_number": 10,
"max_trade_duration_minute": 1440,
"remove_pumps": false
},
"telegram": { "telegram": {
"enabled": false, "enabled": false,
"token": "your_telegram_token", "token": "your_telegram_token",

View File

@@ -56,6 +56,20 @@
"pairlists": [ "pairlists": [
{"method": "StaticPairList"} {"method": "StaticPairList"}
], ],
"edge": {
"enabled": false,
"process_throttle_secs": 3600,
"calculate_since_number_of_days": 7,
"allowed_risk": 0.01,
"stoploss_range_min": -0.01,
"stoploss_range_max": -0.1,
"stoploss_range_step": -0.01,
"minimum_winrate": 0.60,
"minimum_expectancy": 0.20,
"min_trade_number": 10,
"max_trade_duration_minute": 1440,
"remove_pumps": false
},
"telegram": { "telegram": {
"enabled": false, "enabled": false,
"token": "your_telegram_token", "token": "your_telegram_token",

View File

@@ -21,8 +21,8 @@
"ccxt_config": {}, "ccxt_config": {},
"ccxt_async_config": {}, "ccxt_async_config": {},
"pair_whitelist": [ "pair_whitelist": [
"1INCH/USDT:USDT", "1INCH/USDT",
"ALGO/USDT:USDT" "ALGO/USDT"
], ],
"pair_blacklist": [] "pair_blacklist": []
}, },
@@ -60,8 +60,8 @@
"1h" "1h"
], ],
"include_corr_pairlist": [ "include_corr_pairlist": [
"BTC/USDT:USDT", "BTC/USDT",
"ETH/USDT:USDT" "ETH/USDT"
], ],
"label_period_candles": 20, "label_period_candles": 20,
"include_shifted_candles": 2, "include_shifted_candles": 2,

View File

@@ -64,6 +64,20 @@
"pairlists": [ "pairlists": [
{"method": "StaticPairList"} {"method": "StaticPairList"}
], ],
"edge": {
"enabled": false,
"process_throttle_secs": 3600,
"calculate_since_number_of_days": 7,
"allowed_risk": 0.01,
"stoploss_range_min": -0.01,
"stoploss_range_max": -0.1,
"stoploss_range_step": -0.01,
"minimum_winrate": 0.60,
"minimum_expectancy": 0.20,
"min_trade_number": 10,
"max_trade_duration_minute": 1440,
"remove_pumps": false
},
"telegram": { "telegram": {
"enabled": false, "enabled": false,
"token": "your_telegram_token", "token": "your_telegram_token",

View File

@@ -32,7 +32,7 @@ To analyze the entry/exit tags, we now need to use the `freqtrade backtesting-an
with `--analysis-groups` option provided with space-separated arguments (default `0 1 2`): with `--analysis-groups` option provided with space-separated arguments (default `0 1 2`):
``` bash ``` bash
freqtrade backtesting-analysis -c <config.json> --analysis-groups 0 1 2 3 4 5 freqtrade backtesting-analysis -c <config.json> --analysis-groups 0 1 2 3 4
``` ```
This command will read from the last backtesting results. The `--analysis-groups` option is This command will read from the last backtesting results. The `--analysis-groups` option is
@@ -43,7 +43,6 @@ ranging from the simplest (0) to the most detailed per pair, per buy and per sel
* 2: profit summaries grouped by enter_tag and exit_tag * 2: profit summaries grouped by enter_tag and exit_tag
* 3: profit summaries grouped by pair and enter_tag * 3: profit summaries grouped by pair and enter_tag
* 4: profit summaries grouped by pair, enter_ and exit_tag (this can get quite large) * 4: profit summaries grouped by pair, enter_ and exit_tag (this can get quite large)
* 5: profit summaries grouped by exit_tag
More options are available by running with the `-h` option. More options are available by running with the `-h` option.

View File

@@ -75,7 +75,7 @@ This function needs to return a floating point number (`float`). Smaller numbers
## Overriding pre-defined spaces ## Overriding pre-defined spaces
To override a pre-defined space (`roi_space`, `generate_roi_table`, `stoploss_space`, `trailing_space`, `max_open_trades_space`), define a nested class called Hyperopt and define the required spaces as follows: To override a pre-defined space (`roi_space`, `generate_roi_table`, `stoploss_space`, `trailing_space`), define a nested class called Hyperopt and define the required spaces as follows:
```python ```python
from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal
@@ -123,12 +123,6 @@ class MyAwesomeStrategy(IStrategy):
Categorical([True, False], name='trailing_only_offset_is_reached'), Categorical([True, False], name='trailing_only_offset_is_reached'),
] ]
# Define a custom max_open_trades space
def max_open_trades_space(self) -> List[Dimension]:
return [
Integer(-1, 10, name='max_open_trades'),
]
``` ```
!!! Note !!! Note

View File

@@ -300,11 +300,7 @@ A backtesting result will look like that:
| Absolute profit | 0.00762792 BTC | | Absolute profit | 0.00762792 BTC |
| Total profit % | 76.2% | | Total profit % | 76.2% |
| CAGR % | 460.87% | | CAGR % | 460.87% |
| Sortino | 1.88 |
| Sharpe | 2.97 |
| Calmar | 6.29 |
| Profit factor | 1.11 | | Profit factor | 1.11 |
| Expectancy | -0.15 |
| Avg. stake amount | 0.001 BTC | | Avg. stake amount | 0.001 BTC |
| Total trade volume | 0.429 BTC | | Total trade volume | 0.429 BTC |
| | | | | |
@@ -404,11 +400,7 @@ It contains some useful key metrics about performance of your strategy on backte
| Absolute profit | 0.00762792 BTC | | Absolute profit | 0.00762792 BTC |
| Total profit % | 76.2% | | Total profit % | 76.2% |
| CAGR % | 460.87% | | CAGR % | 460.87% |
| Sortino | 1.88 |
| Sharpe | 2.97 |
| Calmar | 6.29 |
| Profit factor | 1.11 | | Profit factor | 1.11 |
| Expectancy | -0.15 |
| Avg. stake amount | 0.001 BTC | | Avg. stake amount | 0.001 BTC |
| Total trade volume | 0.429 BTC | | Total trade volume | 0.429 BTC |
| | | | | |
@@ -455,9 +447,6 @@ It contains some useful key metrics about performance of your strategy on backte
- `Absolute profit`: Profit made in stake currency. - `Absolute profit`: Profit made in stake currency.
- `Total profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. Calculated as `(End capital Starting capital) / Starting capital`. - `Total profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. Calculated as `(End capital Starting capital) / Starting capital`.
- `CAGR %`: Compound annual growth rate. - `CAGR %`: Compound annual growth rate.
- `Sortino`: Annualized Sortino ratio.
- `Sharpe`: Annualized Sharpe ratio.
- `Calmar`: Annualized Calmar ratio.
- `Profit factor`: profit / loss. - `Profit factor`: profit / loss.
- `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount. - `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount.
- `Total trade volume`: Volume generated on the exchange to reach the above profit. - `Total trade volume`: Volume generated on the exchange to reach the above profit.

View File

@@ -75,7 +75,3 @@ This loop will be repeated again and again until the bot is stopped.
!!! Note !!! Note
Both Backtesting and Hyperopt include exchange default Fees in the calculation. Custom fees can be passed to backtesting / hyperopt by specifying the `--fee` argument. Both Backtesting and Hyperopt include exchange default Fees in the calculation. Custom fees can be passed to backtesting / hyperopt by specifying the `--fee` argument.
!!! Warning "Callback call frequency"
Backtesting will call each callback at max. once per candle (`--timeframe-detail` modifies this behavior to once per detailed candle).
Most callbacks will be called once per iteration in live (usually every ~5s) - which can cause backtesting mismatches.

View File

@@ -11,7 +11,7 @@ Per default, the bot loads the configuration from the `config.json` file, locate
You can specify a different configuration file used by the bot with the `-c/--config` command-line option. You can specify a different configuration file used by the bot with the `-c/--config` command-line option.
If you used the [Quick start](docker_quickstart.md#docker-quick-start) method for installing If you used the [Quick start](installation.md/#quick-start) method for installing
the bot, the installation script should have already created the default configuration file (`config.json`) for you. the bot, the installation script should have already created the default configuration file (`config.json`) for you.
If the default configuration file is not created we recommend to use `freqtrade new-config --config config.json` to generate a basic configuration file. If the default configuration file is not created we recommend to use `freqtrade new-config --config config.json` to generate a basic configuration file.
@@ -134,7 +134,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| Parameter | Description | | Parameter | Description |
|------------|-------------| |------------|-------------|
| `max_open_trades` | **Required.** Number of open trades your bot is allowed to have. Only one open trade per pair is possible, so the length of your pairlist is another limitation that can apply. If -1 then it is ignored (i.e. potentially unlimited open trades, limited by the pairlist). [More information below](#configuring-amount-per-trade). [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Positive integer or -1. | `max_open_trades` | **Required.** Number of open trades your bot is allowed to have. Only one open trade per pair is possible, so the length of your pairlist is another limitation that can apply. If -1 then it is ignored (i.e. potentially unlimited open trades, limited by the pairlist). [More information below](#configuring-amount-per-trade).<br> **Datatype:** Positive integer or -1.
| `stake_currency` | **Required.** Crypto-currency used for trading. <br> **Datatype:** String | `stake_currency` | **Required.** Crypto-currency used for trading. <br> **Datatype:** String
| `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade). <br> **Datatype:** Positive float or `"unlimited"`. | `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade). <br> **Datatype:** Positive float or `"unlimited"`.
| `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.99` 99%).*<br> **Datatype:** Positive float between `0.1` and `1.0`. | `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.99` 99%).*<br> **Datatype:** Positive float between `0.1` and `1.0`.
@@ -263,7 +263,6 @@ Values set in the configuration file always overwrite values set in the strategy
* `minimal_roi` * `minimal_roi`
* `timeframe` * `timeframe`
* `stoploss` * `stoploss`
* `max_open_trades`
* `trailing_stop` * `trailing_stop`
* `trailing_stop_positive` * `trailing_stop_positive`
* `trailing_stop_positive_offset` * `trailing_stop_positive_offset`

View File

@@ -75,25 +75,6 @@ Binance has been split into 2, and users must use the correct ccxt exchange ID f
* [binance.com](https://www.binance.com/) - International users. Use exchange id: `binance`. * [binance.com](https://www.binance.com/) - International users. Use exchange id: `binance`.
* [binance.us](https://www.binance.us/) - US based users. Use exchange id: `binanceus`. * [binance.us](https://www.binance.us/) - US based users. Use exchange id: `binanceus`.
### Binance RSA keys
Freqtrade supports binance RSA API keys.
We recommend to use them as environment variable.
``` bash
export FREQTRADE__EXCHANGE__SECRET="$(cat ./rsa_binance.private)"
```
They can however also be configured via configuration file. Since json doesn't support multi-line strings, you'll have to replace all newlines with `\n` to have a valid json file.
``` json
// ...
"key": "<someapikey>",
"secret": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBABACAFQA<...>s8KX8=\n-----END PRIVATE KEY-----"
// ...
```
### Binance Futures ### Binance Futures
Binance has specific (unfortunately complex) [Futures Trading Quantitative Rules](https://www.binance.com/en/support/faq/4f462ebe6ff445d4a170be7d9e897272) which need to be followed, and which prohibit a too low stake-amount (among others) for too many orders. Binance has specific (unfortunately complex) [Futures Trading Quantitative Rules](https://www.binance.com/en/support/faq/4f462ebe6ff445d4a170be7d9e897272) which need to be followed, and which prohibit a too low stake-amount (among others) for too many orders.

View File

@@ -43,113 +43,116 @@ The FreqAI strategy requires including the following lines of code in the standa
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# the model will return all labels created by user in `set_freqai_labels()` # the model will return all labels created by user in `populate_any_indicators`
# (& appended targets), an indication of whether or not the prediction should be accepted, # (& appended targets), an indication of whether or not the prediction should be accepted,
# the target mean/std values for each of the labels created by user in # the target mean/std values for each of the labels created by user in
# `feature_engineering_*` for each training period. # `populate_any_indicators()` for each training period.
dataframe = self.freqai.start(dataframe, metadata, self) dataframe = self.freqai.start(dataframe, metadata, self)
return dataframe return dataframe
def feature_engineering_expand_all(self, dataframe, period, **kwargs): def populate_any_indicators(
self, pair, df, tf, informative=None, set_generalized_indicators=False
):
""" """
*Only functional with FreqAI enabled strategies* Function designed to automatically generate, name and merge features
This function will automatically expand the defined features on the config defined from user indicated timeframes in the configuration file. User controls the indicators
`indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and passed to the training/prediction by prepending indicators with `'%-' + pair `
`include_corr_pairs`. In other words, a single feature defined in this function (see convention below). I.e. user should not prepend any supporting metrics
will automatically expand to a total of (e.g. bb_lowerband below) with % unless they explicitly want to pass that metric to the
`indicator_periods_candles` * `include_timeframes` * `include_shifted_candles` * model.
`include_corr_pairs` numbers of features added to the model. :param pair: pair to be used as informative
:param df: strategy dataframe which will receive merges from informatives
All features must be prepended with `%` to be recognized by FreqAI internals. :param tf: timeframe of the dataframe which will modify the feature names
:param informative: the dataframe associated with the informative pair
:param df: strategy dataframe which will receive the features
:param period: period of the indicator - usage example:
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
""" """
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period) if informative is None:
dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period) informative = self.dp.get_pair_dataframe(pair, tf)
dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period)
dataframe["%-sma-period"] = ta.SMA(dataframe, timeperiod=period)
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
return dataframe # first loop is automatically duplicating indicators for time periods
for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]:
t = int(t)
informative[f"%-{pair}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
informative[f"%-{pair}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
informative[f"%-{pair}adx-period_{t}"] = ta.ADX(informative, window=t)
def feature_engineering_expand_basic(self, dataframe, **kwargs): indicators = [col for col in informative if col.startswith("%")]
""" # This loop duplicates and shifts all indicators to add a sense of recency to data
*Only functional with FreqAI enabled strategies* for n in range(self.freqai_info["feature_parameters"]["include_shifted_candles"] + 1):
This function will automatically expand the defined features on the config defined if n == 0:
`include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`. continue
In other words, a single feature defined in this function informative_shift = informative[indicators].shift(n)
will automatically expand to a total of informative_shift = informative_shift.add_suffix("_shift-" + str(n))
`include_timeframes` * `include_shifted_candles` * `include_corr_pairs` informative = pd.concat((informative, informative_shift), axis=1)
numbers of features added to the model.
Features defined here will *not* be automatically duplicated on user defined df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True)
`indicator_periods_candles` skip_columns = [
(s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"]
]
df = df.drop(columns=skip_columns)
All features must be prepended with `%` to be recognized by FreqAI internals. # Add generalized indicators here (because in live, it will call this
# function to populate indicators during training). Notice how we ensure not to
# add them multiple times
if set_generalized_indicators:
:param df: strategy dataframe which will receive the features # user adds targets here by prepending them with &- (see convention below)
dataframe["%-pct-change"] = dataframe["close"].pct_change() # If user wishes to use multiple targets, a multioutput prediction model
dataframe["%-ema-200"] = ta.EMA(dataframe, timeperiod=200) # needs to be used such as templates/CatboostPredictionMultiModel.py
""" df["&-s_close"] = (
dataframe["%-pct-change"] = dataframe["close"].pct_change() df["close"]
dataframe["%-raw_volume"] = dataframe["volume"] .shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
dataframe["%-raw_price"] = dataframe["close"] .rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
return dataframe .mean()
/ df["close"]
def feature_engineering_standard(self, dataframe, **kwargs): - 1
"""
*Only functional with FreqAI enabled strategies*
This optional function will be called once with the dataframe of the base timeframe.
This is the final function to be called, which means that the dataframe entering this
function will contain all the features and columns created by all other
freqai_feature_engineering_* functions.
This function is a good place to do custom exotic feature extractions (e.g. tsfresh).
This function is a good place for any feature that should not be auto-expanded upon
(e.g. day of the week).
All features must be prepended with `%` to be recognized by FreqAI internals.
:param df: strategy dataframe which will receive the features
usage example: dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7
"""
dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7
dataframe["%-hour_of_day"] = (dataframe["date"].dt.hour + 1) / 25
return dataframe
def set_freqai_targets(self, dataframe, **kwargs):
"""
*Only functional with FreqAI enabled strategies*
Required function to set the targets for the model.
All targets must be prepended with `&` to be recognized by the FreqAI internals.
:param df: strategy dataframe which will receive the targets
usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"]
"""
dataframe["&-s_close"] = (
dataframe["close"]
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
.mean()
/ dataframe["close"]
- 1
) )
return df
``` ```
Notice how the `feature_engineering_*()` is where [features](freqai-feature-engineering.md#feature-engineering) are added. Meanwhile `set_freqai_targets()` adds the labels/targets. A full example strategy is available in `templates/FreqaiExampleStrategy.py`. Notice how the `populate_any_indicators()` is where [features](freqai-feature-engineering.md#feature-engineering) and labels/targets are added. A full example strategy is available in `templates/FreqaiExampleStrategy.py`.
Notice also the location of the labels under `if set_generalized_indicators:` at the bottom of the example. This is where single features and labels/targets should be added to the feature set to avoid duplication of them from various configuration parameters that multiply the feature set, such as `include_timeframes`.
!!! Note !!! Note
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 `feature_engineering_*()`. 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, you should use `feature_engineering_standard()` 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`):
```python
def populate_any_indicators(self, pair, df, tf, informative=None, set_generalized_indicators=False):
...
# Add generalized indicators here (because in live, it will call only this function to populate
# indicators for retraining). Notice how we ensure not to add them multiple times by associating
# these generalized indicators to the basepair/timeframe
if set_generalized_indicators:
df['%-day_of_week'] = (df["date"].dt.dayofweek + 1) / 7
df['%-hour_of_day'] = (df['date'].dt.hour + 1) / 25
# user adds targets here by prepending them with &- (see convention below)
# If user wishes to use multiple targets, a multioutput prediction model
# needs to be used such as templates/CatboostPredictionMultiModel.py
df["&-s_close"] = (
df["close"]
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
.mean()
/ df["close"]
- 1
)
```
Please see the example script located in `freqtrade/templates/FreqaiExampleStrategy.py` for a full example of `populate_any_indicators()`.
## Important dataframe key patterns ## Important dataframe key patterns
@@ -157,11 +160,11 @@ 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 `set_freqai_targets()` 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['&*']` | 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 -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['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 `feature_engineering_*()` 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. | `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`
@@ -236,3 +239,20 @@ If you want to predict multiple targets you must specify all labels in the same
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')
df['&s-up_or_down'] = np.where( df["close"].shift(-100) == df["close"], 'same', df['&s-up_or_down']) df['&s-up_or_down'] = np.where( df["close"].shift(-100) == df["close"], 'same', df['&s-up_or_down'])
``` ```
### Convolutional Neural Network model
The `CNNPredictionModel` is a non-linear regression based on `Tensorflow` which follows very similar configuration to the other regressors. Feature engineering and label creation remains the same as highlighted [here](#building-a-freqai-strategy) and [here](#setting-model-targets). Control of the model is focused in the `model_training_parameters` configuration dictionary, which accepts any hyperparameter available to the CNN `fit()` function of Tensorflow [more here](https://www.tensorflow.org/api_docs/python/tf/keras/Model#fit). For example, this is where the `epochs` and `batch_size` are controlled:
```json
"model_training_parameters" : {
"batch_size": 64,
"epochs": 10,
"verbose": "auto",
"shuffle": false,
"workers": 1,
"use_multiprocessing": false
}
```
Running the `CNNPredictionModel` is the same as other regressors: `--freqaimodel CNNPredictionModel`.

View File

@@ -2,130 +2,96 @@
## Defining the features ## Defining the features
Low level feature engineering is performed in the user strategy within a set of functions called `feature_engineering_*`. These function set 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. FreqAI is equipped with a set of functions to simplify rapid large-scale feature engineering: 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 `%-{pair}`, while labels/targets are prepended with `&`.
| Function | Description | !!! Note
|---------------|-------------| Adding the full pair string, e.g. XYZ/USD, in the feature name enables improved performance for dataframe caching on the backend. If you decide *not* to add the full pair string in the feature string, FreqAI will operate in a reduced performance mode.
| `feature_engineering__expand_all()` | This optional function will automatically expand the defined features on the config defined `indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`.
| `feature_engineering__expand_basic()` | This optional function will automatically expand the defined features on the config defined `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`. Note: this function does *not* expand across `include_periods_candles`.
| `feature_engineering_standard()` | This optional function will be called once with the dataframe of the base timeframe. This is the final function to be called, which means that the dataframe entering this function will contain all the features and columns from the base asset created by the other `feature_engineering_expand` functions. This function is a good place to do custom exotic feature extractions (e.g. tsfresh). This function is also a good place for any feature that should not be auto-expanded upon (e.g. day of the week).
| `set_freqai_targets()` | Required function to set the targets for the model. All targets must be prepended with `&` to be recognized by the FreqAI internals.
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 `feature_engineering_*` functions 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:
```python ```python
def feature_engineering_expand_all(self, dataframe, period, **kwargs): def populate_any_indicators(
self, pair, df, tf, informative=None, set_generalized_indicators=False
):
""" """
*Only functional with FreqAI enabled strategies* Function designed to automatically generate, name, and merge features
This function will automatically expand the defined features on the config defined from user-indicated timeframes in the configuration file. The user controls the indicators
`indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and passed to the training/prediction by prepending indicators with `'%-' + pair `
`include_corr_pairs`. In other words, a single feature defined in this function (see convention below). I.e., the user should not prepend any supporting metrics
will automatically expand to a total of (e.g., bb_lowerband below) with % unless they explicitly want to pass that metric to the
`indicator_periods_candles` * `include_timeframes` * `include_shifted_candles` * model.
`include_corr_pairs` numbers of features added to the model. :param pair: pair to be used as informative
:param df: strategy dataframe which will receive merges from informatives
All features must be prepended with `%` to be recognized by FreqAI internals. :param tf: timeframe of the dataframe which will modify the feature names
:param informative: the dataframe associated with the informative pair
:param df: strategy dataframe which will receive the features
:param period: period of the indicator - usage example:
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
""" """
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period) if informative is None:
dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period) informative = self.dp.get_pair_dataframe(pair, tf)
dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period)
dataframe["%-sma-period"] = ta.SMA(dataframe, timeperiod=period)
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
bollinger = qtpylib.bollinger_bands( # first loop is automatically duplicating indicators for time periods
qtpylib.typical_price(dataframe), window=period, stds=2.2 for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]:
) t = int(t)
dataframe["bb_lowerband-period"] = bollinger["lower"] informative[f"%-{pair}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
dataframe["bb_middleband-period"] = bollinger["mid"] informative[f"%-{pair}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
dataframe["bb_upperband-period"] = bollinger["upper"] informative[f"%-{pair}adx-period_{t}"] = ta.ADX(informative, window=t)
dataframe["%-bb_width-period"] = ( bollinger = qtpylib.bollinger_bands(
dataframe["bb_upperband-period"] qtpylib.typical_price(informative), window=t, stds=2.2
- dataframe["bb_lowerband-period"]
) / dataframe["bb_middleband-period"]
dataframe["%-close-bb_lower-period"] = (
dataframe["close"] / dataframe["bb_lowerband-period"]
)
dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period)
dataframe["%-relative_volume-period"] = (
dataframe["volume"] / dataframe["volume"].rolling(period).mean()
)
return dataframe
def feature_engineering_expand_basic(self, dataframe, **kwargs):
"""
*Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined
`include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`.
In other words, a single feature defined in this function
will automatically expand to a total of
`include_timeframes` * `include_shifted_candles` * `include_corr_pairs`
numbers of features added to the model.
Features defined here will *not* be automatically duplicated on user defined
`indicator_periods_candles`
All features must be prepended with `%` to be recognized by FreqAI internals.
:param df: strategy dataframe which will receive the features
dataframe["%-pct-change"] = dataframe["close"].pct_change()
dataframe["%-ema-200"] = ta.EMA(dataframe, timeperiod=200)
"""
dataframe["%-pct-change"] = dataframe["close"].pct_change()
dataframe["%-raw_volume"] = dataframe["volume"]
dataframe["%-raw_price"] = dataframe["close"]
return dataframe
def feature_engineering_standard(self, dataframe, **kwargs):
"""
*Only functional with FreqAI enabled strategies*
This optional function will be called once with the dataframe of the base timeframe.
This is the final function to be called, which means that the dataframe entering this
function will contain all the features and columns created by all other
freqai_feature_engineering_* functions.
This function is a good place to do custom exotic feature extractions (e.g. tsfresh).
This function is a good place for any feature that should not be auto-expanded upon
(e.g. day of the week).
All features must be prepended with `%` to be recognized by FreqAI internals.
:param df: strategy dataframe which will receive the features
usage example: dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7
"""
dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7
dataframe["%-hour_of_day"] = (dataframe["date"].dt.hour + 1) / 25
return dataframe
def set_freqai_targets(self, dataframe, **kwargs):
"""
*Only functional with FreqAI enabled strategies*
Required function to set the targets for the model.
All targets must be prepended with `&` to be recognized by the FreqAI internals.
:param df: strategy dataframe which will receive the targets
usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"]
"""
dataframe["&-s_close"] = (
dataframe["close"]
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
.mean()
/ dataframe["close"]
- 1
) )
informative[f"{pair}bb_lowerband-period_{t}"] = bollinger["lower"]
return dataframe informative[f"{pair}bb_middleband-period_{t}"] = bollinger["mid"]
informative[f"{pair}bb_upperband-period_{t}"] = bollinger["upper"]
informative[f"%-{pair}bb_width-period_{t}"] = (
informative[f"{pair}bb_upperband-period_{t}"]
- informative[f"{pair}bb_lowerband-period_{t}"]
) / informative[f"{pair}bb_middleband-period_{t}"]
informative[f"%-{pair}close-bb_lower-period_{t}"] = (
informative["close"] / informative[f"{pair}bb_lowerband-period_{t}"]
)
informative[f"%-{pair}relative_volume-period_{t}"] = (
informative["volume"] / informative["volume"].rolling(t).mean()
)
indicators = [col for col in informative if col.startswith("%")]
# This loop duplicates and shifts all indicators to add a sense of recency to data
for n in range(self.freqai_info["feature_parameters"]["include_shifted_candles"] + 1):
if n == 0:
continue
informative_shift = informative[indicators].shift(n)
informative_shift = informative_shift.add_suffix("_shift-" + str(n))
informative = pd.concat((informative, informative_shift), axis=1)
df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True)
skip_columns = [
(s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"]
]
df = df.drop(columns=skip_columns)
# Add generalized indicators here (because in live, it will call this
# function to populate indicators during training). Notice how we ensure not to
# add them multiple times
if set_generalized_indicators:
df["%-day_of_week"] = (df["date"].dt.dayofweek + 1) / 7
df["%-hour_of_day"] = (df["date"].dt.hour + 1) / 25
# user adds targets here by prepending them with &- (see convention below)
# If user wishes to use multiple targets, a multioutput prediction model
# needs to be used such as templates/CatboostPredictionMultiModel.py
df["&-s_close"] = (
df["close"]
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
.mean()
/ df["close"]
- 1
)
return df
``` ```
In the presented example, the user does not wish to pass the `bb_lowerband` as a feature to the model, In the presented example, the user does not wish to pass the `bb_lowerband` as a feature to the model,
@@ -152,13 +118,13 @@ After having defined the `base features`, the next step is to expand upon them u
} }
``` ```
The `include_timeframes` in the config above are the timeframes (`tf`) of each call to `feature_engineering_expand_*()` in the strategy. In the presented case, the user is asking for the `5m`, `15m`, and `4h` timeframes of the `rsi`, `mfi`, `roc`, and `bb_width` to be included in the feature set. The `include_timeframes` in the config above are the timeframes (`tf`) of each call to `populate_any_indicators()` in the strategy. In the presented case, the user is asking for the `5m`, `15m`, and `4h` timeframes of the `rsi`, `mfi`, `roc`, and `bb_width` to be included in the feature set.
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 `feature_engineering_expand_*()` 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 `feature_engineering_expand_*()` * 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$.
### Returning additional info from training ### Returning additional info from training

View File

@@ -15,7 +15,7 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
| `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 all unused models during live runs (not relevant to backtesting). If set to false (not default), dry/live runs will accumulate all unused models to disk. If <br> **Datatype:** Boolean. <br> Default: `True`. | `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.
| `follow_mode` | Use a `follower` that will look for models associated with a specific `identifier` and load those for inferencing. A `follower` will **not** train new models. <br> **Datatype:** Boolean. <br> Default: `False`. | `follow_mode` | Use a `follower` that will look for models associated with a specific `identifier` and load those for inferencing. A `follower` will **not** train new models. <br> **Datatype:** Boolean. <br> Default: `False`.
@@ -29,12 +29,12 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|------------|-------------| |------------|-------------|
| | **Feature parameters within the `freqai.feature_parameters` sub dictionary** | | **Feature parameters within the `freqai.feature_parameters` sub 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. | `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 `feature_engineering_expand_*()` 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 `feature_engineering_expand_*()` 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 `feature_engineering_expand_all()` (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 `feature_engineering_*()` 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. <br> Default: `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. Plot is stored in `user_data/models/<identifier>/sub-train-<COIN>_<timestamp>.html`. <br> **Datatype:** Integer. <br> Default: `0`. | `plot_feature_importances` | Create a feature importance plot for each model for the top/bottom `plot_feature_importances` number of features. Plot is stored in `user_data/models/<identifier>/sub-train-<COIN>_<timestamp>.html`. <br> **Datatype:** Integer. <br> Default: `0`.
@@ -89,6 +89,6 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
| Parameter | Description | | Parameter | Description |
|------------|-------------| |------------|-------------|
| | **Extraneous parameters** | | **Extraneous parameters**
| `freqai.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`. | `freqai.keras` | If the selected model makes use of Keras (typical for Tensorflow-based prediction models), this flag should be activated so that the model save/loading follows Keras standards. If the the provided `CNNPredictionModel` is used, then this is handled automatically. <br> **Datatype:** Boolean. <br> Default: `False`.
| `freqai.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`. | `freqai.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`.
| `freqai.reduce_df_footprint` | Recast all numeric columns to float32/int32, with the objective of reducing ram/disk usage and decreasing train/inference timing. This parameter is set in the main level of the Freqtrade configuration file (not inside FreqAI). <br> **Datatype:** Boolean. <br> Default: `False`. | `freqai.reduce_df_footprint` | Recast all numeric columns to float32/int32, with the objective of reducing ram/disk usage and decreasing train/inference timing. This parameter is set in the main level of the Freqtrade configuration file (not inside FreqAI). <br> **Datatype:** Boolean. <br> Default: `False`.

View File

@@ -34,36 +34,65 @@ Setting up and running a Reinforcement Learning model is the same as running a R
freqtrade trade --freqaimodel ReinforcementLearner --strategy MyRLStrategy --config config.json freqtrade trade --freqaimodel ReinforcementLearner --strategy MyRLStrategy --config config.json
``` ```
where `ReinforcementLearner` will use the templated `ReinforcementLearner` from `freqai/prediction_models/ReinforcementLearner` (or a custom user defined one located in `user_data/freqaimodels`). The strategy, on the other hand, follows the same base [feature engineering](freqai-feature-engineering.md) with `feature_engineering_*` as a typical Regressor. The difference lies in the creation of the targets, Reinforcement Learning doesn't require them. However, FreqAI requires a default (neutral) value to be set in the action column: where `ReinforcementLearner` will use the templated `ReinforcementLearner` from `freqai/prediction_models/ReinforcementLearner` (or a custom user defined one located in `user_data/freqaimodels`). The strategy, on the other hand, follows the same base [feature engineering](freqai-feature-engineering.md) with `populate_any_indicators` as a typical Regressor:
```python ```python
def set_freqai_targets(self, dataframe, **kwargs): def populate_any_indicators(
""" self, pair, df, tf, informative=None, set_generalized_indicators=False
*Only functional with FreqAI enabled strategies* ):
Required function to set the targets for the model.
All targets must be prepended with `&` to be recognized by the FreqAI internals.
More details about feature engineering available: if informative is None:
informative = self.dp.get_pair_dataframe(pair, tf)
https://www.freqtrade.io/en/latest/freqai-feature-engineering # first loop is automatically duplicating indicators for time periods
for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]:
:param df: strategy dataframe which will receive the targets t = int(t)
usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"] informative[f"%-{pair}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
""" informative[f"%-{pair}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
# For RL, there are no direct targets to set. This is filler (neutral) informative[f"%-{pair}adx-period_{t}"] = ta.ADX(informative, window=t)
# until the agent sends an action.
dataframe["&-action"] = 0 # The following raw price values are necessary for RL models
informative[f"%-{pair}raw_close"] = informative["close"]
informative[f"%-{pair}raw_open"] = informative["open"]
informative[f"%-{pair}raw_high"] = informative["high"]
informative[f"%-{pair}raw_low"] = informative["low"]
indicators = [col for col in informative if col.startswith("%")]
# This loop duplicates and shifts all indicators to add a sense of recency to data
for n in range(self.freqai_info["feature_parameters"]["include_shifted_candles"] + 1):
if n == 0:
continue
informative_shift = informative[indicators].shift(n)
informative_shift = informative_shift.add_suffix("_shift-" + str(n))
informative = pd.concat((informative, informative_shift), axis=1)
df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True)
skip_columns = [
(s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"]
]
df = df.drop(columns=skip_columns)
# Add generalized indicators here (because in live, it will call this
# function to populate indicators during training). Notice how we ensure not to
# add them multiple times
if set_generalized_indicators:
# For RL, there are no direct targets to set. This is filler (neutral)
# until the agent sends an action.
df["&-action"] = 0
return df
``` ```
Most of the function remains the same as for typical Regressors, however, the function above shows how the strategy must pass the raw price data to the agent so that it has access to raw OHLCV in the training environment: Most of the function remains the same as for typical Regressors, however, the function above shows how the strategy must pass the raw price data to the agent so that it has access to raw OHLCV in the training environment:
```python ```python
def feature_engineering_standard(self, dataframe, **kwargs):
# The following features are necessary for RL models # The following features are necessary for RL models
dataframe[f"%-raw_close"] = dataframe["close"] informative[f"%-{pair}raw_close"] = informative["close"]
dataframe[f"%-raw_open"] = dataframe["open"] informative[f"%-{pair}raw_open"] = informative["open"]
dataframe[f"%-raw_high"] = dataframe["high"] informative[f"%-{pair}raw_high"] = informative["high"]
dataframe[f"%-raw_low"] = dataframe["low"] informative[f"%-{pair}raw_low"] = informative["low"]
``` ```
Finally, there is no explicit "label" to make - instead it is necessary to assign the `&-action` column which will contain the agent's actions when accessed in `populate_entry/exit_trends()`. In the present example, the neutral action to 0. This value should align with the environment used. FreqAI provides two environments, both use 0 as the neutral action. Finally, there is no explicit "label" to make - instead it is necessary to assign the `&-action` column which will contain the agent's actions when accessed in `populate_entry/exit_trends()`. In the present example, the neutral action to 0. This value should align with the environment used. FreqAI provides two environments, both use 0 as the neutral action.
@@ -243,14 +272,15 @@ FreqAI also provides a built in episodic summary logger called `self.tensorboard
!!! Note !!! Note
The `self.tensorboard_log()` function is designed for tracking incremented objects only i.e. events, actions inside the training environment. If the event of interest is a float, the float can be passed as the second argument e.g. `self.tensorboard_log("float_metric1", 0.23)` would add 0.23 to `float_metric`. In this case you can also disable incrementing using `inc=False` parameter. The `self.tensorboard_log()` function is designed for tracking incremented objects only i.e. events, actions inside the training environment. If the event of interest is a float, the float can be passed as the second argument e.g. `self.tensorboard_log("float_metric1", 0.23)` would add 0.23 to `float_metric`. In this case you can also disable incrementing using `inc=False` parameter.
### Choosing a base environment ### Choosing a base environment
FreqAI provides three base environments, `Base3ActionRLEnvironment`, `Base4ActionEnvironment` and `Base5ActionEnvironment`. As the names imply, the environments are customized for agents that can select from 3, 4 or 5 actions. The `Base3ActionEnvironment` is the simplest, the agent can select from hold, long, or short. This environment can also be used for long-only bots (it automatically follows the `can_short` flag from the strategy), where long is the enter condition and short is the exit condition. Meanwhile, in the `Base4ActionEnvironment`, the agent can enter long, enter short, hold neutral, or exit position. Finally, in the `Base5ActionEnvironment`, the agent has the same actions as Base4, but instead of a single exit action, it separates exit long and exit short. The main changes stemming from the environment selection include: FreqAI provides two base environments, `Base4ActionEnvironment` and `Base5ActionEnvironment`. As the names imply, the environments are customized for agents that can select from 4 or 5 actions. In the `Base4ActionEnvironment`, the agent can enter long, enter short, hold neutral, or exit position. Meanwhile, in the `Base5ActionEnvironment`, the agent has the same actions as Base4, but instead of a single exit action, it separates exit long and exit short. The main changes stemming from the environment selection include:
* the actions available in the `calculate_reward` * the actions available in the `calculate_reward`
* the actions consumed by the user strategy * the actions consumed by the user strategy
All of the FreqAI provided environments inherit from an action/position agnostic environment object called the `BaseEnvironment`, which contains all shared logic. The architecture is designed to be easily customized. The simplest customization is the `calculate_reward()` (see details [here](#creating-a-custom-reward-function)). However, the customizations can be further extended into any of the functions inside the environment. You can do this by simply overriding those functions inside your `MyRLEnv` in the prediction model file. Or for more advanced customizations, it is encouraged to create an entirely new environment inherited from `BaseEnvironment`. Both of the FreqAI provided environments inherit from an action/position agnostic environment object called the `BaseEnvironment`, which contains all shared logic. The architecture is designed to be easily customized. The simplest customization is the `calculate_reward()` (see details [here](#creating-a-custom-reward-function)). However, the customizations can be further extended into any of the functions inside the environment. You can do this by simply overriding those functions inside your `MyRLEnv` in the prediction model file. Or for more advanced customizations, it is encouraged to create an entirely new environment inherited from `BaseEnvironment`.
!!! Note !!! Note
Only the `Base3ActionRLEnv` can do long-only training/trading (set the user strategy attribute `can_short = False`). FreqAI does not provide by default, a long-only training environment. However, creating one should be as simple as copy-pasting one of the built in environments and removing the `short` actions (and all associated references to those).

View File

@@ -67,10 +67,6 @@ Backtesting mode requires [downloading the necessary data](#downloading-data-to-
*want* to retrain a new model with the same config file, you should simply change the `identifier`. *want* to retrain a new model with the same config file, you should simply change the `identifier`.
This way, you can return to using any model you wish by simply specifying the `identifier`. This way, you can return to using any model you wish by simply specifying the `identifier`.
!!! Note
Backtesting calls `set_freqai_targets()` one time for each backtest window (where the number of windows is the full backtest timerange divided by the `backtest_period_days` parameter). Doing this means that the targets simulate dry/live behavior without look ahead bias. However, the definition of the features in `feature_engineering_*()` is performed once on the entire backtest timerange. This means that you should be sure that features do look-ahead into the future.
More details about look-ahead bias can be found in [Common Mistakes](strategy-customization.md#common-mistakes-when-developing-strategies).
--- ---
### Saving prediction data ### Saving prediction data
@@ -139,7 +135,7 @@ freqtrade hyperopt --hyperopt-loss SharpeHyperOptLoss --strategy FreqaiExampleSt
`hyperopt` requires you to have the data pre-downloaded in the same fashion as if you were doing [backtesting](#backtesting). In addition, you must consider some restrictions when trying to hyperopt FreqAI strategies: `hyperopt` requires you to have the data pre-downloaded in the same fashion as if you were doing [backtesting](#backtesting). In addition, you must consider some restrictions when trying to hyperopt FreqAI strategies:
- The `--analyze-per-epoch` hyperopt parameter is not compatible with FreqAI. - The `--analyze-per-epoch` hyperopt parameter is not compatible with FreqAI.
- It's not possible to hyperopt indicators in the `feature_engineering_*()` and `set_freqai_targets()` functions. This means that you cannot optimize model parameters using hyperopt. Apart from this exception, it is possible to optimize all other [spaces](hyperopt.md#running-hyperopt-with-smaller-search-space). - It's not possible to hyperopt indicators in the `populate_any_indicators()` function. This means that you cannot optimize model parameters using hyperopt. Apart from this exception, it is possible to optimize all other [spaces](hyperopt.md#running-hyperopt-with-smaller-search-space).
- The backtesting instructions also apply to hyperopt. - The backtesting instructions also apply to hyperopt.
The best method for combining hyperopt and FreqAI is to focus on hyperopting entry/exit thresholds/criteria. You need to focus on hyperopting parameters that are not used in your features. For example, you should not try to hyperopt rolling window lengths in the feature creation, or any part of the FreqAI config which changes predictions. In order to efficiently hyperopt the FreqAI strategy, FreqAI stores predictions as dataframes and reuses them. Hence the requirement to hyperopt entry/exit thresholds/criteria only. The best method for combining hyperopt and FreqAI is to focus on hyperopting entry/exit thresholds/criteria. You need to focus on hyperopting parameters that are not used in your features. For example, you should not try to hyperopt rolling window lengths in the feature creation, or any part of the FreqAI config which changes predictions. In order to efficiently hyperopt the FreqAI strategy, FreqAI stores predictions as dataframes and reuses them. Hence the requirement to hyperopt entry/exit thresholds/criteria only.

View File

@@ -72,25 +72,11 @@ pip install -r requirements-freqai.txt
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. 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.
### FreqAI position in open-source machine learning landscape ### FreqAI position in open-source machine learning landscape
Forecasting chaotic time-series based systems, such as equity/cryptocurrency markets, requires a broad set of tools geared toward testing a wide range of hypotheses. Fortunately, a recent maturation of robust machine learning libraries (e.g. `scikit-learn`) has opened up a wide range of research possibilities. Scientists from a diverse range of fields can now easily prototype their studies on an abundance of established machine learning algorithms. Similarly, these user-friendly libraries enable "citzen scientists" to use their basic Python skills for data-exploration. However, leveraging these machine learning libraries on historical and live chaotic data sources can be logistically difficult and expensive. Additionally, robust data-collection, storage, and handling presents a disparate challenge. [`FreqAI`](#freqai) aims to provide a generalized and extensible open-sourced framework geared toward live deployments of adaptive modeling for market forecasting. The `FreqAI` framework is effectively a sandbox for the rich world of open-source machine learning libraries. Inside the `FreqAI` sandbox, users find they can combine a wide variety of third-party libraries to test creative hypotheses on a free live 24/7 chaotic data source - cryptocurrency exchange data. Forecasting chaotic time-series based systems, such as equity/cryptocurrency markets, requires a broad set of tools geared toward testing a wide range of hypotheses. Fortunately, a recent maturation of robust machine learning libraries (e.g. `scikit-learn`) has opened up a wide range of research possibilities. Scientists from a diverse range of fields can now easily prototype their studies on an abundance of established machine learning algorithms. Similarly, these user-friendly libraries enable "citzen scientists" to use their basic Python skills for data-exploration. However, leveraging these machine learning libraries on historical and live chaotic data sources can be logistically difficult and expensive. Additionally, robust data-collection, storage, and handling presents a disparate challenge. [`FreqAI`](#freqai) aims to provide a generalized and extensible open-sourced framework geared toward live deployments of adaptive modeling for market forecasting. The `FreqAI` framework is effectively a sandbox for the rich world of open-source machine learning libraries. Inside the `FreqAI` sandbox, users find they can combine a wide variety of third-party libraries to test creative hypotheses on a free live 24/7 chaotic data source - cryptocurrency exchange data.
### Citing FreqAI
FreqAI is [published in the Journal of Open Source Software](https://joss.theoj.org/papers/10.21105/joss.04864). If you find FreqAI useful in your research, please use the following citation:
```bibtex
@article{Caulk2022,
doi = {10.21105/joss.04864},
url = {https://doi.org/10.21105/joss.04864},
year = {2022}, publisher = {The Open Journal},
volume = {7}, number = {80}, pages = {4864},
author = {Robert A. Caulk and Elin Törnquist and Matthias Voppichler and Andrew R. Lawless and Ryan McMullan and Wagner Costa Santos and Timothy C. Pogue and Johan van der Vlugt and Stefan P. Gehring and Pascal Schmidt},
title = {FreqAI: generalizing adaptive modeling for chaotic time-series market forecasts},
journal = {Journal of Open Source Software} }
```
## 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).
@@ -113,8 +99,6 @@ Code review and software architecture brainstorming:
Software development: Software development:
Wagner Costa @wagnercosta Wagner Costa @wagnercosta
Emre Suzen @aemr3
Timothy Pogue @wizrds
Beta testing and bug reporting: Beta testing and bug reporting:
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 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

View File

@@ -50,7 +50,7 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
[--eps] [--dmmp] [--enable-protections] [--eps] [--dmmp] [--enable-protections]
[--dry-run-wallet DRY_RUN_WALLET] [--dry-run-wallet DRY_RUN_WALLET]
[--timeframe-detail TIMEFRAME_DETAIL] [-e INT] [--timeframe-detail TIMEFRAME_DETAIL] [-e INT]
[--spaces {all,buy,sell,roi,stoploss,trailing,protection,trades,default} [{all,buy,sell,roi,stoploss,trailing,protection,trades,default} ...]] [--spaces {all,buy,sell,roi,stoploss,trailing,protection,default} [{all,buy,sell,roi,stoploss,trailing,protection,default} ...]]
[--print-all] [--no-color] [--print-json] [-j JOBS] [--print-all] [--no-color] [--print-json] [-j JOBS]
[--random-state INT] [--min-trades INT] [--random-state INT] [--min-trades INT]
[--hyperopt-loss NAME] [--disable-param-export] [--hyperopt-loss NAME] [--disable-param-export]
@@ -96,7 +96,7 @@ optional arguments:
Specify detail timeframe for backtesting (`1m`, `5m`, Specify detail timeframe for backtesting (`1m`, `5m`,
`30m`, `1h`, `1d`). `30m`, `1h`, `1d`).
-e INT, --epochs INT Specify number of epochs (default: 100). -e INT, --epochs INT Specify number of epochs (default: 100).
--spaces {all,buy,sell,roi,stoploss,trailing,protection,trades,default} [{all,buy,sell,roi,stoploss,trailing,protection,trades,default} ...] --spaces {all,buy,sell,roi,stoploss,trailing,protection,default} [{all,buy,sell,roi,stoploss,trailing,protection,default} ...]
Specify which parameters to hyperopt. Space-separated Specify which parameters to hyperopt. Space-separated
list. list.
--print-all Print all results, not only the best ones. --print-all Print all results, not only the best ones.
@@ -180,7 +180,6 @@ Rarely you may also need to create a [nested class](advanced-hyperopt.md#overrid
* `generate_roi_table` - for custom ROI optimization (if you need the ranges for the values in the ROI table that differ from default or the number of entries (steps) in the ROI table which differs from the default 4 steps) * `generate_roi_table` - for custom ROI optimization (if you need the ranges for the values in the ROI table that differ from default or the number of entries (steps) in the ROI table which differs from the default 4 steps)
* `stoploss_space` - for custom stoploss optimization (if you need the range for the stoploss parameter in the optimization hyperspace that differs from default) * `stoploss_space` - for custom stoploss optimization (if you need the range for the stoploss parameter in the optimization hyperspace that differs from default)
* `trailing_space` - for custom trailing stop optimization (if you need the ranges for the trailing stop parameters in the optimization hyperspace that differ from default) * `trailing_space` - for custom trailing stop optimization (if you need the ranges for the trailing stop parameters in the optimization hyperspace that differ from default)
* `max_open_trades_space` - for custom max_open_trades optimization (if you need the ranges for the max_open_trades parameter in the optimization hyperspace that differ from default)
!!! Tip "Quickly optimize ROI, stoploss and trailing stoploss" !!! Tip "Quickly optimize ROI, stoploss and trailing stoploss"
You can quickly optimize the spaces `roi`, `stoploss` and `trailing` without changing anything in your strategy. You can quickly optimize the spaces `roi`, `stoploss` and `trailing` without changing anything in your strategy.
@@ -366,7 +365,7 @@ class MyAwesomeStrategy(IStrategy):
timeframe = '15m' timeframe = '15m'
minimal_roi = { minimal_roi = {
"0": 0.10 "0": 0.10
} },
# Define the parameter spaces # Define the parameter spaces
buy_ema_short = IntParameter(3, 50, default=5) buy_ema_short = IntParameter(3, 50, default=5)
buy_ema_long = IntParameter(15, 200, default=50) buy_ema_long = IntParameter(15, 200, default=50)
@@ -401,7 +400,7 @@ class MyAwesomeStrategy(IStrategy):
return dataframe return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
conditions = [] conditions = []
conditions.append(qtpylib.crossed_above( conditions.append(qtpylib.crossed_above(
dataframe[f'ema_long_{self.buy_ema_long.value}'], dataframe[f'ema_short_{self.buy_ema_short.value}'] dataframe[f'ema_long_{self.buy_ema_long.value}'], dataframe[f'ema_short_{self.buy_ema_short.value}']
)) ))
@@ -644,7 +643,6 @@ Legal values are:
* `roi`: just optimize the minimal profit table for your strategy * `roi`: just optimize the minimal profit table for your strategy
* `stoploss`: search for the best stoploss value * `stoploss`: search for the best stoploss value
* `trailing`: search for the best trailing stop values * `trailing`: search for the best trailing stop values
* `trades`: search for the best max open trades values
* `protection`: search for the best protection parameters (read the [protections section](#optimizing-protections) on how to properly define these) * `protection`: search for the best protection parameters (read the [protections section](#optimizing-protections) on how to properly define these)
* `default`: `all` except `trailing` and `protection` * `default`: `all` except `trailing` and `protection`
* space-separated list of any of the above values for example `--spaces roi stoploss` * space-separated list of any of the above values for example `--spaces roi stoploss`
@@ -918,5 +916,5 @@ Once the optimized strategy has been implemented into your strategy, you should
To achieve same the results (number of trades, their durations, profit, etc.) as during Hyperopt, please use the same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting. To achieve same the results (number of trades, their durations, profit, etc.) as during Hyperopt, please use the same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting.
Should results not match, please double-check to make sure you transferred all conditions correctly. Should results not match, please double-check to make sure you transferred all conditions correctly.
Pay special care to the stoploss, max_open_trades and trailing stoploss parameters, as these are often set in configuration files, which override changes to the strategy. Pay special care to the stoploss (and trailing stoploss) parameters, as these are often set in configuration files, which override changes to the strategy.
You should also carefully review the log of your backtest to ensure that there were no parameters inadvertently set by the configuration (like `stoploss`, `max_open_trades` or `trailing_stop`). You should also carefully review the log of your backtest to ensure that there were no parameters inadvertently set by the configuration (like `stoploss` or `trailing_stop`).

View File

@@ -23,7 +23,6 @@ You may also use something like `.*DOWN/BTC` or `.*UP/BTC` to exclude leveraged
* [`StaticPairList`](#static-pair-list) (default, if not configured differently) * [`StaticPairList`](#static-pair-list) (default, if not configured differently)
* [`VolumePairList`](#volume-pair-list) * [`VolumePairList`](#volume-pair-list)
* [`ProducerPairList`](#producerpairlist) * [`ProducerPairList`](#producerpairlist)
* [`RemotePairList`](#remotepairlist)
* [`AgeFilter`](#agefilter) * [`AgeFilter`](#agefilter)
* [`OffsetFilter`](#offsetfilter) * [`OffsetFilter`](#offsetfilter)
* [`PerformanceFilter`](#performancefilter) * [`PerformanceFilter`](#performancefilter)
@@ -174,48 +173,6 @@ You can limit the length of the pairlist with the optional parameter `number_ass
`ProducerPairList` can also be used multiple times in sequence, combining the pairs from multiple producers. `ProducerPairList` can also be used multiple times in sequence, combining the pairs from multiple producers.
Obviously in complex such configurations, the Producer may not provide data for all pairs, so the strategy must be fit for this. Obviously in complex such configurations, the Producer may not provide data for all pairs, so the strategy must be fit for this.
#### RemotePairList
It allows the user to fetch a pairlist from a remote server or a locally stored json file within the freqtrade directory, enabling dynamic updates and customization of the trading pairlist.
The RemotePairList is defined in the pairlists section of the configuration settings. It uses the following configuration options:
```json
"pairlists": [
{
"method": "RemotePairList",
"pairlist_url": "https://example.com/pairlist",
"number_assets": 10,
"refresh_period": 1800,
"keep_pairlist_on_failure": true,
"read_timeout": 60,
"bearer_token": "my-bearer-token"
}
]
```
The `pairlist_url` option specifies the URL of the remote server where the pairlist is located, or the path to a local file (if file:/// is prepended). This allows the user to use either a remote server or a local file as the source for the pairlist.
The user is responsible for providing a server or local file that returns a JSON object with the following structure:
```json
{
"pairs": ["XRP/USDT", "ETH/USDT", "LTC/USDT"],
"refresh_period": 1800,
}
```
The `pairs` property should contain a list of strings with the trading pairs to be used by the bot. The `refresh_period` property is optional and specifies the number of seconds that the pairlist should be cached before being refreshed.
The optional `keep_pairlist_on_failure` specifies whether the previous received pairlist should be used if the remote server is not reachable or returns an error. The default value is true.
The optional `read_timeout` specifies the maximum amount of time (in seconds) to wait for a response from the remote source, The default value is 60.
The optional `bearer_token` will be included in the requests Authorization Header.
!!! Note
In case of a server error the last received pairlist will be kept if `keep_pairlist_on_failure` is set to true, when set to false a empty pairlist is returned.
#### AgeFilter #### AgeFilter
Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`) or more than `max_days_listed` days (defaults `None` mean infinity). Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`) or more than `max_days_listed` days (defaults `None` mean infinity).

View File

@@ -1,7 +1,6 @@
![freqtrade](assets/freqtrade_poweredby.svg) ![freqtrade](assets/freqtrade_poweredby.svg)
[![Freqtrade CI](https://github.com/freqtrade/freqtrade/workflows/Freqtrade%20CI/badge.svg)](https://github.com/freqtrade/freqtrade/actions/) [![Freqtrade CI](https://github.com/freqtrade/freqtrade/workflows/Freqtrade%20CI/badge.svg)](https://github.com/freqtrade/freqtrade/actions/)
[![DOI](https://joss.theoj.org/papers/10.21105/joss.04864/status.svg)](https://doi.org/10.21105/joss.04864)
[![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop) [![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
[![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability) [![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)

View File

@@ -67,6 +67,8 @@ You will also have to pick a "margin mode" (explanation below) - with freqtrade
Freqtrade follows the [ccxt naming conventions for futures](https://docs.ccxt.com/en/latest/manual.html?#perpetual-swap-perpetual-future). Freqtrade follows the [ccxt naming conventions for futures](https://docs.ccxt.com/en/latest/manual.html?#perpetual-swap-perpetual-future).
A futures pair will therefore have the naming of `base/quote:settle` (e.g. `ETH/USDT:USDT`). A futures pair will therefore have the naming of `base/quote:settle` (e.g. `ETH/USDT:USDT`).
Binance is currently still an exception to this naming scheme, where pairs are named `ETH/USDT` also for futures markets, but will be aligned as soon as CCXT is ready.
### Margin mode ### Margin mode
On top of `trading_mode` - you will also have to configure your `margin_mode`. On top of `trading_mode` - you will also have to configure your `margin_mode`.
@@ -90,8 +92,6 @@ One account is used to share collateral between markets (trading pairs). Margin
"margin_mode": "cross" "margin_mode": "cross"
``` ```
Please read the [exchange specific notes](exchanges.md) for exchanges that support this mode and how they differ.
## Set leverage to use ## Set leverage to use
Different strategies and risk profiles will require different levels of leverage. Different strategies and risk profiles will require different levels of leverage.

View File

@@ -11,6 +11,9 @@
{% endif %} {% endif %}
<div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" {{ hidden }}> <div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" {{ hidden }}>
<div class="md-sidebar__scrollwrap"> <div class="md-sidebar__scrollwrap">
<div id="widget-wrapper">
</div>
<div class="md-sidebar__inner"> <div class="md-sidebar__inner">
{% include "partials/nav.html" %} {% include "partials/nav.html" %}
</div> </div>
@@ -41,4 +44,25 @@
<script src="https://code.jquery.com/jquery-3.4.1.min.js" <script src="https://code.jquery.com/jquery-3.4.1.min.js"
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script> integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
<!-- Load binance SDK -->
<script async defer src="https://public.bnbstatic.com/static/js/broker-sdk/broker-sdk@1.0.0.min.js"></script>
<script>
window.onload = function () {
var sidebar = document.getElementById('widget-wrapper')
var newDiv = document.createElement("div");
newDiv.id = "widget";
try {
sidebar.prepend(newDiv);
window.binanceBrokerPortalSdk.initBrokerSDK('#widget', {
apiHost: 'https://www.binance.com',
brokerId: 'R4BD3S82',
slideTime: 4e4,
});
} catch(err) {
console.log(err)
}
}
</script>
{% endblock %} {% endblock %}

View File

@@ -1,6 +1,6 @@
markdown==3.3.7 markdown==3.3.7
mkdocs==1.4.2 mkdocs==1.4.2
mkdocs-material==9.0.5 mkdocs-material==8.5.11
mdx_truly_sane_lists==1.3 mdx_truly_sane_lists==1.3
pymdown-extensions==9.9.1 pymdown-extensions==9.9
jinja2==3.1.2 jinja2==3.1.2

View File

@@ -80,7 +80,7 @@ class AwesomeStrategy(IStrategy):
## Enter Tag ## Enter Tag
When your strategy has multiple buy signals, you can name the signal that triggered. When your strategy has multiple buy signals, you can name the signal that triggered.
Then you can access your buy signal on `custom_exit` Then you can access you buy signal on `custom_exit`
```python ```python
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:

View File

@@ -659,7 +659,6 @@ Position adjustments will always be applied in the direction of the trade, so a
!!! Warning "Backtesting" !!! Warning "Backtesting"
During backtesting this callback is called for each candle in `timeframe` or `timeframe_detail`, so run-time performance will be affected. During backtesting this callback is called for each candle in `timeframe` or `timeframe_detail`, so run-time performance will be affected.
This can also cause deviating results between live and backtesting, since backtesting can adjust the trade only once per candle, whereas live could adjust the trade multiple times per candle.
``` python ``` python
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
@@ -828,7 +827,7 @@ class AwesomeStrategy(IStrategy):
""" """
# Limit orders to use and follow SMA200 as price target for the first 10 minutes since entry trigger for BTC/USDT pair. # Limit orders to use and follow SMA200 as price target for the first 10 minutes since entry trigger for BTC/USDT pair.
if pair == 'BTC/USDT' and entry_tag == 'long_sma200' and side == 'long' and (current_time - timedelta(minutes=10)) > trade.open_date_utc: if pair == 'BTC/USDT' and entry_tag == 'long_sma200' and side == 'long' and (current_time - timedelta(minutes=10) > trade.open_date_utc:
# just cancel the order if it has been filled more than half of the amount # just cancel the order if it has been filled more than half of the amount
if order.filled > order.remaining: if order.filled > order.remaining:
return None return None

View File

@@ -989,18 +989,38 @@ from freqtrade.persistence import Trade
The following example queries for the current pair and trades from today, however other filters can easily be added. The following example queries for the current pair and trades from today, however other filters can easily be added.
``` python ``` python
trades = Trade.get_trades_proxy(pair=metadata['pair'], if self.config['runmode'].value in ('live', 'dry_run'):
open_date=datetime.now(timezone.utc) - timedelta(days=1), trades = Trade.get_trades([Trade.pair == metadata['pair'],
is_open=False, Trade.open_date > datetime.utcnow() - timedelta(days=1),
]).order_by(Trade.close_date).all() Trade.is_open.is_(False),
# Summarize profit for this pair. ]).order_by(Trade.close_date).all()
curdayprofit = sum(trade.close_profit for trade in trades) # Summarize profit for this pair.
curdayprofit = sum(trade.close_profit for trade in trades)
``` ```
For a full list of available methods, please consult the [Trade object](trade-object.md) documentation. Get amount of stake_currency currently invested in Trades:
``` python
if self.config['runmode'].value in ('live', 'dry_run'):
total_stakes = Trade.total_open_trades_stakes()
```
Retrieve performance per pair.
Returns a List of dicts per pair.
``` python
if self.config['runmode'].value in ('live', 'dry_run'):
performance = Trade.get_overall_performance()
```
Sample return value: ETH/BTC had 5 trades, with a total profit of 1.5% (ratio of 0.015).
``` json
{"pair": "ETH/BTC", "profit": 0.015, "count": 5}
```
!!! Warning !!! Warning
Trade history is not available in `populate_*` methods during backtesting or hyperopt, and will result in empty results. Trade history is not available during backtesting or hyperopt.
## Prevent trades from happening for a specific pair ## Prevent trades from happening for a specific pair

View File

@@ -477,254 +477,3 @@ after:
"ignore_buying_expired_candle_after": 120 "ignore_buying_expired_candle_after": 120
} }
``` ```
## FreqAI strategy
The `populate_any_indicators()` method has been split into `feature_engineering_expand_all()`, `feature_engineering_expand_basic()`, `feature_engineering_standard()` and`set_freqai_targets()`.
For each new function, the pair (and timeframe where necessary) will be automatically added to the column.
As such, the definition of features becomes much simpler with the new logic.
For a full explanation of each method, please go to the corresponding [freqAI documentation page](freqai-feature-engineering.md#defining-the-features)
``` python linenums="1" hl_lines="12-37 39-42 63-65 67-75"
def populate_any_indicators(
self, pair, df, tf, informative=None, set_generalized_indicators=False
):
if informative is None:
informative = self.dp.get_pair_dataframe(pair, tf)
# first loop is automatically duplicating indicators for time periods
for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]:
t = int(t)
informative[f"%-{pair}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
informative[f"%-{pair}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
informative[f"%-{pair}adx-period_{t}"] = ta.ADX(informative, timeperiod=t)
informative[f"%-{pair}sma-period_{t}"] = ta.SMA(informative, timeperiod=t)
informative[f"%-{pair}ema-period_{t}"] = ta.EMA(informative, timeperiod=t)
bollinger = qtpylib.bollinger_bands(
qtpylib.typical_price(informative), window=t, stds=2.2
)
informative[f"{pair}bb_lowerband-period_{t}"] = bollinger["lower"]
informative[f"{pair}bb_middleband-period_{t}"] = bollinger["mid"]
informative[f"{pair}bb_upperband-period_{t}"] = bollinger["upper"]
informative[f"%-{pair}bb_width-period_{t}"] = (
informative[f"{pair}bb_upperband-period_{t}"]
- informative[f"{pair}bb_lowerband-period_{t}"]
) / informative[f"{pair}bb_middleband-period_{t}"]
informative[f"%-{pair}close-bb_lower-period_{t}"] = (
informative["close"] / informative[f"{pair}bb_lowerband-period_{t}"]
)
informative[f"%-{pair}roc-period_{t}"] = ta.ROC(informative, timeperiod=t)
informative[f"%-{pair}relative_volume-period_{t}"] = (
informative["volume"] / informative["volume"].rolling(t).mean()
) # (1)
informative[f"%-{pair}pct-change"] = informative["close"].pct_change()
informative[f"%-{pair}raw_volume"] = informative["volume"]
informative[f"%-{pair}raw_price"] = informative["close"]
# (2)
indicators = [col for col in informative if col.startswith("%")]
# This loop duplicates and shifts all indicators to add a sense of recency to data
for n in range(self.freqai_info["feature_parameters"]["include_shifted_candles"] + 1):
if n == 0:
continue
informative_shift = informative[indicators].shift(n)
informative_shift = informative_shift.add_suffix("_shift-" + str(n))
informative = pd.concat((informative, informative_shift), axis=1)
df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True)
skip_columns = [
(s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"]
]
df = df.drop(columns=skip_columns)
# Add generalized indicators here (because in live, it will call this
# function to populate indicators during training). Notice how we ensure not to
# add them multiple times
if set_generalized_indicators:
df["%-day_of_week"] = (df["date"].dt.dayofweek + 1) / 7
df["%-hour_of_day"] = (df["date"].dt.hour + 1) / 25
# (3)
# user adds targets here by prepending them with &- (see convention below)
df["&-s_close"] = (
df["close"]
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
.mean()
/ df["close"]
- 1
) # (4)
return df
```
1. Features - Move to `feature_engineering_expand_all`
2. Basic features, not expanded across `include_periods_candles` - move to`feature_engineering_expand_basic()`.
3. Standard features which should not be expanded - move to `feature_engineering_standard()`.
4. Targets - Move this part to `set_freqai_targets()`.
### freqai - feature engineering expand all
Features will now expand automatically. As such, the expansion loops, as well as the `{pair}` / `{timeframe}` parts will need to be removed.
``` python linenums="1"
def feature_engineering_expand_all(self, dataframe, period, **kwargs):
"""
*Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined
`indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and
`include_corr_pairs`. In other words, a single feature defined in this function
will automatically expand to a total of
`indicator_periods_candles` * `include_timeframes` * `include_shifted_candles` *
`include_corr_pairs` numbers of features added to the model.
All features must be prepended with `%` to be recognized by FreqAI internals.
More details on how these config defined parameters accelerate feature engineering
in the documentation at:
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
:param df: strategy dataframe which will receive the features
:param period: period of the indicator - usage example:
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
"""
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period)
dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period)
dataframe["%-sma-period"] = ta.SMA(dataframe, timeperiod=period)
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
bollinger = qtpylib.bollinger_bands(
qtpylib.typical_price(dataframe), window=period, stds=2.2
)
dataframe["bb_lowerband-period"] = bollinger["lower"]
dataframe["bb_middleband-period"] = bollinger["mid"]
dataframe["bb_upperband-period"] = bollinger["upper"]
dataframe["%-bb_width-period"] = (
dataframe["bb_upperband-period"]
- dataframe["bb_lowerband-period"]
) / dataframe["bb_middleband-period"]
dataframe["%-close-bb_lower-period"] = (
dataframe["close"] / dataframe["bb_lowerband-period"]
)
dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period)
dataframe["%-relative_volume-period"] = (
dataframe["volume"] / dataframe["volume"].rolling(period).mean()
)
return dataframe
```
### Freqai - feature engineering basic
Basic features. Make sure to remove the `{pair}` part from your features.
``` python linenums="1"
def feature_engineering_expand_basic(self, dataframe, **kwargs):
"""
*Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined
`include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`.
In other words, a single feature defined in this function
will automatically expand to a total of
`include_timeframes` * `include_shifted_candles` * `include_corr_pairs`
numbers of features added to the model.
Features defined here will *not* be automatically duplicated on user defined
`indicator_periods_candles`
All features must be prepended with `%` to be recognized by FreqAI internals.
More details on how these config defined parameters accelerate feature engineering
in the documentation at:
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
:param df: strategy dataframe which will receive the features
dataframe["%-pct-change"] = dataframe["close"].pct_change()
dataframe["%-ema-200"] = ta.EMA(dataframe, timeperiod=200)
"""
dataframe["%-pct-change"] = dataframe["close"].pct_change()
dataframe["%-raw_volume"] = dataframe["volume"]
dataframe["%-raw_price"] = dataframe["close"]
return dataframe
```
### FreqAI - feature engineering standard
``` python linenums="1"
def feature_engineering_standard(self, dataframe, **kwargs):
"""
*Only functional with FreqAI enabled strategies*
This optional function will be called once with the dataframe of the base timeframe.
This is the final function to be called, which means that the dataframe entering this
function will contain all the features and columns created by all other
freqai_feature_engineering_* functions.
This function is a good place to do custom exotic feature extractions (e.g. tsfresh).
This function is a good place for any feature that should not be auto-expanded upon
(e.g. day of the week).
All features must be prepended with `%` to be recognized by FreqAI internals.
More details about feature engineering available:
https://www.freqtrade.io/en/latest/freqai-feature-engineering
:param df: strategy dataframe which will receive the features
usage example: dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7
"""
dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
return dataframe
```
### FreqAI - set Targets
Targets now get their own, dedicated method.
``` python linenums="1"
def set_freqai_targets(self, dataframe, **kwargs):
"""
*Only functional with FreqAI enabled strategies*
Required function to set the targets for the model.
All targets must be prepended with `&` to be recognized by the FreqAI internals.
More details about feature engineering available:
https://www.freqtrade.io/en/latest/freqai-feature-engineering
:param df: strategy dataframe which will receive the targets
usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"]
"""
dataframe["&-s_close"] = (
dataframe["close"]
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
.mean()
/ dataframe["close"]
- 1
)
return dataframe
```

View File

@@ -11,3 +11,18 @@
.rst-versions .rst-other-versions { .rst-versions .rst-other-versions {
color: white; color: white;
} }
#widget-wrapper {
height: calc(220px * 0.5625 + 18px);
width: 220px;
margin: 0 auto 16px auto;
border-style: solid;
border-color: var(--md-code-bg-color);
border-width: 1px;
border-radius: 5px;
}
@media screen and (max-width: calc(76.25em - 1px)) {
#widget-wrapper { display: none; }
}

View File

@@ -1,148 +0,0 @@
# Trade Object
## Trade
A position freqtrade enters is stored in a `Trade` object - which is persisted to the database.
It's a core concept of freqtrade - and something you'll come across in many sections of the documentation, which will most likely point you to this location.
It will be passed to the strategy in many [strategy callbacks](strategy-callbacks.md). The object passed to the strategy cannot be modified directly. Indirect modifications may occur based on callback results.
## Trade - Available attributes
The following attributes / properties are available for each individual trade - and can be used with `trade.<property>` (e.g. `trade.pair`).
| Attribute | DataType | Description |
|------------|-------------|-------------|
`pair`| string | Pair of this trade
`is_open`| boolean | Is the trade currently open, or has it been concluded
`open_rate`| float | Rate this trade was entered at (Avg. entry rate in case of trade-adjustments)
`close_rate`| float | Close rate - only set when is_open = False
`stake_amount`| float | Amount in Stake (or Quote) currency.
`amount`| float | Amount in Asset / Base currency that is currently owned.
`open_date`| datetime | Timestamp when trade was opened **use `open_date_utc` instead**
`open_date_utc`| datetime | Timestamp when trade was opened - in UTC
`close_date`| datetime | Timestamp when trade was closed **use `close_date_utc` instead**
`close_date_utc`| datetime | Timestamp when trade was closed - in UTC
`close_profit`| float | Relative profit at the time of trade closure. `0.01` == 1%
`close_profit_abs`| float | Absolute profit (in stake currency) at the time of trade closure.
`leverage` | float | Leverage used for this trade - defaults to 1.0 in spot markets.
`enter_tag`| string | Tag provided on entry via the `enter_tag` column in the dataframe
`is_short` | boolean | True for short trades, False otherwise
`orders` | Order[] | List of order objects attached to this trade (includes both filled and cancelled orders)
`date_last_filled_utc` | datetime | Time of the last filled order
`entry_side` | "buy" / "sell" | Order Side the trade was entered
`exit_side` | "buy" / "sell" | Order Side that will result in a trade exit / position reduction.
`trade_direction` | "long" / "short" | Trade direction in text - long or short.
`nr_of_successful_entries` | int | Number of successful (filled) entry orders
`nr_of_successful_exits` | int | Number of successful (filled) exit orders
## Class methods
The following are class methods - which return generic information, and usually result in an explicit query against the database.
They can be used as `Trade.<method>` - e.g. `open_trades = Trade.get_open_trade_count()`
!!! Warning "Backtesting/hyperopt"
Most methods will work in both backtesting / hyperopt and live/dry modes.
During backtesting, it's limited to usage in [strategy callbacks](strategy-callbacks.md). Usage in `populate_*()` methods is not supported and will result in wrong results.
### get_trades_proxy
When your strategy needs some information on existing (open or close) trades - it's best to use `Trade.get_trades_proxy()`.
Usage:
``` python
from freqtrade.persistence import Trade
from datetime import timedelta
# ...
trade_hist = Trade.get_trades_proxy(pair='ETH/USDT', is_open=False, open_date=current_date - timedelta(days=2))
```
`get_trades_proxy()` supports the following keyword arguments. All arguments are optional - calling `get_trades_proxy()` without arguments will return a list of all trades in the database.
* `pair` e.g. `pair='ETH/USDT'`
* `is_open` e.g. `is_open=False`
* `open_date` e.g. `open_date=current_date - timedelta(days=2)`
* `close_date` e.g. `close_date=current_date - timedelta(days=5)`
### get_open_trade_count
Get the number of currently open trades
``` python
from freqtrade.persistence import Trade
# ...
open_trades = Trade.get_open_trade_count()
```
### get_total_closed_profit
Retrieve the total profit the bot has generated so far.
Aggregates `close_profit_abs` for all closed trades.
``` python
from freqtrade.persistence import Trade
# ...
profit = Trade.get_total_closed_profit()
```
### total_open_trades_stakes
Retrieve the total stake_amount that's currently in trades.
``` python
from freqtrade.persistence import Trade
# ...
profit = Trade.total_open_trades_stakes()
```
### get_overall_performance
Retrieve the overall performance - similar to the `/performance` telegram command.
``` python
from freqtrade.persistence import Trade
# ...
if self.config['runmode'].value in ('live', 'dry_run'):
performance = Trade.get_overall_performance()
```
Sample return value: ETH/BTC had 5 trades, with a total profit of 1.5% (ratio of 0.015).
``` json
{"pair": "ETH/BTC", "profit": 0.015, "count": 5}
```
## Order Object
An `Order` object represents an order on the exchange (or a simulated order in dry-run mode).
An `Order` object will always be tied to it's corresponding [`Trade`](#trade-object), and only really makes sense in the context of a trade.
### Order - Available attributes
an Order object is typically attached to a trade.
Most properties here can be None as they are dependant on the exchange response.
| Attribute | DataType | Description |
|------------|-------------|-------------|
`trade` | Trade | Trade object this order is attached to
`ft_pair` | string | Pair this order is for
`ft_is_open` | boolean | is the order filled?
`order_type` | string | Order type as defined on the exchange - usually market, limit or stoploss
`status` | string | Status as defined by ccxt. Usually open, closed, expired or canceled
`side` | string | Buy or Sell
`price` | float | Price the order was placed at
`average` | float | Average price the order filled at
`amount` | float | Amount in base currency
`filled` | float | Filled amount (in base currency)
`remaining` | float | Remaining amount
`cost` | float | Cost of the order - usually average * filled
`order_date` | datetime | Order creation date **use `order_date_utc` instead**
`order_date_utc` | datetime | Order creation date (in UTC)
`order_fill_date` | datetime | Order fill date **use `order_fill_utc` instead**
`order_fill_date_utc` | datetime | Order fill date

View File

@@ -1,20 +1,19 @@
""" Freqtrade bot """ """ Freqtrade bot """
__version__ = '2023.1' __version__ = '2022.12.dev'
if 'dev' in __version__: if 'dev' in __version__:
from pathlib import Path
try: try:
import subprocess import subprocess
freqtrade_basedir = Path(__file__).parent
__version__ = __version__ + '-' + subprocess.check_output( __version__ = __version__ + '-' + subprocess.check_output(
['git', 'log', '--format="%h"', '-n 1'], ['git', 'log', '--format="%h"', '-n 1'],
stderr=subprocess.DEVNULL, cwd=freqtrade_basedir).decode("utf-8").rstrip().strip('"') stderr=subprocess.DEVNULL).decode("utf-8").rstrip().strip('"')
except Exception: # pragma: no cover except Exception: # pragma: no cover
# git not available, ignore # git not available, ignore
try: try:
# Try Fallback to freqtrade_commit file (created by CI while building docker image) # Try Fallback to freqtrade_commit file (created by CI while building docker image)
from pathlib import Path
versionfile = Path('./freqtrade_commit') versionfile = Path('./freqtrade_commit')
if versionfile.is_file(): if versionfile.is_file():
__version__ = f"docker-{__version__}-{versionfile.read_text()[:8]}" __version__ = f"docker-{__version__}-{versionfile.read_text()[:8]}"

View File

@@ -251,8 +251,7 @@ AVAILABLE_CLI_OPTIONS = {
"spaces": Arg( "spaces": Arg(
'--spaces', '--spaces',
help='Specify which parameters to hyperopt. Space-separated list.', help='Specify which parameters to hyperopt. Space-separated list.',
choices=['all', 'buy', 'sell', 'roi', 'stoploss', choices=['all', 'buy', 'sell', 'roi', 'stoploss', 'trailing', 'protection', 'default'],
'trailing', 'protection', 'trades', 'default'],
nargs='+', nargs='+',
default='default', default='default',
), ),
@@ -633,11 +632,10 @@ AVAILABLE_CLI_OPTIONS = {
"1: by enter_tag, " "1: by enter_tag, "
"2: by enter_tag and exit_tag, " "2: by enter_tag and exit_tag, "
"3: by pair and enter_tag, " "3: by pair and enter_tag, "
"4: by pair, enter_ and exit_tag (this can get quite large), " "4: by pair, enter_ and exit_tag (this can get quite large)"),
"5: by exit_tag"),
nargs='+', nargs='+',
default=['0', '1', '2'], default=['0', '1', '2'],
choices=['0', '1', '2', '3', '4', '5'], choices=['0', '1', '2', '3', '4'],
), ),
"enter_reason_list": Arg( "enter_reason_list": Arg(
"--enter-reason-list", "--enter-reason-list",

View File

@@ -14,7 +14,6 @@ from freqtrade.exceptions import OperationalException
from freqtrade.exchange import market_is_active, timeframe_to_minutes from freqtrade.exchange import market_is_active, timeframe_to_minutes
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist, expand_pairlist from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist, expand_pairlist
from freqtrade.resolvers import ExchangeResolver from freqtrade.resolvers import ExchangeResolver
from freqtrade.util.binance_mig import migrate_binance_futures_data
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -87,7 +86,6 @@ def start_download_data(args: Dict[str, Any]) -> None:
"Please use `--dl-trades` instead for this exchange " "Please use `--dl-trades` instead for this exchange "
"(will unfortunately take a long time)." "(will unfortunately take a long time)."
) )
migrate_binance_futures_data(config)
pairs_not_available = refresh_backtest_ohlcv_data( pairs_not_available = refresh_backtest_ohlcv_data(
exchange, pairs=expanded_pairs, timeframes=config['timeframes'], exchange, pairs=expanded_pairs, timeframes=config['timeframes'],
datadir=config['datadir'], timerange=timerange, datadir=config['datadir'], timerange=timerange,
@@ -147,7 +145,6 @@ def start_convert_data(args: Dict[str, Any], ohlcv: bool = True) -> None:
""" """
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
if ohlcv: if ohlcv:
migrate_binance_futures_data(config)
candle_types = [CandleType.from_string(ct) for ct in config.get('candle_types', ['spot'])] candle_types = [CandleType.from_string(ct) for ct in config.get('candle_types', ['spot'])]
for candle_type in candle_types: for candle_type in candle_types:
convert_ohlcv_format(config, convert_ohlcv_format(config,

View File

@@ -28,7 +28,7 @@ class Configuration:
Reuse this class for the bot, backtesting, hyperopt and every script that required configuration Reuse this class for the bot, backtesting, hyperopt and every script that required configuration
""" """
def __init__(self, args: Dict[str, Any], runmode: Optional[RunMode] = None) -> None: def __init__(self, args: Dict[str, Any], runmode: RunMode = None) -> None:
self.args = args self.args = args
self.config: Optional[Config] = None self.config: Optional[Config] = None
self.runmode = runmode self.runmode = runmode

View File

@@ -6,7 +6,7 @@ import re
import sys import sys
from copy import deepcopy from copy import deepcopy
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional from typing import Any, Dict, List
import rapidjson import rapidjson
@@ -75,8 +75,7 @@ def load_config_file(path: str) -> Dict[str, Any]:
return config return config
def load_from_files( def load_from_files(files: List[str], base_path: Path = None, level: int = 0) -> Dict[str, Any]:
files: List[str], base_path: Optional[Path] = None, level: int = 0) -> Dict[str, Any]:
""" """
Recursively load configuration files if specified. Recursively load configuration files if specified.
Sub-files are assumed to be relative to the initial config. Sub-files are assumed to be relative to the initial config.

View File

@@ -31,7 +31,7 @@ HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss',
'CalmarHyperOptLoss', 'CalmarHyperOptLoss',
'MaxDrawDownHyperOptLoss', 'MaxDrawDownRelativeHyperOptLoss', 'MaxDrawDownHyperOptLoss', 'MaxDrawDownRelativeHyperOptLoss',
'ProfitDrawDownHyperOptLoss'] 'ProfitDrawDownHyperOptLoss']
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'ProducerPairList', 'RemotePairList', AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'ProducerPairList',
'AgeFilter', 'OffsetFilter', 'PerformanceFilter', 'AgeFilter', 'OffsetFilter', 'PerformanceFilter',
'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter',
'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter'] 'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter']
@@ -636,6 +636,7 @@ SCHEMA_TRADE_REQUIRED = [
SCHEMA_BACKTEST_REQUIRED = [ SCHEMA_BACKTEST_REQUIRED = [
'exchange', 'exchange',
'max_open_trades',
'stake_currency', 'stake_currency',
'stake_amount', 'stake_amount',
'dry_run_wallet', 'dry_run_wallet',
@@ -645,7 +646,6 @@ SCHEMA_BACKTEST_REQUIRED = [
SCHEMA_BACKTEST_REQUIRED_FINAL = SCHEMA_BACKTEST_REQUIRED + [ SCHEMA_BACKTEST_REQUIRED_FINAL = SCHEMA_BACKTEST_REQUIRED + [
'stoploss', 'stoploss',
'minimal_roi', 'minimal_roi',
'max_open_trades'
] ]
SCHEMA_MINIMAL_REQUIRED = [ SCHEMA_MINIMAL_REQUIRED = [
@@ -681,4 +681,3 @@ MakerTaker = Literal['maker', 'taker']
BidAsk = Literal['bid', 'ask'] BidAsk = Literal['bid', 'ask']
Config = Dict[str, Any] Config = Dict[str, Any]
IntOrInf = float

View File

@@ -10,7 +10,7 @@ from typing import Any, Dict, List, Optional, Union
import numpy as np import numpy as np
import pandas as pd import pandas as pd
from freqtrade.constants import LAST_BT_RESULT_FN, IntOrInf from freqtrade.constants import LAST_BT_RESULT_FN
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.misc import json_load from freqtrade.misc import json_load
from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename
@@ -20,8 +20,8 @@ from freqtrade.persistence import LocalTrade, Trade, init_db
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Newest format # Newest format
BT_DATA_COLUMNS = ['pair', 'stake_amount', 'max_stake_amount', 'amount', BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date',
'open_date', 'close_date', 'open_rate', 'close_rate', 'open_rate', 'close_rate',
'fee_open', 'fee_close', 'trade_duration', 'fee_open', 'fee_close', 'trade_duration',
'profit_ratio', 'profit_abs', 'exit_reason', 'profit_ratio', 'profit_abs', 'exit_reason',
'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs', 'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs',
@@ -90,8 +90,7 @@ def get_latest_hyperopt_filename(directory: Union[Path, str]) -> str:
return 'hyperopt_results.pickle' return 'hyperopt_results.pickle'
def get_latest_hyperopt_file( def get_latest_hyperopt_file(directory: Union[Path, str], predef_filename: str = None) -> Path:
directory: Union[Path, str], predef_filename: Optional[str] = None) -> Path:
""" """
Get latest hyperopt export based on '.last_result.json'. Get latest hyperopt export based on '.last_result.json'.
:param directory: Directory to search for last result :param directory: Directory to search for last result
@@ -194,7 +193,7 @@ def get_backtest_resultlist(dirname: Path):
def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, str], def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, str],
min_backtest_date: Optional[datetime] = None) -> Dict[str, Any]: min_backtest_date: datetime = None) -> Dict[str, Any]:
""" """
Find existing backtest stats that match specified run IDs and load them. Find existing backtest stats that match specified run IDs and load them.
:param dirname: pathlib.Path object, or string pointing to the file. :param dirname: pathlib.Path object, or string pointing to the file.
@@ -242,33 +241,6 @@ def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, s
return results return results
def _load_backtest_data_df_compatibility(df: pd.DataFrame) -> pd.DataFrame:
"""
Compatibility support for older backtest data.
"""
df['open_date'] = pd.to_datetime(df['open_date'],
utc=True,
infer_datetime_format=True
)
df['close_date'] = pd.to_datetime(df['close_date'],
utc=True,
infer_datetime_format=True
)
# Compatibility support for pre short Columns
if 'is_short' not in df.columns:
df['is_short'] = False
if 'leverage' not in df.columns:
df['leverage'] = 1.0
if 'enter_tag' not in df.columns:
df['enter_tag'] = df['buy_tag']
df = df.drop(['buy_tag'], axis=1)
if 'max_stake_amount' not in df.columns:
df['max_stake_amount'] = df['stake_amount']
if 'orders' not in df.columns:
df['orders'] = None
return df
def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = None) -> pd.DataFrame: def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = None) -> pd.DataFrame:
""" """
Load backtest data file. Load backtest data file.
@@ -297,7 +269,24 @@ def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = Non
data = data['strategy'][strategy]['trades'] data = data['strategy'][strategy]['trades']
df = pd.DataFrame(data) df = pd.DataFrame(data)
if not df.empty: if not df.empty:
df = _load_backtest_data_df_compatibility(df) df['open_date'] = pd.to_datetime(df['open_date'],
utc=True,
infer_datetime_format=True
)
df['close_date'] = pd.to_datetime(df['close_date'],
utc=True,
infer_datetime_format=True
)
# Compatibility support for pre short Columns
if 'is_short' not in df.columns:
df['is_short'] = 0
if 'leverage' not in df.columns:
df['leverage'] = 1.0
if 'enter_tag' not in df.columns:
df['enter_tag'] = df['buy_tag']
df = df.drop(['buy_tag'], axis=1)
if 'orders' not in df.columns:
df['orders'] = None
else: else:
# old format - only with lists. # old format - only with lists.
@@ -333,7 +322,7 @@ def analyze_trade_parallelism(results: pd.DataFrame, timeframe: str) -> pd.DataF
def evaluate_result_multi(results: pd.DataFrame, timeframe: str, def evaluate_result_multi(results: pd.DataFrame, timeframe: str,
max_open_trades: IntOrInf) -> pd.DataFrame: max_open_trades: int) -> pd.DataFrame:
""" """
Find overlapping trades by expanding each trade once per period it was open Find overlapping trades by expanding each trade once per period it was open
and then counting overlaps and then counting overlaps

View File

@@ -281,7 +281,7 @@ class DataProvider:
def historic_ohlcv( def historic_ohlcv(
self, self,
pair: str, pair: str,
timeframe: Optional[str] = None, timeframe: str = None,
candle_type: str = '' candle_type: str = ''
) -> DataFrame: ) -> DataFrame:
""" """
@@ -333,7 +333,7 @@ class DataProvider:
def get_pair_dataframe( def get_pair_dataframe(
self, self,
pair: str, pair: str,
timeframe: Optional[str] = None, timeframe: str = None,
candle_type: str = '' candle_type: str = ''
) -> DataFrame: ) -> DataFrame:
""" """
@@ -415,7 +415,7 @@ class DataProvider:
def refresh(self, def refresh(self,
pairlist: ListPairsWithTimeframes, pairlist: ListPairsWithTimeframes,
helping_pairs: Optional[ListPairsWithTimeframes] = None) -> None: helping_pairs: ListPairsWithTimeframes = None) -> None:
""" """
Refresh data, called with each cycle Refresh data, called with each cycle
""" """
@@ -439,7 +439,7 @@ class DataProvider:
def ohlcv( def ohlcv(
self, self,
pair: str, pair: str,
timeframe: Optional[str] = None, timeframe: str = None,
copy: bool = True, copy: bool = True,
candle_type: str = '' candle_type: str = ''
) -> DataFrame: ) -> DataFrame:

View File

@@ -52,7 +52,7 @@ def _process_candles_and_indicators(pairlist, strategy_name, trades, signal_cand
return analysed_trades_dict return analysed_trades_dict
def _analyze_candles_and_indicators(pair, trades: pd.DataFrame, signal_candles: pd.DataFrame): def _analyze_candles_and_indicators(pair, trades, signal_candles):
buyf = signal_candles buyf = signal_candles
if len(buyf) > 0: if len(buyf) > 0:
@@ -120,7 +120,7 @@ def _do_group_table_output(bigdf, glist):
else: else:
agg_mask = {'profit_abs': ['count', 'sum', 'median', 'mean'], agg_mask = {'profit_abs': ['count', 'sum', 'median', 'mean'],
'profit_ratio': ['median', 'mean', 'sum']} 'profit_ratio': ['sum', 'median', 'mean']}
agg_cols = ['num_buys', 'profit_abs_sum', 'profit_abs_median', agg_cols = ['num_buys', 'profit_abs_sum', 'profit_abs_median',
'profit_abs_mean', 'median_profit_pct', 'mean_profit_pct', 'profit_abs_mean', 'median_profit_pct', 'mean_profit_pct',
'total_profit_pct'] 'total_profit_pct']
@@ -141,12 +141,6 @@ def _do_group_table_output(bigdf, glist):
# 4: profit summaries grouped by pair, enter_ and exit_tag (this can get quite large) # 4: profit summaries grouped by pair, enter_ and exit_tag (this can get quite large)
if g == "4": if g == "4":
group_mask = ['pair', 'enter_reason', 'exit_reason'] group_mask = ['pair', 'enter_reason', 'exit_reason']
# 5: profit summaries grouped by exit_tag
if g == "5":
group_mask = ['exit_reason']
sortcols = ['exit_reason']
if group_mask: if group_mask:
new = bigdf.groupby(group_mask).agg(agg_mask).reset_index() new = bigdf.groupby(group_mask).agg(agg_mask).reset_index()
new.columns = group_mask + agg_cols new.columns = group_mask + agg_cols

View File

@@ -28,8 +28,8 @@ def load_pair_history(pair: str,
fill_up_missing: bool = True, fill_up_missing: bool = True,
drop_incomplete: bool = False, drop_incomplete: bool = False,
startup_candles: int = 0, startup_candles: int = 0,
data_format: Optional[str] = None, data_format: str = None,
data_handler: Optional[IDataHandler] = None, data_handler: IDataHandler = None,
candle_type: CandleType = CandleType.SPOT candle_type: CandleType = CandleType.SPOT
) -> DataFrame: ) -> DataFrame:
""" """
@@ -69,7 +69,7 @@ def load_data(datadir: Path,
fail_without_data: bool = False, fail_without_data: bool = False,
data_format: str = 'json', data_format: str = 'json',
candle_type: CandleType = CandleType.SPOT, candle_type: CandleType = CandleType.SPOT,
user_futures_funding_rate: Optional[int] = None, user_futures_funding_rate: int = None,
) -> Dict[str, DataFrame]: ) -> Dict[str, DataFrame]:
""" """
Load ohlcv history data for a list of pairs. Load ohlcv history data for a list of pairs.
@@ -116,7 +116,7 @@ def refresh_data(*, datadir: Path,
timeframe: str, timeframe: str,
pairs: List[str], pairs: List[str],
exchange: Exchange, exchange: Exchange,
data_format: Optional[str] = None, data_format: str = None,
timerange: Optional[TimeRange] = None, timerange: Optional[TimeRange] = None,
candle_type: CandleType, candle_type: CandleType,
) -> None: ) -> None:
@@ -189,7 +189,7 @@ def _download_pair_history(pair: str, *,
timeframe: str = '5m', timeframe: str = '5m',
process: str = '', process: str = '',
new_pairs_days: int = 30, new_pairs_days: int = 30,
data_handler: Optional[IDataHandler] = None, data_handler: IDataHandler = None,
timerange: Optional[TimeRange] = None, timerange: Optional[TimeRange] = None,
candle_type: CandleType, candle_type: CandleType,
erase: bool = False, erase: bool = False,
@@ -272,7 +272,7 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes
datadir: Path, trading_mode: str, datadir: Path, trading_mode: str,
timerange: Optional[TimeRange] = None, timerange: Optional[TimeRange] = None,
new_pairs_days: int = 30, erase: bool = False, new_pairs_days: int = 30, erase: bool = False,
data_format: Optional[str] = None, data_format: str = None,
prepend: bool = False, prepend: bool = False,
) -> List[str]: ) -> List[str]:
""" """

View File

@@ -374,21 +374,6 @@ class IDataHandler(ABC):
logger.warning(f"{pair}, {candle_type}, {timeframe}, " logger.warning(f"{pair}, {candle_type}, {timeframe}, "
f"data ends at {pairdata.iloc[-1]['date']:%Y-%m-%d %H:%M:%S}") f"data ends at {pairdata.iloc[-1]['date']:%Y-%m-%d %H:%M:%S}")
def rename_futures_data(
self, pair: str, new_pair: str, timeframe: str, candle_type: CandleType):
"""
Temporary method to migrate data from old naming to new naming (BTC/USDT -> BTC/USDT:USDT)
Only used for binance to support the binance futures naming unification.
"""
file_old = self._pair_data_filename(self._datadir, pair, timeframe, candle_type)
file_new = self._pair_data_filename(self._datadir, new_pair, timeframe, candle_type)
# print(file_old, file_new)
if file_new.exists():
logger.warning(f"{file_new} exists already, can't migrate {pair}.")
return
file_old.rename(file_new)
def get_datahandlerclass(datatype: str) -> Type[IDataHandler]: def get_datahandlerclass(datatype: str) -> Type[IDataHandler]:
""" """
@@ -418,8 +403,8 @@ def get_datahandlerclass(datatype: str) -> Type[IDataHandler]:
raise ValueError(f"No datahandler for datatype {datatype} available.") raise ValueError(f"No datahandler for datatype {datatype} available.")
def get_datahandler(datadir: Path, data_format: Optional[str] = None, def get_datahandler(datadir: Path, data_format: str = None,
data_handler: Optional[IDataHandler] = None) -> IDataHandler: data_handler: IDataHandler = None) -> IDataHandler:
""" """
:param datadir: Folder to save data :param datadir: Folder to save data
:param data_format: dataformat to use :param data_format: dataformat to use

View File

@@ -1,6 +1,4 @@
import logging import logging
import math
from datetime import datetime
from typing import Dict, Tuple from typing import Dict, Tuple
import numpy as np import numpy as np
@@ -192,119 +190,3 @@ def calculate_cagr(days_passed: int, starting_balance: float, final_balance: flo
:return: CAGR :return: CAGR
""" """
return (final_balance / starting_balance) ** (1 / (days_passed / 365)) - 1 return (final_balance / starting_balance) ** (1 / (days_passed / 365)) - 1
def calculate_expectancy(trades: pd.DataFrame) -> float:
"""
Calculate expectancy
:param trades: DataFrame containing trades (requires columns close_date and profit_abs)
:return: expectancy
"""
if len(trades) == 0:
return 0
expectancy = 1
profit_sum = trades.loc[trades['profit_abs'] > 0, 'profit_abs'].sum()
loss_sum = abs(trades.loc[trades['profit_abs'] < 0, 'profit_abs'].sum())
nb_win_trades = len(trades.loc[trades['profit_abs'] > 0])
nb_loss_trades = len(trades.loc[trades['profit_abs'] < 0])
if (nb_win_trades > 0) and (nb_loss_trades > 0):
average_win = profit_sum / nb_win_trades
average_loss = loss_sum / nb_loss_trades
risk_reward_ratio = average_win / average_loss
winrate = nb_win_trades / len(trades)
expectancy = ((1 + risk_reward_ratio) * winrate) - 1
elif nb_win_trades == 0:
expectancy = 0
return expectancy
def calculate_sortino(trades: pd.DataFrame, min_date: datetime, max_date: datetime,
starting_balance: float) -> float:
"""
Calculate sortino
:param trades: DataFrame containing trades (requires columns profit_abs)
:return: sortino
"""
if (len(trades) == 0) or (min_date is None) or (max_date is None) or (min_date == max_date):
return 0
total_profit = trades['profit_abs'] / starting_balance
days_period = max(1, (max_date - min_date).days)
expected_returns_mean = total_profit.sum() / days_period
down_stdev = np.std(trades.loc[trades['profit_abs'] < 0, 'profit_abs'] / starting_balance)
if down_stdev != 0 and not np.isnan(down_stdev):
sortino_ratio = expected_returns_mean / down_stdev * np.sqrt(365)
else:
# Define high (negative) sortino ratio to be clear that this is NOT optimal.
sortino_ratio = -100
# print(expected_returns_mean, down_stdev, sortino_ratio)
return sortino_ratio
def calculate_sharpe(trades: pd.DataFrame, min_date: datetime, max_date: datetime,
starting_balance: float) -> float:
"""
Calculate sharpe
:param trades: DataFrame containing trades (requires column profit_abs)
:return: sharpe
"""
if (len(trades) == 0) or (min_date is None) or (max_date is None) or (min_date == max_date):
return 0
total_profit = trades['profit_abs'] / starting_balance
days_period = max(1, (max_date - min_date).days)
expected_returns_mean = total_profit.sum() / days_period
up_stdev = np.std(total_profit)
if up_stdev != 0:
sharp_ratio = expected_returns_mean / up_stdev * np.sqrt(365)
else:
# Define high (negative) sharpe ratio to be clear that this is NOT optimal.
sharp_ratio = -100
# print(expected_returns_mean, up_stdev, sharp_ratio)
return sharp_ratio
def calculate_calmar(trades: pd.DataFrame, min_date: datetime, max_date: datetime,
starting_balance: float) -> float:
"""
Calculate calmar
:param trades: DataFrame containing trades (requires columns close_date and profit_abs)
:return: calmar
"""
if (len(trades) == 0) or (min_date is None) or (max_date is None) or (min_date == max_date):
return 0
total_profit = trades['profit_abs'].sum() / starting_balance
days_period = max(1, (max_date - min_date).days)
# adding slippage of 0.1% per trade
# total_profit = total_profit - 0.0005
expected_returns_mean = total_profit / days_period * 100
# calculate max drawdown
try:
_, _, _, _, _, max_drawdown = calculate_max_drawdown(
trades, value_col="profit_abs", starting_balance=starting_balance
)
except ValueError:
max_drawdown = 0
if max_drawdown != 0:
calmar_ratio = expected_returns_mean / max_drawdown * math.sqrt(365)
else:
# Define high (negative) calmar ratio to be clear that this is NOT optimal.
calmar_ratio = -100
# print(expected_returns_mean, max_drawdown, calmar_ratio)
return calmar_ratio

View File

@@ -11,7 +11,7 @@ from freqtrade.enums import CandleType, MarginMode, TradingMode
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.exchange.common import retrier from freqtrade.exchange.common import retrier
from freqtrade.exchange.types import OHLCVResponse, Tickers from freqtrade.exchange.types import Tickers
from freqtrade.misc import deep_merge_dicts, json_load from freqtrade.misc import deep_merge_dicts, json_load
@@ -28,10 +28,10 @@ class Binance(Exchange):
"trades_pagination": "id", "trades_pagination": "id",
"trades_pagination_arg": "fromId", "trades_pagination_arg": "fromId",
"l2_limit_range": [5, 10, 20, 50, 100, 500, 1000], "l2_limit_range": [5, 10, 20, 50, 100, 500, 1000],
"ccxt_futures_name": "swap" "ccxt_futures_name": "future"
} }
_ft_has_futures: Dict = { _ft_has_futures: Dict = {
"stoploss_order_types": {"limit": "stop", "market": "stop_market"}, "stoploss_order_types": {"limit": "limit", "market": "market"},
"tickers_have_price": False, "tickers_have_price": False,
} }
@@ -112,7 +112,7 @@ class Binance(Exchange):
since_ms: int, candle_type: CandleType, since_ms: int, candle_type: CandleType,
is_new_pair: bool = False, raise_: bool = False, is_new_pair: bool = False, raise_: bool = False,
until_ms: Optional[int] = None until_ms: Optional[int] = None
) -> OHLCVResponse: ) -> Tuple[str, str, str, List]:
""" """
Overwrite to introduce "fast new pair" functionality by detecting the pair's listing date Overwrite to introduce "fast new pair" functionality by detecting the pair's listing date
Does not work for other exchanges, which don't return the earliest data when called with "0" Does not work for other exchanges, which don't return the earliest data when called with "0"

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@
Cryptocurrency Exchanges support Cryptocurrency Exchanges support
""" """
import asyncio import asyncio
import http
import inspect import inspect
import logging import logging
from copy import deepcopy from copy import deepcopy
@@ -35,7 +36,7 @@ from freqtrade.exchange.exchange_utils import (CcxtModuleType, amount_to_contrac
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)
from freqtrade.exchange.types import OHLCVResponse, Ticker, Tickers from freqtrade.exchange.types import Ticker, Tickers
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
@@ -44,6 +45,12 @@ from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Workaround for adding samesite support to pre 3.8 python
# Only applies to python3.7, and only on certain exchanges (kraken)
# Replicates the fix from starlette (which is actually causing this problem)
http.cookies.Morsel._reserved["samesite"] = "SameSite" # type: ignore
class Exchange: class Exchange:
# Parameters to add directly to buy/sell calls (like agreeing to trading agreement) # Parameters to add directly to buy/sell calls (like agreeing to trading agreement)
@@ -467,7 +474,7 @@ class Exchange:
try: try:
if self._api_async: if self._api_async:
self.loop.run_until_complete( self.loop.run_until_complete(
self._api_async.load_markets(reload=reload, params={})) self._api_async.load_markets(reload=reload))
except (asyncio.TimeoutError, ccxt.BaseError) as e: except (asyncio.TimeoutError, ccxt.BaseError) as e:
logger.warning('Could not load async markets. Reason: %s', e) logger.warning('Could not load async markets. Reason: %s', e)
@@ -476,7 +483,7 @@ class Exchange:
def _load_markets(self) -> None: def _load_markets(self) -> None:
""" Initialize markets both sync and async """ """ Initialize markets both sync and async """
try: try:
self._markets = self._api.load_markets(params={}) self._markets = self._api.load_markets()
self._load_async_markets() self._load_async_markets()
self._last_markets_refresh = arrow.utcnow().int_timestamp self._last_markets_refresh = arrow.utcnow().int_timestamp
if self._ft_has['needs_trading_fees']: if self._ft_has['needs_trading_fees']:
@@ -494,7 +501,7 @@ class Exchange:
return None return None
logger.debug("Performing scheduled market reload..") logger.debug("Performing scheduled market reload..")
try: try:
self._markets = self._api.load_markets(reload=True, params={}) self._markets = self._api.load_markets(reload=True)
# Also reload async markets to avoid issues with newly listed pairs # Also reload async markets to avoid issues with newly listed pairs
self._load_async_markets(reload=True) self._load_async_markets(reload=True)
self._last_markets_refresh = arrow.utcnow().int_timestamp self._last_markets_refresh = arrow.utcnow().int_timestamp
@@ -675,7 +682,7 @@ class Exchange:
f"Freqtrade does not support {mm_value} {trading_mode.value} on {self.name}" f"Freqtrade does not support {mm_value} {trading_mode.value} on {self.name}"
) )
def get_option(self, param: str, default: Optional[Any] = None) -> Any: def get_option(self, param: str, default: Any = None) -> Any:
""" """
Get parameter value from _ft_has Get parameter value from _ft_has
""" """
@@ -1350,7 +1357,7 @@ class Exchange:
raise OperationalException(e) from e raise OperationalException(e) from e
@retrier @retrier
def fetch_positions(self, pair: Optional[str] = None) -> List[Dict]: def fetch_positions(self, pair: str = None) -> List[Dict]:
""" """
Fetch positions from the exchange. Fetch positions from the exchange.
If no pair is given, all positions are returned. If no pair is given, all positions are returned.
@@ -1698,7 +1705,7 @@ class Exchange:
return self._config['fee'] return self._config['fee']
# validate that markets are loaded before trying to get fee # validate that markets are loaded before trying to get fee
if self._api.markets is None or len(self._api.markets) == 0: if self._api.markets is None or len(self._api.markets) == 0:
self._api.load_markets(params={}) self._api.load_markets()
return self._api.calculate_fee(symbol=symbol, type=type, side=side, amount=amount, return self._api.calculate_fee(symbol=symbol, type=type, side=side, amount=amount,
price=price, takerOrMaker=taker_or_maker)['rate'] price=price, takerOrMaker=taker_or_maker)['rate']
@@ -1794,7 +1801,7 @@ class Exchange:
def get_historic_ohlcv(self, pair: str, timeframe: str, def get_historic_ohlcv(self, pair: str, timeframe: str,
since_ms: int, candle_type: CandleType, since_ms: int, candle_type: CandleType,
is_new_pair: bool = False, is_new_pair: bool = False,
until_ms: Optional[int] = None) -> List: until_ms: int = None) -> List:
""" """
Get candle history using asyncio and returns the list of candles. Get candle history using asyncio and returns the list of candles.
Handles all async work for this. Handles all async work for this.
@@ -1806,18 +1813,32 @@ class Exchange:
:param candle_type: '', mark, index, premiumIndex, or funding_rate :param candle_type: '', mark, index, premiumIndex, or funding_rate
:return: List with candle (OHLCV) data :return: List with candle (OHLCV) data
""" """
pair, _, _, data, _ = self.loop.run_until_complete( pair, _, _, data = self.loop.run_until_complete(
self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe, self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe,
since_ms=since_ms, until_ms=until_ms, since_ms=since_ms, until_ms=until_ms,
is_new_pair=is_new_pair, candle_type=candle_type)) is_new_pair=is_new_pair, candle_type=candle_type))
logger.info(f"Downloaded data for {pair} with length {len(data)}.") logger.info(f"Downloaded data for {pair} with length {len(data)}.")
return data return data
def get_historic_ohlcv_as_df(self, pair: str, timeframe: str,
since_ms: int, candle_type: CandleType) -> DataFrame:
"""
Minimal wrapper around get_historic_ohlcv - converting the result into a dataframe
:param pair: Pair to download
:param timeframe: Timeframe to get data for
:param since_ms: Timestamp in milliseconds to get history from
:param candle_type: Any of the enum CandleType (must match trading mode!)
:return: OHLCV DataFrame
"""
ticks = self.get_historic_ohlcv(pair, timeframe, since_ms=since_ms, candle_type=candle_type)
return ohlcv_to_dataframe(ticks, timeframe, pair=pair, fill_missing=True,
drop_incomplete=self._ohlcv_partial_candle)
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str, async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
since_ms: int, candle_type: CandleType, since_ms: int, candle_type: CandleType,
is_new_pair: bool = False, raise_: bool = False, is_new_pair: bool = False, raise_: bool = False,
until_ms: Optional[int] = None until_ms: Optional[int] = None
) -> OHLCVResponse: ) -> Tuple[str, str, str, List]:
""" """
Download historic ohlcv Download historic ohlcv
:param is_new_pair: used by binance subclass to allow "fast" new pair downloading :param is_new_pair: used by binance subclass to allow "fast" new pair downloading
@@ -1848,16 +1869,15 @@ class Exchange:
continue continue
else: else:
# Deconstruct tuple if it's not an exception # Deconstruct tuple if it's not an exception
p, _, c, new_data, _ = res p, _, c, new_data = res
if p == pair and c == candle_type: if p == pair and c == candle_type:
data.extend(new_data) data.extend(new_data)
# Sort data again after extending the result - above calls return in "async order" # Sort data again after extending the result - above calls return in "async order"
data = sorted(data, key=lambda x: x[0]) data = sorted(data, key=lambda x: x[0])
return pair, timeframe, candle_type, data, self._ohlcv_partial_candle return pair, timeframe, candle_type, data
def _build_coroutine( def _build_coroutine(self, pair: str, timeframe: str, candle_type: CandleType,
self, pair: str, timeframe: str, candle_type: CandleType, since_ms: Optional[int], cache: bool) -> Coroutine:
since_ms: Optional[int], cache: bool) -> Coroutine[Any, Any, OHLCVResponse]:
not_all_data = cache and self.required_candle_call_count > 1 not_all_data = cache and self.required_candle_call_count > 1
if cache and (pair, timeframe, candle_type) in self._klines: if cache and (pair, timeframe, candle_type) in self._klines:
candle_limit = self.ohlcv_candle_limit(timeframe, candle_type) candle_limit = self.ohlcv_candle_limit(timeframe, candle_type)
@@ -1894,7 +1914,7 @@ class Exchange:
""" """
Build Coroutines to execute as part of refresh_latest_ohlcv Build Coroutines to execute as part of refresh_latest_ohlcv
""" """
input_coroutines: List[Coroutine[Any, Any, OHLCVResponse]] = [] input_coroutines = []
cached_pairs = [] cached_pairs = []
for pair, timeframe, candle_type in set(pair_list): for pair, timeframe, candle_type in set(pair_list):
if (timeframe not in self.timeframes if (timeframe not in self.timeframes
@@ -1958,6 +1978,7 @@ class Exchange:
:return: Dict of [{(pair, timeframe): Dataframe}] :return: Dict of [{(pair, timeframe): Dataframe}]
""" """
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
# Gather coroutines to run # Gather coroutines to run
input_coroutines, cached_pairs = self._build_ohlcv_dl_jobs(pair_list, since_ms, cache) input_coroutines, cached_pairs = self._build_ohlcv_dl_jobs(pair_list, since_ms, cache)
@@ -1975,9 +1996,8 @@ class Exchange:
if isinstance(res, Exception): if isinstance(res, Exception):
logger.warning(f"Async code raised an exception: {repr(res)}") logger.warning(f"Async code raised an exception: {repr(res)}")
continue continue
# Deconstruct tuple (has 5 elements) # Deconstruct tuple (has 4 elements)
pair, timeframe, c_type, ticks, drop_hint = res pair, timeframe, c_type, ticks = res
drop_incomplete = drop_hint if drop_incomplete is None else drop_incomplete
ohlcv_df = self._process_ohlcv_df( ohlcv_df = self._process_ohlcv_df(
pair, timeframe, c_type, ticks, cache, drop_incomplete) pair, timeframe, c_type, ticks, cache, drop_incomplete)
@@ -2005,7 +2025,7 @@ class Exchange:
timeframe: str, timeframe: str,
candle_type: CandleType, candle_type: CandleType,
since_ms: Optional[int] = None, since_ms: Optional[int] = None,
) -> OHLCVResponse: ) -> Tuple[str, str, str, List]:
""" """
Asynchronously get candle history data using fetch_ohlcv Asynchronously get candle history data using fetch_ohlcv
:param candle_type: '', mark, index, premiumIndex, or funding_rate :param candle_type: '', mark, index, premiumIndex, or funding_rate
@@ -2015,8 +2035,8 @@ class Exchange:
# Fetch OHLCV asynchronously # Fetch OHLCV asynchronously
s = '(' + arrow.get(since_ms // 1000).isoformat() + ') ' if since_ms is not None else '' s = '(' + arrow.get(since_ms // 1000).isoformat() + ') ' if since_ms is not None else ''
logger.debug( logger.debug(
"Fetching pair %s, %s, interval %s, since %s %s...", "Fetching pair %s, interval %s, since %s %s...",
pair, candle_type, timeframe, since_ms, s pair, timeframe, since_ms, s
) )
params = deepcopy(self._ft_has.get('ohlcv_params', {})) params = deepcopy(self._ft_has.get('ohlcv_params', {}))
candle_limit = self.ohlcv_candle_limit( candle_limit = self.ohlcv_candle_limit(
@@ -2030,12 +2050,11 @@ class Exchange:
limit=candle_limit, params=params) limit=candle_limit, params=params)
else: else:
# Funding rate # Funding rate
data = await self._fetch_funding_rate_history( data = await self._api_async.fetch_funding_rate_history(
pair=pair, pair, since=since_ms,
timeframe=timeframe, limit=candle_limit)
limit=candle_limit, # Convert funding rate to candle pattern
since_ms=since_ms, data = [[x['timestamp'], x['fundingRate'], 0, 0, 0, 0] for x in data]
)
# Some exchanges sort OHLCV in ASC order and others in DESC. # Some exchanges sort OHLCV in ASC order and others in DESC.
# Ex: Bittrex returns the list of OHLCV in ASC order (oldest first, newest last) # Ex: Bittrex returns the list of OHLCV in ASC order (oldest first, newest last)
# while GDAX returns the list of OHLCV in DESC order (newest first, oldest last) # while GDAX returns the list of OHLCV in DESC order (newest first, oldest last)
@@ -2045,9 +2064,9 @@ class Exchange:
data = sorted(data, key=lambda x: x[0]) data = sorted(data, key=lambda x: x[0])
except IndexError: except IndexError:
logger.exception("Error loading %s. Result was %s.", pair, data) logger.exception("Error loading %s. Result was %s.", pair, data)
return pair, timeframe, candle_type, [], self._ohlcv_partial_candle return pair, timeframe, candle_type, []
logger.debug("Done fetching pair %s, interval %s ...", pair, timeframe) logger.debug("Done fetching pair %s, interval %s ...", pair, timeframe)
return pair, timeframe, candle_type, data, self._ohlcv_partial_candle return pair, timeframe, candle_type, data
except ccxt.NotSupported as e: except ccxt.NotSupported as e:
raise OperationalException( raise OperationalException(
@@ -2063,24 +2082,6 @@ class Exchange:
raise OperationalException(f'Could not fetch historical candle (OHLCV) data ' raise OperationalException(f'Could not fetch historical candle (OHLCV) data '
f'for pair {pair}. Message: {e}') from e f'for pair {pair}. Message: {e}') from e
async def _fetch_funding_rate_history(
self,
pair: str,
timeframe: str,
limit: int,
since_ms: Optional[int] = None,
) -> List[List]:
"""
Fetch funding rate history - used to selectively override this by subclasses.
"""
# Funding rate
data = await self._api_async.fetch_funding_rate_history(
pair, since=since_ms,
limit=limit)
# Convert funding rate to candle pattern
data = [[x['timestamp'], x['fundingRate'], 0, 0, 0, 0] for x in data]
return data
# Fetch historic trades # Fetch historic trades
@retrier_async @retrier_async
@@ -2667,7 +2668,7 @@ class Exchange:
:param amount: Trade amount :param amount: Trade amount
:param open_date: Open date of the trade :param open_date: Open date of the trade
:return: funding fee since open_date :return: funding fee since open_date
:raises: ExchangeError if something goes wrong. :raies: ExchangeError if something goes wrong.
""" """
if self.trading_mode == TradingMode.FUTURES: if self.trading_mode == TradingMode.FUTURES:
if self._config['dry_run']: if self._config['dry_run']:
@@ -2744,16 +2745,11 @@ class Exchange:
""" """
Important: Must be fetching data from cached values as this is used by backtesting! Important: Must be fetching data from cached values as this is used by backtesting!
PERPETUAL: PERPETUAL:
gateio: https://www.gate.io/help/futures/futures/27724/liquidation-price-bankruptcy-price gateio: https://www.gate.io/help/futures/perpetual/22160/calculation-of-liquidation-price
> Liquidation Price = (Entry Price ± Margin / Contract Multiplier / Size) /
[ 1 ± (Maintenance Margin Ratio + Taker Rate)]
Wherein, "+" or "-" depends on whether the contract goes long or short:
"-" for long, and "+" for short.
okex: https://www.okex.com/support/hc/en-us/articles/ okex: https://www.okex.com/support/hc/en-us/articles/
360053909592-VI-Introduction-to-the-isolated-mode-of-Single-Multi-currency-Portfolio-margin 360053909592-VI-Introduction-to-the-isolated-mode-of-Single-Multi-currency-Portfolio-margin
:param pair: Pair to calculate liquidation price for :param exchange_name:
:param open_rate: Entry price of position :param open_rate: Entry price of position
:param is_short: True if the trade is a short, false otherwise :param is_short: True if the trade is a short, false otherwise
:param amount: Absolute value of position size incl. leverage (in base currency) :param amount: Absolute value of position size incl. leverage (in base currency)
@@ -2793,7 +2789,7 @@ class Exchange:
def get_maintenance_ratio_and_amt( def get_maintenance_ratio_and_amt(
self, self,
pair: str, pair: str,
nominal_value: float, nominal_value: float = 0.0,
) -> Tuple[float, Optional[float]]: ) -> Tuple[float, Optional[float]]:
""" """
Important: Must be fetching data from cached values as this is used by backtesting! Important: Must be fetching data from cached values as this is used by backtesting!

View File

@@ -15,19 +15,18 @@ from freqtrade.util import FtPrecise
CcxtModuleType = Any CcxtModuleType = Any
def is_exchange_known_ccxt( def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool:
exchange_name: str, ccxt_module: Optional[CcxtModuleType] = None) -> bool:
return exchange_name in ccxt_exchanges(ccxt_module) return exchange_name in ccxt_exchanges(ccxt_module)
def ccxt_exchanges(ccxt_module: Optional[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
""" """
return ccxt_module.exchanges if ccxt_module is not None else ccxt.exchanges return ccxt_module.exchanges if ccxt_module is not None else ccxt.exchanges
def available_exchanges(ccxt_module: Optional[CcxtModuleType] = None) -> List[str]: def available_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:
""" """
Return exchanges available to the bot, i.e. non-bad exchanges in the ccxt list Return exchanges available to the bot, i.e. non-bad exchanges in the ccxt list
""" """
@@ -87,7 +86,7 @@ def timeframe_to_msecs(timeframe: str) -> int:
return ccxt.Exchange.parse_timeframe(timeframe) * 1000 return ccxt.Exchange.parse_timeframe(timeframe) * 1000
def timeframe_to_prev_date(timeframe: str, date: Optional[datetime] = None) -> datetime: def timeframe_to_prev_date(timeframe: str, date: datetime = None) -> datetime:
""" """
Use Timeframe and determine the candle start date for this date. Use Timeframe and determine the candle start date for this date.
Does not round when given a candle start date. Does not round when given a candle start date.
@@ -103,7 +102,7 @@ def timeframe_to_prev_date(timeframe: str, date: Optional[datetime] = None) -> d
return datetime.fromtimestamp(new_timestamp, tz=timezone.utc) return datetime.fromtimestamp(new_timestamp, tz=timezone.utc)
def timeframe_to_next_date(timeframe: str, date: Optional[datetime] = None) -> datetime: def timeframe_to_next_date(timeframe: str, date: datetime = None) -> datetime:
""" """
Use Timeframe and determine next candle. Use Timeframe and determine next candle.
:param timeframe: timeframe in string format (e.g. "5m") :param timeframe: timeframe in string format (e.g. "5m")

View File

@@ -1,6 +1,4 @@
from typing import Dict, List, Optional, Tuple, TypedDict from typing import Dict, Optional, TypedDict
from freqtrade.enums import CandleType
class Ticker(TypedDict): class Ticker(TypedDict):
@@ -16,6 +14,3 @@ class Ticker(TypedDict):
Tickers = Dict[str, Ticker] Tickers = Dict[str, Ticker]
# pair, timeframe, candleType, OHLCV, drop last?,
OHLCVResponse = Tuple[str, str, CandleType, List, bool]

View File

@@ -1,125 +0,0 @@
import logging
from enum import Enum
from gym import spaces
from freqtrade.freqai.RL.BaseEnvironment import BaseEnvironment, Positions
logger = logging.getLogger(__name__)
class Actions(Enum):
Neutral = 0
Buy = 1
Sell = 2
class Base3ActionRLEnv(BaseEnvironment):
"""
Base class for a 3 action environment
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.actions = Actions
def set_action_space(self):
self.action_space = spaces.Discrete(len(Actions))
def step(self, action: int):
"""
Logic for a single step (incrementing one candle in time)
by the agent
:param: action: int = the action type that the agent plans
to take for the current step.
:returns:
observation = current state of environment
step_reward = the reward from `calculate_reward()`
_done = if the agent "died" or if the candles finished
info = dict passed back to openai gym lib
"""
self._done = False
self._current_tick += 1
if self._current_tick == self._end_tick:
self._done = True
self._update_unrealized_total_profit()
step_reward = self.calculate_reward(action)
self.total_reward += step_reward
self.tensorboard_log(self.actions._member_names_[action])
trade_type = None
if self.is_tradesignal(action):
if action == Actions.Buy.value:
if self._position == Positions.Short:
self._update_total_profit()
self._position = Positions.Long
trade_type = "long"
self._last_trade_tick = self._current_tick
elif action == Actions.Sell.value and self.can_short:
if self._position == Positions.Long:
self._update_total_profit()
self._position = Positions.Short
trade_type = "short"
self._last_trade_tick = self._current_tick
elif action == Actions.Sell.value and not self.can_short:
self._update_total_profit()
self._position = Positions.Neutral
trade_type = "neutral"
self._last_trade_tick = None
else:
print("case not defined")
if trade_type is not None:
self.trade_history.append(
{'price': self.current_price(), 'index': self._current_tick,
'type': trade_type})
if (self._total_profit < self.max_drawdown or
self._total_unrealized_profit < self.max_drawdown):
self._done = True
self._position_history.append(self._position)
info = dict(
tick=self._current_tick,
action=action,
total_reward=self.total_reward,
total_profit=self._total_profit,
position=self._position.value,
trade_duration=self.get_trade_duration(),
current_profit_pct=self.get_unrealized_profit()
)
observation = self._get_observation()
self._update_history(info)
return observation, step_reward, self._done, info
def is_tradesignal(self, action: int) -> bool:
"""
Determine if the signal is a trade signal
e.g.: agent wants a Actions.Buy while it is in a Positions.short
"""
return (
(action == Actions.Buy.value and self._position == Positions.Neutral)
or (action == Actions.Sell.value and self._position == Positions.Long)
or (action == Actions.Sell.value and self._position == Positions.Neutral
and self.can_short)
or (action == Actions.Buy.value and self._position == Positions.Short
and self.can_short)
)
def _is_valid(self, action: int) -> bool:
"""
Determine if the signal is valid.
e.g.: agent wants a Actions.Sell while it is in a Positions.Long
"""
if self.can_short:
return action in [Actions.Buy.value, Actions.Sell.value, Actions.Neutral.value]
else:
if action == Actions.Sell.value and self._position != Positions.Long:
return False
return True

View File

@@ -88,8 +88,7 @@ class Base4ActionRLEnv(BaseEnvironment):
{'price': self.current_price(), 'index': self._current_tick, {'price': self.current_price(), 'index': self._current_tick,
'type': trade_type}) 'type': trade_type})
if (self._total_profit < self.max_drawdown or if self._total_profit < 1 - self.rl_config.get('max_training_drawdown_pct', 0.8):
self._total_unrealized_profit < self.max_drawdown):
self._done = True self._done = True
self._position_history.append(self._position) self._position_history.append(self._position)

View File

@@ -45,7 +45,7 @@ class BaseEnvironment(gym.Env):
def __init__(self, df: DataFrame = DataFrame(), prices: DataFrame = DataFrame(), def __init__(self, df: DataFrame = DataFrame(), prices: DataFrame = DataFrame(),
reward_kwargs: dict = {}, window_size=10, starting_point=True, reward_kwargs: dict = {}, window_size=10, starting_point=True,
id: str = 'baseenv-1', seed: int = 1, config: dict = {}, live: bool = False, id: str = 'baseenv-1', seed: int = 1, config: dict = {}, live: bool = False,
fee: float = 0.0015, can_short: bool = False): fee: float = 0.0015):
""" """
Initializes the training/eval environment. Initializes the training/eval environment.
:param df: dataframe of features :param df: dataframe of features
@@ -58,7 +58,6 @@ class BaseEnvironment(gym.Env):
:param config: Typical user configuration file :param config: Typical user configuration file
:param live: Whether or not this environment is active in dry/live/backtesting :param live: Whether or not this environment is active in dry/live/backtesting
:param fee: The fee to use for environmental interactions. :param fee: The fee to use for environmental interactions.
:param can_short: Whether or not the environment can short
""" """
self.config = config self.config = config
self.rl_config = config['freqai']['rl_config'] self.rl_config = config['freqai']['rl_config']
@@ -74,7 +73,6 @@ class BaseEnvironment(gym.Env):
# set here to default 5Ac, but all children envs can override this # set here to default 5Ac, but all children envs can override this
self.actions: Type[Enum] = BaseActions self.actions: Type[Enum] = BaseActions
self.tensorboard_metrics: dict = {} self.tensorboard_metrics: dict = {}
self.can_short = can_short
self.live = live self.live = live
if not self.live and self.add_state_info: if not self.live and self.add_state_info:
self.add_state_info = False self.add_state_info = False

View File

@@ -165,8 +165,7 @@ class BaseReinforcementLearningModel(IFreqaiModel):
env_info = {"window_size": self.CONV_WIDTH, env_info = {"window_size": self.CONV_WIDTH,
"reward_kwargs": self.reward_params, "reward_kwargs": self.reward_params,
"config": self.config, "config": self.config,
"live": self.live, "live": self.live}
"can_short": self.can_short}
if self.data_provider: if self.data_provider:
env_info["fee"] = self.data_provider._exchange \ env_info["fee"] = self.data_provider._exchange \
.get_fee(symbol=self.data_provider.current_whitelist()[0]) # type: ignore .get_fee(symbol=self.data_provider.current_whitelist()[0]) # type: ignore
@@ -280,36 +279,26 @@ class BaseReinforcementLearningModel(IFreqaiModel):
train_df = data_dictionary["train_features"] train_df = data_dictionary["train_features"]
test_df = data_dictionary["test_features"] test_df = data_dictionary["test_features"]
# %-raw_volume_gen_shift-2_ETH/USDT_1h
# price data for model training and evaluation # price data for model training and evaluation
tf = self.config['timeframe'] tf = self.config['timeframe']
rename_dict = {'%-raw_open': 'open', '%-raw_low': 'low', ohlc_list = [f'%-{pair}raw_open_{tf}', f'%-{pair}raw_low_{tf}',
'%-raw_high': ' high', '%-raw_close': 'close'} f'%-{pair}raw_high_{tf}', f'%-{pair}raw_close_{tf}']
rename_dict_old = {f'%-{pair}raw_open_{tf}': 'open', f'%-{pair}raw_low_{tf}': 'low', rename_dict = {f'%-{pair}raw_open_{tf}': 'open', f'%-{pair}raw_low_{tf}': 'low',
f'%-{pair}raw_high_{tf}': ' high', f'%-{pair}raw_close_{tf}': 'close'} f'%-{pair}raw_high_{tf}': ' high', f'%-{pair}raw_close_{tf}': 'close'}
prices_train = train_df.filter(rename_dict.keys(), axis=1)
prices_train_old = train_df.filter(rename_dict_old.keys(), axis=1)
if prices_train.empty or not prices_train_old.empty:
if not prices_train_old.empty:
prices_train = prices_train_old
rename_dict = rename_dict_old
logger.warning('Reinforcement learning module didnt find the correct raw prices '
'assigned in feature_engineering_standard(). '
'Please assign them with:\n'
'dataframe["%-raw_close"] = dataframe["close"]\n'
'dataframe["%-raw_open"] = dataframe["open"]\n'
'dataframe["%-raw_high"] = dataframe["high"]\n'
'dataframe["%-raw_low"] = dataframe["low"]\n'
'inside `feature_engineering_standard()')
elif prices_train.empty:
raise OperationalException("No prices found, please follow log warning "
"instructions to correct the strategy.")
prices_train = train_df.filter(ohlc_list, axis=1)
if prices_train.empty:
raise OperationalException('Reinforcement learning module didnt find the raw prices '
'assigned in populate_any_indicators. Please assign them '
'with:\n'
'informative[f"%-{pair}raw_close"] = informative["close"]\n'
'informative[f"%-{pair}raw_open"] = informative["open"]\n'
'informative[f"%-{pair}raw_high"] = informative["high"]\n'
'informative[f"%-{pair}raw_low"] = informative["low"]\n')
prices_train.rename(columns=rename_dict, inplace=True) prices_train.rename(columns=rename_dict, inplace=True)
prices_train.reset_index(drop=True) prices_train.reset_index(drop=True)
prices_test = test_df.filter(rename_dict.keys(), axis=1) prices_test = test_df.filter(ohlc_list, axis=1)
prices_test.rename(columns=rename_dict, inplace=True) prices_test.rename(columns=rename_dict, inplace=True)
prices_test.reset_index(drop=True) prices_test.reset_index(drop=True)

View File

@@ -2,6 +2,8 @@ import logging
from time import time from time import time
from typing import Any from typing import Any
import numpy as np
import tensorflow as tf
from pandas import DataFrame from pandas import DataFrame
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
@@ -17,6 +19,14 @@ class BaseTensorFlowModel(IFreqaiModel):
User *must* inherit from this class and set fit() and predict(). User *must* inherit from this class and set fit() and predict().
""" """
def __init__(self, **kwargs):
super().__init__(config=kwargs['config'])
self.keras = True
# if self.ft_params.get("DI_threshold", 0):
# self.ft_params["DI_threshold"] = 0
# logger.warning("DI threshold is not configured for Keras models yet. Deactivating.")
self.dd.model_type = 'keras'
def train( def train(
self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs
) -> Any: ) -> Any:
@@ -33,7 +43,6 @@ class BaseTensorFlowModel(IFreqaiModel):
start_time = time() start_time = time()
# filter the features requested by user in the configuration file and elegantly handle NaNs
features_filtered, labels_filtered = dk.filter_features( features_filtered, labels_filtered = dk.filter_features(
unfiltered_df, unfiltered_df,
dk.training_features_list, dk.training_features_list,
@@ -41,13 +50,9 @@ class BaseTensorFlowModel(IFreqaiModel):
training_filter=True, training_filter=True,
) )
start_date = unfiltered_df["date"].iloc[0].strftime("%Y-%m-%d")
end_date = unfiltered_df["date"].iloc[-1].strftime("%Y-%m-%d")
logger.info(f"-------------------- Training on data from {start_date} to "
f"{end_date} --------------------")
# split data into train/test data. # split data into train/test data.
data_dictionary = dk.make_train_test_datasets(features_filtered, labels_filtered) data_dictionary = dk.make_train_test_datasets(features_filtered, labels_filtered)
if not self.freqai_info.get("fit_live_predictions_candles", 0) or not self.live: if not self.freqai_info.get("fit_live_predictions", 0) or not self.live:
dk.fit_labels() dk.fit_labels()
# normalize all data based on train_dataset only # normalize all data based on train_dataset only
data_dictionary = dk.normalize_data(data_dictionary) data_dictionary = dk.normalize_data(data_dictionary)
@@ -68,3 +73,76 @@ class BaseTensorFlowModel(IFreqaiModel):
f"({end_time - start_time:.2f} secs) --------------------") f"({end_time - start_time:.2f} secs) --------------------")
return model return model
class WindowGenerator:
def __init__(
self,
input_width,
label_width,
shift,
train_df=None,
val_df=None,
test_df=None,
train_labels=None,
val_labels=None,
test_labels=None,
batch_size=None,
):
# Store the raw data.
self.train_df = train_df
self.val_df = val_df
self.test_df = test_df
self.train_labels = train_labels
self.val_labels = val_labels
self.test_labels = test_labels
self.batch_size = batch_size
self.input_width = input_width
self.label_width = label_width
self.shift = shift
self.total_window_size = input_width + shift
self.input_slice = slice(0, input_width)
self.input_indices = np.arange(self.total_window_size)[self.input_slice]
def make_dataset(self, data, labels=None):
data = np.array(data, dtype=np.float32)
if labels is not None:
labels = np.array(labels, dtype=np.float32)
ds = tf.keras.preprocessing.timeseries_dataset_from_array(
data=data,
targets=labels,
sequence_length=self.total_window_size,
sequence_stride=1,
sampling_rate=1,
shuffle=False,
batch_size=self.batch_size,
)
return ds
@property
def train(self):
return self.make_dataset(self.train_df, self.train_labels)
@property
def val(self):
return self.make_dataset(self.val_df, self.val_labels)
@property
def test(self):
return self.make_dataset(self.test_df, self.test_labels)
@property
def inference(self):
return self.make_dataset(self.test_df)
@property
def example(self):
"""Get and cache an example batch of `inputs, labels` for plotting."""
result = getattr(self, "_example", None)
if result is None:
# No example batch was found, so get one from the `.train` dataset
result = next(iter(self.train))
# And cache it for next time
self._example = result
return result

View File

@@ -1,11 +1,10 @@
import copy import copy
import inspect
import logging import logging
import shutil import shutil
from datetime import datetime, timezone from datetime import datetime, timezone
from math import cos, sin from math import cos, sin
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Tuple
import numpy as np import numpy as np
import numpy.typing as npt import numpy.typing as npt
@@ -24,7 +23,6 @@ from freqtrade.constants import Config
from freqtrade.data.converter import reduce_dataframe_footprint from freqtrade.data.converter import reduce_dataframe_footprint
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_seconds from freqtrade.exchange import timeframe_to_seconds
from freqtrade.strategy import merge_informative_pair
from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.interface import IStrategy
@@ -112,7 +110,7 @@ class FreqaiDataKitchen:
def set_paths( def set_paths(
self, self,
pair: str, pair: str,
trained_timestamp: Optional[int] = None, trained_timestamp: int = None,
) -> None: ) -> None:
""" """
Set the paths to the data for the present coin/botloop Set the paths to the data for the present coin/botloop
@@ -1147,9 +1145,9 @@ class FreqaiDataKitchen:
for pair in pairs: for pair in pairs:
pair = pair.replace(':', '') # lightgbm doesnt like colons pair = pair.replace(':', '') # lightgbm doesnt like colons
pair_cols = [col for col in dataframe.columns if col.startswith("%") valid_strs = [f"%-{pair}", f"%{pair}", f"%_{pair}"]
and f"{pair}_" in col] pair_cols = [col for col in dataframe.columns if
any(substr in col for substr in valid_strs)]
if pair_cols: if pair_cols:
pair_cols.insert(0, 'date') pair_cols.insert(0, 'date')
corr_dataframes[pair] = dataframe.filter(pair_cols, axis=1) corr_dataframes[pair] = dataframe.filter(pair_cols, axis=1)
@@ -1178,103 +1176,6 @@ class FreqaiDataKitchen:
return dataframe return dataframe
def get_pair_data_for_features(self,
pair: str,
tf: str,
strategy: IStrategy,
corr_dataframes: dict = {},
base_dataframes: dict = {},
is_corr_pairs: bool = False) -> DataFrame:
"""
Get the data for the pair. If it's not in the dictionary, get it from the data provider
:param pair: str = pair to get data for
:param tf: str = timeframe to get data for
:param strategy: IStrategy = user defined strategy object
:param corr_dataframes: dict = dict containing the df pair dataframes
(for user defined timeframes)
:param base_dataframes: dict = dict containing the current pair dataframes
(for user defined timeframes)
:param is_corr_pairs: bool = whether the pair is a corr pair or not
:return: dataframe = dataframe containing the pair data
"""
if is_corr_pairs:
dataframe = corr_dataframes[pair][tf]
if not dataframe.empty:
return dataframe
else:
dataframe = strategy.dp.get_pair_dataframe(pair=pair, timeframe=tf)
return dataframe
else:
dataframe = base_dataframes[tf]
if not dataframe.empty:
return dataframe
else:
dataframe = strategy.dp.get_pair_dataframe(pair=pair, timeframe=tf)
return dataframe
def merge_features(self, df_main: DataFrame, df_to_merge: DataFrame,
tf: str, timeframe_inf: str, suffix: str) -> DataFrame:
"""
Merge the features of the dataframe and remove HLCV and date added columns
:param df_main: DataFrame = main dataframe
:param df_to_merge: DataFrame = dataframe to merge
:param tf: str = timeframe of the main dataframe
:param timeframe_inf: str = timeframe of the dataframe to merge
:param suffix: str = suffix to add to the columns of the dataframe to merge
:return: dataframe = merged dataframe
"""
dataframe = merge_informative_pair(df_main, df_to_merge, tf, timeframe_inf=timeframe_inf,
append_timeframe=False, suffix=suffix, ffill=True)
skip_columns = [
(f"{s}_{suffix}") for s in ["date", "open", "high", "low", "close", "volume"]
]
dataframe = dataframe.drop(columns=skip_columns)
return dataframe
def populate_features(self, dataframe: DataFrame, pair: str, strategy: IStrategy,
corr_dataframes: dict, base_dataframes: dict,
is_corr_pairs: bool = False) -> DataFrame:
"""
Use the user defined strategy functions for populating features
:param dataframe: DataFrame = dataframe to populate
:param pair: str = pair to populate
:param strategy: IStrategy = user defined strategy object
:param corr_dataframes: dict = dict containing the df pair dataframes
:param base_dataframes: dict = dict containing the current pair dataframes
:param is_corr_pairs: bool = whether the pair is a corr pair or not
:return: dataframe = populated dataframe
"""
tfs: List[str] = self.freqai_config["feature_parameters"].get("include_timeframes")
for tf in tfs:
informative_df = self.get_pair_data_for_features(
pair, tf, strategy, corr_dataframes, base_dataframes, is_corr_pairs)
informative_copy = informative_df.copy()
for t in self.freqai_config["feature_parameters"]["indicator_periods_candles"]:
df_features = strategy.feature_engineering_expand_all(
informative_copy.copy(), t)
suffix = f"{t}"
informative_df = self.merge_features(informative_df, df_features, tf, tf, suffix)
generic_df = strategy.feature_engineering_expand_basic(informative_copy.copy())
suffix = "gen"
informative_df = self.merge_features(informative_df, generic_df, tf, tf, suffix)
indicators = [col for col in informative_df if col.startswith("%")]
for n in range(self.freqai_config["feature_parameters"]["include_shifted_candles"] + 1):
if n == 0:
continue
df_shift = informative_df[indicators].shift(n)
df_shift = df_shift.add_suffix("_shift-" + str(n))
informative_df = pd.concat((informative_df, df_shift), axis=1)
dataframe = self.merge_features(dataframe.copy(), informative_df,
self.config["timeframe"], tf, f'{pair}_{tf}')
return dataframe
def use_strategy_to_populate_indicators( def use_strategy_to_populate_indicators(
self, self,
strategy: IStrategy, strategy: IStrategy,
@@ -1287,87 +1188,7 @@ class FreqaiDataKitchen:
""" """
Use the user defined strategy for populating indicators during retrain Use the user defined strategy for populating indicators during retrain
:param strategy: IStrategy = user defined strategy object :param strategy: IStrategy = user defined strategy object
:param corr_dataframes: dict = dict containing the df pair dataframes :param corr_dataframes: dict = dict containing the informative pair dataframes
(for user defined timeframes)
:param base_dataframes: dict = dict containing the current pair dataframes
(for user defined timeframes)
:param pair: str = pair to populate
:param prediction_dataframe: DataFrame = dataframe containing the pair data
used for prediction
:param do_corr_pairs: bool = whether to populate corr pairs or not
:return:
dataframe: DataFrame = dataframe containing populated indicators
"""
# this is a hack to check if the user is using the populate_any_indicators function
new_version = inspect.getsource(strategy.populate_any_indicators) == (
inspect.getsource(IStrategy.populate_any_indicators))
if new_version:
tfs: List[str] = self.freqai_config["feature_parameters"].get("include_timeframes")
pairs: List[str] = self.freqai_config["feature_parameters"].get(
"include_corr_pairlist", [])
for tf in tfs:
if tf not in base_dataframes:
base_dataframes[tf] = pd.DataFrame()
for p in pairs:
if p not in corr_dataframes:
corr_dataframes[p] = {}
if tf not in corr_dataframes[p]:
corr_dataframes[p][tf] = pd.DataFrame()
if not prediction_dataframe.empty:
dataframe = prediction_dataframe.copy()
else:
dataframe = base_dataframes[self.config["timeframe"]].copy()
corr_pairs: List[str] = self.freqai_config["feature_parameters"].get(
"include_corr_pairlist", [])
dataframe = self.populate_features(dataframe.copy(), pair, strategy,
corr_dataframes, base_dataframes)
dataframe = strategy.feature_engineering_standard(dataframe.copy())
# ensure corr pairs are always last
for corr_pair in corr_pairs:
if pair == corr_pair:
continue # dont repeat anything from whitelist
if corr_pairs and do_corr_pairs:
dataframe = self.populate_features(dataframe.copy(), corr_pair, strategy,
corr_dataframes, base_dataframes, True)
dataframe = strategy.set_freqai_targets(dataframe.copy())
self.get_unique_classes_from_labels(dataframe)
dataframe = self.remove_special_chars_from_feature_names(dataframe)
if self.config.get('reduce_df_footprint', False):
dataframe = reduce_dataframe_footprint(dataframe)
return dataframe
else:
# the user is using the populate_any_indicators functions which is deprecated
df = self.use_strategy_to_populate_indicators_old_version(
strategy, corr_dataframes, base_dataframes, pair,
prediction_dataframe, do_corr_pairs)
return df
def use_strategy_to_populate_indicators_old_version(
self,
strategy: IStrategy,
corr_dataframes: dict = {},
base_dataframes: dict = {},
pair: str = "",
prediction_dataframe: DataFrame = pd.DataFrame(),
do_corr_pairs: bool = True,
) -> DataFrame:
"""
Use the user defined strategy for populating indicators during retrain
:param strategy: IStrategy = user defined strategy object
:param corr_dataframes: dict = dict containing the df pair dataframes
(for user defined timeframes) (for user defined timeframes)
:param base_dataframes: dict = dict containing the current pair dataframes :param base_dataframes: dict = dict containing the current pair dataframes
(for user defined timeframes) (for user defined timeframes)

View File

@@ -1,4 +1,3 @@
import inspect
import logging import logging
import threading import threading
import time import time
@@ -105,9 +104,6 @@ class IFreqaiModel(ABC):
self.metadata: Dict[str, Any] = self.dd.load_global_metadata_from_disk() self.metadata: Dict[str, Any] = self.dd.load_global_metadata_from_disk()
self.data_provider: Optional[DataProvider] = None self.data_provider: Optional[DataProvider] = None
self.max_system_threads = max(int(psutil.cpu_count() * 2 - 2), 1) self.max_system_threads = max(int(psutil.cpu_count() * 2 - 2), 1)
self.can_short = True # overridden in start() with strategy.can_short
self.warned_deprecated_populate_any_indicators = False
record_params(config, self.full_path) record_params(config, self.full_path)
@@ -137,10 +133,6 @@ class IFreqaiModel(ABC):
self.live = strategy.dp.runmode in (RunMode.DRY_RUN, RunMode.LIVE) self.live = strategy.dp.runmode in (RunMode.DRY_RUN, RunMode.LIVE)
self.dd.set_pair_dict_info(metadata) self.dd.set_pair_dict_info(metadata)
self.data_provider = strategy.dp self.data_provider = strategy.dp
self.can_short = strategy.can_short
# check if the strategy has deprecated populate_any_indicators function
self.check_deprecated_populate_any_indicators(strategy)
if self.live: if self.live:
self.inference_timer('start') self.inference_timer('start')
@@ -155,9 +147,12 @@ class IFreqaiModel(ABC):
# the concatenated results for the full backtesting period back to the strategy. # the concatenated results for the full backtesting period back to the strategy.
elif not self.follow_mode: elif not self.follow_mode:
self.dk = FreqaiDataKitchen(self.config, self.live, metadata["pair"]) self.dk = FreqaiDataKitchen(self.config, self.live, metadata["pair"])
dataframe = self.dk.use_strategy_to_populate_indicators(
strategy, prediction_dataframe=dataframe, pair=metadata["pair"]
)
if not self.config.get("freqai_backtest_live_models", False): if not self.config.get("freqai_backtest_live_models", False):
logger.info(f"Training {len(self.dk.training_timeranges)} timeranges") logger.info(f"Training {len(self.dk.training_timeranges)} timeranges")
dk = self.start_backtesting(dataframe, metadata, self.dk, strategy) dk = self.start_backtesting(dataframe, metadata, self.dk)
dataframe = dk.remove_features_from_df(dk.return_dataframe) dataframe = dk.remove_features_from_df(dk.return_dataframe)
else: else:
logger.info( logger.info(
@@ -258,7 +253,7 @@ class IFreqaiModel(ABC):
self.dd.save_metric_tracker_to_disk() self.dd.save_metric_tracker_to_disk()
def start_backtesting( def start_backtesting(
self, dataframe: DataFrame, metadata: dict, dk: FreqaiDataKitchen, strategy: IStrategy self, dataframe: DataFrame, metadata: dict, dk: FreqaiDataKitchen
) -> FreqaiDataKitchen: ) -> FreqaiDataKitchen:
""" """
The main broad execution for backtesting. For backtesting, each pair enters and then gets The main broad execution for backtesting. For backtesting, each pair enters and then gets
@@ -270,22 +265,19 @@ class IFreqaiModel(ABC):
:param dataframe: DataFrame = strategy passed dataframe :param dataframe: DataFrame = strategy passed dataframe
:param metadata: Dict = pair metadata :param metadata: Dict = pair metadata
:param dk: FreqaiDataKitchen = Data management/analysis tool associated to present pair only :param dk: FreqaiDataKitchen = Data management/analysis tool associated to present pair only
:param strategy: Strategy to train on
:return: :return:
FreqaiDataKitchen = Data management/analysis tool associated to present pair only FreqaiDataKitchen = Data management/analysis tool associated to present pair only
""" """
self.pair_it += 1 self.pair_it += 1
train_it = 0 train_it = 0
pair = metadata["pair"]
populate_indicators = True
check_features = True
# Loop enforcing the sliding window training/backtesting paradigm # Loop enforcing the sliding window training/backtesting paradigm
# tr_train is the training time range e.g. 1 historical month # tr_train is the training time range e.g. 1 historical month
# tr_backtest is the backtesting time range e.g. the week directly # tr_backtest is the backtesting time range e.g. the week directly
# following tr_train. Both of these windows slide through the # following tr_train. Both of these windows slide through the
# entire backtest # entire backtest
for tr_train, tr_backtest in zip(dk.training_timeranges, dk.backtesting_timeranges): for tr_train, tr_backtest in zip(dk.training_timeranges, dk.backtesting_timeranges):
pair = metadata["pair"]
(_, _, _) = self.dd.get_pair_dict_info(pair) (_, _, _) = self.dd.get_pair_dict_info(pair)
train_it += 1 train_it += 1
total_trains = len(dk.backtesting_timeranges) total_trains = len(dk.backtesting_timeranges)
@@ -307,42 +299,18 @@ class IFreqaiModel(ABC):
dk.set_new_model_names(pair, timestamp_model_id) dk.set_new_model_names(pair, timestamp_model_id)
if dk.check_if_backtest_prediction_is_valid(len_backtest_df): if dk.check_if_backtest_prediction_is_valid(len_backtest_df):
if check_features: self.dd.load_metadata(dk)
self.dd.load_metadata(dk) dk.find_features(dataframe)
dataframe_dummy_features = self.dk.use_strategy_to_populate_indicators( self.check_if_feature_list_matches_strategy(dk)
strategy, prediction_dataframe=dataframe.tail(1), pair=metadata["pair"]
)
dk.find_features(dataframe_dummy_features)
self.check_if_feature_list_matches_strategy(dk)
check_features = False
append_df = dk.get_backtesting_prediction() append_df = dk.get_backtesting_prediction()
dk.append_predictions(append_df) dk.append_predictions(append_df)
else: else:
if populate_indicators: dataframe_train = dk.slice_dataframe(tr_train, dataframe)
dataframe = self.dk.use_strategy_to_populate_indicators( dataframe_backtest = dk.slice_dataframe(tr_backtest, dataframe)
strategy, prediction_dataframe=dataframe, pair=metadata["pair"]
)
populate_indicators = False
dataframe_base_train = dataframe.loc[dataframe["date"] < tr_train.stopdt, :]
dataframe_base_train = strategy.set_freqai_targets(dataframe_base_train)
dataframe_base_backtest = dataframe.loc[dataframe["date"] < tr_backtest.stopdt, :]
dataframe_base_backtest = strategy.set_freqai_targets(dataframe_base_backtest)
dataframe_train = dk.slice_dataframe(tr_train, dataframe_base_train)
dataframe_backtest = dk.slice_dataframe(tr_backtest, dataframe_base_backtest)
if not self.model_exists(dk): if not self.model_exists(dk):
dk.find_features(dataframe_train) dk.find_features(dataframe_train)
dk.find_labels(dataframe_train) dk.find_labels(dataframe_train)
self.model = self.train(dataframe_train, pair, dk)
try:
self.model = self.train(dataframe_train, pair, dk)
except Exception as msg:
logger.warning(
f"Training {pair} raised exception {msg.__class__.__name__}. "
f"Message: {msg}, skipping.")
self.dd.pair_dict[pair]["trained_timestamp"] = int( self.dd.pair_dict[pair]["trained_timestamp"] = int(
tr_train.stopts) tr_train.stopts)
if self.plot_features: if self.plot_features:
@@ -379,6 +347,7 @@ class IFreqaiModel(ABC):
:returns: :returns:
dk: FreqaiDataKitchen = Data management/analysis tool associated to present pair only dk: FreqaiDataKitchen = Data management/analysis tool associated to present pair only
""" """
# update follower # update follower
if self.follow_mode: if self.follow_mode:
self.dd.update_follower_metadata() self.dd.update_follower_metadata()
@@ -942,28 +911,9 @@ class IFreqaiModel(ABC):
dk.return_dataframe = dk.return_dataframe.drop(columns=list(columns_to_drop)) dk.return_dataframe = dk.return_dataframe.drop(columns=list(columns_to_drop))
dk.return_dataframe = pd.merge( dk.return_dataframe = pd.merge(
dk.return_dataframe, saved_dataframe, how='left', left_on='date', right_on="date_pred") dk.return_dataframe, saved_dataframe, how='left', left_on='date', right_on="date_pred")
# dk.return_dataframe = dk.return_dataframe[saved_dataframe.columns].fillna(0)
return dk return dk
def check_deprecated_populate_any_indicators(self, strategy: IStrategy):
"""
Check and warn if the deprecated populate_any_indicators function is used.
:param strategy: strategy object
"""
if not self.warned_deprecated_populate_any_indicators:
self.warned_deprecated_populate_any_indicators = True
old_version = inspect.getsource(strategy.populate_any_indicators) != (
inspect.getsource(IStrategy.populate_any_indicators))
if old_version:
logger.warning("DEPRECATION WARNING: "
"You are using the deprecated populate_any_indicators function. "
"This function will raise an error on March 1 2023. "
"Please update your strategy by using "
"the new feature_engineering functions. See \n"
"https://www.freqtrade.io/en/latest/freqai-feature-engineering/"
"for details.")
# Following methods which are overridden by user made prediction models. # Following methods which are overridden by user made prediction models.
# See freqai/prediction_models/CatboostPredictionModel.py for an example. # See freqai/prediction_models/CatboostPredictionModel.py for an example.

View File

@@ -0,0 +1,152 @@
import logging
from typing import Any, Dict, Tuple
import numpy as np
import tensorflow as tf
from pandas import DataFrame
from tensorflow.keras.layers import Conv1D, Dense, Input
from tensorflow.keras.models import Model
from freqtrade.exceptions import OperationalException
from freqtrade.freqai.base_models.BaseTensorFlowModel import BaseTensorFlowModel, WindowGenerator
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
logger = logging.getLogger(__name__)
class CNNPredictionModel(BaseTensorFlowModel):
"""
User created prediction model. The class needs to override three necessary
functions, predict(), fit().
"""
def fit(self, data_dictionary: Dict[str, Any], dk: FreqaiDataKitchen) -> Any:
"""
User sets up the training and test data to fit their desired model here
:params:
:data_dictionary: the dictionary constructed by DataHandler to hold
all the training and test data/labels.
"""
train_df = data_dictionary["train_features"]
train_labels = data_dictionary["train_labels"]
test_df = data_dictionary["test_features"]
test_labels = data_dictionary["test_labels"]
n_labels = len(train_labels.columns)
if n_labels > 1:
raise OperationalException(
"Neural Net not yet configured for multi-targets. Please "
" reduce number of targets to 1 in strategy."
)
n_features = len(data_dictionary["train_features"].columns)
BATCH_SIZE = self.model_training_parameters.get("batch_size", 64)
# we need to remove batch_size from the model_training_params because
# we dont want fit() to get the incorrect assignment (we use the WindowGenerator)
# to handle our batches.
if 'batch_size' in self.model_training_parameters:
self.model_training_parameters.pop('batch_size')
input_dims = [BATCH_SIZE, self.CONV_WIDTH, n_features]
w1 = WindowGenerator(
input_width=self.CONV_WIDTH,
label_width=1,
shift=1,
train_df=train_df,
val_df=test_df,
train_labels=train_labels,
val_labels=test_labels,
batch_size=BATCH_SIZE,
)
model = self.create_model(input_dims, n_labels)
steps_per_epoch = np.ceil(len(test_df) / BATCH_SIZE)
lr_schedule = tf.keras.optimizers.schedules.InverseTimeDecay(
0.001, decay_steps=steps_per_epoch * 1000, decay_rate=1, staircase=False
)
early_stopping = tf.keras.callbacks.EarlyStopping(
monitor="loss", patience=3, mode="min", min_delta=0.0001
)
model.compile(
loss=tf.losses.MeanSquaredError(),
optimizer=tf.optimizers.Adam(lr_schedule),
metrics=[tf.metrics.MeanAbsoluteError()],
)
if self.freqai_info.get('data_split_parameters', {}).get('test_size', 0.1) == 0:
val_data = None
else:
val_data = w1.val
model.fit(
w1.train,
validation_data=val_data,
callbacks=[early_stopping],
**self.model_training_parameters,
)
return model
def predict(
self, unfiltered_dataframe: DataFrame, dk: FreqaiDataKitchen, first=True
) -> Tuple[DataFrame, DataFrame]:
"""
Filter the prediction features data and predict with it.
:param: unfiltered_dataframe: Full dataframe for the current backtest period.
:return:
:predictions: np.array of predictions
:do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove
data (NaNs) or felt uncertain about data (PCA and DI index)
"""
dk.find_features(unfiltered_dataframe)
filtered_dataframe, _ = dk.filter_features(
unfiltered_dataframe, dk.training_features_list, training_filter=False
)
filtered_dataframe = dk.normalize_data_from_metadata(filtered_dataframe)
dk.data_dictionary["prediction_features"] = filtered_dataframe
# optional additional data cleaning/analysis
self.data_cleaning_predict(dk)
if first:
full_df = dk.data_dictionary["prediction_features"]
w1 = WindowGenerator(
input_width=self.CONV_WIDTH,
label_width=1,
shift=1,
test_df=full_df,
batch_size=len(full_df),
)
predictions = self.model.predict(w1.inference)
len_diff = len(dk.do_predict) - len(predictions)
if len_diff > 0:
dk.do_predict = dk.do_predict[len_diff:]
else:
data = dk.data_dictionary["prediction_features"]
data = tf.expand_dims(data, axis=0)
data = tf.convert_to_tensor(data)
predictions = self.model(data, training=False)
predictions = predictions[:, 0, 0]
pred_df = DataFrame(predictions, columns=dk.label_list)
pred_df = dk.denormalize_labels_from_metadata(pred_df)
return (pred_df, np.ones(len(pred_df)))
def create_model(self, input_dims, n_labels) -> Any:
input_layer = Input(shape=(input_dims[1], input_dims[2]))
Layer_1 = Conv1D(filters=32, kernel_size=(self.CONV_WIDTH,), activation="relu")(input_layer)
Layer_3 = Dense(units=32, activation="relu")(Layer_1)
output_layer = Dense(units=n_labels)(Layer_3)
return Model(inputs=input_layer, outputs=output_layer)

View File

@@ -33,7 +33,6 @@ from freqtrade.rpc.external_message_consumer import ExternalMessageConsumer
from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.interface import IStrategy
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from freqtrade.util import FtPrecise from freqtrade.util import FtPrecise
from freqtrade.util.binance_mig import migrate_binance_futures_names
from freqtrade.wallets import Wallets from freqtrade.wallets import Wallets
@@ -178,8 +177,6 @@ class FreqtradeBot(LoggingMixin):
Called on startup and after reloading the bot - triggers notifications and Called on startup and after reloading the bot - triggers notifications and
performs startup tasks performs startup tasks
""" """
migrate_binance_futures_names(self.config)
self.rpc.startup_messages(self.config, self.pairlists, self.protections) self.rpc.startup_messages(self.config, self.pairlists, self.protections)
# Update older trades with precision and precision mode # Update older trades with precision and precision mode
self.startup_backpopulate_precision() self.startup_backpopulate_precision()
@@ -377,7 +374,7 @@ class FreqtradeBot(LoggingMixin):
for trade in trades: for trade in trades:
if not trade.is_open and not trade.fee_updated(trade.exit_side): if not trade.is_open and not trade.fee_updated(trade.exit_side):
# Get sell fee # Get sell fee
order = trade.select_order(trade.exit_side, False, only_filled=True) order = trade.select_order(trade.exit_side, False)
if not order: if not order:
order = trade.select_order('stoploss', False) order = trade.select_order('stoploss', False)
if order: if order:
@@ -393,7 +390,7 @@ class FreqtradeBot(LoggingMixin):
for trade in trades: for trade in trades:
with self._exit_lock: with self._exit_lock:
if trade.is_open and not trade.fee_updated(trade.entry_side): if trade.is_open and not trade.fee_updated(trade.entry_side):
order = trade.select_order(trade.entry_side, False, only_filled=True) order = trade.select_order(trade.entry_side, False)
open_order = trade.select_order(trade.entry_side, True) open_order = trade.select_order(trade.entry_side, True)
if order and open_order is None: if order and open_order is None:
logger.info( logger.info(
@@ -723,7 +720,7 @@ class FreqtradeBot(LoggingMixin):
time_in_force=time_in_force, time_in_force=time_in_force,
leverage=leverage leverage=leverage
) )
order_obj = Order.parse_from_ccxt_object(order, pair, side, amount, enter_limit_requested) order_obj = Order.parse_from_ccxt_object(order, pair, side)
order_id = order['id'] order_id = order['id']
order_status = order.get('status') order_status = order.get('status')
logger.info(f"Order #{order_id} was created for {pair} and status is {order_status}.") logger.info(f"Order #{order_id} was created for {pair} and status is {order_status}.")
@@ -915,7 +912,6 @@ class FreqtradeBot(LoggingMixin):
stake_amount=stake_amount, stake_amount=stake_amount,
min_stake_amount=min_stake_amount, min_stake_amount=min_stake_amount,
max_stake_amount=max_stake_amount, max_stake_amount=max_stake_amount,
trade_amount=trade.stake_amount if trade else None,
) )
return enter_limit_requested, stake_amount, leverage return enter_limit_requested, stake_amount, leverage
@@ -1097,8 +1093,7 @@ class FreqtradeBot(LoggingMixin):
leverage=trade.leverage leverage=trade.leverage
) )
order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair, 'stoploss', order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair, 'stoploss')
trade.amount, stop_price)
trade.orders.append(order_obj) trade.orders.append(order_obj)
trade.stoploss_order_id = str(stoploss_order['id']) trade.stoploss_order_id = str(stoploss_order['id'])
trade.stoploss_last_update = datetime.now(timezone.utc) trade.stoploss_last_update = datetime.now(timezone.utc)
@@ -1522,7 +1517,7 @@ class FreqtradeBot(LoggingMixin):
*, *,
exit_tag: Optional[str] = None, exit_tag: Optional[str] = None,
ordertype: Optional[str] = None, ordertype: Optional[str] = None,
sub_trade_amt: Optional[float] = None, sub_trade_amt: float = None,
) -> bool: ) -> bool:
""" """
Executes a trade exit for the given trade and limit Executes a trade exit for the given trade and limit
@@ -1599,7 +1594,7 @@ class FreqtradeBot(LoggingMixin):
self.handle_insufficient_funds(trade) self.handle_insufficient_funds(trade)
return False return False
order_obj = Order.parse_from_ccxt_object(order, trade.pair, trade.exit_side, amount, limit) order_obj = Order.parse_from_ccxt_object(order, trade.pair, trade.exit_side)
trade.orders.append(order_obj) trade.orders.append(order_obj)
trade.open_order_id = order['id'] trade.open_order_id = order['id']
@@ -1616,7 +1611,7 @@ class FreqtradeBot(LoggingMixin):
return True return True
def _notify_exit(self, trade: Trade, order_type: str, fill: bool = False, def _notify_exit(self, trade: Trade, order_type: str, fill: bool = False,
sub_trade: bool = False, order: Optional[Order] = None) -> None: sub_trade: bool = False, order: Order = None) -> None:
""" """
Sends rpc notification when a sell occurred. Sends rpc notification when a sell occurred.
""" """
@@ -1729,9 +1724,8 @@ class FreqtradeBot(LoggingMixin):
# Common update trade state methods # Common update trade state methods
# #
def update_trade_state( def update_trade_state(self, trade: Trade, order_id: str, action_order: Dict[str, Any] = None,
self, trade: Trade, order_id: str, action_order: Optional[Dict[str, Any]] = None, stoploss_order: bool = False, send_msg: bool = True) -> bool:
stoploss_order: bool = False, send_msg: bool = True) -> bool:
""" """
Checks trades with open orders and updates the amount if necessary Checks trades with open orders and updates the amount if necessary
Handles closing both buy and sell orders. Handles closing both buy and sell orders.

View File

@@ -5,7 +5,7 @@ Read the documentation to know what cli arguments you need.
""" """
import logging import logging
import sys import sys
from typing import Any, List, Optional from typing import Any, List
from freqtrade.util.gc_setup import gc_set_threshold from freqtrade.util.gc_setup import gc_set_threshold
@@ -23,7 +23,7 @@ from freqtrade.loggers import setup_logging_pre
logger = logging.getLogger('freqtrade') logger = logging.getLogger('freqtrade')
def main(sysargv: Optional[List[str]] = None) -> None: def main(sysargv: List[str] = None) -> None:
""" """
This function will initiate the bot and start the trading loop. This function will initiate the bot and start the trading loop.
:return: None :return: None

View File

@@ -6,7 +6,7 @@ import logging
import re import re
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Iterator, List, Mapping, Optional, Union from typing import Any, Dict, Iterator, List, Mapping, Union
from typing.io import IO from typing.io import IO
from urllib.parse import urlparse from urllib.parse import urlparse
@@ -205,7 +205,7 @@ def safe_value_fallback2(dict1: dictMap, dict2: dictMap, key1: str, key2: str, d
return default_value return default_value
def plural(num: float, singular: str, plural: Optional[str] = None) -> str: def plural(num: float, singular: str, plural: str = None) -> str:
return singular if (num == 1 or num == -1) else plural or singular + 's' return singular if (num == 1 or num == -1) else plural or singular + 's'
@@ -269,8 +269,6 @@ def dataframe_to_json(dataframe: pd.DataFrame) -> str:
def default(z): def default(z):
if isinstance(z, pd.Timestamp): if isinstance(z, pd.Timestamp):
return z.timestamp() * 1e3 return z.timestamp() * 1e3
if z is pd.NaT:
return 'NaT'
raise TypeError raise TypeError
return str(orjson.dumps(dataframe.to_dict(orient='split'), default=default), 'utf-8') return str(orjson.dumps(dataframe.to_dict(orient='split'), default=default), 'utf-8')

View File

@@ -15,7 +15,7 @@ from pandas import DataFrame
from freqtrade import constants from freqtrade import constants
from freqtrade.configuration import TimeRange, validate_config_consistency from freqtrade.configuration import TimeRange, validate_config_consistency
from freqtrade.constants import DATETIME_PRINT_FORMAT, Config, IntOrInf, LongShort from freqtrade.constants import DATETIME_PRINT_FORMAT, Config, LongShort
from freqtrade.data import history from freqtrade.data import history
from freqtrade.data.btanalysis import find_existing_backtest_stats, trade_list_to_dataframe from freqtrade.data.btanalysis import find_existing_backtest_stats, trade_list_to_dataframe
from freqtrade.data.converter import trim_dataframe, trim_dataframes from freqtrade.data.converter import trim_dataframe, trim_dataframes
@@ -37,7 +37,6 @@ from freqtrade.plugins.protectionmanager import ProtectionManager
from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.interface import IStrategy
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from freqtrade.util.binance_mig import migrate_binance_futures_data
from freqtrade.wallets import Wallets from freqtrade.wallets import Wallets
@@ -158,7 +157,6 @@ class Backtesting:
self._can_short = self.trading_mode != TradingMode.SPOT self._can_short = self.trading_mode != TradingMode.SPOT
self._position_stacking: bool = self.config.get('position_stacking', False) self._position_stacking: bool = self.config.get('position_stacking', False)
self.enable_protections: bool = self.config.get('enable_protections', False) self.enable_protections: bool = self.config.get('enable_protections', False)
migrate_binance_futures_data(config)
self.init_backtest() self.init_backtest()
@@ -575,6 +573,26 @@ class Backtesting:
""" Rate is within candle, therefore filled""" """ Rate is within candle, therefore filled"""
return row[LOW_IDX] <= rate <= row[HIGH_IDX] return row[LOW_IDX] <= rate <= row[HIGH_IDX]
def _get_exit_trade_entry_for_candle(self, trade: LocalTrade,
row: Tuple) -> Optional[LocalTrade]:
# Check if we need to adjust our current positions
if self.strategy.position_adjustment_enable:
trade = self._get_adjust_trade_entry_for_candle(trade, row)
enter = row[SHORT_IDX] if trade.is_short else row[LONG_IDX]
exit_sig = row[ESHORT_IDX] if trade.is_short else row[ELONG_IDX]
exits = self.strategy.should_exit(
trade, row[OPEN_IDX], row[DATE_IDX].to_pydatetime(), # type: ignore
enter=enter, exit_=exit_sig,
low=row[LOW_IDX], high=row[HIGH_IDX]
)
for exit_ in exits:
t = self._get_exit_for_signal(trade, row, exit_)
if t:
return t
return None
def _get_exit_for_signal( def _get_exit_for_signal(
self, trade: LocalTrade, row: Tuple, exit_: ExitCheckTuple, self, trade: LocalTrade, row: Tuple, exit_: ExitCheckTuple,
amount: Optional[float] = None) -> Optional[LocalTrade]: amount: Optional[float] = None) -> Optional[LocalTrade]:
@@ -644,7 +662,7 @@ class Backtesting:
return None return None
def _exit_trade(self, trade: LocalTrade, sell_row: Tuple, def _exit_trade(self, trade: LocalTrade, sell_row: Tuple,
close_rate: float, amount: Optional[float] = None) -> Optional[LocalTrade]: close_rate: float, amount: float = None) -> Optional[LocalTrade]:
self.order_id_counter += 1 self.order_id_counter += 1
exit_candle_time = sell_row[DATE_IDX].to_pydatetime() exit_candle_time = sell_row[DATE_IDX].to_pydatetime()
order_type = self.strategy.order_types['exit'] order_type = self.strategy.order_types['exit']
@@ -674,10 +692,11 @@ class Backtesting:
trade.orders.append(order) trade.orders.append(order)
return trade return trade
def _check_trade_exit(self, trade: LocalTrade, row: Tuple) -> Optional[LocalTrade]: def _get_exit_trade_entry(
self, trade: LocalTrade, row: Tuple, is_first: bool) -> Optional[LocalTrade]:
exit_candle_time: datetime = row[DATE_IDX].to_pydatetime() exit_candle_time: datetime = row[DATE_IDX].to_pydatetime()
if self.trading_mode == TradingMode.FUTURES: if is_first and self.trading_mode == TradingMode.FUTURES:
trade.funding_fees = self.exchange.calculate_funding_fees( trade.funding_fees = self.exchange.calculate_funding_fees(
self.futures_data[trade.pair], self.futures_data[trade.pair],
amount=trade.amount, amount=trade.amount,
@@ -686,22 +705,7 @@ class Backtesting:
close_date=exit_candle_time, close_date=exit_candle_time,
) )
# Check if we need to adjust our current positions return self._get_exit_trade_entry_for_candle(trade, row)
if self.strategy.position_adjustment_enable:
trade = self._get_adjust_trade_entry_for_candle(trade, row)
enter = row[SHORT_IDX] if trade.is_short else row[LONG_IDX]
exit_sig = row[ESHORT_IDX] if trade.is_short else row[ELONG_IDX]
exits = self.strategy.should_exit(
trade, row[OPEN_IDX], row[DATE_IDX].to_pydatetime(), # type: ignore
enter=enter, exit_=exit_sig,
low=row[LOW_IDX], high=row[HIGH_IDX]
)
for exit_ in exits:
t = self._get_exit_for_signal(trade, row, exit_)
if t:
return t
return None
def get_valid_price_and_stake( def get_valid_price_and_stake(
self, pair: str, row: Tuple, propose_rate: float, stake_amount: float, self, pair: str, row: Tuple, propose_rate: float, stake_amount: float,
@@ -765,7 +769,6 @@ class Backtesting:
stake_amount=stake_amount, stake_amount=stake_amount,
min_stake_amount=min_stake_amount, min_stake_amount=min_stake_amount,
max_stake_amount=max_stake_amount, max_stake_amount=max_stake_amount,
trade_amount=trade.stake_amount if trade else None
) )
return propose_rate, stake_amount_val, leverage, min_stake_amount return propose_rate, stake_amount_val, leverage, min_stake_amount
@@ -775,11 +778,6 @@ class Backtesting:
trade: Optional[LocalTrade] = None, trade: Optional[LocalTrade] = None,
requested_rate: Optional[float] = None, requested_rate: Optional[float] = None,
requested_stake: Optional[float] = None) -> Optional[LocalTrade]: requested_stake: Optional[float] = None) -> Optional[LocalTrade]:
"""
:param trade: Trade to adjust - initial entry if None
:param requested_rate: Adjusted entry rate
:param requested_stake: Stake amount for adjusted orders (`adjust_entry_price`).
"""
current_time = row[DATE_IDX].to_pydatetime() current_time = row[DATE_IDX].to_pydatetime()
entry_tag = row[ENTER_TAG_IDX] if len(row) >= ENTER_TAG_IDX + 1 else None entry_tag = row[ENTER_TAG_IDX] if len(row) >= ENTER_TAG_IDX + 1 else None
@@ -805,7 +803,7 @@ class Backtesting:
return trade return trade
time_in_force = self.strategy.order_time_in_force['entry'] time_in_force = self.strategy.order_time_in_force['entry']
if stake_amount and (not min_stake_amount or stake_amount >= min_stake_amount): if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount):
self.order_id_counter += 1 self.order_id_counter += 1
base_currency = self.exchange.get_pair_base_currency(pair) base_currency = self.exchange.get_pair_base_currency(pair)
amount_p = (stake_amount / propose_rate) * leverage amount_p = (stake_amount / propose_rate) * leverage
@@ -921,9 +919,8 @@ class Backtesting:
trade.close(exit_row[OPEN_IDX], show_msg=False) trade.close(exit_row[OPEN_IDX], show_msg=False)
LocalTrade.close_bt_trade(trade) LocalTrade.close_bt_trade(trade)
def trade_slot_available(self, open_trade_count: int) -> bool: def trade_slot_available(self, max_open_trades: int, open_trade_count: int) -> bool:
# Always allow trades when max_open_trades is enabled. # Always allow trades when max_open_trades is enabled.
max_open_trades: IntOrInf = self.config['max_open_trades']
if max_open_trades <= 0 or open_trade_count < max_open_trades: if max_open_trades <= 0 or open_trade_count < max_open_trades:
return True return True
# Rejected trade # Rejected trade
@@ -1053,8 +1050,7 @@ class Backtesting:
def backtest_loop( def backtest_loop(
self, row: Tuple, pair: str, current_time: datetime, end_date: datetime, self, row: Tuple, pair: str, current_time: datetime, end_date: datetime,
open_trade_count_start: int, trade_dir: Optional[LongShort], max_open_trades: int, open_trade_count_start: int, is_first: bool = True) -> int:
is_first: bool = True) -> int:
""" """
NOTE: This method is used by Hyperopt at each iteration. Please keep it optimized. NOTE: This method is used by Hyperopt at each iteration. Please keep it optimized.
@@ -1073,10 +1069,11 @@ class Backtesting:
# max_open_trades must be respected # max_open_trades must be respected
# don't open on the last row # don't open on the last row
# We only open trades on the main candle, not on detail candles # We only open trades on the main candle, not on detail candles
trade_dir = self.check_for_trade_entry(row)
if ( if (
(self._position_stacking or len(LocalTrade.bt_trades_open_pp[pair]) == 0) (self._position_stacking or len(LocalTrade.bt_trades_open_pp[pair]) == 0)
and is_first and is_first
and self.trade_slot_available(open_trade_count_start) and self.trade_slot_available(max_open_trades, open_trade_count_start)
and current_time != end_date and current_time != end_date
and trade_dir is not None and trade_dir is not None
and not PairLocks.is_pair_locked(pair, row[DATE_IDX], trade_dir) and not PairLocks.is_pair_locked(pair, row[DATE_IDX], trade_dir)
@@ -1101,7 +1098,7 @@ class Backtesting:
# 4. Create exit orders (if any) # 4. Create exit orders (if any)
if not trade.open_order_id: if not trade.open_order_id:
self._check_trade_exit(trade, row) # Place exit order if necessary self._get_exit_trade_entry(trade, row, is_first) # Place exit order if necessary
# 5. Process exit orders. # 5. Process exit orders.
order = trade.select_order(trade.exit_side, is_open=True) order = trade.select_order(trade.exit_side, is_open=True)
@@ -1123,7 +1120,8 @@ class Backtesting:
return open_trade_count_start return open_trade_count_start
def backtest(self, processed: Dict, def backtest(self, processed: Dict,
start_date: datetime, end_date: datetime) -> Dict[str, Any]: start_date: datetime, end_date: datetime,
max_open_trades: int = 0) -> Dict[str, Any]:
""" """
Implement backtesting functionality Implement backtesting functionality
@@ -1135,6 +1133,7 @@ class Backtesting:
optimize memory usage! optimize memory usage!
:param start_date: backtesting timerange start datetime :param start_date: backtesting timerange start datetime
:param end_date: backtesting timerange end datetime :param end_date: backtesting timerange end datetime
:param max_open_trades: maximum number of concurrent trades, <= 0 means unlimited
:return: DataFrame with trades (results of backtesting) :return: DataFrame with trades (results of backtesting)
""" """
self.prepare_backtest(self.enable_protections) self.prepare_backtest(self.enable_protections)
@@ -1164,15 +1163,7 @@ class Backtesting:
indexes[pair] = row_index indexes[pair] = row_index
self.dataprovider._set_dataframe_max_index(row_index) self.dataprovider._set_dataframe_max_index(row_index)
current_detail_time: datetime = row[DATE_IDX].to_pydatetime() current_detail_time: datetime = row[DATE_IDX].to_pydatetime()
trade_dir: Optional[LongShort] = self.check_for_trade_entry(row) if self.timeframe_detail and pair in self.detail_data:
if (
(trade_dir is not None or len(LocalTrade.bt_trades_open_pp[pair]) > 0)
and self.timeframe_detail and pair in self.detail_data
):
# Spread out into detail timeframe.
# Should only happen when we are either in a trade for this pair
# or when we got the signal for a new trade.
exit_candle_end = current_detail_time + timedelta(minutes=self.timeframe_min) exit_candle_end = current_detail_time + timedelta(minutes=self.timeframe_min)
detail_data = self.detail_data[pair] detail_data = self.detail_data[pair]
@@ -1183,9 +1174,8 @@ class Backtesting:
if len(detail_data) == 0: if len(detail_data) == 0:
# Fall back to "regular" data if no detail data was found for this candle # Fall back to "regular" data if no detail data was found for this candle
open_trade_count_start = self.backtest_loop( open_trade_count_start = self.backtest_loop(
row, pair, current_time, end_date, row, pair, current_time, end_date, max_open_trades,
open_trade_count_start, trade_dir) open_trade_count_start)
continue
detail_data.loc[:, 'enter_long'] = row[LONG_IDX] detail_data.loc[:, 'enter_long'] = row[LONG_IDX]
detail_data.loc[:, 'exit_long'] = row[ELONG_IDX] detail_data.loc[:, 'exit_long'] = row[ELONG_IDX]
detail_data.loc[:, 'enter_short'] = row[SHORT_IDX] detail_data.loc[:, 'enter_short'] = row[SHORT_IDX]
@@ -1196,14 +1186,13 @@ class Backtesting:
current_time_det = current_time current_time_det = current_time
for det_row in detail_data[HEADERS].values.tolist(): for det_row in detail_data[HEADERS].values.tolist():
open_trade_count_start = self.backtest_loop( open_trade_count_start = self.backtest_loop(
det_row, pair, current_time_det, end_date, det_row, pair, current_time_det, end_date, max_open_trades,
open_trade_count_start, trade_dir, is_first) open_trade_count_start, is_first)
current_time_det += timedelta(minutes=self.timeframe_detail_min) current_time_det += timedelta(minutes=self.timeframe_detail_min)
is_first = False is_first = False
else: else:
open_trade_count_start = self.backtest_loop( open_trade_count_start = self.backtest_loop(
row, pair, current_time, end_date, row, pair, current_time, end_date, max_open_trades, open_trade_count_start)
open_trade_count_start, trade_dir)
# Move time one configured time_interval ahead. # Move time one configured time_interval ahead.
self.progress.increment() self.progress.increment()
@@ -1235,11 +1224,13 @@ class Backtesting:
self._set_strategy(strat) self._set_strategy(strat)
# Use max_open_trades in backtesting, except --disable-max-market-positions is set # Use max_open_trades in backtesting, except --disable-max-market-positions is set
if not self.config.get('use_max_market_positions', True): if self.config.get('use_max_market_positions', True):
# Must come from strategy config, as the strategy may modify this setting.
max_open_trades = self.strategy.config['max_open_trades']
else:
logger.info( logger.info(
'Ignoring max_open_trades (--disable-max-market-positions was used) ...') 'Ignoring max_open_trades (--disable-max-market-positions was used) ...')
self.strategy.max_open_trades = float('inf') max_open_trades = 0
self.config.update({'max_open_trades': self.strategy.max_open_trades})
# need to reprocess data every time to populate signals # need to reprocess data every time to populate signals
preprocessed = self.strategy.advise_all_indicators(data) preprocessed = self.strategy.advise_all_indicators(data)
@@ -1262,6 +1253,7 @@ class Backtesting:
processed=preprocessed, processed=preprocessed,
start_date=min_date, start_date=min_date,
end_date=max_date, end_date=max_date,
max_open_trades=max_open_trades,
) )
backtest_end_time = datetime.now(timezone.utc) backtest_end_time = datetime.now(timezone.utc)
results.update({ results.update({

View File

@@ -74,7 +74,6 @@ class Hyperopt:
self.roi_space: List[Dimension] = [] self.roi_space: List[Dimension] = []
self.stoploss_space: List[Dimension] = [] self.stoploss_space: List[Dimension] = []
self.trailing_space: List[Dimension] = [] self.trailing_space: List[Dimension] = []
self.max_open_trades_space: List[Dimension] = []
self.dimensions: List[Dimension] = [] self.dimensions: List[Dimension] = []
self.config = config self.config = config
@@ -118,10 +117,11 @@ class Hyperopt:
self.current_best_epoch: Optional[Dict[str, Any]] = None self.current_best_epoch: Optional[Dict[str, Any]] = None
# Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set # Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set
if not self.config.get('use_max_market_positions', True): if self.config.get('use_max_market_positions', True):
self.max_open_trades = self.config['max_open_trades']
else:
logger.debug('Ignoring max_open_trades (--disable-max-market-positions was used) ...') logger.debug('Ignoring max_open_trades (--disable-max-market-positions was used) ...')
self.backtesting.strategy.max_open_trades = float('inf') self.max_open_trades = 0
config.update({'max_open_trades': self.backtesting.strategy.max_open_trades})
if HyperoptTools.has_space(self.config, 'sell'): if HyperoptTools.has_space(self.config, 'sell'):
# Make sure use_exit_signal is enabled # Make sure use_exit_signal is enabled
@@ -209,10 +209,6 @@ class Hyperopt:
result['stoploss'] = {p.name: params.get(p.name) for p in self.stoploss_space} result['stoploss'] = {p.name: params.get(p.name) for p in self.stoploss_space}
if HyperoptTools.has_space(self.config, 'trailing'): if HyperoptTools.has_space(self.config, 'trailing'):
result['trailing'] = self.custom_hyperopt.generate_trailing_params(params) result['trailing'] = self.custom_hyperopt.generate_trailing_params(params)
if HyperoptTools.has_space(self.config, 'trades'):
result['max_open_trades'] = {
'max_open_trades': self.backtesting.strategy.max_open_trades
if self.backtesting.strategy.max_open_trades != float('inf') else -1}
return result return result
@@ -233,8 +229,6 @@ class Hyperopt:
'trailing_stop_positive_offset': strategy.trailing_stop_positive_offset, 'trailing_stop_positive_offset': strategy.trailing_stop_positive_offset,
'trailing_only_offset_is_reached': strategy.trailing_only_offset_is_reached, 'trailing_only_offset_is_reached': strategy.trailing_only_offset_is_reached,
} }
if not HyperoptTools.has_space(self.config, 'trades'):
result['max_open_trades'] = {'max_open_trades': strategy.max_open_trades}
return result return result
def print_results(self, results) -> None: def print_results(self, results) -> None:
@@ -286,13 +280,8 @@ class Hyperopt:
logger.debug("Hyperopt has 'trailing' space") logger.debug("Hyperopt has 'trailing' space")
self.trailing_space = self.custom_hyperopt.trailing_space() self.trailing_space = self.custom_hyperopt.trailing_space()
if HyperoptTools.has_space(self.config, 'trades'):
logger.debug("Hyperopt has 'trades' space")
self.max_open_trades_space = self.custom_hyperopt.max_open_trades_space()
self.dimensions = (self.buy_space + self.sell_space + self.protection_space self.dimensions = (self.buy_space + self.sell_space + self.protection_space
+ self.roi_space + self.stoploss_space + self.trailing_space + self.roi_space + self.stoploss_space + self.trailing_space)
+ self.max_open_trades_space)
def assign_params(self, params_dict: Dict, category: str) -> None: def assign_params(self, params_dict: Dict, category: str) -> None:
""" """
@@ -339,20 +328,6 @@ class Hyperopt:
self.backtesting.strategy.trailing_only_offset_is_reached = \ self.backtesting.strategy.trailing_only_offset_is_reached = \
d['trailing_only_offset_is_reached'] d['trailing_only_offset_is_reached']
if HyperoptTools.has_space(self.config, 'trades'):
if self.config["stake_amount"] == "unlimited" and \
(params_dict['max_open_trades'] == -1 or params_dict['max_open_trades'] == 0):
# Ignore unlimited max open trades if stake amount is unlimited
params_dict.update({'max_open_trades': self.config['max_open_trades']})
updated_max_open_trades = int(params_dict['max_open_trades']) \
if (params_dict['max_open_trades'] != -1
and params_dict['max_open_trades'] != 0) else float('inf')
self.config.update({'max_open_trades': updated_max_open_trades})
self.backtesting.strategy.max_open_trades = updated_max_open_trades
with self.data_pickle_file.open('rb') as f: with self.data_pickle_file.open('rb') as f:
processed = load(f, mmap_mode='r') processed = load(f, mmap_mode='r')
if self.analyze_per_epoch: if self.analyze_per_epoch:
@@ -362,7 +337,8 @@ class Hyperopt:
bt_results = self.backtesting.backtest( bt_results = self.backtesting.backtest(
processed=processed, processed=processed,
start_date=self.min_date, start_date=self.min_date,
end_date=self.max_date end_date=self.max_date,
max_open_trades=self.max_open_trades,
) )
backtest_end_time = datetime.now(timezone.utc) backtest_end_time = datetime.now(timezone.utc)
bt_results.update({ bt_results.update({

View File

@@ -91,8 +91,5 @@ class HyperOptAuto(IHyperOpt):
def trailing_space(self) -> List['Dimension']: def trailing_space(self) -> List['Dimension']:
return self._get_func('trailing_space')() return self._get_func('trailing_space')()
def max_open_trades_space(self) -> List['Dimension']:
return self._get_func('max_open_trades_space')()
def generate_estimator(self, dimensions: List['Dimension'], **kwargs) -> EstimatorType: def generate_estimator(self, dimensions: List['Dimension'], **kwargs) -> EstimatorType:
return self._get_func('generate_estimator')(dimensions=dimensions, **kwargs) return self._get_func('generate_estimator')(dimensions=dimensions, **kwargs)

View File

@@ -191,16 +191,6 @@ class IHyperOpt(ABC):
Categorical([True, False], name='trailing_only_offset_is_reached'), Categorical([True, False], name='trailing_only_offset_is_reached'),
] ]
def max_open_trades_space(self) -> List[Dimension]:
"""
Create a max open trades space.
You may override it in your custom Hyperopt class.
"""
return [
Integer(-1, 10, name='max_open_trades'),
]
# This is needed for proper unpickling the class attribute timeframe # This is needed for proper unpickling the class attribute timeframe
# which is set to the actual value by the resolver. # which is set to the actual value by the resolver.
# Why do I still need such shamanic mantras in modern python? # Why do I still need such shamanic mantras in modern python?

View File

@@ -5,11 +5,13 @@ This module defines the alternative HyperOptLoss class which can be used for
Hyperoptimization. Hyperoptimization.
""" """
from datetime import datetime from datetime import datetime
from math import sqrt as msqrt
from typing import Any, Dict
from pandas import DataFrame from pandas import DataFrame
from freqtrade.constants import Config from freqtrade.constants import Config
from freqtrade.data.metrics import calculate_calmar from freqtrade.data.metrics import calculate_max_drawdown
from freqtrade.optimize.hyperopt import IHyperOptLoss from freqtrade.optimize.hyperopt import IHyperOptLoss
@@ -21,15 +23,42 @@ class CalmarHyperOptLoss(IHyperOptLoss):
""" """
@staticmethod @staticmethod
def hyperopt_loss_function(results: DataFrame, trade_count: int, def hyperopt_loss_function(
min_date: datetime, max_date: datetime, results: DataFrame,
config: Config, *args, **kwargs) -> float: trade_count: int,
min_date: datetime,
max_date: datetime,
config: Config,
processed: Dict[str, DataFrame],
backtest_stats: Dict[str, Any],
*args,
**kwargs
) -> float:
""" """
Objective function, returns smaller number for more optimal results. Objective function, returns smaller number for more optimal results.
Uses Calmar Ratio calculation. Uses Calmar Ratio calculation.
""" """
starting_balance = config['dry_run_wallet'] total_profit = backtest_stats["profit_total"]
calmar_ratio = calculate_calmar(results, min_date, max_date, starting_balance) days_period = (max_date - min_date).days
# adding slippage of 0.1% per trade
total_profit = total_profit - 0.0005
expected_returns_mean = total_profit.sum() / days_period * 100
# calculate max drawdown
try:
_, _, _, _, _, max_drawdown = calculate_max_drawdown(
results, value_col="profit_abs"
)
except ValueError:
max_drawdown = 0
if max_drawdown != 0:
calmar_ratio = expected_returns_mean / max_drawdown * msqrt(365)
else:
# Define high (negative) calmar ratio to be clear that this is NOT optimal.
calmar_ratio = -20.0
# print(expected_returns_mean, max_drawdown, calmar_ratio) # print(expected_returns_mean, max_drawdown, calmar_ratio)
return -calmar_ratio return -calmar_ratio

View File

@@ -6,10 +6,9 @@ Hyperoptimization.
""" """
from datetime import datetime from datetime import datetime
import numpy as np
from pandas import DataFrame from pandas import DataFrame
from freqtrade.constants import Config
from freqtrade.data.metrics import calculate_sharpe
from freqtrade.optimize.hyperopt import IHyperOptLoss from freqtrade.optimize.hyperopt import IHyperOptLoss
@@ -23,13 +22,25 @@ class SharpeHyperOptLoss(IHyperOptLoss):
@staticmethod @staticmethod
def hyperopt_loss_function(results: DataFrame, trade_count: int, def hyperopt_loss_function(results: DataFrame, trade_count: int,
min_date: datetime, max_date: datetime, min_date: datetime, max_date: datetime,
config: Config, *args, **kwargs) -> float: *args, **kwargs) -> float:
""" """
Objective function, returns smaller number for more optimal results. Objective function, returns smaller number for more optimal results.
Uses Sharpe Ratio calculation. Uses Sharpe Ratio calculation.
""" """
starting_balance = config['dry_run_wallet'] total_profit = results["profit_ratio"]
sharp_ratio = calculate_sharpe(results, min_date, max_date, starting_balance) days_period = (max_date - min_date).days
# adding slippage of 0.1% per trade
total_profit = total_profit - 0.0005
expected_returns_mean = total_profit.sum() / days_period
up_stdev = np.std(total_profit)
if up_stdev != 0:
sharp_ratio = expected_returns_mean / up_stdev * np.sqrt(365)
else:
# Define high (negative) sharpe ratio to be clear that this is NOT optimal.
sharp_ratio = -20.
# print(expected_returns_mean, up_stdev, sharp_ratio) # print(expected_returns_mean, up_stdev, sharp_ratio)
return -sharp_ratio return -sharp_ratio

View File

@@ -6,10 +6,9 @@ Hyperoptimization.
""" """
from datetime import datetime from datetime import datetime
import numpy as np
from pandas import DataFrame from pandas import DataFrame
from freqtrade.constants import Config
from freqtrade.data.metrics import calculate_sortino
from freqtrade.optimize.hyperopt import IHyperOptLoss from freqtrade.optimize.hyperopt import IHyperOptLoss
@@ -23,13 +22,28 @@ class SortinoHyperOptLoss(IHyperOptLoss):
@staticmethod @staticmethod
def hyperopt_loss_function(results: DataFrame, trade_count: int, def hyperopt_loss_function(results: DataFrame, trade_count: int,
min_date: datetime, max_date: datetime, min_date: datetime, max_date: datetime,
config: Config, *args, **kwargs) -> float: *args, **kwargs) -> float:
""" """
Objective function, returns smaller number for more optimal results. Objective function, returns smaller number for more optimal results.
Uses Sortino Ratio calculation. Uses Sortino Ratio calculation.
""" """
starting_balance = config['dry_run_wallet'] total_profit = results["profit_ratio"]
sortino_ratio = calculate_sortino(results, min_date, max_date, starting_balance) days_period = (max_date - min_date).days
# adding slippage of 0.1% per trade
total_profit = total_profit - 0.0005
expected_returns_mean = total_profit.sum() / days_period
results['downside_returns'] = 0
results.loc[total_profit < 0, 'downside_returns'] = results['profit_ratio']
down_stdev = np.std(results['downside_returns'])
if down_stdev != 0:
sortino_ratio = expected_returns_mean / down_stdev * np.sqrt(365)
else:
# Define high (negative) sortino ratio to be clear that this is NOT optimal.
sortino_ratio = -20.
# print(expected_returns_mean, down_stdev, sortino_ratio) # print(expected_returns_mean, down_stdev, sortino_ratio)
return -sortino_ratio return -sortino_ratio

View File

@@ -96,7 +96,7 @@ class HyperoptTools():
Tell if the space value is contained in the configuration Tell if the space value is contained in the configuration
""" """
# 'trailing' and 'protection spaces are not included in the 'default' set of spaces # 'trailing' and 'protection spaces are not included in the 'default' set of spaces
if space in ('trailing', 'protection', 'trades'): if space in ('trailing', 'protection'):
return any(s in config['spaces'] for s in [space, 'all']) return any(s in config['spaces'] for s in [space, 'all'])
else: else:
return any(s in config['spaces'] for s in [space, 'all', 'default']) return any(s in config['spaces'] for s in [space, 'all', 'default'])
@@ -170,7 +170,7 @@ class HyperoptTools():
@staticmethod @staticmethod
def show_epoch_details(results, total_epochs: int, print_json: bool, def show_epoch_details(results, total_epochs: int, print_json: bool,
no_header: bool = False, header_str: Optional[str] = None) -> None: no_header: bool = False, header_str: str = None) -> None:
""" """
Display details of the hyperopt result Display details of the hyperopt result
""" """
@@ -187,8 +187,7 @@ class HyperoptTools():
if print_json: if print_json:
result_dict: Dict = {} result_dict: Dict = {}
for s in ['buy', 'sell', 'protection', for s in ['buy', 'sell', 'protection', 'roi', 'stoploss', 'trailing']:
'roi', 'stoploss', 'trailing', 'max_open_trades']:
HyperoptTools._params_update_for_json(result_dict, params, non_optimized, s) HyperoptTools._params_update_for_json(result_dict, params, non_optimized, s)
print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE)) print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE))
@@ -202,8 +201,6 @@ class HyperoptTools():
HyperoptTools._params_pretty_print(params, 'roi', "ROI table:", non_optimized) HyperoptTools._params_pretty_print(params, 'roi', "ROI table:", non_optimized)
HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:", non_optimized) HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:", non_optimized)
HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:", non_optimized) HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:", non_optimized)
HyperoptTools._params_pretty_print(
params, 'max_open_trades', "Max Open Trades:", non_optimized)
@staticmethod @staticmethod
def _params_update_for_json(result_dict, params, non_optimized, space: str) -> None: def _params_update_for_json(result_dict, params, non_optimized, space: str) -> None:
@@ -242,9 +239,7 @@ class HyperoptTools():
if space == "stoploss": if space == "stoploss":
stoploss = safe_value_fallback2(space_params, no_params, space, space) stoploss = safe_value_fallback2(space_params, no_params, space, space)
result += (f"stoploss = {stoploss}{appendix}") result += (f"stoploss = {stoploss}{appendix}")
elif space == "max_open_trades":
max_open_trades = safe_value_fallback2(space_params, no_params, space, space)
result += (f"max_open_trades = {max_open_trades}{appendix}")
elif space == "roi": elif space == "roi":
result = result[:-1] + f'{appendix}\n' result = result[:-1] + f'{appendix}\n'
minimal_roi_result = rapidjson.dumps({ minimal_roi_result = rapidjson.dumps({
@@ -264,7 +259,7 @@ class HyperoptTools():
print(result) print(result)
@staticmethod @staticmethod
def _space_params(params, space: str, r: Optional[int] = None) -> Dict: def _space_params(params, space: str, r: int = None) -> Dict:
d = params.get(space) d = params.get(space)
if d: if d:
# Round floats to `r` digits after the decimal point if requested # Round floats to `r` digits after the decimal point if requested

View File

@@ -8,10 +8,9 @@ from pandas import DataFrame, to_datetime
from tabulate import tabulate from tabulate import tabulate
from freqtrade.constants import (DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT, from freqtrade.constants import (DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT,
Config, IntOrInf) Config)
from freqtrade.data.metrics import (calculate_cagr, calculate_calmar, calculate_csum, from freqtrade.data.metrics import (calculate_cagr, calculate_csum, calculate_market_change,
calculate_expectancy, calculate_market_change, calculate_max_drawdown)
calculate_max_drawdown, calculate_sharpe, calculate_sortino)
from freqtrade.misc import decimals_per_coin, file_dump_joblib, file_dump_json, round_coin_value from freqtrade.misc import decimals_per_coin, file_dump_joblib, file_dump_json, round_coin_value
from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename
@@ -191,7 +190,7 @@ def generate_tag_metrics(tag_type: str,
return [] return []
def generate_exit_reason_stats(max_open_trades: IntOrInf, results: DataFrame) -> List[Dict]: def generate_exit_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]:
""" """
Generate small table outlining Backtest results Generate small table outlining Backtest results
:param max_open_trades: Max_open_trades parameter :param max_open_trades: Max_open_trades parameter
@@ -449,10 +448,6 @@ def generate_strategy_stats(pairlist: List[str],
'profit_total_long_abs': results.loc[~results['is_short'], 'profit_abs'].sum(), 'profit_total_long_abs': results.loc[~results['is_short'], 'profit_abs'].sum(),
'profit_total_short_abs': results.loc[results['is_short'], 'profit_abs'].sum(), 'profit_total_short_abs': results.loc[results['is_short'], 'profit_abs'].sum(),
'cagr': calculate_cagr(backtest_days, start_balance, content['final_balance']), 'cagr': calculate_cagr(backtest_days, start_balance, content['final_balance']),
'expectancy': calculate_expectancy(results),
'sortino': calculate_sortino(results, min_date, max_date, start_balance),
'sharpe': calculate_sharpe(results, min_date, max_date, start_balance),
'calmar': calculate_calmar(results, min_date, max_date, start_balance),
'profit_factor': profit_factor, 'profit_factor': profit_factor,
'backtest_start': min_date.strftime(DATETIME_PRINT_FORMAT), 'backtest_start': min_date.strftime(DATETIME_PRINT_FORMAT),
'backtest_start_ts': int(min_date.timestamp() * 1000), 'backtest_start_ts': int(min_date.timestamp() * 1000),
@@ -790,13 +785,8 @@ def text_table_add_metrics(strat_results: Dict) -> str:
strat_results['stake_currency'])), strat_results['stake_currency'])),
('Total profit %', f"{strat_results['profit_total']:.2%}"), ('Total profit %', f"{strat_results['profit_total']:.2%}"),
('CAGR %', f"{strat_results['cagr']:.2%}" if 'cagr' in strat_results else 'N/A'), ('CAGR %', f"{strat_results['cagr']:.2%}" if 'cagr' in strat_results else 'N/A'),
('Sortino', f"{strat_results['sortino']:.2f}" if 'sortino' in strat_results else 'N/A'),
('Sharpe', f"{strat_results['sharpe']:.2f}" if 'sharpe' in strat_results else 'N/A'),
('Calmar', f"{strat_results['calmar']:.2f}" if 'calmar' in strat_results else 'N/A'),
('Profit factor', f'{strat_results["profit_factor"]:.2f}' if 'profit_factor' ('Profit factor', f'{strat_results["profit_factor"]:.2f}' if 'profit_factor'
in strat_results else 'N/A'), in strat_results else 'N/A'),
('Expectancy', f"{strat_results['expectancy']:.2f}" if 'expectancy'
in strat_results else 'N/A'),
('Trades per day', strat_results['trades_per_day']), ('Trades per day', strat_results['trades_per_day']),
('Avg. daily profit %', ('Avg. daily profit %',
f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}"), f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}"),

View File

@@ -109,10 +109,11 @@ def migrate_trades_and_orders_table(
else: else:
is_short = get_column_def(cols, 'is_short', '0') is_short = get_column_def(cols, 'is_short', '0')
# Futures Properties # Margin Properties
interest_rate = get_column_def(cols, 'interest_rate', '0.0') interest_rate = get_column_def(cols, 'interest_rate', '0.0')
# Futures properties
funding_fees = get_column_def(cols, 'funding_fees', '0.0') funding_fees = get_column_def(cols, 'funding_fees', '0.0')
max_stake_amount = get_column_def(cols, 'max_stake_amount', 'stake_amount')
# If ticker-interval existed use that, else null. # If ticker-interval existed use that, else null.
if has_column(cols, 'ticker_interval'): if has_column(cols, 'ticker_interval'):
@@ -161,8 +162,7 @@ def migrate_trades_and_orders_table(
timeframe, open_trade_value, close_profit_abs, timeframe, open_trade_value, close_profit_abs,
trading_mode, leverage, liquidation_price, is_short, trading_mode, leverage, liquidation_price, is_short,
interest_rate, funding_fees, realized_profit, interest_rate, funding_fees, realized_profit,
amount_precision, price_precision, precision_mode, contract_size, amount_precision, price_precision, precision_mode, contract_size
max_stake_amount
) )
select id, lower(exchange), pair, {base_currency} base_currency, select id, lower(exchange), pair, {base_currency} base_currency,
{stake_currency} stake_currency, {stake_currency} stake_currency,
@@ -190,8 +190,7 @@ def migrate_trades_and_orders_table(
{is_short} is_short, {interest_rate} interest_rate, {is_short} is_short, {interest_rate} interest_rate,
{funding_fees} funding_fees, {realized_profit} realized_profit, {funding_fees} funding_fees, {realized_profit} realized_profit,
{amount_precision} amount_precision, {price_precision} price_precision, {amount_precision} amount_precision, {price_precision} price_precision,
{precision_mode} precision_mode, {contract_size} contract_size, {precision_mode} precision_mode, {contract_size} contract_size
{max_stake_amount} max_stake_amount
from {trade_back_name} from {trade_back_name}
""")) """))
@@ -214,22 +213,17 @@ def migrate_orders_table(engine, table_back_name: str, cols_order: List):
average = get_column_def(cols_order, 'average', 'null') average = get_column_def(cols_order, 'average', 'null')
stop_price = get_column_def(cols_order, 'stop_price', 'null') stop_price = get_column_def(cols_order, 'stop_price', 'null')
funding_fee = get_column_def(cols_order, 'funding_fee', '0.0') funding_fee = get_column_def(cols_order, 'funding_fee', '0.0')
ft_amount = get_column_def(cols_order, 'ft_amount', 'coalesce(amount, 0.0)')
ft_price = get_column_def(cols_order, 'ft_price', 'coalesce(price, 0.0)')
# sqlite does not support literals for booleans # sqlite does not support literals for booleans
with engine.begin() as connection: with engine.begin() as connection:
connection.execute(text(f""" connection.execute(text(f"""
insert into orders (id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, insert into orders (id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id,
status, symbol, order_type, side, price, amount, filled, average, remaining, cost, status, symbol, order_type, side, price, amount, filled, average, remaining, cost,
stop_price, order_date, order_filled_date, order_update_date, ft_fee_base, funding_fee, stop_price, order_date, order_filled_date, order_update_date, ft_fee_base, funding_fee)
ft_amount, ft_price
)
select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id,
status, symbol, order_type, side, price, amount, filled, {average} average, remaining, status, symbol, order_type, side, price, amount, filled, {average} average, remaining,
cost, {stop_price} stop_price, order_date, order_filled_date, cost, {stop_price} stop_price, order_date, order_filled_date,
order_update_date, {ft_fee_base} ft_fee_base, {funding_fee} funding_fee, order_update_date, {ft_fee_base} ft_fee_base, {funding_fee} funding_fee
{ft_amount} ft_amount, {ft_price} ft_price
from {table_back_name} from {table_back_name}
""")) """))
@@ -316,8 +310,8 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
# if ('orders' not in previous_tables # if ('orders' not in previous_tables
# or not has_column(cols_orders, 'funding_fee')): # or not has_column(cols_orders, 'funding_fee')):
migrating = False migrating = False
# if not has_column(cols_trades, 'max_stake_amount'): # if not has_column(cols_trades, 'contract_size'):
if not has_column(cols_orders, 'ft_price'): if not has_column(cols_orders, 'funding_fee'):
migrating = True migrating = True
logger.info(f"Running database migration for trades - " logger.info(f"Running database migration for trades - "
f"backup: {table_back_name}, {order_table_bak_name}") f"backup: {table_back_name}, {order_table_bak_name}")

View File

@@ -30,8 +30,8 @@ class PairLocks():
PairLocks.locks = [] PairLocks.locks = []
@staticmethod @staticmethod
def lock_pair(pair: str, until: datetime, reason: Optional[str] = None, *, def lock_pair(pair: str, until: datetime, reason: str = None, *,
now: Optional[datetime] = None, side: str = '*') -> PairLock: now: datetime = None, side: str = '*') -> PairLock:
""" """
Create PairLock from now to "until". Create PairLock from now to "until".
Uses database by default, unless PairLocks.use_db is set to False, Uses database by default, unless PairLocks.use_db is set to False,

View File

@@ -49,8 +49,6 @@ class Order(_DECL_BASE):
ft_order_side: str = Column(String(25), nullable=False) ft_order_side: str = Column(String(25), nullable=False)
ft_pair: str = Column(String(25), nullable=False) ft_pair: str = Column(String(25), nullable=False)
ft_is_open = Column(Boolean, nullable=False, default=True, index=True) ft_is_open = Column(Boolean, nullable=False, default=True, index=True)
ft_amount = Column(Float, nullable=False)
ft_price = Column(Float, nullable=False)
order_id: str = Column(String(255), nullable=False, index=True) order_id: str = Column(String(255), nullable=False, index=True)
status = Column(String(255), nullable=True) status = Column(String(255), nullable=True)
@@ -84,13 +82,9 @@ class Order(_DECL_BASE):
self.order_filled_date.replace(tzinfo=timezone.utc) if self.order_filled_date else None self.order_filled_date.replace(tzinfo=timezone.utc) if self.order_filled_date else None
) )
@property
def safe_amount(self) -> float:
return self.amount or self.ft_amount
@property @property
def safe_price(self) -> float: def safe_price(self) -> float:
return self.average or self.price or self.stop_price or self.ft_price return self.average or self.price or self.stop_price
@property @property
def safe_filled(self) -> float: def safe_filled(self) -> float:
@@ -100,7 +94,7 @@ class Order(_DECL_BASE):
def safe_remaining(self) -> float: def safe_remaining(self) -> float:
return ( return (
self.remaining if self.remaining is not None else self.remaining if self.remaining is not None else
self.safe_amount - (self.filled or 0.0) self.amount - (self.filled or 0.0)
) )
@property @property
@@ -146,7 +140,7 @@ class Order(_DECL_BASE):
# Assign funding fee up to this point # Assign funding fee up to this point
# (represents the funding fee since the last order) # (represents the funding fee since the last order)
self.funding_fee = self.trade.funding_fees self.funding_fee = self.trade.funding_fees
if (order.get('filled', 0.0) or 0.0) > 0 and not self.order_filled_date: if (order.get('filled', 0.0) or 0.0) > 0:
self.order_filled_date = datetime.now(timezone.utc) self.order_filled_date = datetime.now(timezone.utc)
self.order_update_date = datetime.now(timezone.utc) self.order_update_date = datetime.now(timezone.utc)
@@ -233,20 +227,11 @@ class Order(_DECL_BASE):
logger.warning(f"Did not find order for {order}.") logger.warning(f"Did not find order for {order}.")
@staticmethod @staticmethod
def parse_from_ccxt_object( def parse_from_ccxt_object(order: Dict[str, Any], pair: str, side: str) -> 'Order':
order: Dict[str, Any], pair: str, side: str,
amount: Optional[float] = None, price: Optional[float] = None) -> 'Order':
""" """
Parse an order from a ccxt object and return a new order Object. Parse an order from a ccxt object and return a new order Object.
Optional support for overriding amount and price is only used for test simplification.
""" """
o = Order( o = Order(order_id=str(order['id']), ft_order_side=side, ft_pair=pair)
order_id=str(order['id']),
ft_order_side=side,
ft_pair=pair,
ft_amount=amount if amount else order['amount'],
ft_price=price if price else order['price'],
)
o.update_from_ccxt_object(order) o.update_from_ccxt_object(order)
return o return o
@@ -308,7 +293,6 @@ class LocalTrade():
close_profit: Optional[float] = None close_profit: Optional[float] = None
close_profit_abs: Optional[float] = None close_profit_abs: Optional[float] = None
stake_amount: float = 0.0 stake_amount: float = 0.0
max_stake_amount: float = 0.0
amount: float = 0.0 amount: float = 0.0
amount_requested: Optional[float] = None amount_requested: Optional[float] = None
open_date: datetime open_date: datetime
@@ -413,6 +397,12 @@ class LocalTrade():
def close_date_utc(self): def close_date_utc(self):
return self.close_date.replace(tzinfo=timezone.utc) return self.close_date.replace(tzinfo=timezone.utc)
@property
def enter_side(self) -> str:
""" DEPRECATED, please use entry_side instead"""
# TODO: Please remove me after 2022.5
return self.entry_side
@property @property
def entry_side(self) -> str: def entry_side(self) -> str:
if self.is_short: if self.is_short:
@@ -485,8 +475,8 @@ class LocalTrade():
'amount': round(self.amount, 8), 'amount': round(self.amount, 8),
'amount_requested': round(self.amount_requested, 8) if self.amount_requested else None, 'amount_requested': round(self.amount_requested, 8) if self.amount_requested else None,
'stake_amount': round(self.stake_amount, 8), 'stake_amount': round(self.stake_amount, 8),
'max_stake_amount': round(self.max_stake_amount, 8) if self.max_stake_amount else None,
'strategy': self.strategy, 'strategy': self.strategy,
'buy_tag': self.enter_tag,
'enter_tag': self.enter_tag, 'enter_tag': self.enter_tag,
'timeframe': self.timeframe, 'timeframe': self.timeframe,
@@ -523,6 +513,7 @@ class LocalTrade():
'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None, 'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None,
'profit_abs': self.close_profit_abs, 'profit_abs': self.close_profit_abs,
'sell_reason': self.exit_reason, # Deprecated
'exit_reason': self.exit_reason, 'exit_reason': self.exit_reason,
'exit_order_status': self.exit_order_status, 'exit_order_status': self.exit_order_status,
'stop_loss_abs': self.stop_loss, 'stop_loss_abs': self.stop_loss,
@@ -799,7 +790,7 @@ class LocalTrade():
else: else:
return close_trade - fees return close_trade - fees
def calc_close_trade_value(self, rate: float, amount: Optional[float] = None) -> float: def calc_close_trade_value(self, rate: float, amount: float = None) -> float:
""" """
Calculate the Trade's close value including fees Calculate the Trade's close value including fees
:param rate: rate to compare with. :param rate: rate to compare with.
@@ -837,8 +828,7 @@ class LocalTrade():
raise OperationalException( raise OperationalException(
f"{self.trading_mode.value} trading is not yet available using freqtrade") f"{self.trading_mode.value} trading is not yet available using freqtrade")
def calc_profit(self, rate: float, amount: Optional[float] = None, def calc_profit(self, rate: float, amount: float = None, open_rate: float = None) -> float:
open_rate: Optional[float] = None) -> float:
""" """
Calculate the absolute profit in stake currency between Close and Open trade Calculate the absolute profit in stake currency between Close and Open trade
:param rate: close rate to compare with. :param rate: close rate to compare with.
@@ -859,8 +849,7 @@ class LocalTrade():
return float(f"{profit:.8f}") return float(f"{profit:.8f}")
def calc_profit_ratio( def calc_profit_ratio(
self, rate: float, amount: Optional[float] = None, self, rate: float, amount: float = None, open_rate: float = None) -> float:
open_rate: Optional[float] = None) -> float:
""" """
Calculates the profit as ratio (including fee). Calculates the profit as ratio (including fee).
:param rate: rate to compare with. :param rate: rate to compare with.
@@ -893,7 +882,6 @@ class LocalTrade():
ZERO = FtPrecise(0.0) ZERO = FtPrecise(0.0)
current_amount = FtPrecise(0.0) current_amount = FtPrecise(0.0)
current_stake = FtPrecise(0.0) current_stake = FtPrecise(0.0)
max_stake_amount = FtPrecise(0.0)
total_stake = 0.0 # Total stake after all buy orders (does not subtract!) total_stake = 0.0 # Total stake after all buy orders (does not subtract!)
avg_price = FtPrecise(0.0) avg_price = FtPrecise(0.0)
close_profit = 0.0 close_profit = 0.0
@@ -935,9 +923,7 @@ class LocalTrade():
exit_rate, amount=exit_amount, open_rate=avg_price) exit_rate, amount=exit_amount, open_rate=avg_price)
else: else:
total_stake = total_stake + self._calc_open_trade_value(tmp_amount, price) total_stake = total_stake + self._calc_open_trade_value(tmp_amount, price)
max_stake_amount += (tmp_amount * price)
self.funding_fees = funding_fees self.funding_fees = funding_fees
self.max_stake_amount = float(max_stake_amount)
if close_profit: if close_profit:
self.close_profit = close_profit self.close_profit = close_profit
@@ -973,12 +959,11 @@ class LocalTrade():
return None return None
def select_order(self, order_side: Optional[str] = None, def select_order(self, order_side: Optional[str] = None,
is_open: Optional[bool] = None, only_filled: bool = False) -> Optional[Order]: is_open: Optional[bool] = None) -> Optional[Order]:
""" """
Finds latest order for this orderside and status Finds latest order for this orderside and status
:param order_side: ft_order_side of the order (either 'buy', 'sell' or 'stoploss') :param order_side: ft_order_side of the order (either 'buy', 'sell' or 'stoploss')
:param is_open: Only search for open orders? :param is_open: Only search for open orders?
:param only_filled: Only search for Filled orders (only valid with is_open=False).
:return: latest Order object if it exists, else None :return: latest Order object if it exists, else None
""" """
orders = self.orders orders = self.orders
@@ -986,8 +971,6 @@ class LocalTrade():
orders = [o for o in orders if o.ft_order_side == order_side] orders = [o for o in orders if o.ft_order_side == order_side]
if is_open is not None: if is_open is not None:
orders = [o for o in orders if o.ft_is_open == is_open] orders = [o for o in orders if o.ft_is_open == is_open]
if is_open is False and only_filled:
orders = [o for o in orders if o.filled and o.status in NON_OPEN_EXCHANGE_STATES]
if len(orders) > 0: if len(orders) > 0:
return orders[-1] return orders[-1]
else: else:
@@ -1061,9 +1044,8 @@ class LocalTrade():
return self.exit_reason return self.exit_reason
@staticmethod @staticmethod
def get_trades_proxy(*, pair: Optional[str] = None, is_open: Optional[bool] = None, def get_trades_proxy(*, pair: str = None, is_open: bool = None,
open_date: Optional[datetime] = None, open_date: datetime = None, close_date: datetime = None,
close_date: Optional[datetime] = None,
) -> List['LocalTrade']: ) -> List['LocalTrade']:
""" """
Helper function to query Trades. Helper function to query Trades.
@@ -1193,7 +1175,6 @@ class Trade(_DECL_BASE, LocalTrade):
close_profit = Column(Float) close_profit = Column(Float)
close_profit_abs = Column(Float) close_profit_abs = Column(Float)
stake_amount = Column(Float, nullable=False) stake_amount = Column(Float, nullable=False)
max_stake_amount = Column(Float)
amount = Column(Float) amount = Column(Float)
amount_requested = Column(Float) amount_requested = Column(Float)
open_date = Column(DateTime, nullable=False, default=datetime.utcnow) open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
@@ -1260,9 +1241,8 @@ class Trade(_DECL_BASE, LocalTrade):
Trade.query.session.rollback() Trade.query.session.rollback()
@staticmethod @staticmethod
def get_trades_proxy(*, pair: Optional[str] = None, is_open: Optional[bool] = None, def get_trades_proxy(*, pair: str = None, is_open: bool = None,
open_date: Optional[datetime] = None, open_date: datetime = None, close_date: datetime = None,
close_date: Optional[datetime] = None,
) -> List['LocalTrade']: ) -> List['LocalTrade']:
""" """
Helper function to query Trades.j Helper function to query Trades.j

View File

@@ -436,11 +436,11 @@ def create_scatter(
return None return None
def generate_candlestick_graph( def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFrame = None, *,
pair: str, data: pd.DataFrame, trades: Optional[pd.DataFrame] = None, *, indicators1: List[str] = [],
indicators1: List[str] = [], indicators2: List[str] = [], indicators2: List[str] = [],
plot_config: Dict[str, Dict] = {}, plot_config: Dict[str, Dict] = {},
) -> go.Figure: ) -> go.Figure:
""" """
Generate the graph from the data generated by Backtesting or from DB Generate the graph from the data generated by Backtesting or from DB
Volume will always be ploted in row2, so Row 1 and 3 are to our disposal for custom indicators Volume will always be ploted in row2, so Row 1 and 3 are to our disposal for custom indicators

View File

@@ -1,206 +0,0 @@
"""
Remote PairList provider
Provides pair list fetched from a remote source
"""
import json
import logging
from pathlib import Path
from typing import Any, Dict, List, Tuple
import requests
from cachetools import TTLCache
from freqtrade import __version__
from freqtrade.constants import Config
from freqtrade.exceptions import OperationalException
from freqtrade.exchange.types import Tickers
from freqtrade.plugins.pairlist.IPairList import IPairList
logger = logging.getLogger(__name__)
class RemotePairList(IPairList):
def __init__(self, exchange, pairlistmanager,
config: Config, pairlistconfig: Dict[str, Any],
pairlist_pos: int) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
if 'number_assets' not in self._pairlistconfig:
raise OperationalException(
'`number_assets` not specified. Please check your configuration '
'for "pairlist.config.number_assets"')
if 'pairlist_url' not in self._pairlistconfig:
raise OperationalException(
'`pairlist_url` not specified. Please check your configuration '
'for "pairlist.config.pairlist_url"')
self._number_pairs = self._pairlistconfig['number_assets']
self._refresh_period: int = self._pairlistconfig.get('refresh_period', 1800)
self._keep_pairlist_on_failure = self._pairlistconfig.get('keep_pairlist_on_failure', True)
self._pair_cache: TTLCache = TTLCache(maxsize=1, ttl=self._refresh_period)
self._pairlist_url = self._pairlistconfig.get('pairlist_url', '')
self._read_timeout = self._pairlistconfig.get('read_timeout', 60)
self._bearer_token = self._pairlistconfig.get('bearer_token', '')
self._init_done = False
self._last_pairlist: List[Any] = list()
@property
def needstickers(self) -> bool:
"""
Boolean property defining if tickers are necessary.
If no Pairlist requires tickers, an empty Dict is passed
as tickers argument to filter_pairlist
"""
return False
def short_desc(self) -> str:
"""
Short whitelist method description - used for startup-messages
"""
return f"{self.name} - {self._pairlistconfig['number_assets']} pairs from RemotePairlist."
def process_json(self, jsonparse) -> List[str]:
pairlist = jsonparse.get('pairs', [])
remote_refresh_period = int(jsonparse.get('refresh_period', self._refresh_period))
if self._refresh_period < remote_refresh_period:
self.log_once(f'Refresh Period has been increased from {self._refresh_period}'
f' to minimum allowed: {remote_refresh_period} from Remote.', logger.info)
self._refresh_period = remote_refresh_period
self._pair_cache = TTLCache(maxsize=1, ttl=remote_refresh_period)
self._init_done = True
return pairlist
def return_last_pairlist(self) -> List[str]:
if self._keep_pairlist_on_failure:
pairlist = self._last_pairlist
self.log_once('Keeping last fetched pairlist', logger.info)
else:
pairlist = []
return pairlist
def fetch_pairlist(self) -> Tuple[List[str], float]:
headers = {
'User-Agent': 'Freqtrade/' + __version__ + ' Remotepairlist'
}
if self._bearer_token:
headers['Authorization'] = f'Bearer {self._bearer_token}'
try:
response = requests.get(self._pairlist_url, headers=headers,
timeout=self._read_timeout)
content_type = response.headers.get('content-type')
time_elapsed = response.elapsed.total_seconds()
if "application/json" in str(content_type):
jsonparse = response.json()
try:
pairlist = self.process_json(jsonparse)
except Exception as e:
if self._init_done:
pairlist = self.return_last_pairlist()
logger.warning(f'Error while processing JSON data: {type(e)}')
else:
raise OperationalException(f'Error while processing JSON data: {type(e)}')
else:
if self._init_done:
self.log_once(f'Error: RemotePairList is not of type JSON: '
f' {self._pairlist_url}', logger.info)
pairlist = self.return_last_pairlist()
else:
raise OperationalException('RemotePairList is not of type JSON, abort.')
except requests.exceptions.RequestException:
self.log_once(f'Was not able to fetch pairlist from:'
f' {self._pairlist_url}', logger.info)
pairlist = self.return_last_pairlist()
time_elapsed = 0
return pairlist, time_elapsed
def gen_pairlist(self, tickers: Tickers) -> List[str]:
"""
Generate the pairlist
:param tickers: Tickers (from exchange.get_tickers). May be cached.
:return: List of pairs
"""
if self._init_done:
pairlist = self._pair_cache.get('pairlist')
else:
pairlist = []
time_elapsed = 0.0
if pairlist:
# Item found - no refresh necessary
return pairlist.copy()
else:
if self._pairlist_url.startswith("file:///"):
filename = self._pairlist_url.split("file:///", 1)[1]
file_path = Path(filename)
if file_path.exists():
with open(filename) as json_file:
# Load the JSON data into a dictionary
jsonparse = json.load(json_file)
try:
pairlist = self.process_json(jsonparse)
except Exception as e:
if self._init_done:
pairlist = self.return_last_pairlist()
logger.warning(f'Error while processing JSON data: {type(e)}')
else:
raise OperationalException('Error while processing'
f'JSON data: {type(e)}')
else:
raise ValueError(f"{self._pairlist_url} does not exist.")
else:
# Fetch Pairlist from Remote URL
pairlist, time_elapsed = self.fetch_pairlist()
self.log_once(f"Fetched pairs: {pairlist}", logger.debug)
pairlist = self._whitelist_for_active_markets(pairlist)
pairlist = pairlist[:self._number_pairs]
self._pair_cache['pairlist'] = pairlist.copy()
if time_elapsed != 0.0:
self.log_once(f'Pairlist Fetched in {time_elapsed} seconds.', logger.info)
else:
self.log_once('Fetched Pairlist.', logger.info)
self._last_pairlist = list(pairlist)
return pairlist
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
"""
Filters and sorts pairlist and returns the whitelist again.
Called on each bot iteration - please use internal caching if necessary
:param pairlist: pairlist to filter or sort
:param tickers: Tickers (from exchange.get_tickers). May be cached.
:return: new whitelist
"""
rpl_pairlist = self.gen_pairlist(tickers)
merged_list = pairlist + rpl_pairlist
merged_list = sorted(set(merged_list), key=merged_list.index)
return merged_list

View File

@@ -135,7 +135,7 @@ class VolumePairList(IPairList):
filtered_tickers = [ filtered_tickers = [
v for k, v in tickers.items() v for k, v in tickers.items()
if (self._exchange.get_pair_quote_currency(k) == self._stake_currency if (self._exchange.get_pair_quote_currency(k) == self._stake_currency
and (self._use_range or v.get(self._sort_key) is not None) and (self._use_range or v[self._sort_key] is not None)
and v['symbol'] in _pairlist)] and v['symbol'] in _pairlist)]
pairlist = [s['symbol'] for s in filtered_tickers] pairlist = [s['symbol'] for s in filtered_tickers]
else: else:

View File

@@ -23,8 +23,7 @@ logger = logging.getLogger(__name__)
class PairListManager(LoggingMixin): class PairListManager(LoggingMixin):
def __init__( def __init__(self, exchange, config: Config, dataprovider: DataProvider = None) -> None:
self, exchange, config: Config, dataprovider: Optional[DataProvider] = None) -> None:
self._exchange = exchange self._exchange = exchange
self._config = config self._config = config
self._whitelist = self._config['exchange'].get('pair_whitelist') self._whitelist = self._config['exchange'].get('pair_whitelist')
@@ -154,8 +153,7 @@ class PairListManager(LoggingMixin):
return [] return []
return whitelist return whitelist
def create_pair_list( def create_pair_list(self, pairs: List[str], timeframe: str = None) -> ListPairsWithTimeframes:
self, pairs: List[str], timeframe: Optional[str] = None) -> ListPairsWithTimeframes:
""" """
Create list of pair tuples with (pair, timeframe) Create list of pair tuples with (pair, timeframe)
""" """

View File

@@ -89,8 +89,7 @@ class IResolver:
module = importlib.util.module_from_spec(spec) module = importlib.util.module_from_spec(spec)
try: try:
spec.loader.exec_module(module) # type: ignore # importlib does not use typehints spec.loader.exec_module(module) # type: ignore # importlib does not use typehints
except (AttributeError, ModuleNotFoundError, SyntaxError, except (ModuleNotFoundError, SyntaxError, ImportError, NameError) as err:
ImportError, NameError) as err:
# Catch errors in case a specific module is not installed # Catch errors in case a specific module is not installed
logger.warning(f"Could not import {module_path} due to '{err}'") logger.warning(f"Could not import {module_path} due to '{err}'")
if enum_failed: if enum_failed:

View File

@@ -33,7 +33,7 @@ class StrategyResolver(IResolver):
extra_path = "strategy_path" extra_path = "strategy_path"
@staticmethod @staticmethod
def load_strategy(config: Optional[Config] = None) -> IStrategy: def load_strategy(config: Config = None) -> IStrategy:
""" """
Load the custom class from config parameter Load the custom class from config parameter
:param config: configuration dictionary or None :param config: configuration dictionary or None
@@ -76,7 +76,6 @@ class StrategyResolver(IResolver):
("ignore_buying_expired_candle_after", 0), ("ignore_buying_expired_candle_after", 0),
("position_adjustment_enable", False), ("position_adjustment_enable", False),
("max_entry_position_adjustment", -1), ("max_entry_position_adjustment", -1),
("max_open_trades", -1)
] ]
for attribute, default in attributes: for attribute, default in attributes:
StrategyResolver._override_attribute_helper(strategy, config, StrategyResolver._override_attribute_helper(strategy, config,
@@ -111,11 +110,7 @@ class StrategyResolver(IResolver):
val = getattr(strategy, attribute) val = getattr(strategy, attribute)
# None's cannot exist in the config, so do not copy them # None's cannot exist in the config, so do not copy them
if val is not None: if val is not None:
# max_open_trades set to -1 in the strategy will be copied as infinity in the config config[attribute] = val
if attribute == 'max_open_trades' and val == -1:
config[attribute] = float('inf')
else:
config[attribute] = val
# Explicitly check for None here as other "falsy" values are possible # Explicitly check for None here as other "falsy" values are possible
elif default is not None: elif default is not None:
setattr(strategy, attribute, default) setattr(strategy, attribute, default)
@@ -133,8 +128,6 @@ class StrategyResolver(IResolver):
key=lambda t: t[0])) key=lambda t: t[0]))
if hasattr(strategy, 'stoploss'): if hasattr(strategy, 'stoploss'):
strategy.stoploss = float(strategy.stoploss) strategy.stoploss = float(strategy.stoploss)
if hasattr(strategy, 'max_open_trades') and strategy.max_open_trades < 0:
strategy.max_open_trades = float('inf')
return strategy return strategy
@staticmethod @staticmethod

View File

@@ -11,7 +11,6 @@ from freqtrade.configuration.config_validation import validate_config_consistenc
from freqtrade.data.btanalysis import get_backtest_resultlist, load_and_merge_backtest_result from freqtrade.data.btanalysis import get_backtest_resultlist, load_and_merge_backtest_result
from freqtrade.enums import BacktestState from freqtrade.enums import BacktestState
from freqtrade.exceptions import DependencyException from freqtrade.exceptions import DependencyException
from freqtrade.misc import deep_merge_dicts
from freqtrade.rpc.api_server.api_schemas import (BacktestHistoryEntry, BacktestRequest, from freqtrade.rpc.api_server.api_schemas import (BacktestHistoryEntry, BacktestRequest,
BacktestResponse) BacktestResponse)
from freqtrade.rpc.api_server.deps import get_config, is_webserver_mode from freqtrade.rpc.api_server.deps import get_config, is_webserver_mode
@@ -38,11 +37,10 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac
btconfig = deepcopy(config) btconfig = deepcopy(config)
settings = dict(bt_settings) settings = dict(bt_settings)
if settings.get('freqai', None) is not None:
settings['freqai'] = dict(settings['freqai'])
# Pydantic models will contain all keys, but non-provided ones are None # Pydantic models will contain all keys, but non-provided ones are None
for setting in settings.keys():
btconfig = deep_merge_dicts(settings, btconfig, allow_null_overrides=False) if settings[setting] is not None:
btconfig[setting] = settings[setting]
try: try:
btconfig['stake_amount'] = float(btconfig['stake_amount']) btconfig['stake_amount'] = float(btconfig['stake_amount'])
except ValueError: except ValueError:

View File

@@ -3,7 +3,7 @@ from typing import Any, Dict, List, Optional, Union
from pydantic import BaseModel from pydantic import BaseModel
from freqtrade.constants import DATETIME_PRINT_FORMAT, IntOrInf from freqtrade.constants import DATETIME_PRINT_FORMAT
from freqtrade.enums import OrderTypeValues, SignalDirection, TradingMode from freqtrade.enums import OrderTypeValues, SignalDirection, TradingMode
@@ -165,7 +165,7 @@ class ShowConfig(BaseModel):
stake_amount: str stake_amount: str
available_capital: Optional[float] available_capital: Optional[float]
stake_currency_decimals: int stake_currency_decimals: int
max_open_trades: IntOrInf max_open_trades: int
minimal_roi: Dict[str, Any] minimal_roi: Dict[str, Any]
stoploss: Optional[float] stoploss: Optional[float]
trailing_stop: Optional[bool] trailing_stop: Optional[bool]
@@ -217,8 +217,8 @@ class TradeSchema(BaseModel):
amount: float amount: float
amount_requested: float amount_requested: float
stake_amount: float stake_amount: float
max_stake_amount: Optional[float]
strategy: str strategy: str
buy_tag: Optional[str] # Deprecated
enter_tag: Optional[str] enter_tag: Optional[str]
timeframe: int timeframe: int
fee_open: Optional[float] fee_open: Optional[float]
@@ -243,6 +243,7 @@ class TradeSchema(BaseModel):
profit_pct: Optional[float] profit_pct: Optional[float]
profit_abs: Optional[float] profit_abs: Optional[float]
profit_fiat: Optional[float] profit_fiat: Optional[float]
sell_reason: Optional[str] # Deprecated
exit_reason: Optional[str] exit_reason: Optional[str]
exit_order_status: Optional[str] exit_order_status: Optional[str]
stop_loss_abs: Optional[float] stop_loss_abs: Optional[float]
@@ -371,10 +372,6 @@ class StrategyListResponse(BaseModel):
strategies: List[str] strategies: List[str]
class FreqAIModelListResponse(BaseModel):
freqaimodels: List[str]
class StrategyResponse(BaseModel): class StrategyResponse(BaseModel):
strategy: str strategy: str
code: str code: str
@@ -413,22 +410,15 @@ class PairHistory(BaseModel):
} }
class BacktestFreqAIInputs(BaseModel):
identifier: str
class BacktestRequest(BaseModel): class BacktestRequest(BaseModel):
strategy: str strategy: str
timeframe: Optional[str] timeframe: Optional[str]
timeframe_detail: Optional[str] timeframe_detail: Optional[str]
timerange: Optional[str] timerange: Optional[str]
max_open_trades: Optional[IntOrInf] max_open_trades: Optional[int]
stake_amount: Optional[str] stake_amount: Optional[str]
enable_protections: bool enable_protections: bool
dry_run_wallet: Optional[float] dry_run_wallet: Optional[float]
backtest_cache: Optional[str]
freqaimodel: Optional[str]
freqai: Optional[BacktestFreqAIInputs]
class BacktestResponse(BaseModel): class BacktestResponse(BaseModel):

View File

@@ -13,13 +13,12 @@ from freqtrade.rpc import RPC
from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload, from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload,
BlacklistResponse, Count, Daily, BlacklistResponse, Count, Daily,
DeleteLockRequest, DeleteTrade, ForceEnterPayload, DeleteLockRequest, DeleteTrade, ForceEnterPayload,
ForceEnterResponse, ForceExitPayload, ForceEnterResponse, ForceExitPayload, Health,
FreqAIModelListResponse, Health, Locks, Logs, Locks, Logs, OpenTradeSchema, PairHistory,
OpenTradeSchema, PairHistory, PerformanceEntry, PerformanceEntry, Ping, PlotConfig, Profit,
Ping, PlotConfig, Profit, ResultMsg, ShowConfig, ResultMsg, ShowConfig, Stats, StatusMsg,
Stats, StatusMsg, StrategyListResponse, StrategyListResponse, StrategyResponse, SysInfo,
StrategyResponse, SysInfo, Version, Version, WhitelistResponse)
WhitelistResponse)
from freqtrade.rpc.api_server.deps import get_config, get_exchange, get_rpc, get_rpc_optional from freqtrade.rpc.api_server.deps import get_config, get_exchange, get_rpc, get_rpc_optional
from freqtrade.rpc.rpc import RPCException from freqtrade.rpc.rpc import RPCException
@@ -39,9 +38,7 @@ logger = logging.getLogger(__name__)
# 2.17: Forceentry - leverage, partial force_exit # 2.17: Forceentry - leverage, partial force_exit
# 2.20: Add websocket endpoints # 2.20: Add websocket endpoints
# 2.21: Add new_candle messagetype # 2.21: Add new_candle messagetype
# 2.22: Add FreqAI to backtesting API_VERSION = 2.21
# 2.23: Allow plot config request in webserver mode
API_VERSION = 2.23
# Public API, requires no auth. # Public API, requires no auth.
router_public = APIRouter() router_public = APIRouter()
@@ -249,18 +246,8 @@ def pair_history(pair: str, timeframe: str, timerange: str, strategy: str,
@router.get('/plot_config', response_model=PlotConfig, tags=['candle data']) @router.get('/plot_config', response_model=PlotConfig, tags=['candle data'])
def plot_config(strategy: Optional[str] = None, config=Depends(get_config), def plot_config(rpc: RPC = Depends(get_rpc)):
rpc: Optional[RPC] = Depends(get_rpc_optional)): return PlotConfig.parse_obj(rpc._rpc_plot_config())
if not strategy:
if not rpc:
raise RPCException("Strategy is mandatory in webserver mode.")
return PlotConfig.parse_obj(rpc._rpc_plot_config())
else:
config1 = deepcopy(config)
config1.update({
'strategy': strategy
})
return PlotConfig.parse_obj(RPC._rpc_plot_config_with_strategy(config1))
@router.get('/strategies', response_model=StrategyListResponse, tags=['strategy']) @router.get('/strategies', response_model=StrategyListResponse, tags=['strategy'])
@@ -292,16 +279,6 @@ def get_strategy(strategy: str, config=Depends(get_config)):
} }
@router.get('/freqaimodels', response_model=FreqAIModelListResponse, tags=['freqai'])
def list_freqaimodels(config=Depends(get_config)):
from freqtrade.resolvers.freqaimodel_resolver import FreqaiModelResolver
strategies = FreqaiModelResolver.search_all_objects(
config, False)
strategies = sorted(strategies, key=lambda x: x['name'])
return {'freqaimodels': [x['name'] for x in strategies]}
@router.get('/available_pairs', response_model=AvailablePairs, tags=['candle data']) @router.get('/available_pairs', response_model=AvailablePairs, tags=['candle data'])
def list_available_pairs(timeframe: Optional[str] = None, stake_currency: Optional[str] = None, def list_available_pairs(timeframe: Optional[str] = None, stake_currency: Optional[str] = None,
candletype: Optional[CandleType] = None, config=Depends(get_config)): candletype: Optional[CandleType] = None, config=Depends(get_config)):

View File

@@ -673,7 +673,6 @@ class RPC:
if self._freqtrade.state == State.RUNNING: if self._freqtrade.state == State.RUNNING:
# Set 'max_open_trades' to 0 # Set 'max_open_trades' to 0
self._freqtrade.config['max_open_trades'] = 0 self._freqtrade.config['max_open_trades'] = 0
self._freqtrade.strategy.max_open_trades = 0
return {'status': 'No more entries will occur from now. Run /reload_config to reset.'} return {'status': 'No more entries will occur from now. Run /reload_config to reset.'}
@@ -945,7 +944,7 @@ class RPC:
resp['errors'] = errors resp['errors'] = errors
return resp return resp
def _rpc_blacklist(self, add: Optional[List[str]] = None) -> Dict: def _rpc_blacklist(self, add: List[str] = None) -> Dict:
""" Returns the currently active blacklist""" """ Returns the currently active blacklist"""
errors = {} errors = {}
if add: if add:
@@ -1127,12 +1126,12 @@ class RPC:
return self._freqtrade.active_pair_whitelist return self._freqtrade.active_pair_whitelist
@staticmethod @staticmethod
def _rpc_analysed_history_full(config: Config, pair: str, timeframe: str, def _rpc_analysed_history_full(config, pair: str, timeframe: str,
timerange: str, exchange) -> Dict[str, Any]: timerange: str, exchange) -> Dict[str, Any]:
timerange_parsed = TimeRange.parse_timerange(timerange) timerange_parsed = TimeRange.parse_timerange(timerange)
_data = load_data( _data = load_data(
datadir=config["datadir"], datadir=config.get("datadir"),
pairs=[pair], pairs=[pair],
timeframe=timeframe, timeframe=timeframe,
timerange=timerange_parsed, timerange=timerange_parsed,
@@ -1157,16 +1156,6 @@ class RPC:
self._freqtrade.strategy.plot_config['subplots'] = {} self._freqtrade.strategy.plot_config['subplots'] = {}
return self._freqtrade.strategy.plot_config return self._freqtrade.strategy.plot_config
@staticmethod
def _rpc_plot_config_with_strategy(config: Config) -> Dict[str, Any]:
from freqtrade.resolvers.strategy_resolver import StrategyResolver
strategy = StrategyResolver.load_strategy(config)
if (strategy.plot_config and 'subplots' not in strategy.plot_config):
strategy.plot_config['subplots'] = {}
return strategy.plot_config
@staticmethod @staticmethod
def _rpc_sysinfo() -> Dict[str, Any]: def _rpc_sysinfo() -> Dict[str, Any]:
return { return {

View File

@@ -1605,7 +1605,7 @@ class Telegram(RPCHandler):
def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN, def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN,
disable_notification: bool = False, disable_notification: bool = False,
keyboard: Optional[List[List[InlineKeyboardButton]]] = None, keyboard: List[List[InlineKeyboardButton]] = None,
callback_path: str = "", callback_path: str = "",
reload_able: bool = False, reload_able: bool = False,
query: Optional[CallbackQuery] = None) -> None: query: Optional[CallbackQuery] = None) -> None:

View File

@@ -4,7 +4,7 @@ This module defines a base class for auto-hyperoptable strategies.
""" """
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Iterator, List, Optional, Tuple, Type, Union from typing import Any, Dict, Iterator, List, Tuple, Type, Union
from freqtrade.constants import Config from freqtrade.constants import Config
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
@@ -36,8 +36,7 @@ class HyperStrategyMixin:
self._ft_params_from_file = params self._ft_params_from_file = params
# Init/loading of parameters is done as part of ft_bot_start(). # Init/loading of parameters is done as part of ft_bot_start().
def enumerate_parameters( def enumerate_parameters(self, category: str = None) -> Iterator[Tuple[str, BaseParameter]]:
self, category: Optional[str] = None) -> Iterator[Tuple[str, BaseParameter]]:
""" """
Find all optimizable parameters and return (name, attr) iterator. Find all optimizable parameters and return (name, attr) iterator.
:param category: :param category:
@@ -81,8 +80,6 @@ class HyperStrategyMixin:
self.stoploss = params.get('stoploss', {}).get( self.stoploss = params.get('stoploss', {}).get(
'stoploss', getattr(self, 'stoploss', -0.1)) 'stoploss', getattr(self, 'stoploss', -0.1))
self.max_open_trades = params.get('max_open_trades', {}).get(
'max_open_trades', getattr(self, 'max_open_trades', -1))
trailing = params.get('trailing', {}) trailing = params.get('trailing', {})
self.trailing_stop = trailing.get( self.trailing_stop = trailing.get(
'trailing_stop', getattr(self, 'trailing_stop', False)) 'trailing_stop', getattr(self, 'trailing_stop', False))

View File

@@ -10,7 +10,7 @@ from typing import Dict, List, Optional, Tuple, Union
import arrow import arrow
from pandas import DataFrame from pandas import DataFrame
from freqtrade.constants import Config, IntOrInf, ListPairsWithTimeframes from freqtrade.constants import Config, ListPairsWithTimeframes
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, RunMode, SignalDirection, from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, RunMode, SignalDirection,
SignalTagType, SignalType, TradingMode) SignalTagType, SignalType, TradingMode)
@@ -54,9 +54,6 @@ class IStrategy(ABC, HyperStrategyMixin):
# associated stoploss # associated stoploss
stoploss: float stoploss: float
# max open trades for the strategy
max_open_trades: IntOrInf
# trailing stoploss # trailing stoploss
trailing_stop: bool = False trailing_stop: bool = False
trailing_stop_positive: Optional[float] = None trailing_stop_positive: Optional[float] = None
@@ -598,10 +595,9 @@ class IStrategy(ABC, HyperStrategyMixin):
return None return None
def populate_any_indicators(self, pair: str, df: DataFrame, tf: str, def populate_any_indicators(self, pair: str, df: DataFrame, tf: str,
informative: Optional[DataFrame] = None, informative: DataFrame = None,
set_generalized_indicators: bool = False) -> DataFrame: set_generalized_indicators: bool = False) -> DataFrame:
""" """
DEPRECATED - USE FEATURE ENGINEERING FUNCTIONS INSTEAD
Function designed to automatically generate, name and merge features Function designed to automatically generate, name and merge features
from user indicated timeframes in the configuration file. User can add from user indicated timeframes in the configuration file. User can add
additional features here, but must follow the naming convention. additional features here, but must follow the naming convention.
@@ -614,98 +610,6 @@ class IStrategy(ABC, HyperStrategyMixin):
""" """
return df return df
def feature_engineering_expand_all(self, dataframe: DataFrame,
period: int, **kwargs):
"""
*Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined
`indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and
`include_corr_pairs`. In other words, a single feature defined in this function
will automatically expand to a total of
`indicator_periods_candles` * `include_timeframes` * `include_shifted_candles` *
`include_corr_pairs` numbers of features added to the model.
All features must be prepended with `%` to be recognized by FreqAI internals.
More details on how these config defined parameters accelerate feature engineering
in the documentation at:
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
:param df: strategy dataframe which will receive the features
:param period: period of the indicator - usage example:
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
"""
return dataframe
def feature_engineering_expand_basic(self, dataframe: DataFrame, **kwargs):
"""
*Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined
`include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`.
In other words, a single feature defined in this function
will automatically expand to a total of
`include_timeframes` * `include_shifted_candles` * `include_corr_pairs`
numbers of features added to the model.
Features defined here will *not* be automatically duplicated on user defined
`indicator_periods_candles`
All features must be prepended with `%` to be recognized by FreqAI internals.
More details on how these config defined parameters accelerate feature engineering
in the documentation at:
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
:param df: strategy dataframe which will receive the features
dataframe["%-pct-change"] = dataframe["close"].pct_change()
dataframe["%-ema-200"] = ta.EMA(dataframe, timeperiod=200)
"""
return dataframe
def feature_engineering_standard(self, dataframe: DataFrame, **kwargs):
"""
*Only functional with FreqAI enabled strategies*
This optional function will be called once with the dataframe of the base timeframe.
This is the final function to be called, which means that the dataframe entering this
function will contain all the features and columns created by all other
freqai_feature_engineering_* functions.
This function is a good place to do custom exotic feature extractions (e.g. tsfresh).
This function is a good place for any feature that should not be auto-expanded upon
(e.g. day of the week).
All features must be prepended with `%` to be recognized by FreqAI internals.
More details about feature engineering available:
https://www.freqtrade.io/en/latest/freqai-feature-engineering
:param df: strategy dataframe which will receive the features
usage example: dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7
"""
return dataframe
def set_freqai_targets(self, dataframe, **kwargs):
"""
*Only functional with FreqAI enabled strategies*
Required function to set the targets for the model.
All targets must be prepended with `&` to be recognized by the FreqAI internals.
More details about feature engineering available:
https://www.freqtrade.io/en/latest/freqai-feature-engineering
:param df: strategy dataframe which will receive the targets
usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"]
"""
return dataframe
### ###
# END - Intended to be overridden by strategy # END - Intended to be overridden by strategy
### ###
@@ -759,8 +663,7 @@ class IStrategy(ABC, HyperStrategyMixin):
""" """
return self.__class__.__name__ return self.__class__.__name__
def lock_pair(self, pair: str, until: datetime, def lock_pair(self, pair: str, until: datetime, reason: str = None, side: str = '*') -> None:
reason: Optional[str] = None, side: str = '*') -> None:
""" """
Locks pair until a given timestamp happens. Locks pair until a given timestamp happens.
Locked pairs are not analyzed, and are prevented from opening new trades. Locked pairs are not analyzed, and are prevented from opening new trades.
@@ -792,8 +695,7 @@ class IStrategy(ABC, HyperStrategyMixin):
""" """
PairLocks.unlock_reason(reason, datetime.now(timezone.utc)) PairLocks.unlock_reason(reason, datetime.now(timezone.utc))
def is_pair_locked(self, pair: str, *, candle_date: Optional[datetime] = None, def is_pair_locked(self, pair: str, *, candle_date: datetime = None, side: str = '*') -> bool:
side: str = '*') -> bool:
""" """
Checks if a pair is currently locked Checks if a pair is currently locked
The 2nd, optional parameter ensures that locks are applied until the new candle arrives, The 2nd, optional parameter ensures that locks are applied until the new candle arrives,
@@ -964,7 +866,7 @@ class IStrategy(ABC, HyperStrategyMixin):
pair: str, pair: str,
timeframe: str, timeframe: str,
dataframe: DataFrame, dataframe: DataFrame,
is_short: Optional[bool] = None is_short: bool = None
) -> Tuple[bool, bool, Optional[str]]: ) -> Tuple[bool, bool, Optional[str]]:
""" """
Calculates current exit signal based based on the dataframe Calculates current exit signal based based on the dataframe
@@ -1063,7 +965,7 @@ class IStrategy(ABC, HyperStrategyMixin):
def should_exit(self, trade: Trade, rate: float, current_time: datetime, *, def should_exit(self, trade: Trade, rate: float, current_time: datetime, *,
enter: bool, exit_: bool, enter: bool, exit_: bool,
low: Optional[float] = None, high: Optional[float] = None, low: float = None, high: float = None,
force_stoploss: float = 0) -> List[ExitCheckTuple]: force_stoploss: float = 0) -> List[ExitCheckTuple]:
""" """
This function evaluates if one of the conditions required to trigger an exit order This function evaluates if one of the conditions required to trigger an exit order
@@ -1151,8 +1053,8 @@ class IStrategy(ABC, HyperStrategyMixin):
def stop_loss_reached(self, current_rate: float, trade: Trade, def stop_loss_reached(self, current_rate: float, trade: Trade,
current_time: datetime, current_profit: float, current_time: datetime, current_profit: float,
force_stoploss: float, low: Optional[float] = None, force_stoploss: float, low: float = None,
high: Optional[float] = None) -> ExitCheckTuple: high: float = None) -> ExitCheckTuple:
""" """
Based on current profit of the trade and configured (trailing) stoploss, Based on current profit of the trade and configured (trailing) stoploss,
decides to exit or not decides to exit or not

View File

@@ -95,132 +95,65 @@ class FreqaiExampleHybridStrategy(IStrategy):
short_rsi = IntParameter(low=51, high=100, default=70, space='sell', optimize=True, load=True) short_rsi = IntParameter(low=51, high=100, default=70, space='sell', optimize=True, load=True)
exit_short_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True) exit_short_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True)
def feature_engineering_expand_all(self, dataframe, period, **kwargs): # FreqAI required function, user can add or remove indicators, but general structure
# must stay the same.
def populate_any_indicators(
self, pair, df, tf, informative=None, set_generalized_indicators=False
):
""" """
*Only functional with FreqAI enabled strategies* User feeds these indicators to FreqAI to train a classifier to decide
This function will automatically expand the defined features on the config defined if the market will go up or down.
`indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and
`include_corr_pairs`. In other words, a single feature defined in this function
will automatically expand to a total of
`indicator_periods_candles` * `include_timeframes` * `include_shifted_candles` *
`include_corr_pairs` numbers of features added to the model.
All features must be prepended with `%` to be recognized by FreqAI internals. :param pair: pair to be used as informative
:param df: strategy dataframe which will receive merges from informatives
More details on how these config defined parameters accelerate feature engineering :param tf: timeframe of the dataframe which will modify the feature names
in the documentation at: :param informative: the dataframe associated with the informative pair
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
:param df: strategy dataframe which will receive the features
:param period: period of the indicator - usage example:
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
""" """
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period) if informative is None:
dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period) informative = self.dp.get_pair_dataframe(pair, tf)
dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period)
dataframe["%-sma-period"] = ta.SMA(dataframe, timeperiod=period)
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
bollinger = qtpylib.bollinger_bands( # first loop is automatically duplicating indicators for time periods
qtpylib.typical_price(dataframe), window=period, stds=2.2 for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]:
)
dataframe["bb_lowerband-period"] = bollinger["lower"]
dataframe["bb_middleband-period"] = bollinger["mid"]
dataframe["bb_upperband-period"] = bollinger["upper"]
dataframe["%-bb_width-period"] = ( t = int(t)
dataframe["bb_upperband-period"] informative[f"%-{pair}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
- dataframe["bb_lowerband-period"] informative[f"%-{pair}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
) / dataframe["bb_middleband-period"] informative[f"%-{pair}adx-period_{t}"] = ta.ADX(informative, timeperiod=t)
dataframe["%-close-bb_lower-period"] = ( informative[f"%-{pair}sma-period_{t}"] = ta.SMA(informative, timeperiod=t)
dataframe["close"] / dataframe["bb_lowerband-period"] informative[f"%-{pair}ema-period_{t}"] = ta.EMA(informative, timeperiod=t)
) informative[f"%-{pair}roc-period_{t}"] = ta.ROC(informative, timeperiod=t)
informative[f"%-{pair}relative_volume-period_{t}"] = (
informative["volume"] / informative["volume"].rolling(t).mean()
)
dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period) # FreqAI needs the following lines in order to detect features and automatically
# expand upon them.
indicators = [col for col in informative if col.startswith("%")]
# This loop duplicates and shifts all indicators to add a sense of recency to data
for n in range(self.freqai_info["feature_parameters"]["include_shifted_candles"] + 1):
if n == 0:
continue
informative_shift = informative[indicators].shift(n)
informative_shift = informative_shift.add_suffix("_shift-" + str(n))
informative = pd.concat((informative, informative_shift), axis=1)
dataframe["%-relative_volume-period"] = ( df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True)
dataframe["volume"] / dataframe["volume"].rolling(period).mean() skip_columns = [
) (s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"]
]
df = df.drop(columns=skip_columns)
return dataframe # User can set the "target" here (in present case it is the
# "up" or "down")
if set_generalized_indicators:
# User "looks into the future" here to figure out if the future
# will be "up" or "down". This same column name is available to
# the user
df['&s-up_or_down'] = np.where(df["close"].shift(-50) >
df["close"], 'up', 'down')
def feature_engineering_expand_basic(self, dataframe, **kwargs): return df
"""
*Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined
`include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`.
In other words, a single feature defined in this function
will automatically expand to a total of
`include_timeframes` * `include_shifted_candles` * `include_corr_pairs`
numbers of features added to the model.
Features defined here will *not* be automatically duplicated on user defined
`indicator_periods_candles`
All features must be prepended with `%` to be recognized by FreqAI internals.
More details on how these config defined parameters accelerate feature engineering
in the documentation at:
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
:param df: strategy dataframe which will receive the features
dataframe["%-pct-change"] = dataframe["close"].pct_change()
dataframe["%-ema-200"] = ta.EMA(dataframe, timeperiod=200)
"""
dataframe["%-pct-change"] = dataframe["close"].pct_change()
dataframe["%-raw_volume"] = dataframe["volume"]
dataframe["%-raw_price"] = dataframe["close"]
return dataframe
def feature_engineering_standard(self, dataframe, **kwargs):
"""
*Only functional with FreqAI enabled strategies*
This optional function will be called once with the dataframe of the base timeframe.
This is the final function to be called, which means that the dataframe entering this
function will contain all the features and columns created by all other
freqai_feature_engineering_* functions.
This function is a good place to do custom exotic feature extractions (e.g. tsfresh).
This function is a good place for any feature that should not be auto-expanded upon
(e.g. day of the week).
All features must be prepended with `%` to be recognized by FreqAI internals.
More details about feature engineering available:
https://www.freqtrade.io/en/latest/freqai-feature-engineering
:param df: strategy dataframe which will receive the features
usage example: dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7
"""
dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
return dataframe
def set_freqai_targets(self, dataframe, **kwargs):
"""
*Only functional with FreqAI enabled strategies*
Required function to set the targets for the model.
All targets must be prepended with `&` to be recognized by the FreqAI internals.
More details about feature engineering available:
https://www.freqtrade.io/en/latest/freqai-feature-engineering
:param df: strategy dataframe which will receive the targets
usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"]
"""
dataframe['&s-up_or_down'] = np.where(dataframe["close"].shift(-50) >
dataframe["close"], 'up', 'down')
return dataframe
# flake8: noqa: C901 # flake8: noqa: C901
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:

View File

@@ -1,11 +1,12 @@
import logging import logging
from functools import reduce from functools import reduce
import pandas as pd
import talib.abstract as ta import talib.abstract as ta
from pandas import DataFrame from pandas import DataFrame
from technical import qtpylib from technical import qtpylib
from freqtrade.strategy import CategoricalParameter, IStrategy from freqtrade.strategy import CategoricalParameter, IStrategy, merge_informative_pair
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -17,8 +18,8 @@ class FreqaiExampleStrategy(IStrategy):
IFreqaiModel to the strategy. Namely, the user uses: IFreqaiModel to the strategy. Namely, the user uses:
self.freqai.start(dataframe, metadata) self.freqai.start(dataframe, metadata)
to make predictions on their data. feature_engineering_*() automatically to make predictions on their data. populate_any_indicators() automatically
generate the variety of features indicated by the user in the generates the variety of features indicated by the user in the
canonical freqtrade configuration file under config['freqai']. canonical freqtrade configuration file under config['freqai'].
""" """
@@ -27,7 +28,7 @@ class FreqaiExampleStrategy(IStrategy):
plot_config = { plot_config = {
"main_plot": {}, "main_plot": {},
"subplots": { "subplots": {
"&-s_close": {"prediction": {"color": "blue"}}, "prediction": {"prediction": {"color": "blue"}},
"do_predict": { "do_predict": {
"do_predict": {"color": "brown"}, "do_predict": {"color": "brown"},
}, },
@@ -39,179 +40,133 @@ class FreqaiExampleStrategy(IStrategy):
use_exit_signal = True use_exit_signal = True
# this is the maximum period fed to talib (timeframe independent) # this is the maximum period fed to talib (timeframe independent)
startup_candle_count: int = 40 startup_candle_count: int = 40
can_short = True can_short = False
std_dev_multiplier_buy = CategoricalParameter( std_dev_multiplier_buy = CategoricalParameter(
[0.75, 1, 1.25, 1.5, 1.75], default=1.25, space="buy", optimize=True) [0.75, 1, 1.25, 1.5, 1.75], default=1.25, space="buy", optimize=True)
std_dev_multiplier_sell = CategoricalParameter( std_dev_multiplier_sell = CategoricalParameter(
[0.75, 1, 1.25, 1.5, 1.75], space="sell", default=1.25, optimize=True) [0.75, 1, 1.25, 1.5, 1.75], space="sell", default=1.25, optimize=True)
def feature_engineering_expand_all(self, dataframe, period, **kwargs): def populate_any_indicators(
self, pair, df, tf, informative=None, set_generalized_indicators=False
):
""" """
*Only functional with FreqAI enabled strategies* Function designed to automatically generate, name and merge features
This function will automatically expand the defined features on the config defined from user indicated timeframes in the configuration file. User controls the indicators
`indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and passed to the training/prediction by prepending indicators with `f'%-{pair}`
`include_corr_pairs`. In other words, a single feature defined in this function (see convention below). I.e. user should not prepend any supporting metrics
will automatically expand to a total of (e.g. bb_lowerband below) with % unless they explicitly want to pass that metric to the
`indicator_periods_candles` * `include_timeframes` * `include_shifted_candles` * model.
`include_corr_pairs` numbers of features added to the model. :param pair: pair to be used as informative
:param df: strategy dataframe which will receive merges from informatives
All features must be prepended with `%` to be recognized by FreqAI internals. :param tf: timeframe of the dataframe which will modify the feature names
:param informative: the dataframe associated with the informative pair
More details on how these config defined parameters accelerate feature engineering
in the documentation at:
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
:param df: strategy dataframe which will receive the features
:param period: period of the indicator - usage example:
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
""" """
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period) if informative is None:
dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period) informative = self.dp.get_pair_dataframe(pair, tf)
dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period)
dataframe["%-sma-period"] = ta.SMA(dataframe, timeperiod=period)
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
bollinger = qtpylib.bollinger_bands( # first loop is automatically duplicating indicators for time periods
qtpylib.typical_price(dataframe), window=period, stds=2.2 for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]:
)
dataframe["bb_lowerband-period"] = bollinger["lower"]
dataframe["bb_middleband-period"] = bollinger["mid"]
dataframe["bb_upperband-period"] = bollinger["upper"]
dataframe["%-bb_width-period"] = ( t = int(t)
dataframe["bb_upperband-period"] informative[f"%-{pair}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
- dataframe["bb_lowerband-period"] informative[f"%-{pair}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
) / dataframe["bb_middleband-period"] informative[f"%-{pair}adx-period_{t}"] = ta.ADX(informative, timeperiod=t)
dataframe["%-close-bb_lower-period"] = ( informative[f"%-{pair}sma-period_{t}"] = ta.SMA(informative, timeperiod=t)
dataframe["close"] / dataframe["bb_lowerband-period"] informative[f"%-{pair}ema-period_{t}"] = ta.EMA(informative, timeperiod=t)
)
dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period) bollinger = qtpylib.bollinger_bands(
qtpylib.typical_price(informative), window=t, stds=2.2
)
informative[f"{pair}bb_lowerband-period_{t}"] = bollinger["lower"]
informative[f"{pair}bb_middleband-period_{t}"] = bollinger["mid"]
informative[f"{pair}bb_upperband-period_{t}"] = bollinger["upper"]
dataframe["%-relative_volume-period"] = ( informative[f"%-{pair}bb_width-period_{t}"] = (
dataframe["volume"] / dataframe["volume"].rolling(period).mean() informative[f"{pair}bb_upperband-period_{t}"]
) - informative[f"{pair}bb_lowerband-period_{t}"]
) / informative[f"{pair}bb_middleband-period_{t}"]
return dataframe informative[f"%-{pair}close-bb_lower-period_{t}"] = (
informative["close"] / informative[f"{pair}bb_lowerband-period_{t}"]
def feature_engineering_expand_basic(self, dataframe, **kwargs):
"""
*Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined
`include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`.
In other words, a single feature defined in this function
will automatically expand to a total of
`include_timeframes` * `include_shifted_candles` * `include_corr_pairs`
numbers of features added to the model.
Features defined here will *not* be automatically duplicated on user defined
`indicator_periods_candles`
All features must be prepended with `%` to be recognized by FreqAI internals.
More details on how these config defined parameters accelerate feature engineering
in the documentation at:
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
:param df: strategy dataframe which will receive the features
dataframe["%-pct-change"] = dataframe["close"].pct_change()
dataframe["%-ema-200"] = ta.EMA(dataframe, timeperiod=200)
"""
dataframe["%-pct-change"] = dataframe["close"].pct_change()
dataframe["%-raw_volume"] = dataframe["volume"]
dataframe["%-raw_price"] = dataframe["close"]
return dataframe
def feature_engineering_standard(self, dataframe, **kwargs):
"""
*Only functional with FreqAI enabled strategies*
This optional function will be called once with the dataframe of the base timeframe.
This is the final function to be called, which means that the dataframe entering this
function will contain all the features and columns created by all other
freqai_feature_engineering_* functions.
This function is a good place to do custom exotic feature extractions (e.g. tsfresh).
This function is a good place for any feature that should not be auto-expanded upon
(e.g. day of the week).
All features must be prepended with `%` to be recognized by FreqAI internals.
More details about feature engineering available:
https://www.freqtrade.io/en/latest/freqai-feature-engineering
:param df: strategy dataframe which will receive the features
usage example: dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7
"""
dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
return dataframe
def set_freqai_targets(self, dataframe, **kwargs):
"""
*Only functional with FreqAI enabled strategies*
Required function to set the targets for the model.
All targets must be prepended with `&` to be recognized by the FreqAI internals.
More details about feature engineering available:
https://www.freqtrade.io/en/latest/freqai-feature-engineering
:param df: strategy dataframe which will receive the targets
usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"]
"""
dataframe["&-s_close"] = (
dataframe["close"]
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
.mean()
/ dataframe["close"]
- 1
) )
# Classifiers are typically set up with strings as targets: informative[f"%-{pair}roc-period_{t}"] = ta.ROC(informative, timeperiod=t)
# df['&s-up_or_down'] = np.where( df["close"].shift(-100) >
# df["close"], 'up', 'down')
# If user wishes to use multiple targets, they can add more by informative[f"%-{pair}relative_volume-period_{t}"] = (
# appending more columns with '&'. User should keep in mind that multi targets informative["volume"] / informative["volume"].rolling(t).mean()
# requires a multioutput prediction model such as )
# freqai/prediction_models/CatboostRegressorMultiTarget.py,
# freqtrade trade --freqaimodel CatboostRegressorMultiTarget
# df["&-s_range"] = ( informative[f"%-{pair}pct-change"] = informative["close"].pct_change()
# df["close"] informative[f"%-{pair}raw_volume"] = informative["volume"]
# .shift(-self.freqai_info["feature_parameters"]["label_period_candles"]) informative[f"%-{pair}raw_price"] = informative["close"]
# .rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
# .max()
# -
# df["close"]
# .shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
# .rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
# .min()
# )
return dataframe indicators = [col for col in informative if col.startswith("%")]
# This loop duplicates and shifts all indicators to add a sense of recency to data
for n in range(self.freqai_info["feature_parameters"]["include_shifted_candles"] + 1):
if n == 0:
continue
informative_shift = informative[indicators].shift(n)
informative_shift = informative_shift.add_suffix("_shift-" + str(n))
informative = pd.concat((informative, informative_shift), axis=1)
df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True)
skip_columns = [
(s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"]
]
df = df.drop(columns=skip_columns)
# Add generalized indicators here (because in live, it will call this
# function to populate indicators during training). Notice how we ensure not to
# add them multiple times
if set_generalized_indicators:
df["%-day_of_week"] = (df["date"].dt.dayofweek + 1) / 7
df["%-hour_of_day"] = (df["date"].dt.hour + 1) / 25
# user adds targets here by prepending them with &- (see convention below)
df["&-s_close"] = (
df["close"]
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
.mean()
/ df["close"]
- 1
)
# Classifiers are typically set up with strings as targets:
# df['&s-up_or_down'] = np.where( df["close"].shift(-100) >
# df["close"], 'up', 'down')
# If user wishes to use multiple targets, they can add more by
# appending more columns with '&'. User should keep in mind that multi targets
# requires a multioutput prediction model such as
# templates/CatboostPredictionMultiModel.py,
# df["&-s_range"] = (
# df["close"]
# .shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
# .rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
# .max()
# -
# df["close"]
# .shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
# .rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
# .min()
# )
return df
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# All indicators must be populated by feature_engineering_*() functions # All indicators must be populated by populate_any_indicators() for live functionality
# to work correctly.
# the model will return all labels created by user in `feature_engineering_*` # the model will return all labels created by user in `populate_any_indicators`
# (& appended targets), an indication of whether or not the prediction should be accepted, # (& appended targets), an indication of whether or not the prediction should be accepted,
# the target mean/std values for each of the labels created by user in # the target mean/std values for each of the labels created by user in
# `set_freqai_targets()` for each training period. # `populate_any_indicators()` for each training period.
dataframe = self.freqai.start(dataframe, metadata, self) dataframe = self.freqai.start(dataframe, metadata, self)
for val in self.std_dev_multiplier_buy.range: for val in self.std_dev_multiplier_buy.range:
dataframe[f'target_roi_{val}'] = ( dataframe[f'target_roi_{val}'] = (
dataframe["&-s_close_mean"] + dataframe["&-s_close_std"] * val dataframe["&-s_close_mean"] + dataframe["&-s_close_std"] * val

View File

@@ -41,6 +41,20 @@
"pairlists": [ "pairlists": [
{{ '{"method": "StaticPairList"}' if exchange_name == 'bittrex' else volume_pairlist }} {{ '{"method": "StaticPairList"}' if exchange_name == 'bittrex' else volume_pairlist }}
], ],
"edge": {
"enabled": false,
"process_throttle_secs": 3600,
"calculate_since_number_of_days": 7,
"allowed_risk": 0.01,
"stoploss_range_min": -0.01,
"stoploss_range_max": -0.1,
"stoploss_range_step": -0.01,
"minimum_winrate": 0.60,
"minimum_expectancy": 0.20,
"min_trade_number": 10,
"max_trade_duration_minute": 1440,
"remove_pumps": false
},
"telegram": { "telegram": {
"enabled": {{ telegram | lower }}, "enabled": {{ telegram | lower }},
"token": "{{ telegram_token }}", "token": "{{ telegram_token }}",

View File

@@ -1,78 +0,0 @@
import logging
from packaging import version
from freqtrade.constants import Config
from freqtrade.enums.tradingmode import TradingMode
from freqtrade.exceptions import OperationalException
from freqtrade.persistence.pairlock import PairLock
from freqtrade.persistence.trade_model import Trade
logger = logging.getLogger(__name__)
def migrate_binance_futures_names(config: Config):
if (
not (config.get('trading_mode', TradingMode.SPOT) == TradingMode.FUTURES
and config['exchange']['name'] == 'binance')
):
# only act on new futures
return
import ccxt
if version.parse("2.6.26") > version.parse(ccxt.__version__):
raise OperationalException(
"Please follow the update instructions in the docs "
"(https://www.freqtrade.io/en/latest/updating/) to install a compatible ccxt version.")
_migrate_binance_futures_db(config)
migrate_binance_futures_data(config)
def _migrate_binance_futures_db(config: Config):
logger.warning('Migrating binance futures pairs in database.')
trades = Trade.get_trades([Trade.exchange == 'binance', Trade.trading_mode == 'FUTURES']).all()
for trade in trades:
if ':' in trade.pair:
# already migrated
continue
new_pair = f"{trade.pair}:{trade.stake_currency}"
trade.pair = new_pair
for order in trade.orders:
order.ft_pair = new_pair
# Should symbol be migrated too?
# order.symbol = new_pair
Trade.commit()
pls = PairLock.query.filter(PairLock.pair.notlike('%:%'))
for pl in pls:
pl.pair = f"{pl.pair}:{config['stake_currency']}"
# print(pls)
# pls.update({'pair': concat(PairLock.pair,':USDT')})
Trade.commit()
logger.warning('Done migrating binance futures pairs in database.')
def migrate_binance_futures_data(config: Config):
if (
not (config.get('trading_mode', TradingMode.SPOT) == TradingMode.FUTURES
and config['exchange']['name'] == 'binance')
):
# only act on new futures
return
from freqtrade.data.history.idatahandler import get_datahandler
dhc = get_datahandler(config['datadir'], config.get('dataformat_ohlcv', 'json'))
paircombs = dhc.ohlcv_get_available_data(
config['datadir'],
config.get('trading_mode', TradingMode.SPOT)
)
for pair, timeframe, candle_type in paircombs:
if ':' in pair:
# already migrated
continue
new_pair = f"{pair}:{config['stake_currency']}"
dhc.rename_futures_data(pair, new_pair, timeframe, candle_type)

View File

@@ -291,22 +291,17 @@ class Wallets:
return self._check_available_stake_amount(stake_amount, available_amount) return self._check_available_stake_amount(stake_amount, available_amount)
def validate_stake_amount(self, pair: str, stake_amount: Optional[float], def validate_stake_amount(self, pair: str, stake_amount: Optional[float],
min_stake_amount: Optional[float], max_stake_amount: float, min_stake_amount: Optional[float], max_stake_amount: float):
trade_amount: Optional[float]):
if not stake_amount: if not stake_amount:
logger.debug(f"Stake amount is {stake_amount}, ignoring possible trade for {pair}.") logger.debug(f"Stake amount is {stake_amount}, ignoring possible trade for {pair}.")
return 0 return 0
max_allowed_stake = min(max_stake_amount, self.get_available_stake_amount()) max_stake_amount = min(max_stake_amount, self.get_available_stake_amount())
if trade_amount:
# if in a trade, then the resulting trade size cannot go beyond the max stake
# Otherwise we could no longer exit.
max_allowed_stake = min(max_allowed_stake, max_stake_amount - trade_amount)
if min_stake_amount is not None and min_stake_amount > max_allowed_stake: if min_stake_amount is not None and min_stake_amount > max_stake_amount:
if self._log: if self._log:
logger.warning("Minimum stake amount > available balance. " logger.warning("Minimum stake amount > available balance. "
f"{min_stake_amount} > {max_allowed_stake}") f"{min_stake_amount} > {max_stake_amount}")
return 0 return 0
if min_stake_amount is not None and stake_amount < min_stake_amount: if min_stake_amount is not None and stake_amount < min_stake_amount:
if self._log: if self._log:
@@ -325,11 +320,11 @@ class Wallets:
return 0 return 0
stake_amount = min_stake_amount stake_amount = min_stake_amount
if stake_amount > max_allowed_stake: if stake_amount > max_stake_amount:
if self._log: if self._log:
logger.info( logger.info(
f"Stake amount for pair {pair} is too big " f"Stake amount for pair {pair} is too big "
f"({stake_amount} > {max_allowed_stake}), adjusting to {max_allowed_stake}." f"({stake_amount} > {max_stake_amount}), adjusting to {max_stake_amount}."
) )
stake_amount = max_allowed_stake stake_amount = max_stake_amount
return stake_amount return stake_amount

View File

@@ -26,7 +26,7 @@ class Worker:
Freqtradebot worker class Freqtradebot worker class
""" """
def __init__(self, args: Dict[str, Any], config: Optional[Config] = None) -> None: def __init__(self, args: Dict[str, Any], config: Config = None) -> None:
""" """
Init all variables and objects the bot needs to work Init all variables and objects the bot needs to work
""" """

View File

@@ -41,7 +41,6 @@ nav:
- Backtest analysis: advanced-backtesting.md - Backtest analysis: advanced-backtesting.md
- Advanced Topics: - Advanced Topics:
- Advanced Post-installation Tasks: advanced-setup.md - Advanced Post-installation Tasks: advanced-setup.md
- Trade Object: trade-object.md
- Advanced Strategy: strategy-advanced.md - Advanced Strategy: strategy-advanced.md
- Advanced Hyperopt: advanced-hyperopt.md - Advanced Hyperopt: advanced-hyperopt.md
- Producer/Consumer mode: producer-consumer.md - Producer/Consumer mode: producer-consumer.md
@@ -59,11 +58,7 @@ theme:
favicon: "images/logo.png" favicon: "images/logo.png"
custom_dir: "docs/overrides" custom_dir: "docs/overrides"
features: features:
- content.code.annotate
- search.share - search.share
- content.code.copy
- navigation.top
- navigation.footer
palette: palette:
- scheme: default - scheme: default
primary: "blue grey" primary: "blue grey"

View File

@@ -31,6 +31,7 @@ asyncio_mode = "auto"
[tool.mypy] [tool.mypy]
ignore_missing_imports = true ignore_missing_imports = true
namespace_packages = false namespace_packages = false
implicit_optional = true
warn_unused_ignores = true warn_unused_ignores = true
exclude = [ exclude = [
'^build_helpers\.py$' '^build_helpers\.py$'
@@ -40,11 +41,6 @@ exclude = [
module = "tests.*" module = "tests.*"
ignore_errors = true ignore_errors = true
[[tool.mypy.overrides]]
# Telegram does not use implicit_optional = false in the current version.
module = "telegram.*"
implicit_optional = true
[build-system] [build-system]
requires = ["setuptools >= 46.4.0", "wheel"] requires = ["setuptools >= 46.4.0", "wheel"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
@@ -56,3 +52,6 @@ exclude = [
"build_helpers/*.py", "build_helpers/*.py",
] ]
ignore = ["freqtrade/vendor/**"] ignore = ["freqtrade/vendor/**"]
# Align pyright to mypy config
strictParameterNoneValue = false

View File

@@ -10,24 +10,24 @@ coveralls==3.3.1
flake8==6.0.0 flake8==6.0.0
flake8-tidy-imports==4.8.0 flake8-tidy-imports==4.8.0
mypy==0.991 mypy==0.991
pre-commit==2.21.0 pre-commit==2.20.0
pytest==7.2.1 pytest==7.2.0
pytest-asyncio==0.20.3 pytest-asyncio==0.20.3
pytest-cov==4.0.0 pytest-cov==4.0.0
pytest-mock==3.10.0 pytest-mock==3.10.0
pytest-random-order==1.1.0 pytest-random-order==1.1.0
isort==5.11.4 isort==5.10.1
# For datetime mocking # For datetime mocking
time-machine==2.9.0 time-machine==2.8.2
# fastapi testing # fastapi testing
httpx==0.23.3 httpx==0.23.1
# Convert jupyter notebooks to markdown documents # Convert jupyter notebooks to markdown documents
nbconvert==7.2.8 nbconvert==7.2.6
# mypy types # mypy types
types-cachetools==5.2.1 types-cachetools==5.2.1
types-filelock==3.2.7 types-filelock==3.2.7
types-requests==2.28.11.8 types-requests==2.28.11.5
types-tabulate==0.9.0.0 types-tabulate==0.9.0.0
types-python-dateutil==2.8.19.6 types-python-dateutil==2.8.19.4

View File

@@ -2,7 +2,7 @@
-r requirements-freqai.txt -r requirements-freqai.txt
# Required for freqai-rl # Required for freqai-rl
torch==1.13.1 torch==1.13.0
stable-baselines3==1.6.2 stable-baselines3==1.6.2
sb3-contrib==1.6.2 sb3-contrib==1.6.2
# Gym is forced to this version by stable-baselines3. # Gym is forced to this version by stable-baselines3.

View File

@@ -6,6 +6,7 @@
scikit-learn==1.1.3 scikit-learn==1.1.3
joblib==1.2.0 joblib==1.2.0
catboost==1.1.1; platform_machine != 'aarch64' catboost==1.1.1; platform_machine != 'aarch64'
lightgbm==3.3.4 lightgbm==3.3.3
xgboost==1.7.3 xgboost==1.7.2
tensorboard==2.11.2 tensorboard==2.11.0
tensorflow==2.11.0

View File

@@ -2,8 +2,8 @@
-r requirements.txt -r requirements.txt
# Required for hyperopt # Required for hyperopt
scipy==1.10.0 scipy==1.9.3
scikit-learn==1.1.3 scikit-learn==1.1.3
scikit-optimize==0.9.0 scikit-optimize==0.9.0
filelock==3.9.0 filelock==3.8.2
progressbar2==4.2.0 progressbar2==4.2.0

View File

@@ -1,26 +1,27 @@
numpy==1.24.1 numpy==1.23.5
pandas==1.5.3 pandas==1.5.2
pandas-ta==0.3.14b pandas-ta==0.3.14b
ccxt==2.7.12 ccxt==2.2.92
# 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; platform_machine == 'armv7l' cryptography==38.0.1; platform_machine == 'armv7l'
cryptography==39.0.0; platform_machine != 'armv7l' cryptography==38.0.4; platform_machine != 'armv7l'
aiohttp==3.8.3 aiohttp==3.8.3
SQLAlchemy==1.4.46 SQLAlchemy==1.4.45
python-telegram-bot==13.15 python-telegram-bot==13.15
arrow==1.2.3 arrow==1.2.3
cachetools==4.2.2 cachetools==4.2.2
requests==2.28.2 requests==2.28.1
urllib3==1.26.14 urllib3==1.26.13
jsonschema==4.17.3 jsonschema==4.17.3
TA-Lib==0.4.25 TA-Lib==0.4.25
technical==1.3.0 technical==1.3.0
tabulate==0.9.0 tabulate==0.9.0
pycoingecko==3.1.0 pycoingecko==3.1.0
jinja2==3.1.2 jinja2==3.1.2
tables==3.8.0 tables==3.7.0
blosc==1.11.1 blosc==1.10.6; platform_machine == 'arm64'
blosc==1.11.0; platform_machine != 'arm64'
joblib==1.2.0 joblib==1.2.0
pyarrow==10.0.1; platform_machine != 'armv7l' pyarrow==10.0.1; platform_machine != 'armv7l'
@@ -30,14 +31,14 @@ py_find_1st==1.1.5
# Load ticker files 30% faster # Load ticker files 30% faster
python-rapidjson==1.9 python-rapidjson==1.9
# Properly format api responses # Properly format api responses
orjson==3.8.5 orjson==3.8.3
# Notify systemd # Notify systemd
sdnotify==0.3.2 sdnotify==0.3.2
# API Server # API Server
fastapi==0.89.1 fastapi==0.88.0
pydantic==1.10.4 pydantic==1.10.2
uvicorn==0.20.0 uvicorn==0.20.0
pyjwt==2.6.0 pyjwt==2.6.0
aiofiles==22.1.0 aiofiles==22.1.0

View File

@@ -14,7 +14,6 @@ import logging
import re import re
import sys import sys
from pathlib import Path from pathlib import Path
from typing import Optional
from urllib.parse import urlencode, urlparse, urlunparse from urllib.parse import urlencode, urlparse, urlunparse
import rapidjson import rapidjson
@@ -37,7 +36,7 @@ class FtRestClient():
self._session = requests.Session() self._session = requests.Session()
self._session.auth = (username, password) self._session.auth = (username, password)
def _call(self, method, apipath, params: Optional[dict] = None, data=None, files=None): def _call(self, method, apipath, params: dict = None, data=None, files=None):
if str(method).upper() not in ('GET', 'POST', 'PUT', 'DELETE'): if str(method).upper() not in ('GET', 'POST', 'PUT', 'DELETE'):
raise ValueError(f'invalid method <{method}>') raise ValueError(f'invalid method <{method}>')
@@ -61,13 +60,13 @@ class FtRestClient():
except ConnectionError: except ConnectionError:
logger.warning("Connection error") logger.warning("Connection error")
def _get(self, apipath, params: Optional[dict] = None): def _get(self, apipath, params: dict = None):
return self._call("GET", apipath, params=params) return self._call("GET", apipath, params=params)
def _delete(self, apipath, params: Optional[dict] = None): def _delete(self, apipath, params: dict = None):
return self._call("DELETE", apipath, params=params) return self._call("DELETE", apipath, params=params)
def _post(self, apipath, params: Optional[dict] = None, data: Optional[dict] = None): def _post(self, apipath, params: dict = None, data: dict = None):
return self._call("POST", apipath, params=params, data=data) return self._call("POST", apipath, params=params, data=data)
def start(self): def start(self):

Some files were not shown because too many files have changed in this diff Show More