Merge branch 'develop' of github.com:lolongcovas/freqtrade into strategies

This commit is contained in:
longyu 2023-01-14 08:58:26 +01:00
commit d595d9000d
125 changed files with 4135 additions and 1950 deletions

View File

@ -20,7 +20,7 @@ Please do not use bug reports to request new features.
* Operating system: ____ * Operating system: ____
* Python Version: _____ (`python -V`) * Python Version: _____ (`python -V`)
* CCXT version: _____ (`pip freeze | grep ccxt`) * CCXT version: _____ (`pip freeze | grep ccxt`)
* Freqtrade Version: ____ (`freqtrade -V` or `docker-compose run --rm freqtrade -V` for Freqtrade running in docker) * Freqtrade Version: ____ (`freqtrade -V` or `docker compose run --rm freqtrade -V` for Freqtrade running in docker)
Note: All issues other than enhancement requests will be closed without further comment if the above template is deleted or not filled out. Note: All issues other than enhancement requests will be closed without further comment if the above template is deleted or not filled out.

View File

@ -18,7 +18,7 @@ Have you search for this feature before requesting it? It's highly likely that a
* Operating system: ____ * Operating system: ____
* Python Version: _____ (`python -V`) * Python Version: _____ (`python -V`)
* CCXT version: _____ (`pip freeze | grep ccxt`) * CCXT version: _____ (`pip freeze | grep ccxt`)
* Freqtrade Version: ____ (`freqtrade -V` or `docker-compose run --rm freqtrade -V` for Freqtrade running in docker) * Freqtrade Version: ____ (`freqtrade -V` or `docker compose run --rm freqtrade -V` for Freqtrade running in docker)
## Describe the enhancement ## Describe the enhancement

View File

@ -18,7 +18,7 @@ Please do not use the question template to report bugs or to request new feature
* Operating system: ____ * Operating system: ____
* Python Version: _____ (`python -V`) * Python Version: _____ (`python -V`)
* CCXT version: _____ (`pip freeze | grep ccxt`) * CCXT version: _____ (`pip freeze | grep ccxt`)
* Freqtrade Version: ____ (`freqtrade -V` or `docker-compose run --rm freqtrade -V` for Freqtrade running in docker) * Freqtrade Version: ____ (`freqtrade -V` or `docker compose run --rm freqtrade -V` for Freqtrade running in docker)
## Your question ## Your question

View File

@ -88,7 +88,7 @@ jobs:
run: | run: |
cp config_examples/config_bittrex.example.json config.json cp config_examples/config_bittrex.example.json config.json
freqtrade create-userdir --userdir user_data freqtrade create-userdir --userdir user_data
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all freqtrade hyperopt --datadir tests/testdata -e 6 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
- name: Flake8 - name: Flake8
run: | run: |
@ -148,6 +148,19 @@ 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
@ -410,7 +423,7 @@ jobs:
python setup.py sdist bdist_wheel python setup.py sdist bdist_wheel
- name: Publish to PyPI (Test) - name: Publish to PyPI (Test)
uses: pypa/gh-action-pypi-publish@v1.6.1 uses: pypa/gh-action-pypi-publish@v1.6.4
if: (github.event_name == 'release') if: (github.event_name == 'release')
with: with:
user: __token__ user: __token__
@ -418,7 +431,7 @@ jobs:
repository_url: https://test.pypi.org/legacy/ repository_url: https://test.pypi.org/legacy/
- name: Publish to PyPI - name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@v1.6.1 uses: pypa/gh-action-pypi-publish@v1.6.4
if: (github.event_name == 'release') if: (github.event_name == 'release')
with: with:
user: __token__ user: __token__

View File

@ -15,9 +15,9 @@ repos:
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.5 - types-requests==2.28.11.7
- types-tabulate==0.9.0.0 - types-tabulate==0.9.0.0
- types-python-dateutil==2.8.19.4 - types-python-dateutil==2.8.19.5
# stages: [push] # stages: [push]
- repo: https://github.com/pycqa/isort - repo: https://github.com/pycqa/isort

View File

@ -1,6 +1,7 @@
# ![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

@ -79,9 +79,7 @@
"test_size": 0.33, "test_size": 0.33,
"random_state": 1 "random_state": 1
}, },
"model_training_parameters": { "model_training_parameters": {}
"n_estimators": 1000
}
}, },
"bot_name": "", "bot_name": "",
"force_entry_enable": true, "force_entry_enable": true,

View File

@ -300,7 +300,11 @@ 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 |
| | | | | |
@ -400,7 +404,11 @@ 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 |
| | | | | |
@ -447,6 +455,9 @@ 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

@ -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](installation.md/#quick-start) method for installing If you used the [Quick start](docker_quickstart.md#docker-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.

View File

@ -5,7 +5,7 @@ You can analyze the results of backtests and trading history easily using Jupyte
## Quick start with docker ## Quick start with docker
Freqtrade provides a docker-compose file which starts up a jupyter lab server. Freqtrade provides a docker-compose file which starts up a jupyter lab server.
You can run this server using the following command: `docker-compose -f docker/docker-compose-jupyter.yml up` You can run this server using the following command: `docker compose -f docker/docker-compose-jupyter.yml up`
This will create a dockercontainer running jupyter lab, which will be accessible using `https://127.0.0.1:8888/lab`. This will create a dockercontainer running jupyter lab, which will be accessible using `https://127.0.0.1:8888/lab`.
Please use the link that's printed in the console after startup for simplified login. Please use the link that's printed in the console after startup for simplified login.

View File

@ -4,20 +4,22 @@ This page explains how to run the bot with Docker. It is not meant to work out o
## Install Docker ## Install Docker
Start by downloading and installing Docker CE for your platform: Start by downloading and installing Docker / Docker Desktop for your platform:
* [Mac](https://docs.docker.com/docker-for-mac/install/) * [Mac](https://docs.docker.com/docker-for-mac/install/)
* [Windows](https://docs.docker.com/docker-for-windows/install/) * [Windows](https://docs.docker.com/docker-for-windows/install/)
* [Linux](https://docs.docker.com/install/) * [Linux](https://docs.docker.com/install/)
To simplify running freqtrade, [`docker-compose`](https://docs.docker.com/compose/install/) should be installed and available to follow the below [docker quick start guide](#docker-quick-start). !!! Info "Docker compose install"
Freqtrade documentation assumes the use of Docker desktop (or the docker compose plugin).
While the docker-compose standalone installation still works, it will require changing all `docker compose` commands from `docker compose` to `docker-compose` to work (e.g. `docker compose up -d` will become `docker-compose up -d`).
## Freqtrade with docker-compose ## Freqtrade with docker
Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/), as well as a [docker-compose file](https://github.com/freqtrade/freqtrade/blob/stable/docker-compose.yml) ready for usage. Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/), as well as a [docker compose file](https://github.com/freqtrade/freqtrade/blob/stable/docker-compose.yml) ready for usage.
!!! Note !!! Note
- The following section assumes that `docker` and `docker-compose` are installed and available to the logged in user. - The following section assumes that `docker` is installed and available to the logged in user.
- All below commands use relative directories and will have to be executed from the directory containing the `docker-compose.yml` file. - All below commands use relative directories and will have to be executed from the directory containing the `docker-compose.yml` file.
### Docker quick start ### Docker quick start
@ -31,13 +33,13 @@ cd ft_userdata/
curl https://raw.githubusercontent.com/freqtrade/freqtrade/stable/docker-compose.yml -o docker-compose.yml curl https://raw.githubusercontent.com/freqtrade/freqtrade/stable/docker-compose.yml -o docker-compose.yml
# Pull the freqtrade image # Pull the freqtrade image
docker-compose pull docker compose pull
# Create user directory structure # Create user directory structure
docker-compose run --rm freqtrade create-userdir --userdir user_data docker compose run --rm freqtrade create-userdir --userdir user_data
# Create configuration - Requires answering interactive questions # Create configuration - Requires answering interactive questions
docker-compose run --rm freqtrade new-config --config user_data/config.json docker compose run --rm freqtrade new-config --config user_data/config.json
``` ```
The above snippet creates a new directory called `ft_userdata`, downloads the latest compose file and pulls the freqtrade image. The above snippet creates a new directory called `ft_userdata`, downloads the latest compose file and pulls the freqtrade image.
@ -64,7 +66,7 @@ The `SampleStrategy` is run by default.
Once this is done, you're ready to launch the bot in trading mode (Dry-run or Live-trading, depending on your answer to the corresponding question you made above). Once this is done, you're ready to launch the bot in trading mode (Dry-run or Live-trading, depending on your answer to the corresponding question you made above).
``` bash ``` bash
docker-compose up -d docker compose up -d
``` ```
!!! Warning "Default configuration" !!! Warning "Default configuration"
@ -84,27 +86,27 @@ You can now access the UI by typing localhost:8080 in your browser.
#### Monitoring the bot #### Monitoring the bot
You can check for running instances with `docker-compose ps`. You can check for running instances with `docker compose ps`.
This should list the service `freqtrade` as `running`. If that's not the case, best check the logs (see next point). This should list the service `freqtrade` as `running`. If that's not the case, best check the logs (see next point).
#### Docker-compose logs #### Docker compose logs
Logs will be written to: `user_data/logs/freqtrade.log`. Logs will be written to: `user_data/logs/freqtrade.log`.
You can also check the latest log with the command `docker-compose logs -f`. You can also check the latest log with the command `docker compose logs -f`.
#### Database #### Database
The database will be located at: `user_data/tradesv3.sqlite` The database will be located at: `user_data/tradesv3.sqlite`
#### Updating freqtrade with docker-compose #### Updating freqtrade with docker
Updating freqtrade when using `docker-compose` is as simple as running the following 2 commands: Updating freqtrade when using `docker` is as simple as running the following 2 commands:
``` bash ``` bash
# Download the latest image # Download the latest image
docker-compose pull docker compose pull
# Restart the image # Restart the image
docker-compose up -d docker compose up -d
``` ```
This will first pull the latest image, and will then restart the container with the just pulled version. This will first pull the latest image, and will then restart the container with the just pulled version.
@ -116,43 +118,43 @@ This will first pull the latest image, and will then restart the container with
Advanced users may edit the docker-compose file further to include all possible options or arguments. Advanced users may edit the docker-compose file further to include all possible options or arguments.
All freqtrade arguments will be available by running `docker-compose run --rm freqtrade <command> <optional arguments>`. All freqtrade arguments will be available by running `docker compose run --rm freqtrade <command> <optional arguments>`.
!!! Warning "`docker-compose` for trade commands" !!! Warning "`docker compose` for trade commands"
Trade commands (`freqtrade trade <...>`) should not be ran via `docker-compose run` - but should use `docker-compose up -d` instead. Trade commands (`freqtrade trade <...>`) should not be ran via `docker compose run` - but should use `docker compose up -d` instead.
This makes sure that the container is properly started (including port forwardings) and will make sure that the container will restart after a system reboot. This makes sure that the container is properly started (including port forwardings) and will make sure that the container will restart after a system reboot.
If you intend to use freqUI, please also ensure to adjust the [configuration accordingly](rest-api.md#configuration-with-docker), otherwise the UI will not be available. If you intend to use freqUI, please also ensure to adjust the [configuration accordingly](rest-api.md#configuration-with-docker), otherwise the UI will not be available.
!!! Note "`docker-compose run --rm`" !!! Note "`docker compose run --rm`"
Including `--rm` will remove the container after completion, and is highly recommended for all modes except trading mode (running with `freqtrade trade` command). Including `--rm` will remove the container after completion, and is highly recommended for all modes except trading mode (running with `freqtrade trade` command).
??? Note "Using docker without docker-compose" ??? Note "Using docker without docker"
"`docker-compose run --rm`" will require a compose file to be provided. "`docker compose run --rm`" will require a compose file to be provided.
Some freqtrade commands that don't require authentication such as `list-pairs` can be run with "`docker run --rm`" instead. Some freqtrade commands that don't require authentication such as `list-pairs` can be run with "`docker run --rm`" instead.
For example `docker run --rm freqtradeorg/freqtrade:stable list-pairs --exchange binance --quote BTC --print-json`. For example `docker run --rm freqtradeorg/freqtrade:stable list-pairs --exchange binance --quote BTC --print-json`.
This can be useful for fetching exchange information to add to your `config.json` without affecting your running containers. This can be useful for fetching exchange information to add to your `config.json` without affecting your running containers.
#### Example: Download data with docker-compose #### Example: Download data with docker
Download backtesting data for 5 days for the pair ETH/BTC and 1h timeframe from Binance. The data will be stored in the directory `user_data/data/` on the host. Download backtesting data for 5 days for the pair ETH/BTC and 1h timeframe from Binance. The data will be stored in the directory `user_data/data/` on the host.
``` bash ``` bash
docker-compose run --rm freqtrade download-data --pairs ETH/BTC --exchange binance --days 5 -t 1h docker compose run --rm freqtrade download-data --pairs ETH/BTC --exchange binance --days 5 -t 1h
``` ```
Head over to the [Data Downloading Documentation](data-download.md) for more details on downloading data. Head over to the [Data Downloading Documentation](data-download.md) for more details on downloading data.
#### Example: Backtest with docker-compose #### Example: Backtest with docker
Run backtesting in docker-containers for SampleStrategy and specified timerange of historical data, on 5m timeframe: Run backtesting in docker-containers for SampleStrategy and specified timerange of historical data, on 5m timeframe:
``` bash ``` bash
docker-compose run --rm freqtrade backtesting --config user_data/config.json --strategy SampleStrategy --timerange 20190801-20191001 -i 5m docker compose run --rm freqtrade backtesting --config user_data/config.json --strategy SampleStrategy --timerange 20190801-20191001 -i 5m
``` ```
Head over to the [Backtesting Documentation](backtesting.md) to learn more. Head over to the [Backtesting Documentation](backtesting.md) to learn more.
### Additional dependencies with docker-compose ### Additional dependencies with docker
If your strategy requires dependencies not included in the default image - it will be necessary to build the image on your host. If your strategy requires dependencies not included in the default image - it will be necessary to build the image on your host.
For this, please create a Dockerfile containing installation steps for the additional dependencies (have a look at [docker/Dockerfile.custom](https://github.com/freqtrade/freqtrade/blob/develop/docker/Dockerfile.custom) for an example). For this, please create a Dockerfile containing installation steps for the additional dependencies (have a look at [docker/Dockerfile.custom](https://github.com/freqtrade/freqtrade/blob/develop/docker/Dockerfile.custom) for an example).
@ -166,15 +168,15 @@ You'll then also need to modify the `docker-compose.yml` file and uncomment the
dockerfile: "./Dockerfile.<yourextension>" dockerfile: "./Dockerfile.<yourextension>"
``` ```
You can then run `docker-compose build --pull` to build the docker image, and run it using the commands described above. You can then run `docker compose build --pull` to build the docker image, and run it using the commands described above.
### Plotting with docker-compose ### Plotting with docker
Commands `freqtrade plot-profit` and `freqtrade plot-dataframe` ([Documentation](plotting.md)) are available by changing the image to `*_plot` in your docker-compose.yml file. Commands `freqtrade plot-profit` and `freqtrade plot-dataframe` ([Documentation](plotting.md)) are available by changing the image to `*_plot` in your docker-compose.yml file.
You can then use these commands as follows: You can then use these commands as follows:
``` bash ``` bash
docker-compose run --rm freqtrade plot-dataframe --strategy AwesomeStrategy -p BTC/ETH --timerange=20180801-20180805 docker compose run --rm freqtrade plot-dataframe --strategy AwesomeStrategy -p BTC/ETH --timerange=20180801-20180805
``` ```
The output will be stored in the `user_data/plot` directory, and can be opened with any modern browser. The output will be stored in the `user_data/plot` directory, and can be opened with any modern browser.
@ -185,7 +187,7 @@ Freqtrade provides a docker-compose file which starts up a jupyter lab server.
You can run this server using the following command: You can run this server using the following command:
``` bash ``` bash
docker-compose -f docker/docker-compose-jupyter.yml up docker compose -f docker/docker-compose-jupyter.yml up
``` ```
This will create a docker-container running jupyter lab, which will be accessible using `https://127.0.0.1:8888/lab`. This will create a docker-container running jupyter lab, which will be accessible using `https://127.0.0.1:8888/lab`.
@ -194,7 +196,7 @@ Please use the link that's printed in the console after startup for simplified l
Since part of this image is built on your machine, it is recommended to rebuild the image from time to time to keep freqtrade (and dependencies) up-to-date. Since part of this image is built on your machine, it is recommended to rebuild the image from time to time to keep freqtrade (and dependencies) up-to-date.
``` bash ``` bash
docker-compose -f docker/docker-compose-jupyter.yml build --no-cache docker compose -f docker/docker-compose-jupyter.yml build --no-cache
``` ```
## Troubleshooting ## Troubleshooting

View File

@ -26,10 +26,7 @@ FreqAI is configured through the typical [Freqtrade config file](configuration.m
}, },
"data_split_parameters" : { "data_split_parameters" : {
"test_size": 0.25 "test_size": 0.25
}, }
"model_training_parameters" : {
"n_estimators": 100
},
} }
``` ```
@ -46,116 +43,113 @@ 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 `populate_any_indicators` # the model will return all labels created by user in `set_freqai_labels()`
# (& appended targets), an indication of whether or not the prediction should be accepted, # (& appended targets), an indication of whether or not the prediction should be accepted,
# the target mean/std values for each of the labels created by user in # the target mean/std values for each of the labels created by user in
# `populate_any_indicators()` for each training period. # `feature_engineering_*` for each training period.
dataframe = self.freqai.start(dataframe, metadata, self) dataframe = self.freqai.start(dataframe, metadata, self)
return dataframe return dataframe
def populate_any_indicators( def feature_engineering_expand_all(self, dataframe, period, **kwargs):
self, pair, df, tf, informative=None, set_generalized_indicators=False
):
""" """
Function designed to automatically generate, name and merge features *Only functional with FreqAI enabled strategies*
from user indicated timeframes in the configuration file. User controls the indicators This function will automatically expand the defined features on the config defined
passed to the training/prediction by prepending indicators with `'%-' + pair ` `indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and
(see convention below). I.e. user should not prepend any supporting metrics `include_corr_pairs`. In other words, a single feature defined in this function
(e.g. bb_lowerband below) with % unless they explicitly want to pass that metric to the will automatically expand to a total of
model. `indicator_periods_candles` * `include_timeframes` * `include_shifted_candles` *
:param pair: pair to be used as informative `include_corr_pairs` numbers of features added to the model.
:param df: strategy dataframe which will receive merges from informatives
:param tf: timeframe of the dataframe which will modify the feature names All features must be prepended with `%` to be recognized by FreqAI internals.
: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)
""" """
if informative is None: dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
informative = self.dp.get_pair_dataframe(pair, tf) 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)
# first loop is automatically duplicating indicators for time periods return dataframe
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)
indicators = [col for col in informative if col.startswith("%")] def feature_engineering_expand_basic(self, dataframe, **kwargs):
# 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): *Only functional with FreqAI enabled strategies*
if n == 0: This function will automatically expand the defined features on the config defined
continue `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`.
informative_shift = informative[indicators].shift(n) In other words, a single feature defined in this function
informative_shift = informative_shift.add_suffix("_shift-" + str(n)) will automatically expand to a total of
informative = pd.concat((informative, informative_shift), axis=1) `include_timeframes` * `include_shifted_candles` * `include_corr_pairs`
numbers of features added to the model.
df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True) Features defined here will *not* be automatically duplicated on user defined
skip_columns = [ `indicator_periods_candles`
(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 All features must be prepended with `%` to be recognized by FreqAI internals.
# function to populate indicators during training). Notice how we ensure not to
# add them multiple times
if set_generalized_indicators:
# user adds targets here by prepending them with &- (see convention below) :param df: strategy dataframe which will receive the features
# If user wishes to use multiple targets, a multioutput prediction model dataframe["%-pct-change"] = dataframe["close"].pct_change()
# needs to be used such as templates/CatboostPredictionMultiModel.py dataframe["%-ema-200"] = ta.EMA(dataframe, timeperiod=200)
df["&-s_close"] = ( """
df["close"] dataframe["%-pct-change"] = dataframe["close"].pct_change()
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"]) dataframe["%-raw_volume"] = dataframe["volume"]
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"]) dataframe["%-raw_price"] = dataframe["close"]
.mean() return dataframe
/ df["close"]
- 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.
: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 `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 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 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 `populate_any_indicators()`. Defining FreqAI features in `populate_indicators()` Features **must** be defined in `feature_engineering_*()`. Defining FreqAI features in `populate_indicators()`
will cause the algorithm to fail in live/dry mode. In order to add generalized features that are not associated with a specific pair or timeframe, the following structure inside `populate_any_indicators()` should be used will cause the algorithm to fail in live/dry mode. In order to add generalized features that are not associated with a specific pair or timeframe, you should use `feature_engineering_standard()`
(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
@ -163,11 +157,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 `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['&*']` | 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['&*_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 `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. | `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.
## Setting the `startup_candle_count` ## Setting the `startup_candle_count`
@ -182,7 +176,7 @@ The `startup_candle_count` in the FreqAI strategy needs to be set up in the same
## Creating a dynamic target threshold ## Creating a dynamic target threshold
Deciding when to enter or exit a trade can be done in a dynamic way to reflect current market conditions. FreqAI allows you to return additional information from the training of a model (more info [here](freqai-feature-engineering.md#returning-additional-info-from-training)). For example, the `&*_std/mean` return values describe the statistical distribution of the target/label *during the most recent training*. Comparing a given prediction to these values allows you to know the rarity of the prediction. In `templates/FreqaiExampleStrategy.py`, the `target_roi` and `sell_roi` are defined to be 1.25 z-scores away from the mean which causes predictions that are closer to the mean to be filtered out. Deciding when to enter or exit a trade can be done in a dynamic way to reflect current market conditions. FreqAI allows you to return additional information from the training of a model (more info [here](freqai-feature-engineering.md#returning-additional-info-from-training)). For example, the `&*_std/mean` return values describe the statistical distribution of the target/label *during the most recent training*. Comparing a given prediction to these values allows you to know the rarity of the prediction. In `templates/FreqaiExampleStrategy.py`, the `target_roi` and `sell_roi` are defined to be 1.25 z-scores away from the mean which causes predictions that are closer to the mean to be filtered out.
```python ```python
dataframe["target_roi"] = dataframe["&-s_close_mean"] + dataframe["&-s_close_std"] * 1.25 dataframe["target_roi"] = dataframe["&-s_close_mean"] + dataframe["&-s_close_std"] * 1.25
@ -230,7 +224,7 @@ If you want to predict multiple targets, you need to define multiple labels usin
#### Classifiers #### Classifiers
If you are using a classifier, you need to specify a target that has discrete values. FreqAI includes a variety of classifiers, such as the `CatboostClassifier` via the flag `--freqaimodel CatboostClassifier`. If you elects to use a classifier, the classes need to be set using strings. For example, if you want to predict if the price 100 candles into the future goes up or down you would set If you are using a classifier, you need to specify a target that has discrete values. FreqAI includes a variety of classifiers, such as the `CatboostClassifier` via the flag `--freqaimodel CatboostClassifier`. If you elects to use a classifier, the classes need to be set using strings. For example, if you want to predict if the price 100 candles into the future goes up or down you would set
```python ```python
df['&s-up_or_down'] = np.where( df["close"].shift(-100) > df["close"], 'up', 'down') df['&s-up_or_down'] = np.where( df["close"].shift(-100) > df["close"], 'up', 'down')

View File

@ -2,96 +2,130 @@
## Defining the features ## Defining the features
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 `&`. 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:
!!! Note | Function | Description |
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 `populate_any_indicators()` in the source provided example strategy (found in `templates/FreqaiExampleStrategy.py`) to ensure that the feature definitions are following the correct conventions. Here is an example of how to set the indicators and labels in the strategy: It is advisable to start from the template `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:
```python ```python
def populate_any_indicators( def feature_engineering_expand_all(self, dataframe, period, **kwargs):
self, pair, df, tf, informative=None, set_generalized_indicators=False
):
""" """
Function designed to automatically generate, name, and merge features *Only functional with FreqAI enabled strategies*
from user-indicated timeframes in the configuration file. The user controls the indicators This function will automatically expand the defined features on the config defined
passed to the training/prediction by prepending indicators with `'%-' + pair ` `indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and
(see convention below). I.e., the user should not prepend any supporting metrics `include_corr_pairs`. In other words, a single feature defined in this function
(e.g., bb_lowerband below) with % unless they explicitly want to pass that metric to the will automatically expand to a total of
model. `indicator_periods_candles` * `include_timeframes` * `include_shifted_candles` *
:param pair: pair to be used as informative `include_corr_pairs` numbers of features added to the model.
:param df: strategy dataframe which will receive merges from informatives
:param tf: timeframe of the dataframe which will modify the feature names All features must be prepended with `%` to be recognized by FreqAI internals.
: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)
""" """
if informative is None: dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
informative = self.dp.get_pair_dataframe(pair, tf) 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)
# first loop is automatically duplicating indicators for time periods bollinger = qtpylib.bollinger_bands(
for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]: qtpylib.typical_price(dataframe), window=period, stds=2.2
t = int(t) )
informative[f"%-{pair}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t) dataframe["bb_lowerband-period"] = bollinger["lower"]
informative[f"%-{pair}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t) dataframe["bb_middleband-period"] = bollinger["mid"]
informative[f"%-{pair}adx-period_{t}"] = ta.ADX(informative, window=t) dataframe["bb_upperband-period"] = bollinger["upper"]
bollinger = qtpylib.bollinger_bands( dataframe["%-bb_width-period"] = (
qtpylib.typical_price(informative), window=t, stds=2.2 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
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"]
informative[f"{pair}bb_middleband-period_{t}"] = bollinger["mid"] return dataframe
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,
@ -118,13 +152,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 `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. 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.
You can ask for each of the defined features to be included also for informative pairs using the `include_corr_pairlist`. This means that the feature set will include all the features from `populate_any_indicators` on all the `include_timeframes` for each of the correlated pairs defined in the config (`ETH/USD`, `LINK/USD`, and `BNB/USD` in the presented example). You can ask for each of the defined features to be included also for informative pairs using the `include_corr_pairlist`. This means that the feature set will include all the features from `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).
`include_shifted_candles` indicates the number of previous candles to include in the feature set. For example, `include_shifted_candles: 2` tells FreqAI to include the past 2 candles for each of the features in the feature set. `include_shifted_candles` indicates the number of previous candles to include in the feature set. For example, `include_shifted_candles: 2` tells FreqAI to include the past 2 candles for each of the features in the feature set.
In total, the number of features the user of the presented example strat has created is: length of `include_timeframes` * no. features in `populate_any_indicators()` * length of `include_corr_pairlist` * no. `include_shifted_candles` * length of `indicator_periods_candles` In total, the number of features the user of the presented example strat has created is: length of `include_timeframes` * no. features in `feature_engineering_expand_*()` * 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 obsolete models. <br> **Datatype:** Boolean. <br> Default: `False` (all historic models remain on disk). | `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`.
| `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 `populate_any_indicators` will be created for. The list is added as features to the base indicators dataset. <br> **Datatype:** List of timeframes (strings). | `include_timeframes` | A list of timeframes that all indicators in `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_corr_pairlist` | A list of correlated coins that FreqAI will add as additional features to all `pair_whitelist` coins. All indicators set in `populate_any_indicators` during feature engineering (see details [here](freqai-feature-engineering.md)) will be created for each correlated coin. The correlated coins features are added to the base indicators dataset. <br> **Datatype:** List of assets (strings). | `include_corr_pairlist` | A list of correlated coins that FreqAI will add as additional features to all `pair_whitelist` coins. All indicators set in `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).
| `label_period_candles` | Number of candles into the future that the labels are created for. This is used in `populate_any_indicators` (see `templates/FreqaiExampleStrategy.py` for detailed usage). You can create custom labels and choose whether to make use of this parameter or not. <br> **Datatype:** Positive integer. | `label_period_candles` | Number of candles into the future that the labels are created for. This is used in `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.
| `include_shifted_candles` | Add features from previous candles to subsequent candles with the intent of adding historical information. If used, FreqAI will duplicate and shift all features from the `include_shifted_candles` previous candles so that the information is available for the subsequent candle. <br> **Datatype:** Positive integer. | `include_shifted_candles` | Add features from previous candles to subsequent candles with the intent of adding historical information. If used, FreqAI will duplicate and shift all features from the `include_shifted_candles` previous candles so that the information is available for the subsequent candle. <br> **Datatype:** Positive integer.
| `weight_factor` | Weight training data points according to their recency (see details [here](freqai-feature-engineering.md#weighting-features-for-temporal-importance)). <br> **Datatype:** Positive float (typically < 1). | `weight_factor` | Weight training data points according to their recency (see details [here](freqai-feature-engineering.md#weighting-features-for-temporal-importance)). <br> **Datatype:** Positive float (typically < 1).
| `indicator_max_period_candles` | **No longer used (#7325)**. Replaced by `startup_candle_count` which is set in the [strategy](freqai-configuration.md#building-a-freqai-strategy). `startup_candle_count` is timeframe independent and defines the maximum *period* used in `populate_any_indicators()` for indicator creation. FreqAI uses this parameter together with the maximum timeframe in `include_time_frames` to calculate how many data points to download such that the first data point does not include a NaN. <br> **Datatype:** Positive integer. | `indicator_max_period_candles` | **No longer used (#7325)**. Replaced by `startup_candle_count` which is set in the [strategy](freqai-configuration.md#building-a-freqai-strategy). `startup_candle_count` is timeframe independent and defines the maximum *period* used in `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_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`.

View File

@ -34,65 +34,36 @@ 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 `populate_any_indicators` as a typical Regressor: 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 doesnt require them. However, FreqAI requires a default (neutral) value to be set in the action column:
```python ```python
def populate_any_indicators( def set_freqai_targets(self, dataframe, **kwargs):
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.
if informative is None: More details about feature engineering available:
informative = self.dp.get_pair_dataframe(pair, tf)
# first loop is automatically duplicating indicators for time periods https://www.freqtrade.io/en/latest/freqai-feature-engineering
for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]:
t = int(t) :param df: strategy dataframe which will receive the targets
informative[f"%-{pair}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t) usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"]
informative[f"%-{pair}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t) """
informative[f"%-{pair}adx-period_{t}"] = ta.ADX(informative, window=t) # For RL, there are no direct targets to set. This is filler (neutral)
# until the agent sends an action.
# The following raw price values are necessary for RL models df["&-action"] = 0
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():
# The following features are necessary for RL models # The following features are necessary for RL models
informative[f"%-{pair}raw_close"] = informative["close"] informative[f"%-raw_close"] = informative["close"]
informative[f"%-{pair}raw_open"] = informative["open"] informative[f"%-raw_open"] = informative["open"]
informative[f"%-{pair}raw_high"] = informative["high"] informative[f"%-raw_high"] = informative["high"]
informative[f"%-{pair}raw_low"] = informative["low"] informative[f"%-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.
@ -247,14 +218,40 @@ where `unique-id` is the `identifier` set in the `freqai` configuration file. Th
![tensorboard](assets/tensorboard.jpg) ![tensorboard](assets/tensorboard.jpg)
### Custom logging
FreqAI also provides a built in episodic summary logger called `self.tensorboard_log` for adding custom information to the Tensorboard log. By default, this function is already called once per step inside the environment to record the agent actions. All values accumulated for all steps in a single episode are reported at the conclusion of each episode, followed by a full reset of all metrics to 0 in preparation for the subsequent episode.
`self.tensorboard_log` can also be used anywhere inside the environment, for example, it can be added to the `calculate_reward` function to collect more detailed information about how often various parts of the reward were called:
```py
class MyRLEnv(Base5ActionRLEnv):
"""
User made custom environment. This class inherits from BaseEnvironment and gym.env.
Users can override any functions from those parent classes. Here is an example
of a user customized `calculate_reward()` function.
"""
def calculate_reward(self, action: int) -> float:
if not self._is_valid(action):
self.tensorboard_log("is_valid")
return -2
```
!!! 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.
### Choosing a base environment ### Choosing a base environment
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: 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:
* 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
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`. 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`.
!!! Note !!! Note
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). Only the `Base3ActionRLEnv` can do long-only training/trading (set the user strategy attribute `can_short = False`).

View File

@ -67,6 +67,10 @@ 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
@ -135,7 +139,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 `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). - 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).
- 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,11 +72,25 @@ 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).
@ -99,6 +113,8 @@ 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, Timothy Pogue @wizrds 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

View File

@ -365,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)
@ -400,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}']
)) ))

