Merge remote-tracking branch 'origin/develop' into feat/convolutional-neural-net
This commit is contained in:
commit
6c96a2464f
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -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.
|
||||||
|
|
||||||
|
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -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
|
||||||
|
2
.github/ISSUE_TEMPLATE/question.md
vendored
2
.github/ISSUE_TEMPLATE/question.md
vendored
@ -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
|
||||||
|
|
||||||
|
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@ -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: |
|
||||||
@ -410,7 +410,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 +418,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__
|
||||||
|
@ -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,
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -118,7 +115,7 @@ The FreqAI strategy requires including the following lines of code in the standa
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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 `populate_any_indicators()` is where [features](freqai-feature-engineering.md#feature-engineering) and labels/targets are added. A full example strategy is available in `templates/FreqaiExampleStrategy.py`.
|
||||||
|
|
||||||
Notice also the location of the labels under `if set_generalized_indicators:` at the bottom of the example. This is where single features and labels/targets should be added to the feature set to avoid duplication of them from various configuration parameters that multiply the feature set, such as `include_timeframes`.
|
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`.
|
||||||
|
|
||||||
@ -182,7 +179,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 +227,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')
|
||||||
|
@ -247,6 +247,32 @@ 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 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:
|
||||||
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -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`).
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -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()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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"
|
]
|
||||||
]
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
@ -104,13 +106,15 @@ class DataProvider:
|
|||||||
def _emit_df(
|
def _emit_df(
|
||||||
self,
|
self,
|
||||||
pair_key: PairWithTimeframe,
|
pair_key: PairWithTimeframe,
|
||||||
dataframe: DataFrame
|
dataframe: DataFrame,
|
||||||
|
new_candle: bool
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Send this dataframe as an ANALYZED_DF message to RPC
|
Send this dataframe as an ANALYZED_DF message to RPC
|
||||||
|
|
||||||
:param pair_key: PairWithTimeframe tuple
|
:param pair_key: PairWithTimeframe tuple
|
||||||
:param data: Tuple containing the DataFrame and the datetime it was cached
|
:param dataframe: Dataframe to emit
|
||||||
|
:param new_candle: This is a new candle
|
||||||
"""
|
"""
|
||||||
if self.__rpc:
|
if self.__rpc:
|
||||||
self.__rpc.send_msg(
|
self.__rpc.send_msg(
|
||||||
@ -118,13 +122,18 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
if new_candle:
|
||||||
|
self.__rpc.send_msg({
|
||||||
|
'type': RPCMessageType.NEW_CANDLE,
|
||||||
|
'data': pair_key,
|
||||||
|
})
|
||||||
|
|
||||||
def _add_external_df(
|
def _replace_external_df(
|
||||||
self,
|
self,
|
||||||
pair: str,
|
pair: str,
|
||||||
dataframe: DataFrame,
|
dataframe: DataFrame,
|
||||||
@ -150,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,
|
||||||
|
@ -6,7 +6,7 @@ from freqtrade.enums.exittype import ExitType
|
|||||||
from freqtrade.enums.hyperoptstate import HyperoptState
|
from freqtrade.enums.hyperoptstate import HyperoptState
|
||||||
from freqtrade.enums.marginmode import MarginMode
|
from freqtrade.enums.marginmode import MarginMode
|
||||||
from freqtrade.enums.ordertypevalue import OrderTypeValues
|
from freqtrade.enums.ordertypevalue import OrderTypeValues
|
||||||
from freqtrade.enums.rpcmessagetype import RPCMessageType, RPCRequestType
|
from freqtrade.enums.rpcmessagetype import NO_ECHO_MESSAGES, RPCMessageType, RPCRequestType
|
||||||
from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode
|
from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode
|
||||||
from freqtrade.enums.signaltype import SignalDirection, SignalTagType, SignalType
|
from freqtrade.enums.signaltype import SignalDirection, SignalTagType, SignalType
|
||||||
from freqtrade.enums.state import State
|
from freqtrade.enums.state import State
|
||||||
|
@ -21,6 +21,7 @@ class RPCMessageType(str, Enum):
|
|||||||
|
|
||||||
WHITELIST = 'whitelist'
|
WHITELIST = 'whitelist'
|
||||||
ANALYZED_DF = 'analyzed_df'
|
ANALYZED_DF = 'analyzed_df'
|
||||||
|
NEW_CANDLE = 'new_candle'
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return self.value
|
return self.value
|
||||||
@ -35,3 +36,6 @@ class RPCRequestType(str, Enum):
|
|||||||
|
|
||||||
WHITELIST = 'whitelist'
|
WHITELIST = 'whitelist'
|
||||||
ANALYZED_DF = 'analyzed_df'
|
ANALYZED_DF = 'analyzed_df'
|
||||||
|
|
||||||
|
|
||||||
|
NO_ECHO_MESSAGES = (RPCMessageType.ANALYZED_DF, RPCMessageType.WHITELIST, RPCMessageType.NEW_CANDLE)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
|
@ -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):
|
||||||
@ -92,9 +95,12 @@ class Base4ActionRLEnv(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()
|
||||||
|
@ -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()
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
Initializes the training/eval environment.
|
Initializes the training/eval environment.
|
||||||
:param df: dataframe of features
|
:param df: dataframe of features
|
||||||
@ -47,22 +56,29 @@ 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.
|
||||||
"""
|
"""
|
||||||
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.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 +133,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 +318,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):
|
||||||
|
@ -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,35 @@ 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}
|
||||||
|
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):
|
||||||
"""
|
"""
|
||||||
@ -377,8 +393,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 +402,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
|
||||||
|
59
freqtrade/freqai/RL/TensorboardCallback.py
Normal file
59
freqtrade/freqai/RL/TensorboardCallback.py
Normal 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
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -462,10 +462,10 @@ class FreqaiDataKitchen:
|
|||||||
:param df: Dataframe containing all candles to run the entire backtest. Here
|
:param df: Dataframe containing all candles to run the entire backtest. Here
|
||||||
it is sliced down to just the present training period.
|
it is sliced down to just the present training period.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
df = df.loc[df["date"] >= timerange.startdt, :]
|
|
||||||
if not self.live:
|
if not self.live:
|
||||||
df = df.loc[df["date"] < timerange.stopdt, :]
|
df = df.loc[(df["date"] >= timerange.startdt) & (df["date"] < timerange.stopdt), :]
|
||||||
|
else:
|
||||||
|
df = df.loc[df["date"] >= timerange.startdt, :]
|
||||||
|
|
||||||
return df
|
return df
|
||||||
|
|
||||||
|
@ -282,10 +282,10 @@ class IFreqaiModel(ABC):
|
|||||||
train_it += 1
|
train_it += 1
|
||||||
total_trains = len(dk.backtesting_timeranges)
|
total_trains = len(dk.backtesting_timeranges)
|
||||||
self.training_timerange = tr_train
|
self.training_timerange = tr_train
|
||||||
dataframe_train = dk.slice_dataframe(tr_train, dataframe)
|
len_backtest_df = len(dataframe.loc[(dataframe["date"] >= tr_backtest.startdt) & (
|
||||||
dataframe_backtest = dk.slice_dataframe(tr_backtest, dataframe)
|
dataframe["date"] < tr_backtest.stopdt), :])
|
||||||
|
|
||||||
if not self.ensure_data_exists(dataframe_backtest, tr_backtest, pair):
|
if not self.ensure_data_exists(len_backtest_df, tr_backtest, pair):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
self.log_backtesting_progress(tr_train, pair, train_it, total_trains)
|
self.log_backtesting_progress(tr_train, pair, train_it, total_trains)
|
||||||
@ -298,13 +298,15 @@ 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(dataframe_backtest)):
|
if dk.check_if_backtest_prediction_is_valid(len_backtest_df):
|
||||||
self.dd.load_metadata(dk)
|
self.dd.load_metadata(dk)
|
||||||
dk.find_features(dataframe_train)
|
dk.find_features(dataframe)
|
||||||
self.check_if_feature_list_matches_strategy(dk)
|
self.check_if_feature_list_matches_strategy(dk)
|
||||||
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)
|
||||||
|
dataframe_backtest = dk.slice_dataframe(tr_backtest, dataframe)
|
||||||
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)
|
||||||
@ -804,16 +806,16 @@ class IFreqaiModel(ABC):
|
|||||||
self.pair_it = 1
|
self.pair_it = 1
|
||||||
self.current_candle = self.dd.current_candle
|
self.current_candle = self.dd.current_candle
|
||||||
|
|
||||||
def ensure_data_exists(self, dataframe_backtest: DataFrame,
|
def ensure_data_exists(self, len_dataframe_backtest: int,
|
||||||
tr_backtest: TimeRange, pair: str) -> bool:
|
tr_backtest: TimeRange, pair: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if the dataframe is empty, if not, report useful information to user.
|
Check if the dataframe is empty, if not, report useful information to user.
|
||||||
:param dataframe_backtest: the backtesting dataframe, maybe empty.
|
:param len_dataframe_backtest: the len of backtesting dataframe
|
||||||
:param tr_backtest: current backtesting timerange.
|
:param tr_backtest: current backtesting timerange.
|
||||||
:param pair: current pair
|
:param pair: current pair
|
||||||
:return: if the data exists or not
|
:return: if the data exists or not
|
||||||
"""
|
"""
|
||||||
if self.config.get("freqai_backtest_live_models", False) and len(dataframe_backtest) == 0:
|
if self.config.get("freqai_backtest_live_models", False) and len_dataframe_backtest == 0:
|
||||||
logger.info(f"No data found for pair {pair} from "
|
logger.info(f"No data found for pair {pair} from "
|
||||||
f"from { tr_backtest.start_fmt} to {tr_backtest.stop_fmt}. "
|
f"from { tr_backtest.start_fmt} to {tr_backtest.stop_fmt}. "
|
||||||
"Probably more than one training within the same candle period.")
|
"Probably more than one training within the same candle period.")
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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:
|
||||||
"""
|
"""
|
||||||
|
@ -301,3 +301,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
|
||||||
|
@ -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:
|
||||||
|
@ -37,7 +37,8 @@ logger = logging.getLogger(__name__)
|
|||||||
# 2.16: Additional daily metrics
|
# 2.16: Additional daily metrics
|
||||||
# 2.17: Forceentry - leverage, partial force_exit
|
# 2.17: Forceentry - leverage, partial force_exit
|
||||||
# 2.20: Add websocket endpoints
|
# 2.20: Add websocket endpoints
|
||||||
API_VERSION = 2.20
|
# 2.21: Add new_candle messagetype
|
||||||
|
API_VERSION = 2.21
|
||||||
|
|
||||||
# Public API, requires no auth.
|
# Public API, requires no auth.
|
||||||
router_public = APIRouter()
|
router_public = APIRouter()
|
||||||
|
@ -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))
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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 ----------------------------
|
||||||
|
@ -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}")
|
||||||
|
@ -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 """
|
||||||
|
@ -6,7 +6,7 @@ from collections import deque
|
|||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from freqtrade.constants import Config
|
from freqtrade.constants import Config
|
||||||
from freqtrade.enums import RPCMessageType
|
from freqtrade.enums import NO_ECHO_MESSAGES, RPCMessageType
|
||||||
from freqtrade.rpc import RPC, RPCHandler
|
from freqtrade.rpc import RPC, RPCHandler
|
||||||
|
|
||||||
|
|
||||||
@ -67,7 +67,7 @@ class RPCManager:
|
|||||||
'status': 'stopping bot'
|
'status': 'stopping bot'
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
if msg.get('type') not in (RPCMessageType.ANALYZED_DF, RPCMessageType.WHITELIST):
|
if msg.get('type') not in NO_ECHO_MESSAGES:
|
||||||
logger.info('Sending rpc message: %s', msg)
|
logger.info('Sending rpc message: %s', msg)
|
||||||
if 'pair' in msg:
|
if 'pair' in msg:
|
||||||
msg.update({
|
msg.update({
|
||||||
|
@ -68,6 +68,7 @@ class Webhook(RPCHandler):
|
|||||||
RPCMessageType.PROTECTION_TRIGGER_GLOBAL,
|
RPCMessageType.PROTECTION_TRIGGER_GLOBAL,
|
||||||
RPCMessageType.WHITELIST,
|
RPCMessageType.WHITELIST,
|
||||||
RPCMessageType.ANALYZED_DF,
|
RPCMessageType.ANALYZED_DF,
|
||||||
|
RPCMessageType.NEW_CANDLE,
|
||||||
RPCMessageType.STRATEGY_MSG):
|
RPCMessageType.STRATEGY_MSG):
|
||||||
# Don't fail for non-implemented types
|
# Don't fail for non-implemented types
|
||||||
return None
|
return None
|
||||||
|
@ -739,10 +739,10 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
"""
|
"""
|
||||||
pair = str(metadata.get('pair'))
|
pair = str(metadata.get('pair'))
|
||||||
|
|
||||||
|
new_candle = self._last_candle_seen_per_pair.get(pair, None) != dataframe.iloc[-1]['date']
|
||||||
# Test if seen this pair and last candle before.
|
# Test if seen this pair and last candle before.
|
||||||
# always run if process_only_new_candles is set to false
|
# always run if process_only_new_candles is set to false
|
||||||
if (not self.process_only_new_candles or
|
if not self.process_only_new_candles or new_candle:
|
||||||
self._last_candle_seen_per_pair.get(pair, None) != dataframe.iloc[-1]['date']):
|
|
||||||
|
|
||||||
# Defs that only make change on new candle data.
|
# Defs that only make change on new candle data.
|
||||||
dataframe = self.analyze_ticker(dataframe, metadata)
|
dataframe = self.analyze_ticker(dataframe, metadata)
|
||||||
@ -751,7 +751,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
|
|
||||||
candle_type = self.config.get('candle_type_def', CandleType.SPOT)
|
candle_type = self.config.get('candle_type_def', CandleType.SPOT)
|
||||||
self.dp._set_cached_df(pair, self.timeframe, dataframe, candle_type=candle_type)
|
self.dp._set_cached_df(pair, self.timeframe, dataframe, candle_type=candle_type)
|
||||||
self.dp._emit_df((pair, self.timeframe, candle_type), dataframe)
|
self.dp._emit_df((pair, self.timeframe, candle_type), dataframe, new_candle)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logger.debug("Skipping TA Analysis for already analyzed candle")
|
logger.debug("Skipping TA Analysis for already analyzed candle")
|
||||||
|
@ -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"
|
||||||
},
|
},
|
||||||
|
@ -12,7 +12,7 @@ flake8-tidy-imports==4.8.0
|
|||||||
mypy==0.991
|
mypy==0.991
|
||||||
pre-commit==2.20.0
|
pre-commit==2.20.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
|
||||||
@ -23,7 +23,7 @@ time-machine==2.8.2
|
|||||||
httpx==0.23.1
|
httpx==0.23.1
|
||||||
|
|
||||||
# Convert jupyter notebooks to markdown documents
|
# Convert jupyter notebooks to markdown documents
|
||||||
nbconvert==7.2.5
|
nbconvert==7.2.6
|
||||||
|
|
||||||
# mypy types
|
# mypy types
|
||||||
types-cachetools==5.2.1
|
types-cachetools==5.2.1
|
||||||
|
@ -7,6 +7,6 @@ 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.3
|
||||||
xgboost==1.7.1
|
xgboost==1.7.2
|
||||||
tensorboard==2.11.0
|
tensorboard==2.11.0
|
||||||
tensorflow==2.11.0
|
tensorflow==2.11.0
|
||||||
|
@ -5,5 +5,5 @@
|
|||||||
scipy==1.9.3
|
scipy==1.9.3
|
||||||
scikit-learn==1.1.3
|
scikit-learn==1.1.3
|
||||||
scikit-optimize==0.9.0
|
scikit-optimize==0.9.0
|
||||||
filelock==3.8.0
|
filelock==3.8.2
|
||||||
progressbar2==4.2.0
|
progressbar2==4.2.0
|
||||||
|
@ -2,13 +2,13 @@ numpy==1.23.5
|
|||||||
pandas==1.5.2
|
pandas==1.5.2
|
||||||
pandas-ta==0.3.14b
|
pandas-ta==0.3.14b
|
||||||
|
|
||||||
ccxt==2.2.67
|
ccxt==2.2.92
|
||||||
# Pin cryptography for now due to rust build errors with piwheels
|
# Pin cryptography for now due to rust build errors with piwheels
|
||||||
cryptography==38.0.1; platform_machine == 'armv7l'
|
cryptography==38.0.1; platform_machine == 'armv7l'
|
||||||
cryptography==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.45
|
||||||
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
|
||||||
@ -20,7 +20,8 @@ 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.7.0
|
||||||
blosc==1.10.6
|
blosc==1.10.6; platform_machine == 'arm64'
|
||||||
|
blosc==1.11.0; platform_machine != 'arm64'
|
||||||
joblib==1.2.0
|
joblib==1.2.0
|
||||||
pyarrow==10.0.1; platform_machine != 'armv7l'
|
pyarrow==10.0.1; platform_machine != 'armv7l'
|
||||||
|
|
||||||
@ -47,7 +48,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
|
||||||
|
|
||||||
|
@ -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:
|
||||||
"""
|
"""
|
||||||
|
@ -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
|
||||||
@ -207,15 +207,21 @@ def test_emit_df(mocker, default_conf, ohlcv_history):
|
|||||||
assert send_mock.call_count == 0
|
assert send_mock.call_count == 0
|
||||||
|
|
||||||
# Rpc is added, we call emit, should call send_msg
|
# Rpc is added, we call emit, should call send_msg
|
||||||
dataprovider._emit_df(pair, ohlcv_history)
|
dataprovider._emit_df(pair, ohlcv_history, False)
|
||||||
assert send_mock.call_count == 1
|
assert send_mock.call_count == 1
|
||||||
|
|
||||||
|
send_mock.reset_mock()
|
||||||
|
dataprovider._emit_df(pair, ohlcv_history, True)
|
||||||
|
assert send_mock.call_count == 2
|
||||||
|
|
||||||
|
send_mock.reset_mock()
|
||||||
|
|
||||||
# No rpc added, emit called, should not call send_msg
|
# No rpc added, emit called, should not call send_msg
|
||||||
dataprovider_no_rpc._emit_df(pair, ohlcv_history)
|
dataprovider_no_rpc._emit_df(pair, ohlcv_history, False)
|
||||||
assert send_mock.call_count == 1
|
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)
|
||||||
|
|
||||||
@ -406,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
|
||||||
|
@ -224,8 +224,13 @@ class TestCCXTExchange():
|
|||||||
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 = exchange.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 = exchange.get_next_limit_in_list(
|
||||||
val, l2_limit_range, l2_limit_range_required)
|
val, l2_limit_range, l2_limit_range_required)
|
||||||
|
@ -4014,9 +4014,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"}}),
|
||||||
|
@ -242,7 +242,6 @@ def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog)
|
|||||||
df = freqai.cache_corr_pairlist_dfs(df, freqai.dk)
|
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)
|
||||||
|
@ -588,7 +588,7 @@ def test_api_show_config(botclient):
|
|||||||
assert 'unfilledtimeout' in response
|
assert 'unfilledtimeout' in response
|
||||||
assert 'version' in response
|
assert 'version' in response
|
||||||
assert 'api_version' in response
|
assert 'api_version' in response
|
||||||
assert 2.1 <= response['api_version'] <= 2.2
|
assert 2.1 <= response['api_version'] < 3.0
|
||||||
|
|
||||||
|
|
||||||
def test_api_daily(botclient, mocker, ticker, fee, markets):
|
def test_api_daily(botclient, mocker, ticker, fee, markets):
|
||||||
|
@ -83,6 +83,7 @@ def test_emc_init(patched_emc):
|
|||||||
def test_emc_handle_producer_message(patched_emc, caplog, ohlcv_history):
|
def test_emc_handle_producer_message(patched_emc, caplog, ohlcv_history):
|
||||||
test_producer = {"name": "test", "url": "ws://test", "ws_token": "test"}
|
test_producer = {"name": "test", "url": "ws://test", "ws_token": "test"}
|
||||||
producer_name = test_producer['name']
|
producer_name = test_producer['name']
|
||||||
|
invalid_msg = r"Invalid message .+"
|
||||||
|
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
|
|
||||||
@ -94,7 +95,7 @@ def test_emc_handle_producer_message(patched_emc, caplog, ohlcv_history):
|
|||||||
assert log_has(
|
assert log_has(
|
||||||
f"Consumed message from `{producer_name}` of type `RPCMessageType.WHITELIST`", caplog)
|
f"Consumed message from `{producer_name}` of type `RPCMessageType.WHITELIST`", caplog)
|
||||||
|
|
||||||
# Test handle analyzed_df message
|
# Test handle analyzed_df single candle message
|
||||||
df_message = {
|
df_message = {
|
||||||
"type": "analyzed_df",
|
"type": "analyzed_df",
|
||||||
"data": {
|
"data": {
|
||||||
@ -106,8 +107,7 @@ def test_emc_handle_producer_message(patched_emc, caplog, ohlcv_history):
|
|||||||
patched_emc.handle_producer_message(test_producer, df_message)
|
patched_emc.handle_producer_message(test_producer, df_message)
|
||||||
|
|
||||||
assert log_has(f"Received message of type `analyzed_df` from `{producer_name}`", caplog)
|
assert log_has(f"Received message of type `analyzed_df` from `{producer_name}`", caplog)
|
||||||
assert log_has(
|
assert log_has_re(r"Holes in data or no existing df,.+", caplog)
|
||||||
f"Consumed message from `{producer_name}` of type `RPCMessageType.ANALYZED_DF`", caplog)
|
|
||||||
|
|
||||||
# Test unhandled message
|
# Test unhandled message
|
||||||
unhandled_message = {"type": "status", "data": "RUNNING"}
|
unhandled_message = {"type": "status", "data": "RUNNING"}
|
||||||
@ -120,7 +120,8 @@ def test_emc_handle_producer_message(patched_emc, caplog, ohlcv_history):
|
|||||||
malformed_message = {"type": "whitelist", "data": {"pair": "BTC/USDT"}}
|
malformed_message = {"type": "whitelist", "data": {"pair": "BTC/USDT"}}
|
||||||
patched_emc.handle_producer_message(test_producer, malformed_message)
|
patched_emc.handle_producer_message(test_producer, malformed_message)
|
||||||
|
|
||||||
assert log_has_re(r"Invalid message .+", caplog)
|
assert log_has_re(invalid_msg, caplog)
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
malformed_message = {
|
malformed_message = {
|
||||||
"type": "analyzed_df",
|
"type": "analyzed_df",
|
||||||
@ -133,13 +134,30 @@ def test_emc_handle_producer_message(patched_emc, caplog, ohlcv_history):
|
|||||||
patched_emc.handle_producer_message(test_producer, malformed_message)
|
patched_emc.handle_producer_message(test_producer, malformed_message)
|
||||||
|
|
||||||
assert log_has(f"Received message of type `analyzed_df` from `{producer_name}`", caplog)
|
assert log_has(f"Received message of type `analyzed_df` from `{producer_name}`", caplog)
|
||||||
assert log_has_re(r"Invalid message .+", caplog)
|
assert log_has_re(invalid_msg, caplog)
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
|
# Empty dataframe
|
||||||
|
malformed_message = {
|
||||||
|
"type": "analyzed_df",
|
||||||
|
"data": {
|
||||||
|
"key": ("BTC/USDT", "5m", "spot"),
|
||||||
|
"df": ohlcv_history.loc[ohlcv_history['open'] < 0],
|
||||||
|
"la": datetime.now(timezone.utc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
patched_emc.handle_producer_message(test_producer, malformed_message)
|
||||||
|
|
||||||
|
assert log_has(f"Received message of type `analyzed_df` from `{producer_name}`", caplog)
|
||||||
|
assert not log_has_re(invalid_msg, caplog)
|
||||||
|
assert log_has_re(r"Received Empty Dataframe for.+", caplog)
|
||||||
|
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
malformed_message = {"some": "stuff"}
|
malformed_message = {"some": "stuff"}
|
||||||
patched_emc.handle_producer_message(test_producer, malformed_message)
|
patched_emc.handle_producer_message(test_producer, malformed_message)
|
||||||
|
|
||||||
assert log_has_re(r"Invalid message .+", caplog)
|
assert log_has_re(invalid_msg, caplog)
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
malformed_message = {"type": "whitelist", "data": None}
|
malformed_message = {"type": "whitelist", "data": None}
|
||||||
@ -183,7 +201,7 @@ async def test_emc_create_connection_success(default_conf, caplog, mocker):
|
|||||||
async with websockets.serve(eat, _TEST_WS_HOST, _TEST_WS_PORT):
|
async with websockets.serve(eat, _TEST_WS_HOST, _TEST_WS_PORT):
|
||||||
await emc._create_connection(test_producer, lock)
|
await emc._create_connection(test_producer, lock)
|
||||||
|
|
||||||
assert log_has_re(r"Producer connection success.+", caplog)
|
assert log_has_re(r"Connected to channel.+", caplog)
|
||||||
finally:
|
finally:
|
||||||
emc.shutdown()
|
emc.shutdown()
|
||||||
|
|
||||||
@ -212,7 +230,8 @@ async def test_emc_create_connection_invalid_url(default_conf, caplog, mocker, h
|
|||||||
|
|
||||||
dp = DataProvider(default_conf, None, None, None)
|
dp = DataProvider(default_conf, None, None, None)
|
||||||
# Handle start explicitly to avoid messing with threading in tests
|
# Handle start explicitly to avoid messing with threading in tests
|
||||||
mocker.patch("freqtrade.rpc.external_message_consumer.ExternalMessageConsumer.start",)
|
mocker.patch("freqtrade.rpc.external_message_consumer.ExternalMessageConsumer.start")
|
||||||
|
mocker.patch("freqtrade.rpc.api_server.ws.channel.create_channel")
|
||||||
emc = ExternalMessageConsumer(default_conf, dp)
|
emc = ExternalMessageConsumer(default_conf, dp)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -390,7 +409,9 @@ async def test_emc_receive_messages_timeout(default_conf, caplog, mocker):
|
|||||||
try:
|
try:
|
||||||
change_running(emc)
|
change_running(emc)
|
||||||
loop.call_soon(functools.partial(change_running, emc=emc))
|
loop.call_soon(functools.partial(change_running, emc=emc))
|
||||||
await emc._receive_messages(TestChannel(), test_producer, lock)
|
|
||||||
|
with pytest.raises(asyncio.TimeoutError):
|
||||||
|
await emc._receive_messages(TestChannel(), test_producer, lock)
|
||||||
|
|
||||||
assert log_has_re(r"Ping error.+", caplog)
|
assert log_has_re(r"Ping error.+", caplog)
|
||||||
finally:
|
finally:
|
||||||
|
@ -12,6 +12,7 @@ from unittest.mock import ANY, MagicMock
|
|||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import pytest
|
import pytest
|
||||||
|
import time_machine
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
from telegram import Chat, Message, ReplyKeyboardMarkup, Update
|
from telegram import Chat, Message, ReplyKeyboardMarkup, Update
|
||||||
from telegram.error import BadRequest, NetworkError, TelegramError
|
from telegram.error import BadRequest, NetworkError, TelegramError
|
||||||
@ -1906,119 +1907,120 @@ def test_send_msg_entry_fill_notification(default_conf, mocker, message_type, en
|
|||||||
|
|
||||||
def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
||||||
|
|
||||||
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
with time_machine.travel("2022-09-01 05:00:00 +00:00", tick=False):
|
||||||
|
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||||
|
|
||||||
old_convamount = telegram._rpc._fiat_converter.convert_amount
|
old_convamount = telegram._rpc._fiat_converter.convert_amount
|
||||||
telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812
|
telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812
|
||||||
telegram.send_msg({
|
telegram.send_msg({
|
||||||
'type': RPCMessageType.EXIT,
|
'type': RPCMessageType.EXIT,
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
'exchange': 'Binance',
|
'exchange': 'Binance',
|
||||||
'pair': 'KEY/ETH',
|
'pair': 'KEY/ETH',
|
||||||
'leverage': 1.0,
|
'leverage': 1.0,
|
||||||
'direction': 'Long',
|
'direction': 'Long',
|
||||||
'gain': 'loss',
|
'gain': 'loss',
|
||||||
'order_rate': 3.201e-05,
|
'order_rate': 3.201e-05,
|
||||||
'amount': 1333.3333333333335,
|
'amount': 1333.3333333333335,
|
||||||
'order_type': 'market',
|
'order_type': 'market',
|
||||||
'open_rate': 7.5e-05,
|
'open_rate': 7.5e-05,
|
||||||
'current_rate': 3.201e-05,
|
'current_rate': 3.201e-05,
|
||||||
'profit_amount': -0.05746268,
|
'profit_amount': -0.05746268,
|
||||||
'profit_ratio': -0.57405275,
|
'profit_ratio': -0.57405275,
|
||||||
'stake_currency': 'ETH',
|
'stake_currency': 'ETH',
|
||||||
'fiat_currency': 'USD',
|
'fiat_currency': 'USD',
|
||||||
'enter_tag': 'buy_signal1',
|
'enter_tag': 'buy_signal1',
|
||||||
'exit_reason': ExitType.STOP_LOSS.value,
|
'exit_reason': ExitType.STOP_LOSS.value,
|
||||||
'open_date': arrow.utcnow().shift(hours=-1),
|
'open_date': arrow.utcnow().shift(hours=-1),
|
||||||
'close_date': arrow.utcnow(),
|
'close_date': arrow.utcnow(),
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] == (
|
assert msg_mock.call_args[0][0] == (
|
||||||
'\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n'
|
'\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n'
|
||||||
'*Unrealized Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n'
|
'*Unrealized Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n'
|
||||||
'*Enter Tag:* `buy_signal1`\n'
|
'*Enter Tag:* `buy_signal1`\n'
|
||||||
'*Exit Reason:* `stop_loss`\n'
|
'*Exit Reason:* `stop_loss`\n'
|
||||||
'*Direction:* `Long`\n'
|
'*Direction:* `Long`\n'
|
||||||
'*Amount:* `1333.33333333`\n'
|
'*Amount:* `1333.33333333`\n'
|
||||||
'*Open Rate:* `0.00007500`\n'
|
'*Open Rate:* `0.00007500`\n'
|
||||||
'*Current Rate:* `0.00003201`\n'
|
'*Current Rate:* `0.00003201`\n'
|
||||||
'*Exit Rate:* `0.00003201`\n'
|
'*Exit Rate:* `0.00003201`\n'
|
||||||
'*Duration:* `1:00:00 (60.0 min)`'
|
'*Duration:* `1:00:00 (60.0 min)`'
|
||||||
)
|
|
||||||
|
|
||||||
msg_mock.reset_mock()
|
|
||||||
telegram.send_msg({
|
|
||||||
'type': RPCMessageType.EXIT,
|
|
||||||
'trade_id': 1,
|
|
||||||
'exchange': 'Binance',
|
|
||||||
'pair': 'KEY/ETH',
|
|
||||||
'direction': 'Long',
|
|
||||||
'gain': 'loss',
|
|
||||||
'order_rate': 3.201e-05,
|
|
||||||
'amount': 1333.3333333333335,
|
|
||||||
'order_type': 'market',
|
|
||||||
'open_rate': 7.5e-05,
|
|
||||||
'current_rate': 3.201e-05,
|
|
||||||
'cumulative_profit': -0.15746268,
|
|
||||||
'profit_amount': -0.05746268,
|
|
||||||
'profit_ratio': -0.57405275,
|
|
||||||
'stake_currency': 'ETH',
|
|
||||||
'fiat_currency': 'USD',
|
|
||||||
'enter_tag': 'buy_signal1',
|
|
||||||
'exit_reason': ExitType.STOP_LOSS.value,
|
|
||||||
'open_date': arrow.utcnow().shift(days=-1, hours=-2, minutes=-30),
|
|
||||||
'close_date': arrow.utcnow(),
|
|
||||||
'stake_amount': 0.01,
|
|
||||||
'sub_trade': True,
|
|
||||||
})
|
|
||||||
assert msg_mock.call_args[0][0] == (
|
|
||||||
'\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n'
|
|
||||||
'*Unrealized Sub Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n'
|
|
||||||
'*Cumulative Profit:* (`-0.15746268 ETH / -24.812 USD`)\n'
|
|
||||||
'*Enter Tag:* `buy_signal1`\n'
|
|
||||||
'*Exit Reason:* `stop_loss`\n'
|
|
||||||
'*Direction:* `Long`\n'
|
|
||||||
'*Amount:* `1333.33333333`\n'
|
|
||||||
'*Open Rate:* `0.00007500`\n'
|
|
||||||
'*Current Rate:* `0.00003201`\n'
|
|
||||||
'*Exit Rate:* `0.00003201`\n'
|
|
||||||
'*Remaining:* `(0.01 ETH, -24.812 USD)`'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
telegram.send_msg({
|
telegram.send_msg({
|
||||||
'type': RPCMessageType.EXIT,
|
'type': RPCMessageType.EXIT,
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
'exchange': 'Binance',
|
'exchange': 'Binance',
|
||||||
'pair': 'KEY/ETH',
|
'pair': 'KEY/ETH',
|
||||||
'direction': 'Long',
|
'direction': 'Long',
|
||||||
'gain': 'loss',
|
'gain': 'loss',
|
||||||
'order_rate': 3.201e-05,
|
'order_rate': 3.201e-05,
|
||||||
'amount': 1333.3333333333335,
|
'amount': 1333.3333333333335,
|
||||||
'order_type': 'market',
|
'order_type': 'market',
|
||||||
'open_rate': 7.5e-05,
|
'open_rate': 7.5e-05,
|
||||||
'current_rate': 3.201e-05,
|
'current_rate': 3.201e-05,
|
||||||
'profit_amount': -0.05746268,
|
'cumulative_profit': -0.15746268,
|
||||||
'profit_ratio': -0.57405275,
|
'profit_amount': -0.05746268,
|
||||||
'stake_currency': 'ETH',
|
'profit_ratio': -0.57405275,
|
||||||
'enter_tag': 'buy_signal1',
|
'stake_currency': 'ETH',
|
||||||
'exit_reason': ExitType.STOP_LOSS.value,
|
'fiat_currency': 'USD',
|
||||||
'open_date': arrow.utcnow().shift(days=-1, hours=-2, minutes=-30),
|
'enter_tag': 'buy_signal1',
|
||||||
'close_date': arrow.utcnow(),
|
'exit_reason': ExitType.STOP_LOSS.value,
|
||||||
})
|
'open_date': arrow.utcnow().shift(days=-1, hours=-2, minutes=-30),
|
||||||
assert msg_mock.call_args[0][0] == (
|
'close_date': arrow.utcnow(),
|
||||||
'\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n'
|
'stake_amount': 0.01,
|
||||||
'*Unrealized Profit:* `-57.41% (loss: -0.05746268 ETH)`\n'
|
'sub_trade': True,
|
||||||
'*Enter Tag:* `buy_signal1`\n'
|
})
|
||||||
'*Exit Reason:* `stop_loss`\n'
|
assert msg_mock.call_args[0][0] == (
|
||||||
'*Direction:* `Long`\n'
|
'\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n'
|
||||||
'*Amount:* `1333.33333333`\n'
|
'*Unrealized Sub Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n'
|
||||||
'*Open Rate:* `0.00007500`\n'
|
'*Cumulative Profit:* (`-0.15746268 ETH / -24.812 USD`)\n'
|
||||||
'*Current Rate:* `0.00003201`\n'
|
'*Enter Tag:* `buy_signal1`\n'
|
||||||
'*Exit Rate:* `0.00003201`\n'
|
'*Exit Reason:* `stop_loss`\n'
|
||||||
'*Duration:* `1 day, 2:30:00 (1590.0 min)`'
|
'*Direction:* `Long`\n'
|
||||||
)
|
'*Amount:* `1333.33333333`\n'
|
||||||
# Reset singleton function to avoid random breaks
|
'*Open Rate:* `0.00007500`\n'
|
||||||
telegram._rpc._fiat_converter.convert_amount = old_convamount
|
'*Current Rate:* `0.00003201`\n'
|
||||||
|
'*Exit Rate:* `0.00003201`\n'
|
||||||
|
'*Remaining:* `(0.01 ETH, -24.812 USD)`'
|
||||||
|
)
|
||||||
|
|
||||||
|
msg_mock.reset_mock()
|
||||||
|
telegram.send_msg({
|
||||||
|
'type': RPCMessageType.EXIT,
|
||||||
|
'trade_id': 1,
|
||||||
|
'exchange': 'Binance',
|
||||||
|
'pair': 'KEY/ETH',
|
||||||
|
'direction': 'Long',
|
||||||
|
'gain': 'loss',
|
||||||
|
'order_rate': 3.201e-05,
|
||||||
|
'amount': 1333.3333333333335,
|
||||||
|
'order_type': 'market',
|
||||||
|
'open_rate': 7.5e-05,
|
||||||
|
'current_rate': 3.201e-05,
|
||||||
|
'profit_amount': -0.05746268,
|
||||||
|
'profit_ratio': -0.57405275,
|
||||||
|
'stake_currency': 'ETH',
|
||||||
|
'enter_tag': 'buy_signal1',
|
||||||
|
'exit_reason': ExitType.STOP_LOSS.value,
|
||||||
|
'open_date': arrow.utcnow().shift(days=-1, hours=-2, minutes=-30),
|
||||||
|
'close_date': arrow.utcnow(),
|
||||||
|
})
|
||||||
|
assert msg_mock.call_args[0][0] == (
|
||||||
|
'\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n'
|
||||||
|
'*Unrealized Profit:* `-57.41% (loss: -0.05746268 ETH)`\n'
|
||||||
|
'*Enter Tag:* `buy_signal1`\n'
|
||||||
|
'*Exit Reason:* `stop_loss`\n'
|
||||||
|
'*Direction:* `Long`\n'
|
||||||
|
'*Amount:* `1333.33333333`\n'
|
||||||
|
'*Open Rate:* `0.00007500`\n'
|
||||||
|
'*Current Rate:* `0.00003201`\n'
|
||||||
|
'*Exit Rate:* `0.00003201`\n'
|
||||||
|
'*Duration:* `1 day, 2:30:00 (1590.0 min)`'
|
||||||
|
)
|
||||||
|
# Reset singleton function to avoid random breaks
|
||||||
|
telegram._rpc._fiat_converter.convert_amount = old_convamount
|
||||||
|
|
||||||
|
|
||||||
def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None:
|
def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None:
|
||||||
@ -2065,41 +2067,42 @@ def test_send_msg_sell_fill_notification(default_conf, mocker, direction,
|
|||||||
default_conf['telegram']['notification_settings']['exit_fill'] = 'on'
|
default_conf['telegram']['notification_settings']['exit_fill'] = 'on'
|
||||||
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||||
|
|
||||||
telegram.send_msg({
|
with time_machine.travel("2022-09-01 05:00:00 +00:00", tick=False):
|
||||||
'type': RPCMessageType.EXIT_FILL,
|
telegram.send_msg({
|
||||||
'trade_id': 1,
|
'type': RPCMessageType.EXIT_FILL,
|
||||||
'exchange': 'Binance',
|
'trade_id': 1,
|
||||||
'pair': 'KEY/ETH',
|
'exchange': 'Binance',
|
||||||
'leverage': leverage,
|
'pair': 'KEY/ETH',
|
||||||
'direction': direction,
|
'leverage': leverage,
|
||||||
'gain': 'loss',
|
'direction': direction,
|
||||||
'limit': 3.201e-05,
|
'gain': 'loss',
|
||||||
'amount': 1333.3333333333335,
|
'limit': 3.201e-05,
|
||||||
'order_type': 'market',
|
'amount': 1333.3333333333335,
|
||||||
'open_rate': 7.5e-05,
|
'order_type': 'market',
|
||||||
'close_rate': 3.201e-05,
|
'open_rate': 7.5e-05,
|
||||||
'profit_amount': -0.05746268,
|
'close_rate': 3.201e-05,
|
||||||
'profit_ratio': -0.57405275,
|
'profit_amount': -0.05746268,
|
||||||
'stake_currency': 'ETH',
|
'profit_ratio': -0.57405275,
|
||||||
'enter_tag': enter_signal,
|
'stake_currency': 'ETH',
|
||||||
'exit_reason': ExitType.STOP_LOSS.value,
|
'enter_tag': enter_signal,
|
||||||
'open_date': arrow.utcnow().shift(days=-1, hours=-2, minutes=-30),
|
'exit_reason': ExitType.STOP_LOSS.value,
|
||||||
'close_date': arrow.utcnow(),
|
'open_date': arrow.utcnow().shift(days=-1, hours=-2, minutes=-30),
|
||||||
})
|
'close_date': arrow.utcnow(),
|
||||||
|
})
|
||||||
|
|
||||||
leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else ''
|
leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else ''
|
||||||
assert msg_mock.call_args[0][0] == (
|
assert msg_mock.call_args[0][0] == (
|
||||||
'\N{WARNING SIGN} *Binance (dry):* Exited KEY/ETH (#1)\n'
|
'\N{WARNING SIGN} *Binance (dry):* Exited KEY/ETH (#1)\n'
|
||||||
'*Profit:* `-57.41% (loss: -0.05746268 ETH)`\n'
|
'*Profit:* `-57.41% (loss: -0.05746268 ETH)`\n'
|
||||||
f'*Enter Tag:* `{enter_signal}`\n'
|
f'*Enter Tag:* `{enter_signal}`\n'
|
||||||
'*Exit Reason:* `stop_loss`\n'
|
'*Exit Reason:* `stop_loss`\n'
|
||||||
f"*Direction:* `{direction}`\n"
|
f"*Direction:* `{direction}`\n"
|
||||||
f"{leverage_text}"
|
f"{leverage_text}"
|
||||||
'*Amount:* `1333.33333333`\n'
|
'*Amount:* `1333.33333333`\n'
|
||||||
'*Open Rate:* `0.00007500`\n'
|
'*Open Rate:* `0.00007500`\n'
|
||||||
'*Exit Rate:* `0.00003201`\n'
|
'*Exit Rate:* `0.00003201`\n'
|
||||||
'*Duration:* `1 day, 2:30:00 (1590.0 min)`'
|
'*Duration:* `1 day, 2:30:00 (1590.0 min)`'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_send_msg_status_notification(default_conf, mocker) -> None:
|
def test_send_msg_status_notification(default_conf, mocker) -> None:
|
||||||
|
@ -1046,8 +1046,13 @@ def test__validate_freqai_include_timeframes(default_conf, caplog) -> None:
|
|||||||
# Validation pass
|
# Validation pass
|
||||||
conf.update({'timeframe': '1m'})
|
conf.update({'timeframe': '1m'})
|
||||||
validate_config_consistency(conf)
|
validate_config_consistency(conf)
|
||||||
conf.update({'analyze_per_epoch': True})
|
|
||||||
|
|
||||||
|
# Ensure base timeframe is in include_timeframes
|
||||||
|
conf['freqai']['feature_parameters']['include_timeframes'] = ["5m", "15m"]
|
||||||
|
validate_config_consistency(conf)
|
||||||
|
assert conf['freqai']['feature_parameters']['include_timeframes'] == ["1m", "5m", "15m"]
|
||||||
|
|
||||||
|
conf.update({'analyze_per_epoch': True})
|
||||||
with pytest.raises(OperationalException,
|
with pytest.raises(OperationalException,
|
||||||
match=r"Using analyze-per-epoch .* not supported with a FreqAI strategy."):
|
match=r"Using analyze-per-epoch .* not supported with a FreqAI strategy."):
|
||||||
validate_config_consistency(conf)
|
validate_config_consistency(conf)
|
||||||
|
@ -88,6 +88,18 @@ def test_bot_cleanup(mocker, default_conf_usdt, caplog) -> None:
|
|||||||
assert coo_mock.call_count == 1
|
assert coo_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_bot_cleanup_db_errors(mocker, default_conf_usdt, caplog) -> None:
|
||||||
|
mocker.patch('freqtrade.freqtradebot.Trade.commit',
|
||||||
|
side_effect=OperationalException())
|
||||||
|
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.check_for_open_trades',
|
||||||
|
side_effect=OperationalException())
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||||
|
freqtrade.emc = MagicMock()
|
||||||
|
freqtrade.emc.shutdown = MagicMock()
|
||||||
|
freqtrade.cleanup()
|
||||||
|
assert freqtrade.emc.shutdown.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('runmode', [
|
@pytest.mark.parametrize('runmode', [
|
||||||
RunMode.DRY_RUN,
|
RunMode.DRY_RUN,
|
||||||
RunMode.LIVE
|
RunMode.LIVE
|
||||||
|
Loading…
Reference in New Issue
Block a user