Merge remote-tracking branch 'origin/develop' into update-freqai-tf-handling
This commit is contained in:
commit
272c3302e3
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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -104,13 +104,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(
|
||||||
@ -123,6 +125,11 @@ class DataProvider:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
if new_candle:
|
||||||
|
self.__rpc.send_msg({
|
||||||
|
'type': RPCMessageType.NEW_CANDLE,
|
||||||
|
'data': pair_key,
|
||||||
|
})
|
||||||
|
|
||||||
def _add_external_df(
|
def _add_external_df(
|
||||||
self,
|
self,
|
||||||
|
@ -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)
|
||||||
|
@ -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))
|
||||||
@ -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))
|
||||||
@ -98,9 +101,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
|
||||||
|
|
||||||
import gym
|
import gym
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@ -12,11 +12,23 @@ from gym.utils import seeding
|
|||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.data.dataprovider import DataProvider
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
|
from freqtrade.enums import RunMode
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
@ -64,6 +76,16 @@ class BaseEnvironment(gym.Env):
|
|||||||
else:
|
else:
|
||||||
self.fee = 0.0015
|
self.fee = 0.0015
|
||||||
|
|
||||||
|
# set here to default 5Ac, but all children envs can override this
|
||||||
|
self.actions: Type[Enum] = BaseActions
|
||||||
|
self.custom_info: dict = {}
|
||||||
|
self.live: bool = False
|
||||||
|
if dp:
|
||||||
|
self.live = dp.runmode in (RunMode.DRY_RUN, RunMode.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.")
|
||||||
|
|
||||||
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):
|
||||||
"""
|
"""
|
||||||
@ -118,6 +140,19 @@ class BaseEnvironment(gym.Env):
|
|||||||
return [seed]
|
return [seed]
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
|
"""
|
||||||
|
Reset is called at the beginning of every episode
|
||||||
|
"""
|
||||||
|
# custom_info is used for episodic reports and tensorboard logging
|
||||||
|
self.custom_info["Invalid"] = 0
|
||||||
|
self.custom_info["Hold"] = 0
|
||||||
|
self.custom_info["Unknown"] = 0
|
||||||
|
self.custom_info["pnl_factor"] = 0
|
||||||
|
self.custom_info["duration_factor"] = 0
|
||||||
|
self.custom_info["reward_exit"] = 0
|
||||||
|
self.custom_info["reward_hold"] = 0
|
||||||
|
for action in self.actions:
|
||||||
|
self.custom_info[f"{action.name}"] = 0
|
||||||
|
|
||||||
self._done = False
|
self._done = False
|
||||||
|
|
||||||
@ -160,7 +195,7 @@ class BaseEnvironment(gym.Env):
|
|||||||
"""
|
"""
|
||||||
features_window = self.signal_features[(
|
features_window = self.signal_features[(
|
||||||
self._current_tick - self.window_size):self._current_tick]
|
self._current_tick - self.window_size):self._current_tick]
|
||||||
if self.add_state_info:
|
if self.add_state_info and self.live:
|
||||||
features_and_state = DataFrame(np.zeros((len(features_window), 3)),
|
features_and_state = DataFrame(np.zeros((len(features_window), 3)),
|
||||||
columns=['current_profit_pct',
|
columns=['current_profit_pct',
|
||||||
'position',
|
'position',
|
||||||
@ -271,6 +306,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):
|
||||||
"""
|
"""
|
||||||
@ -156,6 +159,9 @@ class BaseReinforcementLearningModel(IFreqaiModel):
|
|||||||
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)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def fit(self, data_dictionary: Dict[str, Any], dk: FreqaiDataKitchen, **kwargs):
|
def fit(self, data_dictionary: Dict[str, Any], dk: FreqaiDataKitchen, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
60
freqtrade/freqai/RL/TensorboardCallback.py
Normal file
60
freqtrade/freqai/RL/TensorboardCallback.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
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:
|
||||||
|
custom_info = self.training_env.get_attr("custom_info")[0]
|
||||||
|
self.logger.record("_state/position", self.locals["infos"][0]["position"])
|
||||||
|
self.logger.record("_state/trade_duration", self.locals["infos"][0]["trade_duration"])
|
||||||
|
self.logger.record("_state/current_profit_pct", self.locals["infos"]
|
||||||
|
[0]["current_profit_pct"])
|
||||||
|
self.logger.record("_reward/total_profit", self.locals["infos"][0]["total_profit"])
|
||||||
|
self.logger.record("_reward/total_reward", self.locals["infos"][0]["total_reward"])
|
||||||
|
self.logger.record_mean("_reward/mean_trade_duration", self.locals["infos"]
|
||||||
|
[0]["trade_duration"])
|
||||||
|
self.logger.record("_actions/action", self.locals["infos"][0]["action"])
|
||||||
|
self.logger.record("_actions/_Invalid", custom_info["Invalid"])
|
||||||
|
self.logger.record("_actions/_Unknown", custom_info["Unknown"])
|
||||||
|
self.logger.record("_actions/Hold", custom_info["Hold"])
|
||||||
|
for action in self.actions:
|
||||||
|
self.logger.record(f"_actions/{action.name}", custom_info[action.name])
|
||||||
|
return True
|
@ -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.")
|
||||||
|
@ -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,17 +100,24 @@ 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.custom_info["Invalid"] += 1
|
||||||
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):
|
and self._position == Positions.Neutral):
|
||||||
|
self.custom_info[f"{Actions.Long_enter.name}"] += 1
|
||||||
|
return 25
|
||||||
|
if (action == Actions.Short_enter.value
|
||||||
|
and self._position == Positions.Neutral):
|
||||||
|
self.custom_info[f"{Actions.Short_enter.name}"] += 1
|
||||||
return 25
|
return 25
|
||||||
# discourage agent from not entering trades
|
# discourage agent from not entering trades
|
||||||
if action == Actions.Neutral.value and self._position == Positions.Neutral:
|
if action == Actions.Neutral.value and self._position == Positions.Neutral:
|
||||||
|
self.custom_info[f"{Actions.Neutral.name}"] += 1
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
max_trade_duration = self.rl_config.get('max_trade_duration_candles', 300)
|
max_trade_duration = self.rl_config.get('max_trade_duration_candles', 300)
|
||||||
@ -124,18 +131,22 @@ class ReinforcementLearner(BaseReinforcementLearningModel):
|
|||||||
# discourage sitting in position
|
# discourage sitting in position
|
||||||
if (self._position in (Positions.Short, Positions.Long) and
|
if (self._position in (Positions.Short, Positions.Long) and
|
||||||
action == Actions.Neutral.value):
|
action == Actions.Neutral.value):
|
||||||
|
self.custom_info["Hold"] += 1
|
||||||
return -1 * trade_duration / max_trade_duration
|
return -1 * trade_duration / max_trade_duration
|
||||||
|
|
||||||
# close long
|
# close long
|
||||||
if action == Actions.Long_exit.value and self._position == Positions.Long:
|
if action == Actions.Long_exit.value and self._position == Positions.Long:
|
||||||
if pnl > self.profit_aim * self.rr:
|
if pnl > self.profit_aim * self.rr:
|
||||||
factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2)
|
factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2)
|
||||||
|
self.custom_info[f"{Actions.Long_exit.name}"] += 1
|
||||||
return float(pnl * factor)
|
return float(pnl * factor)
|
||||||
|
|
||||||
# close short
|
# close short
|
||||||
if action == Actions.Short_exit.value and self._position == Positions.Short:
|
if action == Actions.Short_exit.value and self._position == Positions.Short:
|
||||||
if pnl > self.profit_aim * self.rr:
|
if pnl > self.profit_aim * self.rr:
|
||||||
factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2)
|
factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2)
|
||||||
|
self.custom_info[f"{Actions.Short_exit.name}"] += 1
|
||||||
return float(pnl * factor)
|
return float(pnl * factor)
|
||||||
|
|
||||||
|
self.custom_info["Unknown"] += 1
|
||||||
return 0.
|
return 0.
|
||||||
|
@ -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__)
|
||||||
@ -49,3 +49,6 @@ class ReinforcementLearner_multiproc(ReinforcementLearner):
|
|||||||
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)
|
||||||
|
@ -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()
|
||||||
|
@ -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"
|
||||||
},
|
},
|
||||||
|
@ -207,12 +207,18 @@ 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, ohlcv_history):
|
||||||
|
@ -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)
|
||||||
|
@ -237,7 +237,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):
|
||||||
|
@ -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:
|
||||||
|
Loading…
Reference in New Issue
Block a user