View File

@ -23,6 +23,7 @@ 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)
@ -173,6 +174,48 @@ 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,6 +1,7 @@
![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

@ -92,6 +92,8 @@ 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,9 +11,6 @@
{% 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>
@ -44,25 +41,4 @@
<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==8.5.11 mkdocs-material==9.0.3
mdx_truly_sane_lists==1.3 mdx_truly_sane_lists==1.3
pymdown-extensions==9.9 pymdown-extensions==9.9
jinja2==3.1.2 jinja2==3.1.2

View File

@ -13,12 +13,12 @@ Feel free to use a visual Database editor like SqliteBrowser if you feel more co
sudo apt-get install sqlite3 sudo apt-get install sqlite3
``` ```
### Using sqlite3 via docker-compose ### Using sqlite3 via docker
The freqtrade docker image does contain sqlite3, so you can edit the database without having to install anything on the host system. The freqtrade docker image does contain sqlite3, so you can edit the database without having to install anything on the host system.
``` bash ``` bash
docker-compose exec freqtrade /bin/bash docker compose exec freqtrade /bin/bash
sqlite3 <database-file>.sqlite sqlite3 <database-file>.sqlite
``` ```

View File

@ -773,7 +773,7 @@ class DigDeeperStrategy(IStrategy):
* Sell 100@10\$ -> Avg price: 8.5\$, realized profit 150\$, 17.65% * Sell 100@10\$ -> Avg price: 8.5\$, realized profit 150\$, 17.65%
* Buy 150@11\$ -> Avg price: 10\$, realized profit 150\$, 17.65% * Buy 150@11\$ -> Avg price: 10\$, realized profit 150\$, 17.65%
* Sell 100@12\$ -> Avg price: 10\$, total realized profit 350\$, 20% * Sell 100@12\$ -> Avg price: 10\$, total realized profit 350\$, 20%
* Sell 150@14\$ -> Avg price: 10\$, total realized profit 950\$, 40% * Sell 150@14\$ -> Avg price: 10\$, total realized profit 950\$, 40% <- *This will be the last "Exit" message*
The total profit for this trade was 950$ on a 3350$ investment (`100@8$ + 100@9$ + 150@11$`). As such - the final relative profit is 28.35% (`950 / 3350`). The total profit for this trade was 950$ on a 3350$ investment (`100@8$ + 100@9$ + 150@11$`). As such - the final relative profit is 28.35% (`950 / 3350`).

View File

@ -363,9 +363,9 @@ class AwesomeStrategy(IStrategy):
timeframe = "1d" timeframe = "1d"
timeframe_mins = timeframe_to_minutes(timeframe) timeframe_mins = timeframe_to_minutes(timeframe)
minimal_roi = { minimal_roi = {
"0": 0.05, # 5% for the first 3 candles "0": 0.05, # 5% for the first 3 candles
str(timeframe_mins * 3)): 0.02, # 2% after 3 candles str(timeframe_mins * 3): 0.02, # 2% after 3 candles
str(timeframe_mins * 6)): 0.01, # 1% After 6 candles str(timeframe_mins * 6): 0.01, # 1% After 6 candles
} }
``` ```
@ -989,38 +989,18 @@ 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
if self.config['runmode'].value in ('live', 'dry_run'): trades = Trade.get_trades_proxy(pair=metadata['pair'],
trades = Trade.get_trades([Trade.pair == metadata['pair'], open_date=datetime.now(timezone.utc) - timedelta(days=1),
Trade.open_date > datetime.utcnow() - timedelta(days=1), is_open=False,
Trade.is_open.is_(False), ]).order_by(Trade.close_date).all()
]).order_by(Trade.close_date).all() # Summarize profit for this pair.
# Summarize profit for this pair. curdayprofit = sum(trade.close_profit for trade in trades)
curdayprofit = sum(trade.close_profit for trade in trades)
``` ```
Get amount of stake_currency currently invested in Trades: For a full list of available methods, please consult the [Trade object](trade-object.md) documentation.
``` 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 during backtesting or hyperopt. Trade history is not available in `populate_*` methods during backtesting or hyperopt, and will result in empty results.
## Prevent trades from happening for a specific pair ## Prevent trades from happening for a specific pair

View File

@ -2,12 +2,37 @@
Debugging a strategy can be time-consuming. Freqtrade offers helper functions to visualize raw data. Debugging a strategy can be time-consuming. Freqtrade offers helper functions to visualize raw data.
The following assumes you work with SampleStrategy, data for 5m timeframe from Binance and have downloaded them into the data directory in the default location. The following assumes you work with SampleStrategy, data for 5m timeframe from Binance and have downloaded them into the data directory in the default location.
Please follow the [documentation](https://www.freqtrade.io/en/stable/data-download/) for more details.
## Setup ## Setup
### Change Working directory to repository root
```python ```python
import os
from pathlib import Path from pathlib import Path
# Change directory
# Modify this cell to insure that the output shows the correct path.
# Define all paths relative to the project root shown in the cell output
project_root = "somedir/freqtrade"
i=0
try:
os.chdirdir(project_root)
assert Path('LICENSE').is_file()
except:
while i<4 and (not Path('LICENSE').is_file()):
os.chdir(Path(Path.cwd(), '../'))
i+=1
project_root = Path.cwd()
print(Path.cwd())
```
### Configure Freqtrade environment
```python
from freqtrade.configuration import Configuration from freqtrade.configuration import Configuration
# Customize these according to your needs. # Customize these according to your needs.
@ -15,14 +40,14 @@ from freqtrade.configuration import Configuration
# Initialize empty configuration object # Initialize empty configuration object
config = Configuration.from_files([]) config = Configuration.from_files([])
# Optionally (recommended), use existing configuration file # Optionally (recommended), use existing configuration file
# config = Configuration.from_files(["config.json"]) # config = Configuration.from_files(["user_data/config.json"])
# Define some constants # Define some constants
config["timeframe"] = "5m" config["timeframe"] = "5m"
# Name of the strategy class # Name of the strategy class
config["strategy"] = "SampleStrategy" config["strategy"] = "SampleStrategy"
# Location of the data # Location of the data
data_location = config['datadir'] data_location = config["datadir"]
# Pair to analyze - Only use one pair here # Pair to analyze - Only use one pair here
pair = "BTC/USDT" pair = "BTC/USDT"
``` ```
@ -36,12 +61,12 @@ from freqtrade.enums import CandleType
candles = load_pair_history(datadir=data_location, candles = load_pair_history(datadir=data_location,
timeframe=config["timeframe"], timeframe=config["timeframe"],
pair=pair, pair=pair,
data_format = "hdf5", data_format = "json", # Make sure to update this to your data
candle_type=CandleType.SPOT, candle_type=CandleType.SPOT,
) )
# Confirm success # Confirm success
print("Loaded " + str(len(candles)) + f" rows of data for {pair} from {data_location}") print(f"Loaded {len(candles)} rows of data for {pair} from {data_location}")
candles.head() candles.head()
``` ```

View File

@ -477,3 +477,254 @@ 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,18 +11,3 @@
.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; }
}

148
docs/trade-object.md Normal file
View File

@ -0,0 +1,148 @@
# 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

@ -6,14 +6,14 @@ To update your freqtrade installation, please use one of the below methods, corr
Breaking changes / changed behavior will be documented in the changelog that is posted alongside every release. Breaking changes / changed behavior will be documented in the changelog that is posted alongside every release.
For the develop branch, please follow PR's to avoid being surprised by changes. For the develop branch, please follow PR's to avoid being surprised by changes.
## docker-compose ## docker
!!! Note "Legacy installations using the `master` image" !!! Note "Legacy installations using the `master` image"
We're switching from master to stable for the release Images - please adjust your docker-file and replace `freqtradeorg/freqtrade:master` with `freqtradeorg/freqtrade:stable` We're switching from master to stable for the release Images - please adjust your docker-file and replace `freqtradeorg/freqtrade:master` with `freqtradeorg/freqtrade:stable`
``` bash ``` bash
docker-compose pull docker compose pull
docker-compose up -d docker compose up -d
``` ```
## Installation via setup script ## Installation via setup script

View File

@ -652,7 +652,7 @@ Common arguments:
You can also use webserver mode via docker. You can also use webserver mode via docker.
Starting a one-off container requires the configuration of the port explicitly, as ports are not exposed by default. Starting a one-off container requires the configuration of the port explicitly, as ports are not exposed by default.
You can use `docker-compose run --rm -p 127.0.0.1:8080:8080 freqtrade webserver` to start a one-off container that'll be removed once you stop it. This assumes that port 8080 is still available and no other bot is running on that port. You can use `docker compose run --rm -p 127.0.0.1:8080:8080 freqtrade webserver` to start a one-off container that'll be removed once you stop it. This assumes that port 8080 is still available and no other bot is running on that port.
Alternatively, you can reconfigure the docker-compose file to have the command updated: Alternatively, you can reconfigure the docker-compose file to have the command updated:
@ -662,7 +662,7 @@ Alternatively, you can reconfigure the docker-compose file to have the command u
--config /freqtrade/user_data/config.json --config /freqtrade/user_data/config.json
``` ```
You can now use `docker-compose up` to start the webserver. You can now use `docker compose up` to start the webserver.
This assumes that the configuration has a webserver enabled and configured for docker (listening port = `0.0.0.0`). This assumes that the configuration has a webserver enabled and configured for docker (listening port = `0.0.0.0`).
!!! Tip !!! Tip

View File

@ -1,19 +1,20 @@
""" Freqtrade bot """ """ Freqtrade bot """
__version__ = '2022.12.dev' __version__ = '2023.1.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).decode("utf-8").rstrip().strip('"') stderr=subprocess.DEVNULL, cwd=freqtrade_basedir).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

@ -355,6 +355,13 @@ def _validate_freqai_include_timeframes(conf: Dict[str, Any]) -> None:
f"Main timeframe of {main_tf} must be smaller or equal to FreqAI " f"Main timeframe of {main_tf} must be smaller or equal to FreqAI "
f"`include_timeframes`.Offending include-timeframes: {', '.join(offending_lines)}") f"`include_timeframes`.Offending include-timeframes: {', '.join(offending_lines)}")
# Ensure that the base timeframe is included in the include_timeframes list
if main_tf not in freqai_include_timeframes:
feature_parameters = conf.get('freqai', {}).get('feature_parameters', {})
include_timeframes = [main_tf] + freqai_include_timeframes
conf.get('freqai', {}).get('feature_parameters', {}) \
.update({**feature_parameters, 'include_timeframes': include_timeframes})
def _validate_freqai_backtest(conf: Dict[str, Any]) -> None: def _validate_freqai_backtest(conf: Dict[str, Any]) -> None:
if conf.get('runmode', RunMode.OTHER) == RunMode.BACKTEST: if conf.get('runmode', RunMode.OTHER) == RunMode.BACKTEST:

View File

@ -31,7 +31,7 @@ HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss',
'CalmarHyperOptLoss', 'CalmarHyperOptLoss',
'MaxDrawDownHyperOptLoss', 'MaxDrawDownRelativeHyperOptLoss', 'MaxDrawDownHyperOptLoss', 'MaxDrawDownRelativeHyperOptLoss',
'ProfitDrawDownHyperOptLoss'] 'ProfitDrawDownHyperOptLoss']
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'ProducerPairList', AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'ProducerPairList', 'RemotePairList',
'AgeFilter', 'OffsetFilter', 'PerformanceFilter', 'AgeFilter', 'OffsetFilter', 'PerformanceFilter',
'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter',
'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter'] 'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter']
@ -61,6 +61,7 @@ USERPATH_FREQAIMODELS = 'freqaimodels'
TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent'] TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent']
WEBHOOK_FORMAT_OPTIONS = ['form', 'json', 'raw'] WEBHOOK_FORMAT_OPTIONS = ['form', 'json', 'raw']
FULL_DATAFRAME_THRESHOLD = 100
ENV_VAR_PREFIX = 'FREQTRADE__' ENV_VAR_PREFIX = 'FREQTRADE__'
@ -608,9 +609,8 @@ CONF_SCHEMA = {
"backtest_period_days", "backtest_period_days",
"identifier", "identifier",
"feature_parameters", "feature_parameters",
"data_split_parameters", "data_split_parameters"
"model_training_parameters" ]
]
}, },
}, },
} }

View File

@ -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', 'amount', 'open_date', 'close_date', BT_DATA_COLUMNS = ['pair', 'stake_amount', 'max_stake_amount', 'amount',
'open_rate', 'close_rate', 'open_date', 'close_date', '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',
@ -241,6 +241,33 @@ 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.
@ -269,24 +296,7 @@ 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['open_date'] = pd.to_datetime(df['open_date'], df = _load_backtest_data_df_compatibility(df)
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.

View File

@ -9,14 +9,16 @@ from collections import deque
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
from pandas import DataFrame from pandas import DataFrame, to_timedelta
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.constants import Config, ListPairsWithTimeframes, PairWithTimeframe from freqtrade.constants import (FULL_DATAFRAME_THRESHOLD, Config, ListPairsWithTimeframes,
PairWithTimeframe)
from freqtrade.data.history import load_pair_history from freqtrade.data.history import load_pair_history
from freqtrade.enums import CandleType, RPCMessageType, RunMode from freqtrade.enums import CandleType, RPCMessageType, RunMode
from freqtrade.exceptions import ExchangeError, OperationalException from freqtrade.exceptions import ExchangeError, OperationalException
from freqtrade.exchange import Exchange, timeframe_to_seconds from freqtrade.exchange import Exchange, timeframe_to_seconds
from freqtrade.misc import append_candles_to_dataframe
from freqtrade.rpc import RPCManager from freqtrade.rpc import RPCManager
from freqtrade.util import PeriodicCache from freqtrade.util import PeriodicCache
@ -120,7 +122,7 @@ class DataProvider:
'type': RPCMessageType.ANALYZED_DF, 'type': RPCMessageType.ANALYZED_DF,
'data': { 'data': {
'key': pair_key, 'key': pair_key,
'df': dataframe, 'df': dataframe.tail(1),
'la': datetime.now(timezone.utc) 'la': datetime.now(timezone.utc)
} }
} }
@ -131,7 +133,7 @@ class DataProvider:
'data': pair_key, 'data': pair_key,
}) })
def _add_external_df( def _replace_external_df(
self, self,
pair: str, pair: str,
dataframe: DataFrame, dataframe: DataFrame,
@ -157,6 +159,85 @@ class DataProvider:
self.__producer_pairs_df[producer_name][pair_key] = (dataframe, _last_analyzed) self.__producer_pairs_df[producer_name][pair_key] = (dataframe, _last_analyzed)
logger.debug(f"External DataFrame for {pair_key} from {producer_name} added.") logger.debug(f"External DataFrame for {pair_key} from {producer_name} added.")
def _add_external_df(
self,
pair: str,
dataframe: DataFrame,
last_analyzed: datetime,
timeframe: str,
candle_type: CandleType,
producer_name: str = "default"
) -> Tuple[bool, int]:
"""
Append a candle to the existing external dataframe. The incoming dataframe
must have at least 1 candle.
:param pair: pair to get the data for
:param timeframe: Timeframe to get data for
:param candle_type: Any of the enum CandleType (must match trading mode!)
:returns: False if the candle could not be appended, or the int number of missing candles.
"""
pair_key = (pair, timeframe, candle_type)
if dataframe.empty:
# The incoming dataframe must have at least 1 candle
return (False, 0)
if len(dataframe) >= FULL_DATAFRAME_THRESHOLD:
# This is likely a full dataframe
# Add the dataframe to the dataprovider
self._replace_external_df(
pair,
dataframe,
last_analyzed=last_analyzed,
timeframe=timeframe,
candle_type=candle_type,
producer_name=producer_name
)
return (True, 0)
if (producer_name not in self.__producer_pairs_df
or pair_key not in self.__producer_pairs_df[producer_name]):
# We don't have data from this producer yet,
# or we don't have data for this pair_key
# return False and 1000 for the full df
return (False, 1000)
existing_df, _ = self.__producer_pairs_df[producer_name][pair_key]
# CHECK FOR MISSING CANDLES
timeframe_delta = to_timedelta(timeframe) # Convert the timeframe to a timedelta for pandas
local_last = existing_df.iloc[-1]['date'] # We want the last date from our copy
incoming_first = dataframe.iloc[0]['date'] # We want the first date from the incoming
# Remove existing candles that are newer than the incoming first candle
existing_df1 = existing_df[existing_df['date'] < incoming_first]
candle_difference = (incoming_first - local_last) / timeframe_delta
# If the difference divided by the timeframe is 1, then this
# is the candle we want and the incoming data isn't missing any.
# If the candle_difference is more than 1, that means
# we missed some candles between our data and the incoming
# so return False and candle_difference.
if candle_difference > 1:
return (False, candle_difference)
if existing_df1.empty:
appended_df = dataframe
else:
appended_df = append_candles_to_dataframe(existing_df1, dataframe)
# Everything is good, we appended
self._replace_external_df(
pair,
appended_df,
last_analyzed=last_analyzed,
timeframe=timeframe,
candle_type=candle_type,
producer_name=producer_name
)
return (True, 0)
def get_producer_df( def get_producer_df(
self, self,
pair: str, pair: str,

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, signal_candles): def _analyze_candles_and_indicators(pair, trades: pd.DataFrame, signal_candles: pd.DataFrame):
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': ['sum', 'median', 'mean']} 'profit_ratio': ['median', 'mean', 'sum']}
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']

View File

@ -1,4 +1,6 @@
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
@ -190,3 +192,119 @@ 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

@ -3,7 +3,6 @@
from freqtrade.exchange.common import remove_credentials, MAP_EXCHANGE_CHILDCLASS from freqtrade.exchange.common import remove_credentials, MAP_EXCHANGE_CHILDCLASS
from freqtrade.exchange.exchange import Exchange from freqtrade.exchange.exchange import Exchange
# isort: on # isort: on
from freqtrade.exchange.bibox import Bibox
from freqtrade.exchange.binance import Binance from freqtrade.exchange.binance import Binance
from freqtrade.exchange.bitpanda import Bitpanda from freqtrade.exchange.bitpanda import Bitpanda
from freqtrade.exchange.bittrex import Bittrex from freqtrade.exchange.bittrex import Bittrex

View File

@ -1,28 +0,0 @@
""" Bibox exchange subclass """
import logging
from typing import Dict
from freqtrade.exchange import Exchange
logger = logging.getLogger(__name__)
class Bibox(Exchange):
"""
Bibox exchange class. Contains adjustments needed for Freqtrade to work
with this exchange.
Please note that this exchange is not included in the list of exchanges
officially supported by the Freqtrade development team. So some features
may still not work as expected.
"""
# fetchCurrencies API point requires authentication for Bibox,
# so switch it off for Freqtrade load_markets()
@property
def _ccxt_config(self) -> Dict:
# Parameters to add directly to ccxt sync/async initialization.
config = {"has": {"fetchCurrencies": False}}
config.update(super()._ccxt_config)
return config

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 Tickers from freqtrade.exchange.types import OHLCVResponse, Tickers
from freqtrade.misc import deep_merge_dicts, json_load from freqtrade.misc import deep_merge_dicts, json_load
@ -31,7 +31,7 @@ class Binance(Exchange):
"ccxt_futures_name": "future" "ccxt_futures_name": "future"
} }
_ft_has_futures: Dict = { _ft_has_futures: Dict = {
"stoploss_order_types": {"limit": "limit", "market": "market"}, "stoploss_order_types": {"limit": "stop", "market": "stop_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
) -> Tuple[str, str, str, List]: ) -> OHLCVResponse:
""" """
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"

View File

@ -36,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 Ticker, Tickers from freqtrade.exchange.types import OHLCVResponse, 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
@ -474,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)) self._api_async.load_markets(reload=reload, params={}))
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)
@ -483,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() self._markets = self._api.load_markets(params={})
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']:
@ -501,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) self._markets = self._api.load_markets(reload=True, params={})
# 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
@ -1705,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() self._api.load_markets(params={})
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']
@ -1813,32 +1813,18 @@ 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
) -> Tuple[str, str, str, List]: ) -> OHLCVResponse:
""" """
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
@ -1869,15 +1855,16 @@ 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 return pair, timeframe, candle_type, data, self._ohlcv_partial_candle
def _build_coroutine(self, pair: str, timeframe: str, candle_type: CandleType, def _build_coroutine(
since_ms: Optional[int], cache: bool) -> Coroutine: self, pair: str, timeframe: str, candle_type: CandleType,
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)
@ -1914,7 +1901,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 = [] input_coroutines: List[Coroutine[Any, Any, OHLCVResponse]] = []
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
@ -1978,7 +1965,6 @@ 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)
@ -1996,8 +1982,9 @@ 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 4 elements) # Deconstruct tuple (has 5 elements)
pair, timeframe, c_type, ticks = res pair, timeframe, c_type, ticks, drop_hint = 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)
@ -2025,7 +2012,7 @@ class Exchange:
timeframe: str, timeframe: str,
candle_type: CandleType, candle_type: CandleType,
since_ms: Optional[int] = None, since_ms: Optional[int] = None,
) -> Tuple[str, str, str, List]: ) -> OHLCVResponse:
""" """
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
@ -2035,8 +2022,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, interval %s, since %s %s...", "Fetching pair %s, %s, interval %s, since %s %s...",
pair, timeframe, since_ms, s pair, candle_type, 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(
@ -2050,11 +2037,12 @@ class Exchange:
limit=candle_limit, params=params) limit=candle_limit, params=params)
else: else:
# Funding rate # Funding rate
data = await self._api_async.fetch_funding_rate_history( data = await self._fetch_funding_rate_history(
pair, since=since_ms, pair=pair,
limit=candle_limit) timeframe=timeframe,
# Convert funding rate to candle pattern limit=candle_limit,
data = [[x['timestamp'], x['fundingRate'], 0, 0, 0, 0] for x in data] since_ms=since_ms,
)
# 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)
@ -2064,9 +2052,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, [] return pair, timeframe, candle_type, [], self._ohlcv_partial_candle
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 return pair, timeframe, candle_type, data, self._ohlcv_partial_candle
except ccxt.NotSupported as e: except ccxt.NotSupported as e:
raise OperationalException( raise OperationalException(
@ -2082,6 +2070,24 @@ 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
@ -2745,11 +2751,16 @@ 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/perpetual/22160/calculation-of-liquidation-price gateio: https://www.gate.io/help/futures/futures/27724/liquidation-price-bankruptcy-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 exchange_name: :param pair: Pair to calculate liquidation price for
: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)
@ -2789,7 +2800,7 @@ class Exchange:
def get_maintenance_ratio_and_amt( def get_maintenance_ratio_and_amt(
self, self,
pair: str, pair: str,
nominal_value: float = 0.0, nominal_value: float,
) -> 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

@ -1,4 +1,6 @@
from typing import Dict, Optional, TypedDict from typing import Dict, List, Optional, Tuple, TypedDict
from freqtrade.enums import CandleType
class Ticker(TypedDict): class Ticker(TypedDict):
@ -14,3 +16,6 @@ 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

@ -0,0 +1,125 @@
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

@ -20,6 +20,9 @@ class Base4ActionRLEnv(BaseEnvironment):
""" """
Base class for a 4 action environment Base class for a 4 action environment
""" """
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.actions = Actions
def set_action_space(self): def set_action_space(self):
self.action_space = spaces.Discrete(len(Actions)) self.action_space = spaces.Discrete(len(Actions))
@ -43,9 +46,9 @@ class Base4ActionRLEnv(BaseEnvironment):
self._done = True self._done = True
self._update_unrealized_total_profit() self._update_unrealized_total_profit()
step_reward = self.calculate_reward(action) step_reward = self.calculate_reward(action)
self.total_reward += step_reward self.total_reward += step_reward
self.tensorboard_log(self.actions._member_names_[action])
trade_type = None trade_type = None
if self.is_tradesignal(action): if self.is_tradesignal(action):
@ -85,16 +88,20 @@ 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 < 1 - self.rl_config.get('max_training_drawdown_pct', 0.8): if (self._total_profit < self.max_drawdown or
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)
info = dict( info = dict(
tick=self._current_tick, tick=self._current_tick,
action=action,
total_reward=self.total_reward, total_reward=self.total_reward,
total_profit=self._total_profit, total_profit=self._total_profit,
position=self._position.value position=self._position.value,
trade_duration=self.get_trade_duration(),
current_profit_pct=self.get_unrealized_profit()
) )
observation = self._get_observation() observation = self._get_observation()

View File

@ -21,6 +21,9 @@ class Base5ActionRLEnv(BaseEnvironment):
""" """
Base class for a 5 action environment Base class for a 5 action environment
""" """
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.actions = Actions
def set_action_space(self): def set_action_space(self):
self.action_space = spaces.Discrete(len(Actions)) self.action_space = spaces.Discrete(len(Actions))
@ -46,6 +49,7 @@ class Base5ActionRLEnv(BaseEnvironment):
self._update_unrealized_total_profit() self._update_unrealized_total_profit()
step_reward = self.calculate_reward(action) step_reward = self.calculate_reward(action)
self.total_reward += step_reward self.total_reward += step_reward
self.tensorboard_log(self.actions._member_names_[action])
trade_type = None trade_type = None
if self.is_tradesignal(action): if self.is_tradesignal(action):
@ -98,9 +102,12 @@ class Base5ActionRLEnv(BaseEnvironment):
info = dict( info = dict(
tick=self._current_tick, tick=self._current_tick,
action=action,
total_reward=self.total_reward, total_reward=self.total_reward,
total_profit=self._total_profit, total_profit=self._total_profit,
position=self._position.value position=self._position.value,
trade_duration=self.get_trade_duration(),
current_profit_pct=self.get_unrealized_profit()
) )
observation = self._get_observation() observation = self._get_observation()

View File

@ -2,7 +2,7 @@ import logging
import random import random
from abc import abstractmethod from abc import abstractmethod
from enum import Enum from enum import Enum
from typing import Optional from typing import Optional, Type, Union
import gym import gym
import numpy as np import numpy as np
@ -11,12 +11,21 @@ from gym import spaces
from gym.utils import seeding from gym.utils import seeding
from pandas import DataFrame from pandas import DataFrame
from freqtrade.data.dataprovider import DataProvider
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class BaseActions(Enum):
"""
Default action space, mostly used for type handling.
"""
Neutral = 0
Long_enter = 1
Long_exit = 2
Short_enter = 3
Short_exit = 4
class Positions(Enum): class Positions(Enum):
Short = 0 Short = 0
Long = 1 Long = 1
@ -35,8 +44,8 @@ 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 = {}, id: str = 'baseenv-1', seed: int = 1, config: dict = {}, live: bool = False,
dp: Optional[DataProvider] = None): fee: float = 0.0015, can_short: bool = False):
""" """
Initializes the training/eval environment. Initializes the training/eval environment.
:param df: dataframe of features :param df: dataframe of features
@ -47,22 +56,31 @@ class BaseEnvironment(gym.Env):
:param id: string id of the environment (used in backend for multiprocessed env) :param id: string id of the environment (used in backend for multiprocessed env)
:param seed: Sets the seed of the environment higher in the gym.Env object :param seed: Sets the seed of the environment higher in the gym.Env object
:param config: Typical user configuration file :param config: Typical user configuration file
:param dp: dataprovider from freqtrade :param live: Whether or not this environment is active in dry/live/backtesting
: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']
self.add_state_info = self.rl_config.get('add_state_info', False) self.add_state_info = self.rl_config.get('add_state_info', False)
self.id = id self.id = id
self.seed(seed)
self.reset_env(df, prices, window_size, reward_kwargs, starting_point)
self.max_drawdown = 1 - self.rl_config.get('max_training_drawdown_pct', 0.8) self.max_drawdown = 1 - self.rl_config.get('max_training_drawdown_pct', 0.8)
self.compound_trades = config['stake_amount'] == 'unlimited' self.compound_trades = config['stake_amount'] == 'unlimited'
if self.config.get('fee', None) is not None: if self.config.get('fee', None) is not None:
self.fee = self.config['fee'] self.fee = self.config['fee']
elif dp is not None:
self.fee = dp._exchange.get_fee(symbol=dp.current_whitelist()[0]) # type: ignore
else: else:
self.fee = 0.0015 self.fee = fee
# set here to default 5Ac, but all children envs can override this
self.actions: Type[Enum] = BaseActions
self.tensorboard_metrics: dict = {}
self.can_short = can_short
self.live = live
if not self.live and self.add_state_info:
self.add_state_info = False
logger.warning("add_state_info is not available in backtesting. Deactivating.")
self.seed(seed)
self.reset_env(df, prices, window_size, reward_kwargs, starting_point)
def reset_env(self, df: DataFrame, prices: DataFrame, window_size: int, def reset_env(self, df: DataFrame, prices: DataFrame, window_size: int,
reward_kwargs: dict, starting_point=True): reward_kwargs: dict, starting_point=True):
@ -117,7 +135,38 @@ class BaseEnvironment(gym.Env):
self.np_random, seed = seeding.np_random(seed) self.np_random, seed = seeding.np_random(seed)
return [seed] return [seed]
def tensorboard_log(self, metric: str, value: Union[int, float] = 1, inc: bool = True):
"""
Function builds the tensorboard_metrics dictionary
to be parsed by the TensorboardCallback. This
function is designed for tracking incremented objects,
events, actions inside the training environment.
For example, a user can call this to track the
frequency of occurence of an `is_valid` call in
their `calculate_reward()`:
def calculate_reward(self, action: int) -> float:
if not self._is_valid(action):
self.tensorboard_log("is_valid")
return -2
:param metric: metric to be tracked and incremented
:param value: value to increment `metric` by
:param inc: sets whether the `value` is incremented or not
"""
if not inc or metric not in self.tensorboard_metrics:
self.tensorboard_metrics[metric] = value
else:
self.tensorboard_metrics[metric] += value
def reset_tensorboard_log(self):
self.tensorboard_metrics = {}
def reset(self): def reset(self):
"""
Reset is called at the beginning of every episode
"""
self.reset_tensorboard_log()
self._done = False self._done = False
@ -271,6 +320,13 @@ class BaseEnvironment(gym.Env):
def current_price(self) -> float: def current_price(self) -> float:
return self.prices.iloc[self._current_tick].open return self.prices.iloc[self._current_tick].open
def get_actions(self) -> Type[Enum]:
"""
Used by SubprocVecEnv to get actions from
initialized env for tensorboard callback
"""
return self.actions
# Keeping around incase we want to start building more complex environment # Keeping around incase we want to start building more complex environment
# templates in the future. # templates in the future.
# def most_recent_return(self): # def most_recent_return(self):

View File

@ -21,7 +21,8 @@ from freqtrade.exceptions import OperationalException
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
from freqtrade.freqai.freqai_interface import IFreqaiModel from freqtrade.freqai.freqai_interface import IFreqaiModel
from freqtrade.freqai.RL.Base5ActionRLEnv import Actions, Base5ActionRLEnv from freqtrade.freqai.RL.Base5ActionRLEnv import Actions, Base5ActionRLEnv
from freqtrade.freqai.RL.BaseEnvironment import Positions from freqtrade.freqai.RL.BaseEnvironment import BaseActions, Positions
from freqtrade.freqai.RL.TensorboardCallback import TensorboardCallback
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
@ -44,8 +45,8 @@ class BaseReinforcementLearningModel(IFreqaiModel):
'cpu_count', 1), max(int(self.max_system_threads / 2), 1)) 'cpu_count', 1), max(int(self.max_system_threads / 2), 1))
th.set_num_threads(self.max_threads) th.set_num_threads(self.max_threads)
self.reward_params = self.freqai_info['rl_config']['model_reward_parameters'] self.reward_params = self.freqai_info['rl_config']['model_reward_parameters']
self.train_env: Union[SubprocVecEnv, gym.Env] = None self.train_env: Union[SubprocVecEnv, Type[gym.Env]] = gym.Env()
self.eval_env: Union[SubprocVecEnv, gym.Env] = None self.eval_env: Union[SubprocVecEnv, Type[gym.Env]] = gym.Env()
self.eval_callback: Optional[EvalCallback] = None self.eval_callback: Optional[EvalCallback] = None
self.model_type = self.freqai_info['rl_config']['model_type'] self.model_type = self.freqai_info['rl_config']['model_type']
self.rl_config = self.freqai_info['rl_config'] self.rl_config = self.freqai_info['rl_config']
@ -65,6 +66,8 @@ class BaseReinforcementLearningModel(IFreqaiModel):
self.unset_outlier_removal() self.unset_outlier_removal()
self.net_arch = self.rl_config.get('net_arch', [128, 128]) self.net_arch = self.rl_config.get('net_arch', [128, 128])
self.dd.model_type = import_str self.dd.model_type = import_str
self.tensorboard_callback: TensorboardCallback = \
TensorboardCallback(verbose=1, actions=BaseActions)
def unset_outlier_removal(self): def unset_outlier_removal(self):
""" """
@ -140,22 +143,36 @@ 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"]
env_info = self.pack_env_dict()
self.train_env = self.MyRLEnv(df=train_df, self.train_env = self.MyRLEnv(df=train_df,
prices=prices_train, prices=prices_train,
window_size=self.CONV_WIDTH, **env_info)
reward_kwargs=self.reward_params,
config=self.config,
dp=self.data_provider)
self.eval_env = Monitor(self.MyRLEnv(df=test_df, self.eval_env = Monitor(self.MyRLEnv(df=test_df,
prices=prices_test, prices=prices_test,
window_size=self.CONV_WIDTH, **env_info))
reward_kwargs=self.reward_params,
config=self.config,
dp=self.data_provider))
self.eval_callback = EvalCallback(self.eval_env, deterministic=True, self.eval_callback = EvalCallback(self.eval_env, deterministic=True,
render=False, eval_freq=len(train_df), render=False, eval_freq=len(train_df),
best_model_save_path=str(dk.data_path)) best_model_save_path=str(dk.data_path))
actions = self.train_env.get_actions()
self.tensorboard_callback = TensorboardCallback(verbose=1, actions=actions)
def pack_env_dict(self) -> Dict[str, Any]:
"""
Create dictionary of environment arguments
"""
env_info = {"window_size": self.CONV_WIDTH,
"reward_kwargs": self.reward_params,
"config": self.config,
"live": self.live,
"can_short": self.can_short}
if self.data_provider:
env_info["fee"] = self.data_provider._exchange \
.get_fee(symbol=self.data_provider.current_whitelist()[0]) # type: ignore
return env_info
@abstractmethod @abstractmethod
def fit(self, data_dictionary: Dict[str, Any], dk: FreqaiDataKitchen, **kwargs): def fit(self, data_dictionary: Dict[str, Any], dk: FreqaiDataKitchen, **kwargs):
""" """
@ -263,26 +280,36 @@ 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']
ohlc_list = [f'%-{pair}raw_open_{tf}', f'%-{pair}raw_low_{tf}', rename_dict = {'%-raw_open': 'open', '%-raw_low': 'low',
f'%-{pair}raw_high_{tf}', f'%-{pair}raw_close_{tf}'] '%-raw_high': ' high', '%-raw_close': 'close'}
rename_dict = {f'%-{pair}raw_open_{tf}': 'open', f'%-{pair}raw_low_{tf}': 'low', rename_dict_old = {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(ohlc_list, axis=1) prices_test = test_df.filter(rename_dict.keys(), 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)
@ -377,8 +404,8 @@ class BaseReinforcementLearningModel(IFreqaiModel):
def make_env(MyRLEnv: Type[gym.Env], env_id: str, rank: int, def make_env(MyRLEnv: Type[gym.Env], env_id: str, rank: int,
seed: int, train_df: DataFrame, price: DataFrame, seed: int, train_df: DataFrame, price: DataFrame,
reward_params: Dict[str, int], window_size: int, monitor: bool = False, monitor: bool = False,
config: Dict[str, Any] = {}) -> Callable: env_info: Dict[str, Any] = {}) -> Callable:
""" """
Utility function for multiprocessed env. Utility function for multiprocessed env.
@ -386,13 +413,14 @@ def make_env(MyRLEnv: Type[gym.Env], env_id: str, rank: int,
:param num_env: (int) the number of environment you wish to have in subprocesses :param num_env: (int) the number of environment you wish to have in subprocesses
:param seed: (int) the inital seed for RNG :param seed: (int) the inital seed for RNG
:param rank: (int) index of the subprocess :param rank: (int) index of the subprocess
:param env_info: (dict) all required arguments to instantiate the environment.
:return: (Callable) :return: (Callable)
""" """
def _init() -> gym.Env: def _init() -> gym.Env:
env = MyRLEnv(df=train_df, prices=price, window_size=window_size, env = MyRLEnv(df=train_df, prices=price, id=env_id, seed=seed + rank,
reward_kwargs=reward_params, id=env_id, seed=seed + rank, config=config) **env_info)
if monitor: if monitor:
env = Monitor(env) env = Monitor(env)
return env return env

View File

@ -0,0 +1,59 @@
from enum import Enum
from typing import Any, Dict, Type, Union
from stable_baselines3.common.callbacks import BaseCallback
from stable_baselines3.common.logger import HParam
from freqtrade.freqai.RL.BaseEnvironment import BaseActions, BaseEnvironment
class TensorboardCallback(BaseCallback):
"""
Custom callback for plotting additional values in tensorboard and
episodic summary reports.
"""
def __init__(self, verbose=1, actions: Type[Enum] = BaseActions):
super(TensorboardCallback, self).__init__(verbose)
self.model: Any = None
self.logger = None # type: Any
self.training_env: BaseEnvironment = None # type: ignore
self.actions: Type[Enum] = actions
def _on_training_start(self) -> None:
hparam_dict = {
"algorithm": self.model.__class__.__name__,
"learning_rate": self.model.learning_rate,
# "gamma": self.model.gamma,
# "gae_lambda": self.model.gae_lambda,
# "batch_size": self.model.batch_size,
# "n_steps": self.model.n_steps,
}
metric_dict: Dict[str, Union[float, int]] = {
"eval/mean_reward": 0,
"rollout/ep_rew_mean": 0,
"rollout/ep_len_mean": 0,
"train/value_loss": 0,
"train/explained_variance": 0,
}
self.logger.record(
"hparams",
HParam(hparam_dict, metric_dict),
exclude=("stdout", "log", "json", "csv"),
)
def _on_step(self) -> bool:
local_info = self.locals["infos"][0]
tensorboard_metrics = self.training_env.get_attr("tensorboard_metrics")[0]
for info in local_info:
if info not in ["episode", "terminal_observation"]:
self.logger.record(f"_info/{info}", local_info[info])
for info in tensorboard_metrics:
if info in [action.name for action in self.actions]:
self.logger.record(f"_actions/{info}", tensorboard_metrics[info])
else:
self.logger.record(f"_custom/{info}", tensorboard_metrics[info])
return True

View File

@ -95,9 +95,14 @@ class BaseClassifierModel(IFreqaiModel):
self.data_cleaning_predict(dk) self.data_cleaning_predict(dk)
predictions = self.model.predict(dk.data_dictionary["prediction_features"]) predictions = self.model.predict(dk.data_dictionary["prediction_features"])
if self.CONV_WIDTH == 1:
predictions = np.reshape(predictions, (-1, len(dk.label_list)))
pred_df = DataFrame(predictions, columns=dk.label_list) pred_df = DataFrame(predictions, columns=dk.label_list)
predictions_prob = self.model.predict_proba(dk.data_dictionary["prediction_features"]) predictions_prob = self.model.predict_proba(dk.data_dictionary["prediction_features"])
if self.CONV_WIDTH == 1:
predictions_prob = np.reshape(predictions_prob, (-1, len(self.model.classes_)))
pred_df_prob = DataFrame(predictions_prob, columns=self.model.classes_) pred_df_prob = DataFrame(predictions_prob, columns=self.model.classes_)
pred_df = pd.concat([pred_df, pred_df_prob], axis=1) pred_df = pd.concat([pred_df, pred_df_prob], axis=1)

View File

@ -95,6 +95,9 @@ class BaseRegressionModel(IFreqaiModel):
self.data_cleaning_predict(dk) self.data_cleaning_predict(dk)
predictions = self.model.predict(dk.data_dictionary["prediction_features"]) predictions = self.model.predict(dk.data_dictionary["prediction_features"])
if self.CONV_WIDTH == 1:
predictions = np.reshape(predictions, (-1, len(dk.label_list)))
pred_df = DataFrame(predictions, columns=dk.label_list) pred_df = DataFrame(predictions, columns=dk.label_list)
pred_df = dk.denormalize_labels_from_metadata(pred_df) pred_df = dk.denormalize_labels_from_metadata(pred_df)

View File

@ -1,4 +1,5 @@
import copy import copy
import inspect
import logging import logging
import shutil import shutil
from datetime import datetime, timezone from datetime import datetime, timezone
@ -23,6 +24,7 @@ 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
@ -1159,9 +1161,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
valid_strs = [f"%-{pair}", f"%{pair}", f"%_{pair}"] pair_cols = [col for col in dataframe.columns if col.startswith("%")
pair_cols = [col for col in dataframe.columns if and f"{pair}_" in col]
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)
@ -1190,6 +1192,103 @@ 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,
@ -1202,7 +1301,87 @@ 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 informative pair dataframes :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 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,3 +1,4 @@
import inspect
import logging import logging
import threading import threading
import time import time
@ -104,6 +105,9 @@ 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)
@ -133,6 +137,10 @@ 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')
@ -147,12 +155,9 @@ 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) dk = self.start_backtesting(dataframe, metadata, self.dk, strategy)
dataframe = dk.remove_features_from_df(dk.return_dataframe) dataframe = dk.remove_features_from_df(dk.return_dataframe)
else: else:
logger.info( logger.info(
@ -253,7 +258,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 self, dataframe: DataFrame, metadata: dict, dk: FreqaiDataKitchen, strategy: IStrategy
) -> 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
@ -265,19 +270,22 @@ 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)
@ -299,18 +307,42 @@ 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):
self.dd.load_metadata(dk) if check_features:
dk.find_features(dataframe) self.dd.load_metadata(dk)
self.check_if_feature_list_matches_strategy(dk) dataframe_dummy_features = self.dk.use_strategy_to_populate_indicators(
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:
dataframe_train = dk.slice_dataframe(tr_train, dataframe) if populate_indicators:
dataframe_backtest = dk.slice_dataframe(tr_backtest, dataframe) dataframe = self.dk.use_strategy_to_populate_indicators(
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:
@ -347,7 +379,6 @@ 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()
@ -911,9 +942,28 @@ 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

@ -61,7 +61,7 @@ class ReinforcementLearner(BaseReinforcementLearningModel):
model = self.MODELCLASS(self.policy_type, self.train_env, policy_kwargs=policy_kwargs, model = self.MODELCLASS(self.policy_type, self.train_env, policy_kwargs=policy_kwargs,
tensorboard_log=Path( tensorboard_log=Path(
dk.full_path / "tensorboard" / dk.pair.split('/')[0]), dk.full_path / "tensorboard" / dk.pair.split('/')[0]),
**self.freqai_info['model_training_parameters'] **self.freqai_info.get('model_training_parameters', {})
) )
else: else:
logger.info('Continual training activated - starting training from previously ' logger.info('Continual training activated - starting training from previously '
@ -71,7 +71,7 @@ class ReinforcementLearner(BaseReinforcementLearningModel):
model.learn( model.learn(
total_timesteps=int(total_timesteps), total_timesteps=int(total_timesteps),
callback=self.eval_callback callback=[self.eval_callback, self.tensorboard_callback]
) )
if Path(dk.data_path / "best_model.zip").is_file(): if Path(dk.data_path / "best_model.zip").is_file():
@ -100,13 +100,17 @@ class ReinforcementLearner(BaseReinforcementLearningModel):
""" """
# first, penalize if the action is not valid # first, penalize if the action is not valid
if not self._is_valid(action): if not self._is_valid(action):
self.tensorboard_log("is_valid")
return -2 return -2
pnl = self.get_unrealized_profit() pnl = self.get_unrealized_profit()
factor = 100. factor = 100.
# reward agent for entering trades # reward agent for entering trades
if (action in (Actions.Long_enter.value, Actions.Short_enter.value) if (action == Actions.Long_enter.value
and self._position == Positions.Neutral):
return 25
if (action == Actions.Short_enter.value
and self._position == Positions.Neutral): and self._position == Positions.Neutral):
return 25 return 25
# discourage agent from not entering trades # discourage agent from not entering trades

View File

@ -1,7 +1,6 @@
import logging import logging
from typing import Any, Dict # , Tuple from typing import Any, Dict
# import numpy.typing as npt
from pandas import DataFrame from pandas import DataFrame
from stable_baselines3.common.callbacks import EvalCallback from stable_baselines3.common.callbacks import EvalCallback
from stable_baselines3.common.vec_env import SubprocVecEnv from stable_baselines3.common.vec_env import SubprocVecEnv
@ -9,6 +8,7 @@ from stable_baselines3.common.vec_env import SubprocVecEnv
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
from freqtrade.freqai.prediction_models.ReinforcementLearner import ReinforcementLearner from freqtrade.freqai.prediction_models.ReinforcementLearner import ReinforcementLearner
from freqtrade.freqai.RL.BaseReinforcementLearningModel import make_env from freqtrade.freqai.RL.BaseReinforcementLearningModel import make_env
from freqtrade.freqai.RL.TensorboardCallback import TensorboardCallback
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -34,18 +34,24 @@ class ReinforcementLearner_multiproc(ReinforcementLearner):
train_df = data_dictionary["train_features"] train_df = data_dictionary["train_features"]
test_df = data_dictionary["test_features"] test_df = data_dictionary["test_features"]
env_info = self.pack_env_dict()
env_id = "train_env" env_id = "train_env"
self.train_env = SubprocVecEnv([make_env(self.MyRLEnv, env_id, i, 1, train_df, prices_train, self.train_env = SubprocVecEnv([make_env(self.MyRLEnv, env_id, i, 1,
self.reward_params, self.CONV_WIDTH, monitor=True, train_df, prices_train,
config=self.config) for i monitor=True,
env_info=env_info) for i
in range(self.max_threads)]) in range(self.max_threads)])
eval_env_id = 'eval_env' eval_env_id = 'eval_env'
self.eval_env = SubprocVecEnv([make_env(self.MyRLEnv, eval_env_id, i, 1, self.eval_env = SubprocVecEnv([make_env(self.MyRLEnv, eval_env_id, i, 1,
test_df, prices_test, test_df, prices_test,
self.reward_params, self.CONV_WIDTH, monitor=True, monitor=True,
config=self.config) for i env_info=env_info) for i
in range(self.max_threads)]) in range(self.max_threads)])
self.eval_callback = EvalCallback(self.eval_env, deterministic=True, self.eval_callback = EvalCallback(self.eval_env, deterministic=True,
render=False, eval_freq=len(train_df), render=False, eval_freq=len(train_df),
best_model_save_path=str(dk.data_path)) best_model_save_path=str(dk.data_path))
actions = self.train_env.env_method("get_actions")[0]
self.tensorboard_callback = TensorboardCallback(verbose=1, actions=actions)

View File

@ -155,6 +155,8 @@ class FreqtradeBot(LoggingMixin):
self.cancel_all_open_orders() self.cancel_all_open_orders()
self.check_for_open_trades() self.check_for_open_trades()
except Exception as e:
logger.warning(f'Exception during cleanup: {e.__class__.__name__} {e}')
finally: finally:
self.strategy.ft_bot_cleanup() self.strategy.ft_bot_cleanup()
@ -162,8 +164,13 @@ class FreqtradeBot(LoggingMixin):
self.rpc.cleanup() self.rpc.cleanup()
if self.emc: if self.emc:
self.emc.shutdown() self.emc.shutdown()
Trade.commit()
self.exchange.close() self.exchange.close()
try:
Trade.commit()
except Exception:
# Exeptions here will be happening if the db disappeared.
# At which point we can no longer commit anyway.
pass
def startup(self) -> None: def startup(self) -> None:
""" """
@ -367,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) order = trade.select_order(trade.exit_side, False, only_filled=True)
if not order: if not order:
order = trade.select_order('stoploss', False) order = trade.select_order('stoploss', False)
if order: if order:
@ -383,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) order = trade.select_order(trade.entry_side, False, only_filled=True)
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(
@ -713,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) order_obj = Order.parse_from_ccxt_object(order, pair, side, amount, enter_limit_requested)
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}.")
@ -905,6 +912,7 @@ 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
@ -1086,7 +1094,8 @@ 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)
@ -1587,7 +1596,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) order_obj = Order.parse_from_ccxt_object(order, trade.pair, trade.exit_side, amount, limit)
trade.orders.append(order_obj) trade.orders.append(order_obj)
trade.open_order_id = order['id'] trade.open_order_id = order['id']

View File

@ -269,6 +269,8 @@ 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')
@ -301,3 +303,21 @@ def remove_entry_exit_signals(dataframe: pd.DataFrame):
dataframe[SignalTagType.EXIT_TAG.value] = None dataframe[SignalTagType.EXIT_TAG.value] = None
return dataframe return dataframe
def append_candles_to_dataframe(left: pd.DataFrame, right: pd.DataFrame) -> pd.DataFrame:
"""
Append the `right` dataframe to the `left` dataframe
:param left: The full dataframe you want appended to
:param right: The new dataframe containing the data you want appended
:returns: The dataframe with the right data in it
"""
if left.iloc[-1]['date'] != right.iloc[-1]['date']:
left = pd.concat([left, right])
# Only keep the last 1500 candles in memory
left = left[-1500:] if len(left) > 1500 else left
left.reset_index(drop=True, inplace=True)
return left

View File

@ -769,6 +769,7 @@ 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
@ -1050,7 +1051,8 @@ 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,
max_open_trades: int, open_trade_count_start: int, is_first: bool = True) -> int: max_open_trades: int, open_trade_count_start: int, trade_dir: Optional[LongShort],
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.
@ -1069,7 +1071,6 @@ 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
@ -1163,7 +1164,15 @@ 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()
if self.timeframe_detail and pair in self.detail_data: trade_dir: Optional[LongShort] = self.check_for_trade_entry(row)
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]
@ -1175,7 +1184,8 @@ class Backtesting:
# 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, max_open_trades, row, pair, current_time, end_date, max_open_trades,
open_trade_count_start) open_trade_count_start, trade_dir)
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]
@ -1187,12 +1197,13 @@ class Backtesting:
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, max_open_trades, det_row, pair, current_time_det, end_date, max_open_trades,
open_trade_count_start, is_first) open_trade_count_start, trade_dir, 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, max_open_trades, open_trade_count_start) row, pair, current_time, end_date, max_open_trades,
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()

View File

@ -5,13 +5,11 @@ 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_max_drawdown from freqtrade.data.metrics import calculate_calmar
from freqtrade.optimize.hyperopt import IHyperOptLoss from freqtrade.optimize.hyperopt import IHyperOptLoss
@ -23,42 +21,15 @@ class CalmarHyperOptLoss(IHyperOptLoss):
""" """
@staticmethod @staticmethod
def hyperopt_loss_function( def hyperopt_loss_function(results: DataFrame, trade_count: int,
results: DataFrame, min_date: datetime, max_date: datetime,
trade_count: int, config: Config, *args, **kwargs) -> float:
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.
""" """
total_profit = backtest_stats["profit_total"] starting_balance = config['dry_run_wallet']
days_period = (max_date - min_date).days calmar_ratio = calculate_calmar(results, min_date, max_date, starting_balance)
# 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,9 +6,10 @@ 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
@ -22,25 +23,13 @@ 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,
*args, **kwargs) -> float: config: Config, *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.
""" """
total_profit = results["profit_ratio"] starting_balance = config['dry_run_wallet']
days_period = (max_date - min_date).days sharp_ratio = calculate_sharpe(results, min_date, max_date, starting_balance)
# 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,9 +6,10 @@ 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
@ -22,28 +23,13 @@ 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,
*args, **kwargs) -> float: config: Config, *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.
""" """
total_profit = results["profit_ratio"] starting_balance = config['dry_run_wallet']
days_period = (max_date - min_date).days sortino_ratio = calculate_sortino(results, min_date, max_date, starting_balance)
# 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

@ -9,8 +9,9 @@ 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) Config)
from freqtrade.data.metrics import (calculate_cagr, calculate_csum, calculate_market_change, from freqtrade.data.metrics import (calculate_cagr, calculate_calmar, calculate_csum,
calculate_max_drawdown) calculate_expectancy, calculate_market_change,
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
@ -448,6 +449,10 @@ 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),
@ -785,8 +790,13 @@ 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,11 +109,10 @@ 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')
# Margin Properties # Futures 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'):
@ -162,7 +161,8 @@ 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,7 +190,8 @@ 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}
""")) """))
@ -213,17 +214,22 @@ 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}
""")) """))
@ -310,8 +316,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, 'contract_size'): # if not has_column(cols_trades, 'max_stake_amount'):
if not has_column(cols_orders, 'funding_fee'): if not has_column(cols_orders, 'ft_price'):
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

@ -49,6 +49,8 @@ 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)
@ -82,9 +84,13 @@ 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 return self.average or self.price or self.stop_price or self.ft_price
@property @property
def safe_filled(self) -> float: def safe_filled(self) -> float:
@ -94,7 +100,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.amount - (self.filled or 0.0) self.safe_amount - (self.filled or 0.0)
) )
@property @property
@ -227,11 +233,20 @@ 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(order: Dict[str, Any], pair: str, side: str) -> 'Order': def parse_from_ccxt_object(
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(order_id=str(order['id']), ft_order_side=side, ft_pair=pair) o = Order(
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
@ -293,6 +308,7 @@ 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
@ -397,12 +413,6 @@ 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:
@ -475,8 +485,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,
@ -513,7 +523,6 @@ 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,
@ -882,6 +891,7 @@ 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
@ -923,7 +933,9 @@ 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
@ -959,11 +971,12 @@ 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) -> Optional[Order]: is_open: Optional[bool] = None, only_filled: bool = False) -> 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
@ -971,6 +984,8 @@ 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:
@ -1175,6 +1190,7 @@ 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)

View File

@ -0,0 +1,206 @@
"""
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[self._sort_key] is not None) and (self._use_range or v.get(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:
@ -218,7 +218,7 @@ class VolumePairList(IPairList):
else: else:
filtered_tickers[i]['quoteVolume'] = 0 filtered_tickers[i]['quoteVolume'] = 0
else: else:
# Tickers mode - filter based on incomming pairlist. # Tickers mode - filter based on incoming pairlist.
filtered_tickers = [v for k, v in tickers.items() if k in pairlist] filtered_tickers = [v for k, v in tickers.items() if k in pairlist]
if self._min_value > 0: if self._min_value > 0:

View File

@ -11,6 +11,7 @@ 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
@ -37,10 +38,11 @@ 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():
if settings[setting] is not None: btconfig = deep_merge_dicts(settings, btconfig, allow_null_overrides=False)
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

@ -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,7 +243,6 @@ 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]
@ -372,6 +371,10 @@ 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
@ -410,6 +413,10 @@ class PairHistory(BaseModel):
} }
class BacktestFreqAIInputs(BaseModel):
identifier: str
class BacktestRequest(BaseModel): class BacktestRequest(BaseModel):
strategy: str strategy: str
timeframe: Optional[str] timeframe: Optional[str]
@ -419,6 +426,9 @@ class BacktestRequest(BaseModel):
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,12 +13,13 @@ 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, Health, ForceEnterResponse, ForceExitPayload,
Locks, Logs, OpenTradeSchema, PairHistory, FreqAIModelListResponse, Health, Locks, Logs,
PerformanceEntry, Ping, PlotConfig, Profit, OpenTradeSchema, PairHistory, PerformanceEntry,
ResultMsg, ShowConfig, Stats, StatusMsg, Ping, PlotConfig, Profit, ResultMsg, ShowConfig,
StrategyListResponse, StrategyResponse, SysInfo, Stats, StatusMsg, StrategyListResponse,
Version, WhitelistResponse) StrategyResponse, SysInfo, Version,
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
@ -38,7 +39,8 @@ 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
API_VERSION = 2.21 # 2.22: Add FreqAI to backtesting
API_VERSION = 2.22
# Public API, requires no auth. # Public API, requires no auth.
router_public = APIRouter() router_public = APIRouter()
@ -279,6 +281,16 @@ 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

@ -91,9 +91,10 @@ async def _process_consumer_request(
elif type == RPCRequestType.ANALYZED_DF: elif type == RPCRequestType.ANALYZED_DF:
# Limit the amount of candles per dataframe to 'limit' or 1500 # Limit the amount of candles per dataframe to 'limit' or 1500
limit = min(data.get('limit', 1500), 1500) if data else None limit = min(data.get('limit', 1500), 1500) if data else None
pair = data.get('pair', None) if data else None
# For every pair in the generator, send a separate message # For every pair in the generator, send a separate message
for message in rpc._ws_request_analyzed_df(limit): for message in rpc._ws_request_analyzed_df(limit, pair):
# Format response # Format response
response = WSAnalyzedDFMessage(data=message) response = WSAnalyzedDFMessage(data=message)
await channel.send(response.dict(exclude_none=True)) await channel.send(response.dict(exclude_none=True))

View File

@ -27,7 +27,8 @@ class WebSocketChannel:
self, self,
websocket: WebSocketType, websocket: WebSocketType,
channel_id: Optional[str] = None, channel_id: Optional[str] = None,
serializer_cls: Type[WebSocketSerializer] = HybridJSONWebSocketSerializer serializer_cls: Type[WebSocketSerializer] = HybridJSONWebSocketSerializer,
send_throttle: float = 0.01
): ):
self.channel_id = channel_id if channel_id else uuid4().hex[:8] self.channel_id = channel_id if channel_id else uuid4().hex[:8]
self._websocket = WebSocketProxy(websocket) self._websocket = WebSocketProxy(websocket)
@ -41,6 +42,7 @@ class WebSocketChannel:
self._send_times: Deque[float] = deque([], maxlen=10) self._send_times: Deque[float] = deque([], maxlen=10)
# High limit defaults to 3 to start # High limit defaults to 3 to start
self._send_high_limit = 3 self._send_high_limit = 3
self._send_throttle = send_throttle
# The subscribed message types # The subscribed message types
self._subscriptions: List[str] = [] self._subscriptions: List[str] = []
@ -106,7 +108,8 @@ class WebSocketChannel:
# Explicitly give control back to event loop as # Explicitly give control back to event loop as
# websockets.send does not # websockets.send does not
await asyncio.sleep(0.01) # Also throttles how fast we send
await asyncio.sleep(self._send_throttle)
async def recv(self): async def recv(self):
""" """

View File

@ -47,7 +47,7 @@ class WSWhitelistRequest(WSRequestSchema):
class WSAnalyzedDFRequest(WSRequestSchema): class WSAnalyzedDFRequest(WSRequestSchema):
type: RPCRequestType = RPCRequestType.ANALYZED_DF type: RPCRequestType = RPCRequestType.ANALYZED_DF
data: Dict[str, Any] = {"limit": 1500} data: Dict[str, Any] = {"limit": 1500, "pair": None}
# ------------------------------ MESSAGE SCHEMAS ---------------------------- # ------------------------------ MESSAGE SCHEMAS ----------------------------

View File

@ -8,15 +8,17 @@ import asyncio
import logging import logging
import socket import socket
from threading import Thread from threading import Thread
from typing import TYPE_CHECKING, Any, Callable, Dict, List, TypedDict from typing import TYPE_CHECKING, Any, Callable, Dict, List, TypedDict, Union
import websockets import websockets
from pydantic import ValidationError from pydantic import ValidationError
from freqtrade.constants import FULL_DATAFRAME_THRESHOLD
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.enums import RPCMessageType from freqtrade.enums import RPCMessageType
from freqtrade.misc import remove_entry_exit_signals from freqtrade.misc import remove_entry_exit_signals
from freqtrade.rpc.api_server.ws import WebSocketChannel from freqtrade.rpc.api_server.ws.channel import WebSocketChannel, create_channel
from freqtrade.rpc.api_server.ws.message_stream import MessageStream
from freqtrade.rpc.api_server.ws_schemas import (WSAnalyzedDFMessage, WSAnalyzedDFRequest, from freqtrade.rpc.api_server.ws_schemas import (WSAnalyzedDFMessage, WSAnalyzedDFRequest,
WSMessageSchema, WSRequestSchema, WSMessageSchema, WSRequestSchema,
WSSubscribeRequest, WSWhitelistMessage, WSSubscribeRequest, WSWhitelistMessage,
@ -38,6 +40,10 @@ class Producer(TypedDict):
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def schema_to_dict(schema: Union[WSMessageSchema, WSRequestSchema]):
return schema.dict(exclude_none=True)
class ExternalMessageConsumer: class ExternalMessageConsumer:
""" """
The main controller class for consuming external messages from The main controller class for consuming external messages from
@ -92,6 +98,8 @@ class ExternalMessageConsumer:
RPCMessageType.ANALYZED_DF: self._consume_analyzed_df_message, RPCMessageType.ANALYZED_DF: self._consume_analyzed_df_message,
} }
self._channel_streams: Dict[str, MessageStream] = {}
self.start() self.start()
def start(self): def start(self):
@ -118,6 +126,8 @@ class ExternalMessageConsumer:
logger.info("Stopping ExternalMessageConsumer") logger.info("Stopping ExternalMessageConsumer")
self._running = False self._running = False
self._channel_streams = {}
if self._sub_tasks: if self._sub_tasks:
# Cancel sub tasks # Cancel sub tasks
for task in self._sub_tasks: for task in self._sub_tasks:
@ -175,7 +185,6 @@ class ExternalMessageConsumer:
:param producer: Dictionary containing producer info :param producer: Dictionary containing producer info
:param lock: An asyncio Lock :param lock: An asyncio Lock
""" """
channel = None
while self._running: while self._running:
try: try:
host, port = producer['host'], producer['port'] host, port = producer['host'], producer['port']
@ -190,19 +199,21 @@ class ExternalMessageConsumer:
max_size=self.message_size_limit, max_size=self.message_size_limit,
ping_interval=None ping_interval=None
) as ws: ) as ws:
channel = WebSocketChannel(ws, channel_id=name) async with create_channel(
ws,
channel_id=name,
send_throttle=0.5
) as channel:
logger.info(f"Producer connection success - {channel}") # Create the message stream for this channel
self._channel_streams[name] = MessageStream()
# Now request the initial data from this Producer # Run the channel tasks while connected
for request in self._initial_requests: await channel.run_channel_tasks(
await channel.send( self._receive_messages(channel, producer, lock),
request.dict(exclude_none=True) self._send_requests(channel, self._channel_streams[name])
) )
# Now receive data, if none is within the time limit, ping
await self._receive_messages(channel, producer, lock)
except (websockets.exceptions.InvalidURI, ValueError) as e: except (websockets.exceptions.InvalidURI, ValueError) as e:
logger.error(f"{ws_url} is an invalid WebSocket URL - {e}") logger.error(f"{ws_url} is an invalid WebSocket URL - {e}")
break break
@ -229,11 +240,19 @@ class ExternalMessageConsumer:
# An unforseen error has occurred, log and continue # An unforseen error has occurred, log and continue
logger.error("Unexpected error has occurred:") logger.error("Unexpected error has occurred:")
logger.exception(e) logger.exception(e)
await asyncio.sleep(self.sleep_time)
continue continue
finally: async def _send_requests(self, channel: WebSocketChannel, channel_stream: MessageStream):
if channel: # Send the initial requests
await channel.close() for init_request in self._initial_requests:
await channel.send(schema_to_dict(init_request))
# Now send any subsequent requests published to
# this channel's stream
async for request, _ in channel_stream:
logger.debug(f"Sending request to channel - {channel} - {request}")
await channel.send(request)
async def _receive_messages( async def _receive_messages(
self, self,
@ -270,19 +289,31 @@ class ExternalMessageConsumer:
latency = (await asyncio.wait_for(pong, timeout=self.ping_timeout) * 1000) latency = (await asyncio.wait_for(pong, timeout=self.ping_timeout) * 1000)
logger.info(f"Connection to {channel} still alive, latency: {latency}ms") logger.info(f"Connection to {channel} still alive, latency: {latency}ms")
continue continue
except (websockets.exceptions.ConnectionClosed):
# Just eat the error and continue reconnecting
logger.warning(f"Disconnection in {channel} - retrying in {self.sleep_time}s")
await asyncio.sleep(self.sleep_time)
break
except Exception as e: except Exception as e:
# Just eat the error and continue reconnecting
logger.warning(f"Ping error {channel} - {e} - retrying in {self.sleep_time}s") logger.warning(f"Ping error {channel} - {e} - retrying in {self.sleep_time}s")
logger.debug(e, exc_info=e) logger.debug(e, exc_info=e)
await asyncio.sleep(self.sleep_time) raise
break def send_producer_request(
self,
producer_name: str,
request: Union[WSRequestSchema, Dict[str, Any]]
):
"""
Publish a message to the producer's message stream to be
sent by the channel task.
:param producer_name: The name of the producer to publish the message to
:param request: The request to send to the producer
"""
if isinstance(request, WSRequestSchema):
request = schema_to_dict(request)
if channel_stream := self._channel_streams.get(producer_name):
channel_stream.publish(request)
def handle_producer_message(self, producer: Producer, message: Dict[str, Any]): def handle_producer_message(self, producer: Producer, message: Dict[str, Any]):
""" """
@ -336,16 +367,45 @@ class ExternalMessageConsumer:
pair, timeframe, candle_type = key pair, timeframe, candle_type = key
if df.empty:
logger.debug(f"Received Empty Dataframe for {key}")
return
# If set, remove the Entry and Exit signals from the Producer # If set, remove the Entry and Exit signals from the Producer
if self._emc_config.get('remove_entry_exit_signals', False): if self._emc_config.get('remove_entry_exit_signals', False):
df = remove_entry_exit_signals(df) df = remove_entry_exit_signals(df)
# Add the dataframe to the dataprovider logger.debug(f"Received {len(df)} candle(s) for {key}")
self._dp._add_external_df(pair, df,
last_analyzed=la, did_append, n_missing = self._dp._add_external_df(
timeframe=timeframe, pair,
candle_type=candle_type, df,
producer_name=producer_name) last_analyzed=la,
timeframe=timeframe,
candle_type=candle_type,
producer_name=producer_name
)
if not did_append:
# We want an overlap in candles incase some data has changed
n_missing += 1
# Set to None for all candles if we missed a full df's worth of candles
n_missing = n_missing if n_missing < FULL_DATAFRAME_THRESHOLD else 1500
logger.warning(f"Holes in data or no existing df, requesting {n_missing} candles "
f"for {key} from `{producer_name}`")
self.send_producer_request(
producer_name,
WSAnalyzedDFRequest(
data={
"limit": n_missing,
"pair": pair
}
)
)
return
logger.debug( logger.debug(
f"Consumed message from `{producer_name}` of type `RPCMessageType.ANALYZED_DF`") f"Consumed message from `{producer_name}` "
f"of type `RPCMessageType.ANALYZED_DF` for {key}")

View File

@ -167,6 +167,7 @@ class RPC:
results = [] results = []
for trade in trades: for trade in trades:
order: Optional[Order] = None order: Optional[Order] = None
current_profit_fiat: Optional[float] = None
if trade.open_order_id: if trade.open_order_id:
order = trade.select_order_by_order_id(trade.open_order_id) order = trade.select_order_by_order_id(trade.open_order_id)
# calculate profit and send message to user # calculate profit and send message to user
@ -176,23 +177,26 @@ class RPC:
trade.pair, side='exit', is_short=trade.is_short, refresh=False) trade.pair, side='exit', is_short=trade.is_short, refresh=False)
except (ExchangeError, PricingError): except (ExchangeError, PricingError):
current_rate = NAN current_rate = NAN
if len(trade.select_filled_orders(trade.entry_side)) > 0:
current_profit = trade.calc_profit_ratio(
current_rate) if not isnan(current_rate) else NAN
current_profit_abs = trade.calc_profit(
current_rate) if not isnan(current_rate) else NAN
else:
current_profit = current_profit_abs = current_profit_fiat = 0.0
else: else:
# Closed trade ...
current_rate = trade.close_rate current_rate = trade.close_rate
if len(trade.select_filled_orders(trade.entry_side)) > 0: current_profit = trade.close_profit
current_profit = trade.calc_profit_ratio( current_profit_abs = trade.close_profit_abs
current_rate) if not isnan(current_rate) else NAN
current_profit_abs = trade.calc_profit( # Calculate fiat profit
current_rate) if not isnan(current_rate) else NAN if not isnan(current_profit_abs) and self._fiat_converter:
current_profit_fiat: Optional[float] = None current_profit_fiat = self._fiat_converter.convert_amount(
# Calculate fiat profit current_profit_abs,
if self._fiat_converter: self._freqtrade.config['stake_currency'],
current_profit_fiat = self._fiat_converter.convert_amount( self._freqtrade.config['fiat_display_currency']
current_profit_abs, )
self._freqtrade.config['stake_currency'],
self._freqtrade.config['fiat_display_currency']
)
else:
current_profit = current_profit_abs = current_profit_fiat = 0.0
# Calculate guaranteed profit (in case of trailing stop) # Calculate guaranteed profit (in case of trailing stop)
stoploss_entry_dist = trade.calc_profit(trade.stop_loss) stoploss_entry_dist = trade.calc_profit(trade.stop_loss)
@ -1058,15 +1062,26 @@ class RPC:
return self._convert_dataframe_to_dict(self._freqtrade.config['strategy'], return self._convert_dataframe_to_dict(self._freqtrade.config['strategy'],
pair, timeframe, _data, last_analyzed) pair, timeframe, _data, last_analyzed)
def __rpc_analysed_dataframe_raw(self, pair: str, timeframe: str, def __rpc_analysed_dataframe_raw(
limit: Optional[int]) -> Tuple[DataFrame, datetime]: self,
""" Get the dataframe and last analyze from the dataprovider """ pair: str,
timeframe: str,
limit: Optional[int]
) -> Tuple[DataFrame, datetime]:
"""
Get the dataframe and last analyze from the dataprovider
:param pair: The pair to get
:param timeframe: The timeframe of data to get
:param limit: The amount of candles in the dataframe
"""
_data, last_analyzed = self._freqtrade.dataprovider.get_analyzed_dataframe( _data, last_analyzed = self._freqtrade.dataprovider.get_analyzed_dataframe(
pair, timeframe) pair, timeframe)
_data = _data.copy() _data = _data.copy()
if limit: if limit:
_data = _data.iloc[-limit:] _data = _data.iloc[-limit:]
return _data, last_analyzed return _data, last_analyzed
def _ws_all_analysed_dataframes( def _ws_all_analysed_dataframes(
@ -1074,7 +1089,16 @@ class RPC:
pairlist: List[str], pairlist: List[str],
limit: Optional[int] limit: Optional[int]
) -> Generator[Dict[str, Any], None, None]: ) -> Generator[Dict[str, Any], None, None]:
""" Get the analysed dataframes of each pair in the pairlist """ """
Get the analysed dataframes of each pair in the pairlist.
If specified, only return the most recent `limit` candles for
each dataframe.
:param pairlist: A list of pairs to get
:param limit: If an integer, limits the size of dataframe
If a list of string date times, only returns those candles
:returns: A generator of dictionaries with the key, dataframe, and last analyzed timestamp
"""
timeframe = self._freqtrade.config['timeframe'] timeframe = self._freqtrade.config['timeframe']
candle_type = self._freqtrade.config.get('candle_type_def', CandleType.SPOT) candle_type = self._freqtrade.config.get('candle_type_def', CandleType.SPOT)
@ -1087,10 +1111,15 @@ class RPC:
"la": last_analyzed "la": last_analyzed
} }
def _ws_request_analyzed_df(self, limit: Optional[int]): def _ws_request_analyzed_df(
self,
limit: Optional[int] = None,
pair: Optional[str] = None
):
""" Historical Analyzed Dataframes for WebSocket """ """ Historical Analyzed Dataframes for WebSocket """
whitelist = self._freqtrade.active_pair_whitelist pairlist = [pair] if pair else self._freqtrade.active_pair_whitelist
return self._ws_all_analysed_dataframes(whitelist, limit)
return self._ws_all_analysed_dataframes(pairlist, limit)
def _ws_request_whitelist(self): def _ws_request_whitelist(self):
""" Whitelist data for WebSocket """ """ Whitelist data for WebSocket """

View File

@ -598,6 +598,7 @@ class IStrategy(ABC, HyperStrategyMixin):
informative: 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.
@ -610,6 +611,98 @@ 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
### ###

View File

@ -95,65 +95,132 @@ 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)
# FreqAI required function, user can add or remove indicators, but general structure def feature_engineering_expand_all(self, dataframe, period, **kwargs):
# must stay the same.
def populate_any_indicators(
self, pair, df, tf, informative=None, set_generalized_indicators=False
):
""" """
User feeds these indicators to FreqAI to train a classifier to decide *Only functional with FreqAI enabled strategies*
if the market will go up or down. 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.
:param pair: pair to be used as informative All features must be prepended with `%` to be recognized by FreqAI internals.
:param df: strategy dataframe which will receive merges from informatives
:param tf: timeframe of the dataframe which will modify the feature names More details on how these config defined parameters accelerate feature engineering
:param informative: the dataframe associated with the informative pair 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)
""" """
if informative is None: dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
informative = self.dp.get_pair_dataframe(pair, tf) 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)
# first loop is automatically duplicating indicators for time periods bollinger = qtpylib.bollinger_bands(
for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]: 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"]
t = int(t) dataframe["%-bb_width-period"] = (
informative[f"%-{pair}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t) dataframe["bb_upperband-period"]
informative[f"%-{pair}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t) - dataframe["bb_lowerband-period"]
informative[f"%-{pair}adx-period_{t}"] = ta.ADX(informative, timeperiod=t) ) / dataframe["bb_middleband-period"]
informative[f"%-{pair}sma-period_{t}"] = ta.SMA(informative, timeperiod=t) dataframe["%-close-bb_lower-period"] = (
informative[f"%-{pair}ema-period_{t}"] = ta.EMA(informative, timeperiod=t) dataframe["close"] / dataframe["bb_lowerband-period"]
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()
)
# FreqAI needs the following lines in order to detect features and automatically dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period)
# 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)
df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True) dataframe["%-relative_volume-period"] = (
skip_columns = [ dataframe["volume"] / dataframe["volume"].rolling(period).mean()
(s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"] )
]
df = df.drop(columns=skip_columns)
# User can set the "target" here (in present case it is the return dataframe
# "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')
return df 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-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,12 +1,11 @@
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, merge_informative_pair from freqtrade.strategy import CategoricalParameter, IStrategy
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -18,8 +17,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. populate_any_indicators() automatically to make predictions on their data. feature_engineering_*() automatically
generates the variety of features indicated by the user in the generate the variety of features indicated by the user in the
canonical freqtrade configuration file under config['freqai']. canonical freqtrade configuration file under config['freqai'].
""" """
@ -28,7 +27,7 @@ class FreqaiExampleStrategy(IStrategy):
plot_config = { plot_config = {
"main_plot": {}, "main_plot": {},
"subplots": { "subplots": {
"prediction": {"prediction": {"color": "blue"}}, "&-s_close": {"prediction": {"color": "blue"}},
"do_predict": { "do_predict": {
"do_predict": {"color": "brown"}, "do_predict": {"color": "brown"},
}, },
@ -40,133 +39,179 @@ 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 = False can_short = True
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 populate_any_indicators( def feature_engineering_expand_all(self, dataframe, period, **kwargs):
self, pair, df, tf, informative=None, set_generalized_indicators=False
):
""" """
Function designed to automatically generate, name and merge features *Only functional with FreqAI enabled strategies*
from user indicated timeframes in the configuration file. User controls the indicators This function will automatically expand the defined features on the config defined
passed to the training/prediction by prepending indicators with `f'%-{pair}` `indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and
(see convention below). I.e. user should not prepend any supporting metrics `include_corr_pairs`. In other words, a single feature defined in this function
(e.g. bb_lowerband below) with % unless they explicitly want to pass that metric to the will automatically expand to a total of
model. `indicator_periods_candles` * `include_timeframes` * `include_shifted_candles` *
:param pair: pair to be used as informative `include_corr_pairs` numbers of features added to the model.
:param df: strategy dataframe which will receive merges from informatives
:param tf: timeframe of the dataframe which will modify the feature names All features must be prepended with `%` to be recognized by FreqAI internals.
: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)
""" """
if informative is None: dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
informative = self.dp.get_pair_dataframe(pair, tf) 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)
# first loop is automatically duplicating indicators for time periods bollinger = qtpylib.bollinger_bands(
for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]: 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"]
t = int(t) dataframe["%-bb_width-period"] = (
informative[f"%-{pair}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t) dataframe["bb_upperband-period"]
informative[f"%-{pair}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t) - dataframe["bb_lowerband-period"]
informative[f"%-{pair}adx-period_{t}"] = ta.ADX(informative, timeperiod=t) ) / dataframe["bb_middleband-period"]
informative[f"%-{pair}sma-period_{t}"] = ta.SMA(informative, timeperiod=t) dataframe["%-close-bb_lower-period"] = (
informative[f"%-{pair}ema-period_{t}"] = ta.EMA(informative, timeperiod=t) dataframe["close"] / dataframe["bb_lowerband-period"]
)
bollinger = qtpylib.bollinger_bands( dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period)
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}"] = ( dataframe["%-relative_volume-period"] = (
informative[f"{pair}bb_upperband-period_{t}"] dataframe["volume"] / dataframe["volume"].rolling(period).mean()
- informative[f"{pair}bb_lowerband-period_{t}"] )
) / informative[f"{pair}bb_middleband-period_{t}"]
informative[f"%-{pair}close-bb_lower-period_{t}"] = ( return dataframe
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
) )
informative[f"%-{pair}roc-period_{t}"] = ta.ROC(informative, timeperiod=t) # Classifiers are typically set up with strings as targets:
# df['&s-up_or_down'] = np.where( df["close"].shift(-100) >
# df["close"], 'up', 'down')
informative[f"%-{pair}relative_volume-period_{t}"] = ( # If user wishes to use multiple targets, they can add more by
informative["volume"] / informative["volume"].rolling(t).mean() # appending more columns with '&'. User should keep in mind that multi targets
) # requires a multioutput prediction model such as
# freqai/prediction_models/CatboostRegressorMultiTarget.py,
# freqtrade trade --freqaimodel CatboostRegressorMultiTarget
informative[f"%-{pair}pct-change"] = informative["close"].pct_change() # df["&-s_range"] = (
informative[f"%-{pair}raw_volume"] = informative["volume"] # df["close"]
informative[f"%-{pair}raw_price"] = informative["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()
# )
indicators = [col for col in informative if col.startswith("%")] return dataframe
# 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 populate_any_indicators() for live functionality # All indicators must be populated by feature_engineering_*() functions
# to work correctly.
# the model will return all labels created by user in `populate_any_indicators` # the model will return all labels created by user in `feature_engineering_*`
# (& appended targets), an indication of whether or not the prediction should be accepted, # (& appended targets), an indication of whether or not the prediction should be accepted,
# the target mean/std values for each of the labels created by user in # the target mean/std values for each of the labels created by user in
# `populate_any_indicators()` for each training period. # `set_freqai_targets()` 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

@ -7,14 +7,17 @@
"# Strategy analysis example\n", "# Strategy analysis example\n",
"\n", "\n",
"Debugging a strategy can be time-consuming. Freqtrade offers helper functions to visualize raw data.\n", "Debugging a strategy can be time-consuming. Freqtrade offers helper functions to visualize raw data.\n",
"The following assumes you work with SampleStrategy, data for 5m timeframe from Binance and have downloaded them into the data directory in the default location." "The following assumes you work with SampleStrategy, data for 5m timeframe from Binance and have downloaded them into the data directory in the default location.\n",
"Please follow the [documentation](https://www.freqtrade.io/en/stable/data-download/) for more details."
] ]
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},
"source": [ "source": [
"## Setup" "## Setup\n",
"\n",
"### Change Working directory to repository root"
] ]
}, },
{ {
@ -23,7 +26,38 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"import os\n",
"from pathlib import Path\n", "from pathlib import Path\n",
"\n",
"# Change directory\n",
"# Modify this cell to insure that the output shows the correct path.\n",
"# Define all paths relative to the project root shown in the cell output\n",
"project_root = \"somedir/freqtrade\"\n",
"i=0\n",
"try:\n",
" os.chdirdir(project_root)\n",
" assert Path('LICENSE').is_file()\n",
"except:\n",
" while i<4 and (not Path('LICENSE').is_file()):\n",
" os.chdir(Path(Path.cwd(), '../'))\n",
" i+=1\n",
" project_root = Path.cwd()\n",
"print(Path.cwd())"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Configure Freqtrade environment"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from freqtrade.configuration import Configuration\n", "from freqtrade.configuration import Configuration\n",
"\n", "\n",
"# Customize these according to your needs.\n", "# Customize these according to your needs.\n",
@ -31,14 +65,14 @@
"# Initialize empty configuration object\n", "# Initialize empty configuration object\n",
"config = Configuration.from_files([])\n", "config = Configuration.from_files([])\n",
"# Optionally (recommended), use existing configuration file\n", "# Optionally (recommended), use existing configuration file\n",
"# config = Configuration.from_files([\"config.json\"])\n", "# config = Configuration.from_files([\"user_data/config.json\"])\n",
"\n", "\n",
"# Define some constants\n", "# Define some constants\n",
"config[\"timeframe\"] = \"5m\"\n", "config[\"timeframe\"] = \"5m\"\n",
"# Name of the strategy class\n", "# Name of the strategy class\n",
"config[\"strategy\"] = \"SampleStrategy\"\n", "config[\"strategy\"] = \"SampleStrategy\"\n",
"# Location of the data\n", "# Location of the data\n",
"data_location = config['datadir']\n", "data_location = config[\"datadir\"]\n",
"# Pair to analyze - Only use one pair here\n", "# Pair to analyze - Only use one pair here\n",
"pair = \"BTC/USDT\"" "pair = \"BTC/USDT\""
] ]
@ -56,12 +90,12 @@
"candles = load_pair_history(datadir=data_location,\n", "candles = load_pair_history(datadir=data_location,\n",
" timeframe=config[\"timeframe\"],\n", " timeframe=config[\"timeframe\"],\n",
" pair=pair,\n", " pair=pair,\n",
" data_format = \"hdf5\",\n", " data_format = \"json\", # Make sure to update this to your data\n",
" candle_type=CandleType.SPOT,\n", " candle_type=CandleType.SPOT,\n",
" )\n", " )\n",
"\n", "\n",
"# Confirm success\n", "# Confirm success\n",
"print(\"Loaded \" + str(len(candles)) + f\" rows of data for {pair} from {data_location}\")\n", "print(f\"Loaded {len(candles)} rows of data for {pair} from {data_location}\")\n",
"candles.head()" "candles.head()"
] ]
}, },
@ -365,7 +399,7 @@
"metadata": { "metadata": {
"file_extension": ".py", "file_extension": ".py",
"kernelspec": { "kernelspec": {
"display_name": "Python 3.9.7 64-bit ('trade_397')", "display_name": "Python 3.9.7 64-bit",
"language": "python", "language": "python",
"name": "python3" "name": "python3"
}, },

View File

@ -291,12 +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_stake_amount = 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_stake_amount = min(max_stake_amount, max_stake_amount - trade_amount)
if min_stake_amount is not None and min_stake_amount > max_stake_amount: if min_stake_amount is not None and min_stake_amount > max_stake_amount:
if self._log: if self._log:

View File

@ -41,6 +41,7 @@ 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
@ -58,7 +59,11 @@ 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

@ -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.20.0 pre-commit==2.21.0
pytest==7.2.0 pytest==7.2.0
pytest-asyncio==0.20.2 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.10.1 isort==5.11.4
# For datetime mocking # For datetime mocking
time-machine==2.8.2 time-machine==2.9.0
# fastapi testing # fastapi testing
httpx==0.23.1 httpx==0.23.3
# Convert jupyter notebooks to markdown documents # Convert jupyter notebooks to markdown documents
nbconvert==7.2.5 nbconvert==7.2.7
# 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.5 types-requests==2.28.11.7
types-tabulate==0.9.0.0 types-tabulate==0.9.0.0
types-python-dateutil==2.8.19.4 types-python-dateutil==2.8.19.5

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.0 torch==1.13.1
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,6 @@
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.3 lightgbm==3.3.4
xgboost==1.7.1 xgboost==1.7.2
tensorboard==2.11.0 tensorboard==2.11.0

View File

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

View File

@ -1,14 +1,14 @@
numpy==1.23.5 numpy==1.24.1
pandas==1.5.2 pandas==1.5.2
pandas-ta==0.3.14b pandas-ta==0.3.14b
ccxt==2.2.67 ccxt==2.5.56
# 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==38.0.4; platform_machine != 'armv7l' cryptography==38.0.4; platform_machine != 'armv7l'
aiohttp==3.8.3 aiohttp==3.8.3
SQLAlchemy==1.4.44 SQLAlchemy==1.4.46
python-telegram-bot==13.14 python-telegram-bot==13.15
arrow==1.2.3 arrow==1.2.3
cachetools==4.2.2 cachetools==4.2.2
requests==2.28.1 requests==2.28.1
@ -19,8 +19,8 @@ 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.7.0 tables==3.8.0
blosc==1.10.6 blosc==1.11.1
joblib==1.2.0 joblib==1.2.0
pyarrow==10.0.1; platform_machine != 'armv7l' pyarrow==10.0.1; platform_machine != 'armv7l'
@ -30,14 +30,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.3 orjson==3.8.4
# Notify systemd # Notify systemd
sdnotify==0.3.2 sdnotify==0.3.2
# API Server # API Server
fastapi==0.88.0 fastapi==0.89.0
pydantic==1.10.2 pydantic==1.10.4
uvicorn==0.20.0 uvicorn==0.20.0
pyjwt==2.6.0 pyjwt==2.6.0
aiofiles==22.1.0 aiofiles==22.1.0
@ -47,7 +47,7 @@ psutil==5.9.4
colorama==0.4.6 colorama==0.4.6
# Building config files interactively # Building config files interactively
questionary==1.10.0 questionary==1.10.0
prompt-toolkit==3.0.33 prompt-toolkit==3.0.36
# Extensions to datetime library # Extensions to datetime library
python-dateutil==2.8.2 python-dateutil==2.8.2

View File

@ -25,6 +25,11 @@ freqai_rl = [
'sb3-contrib' 'sb3-contrib'
] ]
hdf5 = [
'tables',
'blosc',
]
develop = [ develop = [
'coveralls', 'coveralls',
'flake8', 'flake8',
@ -44,7 +49,7 @@ jupyter = [
'nbconvert', 'nbconvert',
] ]
all_extra = plot + develop + jupyter + hyperopt + freqai + freqai_rl all_extra = plot + develop + jupyter + hyperopt + hdf5 + freqai + freqai_rl
setup( setup(
tests_require=[ tests_require=[
@ -78,8 +83,6 @@ setup(
'prompt-toolkit', 'prompt-toolkit',
'numpy', 'numpy',
'pandas', 'pandas',
'tables',
'blosc',
'joblib>=1.2.0', 'joblib>=1.2.0',
'pyarrow; platform_machine != "armv7l"', 'pyarrow; platform_machine != "armv7l"',
'fastapi', 'fastapi',
@ -97,6 +100,7 @@ setup(
'plot': plot, 'plot': plot,
'jupyter': jupyter, 'jupyter': jupyter,
'hyperopt': hyperopt, 'hyperopt': hyperopt,
'hdf5': hdf5,
'freqai': freqai, 'freqai': freqai,
'freqai_rl': freqai_rl, 'freqai_rl': freqai_rl,
'all': all_extra, 'all': all_extra,

View File

@ -746,9 +746,7 @@ def test_download_data_no_exchange(mocker, caplog):
start_download_data(pargs) start_download_data(pargs)
def test_download_data_no_pairs(mocker, caplog): def test_download_data_no_pairs(mocker):
mocker.patch.object(Path, "exists", MagicMock(return_value=False))
mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data', mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data',
MagicMock(return_value=["ETH/BTC", "XRP/BTC"])) MagicMock(return_value=["ETH/BTC", "XRP/BTC"]))
@ -770,8 +768,6 @@ def test_download_data_no_pairs(mocker, caplog):
def test_download_data_all_pairs(mocker, markets): def test_download_data_all_pairs(mocker, markets):
mocker.patch.object(Path, "exists", MagicMock(return_value=False))
dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data', dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data',
MagicMock(return_value=["ETH/BTC", "XRP/BTC"])) MagicMock(return_value=["ETH/BTC", "XRP/BTC"]))
patch_exchange(mocker) patch_exchange(mocker)
@ -1529,7 +1525,7 @@ def test_backtesting_show(mocker, testdatadir, capsys):
args = [ args = [
"backtesting-show", "backtesting-show",
"--export-filename", "--export-filename",
f"{testdatadir / 'backtest_results/backtest-result_new.json'}", f"{testdatadir / 'backtest_results/backtest-result.json'}",
"--show-pair-list" "--show-pair-list"
] ]
pargs = get_args(args) pargs = get_args(args)

View File

@ -408,6 +408,11 @@ def create_mock_trades_usdt(fee, is_short: Optional[bool] = False, use_db: bool
Trade.commit() Trade.commit()
@pytest.fixture(autouse=True)
def patch_gc(mocker) -> None:
mocker.patch("freqtrade.main.gc_set_threshold")
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def patch_coingekko(mocker) -> None: def patch_coingekko(mocker) -> None:
""" """
@ -2601,6 +2606,8 @@ def open_trade():
ft_order_side='buy', ft_order_side='buy',
ft_pair=trade.pair, ft_pair=trade.pair,
ft_is_open=False, ft_is_open=False,
ft_amount=trade.amount,
ft_price=trade.open_rate,
order_id='123456789', order_id='123456789',
status="closed", status="closed",
symbol=trade.pair, symbol=trade.pair,
@ -2637,6 +2644,8 @@ def open_trade_usdt():
ft_order_side='buy', ft_order_side='buy',
ft_pair=trade.pair, ft_pair=trade.pair,
ft_is_open=False, ft_is_open=False,
ft_amount=trade.amount,
ft_price=trade.open_rate,
order_id='123456789', order_id='123456789',
status="closed", status="closed",
symbol=trade.pair, symbol=trade.pair,
@ -2654,6 +2663,8 @@ def open_trade_usdt():
ft_order_side='exit', ft_order_side='exit',
ft_pair=trade.pair, ft_pair=trade.pair,
ft_is_open=True, ft_is_open=True,
ft_amount=trade.amount,
ft_price=trade.open_rate,
order_id='123456789_exit', order_id='123456789_exit',
status="open", status="open",
symbol=trade.pair, symbol=trade.pair,

View File

@ -12,9 +12,11 @@ from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, analyze_trade_parallelis
get_latest_hyperopt_file, load_backtest_data, get_latest_hyperopt_file, load_backtest_data,
load_backtest_metadata, load_trades, load_trades_from_db) load_backtest_metadata, load_trades, load_trades_from_db)
from freqtrade.data.history import load_data, load_pair_history from freqtrade.data.history import load_data, load_pair_history
from freqtrade.data.metrics import (calculate_cagr, calculate_csum, calculate_market_change, from freqtrade.data.metrics import (calculate_cagr, calculate_calmar, calculate_csum,
calculate_max_drawdown, calculate_underwater, calculate_expectancy, calculate_market_change,
combine_dataframes_with_mean, create_cum_profit) calculate_max_drawdown, calculate_sharpe, calculate_sortino,
calculate_underwater, combine_dataframes_with_mean,
create_cum_profit)
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from tests.conftest import CURRENT_TEST_STRATEGY, create_mock_trades from tests.conftest import CURRENT_TEST_STRATEGY, create_mock_trades
from tests.conftest_trades import MOCK_TRADE_COUNT from tests.conftest_trades import MOCK_TRADE_COUNT
@ -30,10 +32,10 @@ def test_get_latest_backtest_filename(testdatadir, mocker):
testdir_bt = testdatadir / "backtest_results" testdir_bt = testdatadir / "backtest_results"
res = get_latest_backtest_filename(testdir_bt) res = get_latest_backtest_filename(testdir_bt)
assert res == 'backtest-result_new.json' assert res == 'backtest-result.json'
res = get_latest_backtest_filename(str(testdir_bt)) res = get_latest_backtest_filename(str(testdir_bt))
assert res == 'backtest-result_new.json' assert res == 'backtest-result.json'
mocker.patch("freqtrade.data.btanalysis.json_load", return_value={}) mocker.patch("freqtrade.data.btanalysis.json_load", return_value={})
@ -81,7 +83,7 @@ def test_load_backtest_data_old_format(testdatadir, mocker):
def test_load_backtest_data_new_format(testdatadir): def test_load_backtest_data_new_format(testdatadir):
filename = testdatadir / "backtest_results/backtest-result_new.json" filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename) bt_data = load_backtest_data(filename)
assert isinstance(bt_data, DataFrame) assert isinstance(bt_data, DataFrame)
assert set(bt_data.columns) == set(BT_DATA_COLUMNS) assert set(bt_data.columns) == set(BT_DATA_COLUMNS)
@ -182,7 +184,7 @@ def test_extract_trades_of_period(testdatadir):
def test_analyze_trade_parallelism(testdatadir): def test_analyze_trade_parallelism(testdatadir):
filename = testdatadir / "backtest_results/backtest-result_new.json" filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename) bt_data = load_backtest_data(filename)
res = analyze_trade_parallelism(bt_data, "5m") res = analyze_trade_parallelism(bt_data, "5m")
@ -256,7 +258,7 @@ def test_combine_dataframes_with_mean_no_data(testdatadir):
def test_create_cum_profit(testdatadir): def test_create_cum_profit(testdatadir):
filename = testdatadir / "backtest_results/backtest-result_new.json" filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename) bt_data = load_backtest_data(filename)
timerange = TimeRange.parse_timerange("20180110-20180112") timerange = TimeRange.parse_timerange("20180110-20180112")
@ -268,11 +270,11 @@ def test_create_cum_profit(testdatadir):
"cum_profits", timeframe="5m") "cum_profits", timeframe="5m")
assert "cum_profits" in cum_profits.columns assert "cum_profits" in cum_profits.columns
assert cum_profits.iloc[0]['cum_profits'] == 0 assert cum_profits.iloc[0]['cum_profits'] == 0
assert pytest.approx(cum_profits.iloc[-1]['cum_profits']) == 8.723007518796964e-06 assert pytest.approx(cum_profits.iloc[-1]['cum_profits']) == 9.0225563e-05
def test_create_cum_profit1(testdatadir): def test_create_cum_profit1(testdatadir):
filename = testdatadir / "backtest_results/backtest-result_new.json" filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename) bt_data = load_backtest_data(filename)
# Move close-time to "off" the candle, to make sure the logic still works # Move close-time to "off" the candle, to make sure the logic still works
bt_data['close_date'] = bt_data.loc[:, 'close_date'] + DateOffset(seconds=20) bt_data['close_date'] = bt_data.loc[:, 'close_date'] + DateOffset(seconds=20)
@ -286,7 +288,7 @@ def test_create_cum_profit1(testdatadir):
"cum_profits", timeframe="5m") "cum_profits", timeframe="5m")
assert "cum_profits" in cum_profits.columns assert "cum_profits" in cum_profits.columns
assert cum_profits.iloc[0]['cum_profits'] == 0 assert cum_profits.iloc[0]['cum_profits'] == 0
assert pytest.approx(cum_profits.iloc[-1]['cum_profits']) == 8.723007518796964e-06 assert pytest.approx(cum_profits.iloc[-1]['cum_profits']) == 9.0225563e-05
with pytest.raises(ValueError, match='Trade dataframe empty.'): with pytest.raises(ValueError, match='Trade dataframe empty.'):
create_cum_profit(df.set_index('date'), bt_data[bt_data["pair"] == 'NOTAPAIR'], create_cum_profit(df.set_index('date'), bt_data[bt_data["pair"] == 'NOTAPAIR'],
@ -294,18 +296,18 @@ def test_create_cum_profit1(testdatadir):
def test_calculate_max_drawdown(testdatadir): def test_calculate_max_drawdown(testdatadir):
filename = testdatadir / "backtest_results/backtest-result_new.json" filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename) bt_data = load_backtest_data(filename)
_, hdate, lowdate, hval, lval, drawdown = calculate_max_drawdown( _, hdate, lowdate, hval, lval, drawdown = calculate_max_drawdown(
bt_data, value_col="profit_abs") bt_data, value_col="profit_abs")
assert isinstance(drawdown, float) assert isinstance(drawdown, float)
assert pytest.approx(drawdown) == 0.12071099 assert pytest.approx(drawdown) == 0.29753914
assert isinstance(hdate, Timestamp) assert isinstance(hdate, Timestamp)
assert isinstance(lowdate, Timestamp) assert isinstance(lowdate, Timestamp)
assert isinstance(hval, float) assert isinstance(hval, float)
assert isinstance(lval, float) assert isinstance(lval, float)
assert hdate == Timestamp('2018-01-25 01:30:00', tz='UTC') assert hdate == Timestamp('2018-01-16 19:30:00', tz='UTC')
assert lowdate == Timestamp('2018-01-25 03:50:00', tz='UTC') assert lowdate == Timestamp('2018-01-16 22:25:00', tz='UTC')
underwater = calculate_underwater(bt_data) underwater = calculate_underwater(bt_data)
assert isinstance(underwater, DataFrame) assert isinstance(underwater, DataFrame)
@ -318,14 +320,15 @@ def test_calculate_max_drawdown(testdatadir):
def test_calculate_csum(testdatadir): def test_calculate_csum(testdatadir):
filename = testdatadir / "backtest_results/backtest-result_new.json" filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename) bt_data = load_backtest_data(filename)
csum_min, csum_max = calculate_csum(bt_data) csum_min, csum_max = calculate_csum(bt_data)
assert isinstance(csum_min, float) assert isinstance(csum_min, float)
assert isinstance(csum_max, float) assert isinstance(csum_max, float)
assert csum_min < 0.01 assert csum_min < csum_max
assert csum_max > 0.02 assert csum_min < 0.0001
assert csum_max > 0.0002
csum_min1, csum_max1 = calculate_csum(bt_data, 5) csum_min1, csum_max1 = calculate_csum(bt_data, 5)
assert csum_min1 == csum_min + 5 assert csum_min1 == csum_min + 5
@ -335,6 +338,69 @@ def test_calculate_csum(testdatadir):
csum_min, csum_max = calculate_csum(DataFrame()) csum_min, csum_max = calculate_csum(DataFrame())
def test_calculate_expectancy(testdatadir):
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
expectancy = calculate_expectancy(DataFrame())
assert expectancy == 0.0
expectancy = calculate_expectancy(bt_data)
assert isinstance(expectancy, float)
assert pytest.approx(expectancy) == 0.07151374226574791
def test_calculate_sortino(testdatadir):
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
sortino = calculate_sortino(DataFrame(), None, None, 0)
assert sortino == 0.0
sortino = calculate_sortino(
bt_data,
bt_data['open_date'].min(),
bt_data['close_date'].max(),
0.01,
)
assert isinstance(sortino, float)
assert pytest.approx(sortino) == 35.17722
def test_calculate_sharpe(testdatadir):
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
sharpe = calculate_sharpe(DataFrame(), None, None, 0)
assert sharpe == 0.0
sharpe = calculate_sharpe(
bt_data,
bt_data['open_date'].min(),
bt_data['close_date'].max(),
0.01,
)
assert isinstance(sharpe, float)
assert pytest.approx(sharpe) == 44.5078669
def test_calculate_calmar(testdatadir):
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
calmar = calculate_calmar(DataFrame(), None, None, 0)
assert calmar == 0.0
calmar = calculate_calmar(
bt_data,
bt_data['open_date'].min(),
bt_data['close_date'].max(),
0.01,
)
assert isinstance(calmar, float)
assert pytest.approx(calmar) == 559.040508
@pytest.mark.parametrize('start,end,days, expected', [ @pytest.mark.parametrize('start,end,days, expected', [
(64900, 176000, 3 * 365, 0.3945), (64900, 176000, 3 * 365, 0.3945),
(64900, 176000, 365, 1.7119), (64900, 176000, 365, 1.7119),

View File

@ -2,13 +2,13 @@ from datetime import datetime, timezone
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest import pytest
from pandas import DataFrame from pandas import DataFrame, Timestamp
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.enums import CandleType, RunMode from freqtrade.enums import CandleType, RunMode
from freqtrade.exceptions import ExchangeError, OperationalException from freqtrade.exceptions import ExchangeError, OperationalException
from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.pairlistmanager import PairListManager
from tests.conftest import get_patched_exchange from tests.conftest import generate_test_data, get_patched_exchange
@pytest.mark.parametrize('candle_type', [ @pytest.mark.parametrize('candle_type', [
@ -144,7 +144,7 @@ def test_available_pairs(mocker, default_conf, ohlcv_history):
assert dp.available_pairs == [("XRP/BTC", timeframe), ("UNITTEST/BTC", timeframe), ] assert dp.available_pairs == [("XRP/BTC", timeframe), ("UNITTEST/BTC", timeframe), ]
def test_producer_pairs(mocker, default_conf, ohlcv_history): def test_producer_pairs(default_conf):
dataprovider = DataProvider(default_conf, None) dataprovider = DataProvider(default_conf, None)
producer = "default" producer = "default"
@ -161,9 +161,9 @@ def test_producer_pairs(mocker, default_conf, ohlcv_history):
assert dataprovider.get_producer_pairs("bad") == [] assert dataprovider.get_producer_pairs("bad") == []
def test_get_producer_df(mocker, default_conf, ohlcv_history): def test_get_producer_df(default_conf):
dataprovider = DataProvider(default_conf, None) dataprovider = DataProvider(default_conf, None)
ohlcv_history = generate_test_data('5m', 150)
pair = 'BTC/USDT' pair = 'BTC/USDT'
timeframe = default_conf['timeframe'] timeframe = default_conf['timeframe']
candle_type = CandleType.SPOT candle_type = CandleType.SPOT
@ -221,7 +221,7 @@ def test_emit_df(mocker, default_conf, ohlcv_history):
assert send_mock.call_count == 0 assert send_mock.call_count == 0
def test_refresh(mocker, default_conf, ohlcv_history): def test_refresh(mocker, default_conf):
refresh_mock = MagicMock() refresh_mock = MagicMock()
mocker.patch("freqtrade.exchange.Exchange.refresh_latest_ohlcv", refresh_mock) mocker.patch("freqtrade.exchange.Exchange.refresh_latest_ohlcv", refresh_mock)
@ -412,3 +412,80 @@ def test_dp_send_msg(default_conf):
dp = DataProvider(default_conf, None) dp = DataProvider(default_conf, None)
dp.send_msg(msg, always_send=True) dp.send_msg(msg, always_send=True)
assert msg not in dp._msg_queue assert msg not in dp._msg_queue
def test_dp__add_external_df(default_conf_usdt):
timeframe = '1h'
default_conf_usdt["timeframe"] = timeframe
dp = DataProvider(default_conf_usdt, None)
df = generate_test_data(timeframe, 24, '2022-01-01 00:00:00+00:00')
last_analyzed = datetime.now(timezone.utc)
res = dp._add_external_df('ETH/USDT', df, last_analyzed, timeframe, CandleType.SPOT)
assert res[0] is False
# Why 1000 ??
assert res[1] == 1000
# Hard add dataframe
dp._replace_external_df('ETH/USDT', df, last_analyzed, timeframe, CandleType.SPOT)
# BTC is not stored yet
res = dp._add_external_df('BTC/USDT', df, last_analyzed, timeframe, CandleType.SPOT)
assert res[0] is False
df_res, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT)
assert len(df_res) == 24
# Add the same dataframe again - dataframe size shall not change.
res = dp._add_external_df('ETH/USDT', df, last_analyzed, timeframe, CandleType.SPOT)
assert res[0] is True
assert res[1] == 0
df, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT)
assert len(df) == 24
# Add a new day.
df2 = generate_test_data(timeframe, 24, '2022-01-02 00:00:00+00:00')
res = dp._add_external_df('ETH/USDT', df2, last_analyzed, timeframe, CandleType.SPOT)
assert res[0] is True
assert res[1] == 0
df, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT)
assert len(df) == 48
# Add a dataframe with a 12 hour offset - so 12 candles are overlapping, and 12 valid.
df3 = generate_test_data(timeframe, 24, '2022-01-02 12:00:00+00:00')
res = dp._add_external_df('ETH/USDT', df3, last_analyzed, timeframe, CandleType.SPOT)
assert res[0] is True
assert res[1] == 0
df, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT)
# New length = 48 + 12 (since we have a 12 hour offset).
assert len(df) == 60
assert df.iloc[-1]['date'] == df3.iloc[-1]['date']
assert df.iloc[-1]['date'] == Timestamp('2022-01-03 11:00:00+00:00')
# Generate 1 new candle
df4 = generate_test_data(timeframe, 1, '2022-01-03 12:00:00+00:00')
res = dp._add_external_df('ETH/USDT', df4, last_analyzed, timeframe, CandleType.SPOT)
# assert res[0] is True
# assert res[1] == 0
df, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT)
# New length = 61 + 1
assert len(df) == 61
assert df.iloc[-2]['date'] == Timestamp('2022-01-03 11:00:00+00:00')
assert df.iloc[-1]['date'] == Timestamp('2022-01-03 12:00:00+00:00')
# Gap in the data ...
df4 = generate_test_data(timeframe, 1, '2022-01-05 00:00:00+00:00')
res = dp._add_external_df('ETH/USDT', df4, last_analyzed, timeframe, CandleType.SPOT)
assert res[0] is False
# 36 hours - from 2022-01-03 12:00:00+00:00 to 2022-01-05 00:00:00+00:00
assert res[1] == 36
df, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT)
# New length = 61 + 1
assert len(df) == 61
# Empty dataframe
df4 = generate_test_data(timeframe, 0, '2022-01-05 00:00:00+00:00')
res = dp._add_external_df('ETH/USDT', df4, last_analyzed, timeframe, CandleType.SPOT)
assert res[0] is False
# 36 hours - from 2022-01-03 12:00:00+00:00 to 2022-01-05 00:00:00+00:00
assert res[1] == 0

View File

@ -23,7 +23,7 @@ from tests.exchange.test_exchange import ccxt_exceptionhandlers
def test_stoploss_order_binance(default_conf, mocker, limitratio, expected, side, trademode): def test_stoploss_order_binance(default_conf, mocker, limitratio, expected, side, trademode):
api_mock = MagicMock() api_mock = MagicMock()
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
order_type = 'stop_loss_limit' if trademode == TradingMode.SPOT else 'limit' order_type = 'stop_loss_limit' if trademode == TradingMode.SPOT else 'stop'
api_mock.create_order = MagicMock(return_value={ api_mock.create_order = MagicMock(return_value={
'id': order_id, 'id': order_id,
@ -557,7 +557,7 @@ async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog, c
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv) exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
pair = 'ETH/BTC' pair = 'ETH/BTC'
respair, restf, restype, res = await exchange._async_get_historic_ohlcv( respair, restf, restype, res, _ = await exchange._async_get_historic_ohlcv(
pair, "5m", 1500000000000, is_new_pair=False, candle_type=candle_type) pair, "5m", 1500000000000, is_new_pair=False, candle_type=candle_type)
assert respair == pair assert respair == pair
assert restf == '5m' assert restf == '5m'
@ -566,7 +566,7 @@ async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog, c
assert exchange._api_async.fetch_ohlcv.call_count > 400 assert exchange._api_async.fetch_ohlcv.call_count > 400
# assert res == ohlcv # assert res == ohlcv
exchange._api_async.fetch_ohlcv.reset_mock() exchange._api_async.fetch_ohlcv.reset_mock()
_, _, _, res = await exchange._async_get_historic_ohlcv( _, _, _, res, _ = await exchange._async_get_historic_ohlcv(
pair, "5m", 1500000000000, is_new_pair=True, candle_type=candle_type) pair, "5m", 1500000000000, is_new_pair=True, candle_type=candle_type)
# Called twice - one "init" call - and one to get the actual data. # Called twice - one "init" call - and one to get the actual data.

View File

@ -8,16 +8,19 @@ suitable to run with freqtrade.
from copy import deepcopy from copy import deepcopy
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
from typing import Tuple
import pytest import pytest
from freqtrade.enums import CandleType from freqtrade.enums import CandleType
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
from freqtrade.exchange.exchange import timeframe_to_msecs from freqtrade.exchange.exchange import Exchange, timeframe_to_msecs
from freqtrade.resolvers.exchange_resolver import ExchangeResolver from freqtrade.resolvers.exchange_resolver import ExchangeResolver
from tests.conftest import get_default_conf_usdt from tests.conftest import get_default_conf_usdt
EXCHANGE_FIXTURE_TYPE = Tuple[Exchange, str]
# Exchanges that should be tested # Exchanges that should be tested
EXCHANGES = { EXCHANGES = {
'bittrex': { 'bittrex': {
@ -74,8 +77,8 @@ EXCHANGES = {
'leverage_in_spot_market': True, 'leverage_in_spot_market': True,
}, },
'huobi': { 'huobi': {
'pair': 'BTC/USDT', 'pair': 'ETH/BTC',
'stake_currency': 'USDT', 'stake_currency': 'BTC',
'hasQuoteVolume': True, 'hasQuoteVolume': True,
'timeframe': '5m', 'timeframe': '5m',
'futures': False, 'futures': False,
@ -141,19 +144,19 @@ def exchange_futures(request, exchange_conf, class_mocker):
@pytest.mark.longrun @pytest.mark.longrun
class TestCCXTExchange(): class TestCCXTExchange():
def test_load_markets(self, exchange): def test_load_markets(self, exchange: EXCHANGE_FIXTURE_TYPE):
exchange, exchangename = exchange exch, exchangename = exchange
pair = EXCHANGES[exchangename]['pair'] pair = EXCHANGES[exchangename]['pair']
markets = exchange.markets markets = exch.markets
assert pair in markets assert pair in markets
assert isinstance(markets[pair], dict) assert isinstance(markets[pair], dict)
assert exchange.market_is_spot(markets[pair]) assert exch.market_is_spot(markets[pair])
def test_has_validations(self, exchange): def test_has_validations(self, exchange: EXCHANGE_FIXTURE_TYPE):
exchange, exchangename = exchange exch, exchangename = exchange
exchange.validate_ordertypes({ exch.validate_ordertypes({
'entry': 'limit', 'entry': 'limit',
'exit': 'limit', 'exit': 'limit',
'stoploss': 'limit', 'stoploss': 'limit',
@ -162,13 +165,13 @@ class TestCCXTExchange():
if exchangename == 'gateio': if exchangename == 'gateio':
# gateio doesn't have market orders on spot # gateio doesn't have market orders on spot
return return
exchange.validate_ordertypes({ exch.validate_ordertypes({
'entry': 'market', 'entry': 'market',
'exit': 'market', 'exit': 'market',
'stoploss': 'market', 'stoploss': 'market',
}) })
def test_load_markets_futures(self, exchange_futures): def test_load_markets_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
exchange, exchangename = exchange_futures exchange, exchangename = exchange_futures
if not exchange: if not exchange:
# exchange_futures only returns values for supported exchanges # exchange_futures only returns values for supported exchanges
@ -181,11 +184,11 @@ class TestCCXTExchange():
assert exchange.market_is_future(markets[pair]) assert exchange.market_is_future(markets[pair])
def test_ccxt_fetch_tickers(self, exchange): def test_ccxt_fetch_tickers(self, exchange: EXCHANGE_FIXTURE_TYPE):
exchange, exchangename = exchange exch, exchangename = exchange
pair = EXCHANGES[exchangename]['pair'] pair = EXCHANGES[exchangename]['pair']
tickers = exchange.get_tickers() tickers = exch.get_tickers()
assert pair in tickers assert pair in tickers
assert 'ask' in tickers[pair] assert 'ask' in tickers[pair]
assert tickers[pair]['ask'] is not None assert tickers[pair]['ask'] is not None
@ -195,11 +198,11 @@ class TestCCXTExchange():
if EXCHANGES[exchangename].get('hasQuoteVolume'): if EXCHANGES[exchangename].get('hasQuoteVolume'):
assert tickers[pair]['quoteVolume'] is not None assert tickers[pair]['quoteVolume'] is not None
def test_ccxt_fetch_ticker(self, exchange): def test_ccxt_fetch_ticker(self, exchange: EXCHANGE_FIXTURE_TYPE):
exchange, exchangename = exchange exch, exchangename = exchange
pair = EXCHANGES[exchangename]['pair'] pair = EXCHANGES[exchangename]['pair']
ticker = exchange.fetch_ticker(pair) ticker = exch.fetch_ticker(pair)
assert 'ask' in ticker assert 'ask' in ticker
assert ticker['ask'] is not None assert ticker['ask'] is not None
assert 'bid' in ticker assert 'bid' in ticker
@ -208,26 +211,31 @@ class TestCCXTExchange():
if EXCHANGES[exchangename].get('hasQuoteVolume'): if EXCHANGES[exchangename].get('hasQuoteVolume'):
assert ticker['quoteVolume'] is not None assert ticker['quoteVolume'] is not None
def test_ccxt_fetch_l2_orderbook(self, exchange): def test_ccxt_fetch_l2_orderbook(self, exchange: EXCHANGE_FIXTURE_TYPE):
exchange, exchangename = exchange exch, exchangename = exchange
pair = EXCHANGES[exchangename]['pair'] pair = EXCHANGES[exchangename]['pair']
l2 = exchange.fetch_l2_order_book(pair) l2 = exch.fetch_l2_order_book(pair)
assert 'asks' in l2 assert 'asks' in l2
assert 'bids' in l2 assert 'bids' in l2
assert len(l2['asks']) >= 1 assert len(l2['asks']) >= 1
assert len(l2['bids']) >= 1 assert len(l2['bids']) >= 1
l2_limit_range = exchange._ft_has['l2_limit_range'] l2_limit_range = exch._ft_has['l2_limit_range']
l2_limit_range_required = exchange._ft_has['l2_limit_range_required'] l2_limit_range_required = exch._ft_has['l2_limit_range_required']
if exchangename == 'gateio': if exchangename == 'gateio':
# TODO: Gateio is unstable here at the moment, ignoring the limit partially. # TODO: Gateio is unstable here at the moment, ignoring the limit partially.
return return
for val in [1, 2, 5, 25, 100]: for val in [1, 2, 5, 25, 100]:
l2 = exchange.fetch_l2_order_book(pair, val) l2 = exch.fetch_l2_order_book(pair, val)
if not l2_limit_range or val in l2_limit_range: if not l2_limit_range or val in l2_limit_range:
assert len(l2['asks']) == val if val > 50:
assert len(l2['bids']) == val # Orderbooks are not always this deep.
assert val - 5 < len(l2['asks']) <= val
assert val - 5 < len(l2['bids']) <= val
else:
assert len(l2['asks']) == val
assert len(l2['bids']) == val
else: else:
next_limit = exchange.get_next_limit_in_list( next_limit = exch.get_next_limit_in_list(
val, l2_limit_range, l2_limit_range_required) val, l2_limit_range, l2_limit_range_required)
if next_limit is None: if next_limit is None:
assert len(l2['asks']) > 100 assert len(l2['asks']) > 100
@ -240,23 +248,23 @@ class TestCCXTExchange():
assert len(l2['asks']) == next_limit assert len(l2['asks']) == next_limit
assert len(l2['asks']) == next_limit assert len(l2['asks']) == next_limit
def test_ccxt_fetch_ohlcv(self, exchange): def test_ccxt_fetch_ohlcv(self, exchange: EXCHANGE_FIXTURE_TYPE):
exchange, exchangename = exchange exch, exchangename = exchange
pair = EXCHANGES[exchangename]['pair'] pair = EXCHANGES[exchangename]['pair']
timeframe = EXCHANGES[exchangename]['timeframe'] timeframe = EXCHANGES[exchangename]['timeframe']
pair_tf = (pair, timeframe, CandleType.SPOT) pair_tf = (pair, timeframe, CandleType.SPOT)
ohlcv = exchange.refresh_latest_ohlcv([pair_tf]) ohlcv = exch.refresh_latest_ohlcv([pair_tf])
assert isinstance(ohlcv, dict) assert isinstance(ohlcv, dict)
assert len(ohlcv[pair_tf]) == len(exchange.klines(pair_tf)) assert len(ohlcv[pair_tf]) == len(exch.klines(pair_tf))
# assert len(exchange.klines(pair_tf)) > 200 # assert len(exch.klines(pair_tf)) > 200
# Assume 90% uptime ... # Assume 90% uptime ...
assert len(exchange.klines(pair_tf)) > exchange.ohlcv_candle_limit( assert len(exch.klines(pair_tf)) > exch.ohlcv_candle_limit(
timeframe, CandleType.SPOT) * 0.90 timeframe, CandleType.SPOT) * 0.90
# Check if last-timeframe is within the last 2 intervals # Check if last-timeframe is within the last 2 intervals
now = datetime.now(timezone.utc) - timedelta(minutes=(timeframe_to_minutes(timeframe) * 2)) now = datetime.now(timezone.utc) - timedelta(minutes=(timeframe_to_minutes(timeframe) * 2))
assert exchange.klines(pair_tf).iloc[-1]['date'] >= timeframe_to_prev_date(timeframe, now) assert exch.klines(pair_tf).iloc[-1]['date'] >= timeframe_to_prev_date(timeframe, now)
def ccxt__async_get_candle_history(self, exchange, exchangename, pair, timeframe, candle_type): def ccxt__async_get_candle_history(self, exchange, exchangename, pair, timeframe, candle_type):
@ -284,17 +292,17 @@ class TestCCXTExchange():
assert len(candles) >= min(candle_count, candle_count1) assert len(candles) >= min(candle_count, candle_count1)
assert candles[0][0] == since_ms or (since_ms + timeframe_ms) assert candles[0][0] == since_ms or (since_ms + timeframe_ms)
def test_ccxt__async_get_candle_history(self, exchange): def test_ccxt__async_get_candle_history(self, exchange: EXCHANGE_FIXTURE_TYPE):
exchange, exchangename = exchange exc, exchangename = exchange
# For some weired reason, this test returns random lengths for bittrex. # For some weired reason, this test returns random lengths for bittrex.
if not exchange._ft_has['ohlcv_has_history'] or exchangename in ('bittrex'): if not exc._ft_has['ohlcv_has_history'] or exchangename in ('bittrex'):
return return
pair = EXCHANGES[exchangename]['pair'] pair = EXCHANGES[exchangename]['pair']
timeframe = EXCHANGES[exchangename]['timeframe'] timeframe = EXCHANGES[exchangename]['timeframe']
self.ccxt__async_get_candle_history( self.ccxt__async_get_candle_history(
exchange, exchangename, pair, timeframe, CandleType.SPOT) exc, exchangename, pair, timeframe, CandleType.SPOT)
def test_ccxt__async_get_candle_history_futures(self, exchange_futures): def test_ccxt__async_get_candle_history_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
exchange, exchangename = exchange_futures exchange, exchangename = exchange_futures
if not exchange: if not exchange:
# exchange_futures only returns values for supported exchanges # exchange_futures only returns values for supported exchanges
@ -304,7 +312,7 @@ class TestCCXTExchange():
self.ccxt__async_get_candle_history( self.ccxt__async_get_candle_history(
exchange, exchangename, pair, timeframe, CandleType.FUTURES) exchange, exchangename, pair, timeframe, CandleType.FUTURES)
def test_ccxt_fetch_funding_rate_history(self, exchange_futures): def test_ccxt_fetch_funding_rate_history(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
exchange, exchangename = exchange_futures exchange, exchangename = exchange_futures
if not exchange: if not exchange:
# exchange_futures only returns values for supported exchanges # exchange_futures only returns values for supported exchanges
@ -342,7 +350,7 @@ class TestCCXTExchange():
(rate['open'].min() != rate['open'].max()) (rate['open'].min() != rate['open'].max())
) )
def test_ccxt_fetch_mark_price_history(self, exchange_futures): def test_ccxt_fetch_mark_price_history(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
exchange, exchangename = exchange_futures exchange, exchangename = exchange_futures
if not exchange: if not exchange:
# exchange_futures only returns values for supported exchanges # exchange_futures only returns values for supported exchanges
@ -366,7 +374,7 @@ class TestCCXTExchange():
assert mark_candles[mark_candles['date'] == prev_hour].iloc[0]['open'] != 0.0 assert mark_candles[mark_candles['date'] == prev_hour].iloc[0]['open'] != 0.0
assert mark_candles[mark_candles['date'] == this_hour].iloc[0]['open'] != 0.0 assert mark_candles[mark_candles['date'] == this_hour].iloc[0]['open'] != 0.0
def test_ccxt__calculate_funding_fees(self, exchange_futures): def test_ccxt__calculate_funding_fees(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
exchange, exchangename = exchange_futures exchange, exchangename = exchange_futures
if not exchange: if not exchange:
# exchange_futures only returns values for supported exchanges # exchange_futures only returns values for supported exchanges
@ -382,16 +390,16 @@ class TestCCXTExchange():
# TODO: tests fetch_trades (?) # TODO: tests fetch_trades (?)
def test_ccxt_get_fee(self, exchange): def test_ccxt_get_fee(self, exchange: EXCHANGE_FIXTURE_TYPE):
exchange, exchangename = exchange exch, exchangename = exchange
pair = EXCHANGES[exchangename]['pair'] pair = EXCHANGES[exchangename]['pair']
threshold = 0.01 threshold = 0.01
assert 0 < exchange.get_fee(pair, 'limit', 'buy') < threshold assert 0 < exch.get_fee(pair, 'limit', 'buy') < threshold
assert 0 < exchange.get_fee(pair, 'limit', 'sell') < threshold assert 0 < exch.get_fee(pair, 'limit', 'sell') < threshold
assert 0 < exchange.get_fee(pair, 'market', 'buy') < threshold assert 0 < exch.get_fee(pair, 'market', 'buy') < threshold
assert 0 < exchange.get_fee(pair, 'market', 'sell') < threshold assert 0 < exch.get_fee(pair, 'market', 'sell') < threshold
def test_ccxt_get_max_leverage_spot(self, exchange): def test_ccxt_get_max_leverage_spot(self, exchange: EXCHANGE_FIXTURE_TYPE):
spot, spot_name = exchange spot, spot_name = exchange
if spot: if spot:
leverage_in_market_spot = EXCHANGES[spot_name].get('leverage_in_spot_market') leverage_in_market_spot = EXCHANGES[spot_name].get('leverage_in_spot_market')
@ -401,7 +409,7 @@ class TestCCXTExchange():
assert (isinstance(spot_leverage, float) or isinstance(spot_leverage, int)) assert (isinstance(spot_leverage, float) or isinstance(spot_leverage, int))
assert spot_leverage >= 1.0 assert spot_leverage >= 1.0
def test_ccxt_get_max_leverage_futures(self, exchange_futures): def test_ccxt_get_max_leverage_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
futures, futures_name = exchange_futures futures, futures_name = exchange_futures
if futures: if futures:
leverage_tiers_public = EXCHANGES[futures_name].get('leverage_tiers_public') leverage_tiers_public = EXCHANGES[futures_name].get('leverage_tiers_public')
@ -414,7 +422,7 @@ class TestCCXTExchange():
assert (isinstance(futures_leverage, float) or isinstance(futures_leverage, int)) assert (isinstance(futures_leverage, float) or isinstance(futures_leverage, int))
assert futures_leverage >= 1.0 assert futures_leverage >= 1.0
def test_ccxt_get_contract_size(self, exchange_futures): def test_ccxt_get_contract_size(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
futures, futures_name = exchange_futures futures, futures_name = exchange_futures
if futures: if futures:
futures_pair = EXCHANGES[futures_name].get( futures_pair = EXCHANGES[futures_name].get(
@ -425,7 +433,7 @@ class TestCCXTExchange():
assert (isinstance(contract_size, float) or isinstance(contract_size, int)) assert (isinstance(contract_size, float) or isinstance(contract_size, int))
assert contract_size >= 0.0 assert contract_size >= 0.0
def test_ccxt_load_leverage_tiers(self, exchange_futures): def test_ccxt_load_leverage_tiers(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
futures, futures_name = exchange_futures futures, futures_name = exchange_futures
if futures and EXCHANGES[futures_name].get('leverage_tiers_public'): if futures and EXCHANGES[futures_name].get('leverage_tiers_public'):
leverage_tiers = futures.load_leverage_tiers() leverage_tiers = futures.load_leverage_tiers()
@ -458,7 +466,7 @@ class TestCCXTExchange():
oldminNotional = tier['minNotional'] oldminNotional = tier['minNotional']
oldmaxNotional = tier['maxNotional'] oldmaxNotional = tier['maxNotional']
def test_ccxt_dry_run_liquidation_price(self, exchange_futures): def test_ccxt_dry_run_liquidation_price(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
futures, futures_name = exchange_futures futures, futures_name = exchange_futures
if futures and EXCHANGES[futures_name].get('leverage_tiers_public'): if futures and EXCHANGES[futures_name].get('leverage_tiers_public'):
@ -489,7 +497,7 @@ class TestCCXTExchange():
assert (isinstance(liquidation_price, float)) assert (isinstance(liquidation_price, float))
assert liquidation_price >= 0.0 assert liquidation_price >= 0.0
def test_ccxt_get_max_pair_stake_amount(self, exchange_futures): def test_ccxt_get_max_pair_stake_amount(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
futures, futures_name = exchange_futures futures, futures_name = exchange_futures
if futures: if futures:
futures_pair = EXCHANGES[futures_name].get( futures_pair = EXCHANGES[futures_name].get(

View File

@ -1955,7 +1955,7 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name, candle_
pair = 'ETH/BTC' pair = 'ETH/BTC'
async def mock_candle_hist(pair, timeframe, candle_type, since_ms): async def mock_candle_hist(pair, timeframe, candle_type, since_ms):
return pair, timeframe, candle_type, ohlcv return pair, timeframe, candle_type, ohlcv, True
exchange._async_get_candle_history = Mock(wraps=mock_candle_hist) exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
# one_call calculation * 1.8 should do 2 calls # one_call calculation * 1.8 should do 2 calls
@ -1988,62 +1988,6 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name, candle_
assert log_has_re(r"Async code raised an exception: .*", caplog) assert log_has_re(r"Async code raised an exception: .*", caplog)
@pytest.mark.parametrize("exchange_name", EXCHANGES)
@pytest.mark.parametrize('candle_type', ['mark', ''])
def test_get_historic_ohlcv_as_df(default_conf, mocker, exchange_name, candle_type):
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
ohlcv = [
[
arrow.utcnow().int_timestamp * 1000, # unix timestamp ms
1, # open
2, # high
3, # low
4, # close
5, # volume (in quote currency)
],
[
arrow.utcnow().shift(minutes=5).int_timestamp * 1000, # unix timestamp ms
1, # open
2, # high
3, # low
4, # close
5, # volume (in quote currency)
],
[
arrow.utcnow().shift(minutes=10).int_timestamp * 1000, # unix timestamp ms
1, # open
2, # high
3, # low
4, # close
5, # volume (in quote currency)
]
]
pair = 'ETH/BTC'
async def mock_candle_hist(pair, timeframe, candle_type, since_ms):
return pair, timeframe, candle_type, ohlcv
exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
# one_call calculation * 1.8 should do 2 calls
since = 5 * 60 * exchange.ohlcv_candle_limit('5m', CandleType.SPOT) * 1.8
ret = exchange.get_historic_ohlcv_as_df(
pair,
"5m",
int((arrow.utcnow().int_timestamp - since) * 1000),
candle_type=candle_type
)
assert exchange._async_get_candle_history.call_count == 2
# Returns twice the above OHLCV data
assert len(ret) == 2
assert isinstance(ret, DataFrame)
assert 'date' in ret.columns
assert 'open' in ret.columns
assert 'close' in ret.columns
assert 'high' in ret.columns
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("exchange_name", EXCHANGES)
@pytest.mark.parametrize('candle_type', [CandleType.MARK, CandleType.SPOT]) @pytest.mark.parametrize('candle_type', [CandleType.MARK, CandleType.SPOT])
@ -2063,7 +2007,7 @@ async def test__async_get_historic_ohlcv(default_conf, mocker, caplog, exchange_
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv) exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
pair = 'ETH/USDT' pair = 'ETH/USDT'
respair, restf, _, res = await exchange._async_get_historic_ohlcv( respair, restf, _, res, _ = await exchange._async_get_historic_ohlcv(
pair, "5m", 1500000000000, candle_type=candle_type, is_new_pair=False) pair, "5m", 1500000000000, candle_type=candle_type, is_new_pair=False)
assert respair == pair assert respair == pair
assert restf == '5m' assert restf == '5m'
@ -2074,7 +2018,7 @@ async def test__async_get_historic_ohlcv(default_conf, mocker, caplog, exchange_
exchange._api_async.fetch_ohlcv.reset_mock() exchange._api_async.fetch_ohlcv.reset_mock()
end_ts = 1_500_500_000_000 end_ts = 1_500_500_000_000
start_ts = 1_500_000_000_000 start_ts = 1_500_000_000_000
respair, restf, _, res = await exchange._async_get_historic_ohlcv( respair, restf, _, res, _ = await exchange._async_get_historic_ohlcv(
pair, "5m", since_ms=start_ts, candle_type=candle_type, is_new_pair=False, pair, "5m", since_ms=start_ts, candle_type=candle_type, is_new_pair=False,
until_ms=end_ts until_ms=end_ts
) )
@ -2306,7 +2250,7 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_
pair = 'ETH/BTC' pair = 'ETH/BTC'
res = await exchange._async_get_candle_history(pair, "5m", CandleType.SPOT) res = await exchange._async_get_candle_history(pair, "5m", CandleType.SPOT)
assert type(res) is tuple assert type(res) is tuple
assert len(res) == 4 assert len(res) == 5
assert res[0] == pair assert res[0] == pair
assert res[1] == "5m" assert res[1] == "5m"
assert res[2] == CandleType.SPOT assert res[2] == CandleType.SPOT
@ -2393,7 +2337,7 @@ async def test__async_get_candle_history_empty(default_conf, mocker, caplog):
pair = 'ETH/BTC' pair = 'ETH/BTC'
res = await exchange._async_get_candle_history(pair, "5m", CandleType.SPOT) res = await exchange._async_get_candle_history(pair, "5m", CandleType.SPOT)
assert type(res) is tuple assert type(res) is tuple
assert len(res) == 4 assert len(res) == 5
assert res[0] == pair assert res[0] == pair
assert res[1] == "5m" assert res[1] == "5m"
assert res[2] == CandleType.SPOT assert res[2] == CandleType.SPOT
@ -4014,9 +3958,6 @@ def test_validate_trading_mode_and_margin_mode(
("binance", "spot", {}), ("binance", "spot", {}),
("binance", "margin", {"options": {"defaultType": "margin"}}), ("binance", "margin", {"options": {"defaultType": "margin"}}),
("binance", "futures", {"options": {"defaultType": "future"}}), ("binance", "futures", {"options": {"defaultType": "future"}}),
("bibox", "spot", {"has": {"fetchCurrencies": False}}),
("bibox", "margin", {"has": {"fetchCurrencies": False}, "options": {"defaultType": "margin"}}),
("bibox", "futures", {"has": {"fetchCurrencies": False}, "options": {"defaultType": "swap"}}),
("bybit", "spot", {"options": {"defaultType": "spot"}}), ("bybit", "spot", {"options": {"defaultType": "spot"}}),
("bybit", "futures", {"options": {"defaultType": "linear"}}), ("bybit", "futures", {"options": {"defaultType": "linear"}}),
("gateio", "futures", {"options": {"defaultType": "swap"}}), ("gateio", "futures", {"options": {"defaultType": "swap"}}),

View File

@ -82,7 +82,7 @@ def test_compute_distances(mocker, freqai_conf):
freqai = make_data_dictionary(mocker, freqai_conf) freqai = make_data_dictionary(mocker, freqai_conf)
freqai_conf['freqai']['feature_parameters'].update({"DI_threshold": 1}) freqai_conf['freqai']['feature_parameters'].update({"DI_threshold": 1})
avg_mean_dist = freqai.dk.compute_distances() avg_mean_dist = freqai.dk.compute_distances()
assert round(avg_mean_dist, 2) == 1.99 assert round(avg_mean_dist, 2) == 1.98
def test_use_SVM_to_remove_outliers_and_outlier_protection(mocker, freqai_conf, caplog): def test_use_SVM_to_remove_outliers_and_outlier_protection(mocker, freqai_conf, caplog):
@ -90,7 +90,7 @@ def test_use_SVM_to_remove_outliers_and_outlier_protection(mocker, freqai_conf,
freqai_conf['freqai']['feature_parameters'].update({"outlier_protection_percentage": 0.1}) freqai_conf['freqai']['feature_parameters'].update({"outlier_protection_percentage": 0.1})
freqai.dk.use_SVM_to_remove_outliers(predict=False) freqai.dk.use_SVM_to_remove_outliers(predict=False)
assert log_has_re( assert log_has_re(
"SVM detected 7.36%", "SVM detected 7.83%",
caplog, caplog,
) )

View File

@ -27,20 +27,23 @@ def is_mac() -> bool:
return "Darwin" in machine return "Darwin" in machine
@pytest.mark.parametrize('model, pca, dbscan, float32', [ @pytest.mark.parametrize('model, pca, dbscan, float32, can_short', [
('LightGBMRegressor', True, False, True), ('LightGBMRegressor', True, False, True, True),
('XGBoostRegressor', False, True, False), ('XGBoostRegressor', False, True, False, True),
('XGBoostRFRegressor', False, False, False), ('XGBoostRFRegressor', False, False, False, True),
('CatboostRegressor', False, False, False), ('CatboostRegressor', False, False, False, True),
('ReinforcementLearner', False, True, False), ('ReinforcementLearner', False, True, False, True),
('ReinforcementLearner_multiproc', False, False, False), ('ReinforcementLearner_multiproc', False, False, False, True),
('ReinforcementLearner_test_4ac', False, False, False) ('ReinforcementLearner_test_3ac', False, False, False, False),
('ReinforcementLearner_test_3ac', False, False, False, True),
('ReinforcementLearner_test_4ac', False, False, False, True)
]) ])
def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, dbscan, float32): def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca,
dbscan, float32, can_short):
if is_arm() and model == 'CatboostRegressor': if is_arm() and model == 'CatboostRegressor':
pytest.skip("CatBoost is not supported on ARM") pytest.skip("CatBoost is not supported on ARM")
if is_mac() and 'Reinforcement' in model: if is_mac() and not is_arm() and 'Reinforcement' in model:
pytest.skip("Reinforcement learning module not available on intel based Mac OS") pytest.skip("Reinforcement learning module not available on intel based Mac OS")
model_save_ext = 'joblib' model_save_ext = 'joblib'
@ -58,9 +61,6 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca,
freqai_conf['freqai']['feature_parameters'].update({"use_SVM_to_remove_outliers": True}) freqai_conf['freqai']['feature_parameters'].update({"use_SVM_to_remove_outliers": True})
freqai_conf['freqai']['data_split_parameters'].update({'shuffle': True}) freqai_conf['freqai']['data_split_parameters'].update({'shuffle': True})
if 'test_4ac' in model:
freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models")
if 'ReinforcementLearner' in model: if 'ReinforcementLearner' in model:
model_save_ext = 'zip' model_save_ext = 'zip'
freqai_conf = make_rl_config(freqai_conf) freqai_conf = make_rl_config(freqai_conf)
@ -68,7 +68,7 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca,
freqai_conf['freqai']['feature_parameters'].update({"use_SVM_to_remove_outliers": True}) freqai_conf['freqai']['feature_parameters'].update({"use_SVM_to_remove_outliers": True})
freqai_conf['freqai']['data_split_parameters'].update({'shuffle': True}) freqai_conf['freqai']['data_split_parameters'].update({'shuffle': True})
if 'test_4ac' in model: if 'test_3ac' in model or 'test_4ac' in model:
freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models") freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models")
strategy = get_patched_freqai_strategy(mocker, freqai_conf) strategy = get_patched_freqai_strategy(mocker, freqai_conf)
@ -77,6 +77,7 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca,
strategy.freqai_info = freqai_conf.get("freqai", {}) strategy.freqai_info = freqai_conf.get("freqai", {})
freqai = strategy.freqai freqai = strategy.freqai
freqai.live = True freqai.live = True
freqai.can_short = can_short
freqai.dk = FreqaiDataKitchen(freqai_conf) freqai.dk = FreqaiDataKitchen(freqai_conf)
freqai.dk.set_paths('ADA/BTC', 10000) freqai.dk.set_paths('ADA/BTC', 10000)
timerange = TimeRange.parse_timerange("20180110-20180130") timerange = TimeRange.parse_timerange("20180110-20180130")
@ -221,6 +222,9 @@ def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog)
if 'test_4ac' in model: if 'test_4ac' in model:
freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models") freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models")
freqai_conf.get("freqai", {}).get("feature_parameters", {}).update(
{"indicator_periods_candles": [2]})
strategy = get_patched_freqai_strategy(mocker, freqai_conf) strategy = get_patched_freqai_strategy(mocker, freqai_conf)
exchange = get_patched_exchange(mocker, freqai_conf) exchange = get_patched_exchange(mocker, freqai_conf)
strategy.dp = DataProvider(freqai_conf, exchange) strategy.dp = DataProvider(freqai_conf, exchange)
@ -231,16 +235,14 @@ def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog)
timerange = TimeRange.parse_timerange("20180110-20180130") timerange = TimeRange.parse_timerange("20180110-20180130")
freqai.dd.load_all_pair_histories(timerange, freqai.dk) freqai.dd.load_all_pair_histories(timerange, freqai.dk)
sub_timerange = TimeRange.parse_timerange("20180110-20180130") sub_timerange = TimeRange.parse_timerange("20180110-20180130")
corr_df, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk) _, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk)
df = base_df[freqai_conf["timeframe"]]
df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, "LTC/BTC")
df = freqai.cache_corr_pairlist_dfs(df, freqai.dk)
for i in range(5): for i in range(5):
df[f'%-constant_{i}'] = i df[f'%-constant_{i}'] = i
# df.loc[:, f'%-constant_{i}'] = i
metadata = {"pair": "LTC/BTC"} metadata = {"pair": "LTC/BTC"}
freqai.start_backtesting(df, metadata, freqai.dk) freqai.start_backtesting(df, metadata, freqai.dk, strategy)
model_folders = [x for x in freqai.dd.full_path.iterdir() if x.is_dir()] model_folders = [x for x in freqai.dd.full_path.iterdir() if x.is_dir()]
assert len(model_folders) == num_files assert len(model_folders) == num_files
@ -261,6 +263,8 @@ def test_start_backtesting_subdaily_backtest_period(mocker, freqai_conf):
freqai_conf.update({"timerange": "20180120-20180124"}) freqai_conf.update({"timerange": "20180120-20180124"})
freqai_conf.get("freqai", {}).update({"backtest_period_days": 0.5}) freqai_conf.get("freqai", {}).update({"backtest_period_days": 0.5})
freqai_conf.get("freqai", {}).update({"save_backtest_models": True}) freqai_conf.get("freqai", {}).update({"save_backtest_models": True})
freqai_conf.get("freqai", {}).get("feature_parameters", {}).update(
{"indicator_periods_candles": [2]})
strategy = get_patched_freqai_strategy(mocker, freqai_conf) strategy = get_patched_freqai_strategy(mocker, freqai_conf)
exchange = get_patched_exchange(mocker, freqai_conf) exchange = get_patched_exchange(mocker, freqai_conf)
strategy.dp = DataProvider(freqai_conf, exchange) strategy.dp = DataProvider(freqai_conf, exchange)
@ -271,12 +275,11 @@ def test_start_backtesting_subdaily_backtest_period(mocker, freqai_conf):
timerange = TimeRange.parse_timerange("20180110-20180130") timerange = TimeRange.parse_timerange("20180110-20180130")
freqai.dd.load_all_pair_histories(timerange, freqai.dk) freqai.dd.load_all_pair_histories(timerange, freqai.dk)
sub_timerange = TimeRange.parse_timerange("20180110-20180130") sub_timerange = TimeRange.parse_timerange("20180110-20180130")
corr_df, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk) _, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk)
df = base_df[freqai_conf["timeframe"]]
df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, "LTC/BTC")
metadata = {"pair": "LTC/BTC"} metadata = {"pair": "LTC/BTC"}
freqai.start_backtesting(df, metadata, freqai.dk) freqai.start_backtesting(df, metadata, freqai.dk, strategy)
model_folders = [x for x in freqai.dd.full_path.iterdir() if x.is_dir()] model_folders = [x for x in freqai.dd.full_path.iterdir() if x.is_dir()]
assert len(model_folders) == 9 assert len(model_folders) == 9
@ -287,6 +290,8 @@ def test_start_backtesting_subdaily_backtest_period(mocker, freqai_conf):
def test_start_backtesting_from_existing_folder(mocker, freqai_conf, caplog): def test_start_backtesting_from_existing_folder(mocker, freqai_conf, caplog):
freqai_conf.update({"timerange": "20180120-20180130"}) freqai_conf.update({"timerange": "20180120-20180130"})
freqai_conf.get("freqai", {}).update({"save_backtest_models": True}) freqai_conf.get("freqai", {}).update({"save_backtest_models": True})
freqai_conf.get("freqai", {}).get("feature_parameters", {}).update(
{"indicator_periods_candles": [2]})
strategy = get_patched_freqai_strategy(mocker, freqai_conf) strategy = get_patched_freqai_strategy(mocker, freqai_conf)
exchange = get_patched_exchange(mocker, freqai_conf) exchange = get_patched_exchange(mocker, freqai_conf)
strategy.dp = DataProvider(freqai_conf, exchange) strategy.dp = DataProvider(freqai_conf, exchange)
@ -296,15 +301,14 @@ def test_start_backtesting_from_existing_folder(mocker, freqai_conf, caplog):
freqai.dk = FreqaiDataKitchen(freqai_conf) freqai.dk = FreqaiDataKitchen(freqai_conf)
timerange = TimeRange.parse_timerange("20180110-20180130") timerange = TimeRange.parse_timerange("20180110-20180130")
freqai.dd.load_all_pair_histories(timerange, freqai.dk) freqai.dd.load_all_pair_histories(timerange, freqai.dk)
sub_timerange = TimeRange.parse_timerange("20180110-20180130") sub_timerange = TimeRange.parse_timerange("20180101-20180130")
corr_df, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk) _, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk)
df = base_df[freqai_conf["timeframe"]]
df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, "LTC/BTC")
pair = "ADA/BTC" pair = "ADA/BTC"
metadata = {"pair": pair} metadata = {"pair": pair}
freqai.dk.pair = pair freqai.dk.pair = pair
freqai.start_backtesting(df, metadata, freqai.dk) freqai.start_backtesting(df, metadata, freqai.dk, strategy)
model_folders = [x for x in freqai.dd.full_path.iterdir() if x.is_dir()] model_folders = [x for x in freqai.dd.full_path.iterdir() if x.is_dir()]
assert len(model_folders) == 2 assert len(model_folders) == 2
@ -322,14 +326,13 @@ def test_start_backtesting_from_existing_folder(mocker, freqai_conf, caplog):
timerange = TimeRange.parse_timerange("20180110-20180130") timerange = TimeRange.parse_timerange("20180110-20180130")
freqai.dd.load_all_pair_histories(timerange, freqai.dk) freqai.dd.load_all_pair_histories(timerange, freqai.dk)
sub_timerange = TimeRange.parse_timerange("20180110-20180130") sub_timerange = TimeRange.parse_timerange("20180110-20180130")
corr_df, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk) _, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk)
df = base_df[freqai_conf["timeframe"]]
df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, "LTC/BTC")
pair = "ADA/BTC" pair = "ADA/BTC"
metadata = {"pair": pair} metadata = {"pair": pair}
freqai.dk.pair = pair freqai.dk.pair = pair
freqai.start_backtesting(df, metadata, freqai.dk) freqai.start_backtesting(df, metadata, freqai.dk, strategy)
assert log_has_re( assert log_has_re(
"Found backtesting prediction file ", "Found backtesting prediction file ",
@ -339,7 +342,7 @@ def test_start_backtesting_from_existing_folder(mocker, freqai_conf, caplog):
pair = "ETH/BTC" pair = "ETH/BTC"
metadata = {"pair": pair} metadata = {"pair": pair}
freqai.dk.pair = pair freqai.dk.pair = pair
freqai.start_backtesting(df, metadata, freqai.dk) freqai.start_backtesting(df, metadata, freqai.dk, strategy)
path = (freqai.dd.full_path / freqai.dk.backtest_predictions_folder) path = (freqai.dd.full_path / freqai.dk.backtest_predictions_folder)
prediction_files = [x for x in path.iterdir() if x.is_file()] prediction_files = [x for x in path.iterdir() if x.is_file()]

View File

@ -0,0 +1,65 @@
import logging
import numpy as np
from freqtrade.freqai.prediction_models.ReinforcementLearner import ReinforcementLearner
from freqtrade.freqai.RL.Base3ActionRLEnv import Actions, Base3ActionRLEnv, Positions
logger = logging.getLogger(__name__)
class ReinforcementLearner_test_3ac(ReinforcementLearner):
"""
User created Reinforcement Learning Model prediction model.
"""
class MyRLEnv(Base3ActionRLEnv):
"""
User can override any function in BaseRLEnv and gym.Env. Here the user
sets a custom reward based on profit and trade duration.
"""
def calculate_reward(self, action: int) -> float:
# first, penalize if the action is not valid
if not self._is_valid(action):
return -2
pnl = self.get_unrealized_profit()
rew = np.sign(pnl) * (pnl + 1)
factor = 100.
# reward agent for entering trades
if (action in (Actions.Buy.value, Actions.Sell.value)
and self._position == Positions.Neutral):
return 25
# discourage agent from not entering trades
if action == Actions.Neutral.value and self._position == Positions.Neutral:
return -1
max_trade_duration = self.rl_config.get('max_trade_duration_candles', 300)
trade_duration = self._current_tick - self._last_trade_tick # type: ignore
if trade_duration <= max_trade_duration:
factor *= 1.5
elif trade_duration > max_trade_duration:
factor *= 0.5
# discourage sitting in position
if self._position in (Positions.Short, Positions.Long) and (
action == Actions.Neutral.value
or (action == Actions.Sell.value and self._position == Positions.Short)
or (action == Actions.Buy.value and self._position == Positions.Long)
):
return -1 * trade_duration / max_trade_duration
# close position
if (action == Actions.Buy.value and self._position == Positions.Short) or (
action == Actions.Sell.value and self._position == Positions.Long
):
if pnl > self.profit_aim * self.rr:
factor *= self.rl_config["model_reward_parameters"].get("win_reward_factor", 2)
return float(rew * factor)
return 0.

View File

@ -1,5 +1,6 @@
import pytest import pytest
from freqtrade.exceptions import OperationalException
from freqtrade.leverage import interest from freqtrade.leverage import interest
from freqtrade.util import FtPrecise from freqtrade.util import FtPrecise
@ -29,3 +30,13 @@ def test_interest(exchange, interest_rate, hours, expected):
rate=FtPrecise(interest_rate), rate=FtPrecise(interest_rate),
hours=hours hours=hours
))) == expected ))) == expected
def test_interest_exception():
with pytest.raises(OperationalException, match=r"Leverage not available on .* with freqtrade"):
interest(
exchange_name='bitmex',
borrowed=FtPrecise(60.0),
rate=FtPrecise(0.0005),
hours=ten_mins
)

View File

@ -48,8 +48,8 @@ def hyperopt_results():
return pd.DataFrame( return pd.DataFrame(
{ {
'pair': ['ETH/USDT', 'ETH/USDT', 'ETH/USDT', 'ETH/USDT'], 'pair': ['ETH/USDT', 'ETH/USDT', 'ETH/USDT', 'ETH/USDT'],
'profit_ratio': [-0.1, 0.2, -0.1, 0.3], 'profit_ratio': [-0.1, 0.2, -0.12, 0.3],
'profit_abs': [-0.2, 0.4, -0.2, 0.6], 'profit_abs': [-0.2, 0.4, -0.21, 0.6],
'trade_duration': [10, 30, 10, 10], 'trade_duration': [10, 30, 10, 10],
'amount': [0.1, 0.1, 0.1, 0.1], 'amount': [0.1, 0.1, 0.1, 0.1],
'exit_reason': [ExitType.STOP_LOSS, ExitType.ROI, ExitType.STOP_LOSS, ExitType.ROI], 'exit_reason': [ExitType.STOP_LOSS, ExitType.ROI, ExitType.STOP_LOSS, ExitType.ROI],

View File

@ -710,6 +710,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
expected = pd.DataFrame( expected = pd.DataFrame(
{'pair': [pair, pair], {'pair': [pair, pair],
'stake_amount': [0.001, 0.001], 'stake_amount': [0.001, 0.001],
'max_stake_amount': [0.001, 0.001],
'amount': [0.00957442, 0.0097064], 'amount': [0.00957442, 0.0097064],
'open_date': pd.to_datetime([Arrow(2018, 1, 29, 18, 40, 0).datetime, 'open_date': pd.to_datetime([Arrow(2018, 1, 29, 18, 40, 0).datetime,
Arrow(2018, 1, 30, 3, 30, 0).datetime], utc=True Arrow(2018, 1, 30, 3, 30, 0).datetime], utc=True

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