commit
9634e516a9
17
.dependabot/config.yml
Normal file
17
.dependabot/config.yml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
version: 1
|
||||||
|
|
||||||
|
update_configs:
|
||||||
|
- package_manager: "python"
|
||||||
|
directory: "/"
|
||||||
|
update_schedule: "weekly"
|
||||||
|
allowed_updates:
|
||||||
|
- match:
|
||||||
|
update_type: "all"
|
||||||
|
target_branch: "develop"
|
||||||
|
|
||||||
|
- package_manager: "docker"
|
||||||
|
directory: "/"
|
||||||
|
update_schedule: "daily"
|
||||||
|
allowed_updates:
|
||||||
|
- match:
|
||||||
|
update_type: "all"
|
1
.github/ISSUE_TEMPLATE.md
vendored
1
.github/ISSUE_TEMPLATE.md
vendored
@ -5,6 +5,7 @@ If it hasn't been reported, please create a new issue.
|
|||||||
|
|
||||||
## Step 2: Describe your environment
|
## Step 2: Describe your environment
|
||||||
|
|
||||||
|
* Operating system: ____
|
||||||
* Python Version: _____ (`python -V`)
|
* Python Version: _____ (`python -V`)
|
||||||
* CCXT version: _____ (`pip freeze | grep ccxt`)
|
* CCXT version: _____ (`pip freeze | grep ccxt`)
|
||||||
* Branch: Master | Develop
|
* Branch: Master | Develop
|
||||||
|
11
.gitignore
vendored
11
.gitignore
vendored
@ -6,7 +6,10 @@ config*.json
|
|||||||
.hyperopt
|
.hyperopt
|
||||||
logfile.txt
|
logfile.txt
|
||||||
hyperopt_trials.pickle
|
hyperopt_trials.pickle
|
||||||
user_data/
|
user_data/*
|
||||||
|
!user_data/notebooks
|
||||||
|
user_data/notebooks/*
|
||||||
|
!user_data/notebooks/*example.ipynb
|
||||||
freqtrade-plot.html
|
freqtrade-plot.html
|
||||||
freqtrade-profit-plot.html
|
freqtrade-profit-plot.html
|
||||||
|
|
||||||
@ -80,8 +83,7 @@ docs/_build/
|
|||||||
target/
|
target/
|
||||||
|
|
||||||
# Jupyter Notebook
|
# Jupyter Notebook
|
||||||
.ipynb_checkpoints
|
*.ipynb_checkpoints
|
||||||
*.ipynb
|
|
||||||
|
|
||||||
# pyenv
|
# pyenv
|
||||||
.python-version
|
.python-version
|
||||||
@ -93,3 +95,6 @@ target/
|
|||||||
|
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
|
|
||||||
|
#exceptions
|
||||||
|
!*.gitkeep
|
||||||
|
37
.pyup.yml
37
.pyup.yml
@ -1,37 +0,0 @@
|
|||||||
# autogenerated pyup.io config file
|
|
||||||
# see https://pyup.io/docs/configuration/ for all available options
|
|
||||||
|
|
||||||
# configure updates globally
|
|
||||||
# default: all
|
|
||||||
# allowed: all, insecure, False
|
|
||||||
update: all
|
|
||||||
|
|
||||||
# configure dependency pinning globally
|
|
||||||
# default: True
|
|
||||||
# allowed: True, False
|
|
||||||
pin: True
|
|
||||||
|
|
||||||
# update schedule
|
|
||||||
# default: empty
|
|
||||||
# allowed: "every day", "every week", ..
|
|
||||||
schedule: "every week"
|
|
||||||
|
|
||||||
|
|
||||||
search: False
|
|
||||||
# Specify requirement files by hand, default is empty
|
|
||||||
# default: empty
|
|
||||||
# allowed: list
|
|
||||||
requirements:
|
|
||||||
- requirements.txt
|
|
||||||
- requirements-dev.txt
|
|
||||||
- requirements-plot.txt
|
|
||||||
- requirements-common.txt
|
|
||||||
|
|
||||||
|
|
||||||
# configure the branch prefix the bot is using
|
|
||||||
# default: pyup-
|
|
||||||
branch_prefix: pyup/
|
|
||||||
|
|
||||||
# allow to close stale PRs
|
|
||||||
# default: True
|
|
||||||
close_prs: True
|
|
19
.travis.yml
19
.travis.yml
@ -10,15 +10,11 @@ services:
|
|||||||
env:
|
env:
|
||||||
global:
|
global:
|
||||||
- IMAGE_NAME=freqtradeorg/freqtrade
|
- IMAGE_NAME=freqtradeorg/freqtrade
|
||||||
addons:
|
|
||||||
apt:
|
|
||||||
packages:
|
|
||||||
- libelf-dev
|
|
||||||
- libdw-dev
|
|
||||||
- binutils-dev
|
|
||||||
install:
|
install:
|
||||||
- cd build_helpers && ./install_ta-lib.sh; cd ..
|
- cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
|
||||||
- export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
|
- export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
|
||||||
|
- export TA_LIBRARY_PATH=${HOME}/dependencies/lib
|
||||||
|
- export TA_INCLUDE_PATH=${HOME}/dependencies/lib/include
|
||||||
- pip install -r requirements-dev.txt
|
- pip install -r requirements-dev.txt
|
||||||
- pip install -e .
|
- pip install -e .
|
||||||
jobs:
|
jobs:
|
||||||
@ -40,6 +36,11 @@ jobs:
|
|||||||
name: hyperopt
|
name: hyperopt
|
||||||
- script: flake8 freqtrade scripts
|
- script: flake8 freqtrade scripts
|
||||||
name: flake8
|
name: flake8
|
||||||
|
- script:
|
||||||
|
# Test Documentation boxes -
|
||||||
|
# !!! <TYPE>: is not allowed!
|
||||||
|
- grep -Er '^!{3}\s\S+:' docs/*; test $? -ne 0
|
||||||
|
name: doc syntax
|
||||||
- script: mypy freqtrade scripts
|
- script: mypy freqtrade scripts
|
||||||
name: mypy
|
name: mypy
|
||||||
|
|
||||||
@ -55,4 +56,4 @@ notifications:
|
|||||||
cache:
|
cache:
|
||||||
pip: True
|
pip: True
|
||||||
directories:
|
directories:
|
||||||
- /usr/local/lib/
|
- $HOME/dependencies
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
FROM python:3.7.3-slim-stretch
|
FROM python:3.7.4-slim-stretch
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get -y install curl build-essential libssl-dev \
|
&& apt-get -y install curl build-essential libssl-dev \
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
if [ ! -f "/usr/local/lib/libta_lib.a" ]; then
|
if [ -z "$1" ]; then
|
||||||
|
INSTALL_LOC=/usr/local
|
||||||
|
else
|
||||||
|
INSTALL_LOC=${1}
|
||||||
|
fi
|
||||||
|
echo "Installing to ${INSTALL_LOC}"
|
||||||
|
if [ ! -f "${INSTALL_LOC}/lib/libta_lib.a" ]; then
|
||||||
tar zxvf ta-lib-0.4.0-src.tar.gz
|
tar zxvf ta-lib-0.4.0-src.tar.gz
|
||||||
cd ta-lib \
|
cd ta-lib \
|
||||||
&& sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \
|
&& sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \
|
||||||
&& ./configure \
|
&& ./configure --prefix=${INSTALL_LOC}/ \
|
||||||
&& make \
|
&& make \
|
||||||
&& which sudo && sudo make install || make install \
|
&& which sudo && sudo make install || make install \
|
||||||
&& cd ..
|
&& cd ..
|
||||||
|
@ -3,9 +3,43 @@
|
|||||||
This page explains how to validate your strategy performance by using
|
This page explains how to validate your strategy performance by using
|
||||||
Backtesting.
|
Backtesting.
|
||||||
|
|
||||||
|
## Getting data for backtesting and hyperopt
|
||||||
|
|
||||||
|
To download data (candles / OHLCV) needed for backtesting and hyperoptimization use the `freqtrade download-data` command.
|
||||||
|
|
||||||
|
If no additional parameter is specified, freqtrade will download data for `"1m"` and `"5m"` timeframes.
|
||||||
|
Exchange and pairs will come from `config.json` (if specified using `-c/--config`). Otherwise `--exchange` becomes mandatory.
|
||||||
|
|
||||||
|
Alternatively, a `pairs.json` file can be used.
|
||||||
|
|
||||||
|
If you are using Binance for example:
|
||||||
|
|
||||||
|
- create a directory `user_data/data/binance` and copy `pairs.json` in that directory.
|
||||||
|
- update the `pairs.json` to contain the currency pairs you are interested in.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p user_data/data/binance
|
||||||
|
cp freqtrade/tests/testdata/pairs.json user_data/data/binance
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
freqtrade download-data --exchange binance
|
||||||
|
```
|
||||||
|
|
||||||
|
This will download ticker data for all the currency pairs you defined in `pairs.json`.
|
||||||
|
|
||||||
|
- To use a different directory than the exchange specific default, use `--datadir user_data/data/some_directory`.
|
||||||
|
- To change the exchange used to download the tickers, please use a different configuration file (you'll probably need to adjust ratelimits etc.)
|
||||||
|
- To use `pairs.json` from some other directory, use `--pairs-file some_other_dir/pairs.json`.
|
||||||
|
- To download ticker data for only 10 days, use `--days 10` (defaults to 30 days).
|
||||||
|
- Use `--timeframes` to specify which tickers to download. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute tickers.
|
||||||
|
- To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with most other options.
|
||||||
|
|
||||||
## Test your strategy with Backtesting
|
## Test your strategy with Backtesting
|
||||||
|
|
||||||
Now you have good Buy and Sell strategies, you want to test it against
|
Now you have good Buy and Sell strategies and some historic data, you want to test it against
|
||||||
real data. This is what we call
|
real data. This is what we call
|
||||||
[backtesting](https://en.wikipedia.org/wiki/Backtesting).
|
[backtesting](https://en.wikipedia.org/wiki/Backtesting).
|
||||||
|
|
||||||
@ -33,22 +67,13 @@ freqtrade backtesting
|
|||||||
freqtrade backtesting --ticker-interval 1m
|
freqtrade backtesting --ticker-interval 1m
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Update cached pairs with the latest data
|
|
||||||
|
|
||||||
```bash
|
|
||||||
freqtrade backtesting --refresh-pairs-cached
|
|
||||||
```
|
|
||||||
|
|
||||||
#### With live data (do not alter your testdata files)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
freqtrade backtesting --live
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Using a different on-disk ticker-data source
|
#### Using a different on-disk ticker-data source
|
||||||
|
|
||||||
|
Assume you downloaded the history data from the Bittrex exchange and kept it in the `user_data/data/bittrex-20180101` directory.
|
||||||
|
You can then use this data for backtesting as follows:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
freqtrade backtesting --datadir freqtrade/tests/testdata-20180101
|
freqtrade backtesting --datadir user_data/data/bittrex-20180101
|
||||||
```
|
```
|
||||||
|
|
||||||
#### With a (custom) strategy file
|
#### With a (custom) strategy file
|
||||||
@ -57,7 +82,15 @@ freqtrade backtesting --datadir freqtrade/tests/testdata-20180101
|
|||||||
freqtrade -s TestStrategy backtesting
|
freqtrade -s TestStrategy backtesting
|
||||||
```
|
```
|
||||||
|
|
||||||
Where `-s TestStrategy` refers to the class name within the strategy file `test_strategy.py` found in the `freqtrade/user_data/strategies` directory
|
Where `-s TestStrategy` refers to the class name within the strategy file `test_strategy.py` found in the `freqtrade/user_data/strategies` directory.
|
||||||
|
|
||||||
|
#### Comparing multiple Strategies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
freqtrade backtesting --strategy-list TestStrategy1 AwesomeStrategy --ticker-interval 5m
|
||||||
|
```
|
||||||
|
|
||||||
|
Where `TestStrategy1` and `AwesomeStrategy` refer to class names of strategies.
|
||||||
|
|
||||||
#### Exporting trades to file
|
#### Exporting trades to file
|
||||||
|
|
||||||
@ -101,37 +134,6 @@ The full timerange specification:
|
|||||||
- Use tickframes between POSIX timestamps 1527595200 1527618600:
|
- Use tickframes between POSIX timestamps 1527595200 1527618600:
|
||||||
`--timerange=1527595200-1527618600`
|
`--timerange=1527595200-1527618600`
|
||||||
|
|
||||||
#### Downloading new set of ticker data
|
|
||||||
|
|
||||||
To download new set of backtesting ticker data, you can use a download script.
|
|
||||||
|
|
||||||
If you are using Binance for example:
|
|
||||||
|
|
||||||
- create a directory `user_data/data/binance` and copy `pairs.json` in that directory.
|
|
||||||
- update the `pairs.json` to contain the currency pairs you are interested in.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p user_data/data/binance
|
|
||||||
cp freqtrade/tests/testdata/pairs.json user_data/data/binance
|
|
||||||
```
|
|
||||||
|
|
||||||
Then run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python scripts/download_backtest_data.py --exchange binance
|
|
||||||
```
|
|
||||||
|
|
||||||
This will download ticker data for all the currency pairs you defined in `pairs.json`.
|
|
||||||
|
|
||||||
- To use a different directory than the exchange specific default, use `--datadir user_data/data/some_directory`.
|
|
||||||
- To change the exchange used to download the tickers, use `--exchange`. Default is `bittrex`.
|
|
||||||
- To use `pairs.json` from some other directory, use `--pairs-file some_other_dir/pairs.json`.
|
|
||||||
- To download ticker data for only 10 days, use `--days 10`.
|
|
||||||
- Use `--timeframes` to specify which tickers to download. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute tickers.
|
|
||||||
- To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with other options.
|
|
||||||
|
|
||||||
For help about backtesting usage, please refer to [Backtesting commands](#backtesting-commands).
|
|
||||||
|
|
||||||
## Understand the backtesting result
|
## Understand the backtesting result
|
||||||
|
|
||||||
The most important in the backtesting is to understand the result.
|
The most important in the backtesting is to understand the result.
|
||||||
@ -237,7 +239,7 @@ All listed Strategies need to be in the same directory.
|
|||||||
freqtrade backtesting --timerange 20180401-20180410 --ticker-interval 5m --strategy-list Strategy001 Strategy002 --export trades
|
freqtrade backtesting --timerange 20180401-20180410 --ticker-interval 5m --strategy-list Strategy001 Strategy002 --export trades
|
||||||
```
|
```
|
||||||
|
|
||||||
This will save the results to `user_data/backtest_data/backtest-result-<strategy>.json`, injecting the strategy-name into the target filename.
|
This will save the results to `user_data/backtest_results/backtest-result-<strategy>.json`, injecting the strategy-name into the target filename.
|
||||||
There will be an additional table comparing win/losses of the different strategies (identical to the "Total" row in the first table).
|
There will be an additional table comparing win/losses of the different strategies (identical to the "Total" row in the first table).
|
||||||
Detailed output for all strategies one after the other will be available, so make sure to scroll up.
|
Detailed output for all strategies one after the other will be available, so make sure to scroll up.
|
||||||
|
|
||||||
|
@ -2,62 +2,70 @@
|
|||||||
|
|
||||||
This page explains the different parameters of the bot and how to run it.
|
This page explains the different parameters of the bot and how to run it.
|
||||||
|
|
||||||
!Note:
|
!!! Note
|
||||||
If you've used `setup.sh`, don't forget to activate your virtual environment (`source .env/bin/activate`) before running freqtrade commands.
|
If you've used `setup.sh`, don't forget to activate your virtual environment (`source .env/bin/activate`) before running freqtrade commands.
|
||||||
|
|
||||||
|
|
||||||
## Bot commands
|
## Bot commands
|
||||||
|
|
||||||
```
|
```
|
||||||
usage: freqtrade [-h] [-v] [--logfile FILE] [--version] [-c PATH] [-d PATH]
|
usage: freqtrade [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
||||||
[-s NAME] [--strategy-path PATH] [--db-url PATH]
|
[--userdir PATH] [-s NAME] [--strategy-path PATH]
|
||||||
[--sd-notify]
|
[--db-url PATH] [--sd-notify]
|
||||||
{backtesting,edge,hyperopt} ...
|
{backtesting,edge,hyperopt,create-userdir,list-exchanges} ...
|
||||||
|
|
||||||
Free, open source crypto trading bot
|
Free, open source crypto trading bot
|
||||||
|
|
||||||
positional arguments:
|
positional arguments:
|
||||||
{backtesting,edge,hyperopt}
|
{backtesting,edge,hyperopt,create-userdir,list-exchanges}
|
||||||
backtesting Backtesting module.
|
backtesting Backtesting module.
|
||||||
edge Edge module.
|
edge Edge module.
|
||||||
hyperopt Hyperopt module.
|
hyperopt Hyperopt module.
|
||||||
|
create-userdir Create user-data directory.
|
||||||
|
list-exchanges Print available exchanges.
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||||
--logfile FILE Log to the file specified
|
--logfile FILE Log to the file specified.
|
||||||
-V, --version show program's version number and exit
|
-V, --version show program's version number and exit
|
||||||
-c PATH, --config PATH
|
-c PATH, --config PATH
|
||||||
Specify configuration file (default: None). Multiple
|
Specify configuration file (default: `config.json`).
|
||||||
--config options may be used. Can be set to '-' to
|
Multiple --config options may be used. Can be set to
|
||||||
read config from stdin.
|
`-` to read config from stdin.
|
||||||
-d PATH, --datadir PATH
|
-d PATH, --datadir PATH
|
||||||
Path to backtest data.
|
Path to directory with historical backtesting data.
|
||||||
|
--userdir PATH, --user-data-dir PATH
|
||||||
|
Path to userdata directory.
|
||||||
-s NAME, --strategy NAME
|
-s NAME, --strategy NAME
|
||||||
Specify strategy class name (default:
|
Specify strategy class name (default:
|
||||||
DefaultStrategy).
|
`DefaultStrategy`).
|
||||||
--strategy-path PATH Specify additional strategy lookup path.
|
--strategy-path PATH Specify additional strategy lookup path.
|
||||||
--db-url PATH Override trades database URL, this is useful if
|
--db-url PATH Override trades database URL, this is useful in custom
|
||||||
dry_run is enabled or in custom deployments (default:
|
deployments (default: `sqlite:///tradesv3.sqlite` for
|
||||||
None).
|
Live Run mode, `sqlite://` for Dry Run).
|
||||||
--sd-notify Notify systemd service manager.
|
--sd-notify Notify systemd service manager.
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### How to use a different configuration file?
|
### How to specify which configuration file be used?
|
||||||
|
|
||||||
The bot allows you to select which configuration file you want to use. Per
|
The bot allows you to select which configuration file you want to use by means of
|
||||||
default, the bot will load the file `./config.json`
|
the `-c/--config` command line option:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
freqtrade -c path/far/far/away/config.json
|
freqtrade -c path/far/far/away/config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Per default, the bot loads the `config.json` configuration file from the current
|
||||||
|
working directory.
|
||||||
|
|
||||||
### How to use multiple configuration files?
|
### How to use multiple configuration files?
|
||||||
|
|
||||||
The bot allows you to use multiple configuration files by specifying multiple
|
The bot allows you to use multiple configuration files by specifying multiple
|
||||||
`-c/--config` configuration options in the command line. Configuration parameters
|
`-c/--config` options in the command line. Configuration parameters
|
||||||
defined in the last configuration file override parameters with the same name
|
defined in the latter configuration files override parameters with the same name
|
||||||
defined in the previous configuration file specified in the command line.
|
defined in the previous configuration files specified in the command line earlier.
|
||||||
|
|
||||||
For example, you can make a separate configuration file with your key and secrete
|
For example, you can make a separate configuration file with your key and secrete
|
||||||
for the Exchange you use for trading, specify default configuration file with
|
for the Exchange you use for trading, specify default configuration file with
|
||||||
@ -82,6 +90,29 @@ of your configuration in the project issues or in the Internet.
|
|||||||
See more details on this technique with examples in the documentation page on
|
See more details on this technique with examples in the documentation page on
|
||||||
[configuration](configuration.md).
|
[configuration](configuration.md).
|
||||||
|
|
||||||
|
### Where to store custom data
|
||||||
|
|
||||||
|
Freqtrade allows the creation of a user-data directory using `freqtrade create-userdir --userdir someDirectory`.
|
||||||
|
This directory will look as follows:
|
||||||
|
|
||||||
|
```
|
||||||
|
user_data/
|
||||||
|
├── backtest_results
|
||||||
|
├── data
|
||||||
|
├── hyperopts
|
||||||
|
├── hyperopts_results
|
||||||
|
├── plot
|
||||||
|
└── strategies
|
||||||
|
```
|
||||||
|
|
||||||
|
You can add the entry "user_data_dir" setting to your configuration, to always point your bot to this directory.
|
||||||
|
Alternatively, pass in `--userdir` to every command.
|
||||||
|
The bot will fail to start if the directory does not exist, but will create necessary subdirectories.
|
||||||
|
|
||||||
|
This directory should contain your custom strategies, custom hyperopts and hyperopt loss functions, backtesting historical data (downloaded using either backtesting command or the download script) and plot outputs.
|
||||||
|
|
||||||
|
It is recommended to use version control to keep track of changes to your strategies.
|
||||||
|
|
||||||
### How to use **--strategy**?
|
### How to use **--strategy**?
|
||||||
|
|
||||||
This parameter will allow you to load your custom strategy class.
|
This parameter will allow you to load your custom strategy class.
|
||||||
@ -110,6 +141,7 @@ Learn more about strategy file in
|
|||||||
|
|
||||||
This parameter allows you to add an additional strategy lookup path, which gets
|
This parameter allows you to add an additional strategy lookup path, which gets
|
||||||
checked before the default locations (The passed path must be a directory!):
|
checked before the default locations (The passed path must be a directory!):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
freqtrade --strategy AwesomeStrategy --strategy-path /some/directory
|
freqtrade --strategy AwesomeStrategy --strategy-path /some/directory
|
||||||
```
|
```
|
||||||
@ -163,9 +195,8 @@ optional arguments:
|
|||||||
Disable applying `max_open_trades` during backtest
|
Disable applying `max_open_trades` during backtest
|
||||||
(same as setting `max_open_trades` to a very high
|
(same as setting `max_open_trades` to a very high
|
||||||
number).
|
number).
|
||||||
-l, --live Use live data.
|
|
||||||
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
||||||
Provide a commaseparated list of strategies to
|
Provide a space-separated list of strategies to
|
||||||
backtest Please note that ticker-interval needs to be
|
backtest Please note that ticker-interval needs to be
|
||||||
set either in config or via command line. When using
|
set either in config or via command line. When using
|
||||||
this together with --export trades, the strategy-name
|
this together with --export trades, the strategy-name
|
||||||
@ -176,24 +207,16 @@ optional arguments:
|
|||||||
--export-filename PATH
|
--export-filename PATH
|
||||||
Save backtest results to this filename requires
|
Save backtest results to this filename requires
|
||||||
--export to be set as well Example --export-
|
--export to be set as well Example --export-
|
||||||
filename=user_data/backtest_data/backtest_today.json
|
filename=user_data/backtest_results/backtest_today.json
|
||||||
(default: user_data/backtest_data/backtest-
|
(default: user_data/backtest_results/backtest-
|
||||||
result.json)
|
result.json)
|
||||||
```
|
```
|
||||||
|
|
||||||
### How to use **--refresh-pairs-cached** parameter?
|
### Getting historic data for backtesting
|
||||||
|
|
||||||
The first time your run Backtesting, it will take the pairs you have
|
The first time your run Backtesting, you will need to download some historic data first.
|
||||||
set in your config file and download data from the Exchange.
|
This can be accomplished by using `freqtrade download-data`.
|
||||||
|
Check the corresponding [help page section](backtesting.md#Getting-data-for-backtesting-and-hyperopt) for more details
|
||||||
If for any reason you want to update your data set, you use
|
|
||||||
`--refresh-pairs-cached` to force Backtesting to update the data it has.
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
Use it only if you want to update your data set. You will not be able to come back to the previous version.
|
|
||||||
|
|
||||||
To test your strategy with latest data, we recommend continuing using
|
|
||||||
the parameter `-l` or `--live`.
|
|
||||||
|
|
||||||
## Hyperopt commands
|
## Hyperopt commands
|
||||||
|
|
||||||
@ -207,7 +230,7 @@ usage: freqtrade hyperopt [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE]
|
|||||||
[--customhyperopt NAME] [--hyperopt-path PATH]
|
[--customhyperopt NAME] [--hyperopt-path PATH]
|
||||||
[--eps] [-e INT]
|
[--eps] [-e INT]
|
||||||
[-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]]
|
[-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]]
|
||||||
[--dmmp] [--print-all] [-j JOBS]
|
[--dmmp] [--print-all] [--no-color] [-j JOBS]
|
||||||
[--random-state INT] [--min-trades INT] [--continue]
|
[--random-state INT] [--min-trades INT] [--continue]
|
||||||
[--hyperopt-loss NAME]
|
[--hyperopt-loss NAME]
|
||||||
|
|
||||||
@ -243,6 +266,8 @@ optional arguments:
|
|||||||
(same as setting `max_open_trades` to a very high
|
(same as setting `max_open_trades` to a very high
|
||||||
number).
|
number).
|
||||||
--print-all Print all results, not only the best ones.
|
--print-all Print all results, not only the best ones.
|
||||||
|
--no-color Disable colorization of hyperopt results. May be
|
||||||
|
useful if you are redirecting output to a file.
|
||||||
-j JOBS, --job-workers JOBS
|
-j JOBS, --job-workers JOBS
|
||||||
The number of concurrently running jobs for
|
The number of concurrently running jobs for
|
||||||
hyperoptimization (hyperopt worker processes). If -1
|
hyperoptimization (hyperopt worker processes). If -1
|
||||||
@ -256,17 +281,18 @@ optional arguments:
|
|||||||
--continue Continue hyperopt from previous runs. By default,
|
--continue Continue hyperopt from previous runs. By default,
|
||||||
temporary files will be removed and hyperopt will
|
temporary files will be removed and hyperopt will
|
||||||
start from scratch.
|
start from scratch.
|
||||||
--hyperopt-loss NAME
|
--hyperopt-loss NAME Specify the class name of the hyperopt loss function
|
||||||
Specify the class name of the hyperopt loss function
|
|
||||||
class (IHyperOptLoss). Different functions can
|
class (IHyperOptLoss). Different functions can
|
||||||
generate completely different results, since the
|
generate completely different results, since the
|
||||||
target for optimization is different. (default:
|
target for optimization is different. Built-in
|
||||||
`DefaultHyperOptLoss`).
|
Hyperopt-loss-functions are: DefaultHyperOptLoss,
|
||||||
|
OnlyProfitHyperOptLoss, SharpeHyperOptLoss.
|
||||||
|
(default: `DefaultHyperOptLoss`).
|
||||||
```
|
```
|
||||||
|
|
||||||
## Edge commands
|
## Edge commands
|
||||||
|
|
||||||
To know your trade expectacny and winrate against historical data, you can use Edge.
|
To know your trade expectancy and winrate against historical data, you can use Edge.
|
||||||
|
|
||||||
```
|
```
|
||||||
usage: freqtrade edge [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE]
|
usage: freqtrade edge [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE]
|
||||||
|
@ -1,15 +1,34 @@
|
|||||||
# Configure the bot
|
# Configure the bot
|
||||||
|
|
||||||
This page explains how to configure your `config.json` file.
|
This page explains how to configure the bot.
|
||||||
|
|
||||||
## Setup config.json
|
## The Freqtrade configuration file
|
||||||
|
|
||||||
We recommend to copy and use the `config.json.example` as a template
|
The bot uses a set of configuration parameters during its operation that all together conform the bot configuration. It normally reads its configuration from a file (Freqtrade configuration file).
|
||||||
|
|
||||||
|
Per default, the bot loads configuration from the `config.json` file located in the current working directory.
|
||||||
|
|
||||||
|
You can change the name of the configuration file used by the bot with the `-c/--config` command line option.
|
||||||
|
|
||||||
|
In some advanced use cases, multiple configuration files can be specified and used by the bot or the bot can read its configuration parameters from the process standard input stream.
|
||||||
|
|
||||||
|
If you used the [Quick start](installation.md/#quick-start) method for installing
|
||||||
|
the bot, the installation script should have already created the default configuration file (`config.json`) for you.
|
||||||
|
|
||||||
|
If default configuration file is not created we recommend you to copy and use the `config.json.example` as a template
|
||||||
for your bot configuration.
|
for your bot configuration.
|
||||||
|
|
||||||
The table below will list all configuration parameters.
|
The Freqtrade configuration file is to be written in the JSON format.
|
||||||
|
|
||||||
Mandatory Parameters are marked as **Required**.
|
Additionally to the standard JSON syntax, you may use one-line `// ...` and multi-line `/* ... */` comments in your configuration files and trailing commas in the lists of parameters.
|
||||||
|
|
||||||
|
Do not worry if you are not familiar with JSON format -- simply open the configuration file with an editor of your choice, make some changes to the parameters you need, save your changes and, finally, restart the bot or, if it was previously stopped, run it again with the changes you made to the configuration. The bot validates syntax of the configuration file at startup and will warn you if you made any errors editing it.
|
||||||
|
|
||||||
|
## Configuration parameters
|
||||||
|
|
||||||
|
The table below will list all configuration parameters available.
|
||||||
|
|
||||||
|
Mandatory parameters are marked as **Required**.
|
||||||
|
|
||||||
| Command | Default | Description |
|
| Command | Default | Description |
|
||||||
|----------|---------|-------------|
|
|----------|---------|-------------|
|
||||||
@ -53,6 +72,7 @@ Mandatory Parameters are marked as **Required**.
|
|||||||
| `experimental.use_sell_signal` | false | Use your sell strategy in addition of the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy).
|
| `experimental.use_sell_signal` | false | Use your sell strategy in addition of the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy).
|
||||||
| `experimental.sell_profit_only` | false | Waits until you have made a positive profit before taking a sell decision. [Strategy Override](#parameters-in-the-strategy).
|
| `experimental.sell_profit_only` | false | Waits until you have made a positive profit before taking a sell decision. [Strategy Override](#parameters-in-the-strategy).
|
||||||
| `experimental.ignore_roi_if_buy_signal` | false | Does not sell if the buy-signal is still active. Takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy).
|
| `experimental.ignore_roi_if_buy_signal` | false | Does not sell if the buy-signal is still active. Takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy).
|
||||||
|
| `experimental.block_bad_exchanges` | true | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now.
|
||||||
| `pairlist.method` | StaticPairList | Use static or dynamic volume-based pairlist. [More information below](#dynamic-pairlists).
|
| `pairlist.method` | StaticPairList | Use static or dynamic volume-based pairlist. [More information below](#dynamic-pairlists).
|
||||||
| `pairlist.config` | None | Additional configuration for dynamic pairlists. [More information below](#dynamic-pairlists).
|
| `pairlist.config` | None | Additional configuration for dynamic pairlists. [More information below](#dynamic-pairlists).
|
||||||
| `telegram.enabled` | true | **Required.** Enable or not the usage of Telegram.
|
| `telegram.enabled` | true | **Required.** Enable or not the usage of Telegram.
|
||||||
@ -71,6 +91,7 @@ Mandatory Parameters are marked as **Required**.
|
|||||||
| `internals.process_throttle_secs` | 5 | **Required.** Set the process throttle. Value in second.
|
| `internals.process_throttle_secs` | 5 | **Required.** Set the process throttle. Value in second.
|
||||||
| `internals.sd_notify` | false | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details.
|
| `internals.sd_notify` | false | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details.
|
||||||
| `logfile` | | Specify Logfile. Uses a rolling strategy of 10 files, with 1Mb per file.
|
| `logfile` | | Specify Logfile. Uses a rolling strategy of 10 files, with 1Mb per file.
|
||||||
|
| `user_data_dir` | cwd()/user_data | Directory containing user data. Defaults to `./user_data/`.
|
||||||
|
|
||||||
### Parameters in the strategy
|
### Parameters in the strategy
|
||||||
|
|
||||||
|
@ -1,42 +1,202 @@
|
|||||||
# Analyzing bot data
|
# Analyzing bot data with Jupyter notebooks
|
||||||
|
|
||||||
After performing backtests, or after running the bot for some time, it will be interesting to analyze the results your bot generated.
|
You can analyze the results of backtests and trading history easily using Jupyter notebooks. Sample notebooks are located at `user_data/notebooks/`.
|
||||||
|
|
||||||
A good way for this is using Jupyter (notebook or lab) - which provides an interactive environment to analyze the data.
|
## Pro tips
|
||||||
|
|
||||||
The following helpers will help you loading the data into Pandas DataFrames, and may also give you some starting points in analyzing the results.
|
* See [jupyter.org](https://jupyter.org/documentation) for usage instructions.
|
||||||
|
* Don't forget to start a Jupyter notebook server from within your conda or venv environment or use [nb_conda_kernels](https://github.com/Anaconda-Platform/nb_conda_kernels)*
|
||||||
|
* Copy the example notebook before use so your changes don't get clobbered with the next freqtrade update.
|
||||||
|
|
||||||
## Backtesting
|
## Fine print
|
||||||
|
|
||||||
To analyze your backtest results, you can [export the trades](#exporting-trades-to-file).
|
Some tasks don't work especially well in notebooks. For example, anything using asynchronous execution is a problem for Jupyter. Also, freqtrade's primary entry point is the shell cli, so using pure python in a notebook bypasses arguments that provide required objects and parameters to helper functions. You may need to set those values or create expected objects manually.
|
||||||
You can then load the trades to perform further analysis.
|
|
||||||
|
|
||||||
Freqtrade provides the `load_backtest_data()` helper function to easily load the backtest results, which takes the path to the the backtest-results file as parameter.
|
## Recommended workflow
|
||||||
|
|
||||||
``` python
|
| Task | Tool |
|
||||||
|
--- | ---
|
||||||
|
Bot operations | CLI
|
||||||
|
Repetitive tasks | Shell scripts
|
||||||
|
Data analysis & visualization | Notebook
|
||||||
|
|
||||||
|
1. Use the CLI to
|
||||||
|
* download historical data
|
||||||
|
* run a backtest
|
||||||
|
* run with real-time data
|
||||||
|
* export results
|
||||||
|
|
||||||
|
1. Collect these actions in shell scripts
|
||||||
|
* save complicated commands with arguments
|
||||||
|
* execute multi-step operations
|
||||||
|
* automate testing strategies and preparing data for analysis
|
||||||
|
|
||||||
|
1. Use a notebook to
|
||||||
|
* visualize data
|
||||||
|
* munge and plot to generate insights
|
||||||
|
|
||||||
|
## Example utility snippets
|
||||||
|
|
||||||
|
### Change directory to root
|
||||||
|
|
||||||
|
Jupyter notebooks execute from the notebook directory. The following snippet searches for the project root, so relative paths remain consistent.
|
||||||
|
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
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())
|
||||||
|
```
|
||||||
|
|
||||||
|
## Load existing objects into a Jupyter notebook
|
||||||
|
|
||||||
|
These examples assume that you have already generated data using the cli. They will allow you to drill deeper into your results, and perform analysis which otherwise would make the output very difficult to digest due to information overload.
|
||||||
|
|
||||||
|
### Load backtest results into a pandas dataframe
|
||||||
|
|
||||||
|
```python
|
||||||
from freqtrade.data.btanalysis import load_backtest_data
|
from freqtrade.data.btanalysis import load_backtest_data
|
||||||
df = load_backtest_data("user_data/backtest-result.json")
|
|
||||||
|
# Load backtest results
|
||||||
|
df = load_backtest_data("user_data/backtest_results/backtest-result.json")
|
||||||
|
|
||||||
# Show value-counts per pair
|
# Show value-counts per pair
|
||||||
df.groupby("pair")["sell_reason"].value_counts()
|
df.groupby("pair")["sell_reason"].value_counts()
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This will allow you to drill deeper into your backtest results, and perform analysis which otherwise would make the regular backtest-output very difficult to digest due to information overload.
|
### Load live trading results into a pandas dataframe
|
||||||
|
|
||||||
If you have some ideas for interesting / helpful backtest data analysis ideas, please submit a Pull Request so the community can benefit from it.
|
|
||||||
|
|
||||||
## Live data
|
|
||||||
|
|
||||||
To analyze the trades your bot generated, you can load them to a DataFrame as follows:
|
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
from freqtrade.data.btanalysis import load_trades_from_db
|
from freqtrade.data.btanalysis import load_trades_from_db
|
||||||
|
|
||||||
|
# Fetch trades from database
|
||||||
df = load_trades_from_db("sqlite:///tradesv3.sqlite")
|
df = load_trades_from_db("sqlite:///tradesv3.sqlite")
|
||||||
|
|
||||||
|
# Display results
|
||||||
df.groupby("pair")["sell_reason"].value_counts()
|
df.groupby("pair")["sell_reason"].value_counts()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Load multiple configuration files
|
||||||
|
|
||||||
|
This option can be useful to inspect the results of passing in multiple configs
|
||||||
|
|
||||||
|
``` python
|
||||||
|
import json
|
||||||
|
from freqtrade.configuration import Configuration
|
||||||
|
|
||||||
|
# Load config from multiple files
|
||||||
|
config = Configuration.from_files(["config1.json", "config2.json"])
|
||||||
|
|
||||||
|
# Show the config in memory
|
||||||
|
print(json.dumps(config, indent=1))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Load exchange data to a pandas dataframe
|
||||||
|
|
||||||
|
This loads candle data to a dataframe
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pathlib import Path
|
||||||
|
from freqtrade.data.history import load_pair_history
|
||||||
|
|
||||||
|
# Load data using values passed to function
|
||||||
|
ticker_interval = "5m"
|
||||||
|
data_location = Path('user_data', 'data', 'bitrex')
|
||||||
|
pair = "BTC_USDT"
|
||||||
|
candles = load_pair_history(datadir=data_location,
|
||||||
|
ticker_interval=ticker_interval,
|
||||||
|
pair=pair)
|
||||||
|
|
||||||
|
# Confirm success
|
||||||
|
print(f"Loaded len(candles) rows of data for {pair} from {data_location}")
|
||||||
|
candles.head()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Strategy debugging example
|
||||||
|
|
||||||
|
Debugging a strategy can be time-consuming. FreqTrade offers helper functions to visualize raw data.
|
||||||
|
|
||||||
|
### Define variables used in analyses
|
||||||
|
|
||||||
|
You can override strategy settings as demonstrated below.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Customize these according to your needs.
|
||||||
|
|
||||||
|
# Define some constants
|
||||||
|
ticker_interval = "5m"
|
||||||
|
# Name of the strategy class
|
||||||
|
strategy_name = 'TestStrategy'
|
||||||
|
# Path to user data
|
||||||
|
user_data_dir = 'user_data'
|
||||||
|
# Location of the strategy
|
||||||
|
strategy_location = Path(user_data_dir, 'strategies')
|
||||||
|
# Location of the data
|
||||||
|
data_location = Path(user_data_dir, 'data', 'binance')
|
||||||
|
# Pair to analyze - Only use one pair here
|
||||||
|
pair = "BTC_USDT"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Load exchange data
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pathlib import Path
|
||||||
|
from freqtrade.data.history import load_pair_history
|
||||||
|
|
||||||
|
# Load data using values set above
|
||||||
|
candles = load_pair_history(datadir=data_location,
|
||||||
|
ticker_interval=ticker_interval,
|
||||||
|
pair=pair)
|
||||||
|
|
||||||
|
# Confirm success
|
||||||
|
print(f"Loaded {len(candles)} rows of data for {pair} from {data_location}")
|
||||||
|
candles.head()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Load and run strategy
|
||||||
|
|
||||||
|
* Rerun each time the strategy file is changed
|
||||||
|
|
||||||
|
```python
|
||||||
|
from freqtrade.resolvers import StrategyResolver
|
||||||
|
|
||||||
|
# Load strategy using values set above
|
||||||
|
strategy = StrategyResolver({'strategy': strategy_name,
|
||||||
|
'user_data_dir': user_data_dir,
|
||||||
|
'strategy_path': strategy_location}).strategy
|
||||||
|
|
||||||
|
# Generate buy/sell signals using strategy
|
||||||
|
df = strategy.analyze_ticker(candles, {'pair': pair})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Display the trade details
|
||||||
|
|
||||||
|
* Note that using `data.tail()` is preferable to `data.head()` as most indicators have some "startup" data at the top of the dataframe.
|
||||||
|
* Some possible problems
|
||||||
|
* Columns with NaN values at the end of the dataframe
|
||||||
|
* Columns used in `crossed*()` functions with completely different units
|
||||||
|
* Comparison with full backtest
|
||||||
|
* having 200 buy signals as output for one pair from `analyze_ticker()` does not necessarily mean that 200 trades will be made during backtesting.
|
||||||
|
* Assuming you use only one condition such as, `df['rsi'] < 30` as buy condition, this will generate multiple "buy" signals for each pair in sequence (until rsi returns > 29). The bot will only buy on the first of these signals (and also only if a trade-slot ("max_open_trades") is still available), or on one of the middle signals, as soon as a "slot" becomes available.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Report results
|
||||||
|
print(f"Generated {df['buy'].sum()} buy signals")
|
||||||
|
data = df.set_index('date', drop=True)
|
||||||
|
data.tail()
|
||||||
```
|
```
|
||||||
|
|
||||||
Feel free to submit an issue or Pull Request enhancing this document if you would like to share ideas on how to best analyze the data.
|
Feel free to submit an issue or Pull Request enhancing this document if you would like to share ideas on how to best analyze the data.
|
||||||
|
@ -4,12 +4,15 @@ This page contains description of the command line arguments, configuration para
|
|||||||
and the bot features that were declared as DEPRECATED by the bot development team
|
and the bot features that were declared as DEPRECATED by the bot development team
|
||||||
and are no longer supported. Please avoid their usage in your configuration.
|
and are no longer supported. Please avoid their usage in your configuration.
|
||||||
|
|
||||||
### the `--live` command line option
|
## Deprecated
|
||||||
|
|
||||||
`--live` in the context of backtesting allows to download the latest tick data for backtesting.
|
### the `--refresh-pairs-cached` command line option
|
||||||
Since this only downloads one set of data (by default 500 candles) - this is not really suitable for extendet backtesting, and has therefore been deprecated.
|
|
||||||
|
|
||||||
This command was deprecated in `2019.6-dev` and will be removed after the next release.
|
`--refresh-pairs-cached` in the context of backtesting, hyperopt and edge allows to refresh candle data for backtesting.
|
||||||
|
Since this leads to much confusion, and slows down backtesting (while not being part of backtesting) this has been singled out
|
||||||
|
as a seperate freqtrade subcommand `freqtrade download-data`.
|
||||||
|
|
||||||
|
This command line option was deprecated in `2019.7-dev` and will be removed after the next release.
|
||||||
|
|
||||||
## Removed features
|
## Removed features
|
||||||
|
|
||||||
@ -17,3 +20,9 @@ This command was deprecated in `2019.6-dev` and will be removed after the next r
|
|||||||
|
|
||||||
This command line option was deprecated in 2018 and removed freqtrade 2019.6-dev (develop branch)
|
This command line option was deprecated in 2018 and removed freqtrade 2019.6-dev (develop branch)
|
||||||
and in freqtrade 2019.7 (master branch).
|
and in freqtrade 2019.7 (master branch).
|
||||||
|
|
||||||
|
### the `--live` command line option
|
||||||
|
|
||||||
|
`--live` in the context of backtesting allowed to download the latest tick data for backtesting.
|
||||||
|
Did only download the latest 500 candles, so was ineffective in getting good backtest data.
|
||||||
|
Removed in 2019-7-dev (develop branch) and in freqtrade 2019-8 (master branch)
|
||||||
|
@ -12,11 +12,34 @@ Special fields for the documentation (like Note boxes, ...) can be found [here](
|
|||||||
|
|
||||||
## Developer setup
|
## Developer setup
|
||||||
|
|
||||||
To configure a development environment, use best use the `setup.sh` script and answer "y" when asked "Do you want to install dependencies for dev [y/N]? ".
|
To configure a development environment, best use the `setup.sh` script and answer "y" when asked "Do you want to install dependencies for dev [y/N]? ".
|
||||||
Alternatively (if your system is not supported by the setup.sh script), follow the manual installation process and run `pip3 install -r requirements-dev.txt`.
|
Alternatively (if your system is not supported by the setup.sh script), follow the manual installation process and run `pip3 install -e .[all]`.
|
||||||
|
|
||||||
This will install all required tools for development, including `pytest`, `flake8`, `mypy`, and `coveralls`.
|
This will install all required tools for development, including `pytest`, `flake8`, `mypy`, and `coveralls`.
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
New code should be covered by basic unittests. Depending on the complexity of the feature, Reviewers may request more in-depth unittests.
|
||||||
|
If necessary, the Freqtrade team can assist and give guidance with writing good tests (however please don't expect anyone to write the tests for you).
|
||||||
|
|
||||||
|
#### Checking log content in tests
|
||||||
|
|
||||||
|
Freqtrade uses 2 main methods to check log content in tests, `log_has()` and `log_has_re()` (to check using regex, in case of dynamic log-messages).
|
||||||
|
These are available from `conftest.py` and can be imported in any test module.
|
||||||
|
|
||||||
|
A sample check looks as follows:
|
||||||
|
|
||||||
|
``` python
|
||||||
|
from freqtrade.tests.conftest import log_has, log_has_re
|
||||||
|
|
||||||
|
def test_method_to_test(caplog):
|
||||||
|
method_to_test()
|
||||||
|
|
||||||
|
assert log_has("This event happened", caplog)
|
||||||
|
# Check regex with trailing number ...
|
||||||
|
assert log_has_re(r"This dynamic event happened and produced \d+", caplog)
|
||||||
|
```
|
||||||
|
|
||||||
## Modules
|
## Modules
|
||||||
|
|
||||||
### Dynamic Pairlist
|
### Dynamic Pairlist
|
||||||
@ -156,6 +179,8 @@ git log --oneline --no-decorate --no-merges master..develop
|
|||||||
|
|
||||||
### Create github release / tag
|
### Create github release / tag
|
||||||
|
|
||||||
|
Once the PR against master is merged (best right after merging):
|
||||||
|
|
||||||
* Use the button "Draft a new release" in the Github UI (subsection releases)
|
* Use the button "Draft a new release" in the Github UI (subsection releases)
|
||||||
* Use the version-number specified as tag.
|
* Use the version-number specified as tag.
|
||||||
* Use "master" as reference (this step comes after the above PR is merged).
|
* Use "master" as reference (this step comes after the above PR is merged).
|
||||||
|
@ -26,6 +26,10 @@ To update the image, simply run the above commands again and restart your runnin
|
|||||||
|
|
||||||
Should you require additional libraries, please [build the image yourself](#build-your-own-docker-image).
|
Should you require additional libraries, please [build the image yourself](#build-your-own-docker-image).
|
||||||
|
|
||||||
|
!!! Note Docker image update frequency
|
||||||
|
The official docker images with tags `master`, `develop` and `latest` are automatically rebuild once a week to keep the base image uptodate.
|
||||||
|
In addition to that, every merge to `develop` will trigger a rebuild for `develop` and `latest`.
|
||||||
|
|
||||||
### Prepare the configuration files
|
### Prepare the configuration files
|
||||||
|
|
||||||
Even though you will use docker, you'll still need some files from the github repository.
|
Even though you will use docker, you'll still need some files from the github repository.
|
||||||
|
@ -234,9 +234,8 @@ An example of its output:
|
|||||||
|
|
||||||
### Update cached pairs with the latest data
|
### Update cached pairs with the latest data
|
||||||
|
|
||||||
```bash
|
Edge requires historic data the same way as backtesting does.
|
||||||
freqtrade edge --refresh-pairs-cached
|
Please refer to the [download section](backtesting.md#Getting-data-for-backtesting-and-hyperopt) of the documentation for details.
|
||||||
```
|
|
||||||
|
|
||||||
### Precising stoploss range
|
### Precising stoploss range
|
||||||
|
|
||||||
|
10
docs/faq.md
10
docs/faq.md
@ -45,6 +45,16 @@ the tutorial [here|Testing-new-strategies-with-Hyperopt](bot-usage.md#hyperopt-c
|
|||||||
|
|
||||||
You can use the `/forcesell all` command from Telegram.
|
You can use the `/forcesell all` command from Telegram.
|
||||||
|
|
||||||
|
### I get the message "RESTRICTED_MARKET"
|
||||||
|
|
||||||
|
Currently known to happen for US Bittrex users.
|
||||||
|
Bittrex split its exchange into US and International versions.
|
||||||
|
The International version has more pairs available, however the API always returns all pairs, so there is currently no automated way to detect if you're affected by the restriction.
|
||||||
|
|
||||||
|
If you have restricted pairs in your whitelist, you'll get a warning message in the log on FreqTrade startup for each restricted pair.
|
||||||
|
If you're an "International" Customer on the Bittrex exchange, then this warning will probably not impact you.
|
||||||
|
If you're a US customer, the bot will fail to create orders for these pairs, and you should remove them from your Whitelist.
|
||||||
|
|
||||||
## Hyperopt module
|
## Hyperopt module
|
||||||
|
|
||||||
### How many epoch do I need to get a good Hyperopt result?
|
### How many epoch do I need to get a good Hyperopt result?
|
||||||
|
@ -18,19 +18,24 @@ Configuring hyperopt is similar to writing your own strategy, and many tasks wil
|
|||||||
|
|
||||||
### Checklist on all tasks / possibilities in hyperopt
|
### Checklist on all tasks / possibilities in hyperopt
|
||||||
|
|
||||||
Depending on the space you want to optimize, only some of the below are required.
|
Depending on the space you want to optimize, only some of the below are required:
|
||||||
|
|
||||||
* fill `populate_indicators` - probably a copy from your strategy
|
* fill `populate_indicators` - probably a copy from your strategy
|
||||||
* fill `buy_strategy_generator` - for buy signal optimization
|
* fill `buy_strategy_generator` - for buy signal optimization
|
||||||
* fill `indicator_space` - for buy signal optimzation
|
* fill `indicator_space` - for buy signal optimzation
|
||||||
* fill `sell_strategy_generator` - for sell signal optimization
|
* fill `sell_strategy_generator` - for sell signal optimization
|
||||||
* fill `sell_indicator_space` - for sell signal optimzation
|
* fill `sell_indicator_space` - for sell signal optimzation
|
||||||
* fill `roi_space` - for ROI optimization
|
|
||||||
* fill `generate_roi_table` - for ROI optimization (if you need more than 3 entries)
|
Optional, but recommended:
|
||||||
* fill `stoploss_space` - stoploss optimization
|
|
||||||
* Optional but recommended
|
* copy `populate_buy_trend` from your strategy - otherwise default-strategy will be used
|
||||||
* copy `populate_buy_trend` from your strategy - otherwise default-strategy will be used
|
* copy `populate_sell_trend` from your strategy - otherwise default-strategy will be used
|
||||||
* copy `populate_sell_trend` from your strategy - otherwise default-strategy will be used
|
|
||||||
|
Rarely you may also need to override:
|
||||||
|
|
||||||
|
* `roi_space` - for custom ROI optimization (if you need the ranges for the ROI parameters in the optimization hyperspace that differ from default)
|
||||||
|
* `generate_roi_table` - for custom ROI optimization (if you need more than 4 entries in the ROI table)
|
||||||
|
* `stoploss_space` - for custom stoploss optimization (if you need the range for the stoploss parameter in the optimization hyperspace that differs from default)
|
||||||
|
|
||||||
### 1. Install a Custom Hyperopt File
|
### 1. Install a Custom Hyperopt File
|
||||||
|
|
||||||
@ -159,7 +164,11 @@ By default, FreqTrade uses a loss function, which has been with freqtrade since
|
|||||||
A different loss function can be specified by using the `--hyperopt-loss <Class-name>` argument.
|
A different loss function can be specified by using the `--hyperopt-loss <Class-name>` argument.
|
||||||
This class should be in its own file within the `user_data/hyperopts/` directory.
|
This class should be in its own file within the `user_data/hyperopts/` directory.
|
||||||
|
|
||||||
Currently, the following loss functions are builtin: `DefaultHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function), `SharpeHyperOptLoss` (optimizes Sharpe Ratio calculated on the trade returns) and `OnlyProfitHyperOptLoss` (which takes only amount of profit into consideration).
|
Currently, the following loss functions are builtin:
|
||||||
|
|
||||||
|
* `DefaultHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function)
|
||||||
|
* `OnlyProfitHyperOptLoss` (which takes only amount of profit into consideration)
|
||||||
|
* `SharpeHyperOptLoss` (optimizes Sharpe Ratio calculated on the trade returns)
|
||||||
|
|
||||||
### Creating and using a custom loss function
|
### Creating and using a custom loss function
|
||||||
|
|
||||||
@ -303,8 +312,10 @@ Given the following result from hyperopt:
|
|||||||
|
|
||||||
```
|
```
|
||||||
Best result:
|
Best result:
|
||||||
135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins.
|
|
||||||
with values:
|
44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367
|
||||||
|
|
||||||
|
Buy hyperspace params:
|
||||||
{ 'adx-value': 44,
|
{ 'adx-value': 44,
|
||||||
'rsi-value': 29,
|
'rsi-value': 29,
|
||||||
'adx-enabled': False,
|
'adx-enabled': False,
|
||||||
@ -341,27 +352,25 @@ def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
|||||||
return dataframe
|
return dataframe
|
||||||
```
|
```
|
||||||
|
|
||||||
|
By default, hyperopt prints colorized results -- epochs with positive profit are printed in the green color. This highlighting helps you find epochs that can be interesting for later analysis. Epochs with zero total profit or with negative profits (losses) are printed in the normal color. If you do not need colorization of results (for instance, when you are redirecting hyperopt output to a file) you can switch colorization off by specifying the `--no-color` option in the command line.
|
||||||
|
|
||||||
|
You can use the `--print-all` command line option if you would like to see all results in the hyperopt output, not only the best ones. When `--print-all` is used, current best results are also colorized by default -- they are printed in bold (bright) style. This can also be switched off with the `--no-color` command line option.
|
||||||
|
|
||||||
### Understand Hyperopt ROI results
|
### Understand Hyperopt ROI results
|
||||||
|
|
||||||
If you are optimizing ROI, you're result will look as follows and include a ROI table.
|
If you are optimizing ROI (i.e. if optimization search-space contains 'all' or 'roi'), your result will look as follows and include a ROI table:
|
||||||
|
|
||||||
```
|
```
|
||||||
Best result:
|
Best result:
|
||||||
135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins.
|
|
||||||
with values:
|
44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367
|
||||||
|
|
||||||
|
Buy hyperspace params:
|
||||||
{ 'adx-value': 44,
|
{ 'adx-value': 44,
|
||||||
'rsi-value': 29,
|
'rsi-value': 29,
|
||||||
'adx-enabled': false,
|
'adx-enabled': False,
|
||||||
'rsi-enabled': True,
|
'rsi-enabled': True,
|
||||||
'trigger': 'bb_lower',
|
'trigger': 'bb_lower'}
|
||||||
'roi_t1': 40,
|
|
||||||
'roi_t2': 57,
|
|
||||||
'roi_t3': 21,
|
|
||||||
'roi_p1': 0.03634636907306948,
|
|
||||||
'roi_p2': 0.055237357937802885,
|
|
||||||
'roi_p3': 0.015163796015548354,
|
|
||||||
'stoploss': -0.37996664668703606
|
|
||||||
}
|
|
||||||
ROI table:
|
ROI table:
|
||||||
{ 0: 0.10674752302642071,
|
{ 0: 0.10674752302642071,
|
||||||
21: 0.09158372701087236,
|
21: 0.09158372701087236,
|
||||||
@ -372,14 +381,49 @@ ROI table:
|
|||||||
This would translate to the following ROI table:
|
This would translate to the following ROI table:
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
minimal_roi = {
|
minimal_roi = {
|
||||||
"118": 0,
|
"118": 0,
|
||||||
"78": 0.0363463,
|
"78": 0.0363,
|
||||||
"21": 0.0915,
|
"21": 0.0915,
|
||||||
"0": 0.106
|
"0": 0.106
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps) with the values that can vary in the following ranges:
|
||||||
|
|
||||||
|
| # | minutes | ROI percentage |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | always 0 | 0.03...0.31 |
|
||||||
|
| 2 | 10...40 | 0.02...0.11 |
|
||||||
|
| 3 | 20...100 | 0.01...0.04 |
|
||||||
|
| 4 | 30...220 | always 0 |
|
||||||
|
|
||||||
|
This structure of the ROI table is sufficient in most cases. Override the `roi_space()` method defining the ranges desired if you need components of the ROI tables to vary in other ranges.
|
||||||
|
|
||||||
|
Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization in these methods if you need a different structure of the ROI table or other amount of rows (steps) in the ROI tables.
|
||||||
|
|
||||||
|
### Understand Hyperopt Stoploss results
|
||||||
|
|
||||||
|
If you are optimizing stoploss values (i.e. if optimization search-space contains 'all' or 'stoploss'), your result will look as follows and include stoploss:
|
||||||
|
|
||||||
|
```
|
||||||
|
Best result:
|
||||||
|
|
||||||
|
44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367
|
||||||
|
|
||||||
|
Buy hyperspace params:
|
||||||
|
{ 'adx-value': 44,
|
||||||
|
'rsi-value': 29,
|
||||||
|
'adx-enabled': False,
|
||||||
|
'rsi-enabled': True,
|
||||||
|
'trigger': 'bb_lower'}
|
||||||
|
Stoploss: -0.37996664668703606
|
||||||
|
```
|
||||||
|
|
||||||
|
If you are optimizing stoploss values, Freqtrade creates the 'stoploss' optimization hyperspace for you. By default, the stoploss values in that hyperspace can vary in the range -0.5...-0.02, which is sufficient in most cases.
|
||||||
|
|
||||||
|
Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization.
|
||||||
|
|
||||||
### Validate backtesting results
|
### Validate backtesting results
|
||||||
|
|
||||||
Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected.
|
Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected.
|
||||||
|
@ -219,6 +219,17 @@ as the watchdog.
|
|||||||
|
|
||||||
------
|
------
|
||||||
|
|
||||||
|
## Using Conda
|
||||||
|
|
||||||
|
Freqtrade can also be installed using Anaconda (or Miniconda).
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
conda env create -f environment.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
This requires the [ta-lib](#1-install-ta-lib) C-library to be installed first.
|
||||||
|
|
||||||
## Windows
|
## Windows
|
||||||
|
|
||||||
We recommend that Windows users use [Docker](docker.md) as this will work much easier and smoother (also more secure).
|
We recommend that Windows users use [Docker](docker.md) as this will work much easier and smoother (also more secure).
|
||||||
|
@ -15,7 +15,7 @@ pip install -U -r requirements-plot.txt
|
|||||||
Usage for the price plotter:
|
Usage for the price plotter:
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
python3 script/plot_dataframe.py [-h] [-p pairs] [--live]
|
python3 script/plot_dataframe.py [-h] [-p pairs]
|
||||||
```
|
```
|
||||||
|
|
||||||
Example
|
Example
|
||||||
@ -41,20 +41,12 @@ To plot multiple pairs, separate them with a comma:
|
|||||||
python3 scripts/plot_dataframe.py -p BTC/ETH,XRP/ETH
|
python3 scripts/plot_dataframe.py -p BTC/ETH,XRP/ETH
|
||||||
```
|
```
|
||||||
|
|
||||||
To plot the current live price use the `--live` flag:
|
|
||||||
|
|
||||||
``` bash
|
|
||||||
python3 scripts/plot_dataframe.py -p BTC/ETH --live
|
|
||||||
```
|
|
||||||
|
|
||||||
To plot a timerange (to zoom in):
|
To plot a timerange (to zoom in):
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
python3 scripts/plot_dataframe.py -p BTC/ETH --timerange=100-200
|
python3 scripts/plot_dataframe.py -p BTC/ETH --timerange=20180801-20180805
|
||||||
```
|
```
|
||||||
|
|
||||||
Timerange doesn't work with live data.
|
|
||||||
|
|
||||||
To plot trades stored in a database use `--db-url` argument:
|
To plot trades stored in a database use `--db-url` argument:
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
@ -64,7 +56,7 @@ python3 scripts/plot_dataframe.py --db-url sqlite:///tradesv3.dry_run.sqlite -p
|
|||||||
To plot trades from a backtesting result, use `--export-filename <filename>`
|
To plot trades from a backtesting result, use `--export-filename <filename>`
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
python3 scripts/plot_dataframe.py --export-filename user_data/backtest_data/backtest-result.json -p BTC/ETH
|
python3 scripts/plot_dataframe.py --export-filename user_data/backtest_results/backtest-result.json -p BTC/ETH
|
||||||
```
|
```
|
||||||
|
|
||||||
To plot a custom strategy the strategy should have first be backtested.
|
To plot a custom strategy the strategy should have first be backtested.
|
||||||
|
@ -1 +1 @@
|
|||||||
mkdocs-material==3.1.0
|
mkdocs-material==4.4.0
|
@ -274,27 +274,24 @@ Please always check the mode of operation to select the correct method to get da
|
|||||||
|
|
||||||
#### Possible options for DataProvider
|
#### Possible options for DataProvider
|
||||||
|
|
||||||
- `available_pairs` - Property with tuples listing cached pairs with their intervals. (pair, interval)
|
- `available_pairs` - Property with tuples listing cached pairs with their intervals (pair, interval).
|
||||||
- `ohlcv(pair, ticker_interval)` - Currently cached ticker data for all pairs in the whitelist, returns DataFrame or empty DataFrame
|
- `ohlcv(pair, ticker_interval)` - Currently cached ticker data for the pair, returns DataFrame or empty DataFrame.
|
||||||
- `historic_ohlcv(pair, ticker_interval)` - Data stored on disk
|
- `historic_ohlcv(pair, ticker_interval)` - Returns historical data stored on disk.
|
||||||
|
- `get_pair_dataframe(pair, ticker_interval)` - This is a universal method, which returns either historical data (for backtesting) or cached live data (for the Dry-Run and Live-Run modes).
|
||||||
- `runmode` - Property containing the current runmode.
|
- `runmode` - Property containing the current runmode.
|
||||||
|
|
||||||
#### ohlcv / historic_ohlcv
|
#### Example: fetch live ohlcv / historic data for the first informative pair
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
if self.dp:
|
if self.dp:
|
||||||
if self.dp.runmode in ('live', 'dry_run'):
|
inf_pair, inf_timeframe = self.informative_pairs()[0]
|
||||||
if (f'{self.stake_currency}/BTC', self.ticker_interval) in self.dp.available_pairs:
|
informative = self.dp.get_pair_dataframe(pair=inf_pair,
|
||||||
data_eth = self.dp.ohlcv(pair='{self.stake_currency}/BTC',
|
ticker_interval=inf_timeframe)
|
||||||
ticker_interval=self.ticker_interval)
|
|
||||||
else:
|
|
||||||
# Get historic ohlcv data (cached on disk).
|
|
||||||
history_eth = self.dp.historic_ohlcv(pair='{self.stake_currency}/BTC',
|
|
||||||
ticker_interval='1h')
|
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Warning Warning about backtesting
|
!!! Warning Warning about backtesting
|
||||||
Be carefull when using dataprovider in backtesting. `historic_ohlcv()` provides the full time-range in one go,
|
Be carefull when using dataprovider in backtesting. `historic_ohlcv()` (and `get_pair_dataframe()`
|
||||||
|
for the backtesting runmode) provides the full time-range in one go,
|
||||||
so please be aware of it and make sure to not "look into the future" to avoid surprises when running in dry/live mode).
|
so please be aware of it and make sure to not "look into the future" to avoid surprises when running in dry/live mode).
|
||||||
|
|
||||||
!!! Warning Warning in hyperopt
|
!!! Warning Warning in hyperopt
|
||||||
@ -309,7 +306,9 @@ if self.dp:
|
|||||||
dataframe['best_bid'] = ob['bids'][0][0]
|
dataframe['best_bid'] = ob['bids'][0][0]
|
||||||
dataframe['best_ask'] = ob['asks'][0][0]
|
dataframe['best_ask'] = ob['asks'][0][0]
|
||||||
```
|
```
|
||||||
!Warning The order book is not part of the historic data which means backtesting and hyperopt will not work if this
|
|
||||||
|
!!! Warning
|
||||||
|
The order book is not part of the historic data which means backtesting and hyperopt will not work if this
|
||||||
method is used.
|
method is used.
|
||||||
|
|
||||||
#### Available Pairs
|
#### Available Pairs
|
||||||
@ -320,7 +319,6 @@ if self.dp:
|
|||||||
print(f"available {pair}, {ticker}")
|
print(f"available {pair}, {ticker}")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
#### Get data for non-tradeable pairs
|
#### Get data for non-tradeable pairs
|
||||||
|
|
||||||
Data for additional, informative pairs (reference pairs) can be beneficial for some strategies.
|
Data for additional, informative pairs (reference pairs) can be beneficial for some strategies.
|
||||||
|
59
environment.yml
Normal file
59
environment.yml
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
name: freqtrade
|
||||||
|
channels:
|
||||||
|
- defaults
|
||||||
|
- conda-forge
|
||||||
|
dependencies:
|
||||||
|
# Required for app
|
||||||
|
- python>=3.6
|
||||||
|
- pip
|
||||||
|
- wheel
|
||||||
|
- numpy
|
||||||
|
- pandas
|
||||||
|
- scipy
|
||||||
|
- SQLAlchemy
|
||||||
|
- scikit-learn
|
||||||
|
- arrow
|
||||||
|
- requests
|
||||||
|
- urllib3
|
||||||
|
- wrapt
|
||||||
|
- joblib
|
||||||
|
- jsonschema
|
||||||
|
- tabulate
|
||||||
|
- python-rapidjson
|
||||||
|
- filelock
|
||||||
|
- flask
|
||||||
|
- python-dotenv
|
||||||
|
- cachetools
|
||||||
|
- scikit-optimize
|
||||||
|
- python-telegram-bot
|
||||||
|
# Optional for plotting
|
||||||
|
- plotly
|
||||||
|
# Optional for development
|
||||||
|
- flake8
|
||||||
|
- pytest
|
||||||
|
- pytest-mock
|
||||||
|
- pytest-asyncio
|
||||||
|
- pytest-cov
|
||||||
|
- coveralls
|
||||||
|
- mypy
|
||||||
|
# Useful for jupyter
|
||||||
|
- jupyter
|
||||||
|
- ipykernel
|
||||||
|
- isort
|
||||||
|
- yapf
|
||||||
|
- pip:
|
||||||
|
# Required for app
|
||||||
|
- cython
|
||||||
|
- coinmarketcap
|
||||||
|
- ccxt
|
||||||
|
- TA-Lib
|
||||||
|
- py_find_1st
|
||||||
|
- sdnotify
|
||||||
|
# Optional for develpment
|
||||||
|
- flake8-tidy-imports
|
||||||
|
- flake8-type-annotations
|
||||||
|
- pytest-random-order
|
||||||
|
- -e .
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
|||||||
""" FreqTrade bot """
|
""" FreqTrade bot """
|
||||||
__version__ = '2019.7'
|
__version__ = '2019.8'
|
||||||
|
|
||||||
|
|
||||||
class DependencyException(Exception):
|
class DependencyException(Exception):
|
||||||
|
@ -1,2 +1,4 @@
|
|||||||
from freqtrade.configuration.arguments import Arguments, TimeRange # noqa: F401
|
from freqtrade.configuration.arguments import Arguments # noqa: F401
|
||||||
|
from freqtrade.configuration.timerange import TimeRange # noqa: F401
|
||||||
from freqtrade.configuration.configuration import Configuration # noqa: F401
|
from freqtrade.configuration.configuration import Configuration # noqa: F401
|
||||||
|
from freqtrade.configuration.config_validation import validate_config_consistency # noqa: F401
|
||||||
|
@ -2,14 +2,12 @@
|
|||||||
This module contains the argument manager class
|
This module contains the argument manager class
|
||||||
"""
|
"""
|
||||||
import argparse
|
import argparse
|
||||||
import re
|
from typing import List, Optional
|
||||||
from typing import List, NamedTuple, Optional
|
|
||||||
|
|
||||||
import arrow
|
|
||||||
from freqtrade.configuration.cli_options import AVAILABLE_CLI_OPTIONS
|
from freqtrade.configuration.cli_options import AVAILABLE_CLI_OPTIONS
|
||||||
from freqtrade import constants
|
from freqtrade import constants
|
||||||
|
|
||||||
ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir"]
|
ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_data_dir"]
|
||||||
|
|
||||||
ARGS_STRATEGY = ["strategy", "strategy_path"]
|
ARGS_STRATEGY = ["strategy", "strategy_path"]
|
||||||
|
|
||||||
@ -19,11 +17,12 @@ ARGS_COMMON_OPTIMIZE = ["ticker_interval", "timerange",
|
|||||||
"max_open_trades", "stake_amount", "refresh_pairs"]
|
"max_open_trades", "stake_amount", "refresh_pairs"]
|
||||||
|
|
||||||
ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions",
|
ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions",
|
||||||
"live", "strategy_list", "export", "exportfilename"]
|
"strategy_list", "export", "exportfilename"]
|
||||||
|
|
||||||
ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
|
ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
|
||||||
"position_stacking", "epochs", "spaces",
|
"position_stacking", "epochs", "spaces",
|
||||||
"use_max_market_positions", "print_all", "hyperopt_jobs",
|
"use_max_market_positions", "print_all",
|
||||||
|
"print_colorized", "print_json", "hyperopt_jobs",
|
||||||
"hyperopt_random_state", "hyperopt_min_trades",
|
"hyperopt_random_state", "hyperopt_min_trades",
|
||||||
"hyperopt_continue", "hyperopt_loss"]
|
"hyperopt_continue", "hyperopt_loss"]
|
||||||
|
|
||||||
@ -31,27 +30,19 @@ ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"]
|
|||||||
|
|
||||||
ARGS_LIST_EXCHANGES = ["print_one_column"]
|
ARGS_LIST_EXCHANGES = ["print_one_column"]
|
||||||
|
|
||||||
ARGS_DOWNLOADER = ARGS_COMMON + ["pairs", "pairs_file", "days", "exchange", "timeframes", "erase"]
|
ARGS_CREATE_USERDIR = ["user_data_dir"]
|
||||||
|
|
||||||
|
ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "exchange", "timeframes", "erase"]
|
||||||
|
|
||||||
ARGS_PLOT_DATAFRAME = (ARGS_COMMON + ARGS_STRATEGY +
|
ARGS_PLOT_DATAFRAME = (ARGS_COMMON + ARGS_STRATEGY +
|
||||||
["pairs", "indicators1", "indicators2", "plot_limit", "db_url",
|
["pairs", "indicators1", "indicators2", "plot_limit", "db_url",
|
||||||
"trade_source", "export", "exportfilename", "timerange",
|
"trade_source", "export", "exportfilename", "timerange",
|
||||||
"refresh_pairs", "live"])
|
"refresh_pairs"])
|
||||||
|
|
||||||
ARGS_PLOT_PROFIT = (ARGS_COMMON + ARGS_STRATEGY +
|
ARGS_PLOT_PROFIT = (ARGS_COMMON + ARGS_STRATEGY +
|
||||||
["pairs", "timerange", "export", "exportfilename", "db_url", "trade_source"])
|
["pairs", "timerange", "export", "exportfilename", "db_url", "trade_source"])
|
||||||
|
|
||||||
|
NO_CONF_REQURIED = ["start_download_data"]
|
||||||
class TimeRange(NamedTuple):
|
|
||||||
"""
|
|
||||||
NamedTuple defining timerange inputs.
|
|
||||||
[start/stop]type defines if [start/stop]ts shall be used.
|
|
||||||
if *type is None, don't use corresponding startvalue.
|
|
||||||
"""
|
|
||||||
starttype: Optional[str] = None
|
|
||||||
stoptype: Optional[str] = None
|
|
||||||
startts: int = 0
|
|
||||||
stopts: int = 0
|
|
||||||
|
|
||||||
|
|
||||||
class Arguments(object):
|
class Arguments(object):
|
||||||
@ -88,7 +79,10 @@ class Arguments(object):
|
|||||||
|
|
||||||
# Workaround issue in argparse with action='append' and default value
|
# Workaround issue in argparse with action='append' and default value
|
||||||
# (see https://bugs.python.org/issue16399)
|
# (see https://bugs.python.org/issue16399)
|
||||||
if not self._no_default_config and parsed_arg.config is None:
|
# Allow no-config for certain commands (like downloading / plotting)
|
||||||
|
if (not self._no_default_config and parsed_arg.config is None
|
||||||
|
and not (hasattr(parsed_arg, 'func')
|
||||||
|
and parsed_arg.func.__name__ in NO_CONF_REQURIED)):
|
||||||
parsed_arg.config = [constants.DEFAULT_CONFIG]
|
parsed_arg.config = [constants.DEFAULT_CONFIG]
|
||||||
|
|
||||||
return parsed_arg
|
return parsed_arg
|
||||||
@ -106,7 +100,7 @@ class Arguments(object):
|
|||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge
|
from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge
|
||||||
from freqtrade.utils import start_list_exchanges
|
from freqtrade.utils import start_create_userdir, start_download_data, start_list_exchanges
|
||||||
|
|
||||||
subparsers = self.parser.add_subparsers(dest='subparser')
|
subparsers = self.parser.add_subparsers(dest='subparser')
|
||||||
|
|
||||||
@ -125,6 +119,11 @@ class Arguments(object):
|
|||||||
hyperopt_cmd.set_defaults(func=start_hyperopt)
|
hyperopt_cmd.set_defaults(func=start_hyperopt)
|
||||||
self._build_args(optionlist=ARGS_HYPEROPT, parser=hyperopt_cmd)
|
self._build_args(optionlist=ARGS_HYPEROPT, parser=hyperopt_cmd)
|
||||||
|
|
||||||
|
create_userdir_cmd = subparsers.add_parser('create-userdir',
|
||||||
|
help="Create user-data directory.")
|
||||||
|
create_userdir_cmd.set_defaults(func=start_create_userdir)
|
||||||
|
self._build_args(optionlist=ARGS_CREATE_USERDIR, parser=create_userdir_cmd)
|
||||||
|
|
||||||
# Add list-exchanges subcommand
|
# Add list-exchanges subcommand
|
||||||
list_exchanges_cmd = subparsers.add_parser(
|
list_exchanges_cmd = subparsers.add_parser(
|
||||||
'list-exchanges',
|
'list-exchanges',
|
||||||
@ -133,44 +132,10 @@ class Arguments(object):
|
|||||||
list_exchanges_cmd.set_defaults(func=start_list_exchanges)
|
list_exchanges_cmd.set_defaults(func=start_list_exchanges)
|
||||||
self._build_args(optionlist=ARGS_LIST_EXCHANGES, parser=list_exchanges_cmd)
|
self._build_args(optionlist=ARGS_LIST_EXCHANGES, parser=list_exchanges_cmd)
|
||||||
|
|
||||||
@staticmethod
|
# Add download-data subcommand
|
||||||
def parse_timerange(text: Optional[str]) -> TimeRange:
|
download_data_cmd = subparsers.add_parser(
|
||||||
"""
|
'download-data',
|
||||||
Parse the value of the argument --timerange to determine what is the range desired
|
help='Download backtesting data.'
|
||||||
:param text: value from --timerange
|
)
|
||||||
:return: Start and End range period
|
download_data_cmd.set_defaults(func=start_download_data)
|
||||||
"""
|
self._build_args(optionlist=ARGS_DOWNLOAD_DATA, parser=download_data_cmd)
|
||||||
if text is None:
|
|
||||||
return TimeRange(None, None, 0, 0)
|
|
||||||
syntax = [(r'^-(\d{8})$', (None, 'date')),
|
|
||||||
(r'^(\d{8})-$', ('date', None)),
|
|
||||||
(r'^(\d{8})-(\d{8})$', ('date', 'date')),
|
|
||||||
(r'^-(\d{10})$', (None, 'date')),
|
|
||||||
(r'^(\d{10})-$', ('date', None)),
|
|
||||||
(r'^(\d{10})-(\d{10})$', ('date', 'date')),
|
|
||||||
(r'^(-\d+)$', (None, 'line')),
|
|
||||||
(r'^(\d+)-$', ('line', None)),
|
|
||||||
(r'^(\d+)-(\d+)$', ('index', 'index'))]
|
|
||||||
for rex, stype in syntax:
|
|
||||||
# Apply the regular expression to text
|
|
||||||
match = re.match(rex, text)
|
|
||||||
if match: # Regex has matched
|
|
||||||
rvals = match.groups()
|
|
||||||
index = 0
|
|
||||||
start: int = 0
|
|
||||||
stop: int = 0
|
|
||||||
if stype[0]:
|
|
||||||
starts = rvals[index]
|
|
||||||
if stype[0] == 'date' and len(starts) == 8:
|
|
||||||
start = arrow.get(starts, 'YYYYMMDD').timestamp
|
|
||||||
else:
|
|
||||||
start = int(starts)
|
|
||||||
index += 1
|
|
||||||
if stype[1]:
|
|
||||||
stops = rvals[index]
|
|
||||||
if stype[1] == 'date' and len(stops) == 8:
|
|
||||||
stop = arrow.get(stops, 'YYYYMMDD').timestamp
|
|
||||||
else:
|
|
||||||
stop = int(stops)
|
|
||||||
return TimeRange(stype[0], stype[1], start, stop)
|
|
||||||
raise Exception('Incorrect syntax for timerange "%s"' % text)
|
|
||||||
|
@ -2,9 +2,9 @@ import logging
|
|||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
from freqtrade import OperationalException
|
from freqtrade import OperationalException
|
||||||
from freqtrade.exchange import (is_exchange_bad, is_exchange_available,
|
from freqtrade.exchange import (available_exchanges, get_exchange_bad_reason,
|
||||||
is_exchange_officially_supported, available_exchanges)
|
is_exchange_available, is_exchange_bad,
|
||||||
|
is_exchange_officially_supported)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -31,9 +31,8 @@ def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if check_for_bad and is_exchange_bad(exchange):
|
if check_for_bad and is_exchange_bad(exchange):
|
||||||
logger.warning(f'Exchange "{exchange}" is known to not work with the bot yet. '
|
raise OperationalException(f'Exchange "{exchange}" is known to not work with the bot yet. '
|
||||||
f'Use it only for development and testing purposes.')
|
f'Reason: {get_exchange_bad_reason(exchange)}')
|
||||||
return False
|
|
||||||
|
|
||||||
if is_exchange_officially_supported(exchange):
|
if is_exchange_officially_supported(exchange):
|
||||||
logger.info(f'Exchange "{exchange}" is officially supported '
|
logger.info(f'Exchange "{exchange}" is officially supported '
|
||||||
|
@ -55,7 +55,12 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
),
|
),
|
||||||
"datadir": Arg(
|
"datadir": Arg(
|
||||||
'-d', '--datadir',
|
'-d', '--datadir',
|
||||||
help='Path to backtest data.',
|
help='Path to directory with historical backtesting data.',
|
||||||
|
metavar='PATH',
|
||||||
|
),
|
||||||
|
"user_data_dir": Arg(
|
||||||
|
'--userdir', '--user-data-dir',
|
||||||
|
help='Path to userdata directory.',
|
||||||
metavar='PATH',
|
metavar='PATH',
|
||||||
),
|
),
|
||||||
# Main options
|
# Main options
|
||||||
@ -123,14 +128,9 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
action='store_false',
|
action='store_false',
|
||||||
default=True,
|
default=True,
|
||||||
),
|
),
|
||||||
"live": Arg(
|
|
||||||
'-l', '--live',
|
|
||||||
help='Use live data.',
|
|
||||||
action='store_true',
|
|
||||||
),
|
|
||||||
"strategy_list": Arg(
|
"strategy_list": Arg(
|
||||||
'--strategy-list',
|
'--strategy-list',
|
||||||
help='Provide a comma-separated list of strategies to backtest. '
|
help='Provide a space-separated list of strategies to backtest. '
|
||||||
'Please note that ticker-interval needs to be set either in config '
|
'Please note that ticker-interval needs to be set either in config '
|
||||||
'or via command line. When using this together with `--export trades`, '
|
'or via command line. When using this together with `--export trades`, '
|
||||||
'the strategy-name is injected into the filename '
|
'the strategy-name is injected into the filename '
|
||||||
@ -146,9 +146,9 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
'--export-filename',
|
'--export-filename',
|
||||||
help='Save backtest results to the file with this filename (default: `%(default)s`). '
|
help='Save backtest results to the file with this filename (default: `%(default)s`). '
|
||||||
'Requires `--export` to be set as well. '
|
'Requires `--export` to be set as well. '
|
||||||
'Example: `--export-filename=user_data/backtest_data/backtest_today.json`',
|
'Example: `--export-filename=user_data/backtest_results/backtest_today.json`',
|
||||||
metavar='PATH',
|
metavar='PATH',
|
||||||
default=os.path.join('user_data', 'backtest_data',
|
default=os.path.join('user_data', 'backtest_results',
|
||||||
'backtest-result.json'),
|
'backtest-result.json'),
|
||||||
),
|
),
|
||||||
# Edge
|
# Edge
|
||||||
@ -191,6 +191,19 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
action='store_true',
|
action='store_true',
|
||||||
default=False,
|
default=False,
|
||||||
),
|
),
|
||||||
|
"print_colorized": Arg(
|
||||||
|
'--no-color',
|
||||||
|
help='Disable colorization of hyperopt results. May be useful if you are '
|
||||||
|
'redirecting output to a file.',
|
||||||
|
action='store_false',
|
||||||
|
default=True,
|
||||||
|
),
|
||||||
|
"print_json": Arg(
|
||||||
|
'--print-json',
|
||||||
|
help='Print best result detailization in JSON format.',
|
||||||
|
action='store_true',
|
||||||
|
default=False,
|
||||||
|
),
|
||||||
"hyperopt_jobs": Arg(
|
"hyperopt_jobs": Arg(
|
||||||
'-j', '--job-workers',
|
'-j', '--job-workers',
|
||||||
help='The number of concurrently running jobs for hyperoptimization '
|
help='The number of concurrently running jobs for hyperoptimization '
|
||||||
@ -226,7 +239,9 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
'--hyperopt-loss',
|
'--hyperopt-loss',
|
||||||
help='Specify the class name of the hyperopt loss function class (IHyperOptLoss). '
|
help='Specify the class name of the hyperopt loss function class (IHyperOptLoss). '
|
||||||
'Different functions can generate completely different results, '
|
'Different functions can generate completely different results, '
|
||||||
'since the target for optimization is different. (default: `%(default)s`).',
|
'since the target for optimization is different. Built-in Hyperopt-loss-functions are: '
|
||||||
|
'DefaultHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss.'
|
||||||
|
'(default: `%(default)s`).',
|
||||||
metavar='NAME',
|
metavar='NAME',
|
||||||
default=constants.DEFAULT_HYPEROPT_LOSS,
|
default=constants.DEFAULT_HYPEROPT_LOSS,
|
||||||
),
|
),
|
||||||
@ -239,7 +254,8 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
# Script options
|
# Script options
|
||||||
"pairs": Arg(
|
"pairs": Arg(
|
||||||
'-p', '--pairs',
|
'-p', '--pairs',
|
||||||
help='Show profits for only these pairs. Pairs are comma-separated.',
|
help='Show profits for only these pairs. Pairs are space-separated.',
|
||||||
|
nargs='+',
|
||||||
),
|
),
|
||||||
# Download data
|
# Download data
|
||||||
"pairs_file": Arg(
|
"pairs_file": Arg(
|
||||||
@ -261,9 +277,10 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
"timeframes": Arg(
|
"timeframes": Arg(
|
||||||
'-t', '--timeframes',
|
'-t', '--timeframes',
|
||||||
help=f'Specify which tickers to download. Space-separated list. '
|
help=f'Specify which tickers to download. Space-separated list. '
|
||||||
f'Default: `{constants.DEFAULT_DOWNLOAD_TICKER_INTERVALS}`.',
|
f'Default: `1m 5m`.',
|
||||||
choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h',
|
choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h',
|
||||||
'6h', '8h', '12h', '1d', '3d', '1w'],
|
'6h', '8h', '12h', '1d', '3d', '1w'],
|
||||||
|
default=['1m', '5m'],
|
||||||
nargs='+',
|
nargs='+',
|
||||||
),
|
),
|
||||||
"erase": Arg(
|
"erase": Arg(
|
||||||
|
113
freqtrade/configuration/config_validation.py
Normal file
113
freqtrade/configuration/config_validation.py
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import logging
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from jsonschema import Draft4Validator, validators
|
||||||
|
from jsonschema.exceptions import ValidationError, best_match
|
||||||
|
|
||||||
|
from freqtrade import constants, OperationalException
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _extend_validator(validator_class):
|
||||||
|
"""
|
||||||
|
Extended validator for the Freqtrade configuration JSON Schema.
|
||||||
|
Currently it only handles defaults for subschemas.
|
||||||
|
"""
|
||||||
|
validate_properties = validator_class.VALIDATORS['properties']
|
||||||
|
|
||||||
|
def set_defaults(validator, properties, instance, schema):
|
||||||
|
for prop, subschema in properties.items():
|
||||||
|
if 'default' in subschema:
|
||||||
|
instance.setdefault(prop, subschema['default'])
|
||||||
|
|
||||||
|
for error in validate_properties(
|
||||||
|
validator, properties, instance, schema,
|
||||||
|
):
|
||||||
|
yield error
|
||||||
|
|
||||||
|
return validators.extend(
|
||||||
|
validator_class, {'properties': set_defaults}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
FreqtradeValidator = _extend_validator(Draft4Validator)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_config_schema(conf: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Validate the configuration follow the Config Schema
|
||||||
|
:param conf: Config in JSON format
|
||||||
|
:return: Returns the config if valid, otherwise throw an exception
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
FreqtradeValidator(constants.CONF_SCHEMA).validate(conf)
|
||||||
|
return conf
|
||||||
|
except ValidationError as e:
|
||||||
|
logger.critical(
|
||||||
|
f"Invalid configuration. See config.json.example. Reason: {e}"
|
||||||
|
)
|
||||||
|
raise ValidationError(
|
||||||
|
best_match(Draft4Validator(constants.CONF_SCHEMA).iter_errors(conf)).message
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_config_consistency(conf: Dict[str, Any]) -> None:
|
||||||
|
"""
|
||||||
|
Validate the configuration consistency.
|
||||||
|
Should be ran after loading both configuration and strategy,
|
||||||
|
since strategies can set certain configuration settings too.
|
||||||
|
:param conf: Config in JSON format
|
||||||
|
:return: Returns None if everything is ok, otherwise throw an OperationalException
|
||||||
|
"""
|
||||||
|
# validating trailing stoploss
|
||||||
|
_validate_trailing_stoploss(conf)
|
||||||
|
_validate_edge(conf)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None:
|
||||||
|
|
||||||
|
if conf.get('stoploss') == 0.0:
|
||||||
|
raise OperationalException(
|
||||||
|
'The config stoploss needs to be different from 0 to avoid problems with sell orders.'
|
||||||
|
)
|
||||||
|
# Skip if trailing stoploss is not activated
|
||||||
|
if not conf.get('trailing_stop', False):
|
||||||
|
return
|
||||||
|
|
||||||
|
tsl_positive = float(conf.get('trailing_stop_positive', 0))
|
||||||
|
tsl_offset = float(conf.get('trailing_stop_positive_offset', 0))
|
||||||
|
tsl_only_offset = conf.get('trailing_only_offset_is_reached', False)
|
||||||
|
|
||||||
|
if tsl_only_offset:
|
||||||
|
if tsl_positive == 0.0:
|
||||||
|
raise OperationalException(
|
||||||
|
'The config trailing_only_offset_is_reached needs '
|
||||||
|
'trailing_stop_positive_offset to be more than 0 in your config.')
|
||||||
|
if tsl_positive > 0 and 0 < tsl_offset <= tsl_positive:
|
||||||
|
raise OperationalException(
|
||||||
|
'The config trailing_stop_positive_offset needs '
|
||||||
|
'to be greater than trailing_stop_positive in your config.')
|
||||||
|
|
||||||
|
# Fetch again without default
|
||||||
|
if 'trailing_stop_positive' in conf and float(conf['trailing_stop_positive']) == 0.0:
|
||||||
|
raise OperationalException(
|
||||||
|
'The config trailing_stop_positive needs to be different from 0 '
|
||||||
|
'to avoid problems with sell orders.'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_edge(conf: Dict[str, Any]) -> None:
|
||||||
|
"""
|
||||||
|
Edge and Dynamic whitelist should not both be enabled, since edge overrides dynamic whitelists.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not conf.get('edge', {}).get('enabled'):
|
||||||
|
return
|
||||||
|
|
||||||
|
if conf.get('pairlist', {}).get('method') == 'VolumePairList':
|
||||||
|
raise OperationalException(
|
||||||
|
"Edge and VolumePairList are incompatible, "
|
||||||
|
"Edge will override whatever pairs VolumePairlist selects."
|
||||||
|
)
|
@ -1,19 +1,21 @@
|
|||||||
"""
|
"""
|
||||||
This module contains the configuration class
|
This module contains the configuration class
|
||||||
"""
|
"""
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
|
||||||
import warnings
|
import warnings
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from typing import Any, Callable, Dict, Optional
|
from pathlib import Path
|
||||||
|
from typing import Any, Callable, Dict, List, Optional
|
||||||
|
|
||||||
from freqtrade import OperationalException, constants
|
from freqtrade import OperationalException, constants
|
||||||
from freqtrade.configuration.check_exchange import check_exchange
|
from freqtrade.configuration.check_exchange import check_exchange
|
||||||
from freqtrade.configuration.create_datadir import create_datadir
|
from freqtrade.configuration.config_validation import (
|
||||||
from freqtrade.configuration.json_schema import validate_config_schema
|
validate_config_consistency, validate_config_schema)
|
||||||
|
from freqtrade.configuration.directory_operations import (create_datadir,
|
||||||
|
create_userdata_dir)
|
||||||
|
from freqtrade.configuration.load_config import load_config_file
|
||||||
from freqtrade.loggers import setup_logging
|
from freqtrade.loggers import setup_logging
|
||||||
from freqtrade.misc import deep_merge_dicts
|
from freqtrade.misc import deep_merge_dicts, json_load
|
||||||
from freqtrade.state import RunMode
|
from freqtrade.state import RunMode
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -40,62 +42,46 @@ class Configuration(object):
|
|||||||
|
|
||||||
return self.config
|
return self.config
|
||||||
|
|
||||||
def _load_config_files(self) -> Dict[str, Any]:
|
@staticmethod
|
||||||
|
def from_files(files: List[str]) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Iterate through the config files passed in the args,
|
Iterate through the config files passed in, loading all of them
|
||||||
loading all of them and merging their contents.
|
and merging their contents.
|
||||||
|
Files are loaded in sequence, parameters in later configuration files
|
||||||
|
override the same parameter from an earlier file (last definition wins).
|
||||||
|
:param files: List of file paths
|
||||||
|
:return: configuration dictionary
|
||||||
"""
|
"""
|
||||||
|
# Keep this method as staticmethod, so it can be used from interactive environments
|
||||||
config: Dict[str, Any] = {}
|
config: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
if not files:
|
||||||
|
return constants.MINIMAL_CONFIG.copy()
|
||||||
|
|
||||||
# We expect here a list of config filenames
|
# We expect here a list of config filenames
|
||||||
for path in self.args.config:
|
for path in files:
|
||||||
logger.info('Using config: %s ...', path)
|
logger.info(f'Using config: {path} ...')
|
||||||
|
|
||||||
# Merge config options, overwriting old values
|
# Merge config options, overwriting old values
|
||||||
config = deep_merge_dicts(self._load_config_file(path), config)
|
config = deep_merge_dicts(load_config_file(path), config)
|
||||||
|
|
||||||
return config
|
# Normalize config
|
||||||
|
|
||||||
def _load_config_file(self, path: str) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Loads a config file from the given path
|
|
||||||
:param path: path as str
|
|
||||||
:return: configuration as dictionary
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Read config from stdin if requested in the options
|
|
||||||
with open(path) if path != '-' else sys.stdin as file:
|
|
||||||
config = json.load(file)
|
|
||||||
except FileNotFoundError:
|
|
||||||
raise OperationalException(
|
|
||||||
f'Config file "{path}" not found!'
|
|
||||||
' Please create a config file or check whether it exists.')
|
|
||||||
|
|
||||||
return config
|
|
||||||
|
|
||||||
def _normalize_config(self, config: Dict[str, Any]) -> None:
|
|
||||||
"""
|
|
||||||
Make config more canonical -- i.e. for example add missing parts that we expect
|
|
||||||
to be normally in it...
|
|
||||||
"""
|
|
||||||
if 'internals' not in config:
|
if 'internals' not in config:
|
||||||
config['internals'] = {}
|
config['internals'] = {}
|
||||||
|
|
||||||
|
# validate configuration before returning
|
||||||
|
logger.info('Validating configuration ...')
|
||||||
|
validate_config_schema(config)
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
def load_config(self) -> Dict[str, Any]:
|
def load_config(self) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Extract information for sys.argv and load the bot configuration
|
Extract information for sys.argv and load the bot configuration
|
||||||
:return: Configuration dictionary
|
:return: Configuration dictionary
|
||||||
"""
|
"""
|
||||||
# Load all configs
|
# Load all configs
|
||||||
config: Dict[str, Any] = self._load_config_files()
|
config: Dict[str, Any] = Configuration.from_files(self.args.config)
|
||||||
|
|
||||||
# Make resulting config more canonical
|
|
||||||
self._normalize_config(config)
|
|
||||||
|
|
||||||
logger.info('Validating configuration ...')
|
|
||||||
validate_config_schema(config)
|
|
||||||
|
|
||||||
self._validate_config_consistency(config)
|
|
||||||
|
|
||||||
self._process_common_options(config)
|
self._process_common_options(config)
|
||||||
|
|
||||||
@ -105,6 +91,13 @@ class Configuration(object):
|
|||||||
|
|
||||||
self._process_runmode(config)
|
self._process_runmode(config)
|
||||||
|
|
||||||
|
# Check if the exchange set by the user is supported
|
||||||
|
check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True))
|
||||||
|
|
||||||
|
self._resolve_pairs_list(config)
|
||||||
|
|
||||||
|
validate_config_consistency(config)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
def _process_logging_options(self, config: Dict[str, Any]) -> None:
|
def _process_logging_options(self, config: Dict[str, Any]) -> None:
|
||||||
@ -123,7 +116,9 @@ class Configuration(object):
|
|||||||
|
|
||||||
setup_logging(config)
|
setup_logging(config)
|
||||||
|
|
||||||
def _process_strategy_options(self, config: Dict[str, Any]) -> None:
|
def _process_common_options(self, config: Dict[str, Any]) -> None:
|
||||||
|
|
||||||
|
self._process_logging_options(config)
|
||||||
|
|
||||||
# Set strategy if not specified in config and or if it's non default
|
# Set strategy if not specified in config and or if it's non default
|
||||||
if self.args.strategy != constants.DEFAULT_STRATEGY or not config.get('strategy'):
|
if self.args.strategy != constants.DEFAULT_STRATEGY or not config.get('strategy'):
|
||||||
@ -132,11 +127,6 @@ class Configuration(object):
|
|||||||
self._args_to_config(config, argname='strategy_path',
|
self._args_to_config(config, argname='strategy_path',
|
||||||
logstring='Using additional Strategy lookup path: {}')
|
logstring='Using additional Strategy lookup path: {}')
|
||||||
|
|
||||||
def _process_common_options(self, config: Dict[str, Any]) -> None:
|
|
||||||
|
|
||||||
self._process_logging_options(config)
|
|
||||||
self._process_strategy_options(config)
|
|
||||||
|
|
||||||
if ('db_url' in self.args and self.args.db_url and
|
if ('db_url' in self.args and self.args.db_url and
|
||||||
self.args.db_url != constants.DEFAULT_DB_PROD_URL):
|
self.args.db_url != constants.DEFAULT_DB_PROD_URL):
|
||||||
config.update({'db_url': self.args.db_url})
|
config.update({'db_url': self.args.db_url})
|
||||||
@ -165,14 +155,21 @@ class Configuration(object):
|
|||||||
if 'sd_notify' in self.args and self.args.sd_notify:
|
if 'sd_notify' in self.args and self.args.sd_notify:
|
||||||
config['internals'].update({'sd_notify': True})
|
config['internals'].update({'sd_notify': True})
|
||||||
|
|
||||||
# Check if the exchange set by the user is supported
|
|
||||||
check_exchange(config)
|
|
||||||
|
|
||||||
def _process_datadir_options(self, config: Dict[str, Any]) -> None:
|
def _process_datadir_options(self, config: Dict[str, Any]) -> None:
|
||||||
"""
|
"""
|
||||||
Extract information for sys.argv and load datadir configuration:
|
Extract information for sys.argv and load directory configurations
|
||||||
the --datadir option
|
--user-data, --datadir
|
||||||
"""
|
"""
|
||||||
|
if 'user_data_dir' in self.args and self.args.user_data_dir:
|
||||||
|
config.update({'user_data_dir': self.args.user_data_dir})
|
||||||
|
elif 'user_data_dir' not in config:
|
||||||
|
# Default to cwd/user_data (legacy option ...)
|
||||||
|
config.update({'user_data_dir': str(Path.cwd() / "user_data")})
|
||||||
|
|
||||||
|
# reset to user_data_dir so this contains the absolute path.
|
||||||
|
config['user_data_dir'] = create_userdata_dir(config['user_data_dir'], create_dir=False)
|
||||||
|
logger.info('Using user-data directory: %s ...', config['user_data_dir'])
|
||||||
|
|
||||||
if 'datadir' in self.args and self.args.datadir:
|
if 'datadir' in self.args and self.args.datadir:
|
||||||
config.update({'datadir': create_datadir(config, self.args.datadir)})
|
config.update({'datadir': create_datadir(config, self.args.datadir)})
|
||||||
else:
|
else:
|
||||||
@ -186,10 +183,6 @@ class Configuration(object):
|
|||||||
logstring='Parameter -i/--ticker-interval detected ... '
|
logstring='Parameter -i/--ticker-interval detected ... '
|
||||||
'Using ticker_interval: {} ...')
|
'Using ticker_interval: {} ...')
|
||||||
|
|
||||||
self._args_to_config(config, argname='live',
|
|
||||||
logstring='Parameter -l/--live detected ...',
|
|
||||||
deprecated_msg='--live will be removed soon.')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='position_stacking',
|
self._args_to_config(config, argname='position_stacking',
|
||||||
logstring='Parameter --enable-position-stacking detected ...')
|
logstring='Parameter --enable-position-stacking detected ...')
|
||||||
|
|
||||||
@ -214,7 +207,8 @@ class Configuration(object):
|
|||||||
self._process_datadir_options(config)
|
self._process_datadir_options(config)
|
||||||
|
|
||||||
self._args_to_config(config, argname='refresh_pairs',
|
self._args_to_config(config, argname='refresh_pairs',
|
||||||
logstring='Parameter -r/--refresh-pairs-cached detected ...')
|
logstring='Parameter -r/--refresh-pairs-cached detected ...',
|
||||||
|
deprecated_msg='-r/--refresh-pairs-cached will be removed soon.')
|
||||||
|
|
||||||
self._args_to_config(config, argname='strategy_list',
|
self._args_to_config(config, argname='strategy_list',
|
||||||
logstring='Using strategy list of {} Strategies', logfun=len)
|
logstring='Using strategy list of {} Strategies', logfun=len)
|
||||||
@ -254,6 +248,15 @@ class Configuration(object):
|
|||||||
self._args_to_config(config, argname='print_all',
|
self._args_to_config(config, argname='print_all',
|
||||||
logstring='Parameter --print-all detected ...')
|
logstring='Parameter --print-all detected ...')
|
||||||
|
|
||||||
|
if 'print_colorized' in self.args and not self.args.print_colorized:
|
||||||
|
logger.info('Parameter --no-color detected ...')
|
||||||
|
config.update({'print_colorized': False})
|
||||||
|
else:
|
||||||
|
config.update({'print_colorized': True})
|
||||||
|
|
||||||
|
self._args_to_config(config, argname='print_json',
|
||||||
|
logstring='Parameter --print-json detected ...')
|
||||||
|
|
||||||
self._args_to_config(config, argname='hyperopt_jobs',
|
self._args_to_config(config, argname='hyperopt_jobs',
|
||||||
logstring='Parameter -j/--job-workers detected: {}')
|
logstring='Parameter -j/--job-workers detected: {}')
|
||||||
|
|
||||||
@ -285,44 +288,28 @@ class Configuration(object):
|
|||||||
self._args_to_config(config, argname='trade_source',
|
self._args_to_config(config, argname='trade_source',
|
||||||
logstring='Using trades from: {}')
|
logstring='Using trades from: {}')
|
||||||
|
|
||||||
|
self._args_to_config(config, argname='erase',
|
||||||
|
logstring='Erase detected. Deleting existing data.')
|
||||||
|
|
||||||
|
self._args_to_config(config, argname='timeframes',
|
||||||
|
logstring='timeframes --timeframes: {}')
|
||||||
|
|
||||||
|
self._args_to_config(config, argname='days',
|
||||||
|
logstring='Detected --days: {}')
|
||||||
|
|
||||||
|
if "exchange" in self.args and self.args.exchange:
|
||||||
|
config['exchange']['name'] = self.args.exchange
|
||||||
|
logger.info(f"Using exchange {config['exchange']['name']}")
|
||||||
|
|
||||||
def _process_runmode(self, config: Dict[str, Any]) -> None:
|
def _process_runmode(self, config: Dict[str, Any]) -> None:
|
||||||
|
|
||||||
if not self.runmode:
|
if not self.runmode:
|
||||||
# Handle real mode, infer dry/live from config
|
# Handle real mode, infer dry/live from config
|
||||||
self.runmode = RunMode.DRY_RUN if config.get('dry_run', True) else RunMode.LIVE
|
self.runmode = RunMode.DRY_RUN if config.get('dry_run', True) else RunMode.LIVE
|
||||||
logger.info("Runmode set to {self.runmode}.")
|
logger.info(f"Runmode set to {self.runmode}.")
|
||||||
|
|
||||||
config.update({'runmode': self.runmode})
|
config.update({'runmode': self.runmode})
|
||||||
|
|
||||||
def _validate_config_consistency(self, conf: Dict[str, Any]) -> None:
|
|
||||||
"""
|
|
||||||
Validate the configuration consistency
|
|
||||||
:param conf: Config in JSON format
|
|
||||||
:return: Returns None if everything is ok, otherwise throw an OperationalException
|
|
||||||
"""
|
|
||||||
# validating trailing stoploss
|
|
||||||
self._validate_trailing_stoploss(conf)
|
|
||||||
|
|
||||||
def _validate_trailing_stoploss(self, conf: Dict[str, Any]) -> None:
|
|
||||||
|
|
||||||
# Skip if trailing stoploss is not activated
|
|
||||||
if not conf.get('trailing_stop', False):
|
|
||||||
return
|
|
||||||
|
|
||||||
tsl_positive = float(conf.get('trailing_stop_positive', 0))
|
|
||||||
tsl_offset = float(conf.get('trailing_stop_positive_offset', 0))
|
|
||||||
tsl_only_offset = conf.get('trailing_only_offset_is_reached', False)
|
|
||||||
|
|
||||||
if tsl_only_offset:
|
|
||||||
if tsl_positive == 0.0:
|
|
||||||
raise OperationalException(
|
|
||||||
f'The config trailing_only_offset_is_reached needs '
|
|
||||||
'trailing_stop_positive_offset to be more than 0 in your config.')
|
|
||||||
if tsl_positive > 0 and 0 < tsl_offset <= tsl_positive:
|
|
||||||
raise OperationalException(
|
|
||||||
f'The config trailing_stop_positive_offset needs '
|
|
||||||
'to be greater than trailing_stop_positive_offset in your config.')
|
|
||||||
|
|
||||||
def _args_to_config(self, config: Dict[str, Any], argname: str,
|
def _args_to_config(self, config: Dict[str, Any], argname: str,
|
||||||
logstring: str, logfun: Optional[Callable] = None,
|
logstring: str, logfun: Optional[Callable] = None,
|
||||||
deprecated_msg: Optional[str] = None) -> None:
|
deprecated_msg: Optional[str] = None) -> None:
|
||||||
@ -344,3 +331,39 @@ class Configuration(object):
|
|||||||
logger.info(logstring.format(config[argname]))
|
logger.info(logstring.format(config[argname]))
|
||||||
if deprecated_msg:
|
if deprecated_msg:
|
||||||
warnings.warn(f"DEPRECATED: {deprecated_msg}", DeprecationWarning)
|
warnings.warn(f"DEPRECATED: {deprecated_msg}", DeprecationWarning)
|
||||||
|
|
||||||
|
def _resolve_pairs_list(self, config: Dict[str, Any]) -> None:
|
||||||
|
"""
|
||||||
|
Helper for download script.
|
||||||
|
Takes first found:
|
||||||
|
* -p (pairs argument)
|
||||||
|
* --pairs-file
|
||||||
|
* whitelist from config
|
||||||
|
"""
|
||||||
|
|
||||||
|
if "pairs" in config:
|
||||||
|
return
|
||||||
|
|
||||||
|
if "pairs_file" in self.args and self.args.pairs_file:
|
||||||
|
pairs_file = Path(self.args.pairs_file)
|
||||||
|
logger.info(f'Reading pairs file "{pairs_file}".')
|
||||||
|
# Download pairs from the pairs file if no config is specified
|
||||||
|
# or if pairs file is specified explicitely
|
||||||
|
if not pairs_file.exists():
|
||||||
|
raise OperationalException(f'No pairs file found with path "{pairs_file}".')
|
||||||
|
with pairs_file.open('r') as f:
|
||||||
|
config['pairs'] = json_load(f)
|
||||||
|
config['pairs'].sort()
|
||||||
|
return
|
||||||
|
|
||||||
|
if "config" in self.args and self.args.config:
|
||||||
|
logger.info("Using pairlist from configuration.")
|
||||||
|
config['pairs'] = config.get('exchange', {}).get('pair_whitelist')
|
||||||
|
else:
|
||||||
|
# Fall back to /dl_path/pairs.json
|
||||||
|
pairs_file = Path(config['datadir']) / config['exchange']['name'].lower() / "pairs.json"
|
||||||
|
if pairs_file.exists():
|
||||||
|
with pairs_file.open('r') as f:
|
||||||
|
config['pairs'] = json_load(f)
|
||||||
|
if 'pairs' in config:
|
||||||
|
config['pairs'].sort()
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
import logging
|
|
||||||
from typing import Any, Dict, Optional
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def create_datadir(config: Dict[str, Any], datadir: Optional[str] = None) -> str:
|
|
||||||
|
|
||||||
folder = Path(datadir) if datadir else Path('user_data/data')
|
|
||||||
if not datadir:
|
|
||||||
# set datadir
|
|
||||||
exchange_name = config.get('exchange', {}).get('name').lower()
|
|
||||||
folder = folder.joinpath(exchange_name)
|
|
||||||
|
|
||||||
if not folder.is_dir():
|
|
||||||
folder.mkdir(parents=True)
|
|
||||||
logger.info(f'Created data directory: {datadir}')
|
|
||||||
return str(folder)
|
|
50
freqtrade/configuration/directory_operations.py
Normal file
50
freqtrade/configuration/directory_operations.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import logging
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from freqtrade import OperationalException
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def create_datadir(config: Dict[str, Any], datadir: Optional[str] = None) -> str:
|
||||||
|
|
||||||
|
folder = Path(datadir) if datadir else Path(f"{config['user_data_dir']}/data")
|
||||||
|
if not datadir:
|
||||||
|
# set datadir
|
||||||
|
exchange_name = config.get('exchange', {}).get('name').lower()
|
||||||
|
folder = folder.joinpath(exchange_name)
|
||||||
|
|
||||||
|
if not folder.is_dir():
|
||||||
|
folder.mkdir(parents=True)
|
||||||
|
logger.info(f'Created data directory: {datadir}')
|
||||||
|
return str(folder)
|
||||||
|
|
||||||
|
|
||||||
|
def create_userdata_dir(directory: str, create_dir=False) -> Path:
|
||||||
|
"""
|
||||||
|
Create userdata directory structure.
|
||||||
|
if create_dir is True, then the parent-directory will be created if it does not exist.
|
||||||
|
Sub-directories will always be created if the parent directory exists.
|
||||||
|
Raises OperationalException if given a non-existing directory.
|
||||||
|
:param directory: Directory to check
|
||||||
|
:param create_dir: Create directory if it does not exist.
|
||||||
|
:return: Path object containing the directory
|
||||||
|
"""
|
||||||
|
sub_dirs = ["backtest_results", "data", "hyperopts", "hyperopt_results", "plot", "strategies", ]
|
||||||
|
folder = Path(directory)
|
||||||
|
if not folder.is_dir():
|
||||||
|
if create_dir:
|
||||||
|
folder.mkdir(parents=True)
|
||||||
|
logger.info(f'Created user-data directory: {folder}')
|
||||||
|
else:
|
||||||
|
raise OperationalException(
|
||||||
|
f"Directory `{folder}` does not exist. "
|
||||||
|
"Please use `freqtrade create-userdir` to create a user directory")
|
||||||
|
|
||||||
|
# Create required subdirectories
|
||||||
|
for f in sub_dirs:
|
||||||
|
subfolder = folder / f
|
||||||
|
if not subfolder.is_dir():
|
||||||
|
subfolder.mkdir(parents=False)
|
||||||
|
return folder
|
@ -1,53 +0,0 @@
|
|||||||
import logging
|
|
||||||
from typing import Any, Dict
|
|
||||||
|
|
||||||
from jsonschema import Draft4Validator, validators
|
|
||||||
from jsonschema.exceptions import ValidationError, best_match
|
|
||||||
|
|
||||||
from freqtrade import constants
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def _extend_validator(validator_class):
|
|
||||||
"""
|
|
||||||
Extended validator for the Freqtrade configuration JSON Schema.
|
|
||||||
Currently it only handles defaults for subschemas.
|
|
||||||
"""
|
|
||||||
validate_properties = validator_class.VALIDATORS['properties']
|
|
||||||
|
|
||||||
def set_defaults(validator, properties, instance, schema):
|
|
||||||
for prop, subschema in properties.items():
|
|
||||||
if 'default' in subschema:
|
|
||||||
instance.setdefault(prop, subschema['default'])
|
|
||||||
|
|
||||||
for error in validate_properties(
|
|
||||||
validator, properties, instance, schema,
|
|
||||||
):
|
|
||||||
yield error
|
|
||||||
|
|
||||||
return validators.extend(
|
|
||||||
validator_class, {'properties': set_defaults}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
FreqtradeValidator = _extend_validator(Draft4Validator)
|
|
||||||
|
|
||||||
|
|
||||||
def validate_config_schema(conf: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Validate the configuration follow the Config Schema
|
|
||||||
:param conf: Config in JSON format
|
|
||||||
:return: Returns the config if valid, otherwise throw an exception
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
FreqtradeValidator(constants.CONF_SCHEMA).validate(conf)
|
|
||||||
return conf
|
|
||||||
except ValidationError as e:
|
|
||||||
logger.critical(
|
|
||||||
f"Invalid configuration. See config.json.example. Reason: {e}"
|
|
||||||
)
|
|
||||||
raise ValidationError(
|
|
||||||
best_match(Draft4Validator(constants.CONF_SCHEMA).iter_errors(conf)).message
|
|
||||||
)
|
|
33
freqtrade/configuration/load_config.py
Normal file
33
freqtrade/configuration/load_config.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
"""
|
||||||
|
This module contain functions to load the configuration file
|
||||||
|
"""
|
||||||
|
import rapidjson
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from freqtrade import OperationalException
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_PARSE_MODE = rapidjson.PM_COMMENTS | rapidjson.PM_TRAILING_COMMAS
|
||||||
|
|
||||||
|
|
||||||
|
def load_config_file(path: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Loads a config file from the given path
|
||||||
|
:param path: path as str
|
||||||
|
:return: configuration as dictionary
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Read config from stdin if requested in the options
|
||||||
|
with open(path) if path != '-' else sys.stdin as file:
|
||||||
|
config = rapidjson.load(file, parse_mode=CONFIG_PARSE_MODE)
|
||||||
|
except FileNotFoundError:
|
||||||
|
raise OperationalException(
|
||||||
|
f'Config file "{path}" not found!'
|
||||||
|
' Please create a config file or check whether it exists.')
|
||||||
|
|
||||||
|
return config
|
70
freqtrade/configuration/timerange.py
Normal file
70
freqtrade/configuration/timerange.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
"""
|
||||||
|
This module contains the argument manager class
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import arrow
|
||||||
|
|
||||||
|
|
||||||
|
class TimeRange():
|
||||||
|
"""
|
||||||
|
object defining timerange inputs.
|
||||||
|
[start/stop]type defines if [start/stop]ts shall be used.
|
||||||
|
if *type is None, don't use corresponding startvalue.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, starttype: Optional[str] = None, stoptype: Optional[str] = None,
|
||||||
|
startts: int = 0, stopts: int = 0):
|
||||||
|
|
||||||
|
self.starttype: Optional[str] = starttype
|
||||||
|
self.stoptype: Optional[str] = stoptype
|
||||||
|
self.startts: int = startts
|
||||||
|
self.stopts: int = stopts
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
"""Override the default Equals behavior"""
|
||||||
|
return (self.starttype == other.starttype and self.stoptype == other.stoptype
|
||||||
|
and self.startts == other.startts and self.stopts == other.stopts)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_timerange(text: Optional[str]):
|
||||||
|
"""
|
||||||
|
Parse the value of the argument --timerange to determine what is the range desired
|
||||||
|
:param text: value from --timerange
|
||||||
|
:return: Start and End range period
|
||||||
|
"""
|
||||||
|
if text is None:
|
||||||
|
return TimeRange(None, None, 0, 0)
|
||||||
|
syntax = [(r'^-(\d{8})$', (None, 'date')),
|
||||||
|
(r'^(\d{8})-$', ('date', None)),
|
||||||
|
(r'^(\d{8})-(\d{8})$', ('date', 'date')),
|
||||||
|
(r'^-(\d{10})$', (None, 'date')),
|
||||||
|
(r'^(\d{10})-$', ('date', None)),
|
||||||
|
(r'^(\d{10})-(\d{10})$', ('date', 'date')),
|
||||||
|
(r'^(-\d+)$', (None, 'line')),
|
||||||
|
(r'^(\d+)-$', ('line', None)),
|
||||||
|
(r'^(\d+)-(\d+)$', ('index', 'index'))]
|
||||||
|
for rex, stype in syntax:
|
||||||
|
# Apply the regular expression to text
|
||||||
|
match = re.match(rex, text)
|
||||||
|
if match: # Regex has matched
|
||||||
|
rvals = match.groups()
|
||||||
|
index = 0
|
||||||
|
start: int = 0
|
||||||
|
stop: int = 0
|
||||||
|
if stype[0]:
|
||||||
|
starts = rvals[index]
|
||||||
|
if stype[0] == 'date' and len(starts) == 8:
|
||||||
|
start = arrow.get(starts, 'YYYYMMDD').timestamp
|
||||||
|
else:
|
||||||
|
start = int(starts)
|
||||||
|
index += 1
|
||||||
|
if stype[1]:
|
||||||
|
stops = rvals[index]
|
||||||
|
if stype[1] == 'date' and len(stops) == 8:
|
||||||
|
stop = arrow.get(stops, 'YYYYMMDD').timestamp
|
||||||
|
else:
|
||||||
|
stop = int(stops)
|
||||||
|
return TimeRange(stype[0], stype[1], start, stop)
|
||||||
|
raise Exception('Incorrect syntax for timerange "%s"' % text)
|
@ -5,7 +5,6 @@ bot constants
|
|||||||
"""
|
"""
|
||||||
DEFAULT_CONFIG = 'config.json'
|
DEFAULT_CONFIG = 'config.json'
|
||||||
DEFAULT_EXCHANGE = 'bittrex'
|
DEFAULT_EXCHANGE = 'bittrex'
|
||||||
DYNAMIC_WHITELIST = 20 # pairs
|
|
||||||
PROCESS_THROTTLE_SECS = 5 # sec
|
PROCESS_THROTTLE_SECS = 5 # sec
|
||||||
DEFAULT_TICKER_INTERVAL = 5 # min
|
DEFAULT_TICKER_INTERVAL = 5 # min
|
||||||
HYPEROPT_EPOCH = 100 # epochs
|
HYPEROPT_EPOCH = 100 # epochs
|
||||||
@ -23,7 +22,6 @@ ORDERTYPE_POSSIBILITIES = ['limit', 'market']
|
|||||||
ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
|
ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
|
||||||
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList']
|
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList']
|
||||||
DRY_RUN_WALLET = 999.9
|
DRY_RUN_WALLET = 999.9
|
||||||
DEFAULT_DOWNLOAD_TICKER_INTERVALS = '1m 5m'
|
|
||||||
|
|
||||||
TICKER_INTERVALS = [
|
TICKER_INTERVALS = [
|
||||||
'1m', '3m', '5m', '15m', '30m',
|
'1m', '3m', '5m', '15m', '30m',
|
||||||
@ -39,6 +37,20 @@ SUPPORTED_FIAT = [
|
|||||||
"BTC", "XBT", "ETH", "XRP", "LTC", "BCH", "USDT"
|
"BTC", "XBT", "ETH", "XRP", "LTC", "BCH", "USDT"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
MINIMAL_CONFIG = {
|
||||||
|
'stake_currency': '',
|
||||||
|
'dry_run': True,
|
||||||
|
'exchange': {
|
||||||
|
'name': '',
|
||||||
|
'key': '',
|
||||||
|
'secret': '',
|
||||||
|
'pair_whitelist': [],
|
||||||
|
'ccxt_async_config': {
|
||||||
|
'enableRateLimit': True,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Required json-schema for user specified config
|
# Required json-schema for user specified config
|
||||||
CONF_SCHEMA = {
|
CONF_SCHEMA = {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
|
@ -30,7 +30,7 @@ def load_backtest_data(filename) -> pd.DataFrame:
|
|||||||
filename = Path(filename)
|
filename = Path(filename)
|
||||||
|
|
||||||
if not filename.is_file():
|
if not filename.is_file():
|
||||||
raise ValueError("File {filename} does not exist.")
|
raise ValueError(f"File {filename} does not exist.")
|
||||||
|
|
||||||
with filename.open() as file:
|
with filename.open() as file:
|
||||||
data = json_load(file)
|
data = json_load(file)
|
||||||
@ -81,19 +81,30 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame:
|
|||||||
"""
|
"""
|
||||||
trades: pd.DataFrame = pd.DataFrame([], columns=BT_DATA_COLUMNS)
|
trades: pd.DataFrame = pd.DataFrame([], columns=BT_DATA_COLUMNS)
|
||||||
persistence.init(db_url, clean_open_orders=False)
|
persistence.init(db_url, clean_open_orders=False)
|
||||||
columns = ["pair", "profit", "open_time", "close_time",
|
|
||||||
"open_rate", "close_rate", "duration", "sell_reason",
|
|
||||||
"max_rate", "min_rate"]
|
|
||||||
|
|
||||||
trades = pd.DataFrame([(t.pair, t.calc_profit(),
|
columns = ["pair", "open_time", "close_time", "profit", "profitperc",
|
||||||
|
"open_rate", "close_rate", "amount", "duration", "sell_reason",
|
||||||
|
"fee_open", "fee_close", "open_rate_requested", "close_rate_requested",
|
||||||
|
"stake_amount", "max_rate", "min_rate", "id", "exchange",
|
||||||
|
"stop_loss", "initial_stop_loss", "strategy", "ticker_interval"]
|
||||||
|
|
||||||
|
trades = pd.DataFrame([(t.pair,
|
||||||
t.open_date.replace(tzinfo=pytz.UTC),
|
t.open_date.replace(tzinfo=pytz.UTC),
|
||||||
t.close_date.replace(tzinfo=pytz.UTC) if t.close_date else None,
|
t.close_date.replace(tzinfo=pytz.UTC) if t.close_date else None,
|
||||||
t.open_rate, t.close_rate,
|
t.calc_profit(), t.calc_profit_percent(),
|
||||||
t.close_date.timestamp() - t.open_date.timestamp()
|
t.open_rate, t.close_rate, t.amount,
|
||||||
if t.close_date else None,
|
(t.close_date.timestamp() - t.open_date.timestamp()
|
||||||
|
if t.close_date else None),
|
||||||
t.sell_reason,
|
t.sell_reason,
|
||||||
|
t.fee_open, t.fee_close,
|
||||||
|
t.open_rate_requested,
|
||||||
|
t.close_rate_requested,
|
||||||
|
t.stake_amount,
|
||||||
t.max_rate,
|
t.max_rate,
|
||||||
t.min_rate,
|
t.min_rate,
|
||||||
|
t.id, t.exchange,
|
||||||
|
t.stop_loss, t.initial_stop_loss,
|
||||||
|
t.strategy, t.ticker_interval
|
||||||
)
|
)
|
||||||
for t in Trade.query.all()],
|
for t in Trade.query.all()],
|
||||||
columns=columns)
|
columns=columns)
|
||||||
|
@ -44,36 +44,49 @@ class DataProvider():
|
|||||||
|
|
||||||
def ohlcv(self, pair: str, ticker_interval: str = None, copy: bool = True) -> DataFrame:
|
def ohlcv(self, pair: str, ticker_interval: str = None, copy: bool = True) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
get ohlcv data for the given pair as DataFrame
|
Get ohlcv data for the given pair as DataFrame
|
||||||
Please check `available_pairs` to verify which pairs are currently cached.
|
Please use the `available_pairs` method to verify which pairs are currently cached.
|
||||||
:param pair: pair to get the data for
|
:param pair: pair to get the data for
|
||||||
:param ticker_interval: ticker_interval to get pair for
|
:param ticker_interval: ticker interval to get data for
|
||||||
:param copy: copy dataframe before returning.
|
:param copy: copy dataframe before returning if True.
|
||||||
Use false only for RO operations (where the dataframe is not modified)
|
Use False only for read-only operations (where the dataframe is not modified)
|
||||||
"""
|
"""
|
||||||
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
|
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
|
||||||
if ticker_interval:
|
return self._exchange.klines((pair, ticker_interval or self._config['ticker_interval']),
|
||||||
pairtick = (pair, ticker_interval)
|
copy=copy)
|
||||||
else:
|
|
||||||
pairtick = (pair, self._config['ticker_interval'])
|
|
||||||
|
|
||||||
return self._exchange.klines(pairtick, copy=copy)
|
|
||||||
else:
|
else:
|
||||||
return DataFrame()
|
return DataFrame()
|
||||||
|
|
||||||
def historic_ohlcv(self, pair: str, ticker_interval: str) -> DataFrame:
|
def historic_ohlcv(self, pair: str, ticker_interval: str = None) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
get stored historic ohlcv data
|
Get stored historic ohlcv data
|
||||||
:param pair: pair to get the data for
|
:param pair: pair to get the data for
|
||||||
:param ticker_interval: ticker_interval to get pair for
|
:param ticker_interval: ticker interval to get data for
|
||||||
"""
|
"""
|
||||||
return load_pair_history(pair=pair,
|
return load_pair_history(pair=pair,
|
||||||
ticker_interval=ticker_interval,
|
ticker_interval=ticker_interval or self._config['ticker_interval'],
|
||||||
refresh_pairs=False,
|
refresh_pairs=False,
|
||||||
datadir=Path(self._config['datadir']) if self._config.get(
|
datadir=Path(self._config['datadir']) if self._config.get(
|
||||||
'datadir') else None
|
'datadir') else None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_pair_dataframe(self, pair: str, ticker_interval: str = None) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Return pair ohlcv data, either live or cached historical -- depending
|
||||||
|
on the runmode.
|
||||||
|
:param pair: pair to get the data for
|
||||||
|
:param ticker_interval: ticker interval to get data for
|
||||||
|
"""
|
||||||
|
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
|
||||||
|
# Get live ohlcv data.
|
||||||
|
data = self.ohlcv(pair=pair, ticker_interval=ticker_interval)
|
||||||
|
else:
|
||||||
|
# Get historic ohlcv data (cached on disk).
|
||||||
|
data = self.historic_ohlcv(pair=pair, ticker_interval=ticker_interval)
|
||||||
|
if len(data) == 0:
|
||||||
|
logger.warning(f"No data found for ({pair}, {ticker_interval}).")
|
||||||
|
return data
|
||||||
|
|
||||||
def ticker(self, pair: str):
|
def ticker(self, pair: str):
|
||||||
"""
|
"""
|
||||||
Return last ticker data
|
Return last ticker data
|
||||||
|
@ -43,7 +43,7 @@ def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]:
|
|||||||
start_index += 1
|
start_index += 1
|
||||||
|
|
||||||
if timerange.stoptype == 'line':
|
if timerange.stoptype == 'line':
|
||||||
start_index = len(tickerlist) + timerange.stopts
|
start_index = max(len(tickerlist) + timerange.stopts, 0)
|
||||||
if timerange.stoptype == 'index':
|
if timerange.stoptype == 'index':
|
||||||
stop_index = timerange.stopts
|
stop_index = timerange.stopts
|
||||||
elif timerange.stoptype == 'date':
|
elif timerange.stoptype == 'date':
|
||||||
@ -57,9 +57,7 @@ def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]:
|
|||||||
return tickerlist[start_index:stop_index]
|
return tickerlist[start_index:stop_index]
|
||||||
|
|
||||||
|
|
||||||
def load_tickerdata_file(
|
def load_tickerdata_file(datadir: Optional[Path], pair: str, ticker_interval: str,
|
||||||
datadir: Optional[Path], pair: str,
|
|
||||||
ticker_interval: str,
|
|
||||||
timerange: Optional[TimeRange] = None) -> Optional[list]:
|
timerange: Optional[TimeRange] = None) -> Optional[list]:
|
||||||
"""
|
"""
|
||||||
Load a pair from file, either .json.gz or .json
|
Load a pair from file, either .json.gz or .json
|
||||||
@ -68,13 +66,22 @@ def load_tickerdata_file(
|
|||||||
filename = pair_data_filename(datadir, pair, ticker_interval)
|
filename = pair_data_filename(datadir, pair, ticker_interval)
|
||||||
pairdata = misc.file_load_json(filename)
|
pairdata = misc.file_load_json(filename)
|
||||||
if not pairdata:
|
if not pairdata:
|
||||||
return None
|
return []
|
||||||
|
|
||||||
if timerange:
|
if timerange:
|
||||||
pairdata = trim_tickerlist(pairdata, timerange)
|
pairdata = trim_tickerlist(pairdata, timerange)
|
||||||
return pairdata
|
return pairdata
|
||||||
|
|
||||||
|
|
||||||
|
def store_tickerdata_file(datadir: Optional[Path], pair: str,
|
||||||
|
ticker_interval: str, data: list, is_zip: bool = False):
|
||||||
|
"""
|
||||||
|
Stores tickerdata to file
|
||||||
|
"""
|
||||||
|
filename = pair_data_filename(datadir, pair, ticker_interval)
|
||||||
|
misc.file_dump_json(filename, data, is_zip=is_zip)
|
||||||
|
|
||||||
|
|
||||||
def load_pair_history(pair: str,
|
def load_pair_history(pair: str,
|
||||||
ticker_interval: str,
|
ticker_interval: str,
|
||||||
datadir: Optional[Path],
|
datadir: Optional[Path],
|
||||||
@ -122,7 +129,7 @@ def load_pair_history(pair: str,
|
|||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f'No history data for pair: "{pair}", interval: {ticker_interval}. '
|
f'No history data for pair: "{pair}", interval: {ticker_interval}. '
|
||||||
'Use --refresh-pairs-cached option or download_backtest_data.py '
|
'Use --refresh-pairs-cached option or `freqtrade download-data` '
|
||||||
'script to download the data'
|
'script to download the data'
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
@ -177,11 +184,14 @@ def pair_data_filename(datadir: Optional[Path], pair: str, ticker_interval: str)
|
|||||||
return filename
|
return filename
|
||||||
|
|
||||||
|
|
||||||
def load_cached_data_for_updating(filename: Path, ticker_interval: str,
|
def load_cached_data_for_updating(datadir: Optional[Path], pair: str, ticker_interval: str,
|
||||||
timerange: Optional[TimeRange]) -> Tuple[List[Any],
|
timerange: Optional[TimeRange]) -> Tuple[List[Any],
|
||||||
Optional[int]]:
|
Optional[int]]:
|
||||||
"""
|
"""
|
||||||
Load cached data and choose what part of the data should be updated
|
Load cached data to download more data.
|
||||||
|
If timerange is passed in, checks wether data from an before the stored data will be downloaded.
|
||||||
|
If that's the case than what's available should be completely overwritten.
|
||||||
|
Only used by download_pair_history().
|
||||||
"""
|
"""
|
||||||
|
|
||||||
since_ms = None
|
since_ms = None
|
||||||
@ -195,9 +205,8 @@ def load_cached_data_for_updating(filename: Path, ticker_interval: str,
|
|||||||
since_ms = arrow.utcnow().shift(minutes=num_minutes).timestamp * 1000
|
since_ms = arrow.utcnow().shift(minutes=num_minutes).timestamp * 1000
|
||||||
|
|
||||||
# read the cached file
|
# read the cached file
|
||||||
if filename.is_file():
|
# Intentionally don't pass timerange in - since we need to load the full dataset.
|
||||||
with open(filename, "rt") as file:
|
data = load_tickerdata_file(datadir, pair, ticker_interval)
|
||||||
data = misc.json_load(file)
|
|
||||||
# remove the last item, could be incomplete candle
|
# remove the last item, could be incomplete candle
|
||||||
if data:
|
if data:
|
||||||
data.pop()
|
data.pop()
|
||||||
@ -239,29 +248,28 @@ def download_pair_history(datadir: Optional[Path],
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
filename = pair_data_filename(datadir, pair, ticker_interval)
|
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f'Download history data for pair: "{pair}", interval: {ticker_interval} '
|
f'Download history data for pair: "{pair}", interval: {ticker_interval} '
|
||||||
f'and store in {datadir}.'
|
f'and store in {datadir}.'
|
||||||
)
|
)
|
||||||
|
|
||||||
data, since_ms = load_cached_data_for_updating(filename, ticker_interval, timerange)
|
data, since_ms = load_cached_data_for_updating(datadir, pair, ticker_interval, timerange)
|
||||||
|
|
||||||
logger.debug("Current Start: %s", misc.format_ms_time(data[1][0]) if data else 'None')
|
logger.debug("Current Start: %s", misc.format_ms_time(data[1][0]) if data else 'None')
|
||||||
logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None')
|
logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None')
|
||||||
|
|
||||||
# Default since_ms to 30 days if nothing is given
|
# Default since_ms to 30 days if nothing is given
|
||||||
new_data = exchange.get_history(pair=pair, ticker_interval=ticker_interval,
|
new_data = exchange.get_historic_ohlcv(pair=pair, ticker_interval=ticker_interval,
|
||||||
since_ms=since_ms if since_ms
|
since_ms=since_ms if since_ms
|
||||||
else
|
else
|
||||||
int(arrow.utcnow().shift(days=-30).float_timestamp) * 1000)
|
int(arrow.utcnow().shift(
|
||||||
|
days=-30).float_timestamp) * 1000)
|
||||||
data.extend(new_data)
|
data.extend(new_data)
|
||||||
|
|
||||||
logger.debug("New Start: %s", misc.format_ms_time(data[0][0]))
|
logger.debug("New Start: %s", misc.format_ms_time(data[0][0]))
|
||||||
logger.debug("New End: %s", misc.format_ms_time(data[-1][0]))
|
logger.debug("New End: %s", misc.format_ms_time(data[-1][0]))
|
||||||
|
|
||||||
misc.file_dump_json(filename, data)
|
store_tickerdata_file(datadir, pair, ticker_interval, data=data)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -10,7 +10,7 @@ import utils_find_1st as utf1st
|
|||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade import constants, OperationalException
|
from freqtrade import constants, OperationalException
|
||||||
from freqtrade.configuration import Arguments, TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.data import history
|
from freqtrade.data import history
|
||||||
from freqtrade.strategy.interface import SellType
|
from freqtrade.strategy.interface import SellType
|
||||||
|
|
||||||
@ -75,7 +75,7 @@ class Edge():
|
|||||||
self._stoploss_range_step
|
self._stoploss_range_step
|
||||||
)
|
)
|
||||||
|
|
||||||
self._timerange: TimeRange = Arguments.parse_timerange("%s-" % arrow.now().shift(
|
self._timerange: TimeRange = TimeRange.parse_timerange("%s-" % arrow.now().shift(
|
||||||
days=-1 * self._since_number_of_days).format('YYYYMMDD'))
|
days=-1 * self._since_number_of_days).format('YYYYMMDD'))
|
||||||
|
|
||||||
self.fee = self.exchange.get_fee()
|
self.fee = self.exchange.get_fee()
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
from freqtrade.exchange.exchange import Exchange # noqa: F401
|
from freqtrade.exchange.exchange import Exchange # noqa: F401
|
||||||
from freqtrade.exchange.exchange import (is_exchange_bad, # noqa: F401
|
from freqtrade.exchange.exchange import (get_exchange_bad_reason, # noqa: F401
|
||||||
|
is_exchange_bad,
|
||||||
is_exchange_available,
|
is_exchange_available,
|
||||||
is_exchange_officially_supported,
|
is_exchange_officially_supported,
|
||||||
available_exchanges)
|
available_exchanges)
|
||||||
from freqtrade.exchange.exchange import (timeframe_to_seconds, # noqa: F401
|
from freqtrade.exchange.exchange import (timeframe_to_seconds, # noqa: F401
|
||||||
timeframe_to_minutes,
|
timeframe_to_minutes,
|
||||||
timeframe_to_msecs)
|
timeframe_to_msecs,
|
||||||
|
timeframe_to_next_date,
|
||||||
|
timeframe_to_prev_date)
|
||||||
from freqtrade.exchange.kraken import Kraken # noqa: F401
|
from freqtrade.exchange.kraken import Kraken # noqa: F401
|
||||||
from freqtrade.exchange.binance import Binance # noqa: F401
|
from freqtrade.exchange.binance import Binance # noqa: F401
|
||||||
|
@ -6,7 +6,7 @@ import asyncio
|
|||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
from math import ceil, floor
|
from math import ceil, floor
|
||||||
from random import randint
|
from random import randint
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
@ -25,6 +25,11 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
API_RETRY_COUNT = 4
|
API_RETRY_COUNT = 4
|
||||||
|
BAD_EXCHANGES = {
|
||||||
|
"bitmex": "Various reasons",
|
||||||
|
"bitstamp": "Does not provide history. "
|
||||||
|
"Details in https://github.com/freqtrade/freqtrade/issues/1983",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def retrier_async(f):
|
def retrier_async(f):
|
||||||
@ -260,7 +265,7 @@ class Exchange(object):
|
|||||||
|
|
||||||
if not self.markets:
|
if not self.markets:
|
||||||
logger.warning('Unable to validate pairs (assuming they are correct).')
|
logger.warning('Unable to validate pairs (assuming they are correct).')
|
||||||
# return
|
return
|
||||||
|
|
||||||
for pair in pairs:
|
for pair in pairs:
|
||||||
# Note: ccxt has BaseCurrency/QuoteCurrency format for pairs
|
# Note: ccxt has BaseCurrency/QuoteCurrency format for pairs
|
||||||
@ -269,6 +274,12 @@ class Exchange(object):
|
|||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f'Pair {pair} is not available on {self.name}. '
|
f'Pair {pair} is not available on {self.name}. '
|
||||||
f'Please remove {pair} from your whitelist.')
|
f'Please remove {pair} from your whitelist.')
|
||||||
|
elif self.markets[pair].get('info', {}).get('IsRestricted', False):
|
||||||
|
# Warn users about restricted pairs in whitelist.
|
||||||
|
# We cannot determine reliably if Users are affected.
|
||||||
|
logger.warning(f"Pair {pair} is restricted for some users on this exchange."
|
||||||
|
f"Please check if you are impacted by this restriction "
|
||||||
|
f"on the exchange and eventually remove {pair} from your whitelist.")
|
||||||
|
|
||||||
def get_valid_pair_combination(self, curr_1, curr_2) -> str:
|
def get_valid_pair_combination(self, curr_1, curr_2) -> str:
|
||||||
"""
|
"""
|
||||||
@ -365,7 +376,7 @@ class Exchange(object):
|
|||||||
'side': side,
|
'side': side,
|
||||||
'remaining': amount,
|
'remaining': amount,
|
||||||
'datetime': arrow.utcnow().isoformat(),
|
'datetime': arrow.utcnow().isoformat(),
|
||||||
'status': "open",
|
'status': "closed" if ordertype == "market" else "open",
|
||||||
'fee': None,
|
'fee': None,
|
||||||
"info": {}
|
"info": {}
|
||||||
}
|
}
|
||||||
@ -397,12 +408,12 @@ class Exchange(object):
|
|||||||
except ccxt.InsufficientFunds as e:
|
except ccxt.InsufficientFunds as e:
|
||||||
raise DependencyException(
|
raise DependencyException(
|
||||||
f'Insufficient funds to create {ordertype} {side} order on market {pair}.'
|
f'Insufficient funds to create {ordertype} {side} order on market {pair}.'
|
||||||
f'Tried to {side} amount {amount} at rate {rate} (total {rate * amount}).'
|
f'Tried to {side} amount {amount} at rate {rate}.'
|
||||||
f'Message: {e}') from e
|
f'Message: {e}') from e
|
||||||
except ccxt.InvalidOrder as e:
|
except ccxt.InvalidOrder as e:
|
||||||
raise DependencyException(
|
raise DependencyException(
|
||||||
f'Could not create {ordertype} {side} order on market {pair}.'
|
f'Could not create {ordertype} {side} order on market {pair}.'
|
||||||
f'Tried to {side} amount {amount} at rate {rate} (total {rate * amount}).'
|
f'Tried to {side} amount {amount} at rate {rate}.'
|
||||||
f'Message: {e}') from e
|
f'Message: {e}') from e
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
@ -461,7 +472,7 @@ class Exchange(object):
|
|||||||
|
|
||||||
order = self.create_order(pair, ordertype, 'sell', amount, rate, params)
|
order = self.create_order(pair, ordertype, 'sell', amount, rate, params)
|
||||||
logger.info('stoploss limit order added for %s. '
|
logger.info('stoploss limit order added for %s. '
|
||||||
'stop price: %s. limit: %s' % (pair, stop_price, rate))
|
'stop price: %s. limit: %s', pair, stop_price, rate)
|
||||||
return order
|
return order
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
@ -535,17 +546,22 @@ class Exchange(object):
|
|||||||
logger.info("returning cached ticker-data for %s", pair)
|
logger.info("returning cached ticker-data for %s", pair)
|
||||||
return self._cached_ticker[pair]
|
return self._cached_ticker[pair]
|
||||||
|
|
||||||
def get_history(self, pair: str, ticker_interval: str,
|
def get_historic_ohlcv(self, pair: str, ticker_interval: str,
|
||||||
since_ms: int) -> List:
|
since_ms: int) -> List:
|
||||||
"""
|
"""
|
||||||
Gets candle history using asyncio and returns the list of candles.
|
Gets candle history using asyncio and returns the list of candles.
|
||||||
Handles all async doing.
|
Handles all async doing.
|
||||||
|
Async over one pair, assuming we get `_ohlcv_candle_limit` candles per call.
|
||||||
|
:param pair: Pair to download
|
||||||
|
:param ticker_interval: Interval to get
|
||||||
|
:param since_ms: Timestamp in milliseconds to get history from
|
||||||
|
:returns List of tickers
|
||||||
"""
|
"""
|
||||||
return asyncio.get_event_loop().run_until_complete(
|
return asyncio.get_event_loop().run_until_complete(
|
||||||
self._async_get_history(pair=pair, ticker_interval=ticker_interval,
|
self._async_get_historic_ohlcv(pair=pair, ticker_interval=ticker_interval,
|
||||||
since_ms=since_ms))
|
since_ms=since_ms))
|
||||||
|
|
||||||
async def _async_get_history(self, pair: str,
|
async def _async_get_historic_ohlcv(self, pair: str,
|
||||||
ticker_interval: str,
|
ticker_interval: str,
|
||||||
since_ms: int) -> List:
|
since_ms: int) -> List:
|
||||||
|
|
||||||
@ -573,7 +589,10 @@ class Exchange(object):
|
|||||||
|
|
||||||
def refresh_latest_ohlcv(self, pair_list: List[Tuple[str, str]]) -> List[Tuple[str, List]]:
|
def refresh_latest_ohlcv(self, pair_list: List[Tuple[str, str]]) -> List[Tuple[str, List]]:
|
||||||
"""
|
"""
|
||||||
Refresh in-memory ohlcv asyncronously and set `_klines` with the result
|
Refresh in-memory ohlcv asynchronously and set `_klines` with the result
|
||||||
|
Loops asynchronously over pair_list and downloads all pairs async (semi-parallel).
|
||||||
|
:param pair_list: List of 2 element tuples containing pair, interval to refresh
|
||||||
|
:return: Returns a List of ticker-dataframes.
|
||||||
"""
|
"""
|
||||||
logger.debug("Refreshing ohlcv data for %d pairs", len(pair_list))
|
logger.debug("Refreshing ohlcv data for %d pairs", len(pair_list))
|
||||||
|
|
||||||
@ -621,7 +640,7 @@ class Exchange(object):
|
|||||||
async def _async_get_candle_history(self, pair: str, ticker_interval: str,
|
async def _async_get_candle_history(self, pair: str, ticker_interval: str,
|
||||||
since_ms: Optional[int] = None) -> Tuple[str, str, List]:
|
since_ms: Optional[int] = None) -> Tuple[str, str, List]:
|
||||||
"""
|
"""
|
||||||
Asyncronously gets candle histories using fetch_ohlcv
|
Asynchronously gets candle histories using fetch_ohlcv
|
||||||
returns tuple: (pair, ticker_interval, ohlcv_list)
|
returns tuple: (pair, ticker_interval, ohlcv_list)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
@ -677,8 +696,13 @@ class Exchange(object):
|
|||||||
@retrier
|
@retrier
|
||||||
def get_order(self, order_id: str, pair: str) -> Dict:
|
def get_order(self, order_id: str, pair: str) -> Dict:
|
||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
|
try:
|
||||||
order = self._dry_run_open_orders[order_id]
|
order = self._dry_run_open_orders[order_id]
|
||||||
return order
|
return order
|
||||||
|
except KeyError as e:
|
||||||
|
# Gracefully handle errors with dry-run orders.
|
||||||
|
raise InvalidOrderException(
|
||||||
|
f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e
|
||||||
try:
|
try:
|
||||||
return self._api.fetch_order(order_id, pair)
|
return self._api.fetch_order(order_id, pair)
|
||||||
except ccxt.InvalidOrder as e:
|
except ccxt.InvalidOrder as e:
|
||||||
@ -719,7 +743,8 @@ class Exchange(object):
|
|||||||
return []
|
return []
|
||||||
try:
|
try:
|
||||||
# Allow 5s offset to catch slight time offsets (discovered in #1185)
|
# Allow 5s offset to catch slight time offsets (discovered in #1185)
|
||||||
my_trades = self._api.fetch_my_trades(pair, since.timestamp() - 5)
|
# since needs to be int in milliseconds
|
||||||
|
my_trades = self._api.fetch_my_trades(pair, int((since.timestamp() - 5) * 1000))
|
||||||
matched_trades = [trade for trade in my_trades if trade['order'] == order_id]
|
matched_trades = [trade for trade in my_trades if trade['order'] == order_id]
|
||||||
|
|
||||||
return matched_trades
|
return matched_trades
|
||||||
@ -747,16 +772,20 @@ class Exchange(object):
|
|||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
|
|
||||||
def is_exchange_bad(exchange: str) -> bool:
|
def is_exchange_bad(exchange_name: str) -> bool:
|
||||||
return exchange in ['bitmex', 'bitstamp']
|
return exchange_name in BAD_EXCHANGES
|
||||||
|
|
||||||
|
|
||||||
def is_exchange_available(exchange: str, ccxt_module=None) -> bool:
|
def get_exchange_bad_reason(exchange_name: str) -> str:
|
||||||
return exchange in available_exchanges(ccxt_module)
|
return BAD_EXCHANGES.get(exchange_name, "")
|
||||||
|
|
||||||
|
|
||||||
def is_exchange_officially_supported(exchange: str) -> bool:
|
def is_exchange_available(exchange_name: str, ccxt_module=None) -> bool:
|
||||||
return exchange in ['bittrex', 'binance']
|
return exchange_name in available_exchanges(ccxt_module)
|
||||||
|
|
||||||
|
|
||||||
|
def is_exchange_officially_supported(exchange_name: str) -> bool:
|
||||||
|
return exchange_name in ['bittrex', 'binance']
|
||||||
|
|
||||||
|
|
||||||
def available_exchanges(ccxt_module=None) -> List[str]:
|
def available_exchanges(ccxt_module=None) -> List[str]:
|
||||||
@ -774,13 +803,45 @@ def timeframe_to_seconds(ticker_interval: str) -> int:
|
|||||||
|
|
||||||
def timeframe_to_minutes(ticker_interval: str) -> int:
|
def timeframe_to_minutes(ticker_interval: str) -> int:
|
||||||
"""
|
"""
|
||||||
Same as above, but returns minutes.
|
Same as timeframe_to_seconds, but returns minutes.
|
||||||
"""
|
"""
|
||||||
return ccxt.Exchange.parse_timeframe(ticker_interval) // 60
|
return ccxt.Exchange.parse_timeframe(ticker_interval) // 60
|
||||||
|
|
||||||
|
|
||||||
def timeframe_to_msecs(ticker_interval: str) -> int:
|
def timeframe_to_msecs(ticker_interval: str) -> int:
|
||||||
"""
|
"""
|
||||||
Same as above, but returns milliseconds.
|
Same as timeframe_to_seconds, but returns milliseconds.
|
||||||
"""
|
"""
|
||||||
return ccxt.Exchange.parse_timeframe(ticker_interval) * 1000
|
return ccxt.Exchange.parse_timeframe(ticker_interval) * 1000
|
||||||
|
|
||||||
|
|
||||||
|
def timeframe_to_prev_date(timeframe: str, date: datetime = None) -> datetime:
|
||||||
|
"""
|
||||||
|
Use Timeframe and determine last possible candle.
|
||||||
|
:param timeframe: timeframe in string format (e.g. "5m")
|
||||||
|
:param date: date to use. Defaults to utcnow()
|
||||||
|
:returns: date of previous candle (with utc timezone)
|
||||||
|
"""
|
||||||
|
if not date:
|
||||||
|
date = datetime.now(timezone.utc)
|
||||||
|
timeframe_secs = timeframe_to_seconds(timeframe)
|
||||||
|
# Get offset based on timerame_secs
|
||||||
|
offset = date.timestamp() % timeframe_secs
|
||||||
|
# Subtract seconds passed since last offset
|
||||||
|
new_timestamp = date.timestamp() - offset
|
||||||
|
return datetime.fromtimestamp(new_timestamp, tz=timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
|
def timeframe_to_next_date(timeframe: str, date: datetime = None) -> datetime:
|
||||||
|
"""
|
||||||
|
Use Timeframe and determine next candle.
|
||||||
|
:param timeframe: timeframe in string format (e.g. "5m")
|
||||||
|
:param date: date to use. Defaults to utcnow()
|
||||||
|
:returns: date of next candle (with utc timezone)
|
||||||
|
"""
|
||||||
|
prevdate = timeframe_to_prev_date(timeframe, date)
|
||||||
|
timeframe_secs = timeframe_to_seconds(timeframe)
|
||||||
|
|
||||||
|
# Add one interval to previous candle
|
||||||
|
new_timestamp = prevdate.timestamp() + timeframe_secs
|
||||||
|
return datetime.fromtimestamp(new_timestamp, tz=timezone.utc)
|
||||||
|
@ -16,11 +16,12 @@ from freqtrade import (DependencyException, OperationalException, InvalidOrderEx
|
|||||||
from freqtrade.data.converter import order_book_to_dataframe
|
from freqtrade.data.converter import order_book_to_dataframe
|
||||||
from freqtrade.data.dataprovider import DataProvider
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
from freqtrade.edge import Edge
|
from freqtrade.edge import Edge
|
||||||
from freqtrade.exchange import timeframe_to_minutes
|
from freqtrade.configuration import validate_config_consistency
|
||||||
|
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.rpc import RPCManager, RPCMessageType
|
from freqtrade.rpc import RPCManager, RPCMessageType
|
||||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver, PairListResolver
|
from freqtrade.resolvers import ExchangeResolver, StrategyResolver, PairListResolver
|
||||||
from freqtrade.state import State
|
from freqtrade.state import State, RunMode
|
||||||
from freqtrade.strategy.interface import SellType, IStrategy
|
from freqtrade.strategy.interface import SellType, IStrategy
|
||||||
from freqtrade.wallets import Wallets
|
from freqtrade.wallets import Wallets
|
||||||
|
|
||||||
@ -51,6 +52,9 @@ class FreqtradeBot(object):
|
|||||||
|
|
||||||
self.strategy: IStrategy = StrategyResolver(self.config).strategy
|
self.strategy: IStrategy = StrategyResolver(self.config).strategy
|
||||||
|
|
||||||
|
# Check config consistency here since strategies can set certain options
|
||||||
|
validate_config_consistency(config)
|
||||||
|
|
||||||
self.rpc: RPCManager = RPCManager(self)
|
self.rpc: RPCManager = RPCManager(self)
|
||||||
|
|
||||||
self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange
|
self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange
|
||||||
@ -75,6 +79,12 @@ class FreqtradeBot(object):
|
|||||||
persistence.init(self.config.get('db_url', None),
|
persistence.init(self.config.get('db_url', None),
|
||||||
clean_open_orders=self.config.get('dry_run', False))
|
clean_open_orders=self.config.get('dry_run', False))
|
||||||
|
|
||||||
|
# Stoploss on exchange does not make sense, therefore we need to disable that.
|
||||||
|
if (self.dataprovider.runmode == RunMode.DRY_RUN and
|
||||||
|
self.strategy.order_types.get('stoploss_on_exchange', False)):
|
||||||
|
logger.info("Disabling stoploss_on_exchange during dry-run.")
|
||||||
|
self.strategy.order_types['stoploss_on_exchange'] = False
|
||||||
|
config['order_types']['stoploss_on_exchange'] = False
|
||||||
# Set initial bot state from config
|
# Set initial bot state from config
|
||||||
initial_state = self.config.get('initial_state')
|
initial_state = self.config.get('initial_state')
|
||||||
self.state = State[initial_state.upper()] if initial_state else State.STOPPED
|
self.state = State[initial_state.upper()] if initial_state else State.STOPPED
|
||||||
@ -99,13 +109,12 @@ class FreqtradeBot(object):
|
|||||||
# Adjust stoploss if it was changed
|
# Adjust stoploss if it was changed
|
||||||
Trade.stoploss_reinitialization(self.strategy.stoploss)
|
Trade.stoploss_reinitialization(self.strategy.stoploss)
|
||||||
|
|
||||||
def process(self) -> bool:
|
def process(self) -> None:
|
||||||
"""
|
"""
|
||||||
Queries the persistence layer for open trades and handles them,
|
Queries the persistence layer for open trades and handles them,
|
||||||
otherwise a new trade is created.
|
otherwise a new trade is created.
|
||||||
:return: True if one or more trades has been created or closed, False otherwise
|
:return: True if one or more trades has been created or closed, False otherwise
|
||||||
"""
|
"""
|
||||||
state_changed = False
|
|
||||||
|
|
||||||
# Check whether markets have to be reloaded
|
# Check whether markets have to be reloaded
|
||||||
self.exchange._reload_markets()
|
self.exchange._reload_markets()
|
||||||
@ -132,19 +141,17 @@ class FreqtradeBot(object):
|
|||||||
|
|
||||||
# First process current opened trades
|
# First process current opened trades
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
state_changed |= self.process_maybe_execute_sell(trade)
|
self.process_maybe_execute_sell(trade)
|
||||||
|
|
||||||
# Then looking for buy opportunities
|
# Then looking for buy opportunities
|
||||||
if len(trades) < self.config['max_open_trades']:
|
if len(trades) < self.config['max_open_trades']:
|
||||||
state_changed = self.process_maybe_execute_buy()
|
self.process_maybe_execute_buy()
|
||||||
|
|
||||||
if 'unfilledtimeout' in self.config:
|
if 'unfilledtimeout' in self.config:
|
||||||
# Check and handle any timed out open orders
|
# Check and handle any timed out open orders
|
||||||
self.check_handle_timedout()
|
self.check_handle_timedout()
|
||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
|
|
||||||
return state_changed
|
|
||||||
|
|
||||||
def _extend_whitelist_with_trades(self, whitelist: List[str], trades: List[Any]):
|
def _extend_whitelist_with_trades(self, whitelist: List[str], trades: List[Any]):
|
||||||
"""
|
"""
|
||||||
Extend whitelist with pairs from open trades
|
Extend whitelist with pairs from open trades
|
||||||
@ -253,11 +260,12 @@ class FreqtradeBot(object):
|
|||||||
amount_reserve_percent = max(amount_reserve_percent, 0.5)
|
amount_reserve_percent = max(amount_reserve_percent, 0.5)
|
||||||
return min(min_stake_amounts) / amount_reserve_percent
|
return min(min_stake_amounts) / amount_reserve_percent
|
||||||
|
|
||||||
def create_trade(self) -> bool:
|
def create_trades(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Checks the implemented trading indicator(s) for a randomly picked pair,
|
Checks the implemented trading strategy for buy-signals, using the active pair whitelist.
|
||||||
if one pair triggers the buy_signal a new trade record gets created
|
If a pair triggers the buy_signal a new trade record gets created.
|
||||||
:return: True if a trade object has been created and persisted, False otherwise
|
Checks pairs as long as the open trade count is below `max_open_trades`.
|
||||||
|
:return: True if at least one trade has been created.
|
||||||
"""
|
"""
|
||||||
interval = self.strategy.ticker_interval
|
interval = self.strategy.ticker_interval
|
||||||
whitelist = copy.deepcopy(self.active_pair_whitelist)
|
whitelist = copy.deepcopy(self.active_pair_whitelist)
|
||||||
@ -276,15 +284,19 @@ class FreqtradeBot(object):
|
|||||||
logger.info("No currency pair in whitelist, but checking to sell open trades.")
|
logger.info("No currency pair in whitelist, but checking to sell open trades.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
buycount = 0
|
||||||
# running get_signal on historical data fetched
|
# running get_signal on historical data fetched
|
||||||
for _pair in whitelist:
|
for _pair in whitelist:
|
||||||
|
if self.strategy.is_pair_locked(_pair):
|
||||||
|
logger.info(f"Pair {_pair} is currently locked.")
|
||||||
|
continue
|
||||||
(buy, sell) = self.strategy.get_signal(
|
(buy, sell) = self.strategy.get_signal(
|
||||||
_pair, interval, self.dataprovider.ohlcv(_pair, self.strategy.ticker_interval))
|
_pair, interval, self.dataprovider.ohlcv(_pair, self.strategy.ticker_interval))
|
||||||
|
|
||||||
if buy and not sell:
|
if buy and not sell and len(Trade.get_open_trades()) < self.config['max_open_trades']:
|
||||||
stake_amount = self._get_trade_stake_amount(_pair)
|
stake_amount = self._get_trade_stake_amount(_pair)
|
||||||
if not stake_amount:
|
if not stake_amount:
|
||||||
return False
|
continue
|
||||||
|
|
||||||
logger.info(f"Buy signal found: about create a new trade with stake_amount: "
|
logger.info(f"Buy signal found: about create a new trade with stake_amount: "
|
||||||
f"{stake_amount} ...")
|
f"{stake_amount} ...")
|
||||||
@ -294,12 +306,13 @@ class FreqtradeBot(object):
|
|||||||
if (bidstrat_check_depth_of_market.get('enabled', False)) and\
|
if (bidstrat_check_depth_of_market.get('enabled', False)) and\
|
||||||
(bidstrat_check_depth_of_market.get('bids_to_ask_delta', 0) > 0):
|
(bidstrat_check_depth_of_market.get('bids_to_ask_delta', 0) > 0):
|
||||||
if self._check_depth_of_market_buy(_pair, bidstrat_check_depth_of_market):
|
if self._check_depth_of_market_buy(_pair, bidstrat_check_depth_of_market):
|
||||||
return self.execute_buy(_pair, stake_amount)
|
buycount += self.execute_buy(_pair, stake_amount)
|
||||||
else:
|
else:
|
||||||
return False
|
continue
|
||||||
return self.execute_buy(_pair, stake_amount)
|
|
||||||
|
|
||||||
return False
|
buycount += self.execute_buy(_pair, stake_amount)
|
||||||
|
|
||||||
|
return buycount > 0
|
||||||
|
|
||||||
def _check_depth_of_market_buy(self, pair: str, conf: Dict) -> bool:
|
def _check_depth_of_market_buy(self, pair: str, conf: Dict) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -423,21 +436,17 @@ class FreqtradeBot(object):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def process_maybe_execute_buy(self) -> bool:
|
def process_maybe_execute_buy(self) -> None:
|
||||||
"""
|
"""
|
||||||
Tries to execute a buy trade in a safe way
|
Tries to execute a buy trade in a safe way
|
||||||
:return: True if executed
|
:return: True if executed
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Create entity and execute trade
|
# Create entity and execute trade
|
||||||
if self.create_trade():
|
if not self.create_trades():
|
||||||
return True
|
logger.info('Found no buy signals for whitelisted currencies. Trying again...')
|
||||||
|
|
||||||
logger.info('Found no buy signals for whitelisted currencies. Trying again..')
|
|
||||||
return False
|
|
||||||
except DependencyException as exception:
|
except DependencyException as exception:
|
||||||
logger.warning('Unable to create trade: %s', exception)
|
logger.warning('Unable to create trade: %s', exception)
|
||||||
return False
|
|
||||||
|
|
||||||
def process_maybe_execute_sell(self, trade: Trade) -> bool:
|
def process_maybe_execute_sell(self, trade: Trade) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -653,6 +662,7 @@ class FreqtradeBot(object):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
except DependencyException as exception:
|
except DependencyException as exception:
|
||||||
|
trade.stoploss_order_id = None
|
||||||
logger.warning('Unable to place a stoploss order on exchange: %s', exception)
|
logger.warning('Unable to place a stoploss order on exchange: %s', exception)
|
||||||
|
|
||||||
# If stoploss order is canceled for some reason we add it
|
# If stoploss order is canceled for some reason we add it
|
||||||
@ -665,6 +675,7 @@ class FreqtradeBot(object):
|
|||||||
trade.stoploss_order_id = str(stoploss_order_id)
|
trade.stoploss_order_id = str(stoploss_order_id)
|
||||||
return False
|
return False
|
||||||
except DependencyException as exception:
|
except DependencyException as exception:
|
||||||
|
trade.stoploss_order_id = None
|
||||||
logger.warning('Stoploss order was cancelled, '
|
logger.warning('Stoploss order was cancelled, '
|
||||||
'but unable to recreate one: %s', exception)
|
'but unable to recreate one: %s', exception)
|
||||||
|
|
||||||
@ -672,6 +683,9 @@ class FreqtradeBot(object):
|
|||||||
if stoploss_order and stoploss_order['status'] == 'closed':
|
if stoploss_order and stoploss_order['status'] == 'closed':
|
||||||
trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
|
trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
|
||||||
trade.update(stoploss_order)
|
trade.update(stoploss_order)
|
||||||
|
# Lock pair for one candle to prevent immediate rebuys
|
||||||
|
self.strategy.lock_pair(trade.pair,
|
||||||
|
timeframe_to_next_date(self.config['ticker_interval']))
|
||||||
self._notify_sell(trade)
|
self._notify_sell(trade)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -714,7 +728,8 @@ class FreqtradeBot(object):
|
|||||||
)['id']
|
)['id']
|
||||||
trade.stoploss_order_id = str(stoploss_order_id)
|
trade.stoploss_order_id = str(stoploss_order_id)
|
||||||
except DependencyException:
|
except DependencyException:
|
||||||
logger.exception(f"Could create trailing stoploss order "
|
trade.stoploss_order_id = None
|
||||||
|
logger.exception(f"Could not create trailing stoploss order "
|
||||||
f"for pair {trade.pair}.")
|
f"for pair {trade.pair}.")
|
||||||
|
|
||||||
def _check_and_execute_sell(self, trade: Trade, sell_rate: float,
|
def _check_and_execute_sell(self, trade: Trade, sell_rate: float,
|
||||||
@ -869,16 +884,23 @@ class FreqtradeBot(object):
|
|||||||
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
|
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
|
||||||
|
|
||||||
# Execute sell and update trade record
|
# Execute sell and update trade record
|
||||||
order_id = self.exchange.sell(pair=str(trade.pair),
|
order = self.exchange.sell(pair=str(trade.pair),
|
||||||
ordertype=self.strategy.order_types[sell_type],
|
ordertype=self.strategy.order_types[sell_type],
|
||||||
amount=trade.amount, rate=limit,
|
amount=trade.amount, rate=limit,
|
||||||
time_in_force=self.strategy.order_time_in_force['sell']
|
time_in_force=self.strategy.order_time_in_force['sell']
|
||||||
)['id']
|
)
|
||||||
|
|
||||||
trade.open_order_id = order_id
|
trade.open_order_id = order['id']
|
||||||
trade.close_rate_requested = limit
|
trade.close_rate_requested = limit
|
||||||
trade.sell_reason = sell_reason.value
|
trade.sell_reason = sell_reason.value
|
||||||
|
# In case of market sell orders the order can be closed immediately
|
||||||
|
if order.get('status', 'unknown') == 'closed':
|
||||||
|
trade.update(order)
|
||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
|
|
||||||
|
# Lock pair for one candle to prevent immediate rebuys
|
||||||
|
self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['ticker_interval']))
|
||||||
|
|
||||||
self._notify_sell(trade)
|
self._notify_sell(trade)
|
||||||
|
|
||||||
def _notify_sell(self, trade: Trade):
|
def _notify_sell(self, trade: Trade):
|
||||||
|
@ -5,11 +5,12 @@ import gzip
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing.io import IO
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import rapidjson
|
import rapidjson
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -39,7 +40,7 @@ def datesarray_to_datetimearray(dates: np.ndarray) -> np.ndarray:
|
|||||||
return dates.dt.to_pydatetime()
|
return dates.dt.to_pydatetime()
|
||||||
|
|
||||||
|
|
||||||
def file_dump_json(filename, data, is_zip=False) -> None:
|
def file_dump_json(filename: Path, data, is_zip=False) -> None:
|
||||||
"""
|
"""
|
||||||
Dump JSON data into a file
|
Dump JSON data into a file
|
||||||
:param filename: file to create
|
:param filename: file to create
|
||||||
@ -49,8 +50,8 @@ def file_dump_json(filename, data, is_zip=False) -> None:
|
|||||||
logger.info(f'dumping json to "{filename}"')
|
logger.info(f'dumping json to "{filename}"')
|
||||||
|
|
||||||
if is_zip:
|
if is_zip:
|
||||||
if not filename.endswith('.gz'):
|
if filename.suffix != '.gz':
|
||||||
filename = filename + '.gz'
|
filename = filename.with_suffix('.gz')
|
||||||
with gzip.open(filename, 'w') as fp:
|
with gzip.open(filename, 'w') as fp:
|
||||||
rapidjson.dump(data, fp, default=str, number_mode=rapidjson.NM_NATIVE)
|
rapidjson.dump(data, fp, default=str, number_mode=rapidjson.NM_NATIVE)
|
||||||
else:
|
else:
|
||||||
@ -60,7 +61,7 @@ def file_dump_json(filename, data, is_zip=False) -> None:
|
|||||||
logger.debug(f'done json to "{filename}"')
|
logger.debug(f'done json to "{filename}"')
|
||||||
|
|
||||||
|
|
||||||
def json_load(datafile):
|
def json_load(datafile: IO):
|
||||||
"""
|
"""
|
||||||
load data with rapidjson
|
load data with rapidjson
|
||||||
Use this to have a consistent experience,
|
Use this to have a consistent experience,
|
||||||
|
@ -64,14 +64,14 @@ def start_hyperopt(args: Namespace) -> None:
|
|||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
# Import here to avoid loading hyperopt module when it's not used
|
# Import here to avoid loading hyperopt module when it's not used
|
||||||
from freqtrade.optimize.hyperopt import Hyperopt, HYPEROPT_LOCKFILE
|
from freqtrade.optimize.hyperopt import Hyperopt
|
||||||
|
|
||||||
# Initialize configuration
|
# Initialize configuration
|
||||||
config = setup_configuration(args, RunMode.HYPEROPT)
|
config = setup_configuration(args, RunMode.HYPEROPT)
|
||||||
|
|
||||||
logger.info('Starting freqtrade in Hyperopt mode')
|
logger.info('Starting freqtrade in Hyperopt mode')
|
||||||
|
|
||||||
lock = FileLock(HYPEROPT_LOCKFILE)
|
lock = FileLock(Hyperopt.get_lock_filename(config))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with lock.acquire(timeout=1):
|
with lock.acquire(timeout=1):
|
||||||
|
@ -10,9 +10,9 @@ from pathlib import Path
|
|||||||
from typing import Any, Dict, List, NamedTuple, Optional
|
from typing import Any, Dict, List, NamedTuple, Optional
|
||||||
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
from tabulate import tabulate
|
|
||||||
|
|
||||||
from freqtrade.configuration import Arguments
|
from freqtrade import OperationalException
|
||||||
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.data import history
|
from freqtrade.data import history
|
||||||
from freqtrade.data.dataprovider import DataProvider
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
from freqtrade.exchange import timeframe_to_minutes
|
from freqtrade.exchange import timeframe_to_minutes
|
||||||
@ -21,6 +21,7 @@ from freqtrade.persistence import Trade
|
|||||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||||
from freqtrade.state import RunMode
|
from freqtrade.state import RunMode
|
||||||
from freqtrade.strategy.interface import IStrategy, SellType
|
from freqtrade.strategy.interface import IStrategy, SellType
|
||||||
|
from tabulate import tabulate
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -88,6 +89,9 @@ class Backtesting(object):
|
|||||||
Load strategy into backtesting
|
Load strategy into backtesting
|
||||||
"""
|
"""
|
||||||
self.strategy = strategy
|
self.strategy = strategy
|
||||||
|
if "ticker_interval" not in self.config:
|
||||||
|
raise OperationalException("Ticker-interval needs to be set in either configuration "
|
||||||
|
"or as cli argument `--ticker-interval 5m`")
|
||||||
|
|
||||||
self.ticker_interval = self.config.get('ticker_interval')
|
self.ticker_interval = self.config.get('ticker_interval')
|
||||||
self.ticker_interval_mins = timeframe_to_minutes(self.ticker_interval)
|
self.ticker_interval_mins = timeframe_to_minutes(self.ticker_interval)
|
||||||
@ -186,7 +190,7 @@ class Backtesting(object):
|
|||||||
return tabulate(tabular_data, headers=headers, # type: ignore
|
return tabulate(tabular_data, headers=headers, # type: ignore
|
||||||
floatfmt=floatfmt, tablefmt="pipe")
|
floatfmt=floatfmt, tablefmt="pipe")
|
||||||
|
|
||||||
def _store_backtest_result(self, recordfilename: str, results: DataFrame,
|
def _store_backtest_result(self, recordfilename: Path, results: DataFrame,
|
||||||
strategyname: Optional[str] = None) -> None:
|
strategyname: Optional[str] = None) -> None:
|
||||||
|
|
||||||
records = [(t.pair, t.profit_percent, t.open_time.timestamp(),
|
records = [(t.pair, t.profit_percent, t.open_time.timestamp(),
|
||||||
@ -197,10 +201,10 @@ class Backtesting(object):
|
|||||||
if records:
|
if records:
|
||||||
if strategyname:
|
if strategyname:
|
||||||
# Inject strategyname to filename
|
# Inject strategyname to filename
|
||||||
recname = Path(recordfilename)
|
recordfilename = Path.joinpath(
|
||||||
recordfilename = str(Path.joinpath(
|
recordfilename.parent,
|
||||||
recname.parent, f'{recname.stem}-{strategyname}').with_suffix(recname.suffix))
|
f'{recordfilename.stem}-{strategyname}').with_suffix(recordfilename.suffix)
|
||||||
logger.info('Dumping backtest results to %s', recordfilename)
|
logger.info(f'Dumping backtest results to {recordfilename}')
|
||||||
file_dump_json(recordfilename, records)
|
file_dump_json(recordfilename, records)
|
||||||
|
|
||||||
def _get_ticker_list(self, processed) -> Dict[str, DataFrame]:
|
def _get_ticker_list(self, processed) -> Dict[str, DataFrame]:
|
||||||
@ -373,7 +377,9 @@ class Backtesting(object):
|
|||||||
continue
|
continue
|
||||||
trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1
|
trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1
|
||||||
|
|
||||||
trade_entry = self._get_sell_trade_entry(pair, row, ticker[pair][indexes[pair]:],
|
# since indexes has been incremented before, we need to go one step back to
|
||||||
|
# also check the buying candle for sell conditions.
|
||||||
|
trade_entry = self._get_sell_trade_entry(pair, row, ticker[pair][indexes[pair]-1:],
|
||||||
trade_count_lock, stake_amount,
|
trade_count_lock, stake_amount,
|
||||||
max_open_trades)
|
max_open_trades)
|
||||||
|
|
||||||
@ -398,7 +404,7 @@ class Backtesting(object):
|
|||||||
logger.info('Using stake_currency: %s ...', self.config['stake_currency'])
|
logger.info('Using stake_currency: %s ...', self.config['stake_currency'])
|
||||||
logger.info('Using stake_amount: %s ...', self.config['stake_amount'])
|
logger.info('Using stake_amount: %s ...', self.config['stake_amount'])
|
||||||
|
|
||||||
timerange = Arguments.parse_timerange(None if self.config.get(
|
timerange = TimeRange.parse_timerange(None if self.config.get(
|
||||||
'timerange') is None else str(self.config.get('timerange')))
|
'timerange') is None else str(self.config.get('timerange')))
|
||||||
data = history.load_data(
|
data = history.load_data(
|
||||||
datadir=Path(self.config['datadir']) if self.config.get('datadir') else None,
|
datadir=Path(self.config['datadir']) if self.config.get('datadir') else None,
|
||||||
@ -407,7 +413,6 @@ class Backtesting(object):
|
|||||||
refresh_pairs=self.config.get('refresh_pairs', False),
|
refresh_pairs=self.config.get('refresh_pairs', False),
|
||||||
exchange=self.exchange,
|
exchange=self.exchange,
|
||||||
timerange=timerange,
|
timerange=timerange,
|
||||||
live=self.config.get('live', False)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
@ -452,7 +457,7 @@ class Backtesting(object):
|
|||||||
for strategy, results in all_results.items():
|
for strategy, results in all_results.items():
|
||||||
|
|
||||||
if self.config.get('export', False):
|
if self.config.get('export', False):
|
||||||
self._store_backtest_result(self.config['exportfilename'], results,
|
self._store_backtest_result(Path(self.config['exportfilename']), results,
|
||||||
strategy if len(self.strategylist) > 1 else None)
|
strategy if len(self.strategylist) > 1 else None)
|
||||||
|
|
||||||
print(f"Result for strategy {strategy}")
|
print(f"Result for strategy {strategy}")
|
||||||
|
@ -5,7 +5,7 @@ from typing import Any, Callable, Dict, List
|
|||||||
|
|
||||||
import talib.abstract as ta
|
import talib.abstract as ta
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
from skopt.space import Categorical, Dimension, Integer, Real
|
from skopt.space import Categorical, Dimension, Integer
|
||||||
|
|
||||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||||
from freqtrade.optimize.hyperopt_interface import IHyperOpt
|
from freqtrade.optimize.hyperopt_interface import IHyperOpt
|
||||||
@ -13,38 +13,49 @@ from freqtrade.optimize.hyperopt_interface import IHyperOpt
|
|||||||
|
|
||||||
class DefaultHyperOpts(IHyperOpt):
|
class DefaultHyperOpts(IHyperOpt):
|
||||||
"""
|
"""
|
||||||
Default hyperopt provided by freqtrade bot.
|
Default hyperopt provided by the Freqtrade bot.
|
||||||
You can override it with your own hyperopt
|
You can override it with your own Hyperopt
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Add several indicators needed for buy and sell strategies defined below.
|
||||||
|
"""
|
||||||
|
# ADX
|
||||||
dataframe['adx'] = ta.ADX(dataframe)
|
dataframe['adx'] = ta.ADX(dataframe)
|
||||||
|
# MACD
|
||||||
macd = ta.MACD(dataframe)
|
macd = ta.MACD(dataframe)
|
||||||
dataframe['macd'] = macd['macd']
|
dataframe['macd'] = macd['macd']
|
||||||
dataframe['macdsignal'] = macd['macdsignal']
|
dataframe['macdsignal'] = macd['macdsignal']
|
||||||
|
# MFI
|
||||||
dataframe['mfi'] = ta.MFI(dataframe)
|
dataframe['mfi'] = ta.MFI(dataframe)
|
||||||
|
# RSI
|
||||||
dataframe['rsi'] = ta.RSI(dataframe)
|
dataframe['rsi'] = ta.RSI(dataframe)
|
||||||
|
# Stochastic Fast
|
||||||
stoch_fast = ta.STOCHF(dataframe)
|
stoch_fast = ta.STOCHF(dataframe)
|
||||||
dataframe['fastd'] = stoch_fast['fastd']
|
dataframe['fastd'] = stoch_fast['fastd']
|
||||||
|
# Minus-DI
|
||||||
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
||||||
# Bollinger bands
|
# Bollinger bands
|
||||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||||
dataframe['bb_lowerband'] = bollinger['lower']
|
dataframe['bb_lowerband'] = bollinger['lower']
|
||||||
dataframe['bb_upperband'] = bollinger['upper']
|
dataframe['bb_upperband'] = bollinger['upper']
|
||||||
|
# SAR
|
||||||
dataframe['sar'] = ta.SAR(dataframe)
|
dataframe['sar'] = ta.SAR(dataframe)
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
|
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
|
||||||
"""
|
"""
|
||||||
Define the buy strategy parameters to be used by hyperopt
|
Define the buy strategy parameters to be used by Hyperopt.
|
||||||
"""
|
"""
|
||||||
def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Buy strategy Hyperopt will build and use
|
Buy strategy Hyperopt will build and use.
|
||||||
"""
|
"""
|
||||||
conditions = []
|
conditions = []
|
||||||
|
|
||||||
# GUARDS AND TRENDS
|
# GUARDS AND TRENDS
|
||||||
if 'mfi-enabled' in params and params['mfi-enabled']:
|
if 'mfi-enabled' in params and params['mfi-enabled']:
|
||||||
conditions.append(dataframe['mfi'] < params['mfi-value'])
|
conditions.append(dataframe['mfi'] < params['mfi-value'])
|
||||||
@ -80,7 +91,7 @@ class DefaultHyperOpts(IHyperOpt):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def indicator_space() -> List[Dimension]:
|
def indicator_space() -> List[Dimension]:
|
||||||
"""
|
"""
|
||||||
Define your Hyperopt space for searching strategy parameters
|
Define your Hyperopt space for searching buy strategy parameters.
|
||||||
"""
|
"""
|
||||||
return [
|
return [
|
||||||
Integer(10, 25, name='mfi-value'),
|
Integer(10, 25, name='mfi-value'),
|
||||||
@ -97,14 +108,14 @@ class DefaultHyperOpts(IHyperOpt):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def sell_strategy_generator(params: Dict[str, Any]) -> Callable:
|
def sell_strategy_generator(params: Dict[str, Any]) -> Callable:
|
||||||
"""
|
"""
|
||||||
Define the sell strategy parameters to be used by hyperopt
|
Define the sell strategy parameters to be used by Hyperopt.
|
||||||
"""
|
"""
|
||||||
def populate_sell_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_sell_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Sell strategy Hyperopt will build and use
|
Sell strategy Hyperopt will build and use.
|
||||||
"""
|
"""
|
||||||
# print(params)
|
|
||||||
conditions = []
|
conditions = []
|
||||||
|
|
||||||
# GUARDS AND TRENDS
|
# GUARDS AND TRENDS
|
||||||
if 'sell-mfi-enabled' in params and params['sell-mfi-enabled']:
|
if 'sell-mfi-enabled' in params and params['sell-mfi-enabled']:
|
||||||
conditions.append(dataframe['mfi'] > params['sell-mfi-value'])
|
conditions.append(dataframe['mfi'] > params['sell-mfi-value'])
|
||||||
@ -140,7 +151,7 @@ class DefaultHyperOpts(IHyperOpt):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def sell_indicator_space() -> List[Dimension]:
|
def sell_indicator_space() -> List[Dimension]:
|
||||||
"""
|
"""
|
||||||
Define your Hyperopt space for searching sell strategy parameters
|
Define your Hyperopt space for searching sell strategy parameters.
|
||||||
"""
|
"""
|
||||||
return [
|
return [
|
||||||
Integer(75, 100, name='sell-mfi-value'),
|
Integer(75, 100, name='sell-mfi-value'),
|
||||||
@ -156,47 +167,11 @@ class DefaultHyperOpts(IHyperOpt):
|
|||||||
'sell-sar_reversal'], name='sell-trigger')
|
'sell-sar_reversal'], name='sell-trigger')
|
||||||
]
|
]
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def generate_roi_table(params: Dict) -> Dict[int, float]:
|
|
||||||
"""
|
|
||||||
Generate the ROI table that will be used by Hyperopt
|
|
||||||
"""
|
|
||||||
roi_table = {}
|
|
||||||
roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3']
|
|
||||||
roi_table[params['roi_t3']] = params['roi_p1'] + params['roi_p2']
|
|
||||||
roi_table[params['roi_t3'] + params['roi_t2']] = params['roi_p1']
|
|
||||||
roi_table[params['roi_t3'] + params['roi_t2'] + params['roi_t1']] = 0
|
|
||||||
|
|
||||||
return roi_table
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def stoploss_space() -> List[Dimension]:
|
|
||||||
"""
|
|
||||||
Stoploss Value to search
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
Real(-0.5, -0.02, name='stoploss'),
|
|
||||||
]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def roi_space() -> List[Dimension]:
|
|
||||||
"""
|
|
||||||
Values to search for each ROI steps
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
Integer(10, 120, name='roi_t1'),
|
|
||||||
Integer(10, 60, name='roi_t2'),
|
|
||||||
Integer(10, 40, name='roi_t3'),
|
|
||||||
Real(0.01, 0.04, name='roi_p1'),
|
|
||||||
Real(0.01, 0.07, name='roi_p2'),
|
|
||||||
Real(0.01, 0.20, name='roi_p3'),
|
|
||||||
]
|
|
||||||
|
|
||||||
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Based on TA indicators. Should be a copy of from strategy
|
Based on TA indicators. Should be a copy of same method from strategy.
|
||||||
must align to populate_indicators in this file
|
Must align to populate_indicators in this file.
|
||||||
Only used when --spaces does not include buy
|
Only used when --spaces does not include buy space.
|
||||||
"""
|
"""
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
(
|
(
|
||||||
@ -211,9 +186,9 @@ class DefaultHyperOpts(IHyperOpt):
|
|||||||
|
|
||||||
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Based on TA indicators. Should be a copy of from strategy
|
Based on TA indicators. Should be a copy of same method from strategy.
|
||||||
must align to populate_indicators in this file
|
Must align to populate_indicators in this file.
|
||||||
Only used when --spaces does not include sell
|
Only used when --spaces does not include sell space.
|
||||||
"""
|
"""
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
(
|
(
|
||||||
@ -223,4 +198,5 @@ class DefaultHyperOpts(IHyperOpt):
|
|||||||
(dataframe['fastd'] > 54)
|
(dataframe['fastd'] > 54)
|
||||||
),
|
),
|
||||||
'sell'] = 1
|
'sell'] = 1
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
@ -9,7 +9,7 @@ from tabulate import tabulate
|
|||||||
from freqtrade import constants
|
from freqtrade import constants
|
||||||
from freqtrade.edge import Edge
|
from freqtrade.edge import Edge
|
||||||
|
|
||||||
from freqtrade.configuration import Arguments
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.resolvers import StrategyResolver
|
from freqtrade.resolvers import StrategyResolver
|
||||||
|
|
||||||
@ -41,7 +41,7 @@ class EdgeCli(object):
|
|||||||
self.edge = Edge(config, self.exchange, self.strategy)
|
self.edge = Edge(config, self.exchange, self.strategy)
|
||||||
self.edge._refresh_pairs = self.config.get('refresh_pairs', False)
|
self.edge._refresh_pairs = self.config.get('refresh_pairs', False)
|
||||||
|
|
||||||
self.timerange = Arguments.parse_timerange(None if self.config.get(
|
self.timerange = TimeRange.parse_timerange(None if self.config.get(
|
||||||
'timerange') is None else str(self.config.get('timerange')))
|
'timerange') is None else str(self.config.get('timerange')))
|
||||||
|
|
||||||
self.edge._timerange = self.timerange
|
self.edge._timerange = self.timerange
|
||||||
|
@ -5,20 +5,24 @@ This module contains the hyperopt logic
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
import rapidjson
|
||||||
|
|
||||||
|
from colorama import init as colorama_init
|
||||||
|
from colorama import Fore, Style
|
||||||
from joblib import Parallel, delayed, dump, load, wrap_non_picklable_objects, cpu_count
|
from joblib import Parallel, delayed, dump, load, wrap_non_picklable_objects, cpu_count
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
from skopt import Optimizer
|
from skopt import Optimizer
|
||||||
from skopt.space import Dimension
|
from skopt.space import Dimension
|
||||||
|
|
||||||
from freqtrade.configuration import Arguments
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.data.history import load_data, get_timeframe
|
from freqtrade.data.history import load_data, get_timeframe
|
||||||
from freqtrade.optimize.backtesting import Backtesting
|
from freqtrade.optimize.backtesting import Backtesting
|
||||||
# Import IHyperOptLoss to allow users import from this file
|
# Import IHyperOptLoss to allow users import from this file
|
||||||
@ -31,12 +35,9 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
INITIAL_POINTS = 30
|
INITIAL_POINTS = 30
|
||||||
MAX_LOSS = 100000 # just a big enough number to be bad result in loss optimization
|
MAX_LOSS = 100000 # just a big enough number to be bad result in loss optimization
|
||||||
TICKERDATA_PICKLE = os.path.join('user_data', 'hyperopt_tickerdata.pkl')
|
|
||||||
TRIALSDATA_PICKLE = os.path.join('user_data', 'hyperopt_results.pickle')
|
|
||||||
HYPEROPT_LOCKFILE = os.path.join('user_data', 'hyperopt.lock')
|
|
||||||
|
|
||||||
|
|
||||||
class Hyperopt(Backtesting):
|
class Hyperopt:
|
||||||
"""
|
"""
|
||||||
Hyperopt class, this class contains all the logic to run a hyperopt simulation
|
Hyperopt class, this class contains all the logic to run a hyperopt simulation
|
||||||
|
|
||||||
@ -45,13 +46,20 @@ class Hyperopt(Backtesting):
|
|||||||
hyperopt.start()
|
hyperopt.start()
|
||||||
"""
|
"""
|
||||||
def __init__(self, config: Dict[str, Any]) -> None:
|
def __init__(self, config: Dict[str, Any]) -> None:
|
||||||
super().__init__(config)
|
self.config = config
|
||||||
|
self.backtesting = Backtesting(self.config)
|
||||||
|
|
||||||
self.custom_hyperopt = HyperOptResolver(self.config).hyperopt
|
self.custom_hyperopt = HyperOptResolver(self.config).hyperopt
|
||||||
|
|
||||||
self.custom_hyperoptloss = HyperOptLossResolver(self.config).hyperoptloss
|
self.custom_hyperoptloss = HyperOptLossResolver(self.config).hyperoptloss
|
||||||
self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function
|
self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function
|
||||||
|
|
||||||
self.total_tries = config.get('epochs', 0)
|
self.trials_file = (self.config['user_data_dir'] /
|
||||||
|
'hyperopt_results' / 'hyperopt_results.pickle')
|
||||||
|
self.tickerdata_pickle = (self.config['user_data_dir'] /
|
||||||
|
'hyperopt_results' / 'hyperopt_tickerdata.pkl')
|
||||||
|
self.total_epochs = config.get('epochs', 0)
|
||||||
|
|
||||||
self.current_best_loss = 100
|
self.current_best_loss = 100
|
||||||
|
|
||||||
if not self.config.get('hyperopt_continue'):
|
if not self.config.get('hyperopt_continue'):
|
||||||
@ -60,15 +68,14 @@ class Hyperopt(Backtesting):
|
|||||||
logger.info("Continuing on previous hyperopt results.")
|
logger.info("Continuing on previous hyperopt results.")
|
||||||
|
|
||||||
# Previous evaluations
|
# Previous evaluations
|
||||||
self.trials_file = TRIALSDATA_PICKLE
|
|
||||||
self.trials: List = []
|
self.trials: List = []
|
||||||
|
|
||||||
# Populate functions here (hasattr is slow so should not be run during "regular" operations)
|
# Populate functions here (hasattr is slow so should not be run during "regular" operations)
|
||||||
if hasattr(self.custom_hyperopt, 'populate_buy_trend'):
|
if hasattr(self.custom_hyperopt, 'populate_buy_trend'):
|
||||||
self.advise_buy = self.custom_hyperopt.populate_buy_trend # type: ignore
|
self.backtesting.advise_buy = self.custom_hyperopt.populate_buy_trend # type: ignore
|
||||||
|
|
||||||
if hasattr(self.custom_hyperopt, 'populate_sell_trend'):
|
if hasattr(self.custom_hyperopt, 'populate_sell_trend'):
|
||||||
self.advise_sell = self.custom_hyperopt.populate_sell_trend # type: ignore
|
self.backtesting.advise_sell = self.custom_hyperopt.populate_sell_trend # type: ignore
|
||||||
|
|
||||||
# Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set
|
# Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set
|
||||||
if self.config.get('use_max_market_positions', True):
|
if self.config.get('use_max_market_positions', True):
|
||||||
@ -78,11 +85,22 @@ class Hyperopt(Backtesting):
|
|||||||
self.max_open_trades = 0
|
self.max_open_trades = 0
|
||||||
self.position_stacking = self.config.get('position_stacking', False),
|
self.position_stacking = self.config.get('position_stacking', False),
|
||||||
|
|
||||||
|
if self.has_space('sell'):
|
||||||
|
# Make sure experimental is enabled
|
||||||
|
if 'experimental' not in self.config:
|
||||||
|
self.config['experimental'] = {}
|
||||||
|
self.config['experimental']['use_sell_signal'] = True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_lock_filename(config) -> str:
|
||||||
|
|
||||||
|
return str(config['user_data_dir'] / 'hyperopt.lock')
|
||||||
|
|
||||||
def clean_hyperopt(self):
|
def clean_hyperopt(self):
|
||||||
"""
|
"""
|
||||||
Remove hyperopt pickle files to restart hyperopt.
|
Remove hyperopt pickle files to restart hyperopt.
|
||||||
"""
|
"""
|
||||||
for f in [TICKERDATA_PICKLE, TRIALSDATA_PICKLE]:
|
for f in [self.tickerdata_pickle, self.trials_file]:
|
||||||
p = Path(f)
|
p = Path(f)
|
||||||
if p.is_file():
|
if p.is_file():
|
||||||
logger.info(f"Removing `{p}`.")
|
logger.info(f"Removing `{p}`.")
|
||||||
@ -115,7 +133,7 @@ class Hyperopt(Backtesting):
|
|||||||
"""
|
"""
|
||||||
logger.info('Reading Trials from \'%s\'', self.trials_file)
|
logger.info('Reading Trials from \'%s\'', self.trials_file)
|
||||||
trials = load(self.trials_file)
|
trials = load(self.trials_file)
|
||||||
os.remove(self.trials_file)
|
self.trials_file.unlink()
|
||||||
return trials
|
return trials
|
||||||
|
|
||||||
def log_trials_result(self) -> None:
|
def log_trials_result(self) -> None:
|
||||||
@ -124,61 +142,105 @@ class Hyperopt(Backtesting):
|
|||||||
"""
|
"""
|
||||||
results = sorted(self.trials, key=itemgetter('loss'))
|
results = sorted(self.trials, key=itemgetter('loss'))
|
||||||
best_result = results[0]
|
best_result = results[0]
|
||||||
logger.info(
|
params = best_result['params']
|
||||||
'Best result:\n%s\nwith values:\n',
|
log_str = self.format_results_logstring(best_result)
|
||||||
best_result['result']
|
print(f"\nBest result:\n\n{log_str}\n")
|
||||||
|
|
||||||
|
if self.config.get('print_json'):
|
||||||
|
result_dict: Dict = {}
|
||||||
|
if self.has_space('buy') or self.has_space('sell'):
|
||||||
|
result_dict['params'] = {}
|
||||||
|
if self.has_space('buy'):
|
||||||
|
result_dict['params'].update({p.name: params.get(p.name)
|
||||||
|
for p in self.hyperopt_space('buy')})
|
||||||
|
if self.has_space('sell'):
|
||||||
|
result_dict['params'].update({p.name: params.get(p.name)
|
||||||
|
for p in self.hyperopt_space('sell')})
|
||||||
|
if self.has_space('roi'):
|
||||||
|
# Convert keys in min_roi dict to strings because
|
||||||
|
# rapidjson cannot dump dicts with integer keys...
|
||||||
|
# OrderedDict is used to keep the numeric order of the items
|
||||||
|
# in the dict.
|
||||||
|
result_dict['minimal_roi'] = OrderedDict(
|
||||||
|
(str(k), v) for k, v in self.custom_hyperopt.generate_roi_table(params).items()
|
||||||
)
|
)
|
||||||
pprint(best_result['params'], indent=4)
|
if self.has_space('stoploss'):
|
||||||
if 'roi_t1' in best_result['params']:
|
result_dict['stoploss'] = params.get('stoploss')
|
||||||
logger.info('ROI table:')
|
print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE))
|
||||||
pprint(self.custom_hyperopt.generate_roi_table(best_result['params']), indent=4)
|
else:
|
||||||
|
if self.has_space('buy'):
|
||||||
|
print('Buy hyperspace params:')
|
||||||
|
pprint({p.name: params.get(p.name) for p in self.hyperopt_space('buy')},
|
||||||
|
indent=4)
|
||||||
|
if self.has_space('sell'):
|
||||||
|
print('Sell hyperspace params:')
|
||||||
|
pprint({p.name: params.get(p.name) for p in self.hyperopt_space('sell')},
|
||||||
|
indent=4)
|
||||||
|
if self.has_space('roi'):
|
||||||
|
print("ROI table:")
|
||||||
|
pprint(self.custom_hyperopt.generate_roi_table(params), indent=4)
|
||||||
|
if self.has_space('stoploss'):
|
||||||
|
print(f"Stoploss: {params.get('stoploss')}")
|
||||||
|
|
||||||
def log_results(self, results) -> None:
|
def log_results(self, results) -> None:
|
||||||
"""
|
"""
|
||||||
Log results if it is better than any previous evaluation
|
Log results if it is better than any previous evaluation
|
||||||
"""
|
"""
|
||||||
print_all = self.config.get('print_all', False)
|
print_all = self.config.get('print_all', False)
|
||||||
if print_all or results['loss'] < self.current_best_loss:
|
is_best_loss = results['loss'] < self.current_best_loss
|
||||||
# Output human-friendly index here (starting from 1)
|
if print_all or is_best_loss:
|
||||||
current = results['current_tries'] + 1
|
if is_best_loss:
|
||||||
total = results['total_tries']
|
|
||||||
res = results['result']
|
|
||||||
loss = results['loss']
|
|
||||||
self.current_best_loss = results['loss']
|
self.current_best_loss = results['loss']
|
||||||
log_msg = f'{current:5d}/{total}: {res} Objective: {loss:.5f}'
|
log_str = self.format_results_logstring(results)
|
||||||
log_msg = f'*{log_msg}' if results['initial_point'] else f' {log_msg}'
|
# Colorize output
|
||||||
|
if self.config.get('print_colorized', False):
|
||||||
|
if results['total_profit'] > 0:
|
||||||
|
log_str = Fore.GREEN + log_str
|
||||||
|
if print_all and is_best_loss:
|
||||||
|
log_str = Style.BRIGHT + log_str
|
||||||
if print_all:
|
if print_all:
|
||||||
print(log_msg)
|
print(log_str)
|
||||||
else:
|
else:
|
||||||
print('\n' + log_msg)
|
print('\n' + log_str)
|
||||||
else:
|
else:
|
||||||
print('.', end='')
|
print('.', end='')
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
def format_results_logstring(self, results) -> str:
|
||||||
|
# Output human-friendly index here (starting from 1)
|
||||||
|
current = results['current_epoch'] + 1
|
||||||
|
total = self.total_epochs
|
||||||
|
res = results['results_explanation']
|
||||||
|
loss = results['loss']
|
||||||
|
log_str = f'{current:5d}/{total}: {res} Objective: {loss:.5f}'
|
||||||
|
log_str = f'*{log_str}' if results['is_initial_point'] else f' {log_str}'
|
||||||
|
return log_str
|
||||||
|
|
||||||
def has_space(self, space: str) -> bool:
|
def has_space(self, space: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Tell if a space value is contained in the configuration
|
Tell if a space value is contained in the configuration
|
||||||
"""
|
"""
|
||||||
if space in self.config['spaces'] or 'all' in self.config['spaces']:
|
return any(s in self.config['spaces'] for s in [space, 'all'])
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def hyperopt_space(self) -> List[Dimension]:
|
def hyperopt_space(self, space: Optional[str] = None) -> List[Dimension]:
|
||||||
"""
|
"""
|
||||||
Return the space to use during Hyperopt
|
Return the dimensions in the hyperoptimization space.
|
||||||
|
:param space: Defines hyperspace to return dimensions for.
|
||||||
|
If None, then the self.has_space() will be used to return dimensions
|
||||||
|
for all hyperspaces used.
|
||||||
"""
|
"""
|
||||||
spaces: List[Dimension] = []
|
spaces: List[Dimension] = []
|
||||||
if self.has_space('buy'):
|
if space == 'buy' or (space is None and self.has_space('buy')):
|
||||||
|
logger.debug("Hyperopt has 'buy' space")
|
||||||
spaces += self.custom_hyperopt.indicator_space()
|
spaces += self.custom_hyperopt.indicator_space()
|
||||||
if self.has_space('sell'):
|
if space == 'sell' or (space is None and self.has_space('sell')):
|
||||||
|
logger.debug("Hyperopt has 'sell' space")
|
||||||
spaces += self.custom_hyperopt.sell_indicator_space()
|
spaces += self.custom_hyperopt.sell_indicator_space()
|
||||||
# Make sure experimental is enabled
|
if space == 'roi' or (space is None and self.has_space('roi')):
|
||||||
if 'experimental' not in self.config:
|
logger.debug("Hyperopt has 'roi' space")
|
||||||
self.config['experimental'] = {}
|
|
||||||
self.config['experimental']['use_sell_signal'] = True
|
|
||||||
if self.has_space('roi'):
|
|
||||||
spaces += self.custom_hyperopt.roi_space()
|
spaces += self.custom_hyperopt.roi_space()
|
||||||
if self.has_space('stoploss'):
|
if space == 'stoploss' or (space is None and self.has_space('stoploss')):
|
||||||
|
logger.debug("Hyperopt has 'stoploss' space")
|
||||||
spaces += self.custom_hyperopt.stoploss_space()
|
spaces += self.custom_hyperopt.stoploss_space()
|
||||||
return spaces
|
return spaces
|
||||||
|
|
||||||
@ -189,22 +251,22 @@ class Hyperopt(Backtesting):
|
|||||||
"""
|
"""
|
||||||
params = self.get_args(_params)
|
params = self.get_args(_params)
|
||||||
if self.has_space('roi'):
|
if self.has_space('roi'):
|
||||||
self.strategy.minimal_roi = self.custom_hyperopt.generate_roi_table(params)
|
self.backtesting.strategy.minimal_roi = self.custom_hyperopt.generate_roi_table(params)
|
||||||
|
|
||||||
if self.has_space('buy'):
|
if self.has_space('buy'):
|
||||||
self.advise_buy = self.custom_hyperopt.buy_strategy_generator(params)
|
self.backtesting.advise_buy = self.custom_hyperopt.buy_strategy_generator(params)
|
||||||
|
|
||||||
if self.has_space('sell'):
|
if self.has_space('sell'):
|
||||||
self.advise_sell = self.custom_hyperopt.sell_strategy_generator(params)
|
self.backtesting.advise_sell = self.custom_hyperopt.sell_strategy_generator(params)
|
||||||
|
|
||||||
if self.has_space('stoploss'):
|
if self.has_space('stoploss'):
|
||||||
self.strategy.stoploss = params['stoploss']
|
self.backtesting.strategy.stoploss = params['stoploss']
|
||||||
|
|
||||||
processed = load(TICKERDATA_PICKLE)
|
processed = load(self.tickerdata_pickle)
|
||||||
|
|
||||||
min_date, max_date = get_timeframe(processed)
|
min_date, max_date = get_timeframe(processed)
|
||||||
|
|
||||||
results = self.backtest(
|
results = self.backtesting.backtest(
|
||||||
{
|
{
|
||||||
'stake_amount': self.config['stake_amount'],
|
'stake_amount': self.config['stake_amount'],
|
||||||
'processed': processed,
|
'processed': processed,
|
||||||
@ -214,9 +276,10 @@ class Hyperopt(Backtesting):
|
|||||||
'end_date': max_date,
|
'end_date': max_date,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
result_explanation = self.format_results(results)
|
results_explanation = self.format_results(results)
|
||||||
|
|
||||||
trade_count = len(results.index)
|
trade_count = len(results.index)
|
||||||
|
total_profit = results.profit_abs.sum()
|
||||||
|
|
||||||
# If this evaluation contains too short amount of trades to be
|
# If this evaluation contains too short amount of trades to be
|
||||||
# interesting -- consider it as 'bad' (assigned max. loss value)
|
# interesting -- consider it as 'bad' (assigned max. loss value)
|
||||||
@ -226,7 +289,8 @@ class Hyperopt(Backtesting):
|
|||||||
return {
|
return {
|
||||||
'loss': MAX_LOSS,
|
'loss': MAX_LOSS,
|
||||||
'params': params,
|
'params': params,
|
||||||
'result': result_explanation,
|
'results_explanation': results_explanation,
|
||||||
|
'total_profit': total_profit,
|
||||||
}
|
}
|
||||||
|
|
||||||
loss = self.calculate_loss(results=results, trade_count=trade_count,
|
loss = self.calculate_loss(results=results, trade_count=trade_count,
|
||||||
@ -235,12 +299,13 @@ class Hyperopt(Backtesting):
|
|||||||
return {
|
return {
|
||||||
'loss': loss,
|
'loss': loss,
|
||||||
'params': params,
|
'params': params,
|
||||||
'result': result_explanation,
|
'results_explanation': results_explanation,
|
||||||
|
'total_profit': total_profit,
|
||||||
}
|
}
|
||||||
|
|
||||||
def format_results(self, results: DataFrame) -> str:
|
def format_results(self, results: DataFrame) -> str:
|
||||||
"""
|
"""
|
||||||
Return the format result in a string
|
Return the formatted results explanation in a string
|
||||||
"""
|
"""
|
||||||
trades = len(results.index)
|
trades = len(results.index)
|
||||||
avg_profit = results.profit_percent.mean() * 100.0
|
avg_profit = results.profit_percent.mean() * 100.0
|
||||||
@ -269,7 +334,7 @@ class Hyperopt(Backtesting):
|
|||||||
|
|
||||||
def load_previous_results(self):
|
def load_previous_results(self):
|
||||||
""" read trials file if we have one """
|
""" read trials file if we have one """
|
||||||
if os.path.exists(self.trials_file) and os.path.getsize(self.trials_file) > 0:
|
if self.trials_file.is_file() and self.trials_file.stat().st_size > 0:
|
||||||
self.trials = self.read_trials()
|
self.trials = self.read_trials()
|
||||||
logger.info(
|
logger.info(
|
||||||
'Loaded %d previous evaluations from disk.',
|
'Loaded %d previous evaluations from disk.',
|
||||||
@ -277,14 +342,14 @@ class Hyperopt(Backtesting):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
timerange = Arguments.parse_timerange(None if self.config.get(
|
timerange = TimeRange.parse_timerange(None if self.config.get(
|
||||||
'timerange') is None else str(self.config.get('timerange')))
|
'timerange') is None else str(self.config.get('timerange')))
|
||||||
data = load_data(
|
data = load_data(
|
||||||
datadir=Path(self.config['datadir']) if self.config.get('datadir') else None,
|
datadir=Path(self.config['datadir']) if self.config.get('datadir') else None,
|
||||||
pairs=self.config['exchange']['pair_whitelist'],
|
pairs=self.config['exchange']['pair_whitelist'],
|
||||||
ticker_interval=self.ticker_interval,
|
ticker_interval=self.backtesting.ticker_interval,
|
||||||
refresh_pairs=self.config.get('refresh_pairs', False),
|
refresh_pairs=self.config.get('refresh_pairs', False),
|
||||||
exchange=self.exchange,
|
exchange=self.backtesting.exchange,
|
||||||
timerange=timerange
|
timerange=timerange
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -301,15 +366,15 @@ class Hyperopt(Backtesting):
|
|||||||
(max_date - min_date).days
|
(max_date - min_date).days
|
||||||
)
|
)
|
||||||
|
|
||||||
self.strategy.advise_indicators = \
|
self.backtesting.strategy.advise_indicators = \
|
||||||
self.custom_hyperopt.populate_indicators # type: ignore
|
self.custom_hyperopt.populate_indicators # type: ignore
|
||||||
|
|
||||||
preprocessed = self.strategy.tickerdata_to_dataframe(data)
|
preprocessed = self.backtesting.strategy.tickerdata_to_dataframe(data)
|
||||||
|
|
||||||
dump(preprocessed, TICKERDATA_PICKLE)
|
dump(preprocessed, self.tickerdata_pickle)
|
||||||
|
|
||||||
# We don't need exchange instance anymore while running hyperopt
|
# We don't need exchange instance anymore while running hyperopt
|
||||||
self.exchange = None # type: ignore
|
self.backtesting.exchange = None # type: ignore
|
||||||
|
|
||||||
self.load_previous_results()
|
self.load_previous_results()
|
||||||
|
|
||||||
@ -319,29 +384,27 @@ class Hyperopt(Backtesting):
|
|||||||
logger.info(f'Number of parallel jobs set as: {config_jobs}')
|
logger.info(f'Number of parallel jobs set as: {config_jobs}')
|
||||||
|
|
||||||
opt = self.get_optimizer(config_jobs)
|
opt = self.get_optimizer(config_jobs)
|
||||||
|
|
||||||
|
if self.config.get('print_colorized', False):
|
||||||
|
colorama_init(autoreset=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with Parallel(n_jobs=config_jobs) as parallel:
|
with Parallel(n_jobs=config_jobs) as parallel:
|
||||||
jobs = parallel._effective_n_jobs()
|
jobs = parallel._effective_n_jobs()
|
||||||
logger.info(f'Effective number of parallel workers used: {jobs}')
|
logger.info(f'Effective number of parallel workers used: {jobs}')
|
||||||
EVALS = max(self.total_tries // jobs, 1)
|
EVALS = max(self.total_epochs // jobs, 1)
|
||||||
for i in range(EVALS):
|
for i in range(EVALS):
|
||||||
asked = opt.ask(n_points=jobs)
|
asked = opt.ask(n_points=jobs)
|
||||||
f_val = self.run_optimizer_parallel(parallel, asked)
|
f_val = self.run_optimizer_parallel(parallel, asked)
|
||||||
opt.tell(asked, [i['loss'] for i in f_val])
|
opt.tell(asked, [v['loss'] for v in f_val])
|
||||||
|
|
||||||
self.trials += f_val
|
|
||||||
for j in range(jobs):
|
for j in range(jobs):
|
||||||
current = i * jobs + j
|
current = i * jobs + j
|
||||||
self.log_results({
|
val = f_val[j]
|
||||||
'loss': f_val[j]['loss'],
|
val['current_epoch'] = current
|
||||||
'current_tries': current,
|
val['is_initial_point'] = current < INITIAL_POINTS
|
||||||
'initial_point': current < INITIAL_POINTS,
|
self.log_results(val)
|
||||||
'total_tries': self.total_tries,
|
self.trials.append(val)
|
||||||
'result': f_val[j]['result'],
|
logger.debug(f"Optimizer epoch evaluated: {val}")
|
||||||
})
|
|
||||||
logger.debug(f"Optimizer params: {f_val[j]['params']}")
|
|
||||||
for j in range(jobs):
|
|
||||||
logger.debug(f"Optimizer state: Xi: {opt.Xi[-j-1]}, yi: {opt.yi[-j-1]}")
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print('User interrupted..')
|
print('User interrupted..')
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ from abc import ABC, abstractmethod
|
|||||||
from typing import Dict, Any, Callable, List
|
from typing import Dict, Any, Callable, List
|
||||||
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
from skopt.space import Dimension
|
from skopt.space import Dimension, Integer, Real
|
||||||
|
|
||||||
|
|
||||||
class IHyperOpt(ABC):
|
class IHyperOpt(ABC):
|
||||||
@ -26,56 +26,80 @@ class IHyperOpt(ABC):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Populate indicators that will be used in the Buy and Sell strategy
|
Populate indicators that will be used in the Buy and Sell strategy.
|
||||||
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
|
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe().
|
||||||
:return: a Dataframe with all mandatory indicators for the strategies
|
:return: A Dataframe with all mandatory indicators for the strategies.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
|
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
|
||||||
"""
|
"""
|
||||||
Create a buy strategy generator
|
Create a buy strategy generator.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def sell_strategy_generator(params: Dict[str, Any]) -> Callable:
|
def sell_strategy_generator(params: Dict[str, Any]) -> Callable:
|
||||||
"""
|
"""
|
||||||
Create a sell strategy generator
|
Create a sell strategy generator.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def indicator_space() -> List[Dimension]:
|
def indicator_space() -> List[Dimension]:
|
||||||
"""
|
"""
|
||||||
Create an indicator space
|
Create an indicator space.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def sell_indicator_space() -> List[Dimension]:
|
def sell_indicator_space() -> List[Dimension]:
|
||||||
"""
|
"""
|
||||||
Create a sell indicator space
|
Create a sell indicator space.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@abstractmethod
|
|
||||||
def generate_roi_table(params: Dict) -> Dict[int, float]:
|
def generate_roi_table(params: Dict) -> Dict[int, float]:
|
||||||
"""
|
"""
|
||||||
Create an roi table
|
Create a ROI table.
|
||||||
|
|
||||||
|
Generates the ROI table that will be used by Hyperopt.
|
||||||
|
You may override it in your custom Hyperopt class.
|
||||||
"""
|
"""
|
||||||
|
roi_table = {}
|
||||||
|
roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3']
|
||||||
|
roi_table[params['roi_t3']] = params['roi_p1'] + params['roi_p2']
|
||||||
|
roi_table[params['roi_t3'] + params['roi_t2']] = params['roi_p1']
|
||||||
|
roi_table[params['roi_t3'] + params['roi_t2'] + params['roi_t1']] = 0
|
||||||
|
|
||||||
|
return roi_table
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@abstractmethod
|
|
||||||
def stoploss_space() -> List[Dimension]:
|
def stoploss_space() -> List[Dimension]:
|
||||||
"""
|
"""
|
||||||
Create a stoploss space
|
Create a stoploss space.
|
||||||
|
|
||||||
|
Defines range of stoploss values to search.
|
||||||
|
You may override it in your custom Hyperopt class.
|
||||||
"""
|
"""
|
||||||
|
return [
|
||||||
|
Real(-0.5, -0.02, name='stoploss'),
|
||||||
|
]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@abstractmethod
|
|
||||||
def roi_space() -> List[Dimension]:
|
def roi_space() -> List[Dimension]:
|
||||||
"""
|
"""
|
||||||
Create a roi space
|
Create a ROI space.
|
||||||
|
|
||||||
|
Defines values to search for each ROI steps.
|
||||||
|
You may override it in your custom Hyperopt class.
|
||||||
"""
|
"""
|
||||||
|
return [
|
||||||
|
Integer(10, 120, name='roi_t1'),
|
||||||
|
Integer(10, 60, name='roi_t2'),
|
||||||
|
Integer(10, 40, name='roi_t3'),
|
||||||
|
Real(0.01, 0.04, name='roi_p1'),
|
||||||
|
Real(0.01, 0.07, name='roi_p2'),
|
||||||
|
Real(0.01, 0.20, name='roi_p3'),
|
||||||
|
]
|
||||||
|
@ -39,7 +39,7 @@ class SharpeHyperOptLoss(IHyperOptLoss):
|
|||||||
sharp_ratio = expected_yearly_return / np.std(total_profit) * np.sqrt(365)
|
sharp_ratio = expected_yearly_return / np.std(total_profit) * np.sqrt(365)
|
||||||
else:
|
else:
|
||||||
# Define high (negative) sharpe ratio to be clear that this is NOT optimal.
|
# Define high (negative) sharpe ratio to be clear that this is NOT optimal.
|
||||||
sharp_ratio = 20.
|
sharp_ratio = -20.
|
||||||
|
|
||||||
# print(expected_yearly_return, np.std(total_profit), sharp_ratio)
|
# print(expected_yearly_return, np.std(total_profit), sharp_ratio)
|
||||||
return -sharp_ratio
|
return -sharp_ratio
|
||||||
|
@ -55,7 +55,6 @@ class VolumePairList(IPairList):
|
|||||||
# Generate dynamic whitelist
|
# Generate dynamic whitelist
|
||||||
self._whitelist = self._gen_pair_whitelist(
|
self._whitelist = self._gen_pair_whitelist(
|
||||||
self._config['stake_currency'], self._sort_key)[:self._number_pairs]
|
self._config['stake_currency'], self._sort_key)[:self._number_pairs]
|
||||||
logger.info(f"Searching pairs: {self._whitelist}")
|
|
||||||
|
|
||||||
@cached(TTLCache(maxsize=1, ttl=1800))
|
@cached(TTLCache(maxsize=1, ttl=1800))
|
||||||
def _gen_pair_whitelist(self, base_currency: str, key: str) -> List[str]:
|
def _gen_pair_whitelist(self, base_currency: str, key: str) -> List[str]:
|
||||||
@ -92,4 +91,6 @@ class VolumePairList(IPairList):
|
|||||||
valid_tickers.remove(t)
|
valid_tickers.remove(t)
|
||||||
|
|
||||||
pairs = [s['symbol'] for s in valid_tickers]
|
pairs = [s['symbol'] for s in valid_tickers]
|
||||||
|
logger.info(f"Searching pairs: {self._whitelist}")
|
||||||
|
|
||||||
return pairs
|
return pairs
|
||||||
|
@ -4,7 +4,7 @@ from typing import Dict, List, Optional
|
|||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from freqtrade.configuration import Arguments
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.data import history
|
from freqtrade.data import history
|
||||||
from freqtrade.data.btanalysis import (combine_tickers_with_mean,
|
from freqtrade.data.btanalysis import (combine_tickers_with_mean,
|
||||||
create_cum_profit, load_trades)
|
create_cum_profit, load_trades)
|
||||||
@ -31,18 +31,18 @@ def init_plotscript(config):
|
|||||||
exchange: Optional[Exchange] = None
|
exchange: Optional[Exchange] = None
|
||||||
|
|
||||||
# Exchange is only needed when downloading data!
|
# Exchange is only needed when downloading data!
|
||||||
if config.get("live", False) or config.get("refresh_pairs", False):
|
if config.get("refresh_pairs", False):
|
||||||
exchange = ExchangeResolver(config.get('exchange', {}).get('name'),
|
exchange = ExchangeResolver(config.get('exchange', {}).get('name'),
|
||||||
config).exchange
|
config).exchange
|
||||||
|
|
||||||
strategy = StrategyResolver(config).strategy
|
strategy = StrategyResolver(config).strategy
|
||||||
if "pairs" in config:
|
if "pairs" in config:
|
||||||
pairs = config["pairs"].split(',')
|
pairs = config["pairs"]
|
||||||
else:
|
else:
|
||||||
pairs = config["exchange"]["pair_whitelist"]
|
pairs = config["exchange"]["pair_whitelist"]
|
||||||
|
|
||||||
# Set timerange to use
|
# Set timerange to use
|
||||||
timerange = Arguments.parse_timerange(config.get("timerange"))
|
timerange = TimeRange.parse_timerange(config.get("timerange"))
|
||||||
|
|
||||||
tickers = history.load_data(
|
tickers = history.load_data(
|
||||||
datadir=Path(str(config.get("datadir"))),
|
datadir=Path(str(config.get("datadir"))),
|
||||||
@ -51,7 +51,6 @@ def init_plotscript(config):
|
|||||||
refresh_pairs=config.get('refresh_pairs', False),
|
refresh_pairs=config.get('refresh_pairs', False),
|
||||||
timerange=timerange,
|
timerange=timerange,
|
||||||
exchange=exchange,
|
exchange=exchange,
|
||||||
live=config.get("live", False),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
trades = load_trades(config)
|
trades = load_trades(config)
|
||||||
@ -308,7 +307,7 @@ def generate_plot_filename(pair, ticker_interval) -> str:
|
|||||||
return file_name
|
return file_name
|
||||||
|
|
||||||
|
|
||||||
def store_plot_file(fig, filename: str, auto_open: bool = False) -> None:
|
def store_plot_file(fig, filename: str, directory: Path, auto_open: bool = False) -> None:
|
||||||
"""
|
"""
|
||||||
Generate a plot html file from pre populated fig plotly object
|
Generate a plot html file from pre populated fig plotly object
|
||||||
:param fig: Plotly Figure to plot
|
:param fig: Plotly Figure to plot
|
||||||
@ -316,8 +315,9 @@ def store_plot_file(fig, filename: str, auto_open: bool = False) -> None:
|
|||||||
:param ticker_interval: Used as part of the filename
|
:param ticker_interval: Used as part of the filename
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
|
directory.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
Path("user_data/plots").mkdir(parents=True, exist_ok=True)
|
_filename = directory.joinpath(filename)
|
||||||
|
plot(fig, filename=str(_filename),
|
||||||
plot(fig, filename=str(Path('user_data/plots').joinpath(filename)),
|
|
||||||
auto_open=auto_open)
|
auto_open=auto_open)
|
||||||
|
logger.info(f"Stored plot as {_filename}")
|
||||||
|
@ -31,7 +31,8 @@ class HyperOptResolver(IResolver):
|
|||||||
|
|
||||||
# Verify the hyperopt is in the configuration, otherwise fallback to the default hyperopt
|
# Verify the hyperopt is in the configuration, otherwise fallback to the default hyperopt
|
||||||
hyperopt_name = config.get('hyperopt') or DEFAULT_HYPEROPT
|
hyperopt_name = config.get('hyperopt') or DEFAULT_HYPEROPT
|
||||||
self.hyperopt = self._load_hyperopt(hyperopt_name, extra_dir=config.get('hyperopt_path'))
|
self.hyperopt = self._load_hyperopt(hyperopt_name, config,
|
||||||
|
extra_dir=config.get('hyperopt_path'))
|
||||||
|
|
||||||
# Assign ticker_interval to be used in hyperopt
|
# Assign ticker_interval to be used in hyperopt
|
||||||
self.hyperopt.__class__.ticker_interval = str(config['ticker_interval'])
|
self.hyperopt.__class__.ticker_interval = str(config['ticker_interval'])
|
||||||
@ -44,17 +45,18 @@ class HyperOptResolver(IResolver):
|
|||||||
"Using populate_sell_trend from DefaultStrategy.")
|
"Using populate_sell_trend from DefaultStrategy.")
|
||||||
|
|
||||||
def _load_hyperopt(
|
def _load_hyperopt(
|
||||||
self, hyperopt_name: str, extra_dir: Optional[str] = None) -> IHyperOpt:
|
self, hyperopt_name: str, config: Dict, extra_dir: Optional[str] = None) -> IHyperOpt:
|
||||||
"""
|
"""
|
||||||
Search and loads the specified hyperopt.
|
Search and loads the specified hyperopt.
|
||||||
:param hyperopt_name: name of the module to import
|
:param hyperopt_name: name of the module to import
|
||||||
|
:param config: configuration dictionary
|
||||||
:param extra_dir: additional directory to search for the given hyperopt
|
:param extra_dir: additional directory to search for the given hyperopt
|
||||||
:return: HyperOpt instance or None
|
:return: HyperOpt instance or None
|
||||||
"""
|
"""
|
||||||
current_path = Path(__file__).parent.parent.joinpath('optimize').resolve()
|
current_path = Path(__file__).parent.parent.joinpath('optimize').resolve()
|
||||||
|
|
||||||
abs_paths = [
|
abs_paths = [
|
||||||
Path.cwd().joinpath('user_data/hyperopts'),
|
config['user_data_dir'].joinpath('hyperopts'),
|
||||||
current_path,
|
current_path,
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -79,7 +81,7 @@ class HyperOptLossResolver(IResolver):
|
|||||||
|
|
||||||
__slots__ = ['hyperoptloss']
|
__slots__ = ['hyperoptloss']
|
||||||
|
|
||||||
def __init__(self, config: Optional[Dict] = None) -> None:
|
def __init__(self, config: Dict = None) -> None:
|
||||||
"""
|
"""
|
||||||
Load the custom class from config parameter
|
Load the custom class from config parameter
|
||||||
:param config: configuration dictionary or None
|
:param config: configuration dictionary or None
|
||||||
@ -89,7 +91,7 @@ class HyperOptLossResolver(IResolver):
|
|||||||
# Verify the hyperopt is in the configuration, otherwise fallback to the default hyperopt
|
# Verify the hyperopt is in the configuration, otherwise fallback to the default hyperopt
|
||||||
hyperopt_name = config.get('hyperopt_loss') or DEFAULT_HYPEROPT_LOSS
|
hyperopt_name = config.get('hyperopt_loss') or DEFAULT_HYPEROPT_LOSS
|
||||||
self.hyperoptloss = self._load_hyperoptloss(
|
self.hyperoptloss = self._load_hyperoptloss(
|
||||||
hyperopt_name, extra_dir=config.get('hyperopt_path'))
|
hyperopt_name, config, extra_dir=config.get('hyperopt_path'))
|
||||||
|
|
||||||
# Assign ticker_interval to be used in hyperopt
|
# Assign ticker_interval to be used in hyperopt
|
||||||
self.hyperoptloss.__class__.ticker_interval = str(config['ticker_interval'])
|
self.hyperoptloss.__class__.ticker_interval = str(config['ticker_interval'])
|
||||||
@ -99,17 +101,19 @@ class HyperOptLossResolver(IResolver):
|
|||||||
f"Found hyperopt {hyperopt_name} does not implement `hyperopt_loss_function`.")
|
f"Found hyperopt {hyperopt_name} does not implement `hyperopt_loss_function`.")
|
||||||
|
|
||||||
def _load_hyperoptloss(
|
def _load_hyperoptloss(
|
||||||
self, hyper_loss_name: str, extra_dir: Optional[str] = None) -> IHyperOptLoss:
|
self, hyper_loss_name: str, config: Dict,
|
||||||
|
extra_dir: Optional[str] = None) -> IHyperOptLoss:
|
||||||
"""
|
"""
|
||||||
Search and loads the specified hyperopt loss class.
|
Search and loads the specified hyperopt loss class.
|
||||||
:param hyper_loss_name: name of the module to import
|
:param hyper_loss_name: name of the module to import
|
||||||
|
:param config: configuration dictionary
|
||||||
:param extra_dir: additional directory to search for the given hyperopt
|
:param extra_dir: additional directory to search for the given hyperopt
|
||||||
:return: HyperOptLoss instance or None
|
:return: HyperOptLoss instance or None
|
||||||
"""
|
"""
|
||||||
current_path = Path(__file__).parent.parent.joinpath('optimize').resolve()
|
current_path = Path(__file__).parent.parent.joinpath('optimize').resolve()
|
||||||
|
|
||||||
abs_paths = [
|
abs_paths = [
|
||||||
Path.cwd().joinpath('user_data/hyperopts'),
|
config['user_data_dir'].joinpath('hyperopts'),
|
||||||
current_path,
|
current_path,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -29,7 +29,8 @@ class IResolver(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Generate spec based on absolute path
|
# Generate spec based on absolute path
|
||||||
spec = importlib.util.spec_from_file_location('unknown', str(module_path))
|
# Pass object_name as first argument to have logging print a reasonable name.
|
||||||
|
spec = importlib.util.spec_from_file_location(object_name, str(module_path))
|
||||||
module = importlib.util.module_from_spec(spec)
|
module = importlib.util.module_from_spec(spec)
|
||||||
try:
|
try:
|
||||||
spec.loader.exec_module(module) # type: ignore # importlib does not use typehints
|
spec.loader.exec_module(module) # type: ignore # importlib does not use typehints
|
||||||
@ -57,7 +58,7 @@ class IResolver(object):
|
|||||||
if not str(entry).endswith('.py'):
|
if not str(entry).endswith('.py'):
|
||||||
logger.debug('Ignoring %s', entry)
|
logger.debug('Ignoring %s', entry)
|
||||||
continue
|
continue
|
||||||
module_path = Path.resolve(directory.joinpath(entry))
|
module_path = entry.resolve()
|
||||||
obj = IResolver._get_valid_object(
|
obj = IResolver._get_valid_object(
|
||||||
object_type, module_path, object_name
|
object_type, module_path, object_name
|
||||||
)
|
)
|
||||||
|
@ -25,21 +25,22 @@ class PairListResolver(IResolver):
|
|||||||
Load the custom class from config parameter
|
Load the custom class from config parameter
|
||||||
:param config: configuration dictionary or None
|
:param config: configuration dictionary or None
|
||||||
"""
|
"""
|
||||||
self.pairlist = self._load_pairlist(pairlist_name, kwargs={'freqtrade': freqtrade,
|
self.pairlist = self._load_pairlist(pairlist_name, config, kwargs={'freqtrade': freqtrade,
|
||||||
'config': config})
|
'config': config})
|
||||||
|
|
||||||
def _load_pairlist(
|
def _load_pairlist(
|
||||||
self, pairlist_name: str, kwargs: dict) -> IPairList:
|
self, pairlist_name: str, config: dict, kwargs: dict) -> IPairList:
|
||||||
"""
|
"""
|
||||||
Search and loads the specified pairlist.
|
Search and loads the specified pairlist.
|
||||||
:param pairlist_name: name of the module to import
|
:param pairlist_name: name of the module to import
|
||||||
|
:param config: configuration dictionary
|
||||||
:param extra_dir: additional directory to search for the given pairlist
|
:param extra_dir: additional directory to search for the given pairlist
|
||||||
:return: PairList instance or None
|
:return: PairList instance or None
|
||||||
"""
|
"""
|
||||||
current_path = Path(__file__).parent.parent.joinpath('pairlist').resolve()
|
current_path = Path(__file__).parent.parent.joinpath('pairlist').resolve()
|
||||||
|
|
||||||
abs_paths = [
|
abs_paths = [
|
||||||
Path.cwd().joinpath('user_data/pairlist'),
|
config['user_data_dir'].joinpath('pairlist'),
|
||||||
current_path,
|
current_path,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -123,7 +123,7 @@ class StrategyResolver(IResolver):
|
|||||||
current_path = Path(__file__).parent.parent.joinpath('strategy').resolve()
|
current_path = Path(__file__).parent.parent.joinpath('strategy').resolve()
|
||||||
|
|
||||||
abs_paths = [
|
abs_paths = [
|
||||||
Path.cwd().joinpath('user_data/strategies'),
|
config['user_data_dir'].joinpath('strategies'),
|
||||||
current_path,
|
current_path,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ from typing import Dict, Any, List, Optional
|
|||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import sqlalchemy as sql
|
import sqlalchemy as sql
|
||||||
from numpy import mean, nan_to_num, NAN
|
from numpy import mean, NAN
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade import TemporaryError, DependencyException
|
from freqtrade import TemporaryError, DependencyException
|
||||||
@ -195,9 +195,9 @@ class RPC(object):
|
|||||||
trades = Trade.query.order_by(Trade.id).all()
|
trades = Trade.query.order_by(Trade.id).all()
|
||||||
|
|
||||||
profit_all_coin = []
|
profit_all_coin = []
|
||||||
profit_all_percent = []
|
profit_all_perc = []
|
||||||
profit_closed_coin = []
|
profit_closed_coin = []
|
||||||
profit_closed_percent = []
|
profit_closed_perc = []
|
||||||
durations = []
|
durations = []
|
||||||
|
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
@ -211,7 +211,7 @@ class RPC(object):
|
|||||||
if not trade.is_open:
|
if not trade.is_open:
|
||||||
profit_percent = trade.calc_profit_percent()
|
profit_percent = trade.calc_profit_percent()
|
||||||
profit_closed_coin.append(trade.calc_profit())
|
profit_closed_coin.append(trade.calc_profit())
|
||||||
profit_closed_percent.append(profit_percent)
|
profit_closed_perc.append(profit_percent)
|
||||||
else:
|
else:
|
||||||
# Get current rate
|
# Get current rate
|
||||||
try:
|
try:
|
||||||
@ -223,7 +223,7 @@ class RPC(object):
|
|||||||
profit_all_coin.append(
|
profit_all_coin.append(
|
||||||
trade.calc_profit(rate=Decimal(trade.close_rate or current_rate))
|
trade.calc_profit(rate=Decimal(trade.close_rate or current_rate))
|
||||||
)
|
)
|
||||||
profit_all_percent.append(profit_percent)
|
profit_all_perc.append(profit_percent)
|
||||||
|
|
||||||
best_pair = Trade.session.query(
|
best_pair = Trade.session.query(
|
||||||
Trade.pair, sql.func.sum(Trade.close_profit).label('profit_sum')
|
Trade.pair, sql.func.sum(Trade.close_profit).label('profit_sum')
|
||||||
@ -238,7 +238,8 @@ class RPC(object):
|
|||||||
|
|
||||||
# Prepare data to display
|
# Prepare data to display
|
||||||
profit_closed_coin_sum = round(sum(profit_closed_coin), 8)
|
profit_closed_coin_sum = round(sum(profit_closed_coin), 8)
|
||||||
profit_closed_percent = round(nan_to_num(mean(profit_closed_percent)) * 100, 2)
|
profit_closed_percent = (round(mean(profit_closed_perc) * 100, 2) if profit_closed_perc
|
||||||
|
else 0.0)
|
||||||
profit_closed_fiat = self._fiat_converter.convert_amount(
|
profit_closed_fiat = self._fiat_converter.convert_amount(
|
||||||
profit_closed_coin_sum,
|
profit_closed_coin_sum,
|
||||||
stake_currency,
|
stake_currency,
|
||||||
@ -246,7 +247,7 @@ class RPC(object):
|
|||||||
) if self._fiat_converter else 0
|
) if self._fiat_converter else 0
|
||||||
|
|
||||||
profit_all_coin_sum = round(sum(profit_all_coin), 8)
|
profit_all_coin_sum = round(sum(profit_all_coin), 8)
|
||||||
profit_all_percent = round(nan_to_num(mean(profit_all_percent)) * 100, 2)
|
profit_all_percent = round(mean(profit_all_perc) * 100, 2) if profit_all_perc else 0.0
|
||||||
profit_all_fiat = self._fiat_converter.convert_amount(
|
profit_all_fiat = self._fiat_converter.convert_amount(
|
||||||
profit_all_coin_sum,
|
profit_all_coin_sum,
|
||||||
stake_currency,
|
stake_currency,
|
||||||
|
@ -4,7 +4,7 @@ This module defines the interface to apply for strategies
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Dict, List, NamedTuple, Optional, Tuple
|
from typing import Dict, List, NamedTuple, Optional, Tuple
|
||||||
import warnings
|
import warnings
|
||||||
@ -107,6 +107,7 @@ class IStrategy(ABC):
|
|||||||
self.config = config
|
self.config = config
|
||||||
# Dict to determine if analysis is necessary
|
# Dict to determine if analysis is necessary
|
||||||
self._last_candle_seen_per_pair: Dict[str, datetime] = {}
|
self._last_candle_seen_per_pair: Dict[str, datetime] = {}
|
||||||
|
self._pair_locked_until: Dict[str, datetime] = {}
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
@ -154,10 +155,45 @@ class IStrategy(ABC):
|
|||||||
"""
|
"""
|
||||||
return self.__class__.__name__
|
return self.__class__.__name__
|
||||||
|
|
||||||
|
def lock_pair(self, pair: str, until: datetime) -> None:
|
||||||
|
"""
|
||||||
|
Locks pair until a given timestamp happens.
|
||||||
|
Locked pairs are not analyzed, and are prevented from opening new trades.
|
||||||
|
:param pair: Pair to lock
|
||||||
|
:param until: datetime in UTC until the pair should be blocked from opening new trades.
|
||||||
|
Needs to be timezone aware `datetime.now(timezone.utc)`
|
||||||
|
"""
|
||||||
|
self._pair_locked_until[pair] = until
|
||||||
|
|
||||||
|
def is_pair_locked(self, pair: str) -> bool:
|
||||||
|
"""
|
||||||
|
Checks if a pair is currently locked
|
||||||
|
"""
|
||||||
|
if pair not in self._pair_locked_until:
|
||||||
|
return False
|
||||||
|
return self._pair_locked_until[pair] >= datetime.now(timezone.utc)
|
||||||
|
|
||||||
def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Parses the given ticker history and returns a populated DataFrame
|
Parses the given ticker history and returns a populated DataFrame
|
||||||
add several TA indicators and buy signal to it
|
add several TA indicators and buy signal to it
|
||||||
|
:param dataframe: Dataframe containing ticker data
|
||||||
|
:param metadata: Metadata dictionary with additional data (e.g. 'pair')
|
||||||
|
:return: DataFrame with ticker data and indicator data
|
||||||
|
"""
|
||||||
|
logger.debug("TA Analysis Launched")
|
||||||
|
dataframe = self.advise_indicators(dataframe, metadata)
|
||||||
|
dataframe = self.advise_buy(dataframe, metadata)
|
||||||
|
dataframe = self.advise_sell(dataframe, metadata)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def _analyze_ticker_internal(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Parses the given ticker history and returns a populated DataFrame
|
||||||
|
add several TA indicators and buy signal to it
|
||||||
|
WARNING: Used internally only, may skip analysis if `process_only_new_candles` is set.
|
||||||
|
:param dataframe: Dataframe containing ticker data
|
||||||
|
:param metadata: Metadata dictionary with additional data (e.g. 'pair')
|
||||||
:return: DataFrame with ticker data and indicator data
|
:return: DataFrame with ticker data and indicator data
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -168,10 +204,7 @@ class IStrategy(ABC):
|
|||||||
if (not self.process_only_new_candles or
|
if (not self.process_only_new_candles or
|
||||||
self._last_candle_seen_per_pair.get(pair, None) != dataframe.iloc[-1]['date']):
|
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.
|
||||||
logger.debug("TA Analysis Launched")
|
dataframe = self.analyze_ticker(dataframe, metadata)
|
||||||
dataframe = self.advise_indicators(dataframe, metadata)
|
|
||||||
dataframe = self.advise_buy(dataframe, metadata)
|
|
||||||
dataframe = self.advise_sell(dataframe, metadata)
|
|
||||||
self._last_candle_seen_per_pair[pair] = dataframe.iloc[-1]['date']
|
self._last_candle_seen_per_pair[pair] = dataframe.iloc[-1]['date']
|
||||||
else:
|
else:
|
||||||
logger.debug("Skipping TA Analysis for already analyzed candle")
|
logger.debug("Skipping TA Analysis for already analyzed candle")
|
||||||
@ -198,7 +231,7 @@ class IStrategy(ABC):
|
|||||||
return False, False
|
return False, False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dataframe = self.analyze_ticker(dataframe, {'pair': pair})
|
dataframe = self._analyze_ticker_internal(dataframe, {'pair': pair})
|
||||||
except ValueError as error:
|
except ValueError as error:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
'Unable to analyze ticker for pair %s: %s',
|
'Unable to analyze ticker for pair %s: %s',
|
||||||
@ -246,8 +279,8 @@ class IStrategy(ABC):
|
|||||||
sell: bool, low: float = None, high: float = None,
|
sell: bool, low: float = None, high: float = None,
|
||||||
force_stoploss: float = 0) -> SellCheckTuple:
|
force_stoploss: float = 0) -> SellCheckTuple:
|
||||||
"""
|
"""
|
||||||
This function evaluate if on the condition required to trigger a sell has been reached
|
This function evaluates if one of the conditions required to trigger a sell
|
||||||
if the threshold is reached and updates the trade record.
|
has been reached, which can either be a stop-loss, ROI or sell-signal.
|
||||||
:param low: Only used during backtesting to simulate stoploss
|
:param low: Only used during backtesting to simulate stoploss
|
||||||
:param high: Only used during backtesting, to simulate ROI
|
:param high: Only used during backtesting, to simulate ROI
|
||||||
:param force_stoploss: Externally provided stoploss
|
:param force_stoploss: Externally provided stoploss
|
||||||
|
133
freqtrade/tests/config_test_comments.json
Normal file
133
freqtrade/tests/config_test_comments.json
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
{
|
||||||
|
/* Single-line C-style comment */
|
||||||
|
"max_open_trades": 3,
|
||||||
|
/*
|
||||||
|
* Multi-line C-style comment
|
||||||
|
*/
|
||||||
|
"stake_currency": "BTC",
|
||||||
|
"stake_amount": 0.05,
|
||||||
|
"fiat_display_currency": "USD", // C++-style comment
|
||||||
|
"amount_reserve_percent" : 0.05, // And more, tabs before this comment
|
||||||
|
"dry_run": false,
|
||||||
|
"ticker_interval": "5m",
|
||||||
|
"trailing_stop": false,
|
||||||
|
"trailing_stop_positive": 0.005,
|
||||||
|
"trailing_stop_positive_offset": 0.0051,
|
||||||
|
"trailing_only_offset_is_reached": false,
|
||||||
|
"minimal_roi": {
|
||||||
|
"40": 0.0,
|
||||||
|
"30": 0.01,
|
||||||
|
"20": 0.02,
|
||||||
|
"0": 0.04
|
||||||
|
},
|
||||||
|
"stoploss": -0.10,
|
||||||
|
"unfilledtimeout": {
|
||||||
|
"buy": 10,
|
||||||
|
"sell": 30, // Trailing comma should also be accepted now
|
||||||
|
},
|
||||||
|
"bid_strategy": {
|
||||||
|
"use_order_book": false,
|
||||||
|
"ask_last_balance": 0.0,
|
||||||
|
"order_book_top": 1,
|
||||||
|
"check_depth_of_market": {
|
||||||
|
"enabled": false,
|
||||||
|
"bids_to_ask_delta": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ask_strategy":{
|
||||||
|
"use_order_book": false,
|
||||||
|
"order_book_min": 1,
|
||||||
|
"order_book_max": 9
|
||||||
|
},
|
||||||
|
"order_types": {
|
||||||
|
"buy": "limit",
|
||||||
|
"sell": "limit",
|
||||||
|
"stoploss": "market",
|
||||||
|
"stoploss_on_exchange": false,
|
||||||
|
"stoploss_on_exchange_interval": 60
|
||||||
|
},
|
||||||
|
"order_time_in_force": {
|
||||||
|
"buy": "gtc",
|
||||||
|
"sell": "gtc"
|
||||||
|
},
|
||||||
|
"pairlist": {
|
||||||
|
"method": "VolumePairList",
|
||||||
|
"config": {
|
||||||
|
"number_assets": 20,
|
||||||
|
"sort_key": "quoteVolume",
|
||||||
|
"precision_filter": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exchange": {
|
||||||
|
"name": "bittrex",
|
||||||
|
"sandbox": false,
|
||||||
|
"key": "your_exchange_key",
|
||||||
|
"secret": "your_exchange_secret",
|
||||||
|
"password": "",
|
||||||
|
"ccxt_config": {"enableRateLimit": true},
|
||||||
|
"ccxt_async_config": {
|
||||||
|
"enableRateLimit": false,
|
||||||
|
"rateLimit": 500,
|
||||||
|
"aiohttp_trust_env": false
|
||||||
|
},
|
||||||
|
"pair_whitelist": [
|
||||||
|
"ETH/BTC",
|
||||||
|
"LTC/BTC",
|
||||||
|
"ETC/BTC",
|
||||||
|
"DASH/BTC",
|
||||||
|
"ZEC/BTC",
|
||||||
|
"XLM/BTC",
|
||||||
|
"NXT/BTC",
|
||||||
|
"POWR/BTC",
|
||||||
|
"ADA/BTC",
|
||||||
|
"XMR/BTC"
|
||||||
|
],
|
||||||
|
"pair_blacklist": [
|
||||||
|
"DOGE/BTC"
|
||||||
|
],
|
||||||
|
"outdated_offset": 5,
|
||||||
|
"markets_refresh_interval": 60
|
||||||
|
},
|
||||||
|
"edge": {
|
||||||
|
"enabled": false,
|
||||||
|
"process_throttle_secs": 3600,
|
||||||
|
"calculate_since_number_of_days": 7,
|
||||||
|
"capital_available_percentage": 0.5,
|
||||||
|
"allowed_risk": 0.01,
|
||||||
|
"stoploss_range_min": -0.01,
|
||||||
|
"stoploss_range_max": -0.1,
|
||||||
|
"stoploss_range_step": -0.01,
|
||||||
|
"minimum_winrate": 0.60,
|
||||||
|
"minimum_expectancy": 0.20,
|
||||||
|
"min_trade_number": 10,
|
||||||
|
"max_trade_duration_minute": 1440,
|
||||||
|
"remove_pumps": false
|
||||||
|
},
|
||||||
|
"experimental": {
|
||||||
|
"use_sell_signal": false,
|
||||||
|
"sell_profit_only": false,
|
||||||
|
"ignore_roi_if_buy_signal": false
|
||||||
|
},
|
||||||
|
"telegram": {
|
||||||
|
// We can now comment out some settings
|
||||||
|
// "enabled": true,
|
||||||
|
"enabled": false,
|
||||||
|
"token": "your_telegram_token",
|
||||||
|
"chat_id": "your_telegram_chat_id"
|
||||||
|
},
|
||||||
|
"api_server": {
|
||||||
|
"enabled": false,
|
||||||
|
"listen_ip_address": "127.0.0.1",
|
||||||
|
"listen_port": 8080,
|
||||||
|
"username": "freqtrader",
|
||||||
|
"password": "SuperSecurePassword"
|
||||||
|
},
|
||||||
|
"db_url": "sqlite:///tradesv3.sqlite",
|
||||||
|
"initial_state": "running",
|
||||||
|
"forcebuy_enable": false,
|
||||||
|
"internals": {
|
||||||
|
"process_throttle_secs": 5
|
||||||
|
},
|
||||||
|
"strategy": "DefaultStrategy",
|
||||||
|
"strategy_path": "user_data/strategies/"
|
||||||
|
}
|
@ -10,6 +10,7 @@ from unittest.mock import MagicMock, PropertyMock
|
|||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import pytest
|
import pytest
|
||||||
|
import numpy as np
|
||||||
from telegram import Chat, Message, Update
|
from telegram import Chat, Message, Update
|
||||||
|
|
||||||
from freqtrade import constants, persistence
|
from freqtrade import constants, persistence
|
||||||
@ -25,17 +26,21 @@ from freqtrade.worker import Worker
|
|||||||
logging.getLogger('').setLevel(logging.INFO)
|
logging.getLogger('').setLevel(logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
|
# Do not mask numpy errors as warnings that no one read, raise the exсeption
|
||||||
|
np.seterr(all='raise')
|
||||||
|
|
||||||
|
|
||||||
def log_has(line, logs):
|
def log_has(line, logs):
|
||||||
# caplog mocker returns log as a tuple: ('freqtrade.something', logging.WARNING, 'foobar')
|
# caplog mocker returns log as a tuple: ('freqtrade.something', logging.WARNING, 'foobar')
|
||||||
# and we want to match line against foobar in the tuple
|
# and we want to match line against foobar in the tuple
|
||||||
return reduce(lambda a, b: a or b,
|
return reduce(lambda a, b: a or b,
|
||||||
filter(lambda x: x[2] == line, logs),
|
filter(lambda x: x[2] == line, logs.record_tuples),
|
||||||
False)
|
False)
|
||||||
|
|
||||||
|
|
||||||
def log_has_re(line, logs):
|
def log_has_re(line, logs):
|
||||||
return reduce(lambda a, b: a or b,
|
return reduce(lambda a, b: a or b,
|
||||||
filter(lambda x: re.match(line, x[2]), logs),
|
filter(lambda x: re.match(line, x[2]), logs.record_tuples),
|
||||||
False)
|
False)
|
||||||
|
|
||||||
|
|
||||||
@ -45,7 +50,7 @@ def get_args(args):
|
|||||||
|
|
||||||
def patched_configuration_load_config_file(mocker, config) -> None:
|
def patched_configuration_load_config_file(mocker, config) -> None:
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
'freqtrade.configuration.configuration.Configuration._load_config_file',
|
'freqtrade.configuration.configuration.load_config_file',
|
||||||
lambda *args, **kwargs: config
|
lambda *args, **kwargs: config
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -234,6 +239,7 @@ def default_conf():
|
|||||||
},
|
},
|
||||||
"initial_state": "running",
|
"initial_state": "running",
|
||||||
"db_url": "sqlite://",
|
"db_url": "sqlite://",
|
||||||
|
"user_data_dir": Path("user_data"),
|
||||||
"verbosity": 3,
|
"verbosity": 3,
|
||||||
}
|
}
|
||||||
return configuration
|
return configuration
|
||||||
@ -304,7 +310,7 @@ def markets():
|
|||||||
'max': 500000,
|
'max': 500000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'info': '',
|
'info': {},
|
||||||
},
|
},
|
||||||
'TKN/BTC': {
|
'TKN/BTC': {
|
||||||
'id': 'tknbtc',
|
'id': 'tknbtc',
|
||||||
@ -329,7 +335,7 @@ def markets():
|
|||||||
'max': 500000,
|
'max': 500000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'info': '',
|
'info': {},
|
||||||
},
|
},
|
||||||
'BLK/BTC': {
|
'BLK/BTC': {
|
||||||
'id': 'blkbtc',
|
'id': 'blkbtc',
|
||||||
@ -354,7 +360,7 @@ def markets():
|
|||||||
'max': 500000,
|
'max': 500000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'info': '',
|
'info': {},
|
||||||
},
|
},
|
||||||
'LTC/BTC': {
|
'LTC/BTC': {
|
||||||
'id': 'ltcbtc',
|
'id': 'ltcbtc',
|
||||||
@ -379,7 +385,7 @@ def markets():
|
|||||||
'max': 500000,
|
'max': 500000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'info': '',
|
'info': {},
|
||||||
},
|
},
|
||||||
'XRP/BTC': {
|
'XRP/BTC': {
|
||||||
'id': 'xrpbtc',
|
'id': 'xrpbtc',
|
||||||
@ -404,7 +410,7 @@ def markets():
|
|||||||
'max': 500000,
|
'max': 500000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'info': '',
|
'info': {},
|
||||||
},
|
},
|
||||||
'NEO/BTC': {
|
'NEO/BTC': {
|
||||||
'id': 'neobtc',
|
'id': 'neobtc',
|
||||||
@ -429,7 +435,7 @@ def markets():
|
|||||||
'max': 500000,
|
'max': 500000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'info': '',
|
'info': {},
|
||||||
},
|
},
|
||||||
'BTT/BTC': {
|
'BTT/BTC': {
|
||||||
'id': 'BTTBTC',
|
'id': 'BTTBTC',
|
||||||
@ -457,7 +463,7 @@ def markets():
|
|||||||
'max': None
|
'max': None
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'info': "",
|
'info': {},
|
||||||
},
|
},
|
||||||
'ETH/USDT': {
|
'ETH/USDT': {
|
||||||
'id': 'USDT-ETH',
|
'id': 'USDT-ETH',
|
||||||
@ -479,7 +485,7 @@ def markets():
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
'active': True,
|
'active': True,
|
||||||
'info': ""
|
'info': {},
|
||||||
},
|
},
|
||||||
'LTC/USDT': {
|
'LTC/USDT': {
|
||||||
'id': 'USDT-LTC',
|
'id': 'USDT-LTC',
|
||||||
@ -501,7 +507,7 @@ def markets():
|
|||||||
'max': None
|
'max': None
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'info': ""
|
'info': {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import pytest
|
|||||||
from arrow import Arrow
|
from arrow import Arrow
|
||||||
from pandas import DataFrame, to_datetime
|
from pandas import DataFrame, to_datetime
|
||||||
|
|
||||||
from freqtrade.configuration import Arguments, TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.data.btanalysis import (BT_DATA_COLUMNS,
|
from freqtrade.data.btanalysis import (BT_DATA_COLUMNS,
|
||||||
combine_tickers_with_mean,
|
combine_tickers_with_mean,
|
||||||
create_cum_profit,
|
create_cum_profit,
|
||||||
@ -45,6 +45,11 @@ def test_load_trades_db(default_conf, fee, mocker):
|
|||||||
assert isinstance(trades, DataFrame)
|
assert isinstance(trades, DataFrame)
|
||||||
assert "pair" in trades.columns
|
assert "pair" in trades.columns
|
||||||
assert "open_time" in trades.columns
|
assert "open_time" in trades.columns
|
||||||
|
assert "profitperc" in trades.columns
|
||||||
|
|
||||||
|
for col in BT_DATA_COLUMNS:
|
||||||
|
if col not in ['index', 'open_at_end']:
|
||||||
|
assert col in trades.columns
|
||||||
|
|
||||||
|
|
||||||
def test_extract_trades_of_period():
|
def test_extract_trades_of_period():
|
||||||
@ -116,7 +121,7 @@ def test_combine_tickers_with_mean():
|
|||||||
def test_create_cum_profit():
|
def test_create_cum_profit():
|
||||||
filename = make_testdata_path(None) / "backtest-result_test.json"
|
filename = make_testdata_path(None) / "backtest-result_test.json"
|
||||||
bt_data = load_backtest_data(filename)
|
bt_data = load_backtest_data(filename)
|
||||||
timerange = Arguments.parse_timerange("20180110-20180112")
|
timerange = TimeRange.parse_timerange("20180110-20180112")
|
||||||
|
|
||||||
df = load_pair_history(pair="POWR/BTC", ticker_interval='5m',
|
df = load_pair_history(pair="POWR/BTC", ticker_interval='5m',
|
||||||
datadir=None, timerange=timerange)
|
datadir=None, timerange=timerange)
|
||||||
|
@ -18,7 +18,7 @@ def test_parse_ticker_dataframe(ticker_history_list, caplog):
|
|||||||
dataframe = parse_ticker_dataframe(ticker_history_list, '5m',
|
dataframe = parse_ticker_dataframe(ticker_history_list, '5m',
|
||||||
pair="UNITTEST/BTC", fill_missing=True)
|
pair="UNITTEST/BTC", fill_missing=True)
|
||||||
assert dataframe.columns.tolist() == columns
|
assert dataframe.columns.tolist() == columns
|
||||||
assert log_has('Parsing tickerlist to dataframe', caplog.record_tuples)
|
assert log_has('Parsing tickerlist to dataframe', caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_ohlcv_fill_up_missing_data(caplog):
|
def test_ohlcv_fill_up_missing_data(caplog):
|
||||||
@ -34,8 +34,7 @@ def test_ohlcv_fill_up_missing_data(caplog):
|
|||||||
assert (data.columns == data2.columns).all()
|
assert (data.columns == data2.columns).all()
|
||||||
|
|
||||||
assert log_has(f"Missing data fillup for UNITTEST/BTC: before: "
|
assert log_has(f"Missing data fillup for UNITTEST/BTC: before: "
|
||||||
f"{len(data)} - after: {len(data2)}",
|
f"{len(data)} - after: {len(data2)}", caplog)
|
||||||
caplog.record_tuples)
|
|
||||||
|
|
||||||
# Test fillup actually fixes invalid backtest data
|
# Test fillup actually fixes invalid backtest data
|
||||||
min_date, max_date = get_timeframe({'UNITTEST/BTC': data})
|
min_date, max_date = get_timeframe({'UNITTEST/BTC': data})
|
||||||
@ -97,8 +96,7 @@ def test_ohlcv_fill_up_missing_data2(caplog):
|
|||||||
assert (data.columns == data2.columns).all()
|
assert (data.columns == data2.columns).all()
|
||||||
|
|
||||||
assert log_has(f"Missing data fillup for UNITTEST/BTC: before: "
|
assert log_has(f"Missing data fillup for UNITTEST/BTC: before: "
|
||||||
f"{len(data)} - after: {len(data2)}",
|
f"{len(data)} - after: {len(data2)}", caplog)
|
||||||
caplog.record_tuples)
|
|
||||||
|
|
||||||
|
|
||||||
def test_ohlcv_drop_incomplete(caplog):
|
def test_ohlcv_drop_incomplete(caplog):
|
||||||
@ -140,11 +138,11 @@ def test_ohlcv_drop_incomplete(caplog):
|
|||||||
data = parse_ticker_dataframe(ticks, ticker_interval, pair="UNITTEST/BTC",
|
data = parse_ticker_dataframe(ticks, ticker_interval, pair="UNITTEST/BTC",
|
||||||
fill_missing=False, drop_incomplete=False)
|
fill_missing=False, drop_incomplete=False)
|
||||||
assert len(data) == 4
|
assert len(data) == 4
|
||||||
assert not log_has("Dropping last candle", caplog.record_tuples)
|
assert not log_has("Dropping last candle", caplog)
|
||||||
|
|
||||||
# Drop last candle
|
# Drop last candle
|
||||||
data = parse_ticker_dataframe(ticks, ticker_interval, pair="UNITTEST/BTC",
|
data = parse_ticker_dataframe(ticks, ticker_interval, pair="UNITTEST/BTC",
|
||||||
fill_missing=False, drop_incomplete=True)
|
fill_missing=False, drop_incomplete=True)
|
||||||
assert len(data) == 3
|
assert len(data) == 3
|
||||||
|
|
||||||
assert log_has("Dropping last candle", caplog.record_tuples)
|
assert log_has("Dropping last candle", caplog)
|
||||||
|
@ -13,6 +13,7 @@ def test_ohlcv(mocker, default_conf, ticker_history):
|
|||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
exchange._klines[("XRP/BTC", ticker_interval)] = ticker_history
|
exchange._klines[("XRP/BTC", ticker_interval)] = ticker_history
|
||||||
exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history
|
exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history
|
||||||
|
|
||||||
dp = DataProvider(default_conf, exchange)
|
dp = DataProvider(default_conf, exchange)
|
||||||
assert dp.runmode == RunMode.DRY_RUN
|
assert dp.runmode == RunMode.DRY_RUN
|
||||||
assert ticker_history.equals(dp.ohlcv("UNITTEST/BTC", ticker_interval))
|
assert ticker_history.equals(dp.ohlcv("UNITTEST/BTC", ticker_interval))
|
||||||
@ -37,11 +38,9 @@ def test_ohlcv(mocker, default_conf, ticker_history):
|
|||||||
|
|
||||||
|
|
||||||
def test_historic_ohlcv(mocker, default_conf, ticker_history):
|
def test_historic_ohlcv(mocker, default_conf, ticker_history):
|
||||||
|
|
||||||
historymock = MagicMock(return_value=ticker_history)
|
historymock = MagicMock(return_value=ticker_history)
|
||||||
mocker.patch("freqtrade.data.dataprovider.load_pair_history", historymock)
|
mocker.patch("freqtrade.data.dataprovider.load_pair_history", historymock)
|
||||||
|
|
||||||
# exchange = get_patched_exchange(mocker, default_conf)
|
|
||||||
dp = DataProvider(default_conf, None)
|
dp = DataProvider(default_conf, None)
|
||||||
data = dp.historic_ohlcv("UNITTEST/BTC", "5m")
|
data = dp.historic_ohlcv("UNITTEST/BTC", "5m")
|
||||||
assert isinstance(data, DataFrame)
|
assert isinstance(data, DataFrame)
|
||||||
@ -51,14 +50,47 @@ def test_historic_ohlcv(mocker, default_conf, ticker_history):
|
|||||||
assert historymock.call_args_list[0][1]["ticker_interval"] == "5m"
|
assert historymock.call_args_list[0][1]["ticker_interval"] == "5m"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_pair_dataframe(mocker, default_conf, ticker_history):
|
||||||
|
default_conf["runmode"] = RunMode.DRY_RUN
|
||||||
|
ticker_interval = default_conf["ticker_interval"]
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
exchange._klines[("XRP/BTC", ticker_interval)] = ticker_history
|
||||||
|
exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history
|
||||||
|
|
||||||
|
dp = DataProvider(default_conf, exchange)
|
||||||
|
assert dp.runmode == RunMode.DRY_RUN
|
||||||
|
assert ticker_history.equals(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval))
|
||||||
|
assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval), DataFrame)
|
||||||
|
assert dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval) is not ticker_history
|
||||||
|
assert not dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval).empty
|
||||||
|
assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty
|
||||||
|
|
||||||
|
# Test with and without parameter
|
||||||
|
assert dp.get_pair_dataframe("UNITTEST/BTC",
|
||||||
|
ticker_interval).equals(dp.get_pair_dataframe("UNITTEST/BTC"))
|
||||||
|
|
||||||
|
default_conf["runmode"] = RunMode.LIVE
|
||||||
|
dp = DataProvider(default_conf, exchange)
|
||||||
|
assert dp.runmode == RunMode.LIVE
|
||||||
|
assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval), DataFrame)
|
||||||
|
assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty
|
||||||
|
|
||||||
|
historymock = MagicMock(return_value=ticker_history)
|
||||||
|
mocker.patch("freqtrade.data.dataprovider.load_pair_history", historymock)
|
||||||
|
default_conf["runmode"] = RunMode.BACKTEST
|
||||||
|
dp = DataProvider(default_conf, exchange)
|
||||||
|
assert dp.runmode == RunMode.BACKTEST
|
||||||
|
assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval), DataFrame)
|
||||||
|
# assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty
|
||||||
|
|
||||||
|
|
||||||
def test_available_pairs(mocker, default_conf, ticker_history):
|
def test_available_pairs(mocker, default_conf, ticker_history):
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
|
||||||
ticker_interval = default_conf["ticker_interval"]
|
ticker_interval = default_conf["ticker_interval"]
|
||||||
exchange._klines[("XRP/BTC", ticker_interval)] = ticker_history
|
exchange._klines[("XRP/BTC", ticker_interval)] = ticker_history
|
||||||
exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history
|
exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history
|
||||||
dp = DataProvider(default_conf, exchange)
|
|
||||||
|
|
||||||
|
dp = DataProvider(default_conf, exchange)
|
||||||
assert len(dp.available_pairs) == 2
|
assert len(dp.available_pairs) == 2
|
||||||
assert dp.available_pairs == [
|
assert dp.available_pairs == [
|
||||||
("XRP/BTC", ticker_interval),
|
("XRP/BTC", ticker_interval),
|
||||||
|
@ -64,8 +64,7 @@ def test_load_data_30min_ticker(mocker, caplog, default_conf) -> None:
|
|||||||
assert isinstance(ld, DataFrame)
|
assert isinstance(ld, DataFrame)
|
||||||
assert not log_has(
|
assert not log_has(
|
||||||
'Download history data for pair: "UNITTEST/BTC", interval: 30m '
|
'Download history data for pair: "UNITTEST/BTC", interval: 30m '
|
||||||
'and store in None.',
|
'and store in None.', caplog
|
||||||
caplog.record_tuples
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -75,22 +74,20 @@ def test_load_data_7min_ticker(mocker, caplog, default_conf) -> None:
|
|||||||
assert ld is None
|
assert ld is None
|
||||||
assert log_has(
|
assert log_has(
|
||||||
'No history data for pair: "UNITTEST/BTC", interval: 7m. '
|
'No history data for pair: "UNITTEST/BTC", interval: 7m. '
|
||||||
'Use --refresh-pairs-cached option or download_backtest_data.py '
|
'Use --refresh-pairs-cached option or `freqtrade download-data` '
|
||||||
'script to download the data',
|
'script to download the data', caplog
|
||||||
caplog.record_tuples
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_load_data_1min_ticker(ticker_history, mocker, caplog) -> None:
|
def test_load_data_1min_ticker(ticker_history, mocker, caplog) -> None:
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history)
|
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ticker_history)
|
||||||
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-1m.json')
|
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-1m.json')
|
||||||
_backup_file(file, copy_file=True)
|
_backup_file(file, copy_file=True)
|
||||||
history.load_data(datadir=None, ticker_interval='1m', pairs=['UNITTEST/BTC'])
|
history.load_data(datadir=None, ticker_interval='1m', pairs=['UNITTEST/BTC'])
|
||||||
assert os.path.isfile(file) is True
|
assert os.path.isfile(file) is True
|
||||||
assert not log_has(
|
assert not log_has(
|
||||||
'Download history data for pair: "UNITTEST/BTC", interval: 1m '
|
'Download history data for pair: "UNITTEST/BTC", interval: 1m '
|
||||||
'and store in None.',
|
'and store in None.', caplog
|
||||||
caplog.record_tuples
|
|
||||||
)
|
)
|
||||||
_clean_test_file(file)
|
_clean_test_file(file)
|
||||||
|
|
||||||
@ -99,7 +96,7 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, defau
|
|||||||
"""
|
"""
|
||||||
Test load_pair_history() with 1 min ticker
|
Test load_pair_history() with 1 min ticker
|
||||||
"""
|
"""
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history_list)
|
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ticker_history_list)
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
|
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
|
||||||
|
|
||||||
@ -112,9 +109,8 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, defau
|
|||||||
assert os.path.isfile(file) is False
|
assert os.path.isfile(file) is False
|
||||||
assert log_has(
|
assert log_has(
|
||||||
'No history data for pair: "MEME/BTC", interval: 1m. '
|
'No history data for pair: "MEME/BTC", interval: 1m. '
|
||||||
'Use --refresh-pairs-cached option or download_backtest_data.py '
|
'Use --refresh-pairs-cached option or `freqtrade download-data` '
|
||||||
'script to download the data',
|
'script to download the data', caplog
|
||||||
caplog.record_tuples
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# download a new pair if refresh_pairs is set
|
# download a new pair if refresh_pairs is set
|
||||||
@ -126,8 +122,7 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, defau
|
|||||||
assert os.path.isfile(file) is True
|
assert os.path.isfile(file) is True
|
||||||
assert log_has(
|
assert log_has(
|
||||||
'Download history data for pair: "MEME/BTC", interval: 1m '
|
'Download history data for pair: "MEME/BTC", interval: 1m '
|
||||||
'and store in None.',
|
'and store in None.', caplog
|
||||||
caplog.record_tuples
|
|
||||||
)
|
)
|
||||||
with pytest.raises(OperationalException, match=r'Exchange needs to be initialized when.*'):
|
with pytest.raises(OperationalException, match=r'Exchange needs to be initialized when.*'):
|
||||||
history.load_pair_history(datadir=None,
|
history.load_pair_history(datadir=None,
|
||||||
@ -149,7 +144,7 @@ def test_load_data_live(default_conf, mocker, caplog) -> None:
|
|||||||
exchange=exchange)
|
exchange=exchange)
|
||||||
assert refresh_mock.call_count == 1
|
assert refresh_mock.call_count == 1
|
||||||
assert len(refresh_mock.call_args_list[0][0][0]) == 2
|
assert len(refresh_mock.call_args_list[0][0][0]) == 2
|
||||||
assert log_has('Live: Downloading data for all defined pairs ...', caplog.record_tuples)
|
assert log_has('Live: Downloading data for all defined pairs ...', caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_load_data_live_noexchange(default_conf, mocker, caplog) -> None:
|
def test_load_data_live_noexchange(default_conf, mocker, caplog) -> None:
|
||||||
@ -183,16 +178,13 @@ def test_load_cached_data_for_updating(mocker) -> None:
|
|||||||
# timeframe starts earlier than the cached data
|
# timeframe starts earlier than the cached data
|
||||||
# should fully update data
|
# should fully update data
|
||||||
timerange = TimeRange('date', None, test_data[0][0] / 1000 - 1, 0)
|
timerange = TimeRange('date', None, test_data[0][0] / 1000 - 1, 0)
|
||||||
data, start_ts = load_cached_data_for_updating(test_filename,
|
data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', timerange)
|
||||||
'1m',
|
|
||||||
timerange)
|
|
||||||
assert data == []
|
assert data == []
|
||||||
assert start_ts == test_data[0][0] - 1000
|
assert start_ts == test_data[0][0] - 1000
|
||||||
|
|
||||||
# same with 'line' timeframe
|
# same with 'line' timeframe
|
||||||
num_lines = (test_data[-1][0] - test_data[1][0]) / 1000 / 60 + 120
|
num_lines = (test_data[-1][0] - test_data[1][0]) / 1000 / 60 + 120
|
||||||
data, start_ts = load_cached_data_for_updating(test_filename,
|
data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m',
|
||||||
'1m',
|
|
||||||
TimeRange(None, 'line', 0, -num_lines))
|
TimeRange(None, 'line', 0, -num_lines))
|
||||||
assert data == []
|
assert data == []
|
||||||
assert start_ts < test_data[0][0] - 1
|
assert start_ts < test_data[0][0] - 1
|
||||||
@ -200,36 +192,29 @@ def test_load_cached_data_for_updating(mocker) -> None:
|
|||||||
# timeframe starts in the center of the cached data
|
# timeframe starts in the center of the cached data
|
||||||
# should return the chached data w/o the last item
|
# should return the chached data w/o the last item
|
||||||
timerange = TimeRange('date', None, test_data[0][0] / 1000 + 1, 0)
|
timerange = TimeRange('date', None, test_data[0][0] / 1000 + 1, 0)
|
||||||
data, start_ts = load_cached_data_for_updating(test_filename,
|
data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', timerange)
|
||||||
'1m',
|
|
||||||
timerange)
|
|
||||||
assert data == test_data[:-1]
|
assert data == test_data[:-1]
|
||||||
assert test_data[-2][0] < start_ts < test_data[-1][0]
|
assert test_data[-2][0] < start_ts < test_data[-1][0]
|
||||||
|
|
||||||
# same with 'line' timeframe
|
# same with 'line' timeframe
|
||||||
num_lines = (test_data[-1][0] - test_data[1][0]) / 1000 / 60 + 30
|
num_lines = (test_data[-1][0] - test_data[1][0]) / 1000 / 60 + 30
|
||||||
timerange = TimeRange(None, 'line', 0, -num_lines)
|
timerange = TimeRange(None, 'line', 0, -num_lines)
|
||||||
data, start_ts = load_cached_data_for_updating(test_filename,
|
data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', timerange)
|
||||||
'1m',
|
|
||||||
timerange)
|
|
||||||
assert data == test_data[:-1]
|
assert data == test_data[:-1]
|
||||||
assert test_data[-2][0] < start_ts < test_data[-1][0]
|
assert test_data[-2][0] < start_ts < test_data[-1][0]
|
||||||
|
|
||||||
# timeframe starts after the chached data
|
# timeframe starts after the chached data
|
||||||
# should return the chached data w/o the last item
|
# should return the chached data w/o the last item
|
||||||
timerange = TimeRange('date', None, test_data[-1][0] / 1000 + 1, 0)
|
timerange = TimeRange('date', None, test_data[-1][0] / 1000 + 1, 0)
|
||||||
data, start_ts = load_cached_data_for_updating(test_filename,
|
data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', timerange)
|
||||||
'1m',
|
|
||||||
timerange)
|
|
||||||
assert data == test_data[:-1]
|
assert data == test_data[:-1]
|
||||||
assert test_data[-2][0] < start_ts < test_data[-1][0]
|
assert test_data[-2][0] < start_ts < test_data[-1][0]
|
||||||
|
|
||||||
# same with 'line' timeframe
|
# Try loading last 30 lines.
|
||||||
|
# Not supported by load_cached_data_for_updating, we always need to get the full data.
|
||||||
num_lines = 30
|
num_lines = 30
|
||||||
timerange = TimeRange(None, 'line', 0, -num_lines)
|
timerange = TimeRange(None, 'line', 0, -num_lines)
|
||||||
data, start_ts = load_cached_data_for_updating(test_filename,
|
data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', timerange)
|
||||||
'1m',
|
|
||||||
timerange)
|
|
||||||
assert data == test_data[:-1]
|
assert data == test_data[:-1]
|
||||||
assert test_data[-2][0] < start_ts < test_data[-1][0]
|
assert test_data[-2][0] < start_ts < test_data[-1][0]
|
||||||
|
|
||||||
@ -237,41 +222,33 @@ def test_load_cached_data_for_updating(mocker) -> None:
|
|||||||
# should return the chached data w/o the last item
|
# should return the chached data w/o the last item
|
||||||
num_lines = 30
|
num_lines = 30
|
||||||
timerange = TimeRange(None, 'line', 0, -num_lines)
|
timerange = TimeRange(None, 'line', 0, -num_lines)
|
||||||
data, start_ts = load_cached_data_for_updating(test_filename,
|
data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', timerange)
|
||||||
'1m',
|
|
||||||
timerange)
|
|
||||||
assert data == test_data[:-1]
|
assert data == test_data[:-1]
|
||||||
assert test_data[-2][0] < start_ts < test_data[-1][0]
|
assert test_data[-2][0] < start_ts < test_data[-1][0]
|
||||||
|
|
||||||
# no datafile exist
|
# no datafile exist
|
||||||
# should return timestamp start time
|
# should return timestamp start time
|
||||||
timerange = TimeRange('date', None, now_ts - 10000, 0)
|
timerange = TimeRange('date', None, now_ts - 10000, 0)
|
||||||
data, start_ts = load_cached_data_for_updating(test_filename.with_name('unexist'),
|
data, start_ts = load_cached_data_for_updating(datadir, 'NONEXIST/BTC', '1m', timerange)
|
||||||
'1m',
|
|
||||||
timerange)
|
|
||||||
assert data == []
|
assert data == []
|
||||||
assert start_ts == (now_ts - 10000) * 1000
|
assert start_ts == (now_ts - 10000) * 1000
|
||||||
|
|
||||||
# same with 'line' timeframe
|
# same with 'line' timeframe
|
||||||
num_lines = 30
|
num_lines = 30
|
||||||
timerange = TimeRange(None, 'line', 0, -num_lines)
|
timerange = TimeRange(None, 'line', 0, -num_lines)
|
||||||
data, start_ts = load_cached_data_for_updating(test_filename.with_name('unexist'),
|
data, start_ts = load_cached_data_for_updating(datadir, 'NONEXIST/BTC', '1m', timerange)
|
||||||
'1m',
|
|
||||||
timerange)
|
|
||||||
assert data == []
|
assert data == []
|
||||||
assert start_ts == (now_ts - num_lines * 60) * 1000
|
assert start_ts == (now_ts - num_lines * 60) * 1000
|
||||||
|
|
||||||
# no datafile exist, no timeframe is set
|
# no datafile exist, no timeframe is set
|
||||||
# should return an empty array and None
|
# should return an empty array and None
|
||||||
data, start_ts = load_cached_data_for_updating(test_filename.with_name('unexist'),
|
data, start_ts = load_cached_data_for_updating(datadir, 'NONEXIST/BTC', '1m', None)
|
||||||
'1m',
|
|
||||||
None)
|
|
||||||
assert data == []
|
assert data == []
|
||||||
assert start_ts is None
|
assert start_ts is None
|
||||||
|
|
||||||
|
|
||||||
def test_download_pair_history(ticker_history_list, mocker, default_conf) -> None:
|
def test_download_pair_history(ticker_history_list, mocker, default_conf) -> None:
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history_list)
|
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ticker_history_list)
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
file1_1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
|
file1_1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
|
||||||
file1_5 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-5m.json')
|
file1_5 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-5m.json')
|
||||||
@ -324,7 +301,7 @@ def test_download_pair_history2(mocker, default_conf) -> None:
|
|||||||
[1509836580000, 0.00161, 0.00161, 0.00161, 0.00161, 82.390199]
|
[1509836580000, 0.00161, 0.00161, 0.00161, 0.00161, 82.390199]
|
||||||
]
|
]
|
||||||
json_dump_mock = mocker.patch('freqtrade.misc.file_dump_json', return_value=None)
|
json_dump_mock = mocker.patch('freqtrade.misc.file_dump_json', return_value=None)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=tick)
|
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=tick)
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
download_pair_history(None, exchange, pair="UNITTEST/BTC", ticker_interval='1m')
|
download_pair_history(None, exchange, pair="UNITTEST/BTC", ticker_interval='1m')
|
||||||
download_pair_history(None, exchange, pair="UNITTEST/BTC", ticker_interval='3m')
|
download_pair_history(None, exchange, pair="UNITTEST/BTC", ticker_interval='3m')
|
||||||
@ -332,7 +309,7 @@ def test_download_pair_history2(mocker, default_conf) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_download_backtesting_data_exception(ticker_history, mocker, caplog, default_conf) -> None:
|
def test_download_backtesting_data_exception(ticker_history, mocker, caplog, default_conf) -> None:
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_history',
|
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv',
|
||||||
side_effect=Exception('File Error'))
|
side_effect=Exception('File Error'))
|
||||||
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
@ -350,8 +327,7 @@ def test_download_backtesting_data_exception(ticker_history, mocker, caplog, def
|
|||||||
_clean_test_file(file1_5)
|
_clean_test_file(file1_5)
|
||||||
assert log_has(
|
assert log_has(
|
||||||
'Failed to download history data for pair: "MEME/BTC", interval: 1m. '
|
'Failed to download history data for pair: "MEME/BTC", interval: 1m. '
|
||||||
'Error: File Error',
|
'Error: File Error', caplog
|
||||||
caplog.record_tuples
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -380,7 +356,7 @@ def test_load_partial_missing(caplog) -> None:
|
|||||||
start_real = tickerdata['UNITTEST/BTC'].iloc[0, 0]
|
start_real = tickerdata['UNITTEST/BTC'].iloc[0, 0]
|
||||||
assert log_has(f'Missing data at start for pair '
|
assert log_has(f'Missing data at start for pair '
|
||||||
f'UNITTEST/BTC, data starts at {start_real.strftime("%Y-%m-%d %H:%M:%S")}',
|
f'UNITTEST/BTC, data starts at {start_real.strftime("%Y-%m-%d %H:%M:%S")}',
|
||||||
caplog.record_tuples)
|
caplog)
|
||||||
# Make sure we start fresh - test missing data at end
|
# Make sure we start fresh - test missing data at end
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
start = arrow.get('2018-01-10T00:00:00')
|
start = arrow.get('2018-01-10T00:00:00')
|
||||||
@ -396,7 +372,7 @@ def test_load_partial_missing(caplog) -> None:
|
|||||||
end_real = arrow.get(tickerdata['UNITTEST/BTC'].iloc[-1, 0]).shift(minutes=5)
|
end_real = arrow.get(tickerdata['UNITTEST/BTC'].iloc[-1, 0]).shift(minutes=5)
|
||||||
assert log_has(f'Missing data at end for pair '
|
assert log_has(f'Missing data at end for pair '
|
||||||
f'UNITTEST/BTC, data ends at {end_real.strftime("%Y-%m-%d %H:%M:%S")}',
|
f'UNITTEST/BTC, data ends at {end_real.strftime("%Y-%m-%d %H:%M:%S")}',
|
||||||
caplog.record_tuples)
|
caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_init(default_conf, mocker) -> None:
|
def test_init(default_conf, mocker) -> None:
|
||||||
@ -560,7 +536,7 @@ def test_validate_backtest_data_warn(default_conf, mocker, caplog) -> None:
|
|||||||
assert len(caplog.record_tuples) == 1
|
assert len(caplog.record_tuples) == 1
|
||||||
assert log_has(
|
assert log_has(
|
||||||
"UNITTEST/BTC has missing frames: expected 14396, got 13680, that's 716 missing values",
|
"UNITTEST/BTC has missing frames: expected 14396, got 13680, that's 716 missing values",
|
||||||
caplog.record_tuples)
|
caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_validate_backtest_data(default_conf, mocker, caplog) -> None:
|
def test_validate_backtest_data(default_conf, mocker, caplog) -> None:
|
||||||
|
@ -311,7 +311,7 @@ def test_edge_process_no_data(mocker, edge_conf, caplog):
|
|||||||
|
|
||||||
assert not edge.calculate()
|
assert not edge.calculate()
|
||||||
assert len(edge._cached_pairs) == 0
|
assert len(edge._cached_pairs) == 0
|
||||||
assert log_has("No data found. Edge is stopped ...", caplog.record_tuples)
|
assert log_has("No data found. Edge is stopped ...", caplog)
|
||||||
assert edge._last_updated == 0
|
assert edge._last_updated == 0
|
||||||
|
|
||||||
|
|
||||||
@ -326,7 +326,7 @@ def test_edge_process_no_trades(mocker, edge_conf, caplog):
|
|||||||
|
|
||||||
assert not edge.calculate()
|
assert not edge.calculate()
|
||||||
assert len(edge._cached_pairs) == 0
|
assert len(edge._cached_pairs) == 0
|
||||||
assert log_has("No trades found.", caplog.record_tuples)
|
assert log_has("No trades found.", caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_edge_init_error(mocker, edge_conf,):
|
def test_edge_init_error(mocker, edge_conf,):
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
# pragma pylint: disable=protected-access
|
# pragma pylint: disable=protected-access
|
||||||
import copy
|
import copy
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
from random import randint
|
from random import randint
|
||||||
from unittest.mock import MagicMock, Mock, PropertyMock
|
from unittest.mock import MagicMock, Mock, PropertyMock
|
||||||
|
|
||||||
@ -11,10 +11,14 @@ import ccxt
|
|||||||
import pytest
|
import pytest
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade import (DependencyException, OperationalException,
|
from freqtrade import (DependencyException, InvalidOrderException,
|
||||||
TemporaryError, InvalidOrderException)
|
OperationalException, TemporaryError)
|
||||||
from freqtrade.exchange import Binance, Exchange, Kraken
|
from freqtrade.exchange import Binance, Exchange, Kraken
|
||||||
from freqtrade.exchange.exchange import API_RETRY_COUNT
|
from freqtrade.exchange.exchange import (API_RETRY_COUNT, timeframe_to_minutes,
|
||||||
|
timeframe_to_msecs,
|
||||||
|
timeframe_to_next_date,
|
||||||
|
timeframe_to_prev_date,
|
||||||
|
timeframe_to_seconds)
|
||||||
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
||||||
from freqtrade.tests.conftest import get_patched_exchange, log_has, log_has_re
|
from freqtrade.tests.conftest import get_patched_exchange, log_has, log_has_re
|
||||||
|
|
||||||
@ -62,7 +66,7 @@ async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fu
|
|||||||
def test_init(default_conf, mocker, caplog):
|
def test_init(default_conf, mocker, caplog):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
get_patched_exchange(mocker, default_conf)
|
get_patched_exchange(mocker, default_conf)
|
||||||
assert log_has('Instance is running with dry_run enabled', caplog.record_tuples)
|
assert log_has('Instance is running with dry_run enabled', caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_init_ccxt_kwargs(default_conf, mocker, caplog):
|
def test_init_ccxt_kwargs(default_conf, mocker, caplog):
|
||||||
@ -71,8 +75,7 @@ def test_init_ccxt_kwargs(default_conf, mocker, caplog):
|
|||||||
conf = copy.deepcopy(default_conf)
|
conf = copy.deepcopy(default_conf)
|
||||||
conf['exchange']['ccxt_async_config'] = {'aiohttp_trust_env': True}
|
conf['exchange']['ccxt_async_config'] = {'aiohttp_trust_env': True}
|
||||||
ex = Exchange(conf)
|
ex = Exchange(conf)
|
||||||
assert log_has("Applying additional ccxt config: {'aiohttp_trust_env': True}",
|
assert log_has("Applying additional ccxt config: {'aiohttp_trust_env': True}", caplog)
|
||||||
caplog.record_tuples)
|
|
||||||
assert ex._api_async.aiohttp_trust_env
|
assert ex._api_async.aiohttp_trust_env
|
||||||
assert not ex._api.aiohttp_trust_env
|
assert not ex._api.aiohttp_trust_env
|
||||||
|
|
||||||
@ -81,20 +84,18 @@ def test_init_ccxt_kwargs(default_conf, mocker, caplog):
|
|||||||
conf = copy.deepcopy(default_conf)
|
conf = copy.deepcopy(default_conf)
|
||||||
conf['exchange']['ccxt_config'] = {'TestKWARG': 11}
|
conf['exchange']['ccxt_config'] = {'TestKWARG': 11}
|
||||||
ex = Exchange(conf)
|
ex = Exchange(conf)
|
||||||
assert not log_has("Applying additional ccxt config: {'aiohttp_trust_env': True}",
|
assert not log_has("Applying additional ccxt config: {'aiohttp_trust_env': True}", caplog)
|
||||||
caplog.record_tuples)
|
|
||||||
assert not ex._api_async.aiohttp_trust_env
|
assert not ex._api_async.aiohttp_trust_env
|
||||||
assert hasattr(ex._api, 'TestKWARG')
|
assert hasattr(ex._api, 'TestKWARG')
|
||||||
assert ex._api.TestKWARG == 11
|
assert ex._api.TestKWARG == 11
|
||||||
assert not hasattr(ex._api_async, 'TestKWARG')
|
assert not hasattr(ex._api_async, 'TestKWARG')
|
||||||
assert log_has("Applying additional ccxt config: {'TestKWARG': 11}",
|
assert log_has("Applying additional ccxt config: {'TestKWARG': 11}", caplog)
|
||||||
caplog.record_tuples)
|
|
||||||
|
|
||||||
|
|
||||||
def test_destroy(default_conf, mocker, caplog):
|
def test_destroy(default_conf, mocker, caplog):
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
get_patched_exchange(mocker, default_conf)
|
get_patched_exchange(mocker, default_conf)
|
||||||
assert log_has('Exchange object destroyed, closing async loop', caplog.record_tuples)
|
assert log_has('Exchange object destroyed, closing async loop', caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_init_exception(default_conf, mocker):
|
def test_init_exception(default_conf, mocker):
|
||||||
@ -120,8 +121,7 @@ def test_exchange_resolver(default_conf, mocker, caplog):
|
|||||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
exchange = ExchangeResolver('Bittrex', default_conf).exchange
|
exchange = ExchangeResolver('Bittrex', default_conf).exchange
|
||||||
assert isinstance(exchange, Exchange)
|
assert isinstance(exchange, Exchange)
|
||||||
assert log_has_re(r"No .* specific subclass found. Using the generic class instead.",
|
assert log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog)
|
||||||
caplog.record_tuples)
|
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
|
||||||
exchange = ExchangeResolver('kraken', default_conf).exchange
|
exchange = ExchangeResolver('kraken', default_conf).exchange
|
||||||
@ -129,7 +129,7 @@ def test_exchange_resolver(default_conf, mocker, caplog):
|
|||||||
assert isinstance(exchange, Kraken)
|
assert isinstance(exchange, Kraken)
|
||||||
assert not isinstance(exchange, Binance)
|
assert not isinstance(exchange, Binance)
|
||||||
assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.",
|
assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.",
|
||||||
caplog.record_tuples)
|
caplog)
|
||||||
|
|
||||||
exchange = ExchangeResolver('binance', default_conf).exchange
|
exchange = ExchangeResolver('binance', default_conf).exchange
|
||||||
assert isinstance(exchange, Exchange)
|
assert isinstance(exchange, Exchange)
|
||||||
@ -137,7 +137,7 @@ def test_exchange_resolver(default_conf, mocker, caplog):
|
|||||||
assert not isinstance(exchange, Kraken)
|
assert not isinstance(exchange, Kraken)
|
||||||
|
|
||||||
assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.",
|
assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.",
|
||||||
caplog.record_tuples)
|
caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_validate_order_time_in_force(default_conf, mocker, caplog):
|
def test_validate_order_time_in_force(default_conf, mocker, caplog):
|
||||||
@ -249,8 +249,7 @@ def test__load_async_markets(default_conf, mocker, caplog):
|
|||||||
exchange._api_async.load_markets = Mock(side_effect=ccxt.BaseError("deadbeef"))
|
exchange._api_async.load_markets = Mock(side_effect=ccxt.BaseError("deadbeef"))
|
||||||
exchange._load_async_markets()
|
exchange._load_async_markets()
|
||||||
|
|
||||||
assert log_has('Could not load async markets. Reason: deadbeef',
|
assert log_has('Could not load async markets. Reason: deadbeef', caplog)
|
||||||
caplog.record_tuples)
|
|
||||||
|
|
||||||
|
|
||||||
def test__load_markets(default_conf, mocker, caplog):
|
def test__load_markets(default_conf, mocker, caplog):
|
||||||
@ -262,7 +261,7 @@ def test__load_markets(default_conf, mocker, caplog):
|
|||||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
assert log_has('Unable to initialize markets. Reason: SomeError', caplog.record_tuples)
|
assert log_has('Unable to initialize markets. Reason: SomeError', caplog)
|
||||||
|
|
||||||
expected_return = {'ETH/BTC': 'available'}
|
expected_return = {'ETH/BTC': 'available'}
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
@ -298,7 +297,7 @@ def test__reload_markets(default_conf, mocker, caplog):
|
|||||||
exchange._last_markets_refresh = arrow.utcnow().timestamp - 15 * 60
|
exchange._last_markets_refresh = arrow.utcnow().timestamp - 15 * 60
|
||||||
exchange._reload_markets()
|
exchange._reload_markets()
|
||||||
assert exchange.markets == updated_markets
|
assert exchange.markets == updated_markets
|
||||||
assert log_has('Performing scheduled market reload..', caplog.record_tuples)
|
assert log_has('Performing scheduled market reload..', caplog)
|
||||||
|
|
||||||
|
|
||||||
def test__reload_markets_exception(default_conf, mocker, caplog):
|
def test__reload_markets_exception(default_conf, mocker, caplog):
|
||||||
@ -312,13 +311,13 @@ def test__reload_markets_exception(default_conf, mocker, caplog):
|
|||||||
# less than 10 minutes have passed, no reload
|
# less than 10 minutes have passed, no reload
|
||||||
exchange._reload_markets()
|
exchange._reload_markets()
|
||||||
assert exchange._last_markets_refresh == 0
|
assert exchange._last_markets_refresh == 0
|
||||||
assert log_has_re(r"Could not reload markets.*", caplog.record_tuples)
|
assert log_has_re(r"Could not reload markets.*", caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs directly
|
def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs directly
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
type(api_mock).markets = PropertyMock(return_value={
|
type(api_mock).markets = PropertyMock(return_value={
|
||||||
'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': ''
|
'ETH/BTC': {}, 'LTC/BTC': {}, 'XRP/BTC': {}, 'NEO/BTC': {}
|
||||||
})
|
})
|
||||||
id_mock = PropertyMock(return_value='test_exchange')
|
id_mock = PropertyMock(return_value='test_exchange')
|
||||||
type(api_mock).id = id_mock
|
type(api_mock).id = id_mock
|
||||||
@ -332,7 +331,7 @@ def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs d
|
|||||||
def test_validate_pairs_not_available(default_conf, mocker):
|
def test_validate_pairs_not_available(default_conf, mocker):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
type(api_mock).markets = PropertyMock(return_value={
|
type(api_mock).markets = PropertyMock(return_value={
|
||||||
'XRP/BTC': 'inactive'
|
'XRP/BTC': {'inactive': True}
|
||||||
})
|
})
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
@ -357,8 +356,23 @@ def test_validate_pairs_exception(default_conf, mocker, caplog):
|
|||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value={}))
|
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value={}))
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
assert log_has('Unable to validate pairs (assuming they are correct).',
|
assert log_has('Unable to validate pairs (assuming they are correct).', caplog)
|
||||||
caplog.record_tuples)
|
|
||||||
|
|
||||||
|
def test_validate_pairs_restricted(default_conf, mocker, caplog):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
type(api_mock).markets = PropertyMock(return_value={
|
||||||
|
'ETH/BTC': {}, 'LTC/BTC': {}, 'NEO/BTC': {},
|
||||||
|
'XRP/BTC': {'info': {'IsRestricted': True}}
|
||||||
|
})
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
|
||||||
|
|
||||||
|
Exchange(default_conf)
|
||||||
|
assert log_has(f"Pair XRP/BTC is restricted for some users on this exchange."
|
||||||
|
f"Please check if you are impacted by this restriction "
|
||||||
|
f"on the exchange and eventually remove XRP/BTC from your whitelist.", caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_validate_timeframes(default_conf, mocker):
|
def test_validate_timeframes(default_conf, mocker):
|
||||||
@ -642,7 +656,13 @@ def test_buy_prod(default_conf, mocker, exchange_name):
|
|||||||
with pytest.raises(DependencyException):
|
with pytest.raises(DependencyException):
|
||||||
api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
|
api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
exchange.buy(pair='ETH/BTC', ordertype=order_type,
|
exchange.buy(pair='ETH/BTC', ordertype='limit',
|
||||||
|
amount=1, rate=200, time_in_force=time_in_force)
|
||||||
|
|
||||||
|
with pytest.raises(DependencyException):
|
||||||
|
api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
|
exchange.buy(pair='ETH/BTC', ordertype='market',
|
||||||
amount=1, rate=200, time_in_force=time_in_force)
|
amount=1, rate=200, time_in_force=time_in_force)
|
||||||
|
|
||||||
with pytest.raises(TemporaryError):
|
with pytest.raises(TemporaryError):
|
||||||
@ -765,7 +785,13 @@ def test_sell_prod(default_conf, mocker, exchange_name):
|
|||||||
with pytest.raises(DependencyException):
|
with pytest.raises(DependencyException):
|
||||||
api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
|
api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)
|
exchange.sell(pair='ETH/BTC', ordertype='limit', amount=1, rate=200)
|
||||||
|
|
||||||
|
# Market orders don't require price, so the behaviour is slightly different
|
||||||
|
with pytest.raises(DependencyException):
|
||||||
|
api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
|
exchange.sell(pair='ETH/BTC', ordertype='market', amount=1, rate=200)
|
||||||
|
|
||||||
with pytest.raises(TemporaryError):
|
with pytest.raises(TemporaryError):
|
||||||
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No Connection"))
|
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No Connection"))
|
||||||
@ -986,7 +1012,7 @@ def test_get_ticker(default_conf, mocker, exchange_name):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
def test_get_history(default_conf, mocker, caplog, exchange_name):
|
def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name):
|
||||||
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||||
tick = [
|
tick = [
|
||||||
[
|
[
|
||||||
@ -1007,7 +1033,7 @@ def test_get_history(default_conf, mocker, caplog, exchange_name):
|
|||||||
# one_call calculation * 1.8 should do 2 calls
|
# one_call calculation * 1.8 should do 2 calls
|
||||||
since = 5 * 60 * 500 * 1.8
|
since = 5 * 60 * 500 * 1.8
|
||||||
print(f"since = {since}")
|
print(f"since = {since}")
|
||||||
ret = exchange.get_history(pair, "5m", int((arrow.utcnow().timestamp - since) * 1000))
|
ret = exchange.get_historic_ohlcv(pair, "5m", int((arrow.utcnow().timestamp - since) * 1000))
|
||||||
|
|
||||||
assert exchange._async_get_candle_history.call_count == 2
|
assert exchange._async_get_candle_history.call_count == 2
|
||||||
# Returns twice the above tick
|
# Returns twice the above tick
|
||||||
@ -1043,7 +1069,7 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
|
|||||||
assert not exchange._klines
|
assert not exchange._klines
|
||||||
exchange.refresh_latest_ohlcv(pairs)
|
exchange.refresh_latest_ohlcv(pairs)
|
||||||
|
|
||||||
assert log_has(f'Refreshing ohlcv data for {len(pairs)} pairs', caplog.record_tuples)
|
assert log_has(f'Refreshing ohlcv data for {len(pairs)} pairs', caplog)
|
||||||
assert exchange._klines
|
assert exchange._klines
|
||||||
assert exchange._api_async.fetch_ohlcv.call_count == 2
|
assert exchange._api_async.fetch_ohlcv.call_count == 2
|
||||||
for pair in pairs:
|
for pair in pairs:
|
||||||
@ -1062,7 +1088,7 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
|
|||||||
|
|
||||||
assert exchange._api_async.fetch_ohlcv.call_count == 2
|
assert exchange._api_async.fetch_ohlcv.call_count == 2
|
||||||
assert log_has(f"Using cached ohlcv data for pair {pairs[0][0]}, interval {pairs[0][1]} ...",
|
assert log_has(f"Using cached ohlcv data for pair {pairs[0][0]}, interval {pairs[0][1]} ...",
|
||||||
caplog.record_tuples)
|
caplog)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@ -1092,7 +1118,7 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_
|
|||||||
assert res[1] == "5m"
|
assert res[1] == "5m"
|
||||||
assert res[2] == tick
|
assert res[2] == tick
|
||||||
assert exchange._api_async.fetch_ohlcv.call_count == 1
|
assert exchange._api_async.fetch_ohlcv.call_count == 1
|
||||||
assert not log_has(f"Using cached ohlcv data for {pair} ...", caplog.record_tuples)
|
assert not log_has(f"Using cached ohlcv data for {pair} ...", caplog)
|
||||||
|
|
||||||
# exchange = Exchange(default_conf)
|
# exchange = Exchange(default_conf)
|
||||||
await async_ccxt_exception(mocker, default_conf, MagicMock(),
|
await async_ccxt_exception(mocker, default_conf, MagicMock(),
|
||||||
@ -1151,8 +1177,8 @@ def test_refresh_latest_ohlcv_inv_result(default_conf, mocker, caplog):
|
|||||||
# Test that each is in list at least once as order is not guaranteed
|
# Test that each is in list at least once as order is not guaranteed
|
||||||
assert type(res[0]) is tuple or type(res[1]) is tuple
|
assert type(res[0]) is tuple or type(res[1]) is tuple
|
||||||
assert type(res[0]) is TypeError or type(res[1]) is TypeError
|
assert type(res[0]) is TypeError or type(res[1]) is TypeError
|
||||||
assert log_has("Error loading ETH/BTC. Result was [[]].", caplog.record_tuples)
|
assert log_has("Error loading ETH/BTC. Result was [[]].", caplog)
|
||||||
assert log_has("Async code raised an exception: TypeError", caplog.record_tuples)
|
assert log_has("Async code raised an exception: TypeError", caplog)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
@ -1314,6 +1340,9 @@ def test_get_order(default_conf, mocker, exchange_name):
|
|||||||
print(exchange.get_order('X', 'TKN/BTC'))
|
print(exchange.get_order('X', 'TKN/BTC'))
|
||||||
assert exchange.get_order('X', 'TKN/BTC').myid == 123
|
assert exchange.get_order('X', 'TKN/BTC').myid == 123
|
||||||
|
|
||||||
|
with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'):
|
||||||
|
exchange.get_order('Y', 'TKN/BTC')
|
||||||
|
|
||||||
default_conf['dry_run'] = False
|
default_conf['dry_run'] = False
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.fetch_order = MagicMock(return_value=456)
|
api_mock.fetch_order = MagicMock(return_value=456)
|
||||||
@ -1344,7 +1373,7 @@ def test_name(default_conf, mocker, exchange_name):
|
|||||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
def test_get_trades_for_order(default_conf, mocker, exchange_name):
|
def test_get_trades_for_order(default_conf, mocker, exchange_name):
|
||||||
order_id = 'ABCD-ABCD'
|
order_id = 'ABCD-ABCD'
|
||||||
since = datetime(2018, 5, 5)
|
since = datetime(2018, 5, 5, tzinfo=timezone.utc)
|
||||||
default_conf["dry_run"] = False
|
default_conf["dry_run"] = False
|
||||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True)
|
mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True)
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
@ -1374,6 +1403,13 @@ def test_get_trades_for_order(default_conf, mocker, exchange_name):
|
|||||||
orders = exchange.get_trades_for_order(order_id, 'LTC/BTC', since)
|
orders = exchange.get_trades_for_order(order_id, 'LTC/BTC', since)
|
||||||
assert len(orders) == 1
|
assert len(orders) == 1
|
||||||
assert orders[0]['price'] == 165
|
assert orders[0]['price'] == 165
|
||||||
|
assert api_mock.fetch_my_trades.call_count == 1
|
||||||
|
# since argument should be
|
||||||
|
assert isinstance(api_mock.fetch_my_trades.call_args[0][1], int)
|
||||||
|
assert api_mock.fetch_my_trades.call_args[0][0] == 'LTC/BTC'
|
||||||
|
# Same test twice, hardcoded number and doing the same calculation
|
||||||
|
assert api_mock.fetch_my_trades.call_args[0][1] == 1525478395000
|
||||||
|
assert api_mock.fetch_my_trades.call_args[0][1] == int(since.timestamp() - 5) * 1000
|
||||||
|
|
||||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
|
||||||
'get_trades_for_order', 'fetch_my_trades',
|
'get_trades_for_order', 'fetch_my_trades',
|
||||||
@ -1523,3 +1559,74 @@ def test_get_valid_pair_combination(default_conf, mocker, markets):
|
|||||||
assert ex.get_valid_pair_combination("BTC", "ETH") == "ETH/BTC"
|
assert ex.get_valid_pair_combination("BTC", "ETH") == "ETH/BTC"
|
||||||
with pytest.raises(DependencyException, match=r"Could not combine.* to get a valid pair."):
|
with pytest.raises(DependencyException, match=r"Could not combine.* to get a valid pair."):
|
||||||
ex.get_valid_pair_combination("NOPAIR", "ETH")
|
ex.get_valid_pair_combination("NOPAIR", "ETH")
|
||||||
|
|
||||||
|
|
||||||
|
def test_timeframe_to_minutes():
|
||||||
|
assert timeframe_to_minutes("5m") == 5
|
||||||
|
assert timeframe_to_minutes("10m") == 10
|
||||||
|
assert timeframe_to_minutes("1h") == 60
|
||||||
|
assert timeframe_to_minutes("1d") == 1440
|
||||||
|
|
||||||
|
|
||||||
|
def test_timeframe_to_seconds():
|
||||||
|
assert timeframe_to_seconds("5m") == 300
|
||||||
|
assert timeframe_to_seconds("10m") == 600
|
||||||
|
assert timeframe_to_seconds("1h") == 3600
|
||||||
|
assert timeframe_to_seconds("1d") == 86400
|
||||||
|
|
||||||
|
|
||||||
|
def test_timeframe_to_msecs():
|
||||||
|
assert timeframe_to_msecs("5m") == 300000
|
||||||
|
assert timeframe_to_msecs("10m") == 600000
|
||||||
|
assert timeframe_to_msecs("1h") == 3600000
|
||||||
|
assert timeframe_to_msecs("1d") == 86400000
|
||||||
|
|
||||||
|
|
||||||
|
def test_timeframe_to_prev_date():
|
||||||
|
# 2019-08-12 13:22:08
|
||||||
|
date = datetime.fromtimestamp(1565616128, tz=timezone.utc)
|
||||||
|
|
||||||
|
tf_list = [
|
||||||
|
# 5m -> 2019-08-12 13:20:00
|
||||||
|
("5m", datetime(2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc)),
|
||||||
|
# 10m -> 2019-08-12 13:20:00
|
||||||
|
("10m", datetime(2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc)),
|
||||||
|
# 1h -> 2019-08-12 13:00:00
|
||||||
|
("1h", datetime(2019, 8, 12, 13, 00, 0, tzinfo=timezone.utc)),
|
||||||
|
# 2h -> 2019-08-12 12:00:00
|
||||||
|
("2h", datetime(2019, 8, 12, 12, 00, 0, tzinfo=timezone.utc)),
|
||||||
|
# 4h -> 2019-08-12 12:00:00
|
||||||
|
("4h", datetime(2019, 8, 12, 12, 00, 0, tzinfo=timezone.utc)),
|
||||||
|
# 1d -> 2019-08-12 00:00:00
|
||||||
|
("1d", datetime(2019, 8, 12, 00, 00, 0, tzinfo=timezone.utc)),
|
||||||
|
]
|
||||||
|
for interval, result in tf_list:
|
||||||
|
assert timeframe_to_prev_date(interval, date) == result
|
||||||
|
|
||||||
|
date = datetime.now(tz=timezone.utc)
|
||||||
|
assert timeframe_to_prev_date("5m", date) < date
|
||||||
|
|
||||||
|
|
||||||
|
def test_timeframe_to_next_date():
|
||||||
|
# 2019-08-12 13:22:08
|
||||||
|
date = datetime.fromtimestamp(1565616128, tz=timezone.utc)
|
||||||
|
tf_list = [
|
||||||
|
# 5m -> 2019-08-12 13:25:00
|
||||||
|
("5m", datetime(2019, 8, 12, 13, 25, 0, tzinfo=timezone.utc)),
|
||||||
|
# 10m -> 2019-08-12 13:30:00
|
||||||
|
("10m", datetime(2019, 8, 12, 13, 30, 0, tzinfo=timezone.utc)),
|
||||||
|
# 1h -> 2019-08-12 14:00:00
|
||||||
|
("1h", datetime(2019, 8, 12, 14, 00, 0, tzinfo=timezone.utc)),
|
||||||
|
# 2h -> 2019-08-12 14:00:00
|
||||||
|
("2h", datetime(2019, 8, 12, 14, 00, 0, tzinfo=timezone.utc)),
|
||||||
|
# 4h -> 2019-08-12 14:00:00
|
||||||
|
("4h", datetime(2019, 8, 12, 16, 00, 0, tzinfo=timezone.utc)),
|
||||||
|
# 1d -> 2019-08-13 00:00:00
|
||||||
|
("1d", datetime(2019, 8, 13, 0, 0, 0, tzinfo=timezone.utc)),
|
||||||
|
]
|
||||||
|
|
||||||
|
for interval, result in tf_list:
|
||||||
|
assert timeframe_to_next_date(interval, date) == result
|
||||||
|
|
||||||
|
date = datetime.now(tz=timezone.utc)
|
||||||
|
assert timeframe_to_next_date("5m", date) > date
|
||||||
|
@ -14,9 +14,8 @@ from freqtrade.tests.optimize import (BTContainer, BTrade,
|
|||||||
_get_frame_time_from_offset,
|
_get_frame_time_from_offset,
|
||||||
tests_ticker_interval)
|
tests_ticker_interval)
|
||||||
|
|
||||||
# Test 0 Sell signal sell
|
# Test 0: Sell with signal sell in candle 3
|
||||||
# Test with Stop-loss at 1%
|
# Test with Stop-loss at 1%
|
||||||
# TC0: Sell signal in candle 3
|
|
||||||
tc0 = BTContainer(data=[
|
tc0 = BTContainer(data=[
|
||||||
# D O H L C V B S
|
# D O H L C V B S
|
||||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||||
@ -29,9 +28,8 @@ tc0 = BTContainer(data=[
|
|||||||
trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4)]
|
trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4)]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test 1 Minus 8% Close
|
# Test 1: Stop-Loss Triggered 1% loss
|
||||||
# Test with Stop-loss at 1%
|
# Test with Stop-loss at 1%
|
||||||
# TC1: Stop-Loss Triggered 1% loss
|
|
||||||
tc1 = BTContainer(data=[
|
tc1 = BTContainer(data=[
|
||||||
# D O H L C V B S
|
# D O H L C V B S
|
||||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||||
@ -45,9 +43,8 @@ tc1 = BTContainer(data=[
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Test 2 Minus 4% Low, minus 1% close
|
# Test 2: Minus 4% Low, minus 1% close
|
||||||
# Test with Stop-Loss at 3%
|
# Test with Stop-Loss at 3%
|
||||||
# TC2: Stop-Loss Triggered 3% Loss
|
|
||||||
tc2 = BTContainer(data=[
|
tc2 = BTContainer(data=[
|
||||||
# D O H L C V B S
|
# D O H L C V B S
|
||||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||||
@ -61,11 +58,11 @@ tc2 = BTContainer(data=[
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Test 3 Candle drops 4%, Recovers 1%.
|
# Test 3: Multiple trades.
|
||||||
|
# Candle drops 4%, Recovers 1%.
|
||||||
# Entry Criteria Met
|
# Entry Criteria Met
|
||||||
# Candle drops 20%
|
# Candle drops 20%
|
||||||
# Test with Stop-Loss at 2%
|
# Trade-A: Stop-Loss Triggered 2% Loss
|
||||||
# TC3: Trade-A: Stop-Loss Triggered 2% Loss
|
|
||||||
# Trade-B: Stop-Loss Triggered 2% Loss
|
# Trade-B: Stop-Loss Triggered 2% Loss
|
||||||
tc3 = BTContainer(data=[
|
tc3 = BTContainer(data=[
|
||||||
# D O H L C V B S
|
# D O H L C V B S
|
||||||
@ -81,10 +78,10 @@ tc3 = BTContainer(data=[
|
|||||||
BTrade(sell_reason=SellType.STOP_LOSS, open_tick=4, close_tick=5)]
|
BTrade(sell_reason=SellType.STOP_LOSS, open_tick=4, close_tick=5)]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test 4 Minus 3% / recovery +15%
|
# Test 4: Minus 3% / recovery +15%
|
||||||
# Candle Data for test 3 – Candle drops 3% Closed 15% up
|
# Candle Data for test 3 – Candle drops 3% Closed 15% up
|
||||||
# Test with Stop-loss at 2% ROI 6%
|
# Test with Stop-loss at 2% ROI 6%
|
||||||
# TC4: Stop-Loss Triggered 2% Loss
|
# Stop-Loss Triggered 2% Loss
|
||||||
tc4 = BTContainer(data=[
|
tc4 = BTContainer(data=[
|
||||||
# D O H L C V B S
|
# D O H L C V B S
|
||||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||||
@ -97,9 +94,8 @@ tc4 = BTContainer(data=[
|
|||||||
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2)]
|
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2)]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test 5 / Drops 0.5% Closes +20%
|
# Test 5: Drops 0.5% Closes +20%, ROI triggers 3% Gain
|
||||||
# Set stop-loss at 1% ROI 3%
|
# stop-loss: 1%, ROI: 3%
|
||||||
# TC5: ROI triggers 3% Gain
|
|
||||||
tc5 = BTContainer(data=[
|
tc5 = BTContainer(data=[
|
||||||
# D O H L C V B S
|
# D O H L C V B S
|
||||||
[0, 5000, 5025, 4980, 4987, 6172, 1, 0],
|
[0, 5000, 5025, 4980, 4987, 6172, 1, 0],
|
||||||
@ -112,9 +108,8 @@ tc5 = BTContainer(data=[
|
|||||||
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)]
|
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test 6 / Drops 3% / Recovers 6% Positive / Closes 1% positve
|
# Test 6: Drops 3% / Recovers 6% Positive / Closes 1% positve, Stop-Loss triggers 2% Loss
|
||||||
# Set stop-loss at 2% ROI at 5%
|
# stop-loss: 2% ROI: 5%
|
||||||
# TC6: Stop-Loss triggers 2% Loss
|
|
||||||
tc6 = BTContainer(data=[
|
tc6 = BTContainer(data=[
|
||||||
# D O H L C V B S
|
# D O H L C V B S
|
||||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||||
@ -127,9 +122,8 @@ tc6 = BTContainer(data=[
|
|||||||
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2)]
|
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2)]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test 7 - 6% Positive / 1% Negative / Close 1% Positve
|
# Test 7: 6% Positive / 1% Negative / Close 1% Positve, ROI Triggers 3% Gain
|
||||||
# Set stop-loss at 2% ROI at 3%
|
# stop-loss: 2% ROI: 3%
|
||||||
# TC7: ROI Triggers 3% Gain
|
|
||||||
tc7 = BTContainer(data=[
|
tc7 = BTContainer(data=[
|
||||||
# D O H L C V B S
|
# D O H L C V B S
|
||||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||||
@ -143,9 +137,8 @@ tc7 = BTContainer(data=[
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Test 8 - trailing_stop should raise so candle 3 causes a stoploss.
|
# Test 8: trailing_stop should raise so candle 3 causes a stoploss.
|
||||||
# Set stop-loss at 10%, ROI at 10% (should not apply)
|
# stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted in candle 2
|
||||||
# TC8: Trailing stoploss - stoploss should be adjusted candle 2
|
|
||||||
tc8 = BTContainer(data=[
|
tc8 = BTContainer(data=[
|
||||||
# D O H L C V B S
|
# D O H L C V B S
|
||||||
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
||||||
@ -158,10 +151,8 @@ tc8 = BTContainer(data=[
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Test 9 - trailing_stop should raise - high and low in same candle.
|
# Test 9: trailing_stop should raise - high and low in same candle.
|
||||||
# Candle Data for test 9
|
# stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted in candle 3
|
||||||
# Set stop-loss at 10%, ROI at 10% (should not apply)
|
|
||||||
# TC9: Trailing stoploss - stoploss should be adjusted candle 3
|
|
||||||
tc9 = BTContainer(data=[
|
tc9 = BTContainer(data=[
|
||||||
# D O H L C V B S
|
# D O H L C V B S
|
||||||
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
||||||
@ -173,10 +164,9 @@ tc9 = BTContainer(data=[
|
|||||||
trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)]
|
trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test 10 - trailing_stop should raise so candle 3 causes a stoploss
|
# Test 10: trailing_stop should raise so candle 3 causes a stoploss
|
||||||
# without applying trailing_stop_positive since stoploss_offset is at 10%.
|
# without applying trailing_stop_positive since stoploss_offset is at 10%.
|
||||||
# Set stop-loss at 10%, ROI at 10% (should not apply)
|
# stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted candle 2
|
||||||
# TC10: Trailing stoploss - stoploss should be adjusted candle 2
|
|
||||||
tc10 = BTContainer(data=[
|
tc10 = BTContainer(data=[
|
||||||
# D O H L C V B S
|
# D O H L C V B S
|
||||||
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
||||||
@ -190,10 +180,9 @@ tc10 = BTContainer(data=[
|
|||||||
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=4)]
|
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=4)]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test 11 - trailing_stop should raise so candle 3 causes a stoploss
|
# Test 11: trailing_stop should raise so candle 3 causes a stoploss
|
||||||
# applying a positive trailing stop of 3% since stop_positive_offset is reached.
|
# applying a positive trailing stop of 3% since stop_positive_offset is reached.
|
||||||
# Set stop-loss at 10%, ROI at 10% (should not apply)
|
# stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted candle 2
|
||||||
# TC11: Trailing stoploss - stoploss should be adjusted candle 2,
|
|
||||||
tc11 = BTContainer(data=[
|
tc11 = BTContainer(data=[
|
||||||
# D O H L C V B S
|
# D O H L C V B S
|
||||||
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
||||||
@ -207,10 +196,9 @@ tc11 = BTContainer(data=[
|
|||||||
trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)]
|
trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test 12 - trailing_stop should raise in candle 2 and cause a stoploss in the same candle
|
# Test 12: trailing_stop should raise in candle 2 and cause a stoploss in the same candle
|
||||||
# applying a positive trailing stop of 3% since stop_positive_offset is reached.
|
# applying a positive trailing stop of 3% since stop_positive_offset is reached.
|
||||||
# Set stop-loss at 10%, ROI at 10% (should not apply)
|
# stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted candle 2
|
||||||
# TC12: Trailing stoploss - stoploss should be adjusted candle 2,
|
|
||||||
tc12 = BTContainer(data=[
|
tc12 = BTContainer(data=[
|
||||||
# D O H L C V B S
|
# D O H L C V B S
|
||||||
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
||||||
@ -224,6 +212,47 @@ tc12 = BTContainer(data=[
|
|||||||
trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=2)]
|
trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=2)]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Test 13: Buy and sell ROI on same candle
|
||||||
|
# stop-loss: 10% (should not apply), ROI: 1%
|
||||||
|
tc13 = BTContainer(data=[
|
||||||
|
# D O H L C V B S
|
||||||
|
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
||||||
|
[1, 5000, 5100, 4950, 5100, 6172, 0, 0],
|
||||||
|
[2, 5100, 5251, 4850, 5100, 6172, 0, 0],
|
||||||
|
[3, 4850, 5050, 4850, 4750, 6172, 0, 0],
|
||||||
|
[4, 4750, 4950, 4850, 4750, 6172, 0, 0]],
|
||||||
|
stop_loss=-0.10, roi=0.01, profit_perc=0.01,
|
||||||
|
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=1)]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test 14 - Buy and Stoploss on same candle
|
||||||
|
# stop-loss: 5%, ROI: 10% (should not apply)
|
||||||
|
tc14 = BTContainer(data=[
|
||||||
|
# D O H L C V B S
|
||||||
|
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
||||||
|
[1, 5000, 5100, 4600, 5100, 6172, 0, 0],
|
||||||
|
[2, 5100, 5251, 4850, 5100, 6172, 0, 0],
|
||||||
|
[3, 4850, 5050, 4850, 4750, 6172, 0, 0],
|
||||||
|
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
|
||||||
|
stop_loss=-0.05, roi=0.10, profit_perc=-0.05,
|
||||||
|
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=1)]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Test 15 - Buy and ROI on same candle, followed by buy and Stoploss on next candle
|
||||||
|
# stop-loss: 5%, ROI: 10% (should not apply)
|
||||||
|
tc15 = BTContainer(data=[
|
||||||
|
# D O H L C V B S
|
||||||
|
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
||||||
|
[1, 5000, 5100, 4900, 5100, 6172, 1, 0],
|
||||||
|
[2, 5100, 5251, 4650, 5100, 6172, 0, 0],
|
||||||
|
[3, 4850, 5050, 4850, 4750, 6172, 0, 0],
|
||||||
|
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
|
||||||
|
stop_loss=-0.05, roi=0.01, profit_perc=-0.04,
|
||||||
|
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=1),
|
||||||
|
BTrade(sell_reason=SellType.STOP_LOSS, open_tick=2, close_tick=2)]
|
||||||
|
)
|
||||||
|
|
||||||
TESTS = [
|
TESTS = [
|
||||||
tc0,
|
tc0,
|
||||||
tc1,
|
tc1,
|
||||||
@ -238,6 +267,9 @@ TESTS = [
|
|||||||
tc10,
|
tc10,
|
||||||
tc11,
|
tc11,
|
||||||
tc12,
|
tc12,
|
||||||
|
tc13,
|
||||||
|
tc14,
|
||||||
|
tc15,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import math
|
import math
|
||||||
import random
|
import random
|
||||||
|
from pathlib import Path
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@ -9,7 +10,7 @@ import pandas as pd
|
|||||||
import pytest
|
import pytest
|
||||||
from arrow import Arrow
|
from arrow import Arrow
|
||||||
|
|
||||||
from freqtrade import DependencyException, constants
|
from freqtrade import DependencyException, OperationalException, constants
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.data import history
|
from freqtrade.data import history
|
||||||
from freqtrade.data.btanalysis import evaluate_result_multi
|
from freqtrade.data.btanalysis import evaluate_result_multi
|
||||||
@ -21,7 +22,8 @@ from freqtrade.optimize.backtesting import Backtesting
|
|||||||
from freqtrade.state import RunMode
|
from freqtrade.state import RunMode
|
||||||
from freqtrade.strategy.default_strategy import DefaultStrategy
|
from freqtrade.strategy.default_strategy import DefaultStrategy
|
||||||
from freqtrade.strategy.interface import SellType
|
from freqtrade.strategy.interface import SellType
|
||||||
from freqtrade.tests.conftest import (get_args, log_has, log_has_re, patch_exchange,
|
from freqtrade.tests.conftest import (get_args, log_has, log_has_re,
|
||||||
|
patch_exchange,
|
||||||
patched_configuration_load_config_file)
|
patched_configuration_load_config_file)
|
||||||
|
|
||||||
|
|
||||||
@ -180,21 +182,15 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
|
|||||||
assert 'exchange' in config
|
assert 'exchange' in config
|
||||||
assert 'pair_whitelist' in config['exchange']
|
assert 'pair_whitelist' in config['exchange']
|
||||||
assert 'datadir' in config
|
assert 'datadir' in config
|
||||||
assert log_has(
|
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
||||||
'Using data directory: {} ...'.format(config['datadir']),
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
assert 'ticker_interval' in config
|
assert 'ticker_interval' in config
|
||||||
assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog.record_tuples)
|
assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog)
|
||||||
|
|
||||||
assert 'live' not in config
|
|
||||||
assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
|
||||||
|
|
||||||
assert 'position_stacking' not in config
|
assert 'position_stacking' not in config
|
||||||
assert not log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples)
|
assert not log_has('Parameter --enable-position-stacking detected ...', caplog)
|
||||||
|
|
||||||
assert 'refresh_pairs' not in config
|
assert 'refresh_pairs' not in config
|
||||||
assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog)
|
||||||
|
|
||||||
assert 'timerange' not in config
|
assert 'timerange' not in config
|
||||||
assert 'export' not in config
|
assert 'export' not in config
|
||||||
@ -202,6 +198,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
|
|||||||
assert config['runmode'] == RunMode.BACKTEST
|
assert config['runmode'] == RunMode.BACKTEST
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.filterwarnings("ignore:DEPRECATED")
|
||||||
def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> None:
|
def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> None:
|
||||||
patched_configuration_load_config_file(mocker, default_conf)
|
patched_configuration_load_config_file(mocker, default_conf)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
@ -215,7 +212,6 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) ->
|
|||||||
'--datadir', '/foo/bar',
|
'--datadir', '/foo/bar',
|
||||||
'backtesting',
|
'backtesting',
|
||||||
'--ticker-interval', '1m',
|
'--ticker-interval', '1m',
|
||||||
'--live',
|
|
||||||
'--enable-position-stacking',
|
'--enable-position-stacking',
|
||||||
'--disable-max-market-positions',
|
'--disable-max-market-positions',
|
||||||
'--refresh-pairs-cached',
|
'--refresh-pairs-cached',
|
||||||
@ -233,43 +229,28 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) ->
|
|||||||
assert 'datadir' in config
|
assert 'datadir' in config
|
||||||
assert config['runmode'] == RunMode.BACKTEST
|
assert config['runmode'] == RunMode.BACKTEST
|
||||||
|
|
||||||
assert log_has(
|
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
||||||
'Using data directory: {} ...'.format(config['datadir']),
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
assert 'ticker_interval' in config
|
assert 'ticker_interval' in config
|
||||||
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
||||||
caplog.record_tuples)
|
caplog)
|
||||||
|
|
||||||
assert 'live' in config
|
|
||||||
assert log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
|
||||||
|
|
||||||
assert 'position_stacking' in config
|
assert 'position_stacking' in config
|
||||||
assert log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples)
|
assert log_has('Parameter --enable-position-stacking detected ...', caplog)
|
||||||
|
|
||||||
assert 'use_max_market_positions' in config
|
assert 'use_max_market_positions' in config
|
||||||
assert log_has('Parameter --disable-max-market-positions detected ...', caplog.record_tuples)
|
assert log_has('Parameter --disable-max-market-positions detected ...', caplog)
|
||||||
assert log_has('max_open_trades set to unlimited ...', caplog.record_tuples)
|
assert log_has('max_open_trades set to unlimited ...', caplog)
|
||||||
|
|
||||||
assert 'refresh_pairs' in config
|
assert 'refresh_pairs' in config
|
||||||
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog)
|
||||||
|
|
||||||
assert 'timerange' in config
|
assert 'timerange' in config
|
||||||
assert log_has(
|
assert log_has('Parameter --timerange detected: {} ...'.format(config['timerange']), caplog)
|
||||||
'Parameter --timerange detected: {} ...'.format(config['timerange']),
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
|
|
||||||
assert 'export' in config
|
assert 'export' in config
|
||||||
assert log_has(
|
assert log_has('Parameter --export detected: {} ...'.format(config['export']), caplog)
|
||||||
'Parameter --export detected: {} ...'.format(config['export']),
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
assert 'exportfilename' in config
|
assert 'exportfilename' in config
|
||||||
assert log_has(
|
assert log_has('Storing backtest results to {} ...'.format(config['exportfilename']), caplog)
|
||||||
'Storing backtest results to {} ...'.format(config['exportfilename']),
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog) -> None:
|
def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog) -> None:
|
||||||
@ -301,10 +282,7 @@ def test_start(mocker, fee, default_conf, caplog) -> None:
|
|||||||
]
|
]
|
||||||
args = get_args(args)
|
args = get_args(args)
|
||||||
start_backtesting(args)
|
start_backtesting(args)
|
||||||
assert log_has(
|
assert log_has('Starting freqtrade in Backtesting mode', caplog)
|
||||||
'Starting freqtrade in Backtesting mode',
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
assert start_mock.call_count == 1
|
assert start_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
@ -344,6 +322,23 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None:
|
|||||||
assert not backtesting.strategy.order_types["stoploss_on_exchange"]
|
assert not backtesting.strategy.order_types["stoploss_on_exchange"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_backtesting_init_no_ticker_interval(mocker, default_conf, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Check that stoploss_on_exchange is set to False while backtesting
|
||||||
|
since backtesting assumes a perfect stoploss anyway.
|
||||||
|
"""
|
||||||
|
patch_exchange(mocker)
|
||||||
|
del default_conf['ticker_interval']
|
||||||
|
default_conf['strategy_list'] = ['DefaultStrategy',
|
||||||
|
'TestStrategy']
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5))
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
Backtesting(default_conf)
|
||||||
|
log_has("Ticker-interval needs to be set in either configuration "
|
||||||
|
"or as cli argument `--ticker-interval 5m`", caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_tickerdata_to_dataframe_bt(default_conf, mocker) -> None:
|
def test_tickerdata_to_dataframe_bt(default_conf, mocker) -> None:
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
timerange = TimeRange(None, 'line', 0, -100)
|
timerange = TimeRange(None, 'line', 0, -100)
|
||||||
@ -477,7 +472,6 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
|
|||||||
|
|
||||||
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
||||||
default_conf['ticker_interval'] = '1m'
|
default_conf['ticker_interval'] = '1m'
|
||||||
default_conf['live'] = False
|
|
||||||
default_conf['datadir'] = None
|
default_conf['datadir'] = None
|
||||||
default_conf['export'] = None
|
default_conf['export'] = None
|
||||||
default_conf['timerange'] = '-100'
|
default_conf['timerange'] = '-100'
|
||||||
@ -492,7 +486,7 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
|
|||||||
'up to 2017-11-14T22:59:00+00:00 (0 days)..'
|
'up to 2017-11-14T22:59:00+00:00 (0 days)..'
|
||||||
]
|
]
|
||||||
for line in exists:
|
for line in exists:
|
||||||
assert log_has(line, caplog.record_tuples)
|
assert log_has(line, caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None:
|
def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None:
|
||||||
@ -511,7 +505,6 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None:
|
|||||||
|
|
||||||
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
||||||
default_conf['ticker_interval'] = "1m"
|
default_conf['ticker_interval'] = "1m"
|
||||||
default_conf['live'] = False
|
|
||||||
default_conf['datadir'] = None
|
default_conf['datadir'] = None
|
||||||
default_conf['export'] = None
|
default_conf['export'] = None
|
||||||
default_conf['timerange'] = '20180101-20180102'
|
default_conf['timerange'] = '20180101-20180102'
|
||||||
@ -520,7 +513,7 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None:
|
|||||||
backtesting.start()
|
backtesting.start()
|
||||||
# check the logs, that will contain the backtest result
|
# check the logs, that will contain the backtest result
|
||||||
|
|
||||||
assert log_has('No data found. Terminating.', caplog.record_tuples)
|
assert log_has('No data found. Terminating.', caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_backtest(default_conf, fee, mocker) -> None:
|
def test_backtest(default_conf, fee, mocker) -> None:
|
||||||
@ -617,8 +610,9 @@ def test_processed(default_conf, mocker) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_backtest_pricecontours(default_conf, fee, mocker) -> None:
|
def test_backtest_pricecontours(default_conf, fee, mocker) -> None:
|
||||||
|
# TODO: Evaluate usefullness of this, the patterns and buy-signls are unrealistic
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
tests = [['raise', 19], ['lower', 0], ['sine', 18]]
|
tests = [['raise', 19], ['lower', 0], ['sine', 35]]
|
||||||
# We need to enable sell-signal - otherwise it sells on ROI!!
|
# We need to enable sell-signal - otherwise it sells on ROI!!
|
||||||
default_conf['experimental'] = {"use_sell_signal": True}
|
default_conf['experimental'] = {"use_sell_signal": True}
|
||||||
|
|
||||||
@ -783,10 +777,10 @@ def test_backtest_record(default_conf, fee, mocker):
|
|||||||
# reset test to test with strategy name
|
# reset test to test with strategy name
|
||||||
names = []
|
names = []
|
||||||
records = []
|
records = []
|
||||||
backtesting._store_backtest_result("backtest-result.json", results, "DefStrat")
|
backtesting._store_backtest_result(Path("backtest-result.json"), results, "DefStrat")
|
||||||
assert len(results) == 4
|
assert len(results) == 4
|
||||||
# Assert file_dump_json was only called once
|
# Assert file_dump_json was only called once
|
||||||
assert names == ['backtest-result-DefStrat.json']
|
assert names == [Path('backtest-result-DefStrat.json')]
|
||||||
records = records[0]
|
records = records[0]
|
||||||
# Ensure records are of correct type
|
# Ensure records are of correct type
|
||||||
assert len(records) == 4
|
assert len(records) == 4
|
||||||
@ -812,7 +806,7 @@ def test_backtest_record(default_conf, fee, mocker):
|
|||||||
assert dur > 0
|
assert dur > 0
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_start_live(default_conf, mocker, caplog):
|
def test_backtest_start_timerange(default_conf, mocker, caplog):
|
||||||
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
||||||
|
|
||||||
async def load_pairs(pair, timeframe, since):
|
async def load_pairs(pair, timeframe, since):
|
||||||
@ -832,7 +826,6 @@ def test_backtest_start_live(default_conf, mocker, caplog):
|
|||||||
'--datadir', 'freqtrade/tests/testdata',
|
'--datadir', 'freqtrade/tests/testdata',
|
||||||
'backtesting',
|
'backtesting',
|
||||||
'--ticker-interval', '1m',
|
'--ticker-interval', '1m',
|
||||||
'--live',
|
|
||||||
'--timerange', '-100',
|
'--timerange', '-100',
|
||||||
'--enable-position-stacking',
|
'--enable-position-stacking',
|
||||||
'--disable-max-market-positions'
|
'--disable-max-market-positions'
|
||||||
@ -842,20 +835,18 @@ def test_backtest_start_live(default_conf, mocker, caplog):
|
|||||||
# check the logs, that will contain the backtest result
|
# check the logs, that will contain the backtest result
|
||||||
exists = [
|
exists = [
|
||||||
'Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
'Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
||||||
'Parameter -l/--live detected ...',
|
|
||||||
'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
|
'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
|
||||||
'Parameter --timerange detected: -100 ...',
|
'Parameter --timerange detected: -100 ...',
|
||||||
'Using data directory: freqtrade/tests/testdata ...',
|
'Using data directory: freqtrade/tests/testdata ...',
|
||||||
'Using stake_currency: BTC ...',
|
'Using stake_currency: BTC ...',
|
||||||
'Using stake_amount: 0.001 ...',
|
'Using stake_amount: 0.001 ...',
|
||||||
'Live: Downloading data for all defined pairs ...',
|
'Backtesting with data from 2017-11-14T21:17:00+00:00 '
|
||||||
'Backtesting with data from 2017-11-14T19:31:00+00:00 '
|
|
||||||
'up to 2017-11-14T22:58:00+00:00 (0 days)..',
|
'up to 2017-11-14T22:58:00+00:00 (0 days)..',
|
||||||
'Parameter --enable-position-stacking detected ...'
|
'Parameter --enable-position-stacking detected ...'
|
||||||
]
|
]
|
||||||
|
|
||||||
for line in exists:
|
for line in exists:
|
||||||
assert log_has(line, caplog.record_tuples)
|
assert log_has(line, caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_start_multi_strat(default_conf, mocker, caplog):
|
def test_backtest_start_multi_strat(default_conf, mocker, caplog):
|
||||||
@ -881,7 +872,6 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog):
|
|||||||
'--datadir', 'freqtrade/tests/testdata',
|
'--datadir', 'freqtrade/tests/testdata',
|
||||||
'backtesting',
|
'backtesting',
|
||||||
'--ticker-interval', '1m',
|
'--ticker-interval', '1m',
|
||||||
'--live',
|
|
||||||
'--timerange', '-100',
|
'--timerange', '-100',
|
||||||
'--enable-position-stacking',
|
'--enable-position-stacking',
|
||||||
'--disable-max-market-positions',
|
'--disable-max-market-positions',
|
||||||
@ -899,14 +889,12 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog):
|
|||||||
# check the logs, that will contain the backtest result
|
# check the logs, that will contain the backtest result
|
||||||
exists = [
|
exists = [
|
||||||
'Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
'Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
||||||
'Parameter -l/--live detected ...',
|
|
||||||
'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
|
'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
|
||||||
'Parameter --timerange detected: -100 ...',
|
'Parameter --timerange detected: -100 ...',
|
||||||
'Using data directory: freqtrade/tests/testdata ...',
|
'Using data directory: freqtrade/tests/testdata ...',
|
||||||
'Using stake_currency: BTC ...',
|
'Using stake_currency: BTC ...',
|
||||||
'Using stake_amount: 0.001 ...',
|
'Using stake_amount: 0.001 ...',
|
||||||
'Live: Downloading data for all defined pairs ...',
|
'Backtesting with data from 2017-11-14T21:17:00+00:00 '
|
||||||
'Backtesting with data from 2017-11-14T19:31:00+00:00 '
|
|
||||||
'up to 2017-11-14T22:58:00+00:00 (0 days)..',
|
'up to 2017-11-14T22:58:00+00:00 (0 days)..',
|
||||||
'Parameter --enable-position-stacking detected ...',
|
'Parameter --enable-position-stacking detected ...',
|
||||||
'Running backtesting for Strategy DefaultStrategy',
|
'Running backtesting for Strategy DefaultStrategy',
|
||||||
@ -914,4 +902,4 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog):
|
|||||||
]
|
]
|
||||||
|
|
||||||
for line in exists:
|
for line in exists:
|
||||||
assert log_has(line, caplog.record_tuples)
|
assert log_has(line, caplog)
|
||||||
|
@ -3,11 +3,14 @@
|
|||||||
|
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from freqtrade.edge import PairInfo
|
from freqtrade.edge import PairInfo
|
||||||
from freqtrade.optimize import setup_configuration, start_edge
|
from freqtrade.optimize import setup_configuration, start_edge
|
||||||
from freqtrade.optimize.edge_cli import EdgeCli
|
from freqtrade.optimize.edge_cli import EdgeCli
|
||||||
from freqtrade.state import RunMode
|
from freqtrade.state import RunMode
|
||||||
from freqtrade.tests.conftest import (get_args, log_has, log_has_re, patch_exchange,
|
from freqtrade.tests.conftest import (get_args, log_has, log_has_re,
|
||||||
|
patch_exchange,
|
||||||
patched_configuration_load_config_file)
|
patched_configuration_load_config_file)
|
||||||
|
|
||||||
|
|
||||||
@ -29,20 +32,18 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
|
|||||||
assert 'exchange' in config
|
assert 'exchange' in config
|
||||||
assert 'pair_whitelist' in config['exchange']
|
assert 'pair_whitelist' in config['exchange']
|
||||||
assert 'datadir' in config
|
assert 'datadir' in config
|
||||||
assert log_has(
|
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
||||||
'Using data directory: {} ...'.format(config['datadir']),
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
assert 'ticker_interval' in config
|
assert 'ticker_interval' in config
|
||||||
assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog.record_tuples)
|
assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog)
|
||||||
|
|
||||||
assert 'refresh_pairs' not in config
|
assert 'refresh_pairs' not in config
|
||||||
assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog)
|
||||||
|
|
||||||
assert 'timerange' not in config
|
assert 'timerange' not in config
|
||||||
assert 'stoploss_range' not in config
|
assert 'stoploss_range' not in config
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.filterwarnings("ignore:DEPRECATED")
|
||||||
def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> None:
|
def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> None:
|
||||||
patched_configuration_load_config_file(mocker, edge_conf)
|
patched_configuration_load_config_file(mocker, edge_conf)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
@ -69,21 +70,15 @@ def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> N
|
|||||||
assert 'pair_whitelist' in config['exchange']
|
assert 'pair_whitelist' in config['exchange']
|
||||||
assert 'datadir' in config
|
assert 'datadir' in config
|
||||||
assert config['runmode'] == RunMode.EDGE
|
assert config['runmode'] == RunMode.EDGE
|
||||||
assert log_has(
|
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
||||||
'Using data directory: {} ...'.format(config['datadir']),
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
assert 'ticker_interval' in config
|
assert 'ticker_interval' in config
|
||||||
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
||||||
caplog.record_tuples)
|
caplog)
|
||||||
|
|
||||||
assert 'refresh_pairs' in config
|
assert 'refresh_pairs' in config
|
||||||
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog)
|
||||||
assert 'timerange' in config
|
assert 'timerange' in config
|
||||||
assert log_has(
|
assert log_has('Parameter --timerange detected: {} ...'.format(config['timerange']), caplog)
|
||||||
'Parameter --timerange detected: {} ...'.format(config['timerange']),
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_start(mocker, fee, edge_conf, caplog) -> None:
|
def test_start(mocker, fee, edge_conf, caplog) -> None:
|
||||||
@ -100,10 +95,7 @@ def test_start(mocker, fee, edge_conf, caplog) -> None:
|
|||||||
]
|
]
|
||||||
args = get_args(args)
|
args = get_args(args)
|
||||||
start_edge(args)
|
start_edge(args)
|
||||||
assert log_has(
|
assert log_has('Starting freqtrade in Edge mode', caplog)
|
||||||
'Starting freqtrade in Edge mode',
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
assert start_mock.call_count == 1
|
assert start_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
# pragma pylint: disable=missing-docstring,W0212,C0103
|
# pragma pylint: disable=missing-docstring,W0212,C0103
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock, PropertyMock
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pytest
|
import pytest
|
||||||
from arrow import Arrow
|
from arrow import Arrow
|
||||||
from filelock import Timeout
|
from filelock import Timeout
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from freqtrade import DependencyException, OperationalException
|
from freqtrade import DependencyException, OperationalException
|
||||||
from freqtrade.data.converter import parse_ticker_dataframe
|
from freqtrade.data.converter import parse_ticker_dataframe
|
||||||
@ -14,8 +15,7 @@ from freqtrade.data.history import load_tickerdata_file
|
|||||||
from freqtrade.optimize import setup_configuration, start_hyperopt
|
from freqtrade.optimize import setup_configuration, start_hyperopt
|
||||||
from freqtrade.optimize.default_hyperopt import DefaultHyperOpts
|
from freqtrade.optimize.default_hyperopt import DefaultHyperOpts
|
||||||
from freqtrade.optimize.default_hyperopt_loss import DefaultHyperOptLoss
|
from freqtrade.optimize.default_hyperopt_loss import DefaultHyperOptLoss
|
||||||
from freqtrade.optimize.hyperopt import (HYPEROPT_LOCKFILE, TICKERDATA_PICKLE,
|
from freqtrade.optimize.hyperopt import Hyperopt
|
||||||
Hyperopt)
|
|
||||||
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver, HyperOptLossResolver
|
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver, HyperOptLossResolver
|
||||||
from freqtrade.state import RunMode
|
from freqtrade.state import RunMode
|
||||||
from freqtrade.strategy.interface import SellType
|
from freqtrade.strategy.interface import SellType
|
||||||
@ -26,6 +26,7 @@ from freqtrade.tests.conftest import (get_args, log_has, log_has_re,
|
|||||||
|
|
||||||
@pytest.fixture(scope='function')
|
@pytest.fixture(scope='function')
|
||||||
def hyperopt(default_conf, mocker):
|
def hyperopt(default_conf, mocker):
|
||||||
|
default_conf.update({'spaces': ['all']})
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
return Hyperopt(default_conf)
|
return Hyperopt(default_conf)
|
||||||
|
|
||||||
@ -53,11 +54,14 @@ def create_trials(mocker, hyperopt) -> None:
|
|||||||
- we might have a pickle'd file so make sure that we return
|
- we might have a pickle'd file so make sure that we return
|
||||||
false when looking for it
|
false when looking for it
|
||||||
"""
|
"""
|
||||||
hyperopt.trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
|
hyperopt.trials_file = Path('freqtrade/tests/optimize/ut_trials.pickle')
|
||||||
|
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.os.path.exists', return_value=False)
|
mocker.patch.object(Path, "is_file", MagicMock(return_value=False))
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.os.path.getsize', return_value=1)
|
stat_mock = MagicMock()
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.os.remove', return_value=True)
|
stat_mock.st_size = PropertyMock(return_value=1)
|
||||||
|
mocker.patch.object(Path, "stat", MagicMock(return_value=False))
|
||||||
|
|
||||||
|
mocker.patch.object(Path, "unlink", MagicMock(return_value=True))
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None)
|
mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None)
|
||||||
|
|
||||||
return [{'loss': 1, 'result': 'foo', 'params': {}}]
|
return [{'loss': 1, 'result': 'foo', 'params': {}}]
|
||||||
@ -78,27 +82,22 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca
|
|||||||
assert 'exchange' in config
|
assert 'exchange' in config
|
||||||
assert 'pair_whitelist' in config['exchange']
|
assert 'pair_whitelist' in config['exchange']
|
||||||
assert 'datadir' in config
|
assert 'datadir' in config
|
||||||
assert log_has(
|
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
||||||
'Using data directory: {} ...'.format(config['datadir']),
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
assert 'ticker_interval' in config
|
assert 'ticker_interval' in config
|
||||||
assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog.record_tuples)
|
assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog)
|
||||||
|
|
||||||
assert 'live' not in config
|
|
||||||
assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
|
||||||
|
|
||||||
assert 'position_stacking' not in config
|
assert 'position_stacking' not in config
|
||||||
assert not log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples)
|
assert not log_has('Parameter --enable-position-stacking detected ...', caplog)
|
||||||
|
|
||||||
assert 'refresh_pairs' not in config
|
assert 'refresh_pairs' not in config
|
||||||
assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog)
|
||||||
|
|
||||||
assert 'timerange' not in config
|
assert 'timerange' not in config
|
||||||
assert 'runmode' in config
|
assert 'runmode' in config
|
||||||
assert config['runmode'] == RunMode.HYPEROPT
|
assert config['runmode'] == RunMode.HYPEROPT
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.filterwarnings("ignore:DEPRECATED")
|
||||||
def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplog) -> None:
|
def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplog) -> None:
|
||||||
patched_configuration_load_config_file(mocker, default_conf)
|
patched_configuration_load_config_file(mocker, default_conf)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
@ -129,41 +128,32 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo
|
|||||||
assert 'datadir' in config
|
assert 'datadir' in config
|
||||||
assert config['runmode'] == RunMode.HYPEROPT
|
assert config['runmode'] == RunMode.HYPEROPT
|
||||||
|
|
||||||
assert log_has(
|
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
||||||
'Using data directory: {} ...'.format(config['datadir']),
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
assert 'ticker_interval' in config
|
assert 'ticker_interval' in config
|
||||||
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
||||||
caplog.record_tuples)
|
caplog)
|
||||||
|
|
||||||
assert 'position_stacking' in config
|
assert 'position_stacking' in config
|
||||||
assert log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples)
|
assert log_has('Parameter --enable-position-stacking detected ...', caplog)
|
||||||
|
|
||||||
assert 'use_max_market_positions' in config
|
assert 'use_max_market_positions' in config
|
||||||
assert log_has('Parameter --disable-max-market-positions detected ...', caplog.record_tuples)
|
assert log_has('Parameter --disable-max-market-positions detected ...', caplog)
|
||||||
assert log_has('max_open_trades set to unlimited ...', caplog.record_tuples)
|
assert log_has('max_open_trades set to unlimited ...', caplog)
|
||||||
|
|
||||||
assert 'refresh_pairs' in config
|
assert 'refresh_pairs' in config
|
||||||
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog)
|
||||||
|
|
||||||
assert 'timerange' in config
|
assert 'timerange' in config
|
||||||
assert log_has(
|
assert log_has('Parameter --timerange detected: {} ...'.format(config['timerange']), caplog)
|
||||||
'Parameter --timerange detected: {} ...'.format(config['timerange']),
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
|
|
||||||
assert 'epochs' in config
|
assert 'epochs' in config
|
||||||
assert log_has('Parameter --epochs detected ... Will run Hyperopt with for 1000 epochs ...',
|
assert log_has('Parameter --epochs detected ... Will run Hyperopt with for 1000 epochs ...',
|
||||||
caplog.record_tuples)
|
caplog)
|
||||||
|
|
||||||
assert 'spaces' in config
|
assert 'spaces' in config
|
||||||
assert log_has(
|
assert log_has('Parameter -s/--spaces detected: {}'.format(config['spaces']), caplog)
|
||||||
'Parameter -s/--spaces detected: {}'.format(config['spaces']),
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
assert 'print_all' in config
|
assert 'print_all' in config
|
||||||
assert log_has('Parameter --print-all detected ...', caplog.record_tuples)
|
assert log_has('Parameter --print-all detected ...', caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_hyperoptresolver(mocker, default_conf, caplog) -> None:
|
def test_hyperoptresolver(mocker, default_conf, caplog) -> None:
|
||||||
@ -180,9 +170,9 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None:
|
|||||||
assert not hasattr(x, 'populate_buy_trend')
|
assert not hasattr(x, 'populate_buy_trend')
|
||||||
assert not hasattr(x, 'populate_sell_trend')
|
assert not hasattr(x, 'populate_sell_trend')
|
||||||
assert log_has("Custom Hyperopt does not provide populate_sell_trend. "
|
assert log_has("Custom Hyperopt does not provide populate_sell_trend. "
|
||||||
"Using populate_sell_trend from DefaultStrategy.", caplog.record_tuples)
|
"Using populate_sell_trend from DefaultStrategy.", caplog)
|
||||||
assert log_has("Custom Hyperopt does not provide populate_buy_trend. "
|
assert log_has("Custom Hyperopt does not provide populate_buy_trend. "
|
||||||
"Using populate_buy_trend from DefaultStrategy.", caplog.record_tuples)
|
"Using populate_buy_trend from DefaultStrategy.", caplog)
|
||||||
assert hasattr(x, "ticker_interval")
|
assert hasattr(x, "ticker_interval")
|
||||||
|
|
||||||
|
|
||||||
@ -228,10 +218,7 @@ def test_start(mocker, default_conf, caplog) -> None:
|
|||||||
import pprint
|
import pprint
|
||||||
pprint.pprint(caplog.record_tuples)
|
pprint.pprint(caplog.record_tuples)
|
||||||
|
|
||||||
assert log_has(
|
assert log_has('Starting freqtrade in Hyperopt mode', caplog)
|
||||||
'Starting freqtrade in Hyperopt mode',
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
assert start_mock.call_count == 1
|
assert start_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
@ -256,7 +243,7 @@ def test_start_no_data(mocker, default_conf, caplog) -> None:
|
|||||||
import pprint
|
import pprint
|
||||||
pprint.pprint(caplog.record_tuples)
|
pprint.pprint(caplog.record_tuples)
|
||||||
|
|
||||||
assert log_has('No data found. Terminating.', caplog.record_tuples)
|
assert log_has('No data found. Terminating.', caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_start_failure(mocker, default_conf, caplog) -> None:
|
def test_start_failure(mocker, default_conf, caplog) -> None:
|
||||||
@ -274,14 +261,11 @@ def test_start_failure(mocker, default_conf, caplog) -> None:
|
|||||||
args = get_args(args)
|
args = get_args(args)
|
||||||
with pytest.raises(DependencyException):
|
with pytest.raises(DependencyException):
|
||||||
start_hyperopt(args)
|
start_hyperopt(args)
|
||||||
assert log_has(
|
assert log_has("Please don't use --strategy for hyperopt.", caplog)
|
||||||
"Please don't use --strategy for hyperopt.",
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_start_filelock(mocker, default_conf, caplog) -> None:
|
def test_start_filelock(mocker, default_conf, caplog) -> None:
|
||||||
start_mock = MagicMock(side_effect=Timeout(HYPEROPT_LOCKFILE))
|
start_mock = MagicMock(side_effect=Timeout(Hyperopt.get_lock_filename(default_conf)))
|
||||||
patched_configuration_load_config_file(mocker, default_conf)
|
patched_configuration_load_config_file(mocker, default_conf)
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock)
|
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
@ -293,10 +277,7 @@ def test_start_filelock(mocker, default_conf, caplog) -> None:
|
|||||||
]
|
]
|
||||||
args = get_args(args)
|
args = get_args(args)
|
||||||
start_hyperopt(args)
|
start_hyperopt(args)
|
||||||
assert log_has(
|
assert log_has("Another running instance of freqtrade Hyperopt detected.", caplog)
|
||||||
"Another running instance of freqtrade Hyperopt detected.",
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_loss_calculation_prefer_correct_trade_count(default_conf, hyperopt_results) -> None:
|
def test_loss_calculation_prefer_correct_trade_count(default_conf, hyperopt_results) -> None:
|
||||||
@ -370,13 +351,13 @@ def test_onlyprofit_loss_prefers_higher_profits(default_conf, hyperopt_results)
|
|||||||
|
|
||||||
def test_log_results_if_loss_improves(hyperopt, capsys) -> None:
|
def test_log_results_if_loss_improves(hyperopt, capsys) -> None:
|
||||||
hyperopt.current_best_loss = 2
|
hyperopt.current_best_loss = 2
|
||||||
|
hyperopt.total_epochs = 2
|
||||||
hyperopt.log_results(
|
hyperopt.log_results(
|
||||||
{
|
{
|
||||||
'loss': 1,
|
'loss': 1,
|
||||||
'current_tries': 1,
|
'current_epoch': 1,
|
||||||
'total_tries': 2,
|
'results_explanation': 'foo.',
|
||||||
'result': 'foo.',
|
'is_initial_point': False
|
||||||
'initial_point': False
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
out, err = capsys.readouterr()
|
out, err = capsys.readouterr()
|
||||||
@ -400,10 +381,7 @@ def test_save_trials_saves_trials(mocker, hyperopt, caplog) -> None:
|
|||||||
hyperopt.save_trials()
|
hyperopt.save_trials()
|
||||||
|
|
||||||
trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
|
trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
|
||||||
assert log_has(
|
assert log_has('Saving 1 evaluations to \'{}\''.format(trials_file), caplog)
|
||||||
'Saving 1 evaluations to \'{}\''.format(trials_file),
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
mock_dump.assert_called_once()
|
mock_dump.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
@ -412,10 +390,7 @@ def test_read_trials_returns_trials_file(mocker, hyperopt, caplog) -> None:
|
|||||||
mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=trials)
|
mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=trials)
|
||||||
hyperopt_trial = hyperopt.read_trials()
|
hyperopt_trial = hyperopt.read_trials()
|
||||||
trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
|
trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
|
||||||
assert log_has(
|
assert log_has('Reading Trials from \'{}\''.format(trials_file), caplog)
|
||||||
'Reading Trials from \'{}\''.format(trials_file),
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
assert hyperopt_trial == trials
|
assert hyperopt_trial == trials
|
||||||
mock_load.assert_called_once()
|
mock_load.assert_called_once()
|
||||||
|
|
||||||
@ -433,7 +408,7 @@ def test_roi_table_generation(hyperopt) -> None:
|
|||||||
assert hyperopt.custom_hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0}
|
assert hyperopt.custom_hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0}
|
||||||
|
|
||||||
|
|
||||||
def test_start_calls_optimizer(mocker, default_conf, caplog) -> None:
|
def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None:
|
||||||
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
|
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
@ -443,7 +418,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog) -> None:
|
|||||||
|
|
||||||
parallel = mocker.patch(
|
parallel = mocker.patch(
|
||||||
'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel',
|
'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel',
|
||||||
MagicMock(return_value=[{'loss': 1, 'result': 'foo result', 'params': {}}])
|
MagicMock(return_value=[{'loss': 1, 'results_explanation': 'foo result', 'params': {}}])
|
||||||
)
|
)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
|
|
||||||
@ -454,16 +429,20 @@ def test_start_calls_optimizer(mocker, default_conf, caplog) -> None:
|
|||||||
'hyperopt_jobs': 1, })
|
'hyperopt_jobs': 1, })
|
||||||
|
|
||||||
hyperopt = Hyperopt(default_conf)
|
hyperopt = Hyperopt(default_conf)
|
||||||
hyperopt.strategy.tickerdata_to_dataframe = MagicMock()
|
hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock()
|
||||||
|
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
|
||||||
|
|
||||||
hyperopt.start()
|
hyperopt.start()
|
||||||
|
|
||||||
parallel.assert_called_once()
|
parallel.assert_called_once()
|
||||||
assert log_has('Best result:\nfoo result\nwith values:\n', caplog.record_tuples)
|
|
||||||
|
out, err = capsys.readouterr()
|
||||||
|
assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out
|
||||||
assert dumper.called
|
assert dumper.called
|
||||||
# Should be called twice, once for tickerdata, once to save evaluations
|
# Should be called twice, once for tickerdata, once to save evaluations
|
||||||
assert dumper.call_count == 2
|
assert dumper.call_count == 2
|
||||||
assert hasattr(hyperopt, "advise_sell")
|
assert hasattr(hyperopt.backtesting, "advise_sell")
|
||||||
assert hasattr(hyperopt, "advise_buy")
|
assert hasattr(hyperopt.backtesting, "advise_buy")
|
||||||
assert hasattr(hyperopt, "max_open_trades")
|
assert hasattr(hyperopt, "max_open_trades")
|
||||||
assert hyperopt.max_open_trades == default_conf['max_open_trades']
|
assert hyperopt.max_open_trades == default_conf['max_open_trades']
|
||||||
assert hasattr(hyperopt, "position_stacking")
|
assert hasattr(hyperopt, "position_stacking")
|
||||||
@ -509,7 +488,7 @@ def test_populate_indicators(hyperopt) -> None:
|
|||||||
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
|
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
|
||||||
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', pair="UNITTEST/BTC",
|
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', pair="UNITTEST/BTC",
|
||||||
fill_missing=True)}
|
fill_missing=True)}
|
||||||
dataframes = hyperopt.strategy.tickerdata_to_dataframe(tickerlist)
|
dataframes = hyperopt.backtesting.strategy.tickerdata_to_dataframe(tickerlist)
|
||||||
dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'],
|
dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'],
|
||||||
{'pair': 'UNITTEST/BTC'})
|
{'pair': 'UNITTEST/BTC'})
|
||||||
|
|
||||||
@ -523,7 +502,7 @@ def test_buy_strategy_generator(hyperopt) -> None:
|
|||||||
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
|
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
|
||||||
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', pair="UNITTEST/BTC",
|
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', pair="UNITTEST/BTC",
|
||||||
fill_missing=True)}
|
fill_missing=True)}
|
||||||
dataframes = hyperopt.strategy.tickerdata_to_dataframe(tickerlist)
|
dataframes = hyperopt.backtesting.strategy.tickerdata_to_dataframe(tickerlist)
|
||||||
dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'],
|
dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'],
|
||||||
{'pair': 'UNITTEST/BTC'})
|
{'pair': 'UNITTEST/BTC'})
|
||||||
|
|
||||||
@ -559,7 +538,7 @@ def test_generate_optimizer(mocker, default_conf) -> None:
|
|||||||
backtest_result = pd.DataFrame.from_records(trades, columns=labels)
|
backtest_result = pd.DataFrame.from_records(trades, columns=labels)
|
||||||
|
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
'freqtrade.optimize.hyperopt.Hyperopt.backtest',
|
'freqtrade.optimize.hyperopt.Backtesting.backtest',
|
||||||
MagicMock(return_value=backtest_result)
|
MagicMock(return_value=backtest_result)
|
||||||
)
|
)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
@ -598,9 +577,10 @@ def test_generate_optimizer(mocker, default_conf) -> None:
|
|||||||
}
|
}
|
||||||
response_expected = {
|
response_expected = {
|
||||||
'loss': 1.9840569076926293,
|
'loss': 1.9840569076926293,
|
||||||
'result': ' 1 trades. Avg profit 2.31%. Total profit 0.00023300 BTC '
|
'results_explanation': ' 1 trades. Avg profit 2.31%. Total profit 0.00023300 BTC '
|
||||||
'( 2.31Σ%). Avg duration 100.0 mins.',
|
'( 2.31Σ%). Avg duration 100.0 mins.',
|
||||||
'params': optimizer_param
|
'params': optimizer_param,
|
||||||
|
'total_profit': 0.00023300
|
||||||
}
|
}
|
||||||
|
|
||||||
hyperopt = Hyperopt(default_conf)
|
hyperopt = Hyperopt(default_conf)
|
||||||
@ -618,10 +598,10 @@ def test_clean_hyperopt(mocker, default_conf, caplog):
|
|||||||
})
|
})
|
||||||
mocker.patch("freqtrade.optimize.hyperopt.Path.is_file", MagicMock(return_value=True))
|
mocker.patch("freqtrade.optimize.hyperopt.Path.is_file", MagicMock(return_value=True))
|
||||||
unlinkmock = mocker.patch("freqtrade.optimize.hyperopt.Path.unlink", MagicMock())
|
unlinkmock = mocker.patch("freqtrade.optimize.hyperopt.Path.unlink", MagicMock())
|
||||||
Hyperopt(default_conf)
|
h = Hyperopt(default_conf)
|
||||||
|
|
||||||
assert unlinkmock.call_count == 2
|
assert unlinkmock.call_count == 2
|
||||||
assert log_has(f"Removing `{TICKERDATA_PICKLE}`.", caplog.record_tuples)
|
assert log_has(f"Removing `{h.tickerdata_pickle}`.", caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_continue_hyperopt(mocker, default_conf, caplog):
|
def test_continue_hyperopt(mocker, default_conf, caplog):
|
||||||
@ -638,4 +618,78 @@ def test_continue_hyperopt(mocker, default_conf, caplog):
|
|||||||
Hyperopt(default_conf)
|
Hyperopt(default_conf)
|
||||||
|
|
||||||
assert unlinkmock.call_count == 0
|
assert unlinkmock.call_count == 0
|
||||||
assert log_has(f"Continuing on previous hyperopt results.", caplog.record_tuples)
|
assert log_has(f"Continuing on previous hyperopt results.", caplog)
|
||||||
|
|
||||||
|
|
||||||
|
def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None:
|
||||||
|
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.optimize.hyperopt.get_timeframe',
|
||||||
|
MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13)))
|
||||||
|
)
|
||||||
|
|
||||||
|
parallel = mocker.patch(
|
||||||
|
'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel',
|
||||||
|
MagicMock(return_value=[{'loss': 1, 'results_explanation': 'foo result', 'params': {}}])
|
||||||
|
)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
|
||||||
|
default_conf.update({'config': 'config.json.example',
|
||||||
|
'epochs': 1,
|
||||||
|
'timerange': None,
|
||||||
|
'spaces': 'all',
|
||||||
|
'hyperopt_jobs': 1,
|
||||||
|
'print_json': True,
|
||||||
|
})
|
||||||
|
|
||||||
|
hyperopt = Hyperopt(default_conf)
|
||||||
|
hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock()
|
||||||
|
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
|
||||||
|
|
||||||
|
hyperopt.start()
|
||||||
|
|
||||||
|
parallel.assert_called_once()
|
||||||
|
|
||||||
|
out, err = capsys.readouterr()
|
||||||
|
assert '{"params":{"mfi-value":null,"fastd-value":null,"adx-value":null,"rsi-value":null,"mfi-enabled":null,"fastd-enabled":null,"adx-enabled":null,"rsi-enabled":null,"trigger":null,"sell-mfi-value":null,"sell-fastd-value":null,"sell-adx-value":null,"sell-rsi-value":null,"sell-mfi-enabled":null,"sell-fastd-enabled":null,"sell-adx-enabled":null,"sell-rsi-enabled":null,"sell-trigger":null},"minimal_roi":{},"stoploss":null}' in out # noqa: E501
|
||||||
|
assert dumper.called
|
||||||
|
# Should be called twice, once for tickerdata, once to save evaluations
|
||||||
|
assert dumper.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_print_json_spaces_roi_stoploss(mocker, default_conf, caplog, capsys) -> None:
|
||||||
|
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.optimize.hyperopt.get_timeframe',
|
||||||
|
MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13)))
|
||||||
|
)
|
||||||
|
|
||||||
|
parallel = mocker.patch(
|
||||||
|
'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel',
|
||||||
|
MagicMock(return_value=[{'loss': 1, 'results_explanation': 'foo result', 'params': {}}])
|
||||||
|
)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
|
||||||
|
default_conf.update({'config': 'config.json.example',
|
||||||
|
'epochs': 1,
|
||||||
|
'timerange': None,
|
||||||
|
'spaces': 'roi stoploss',
|
||||||
|
'hyperopt_jobs': 1,
|
||||||
|
'print_json': True,
|
||||||
|
})
|
||||||
|
|
||||||
|
hyperopt = Hyperopt(default_conf)
|
||||||
|
hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock()
|
||||||
|
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
|
||||||
|
|
||||||
|
hyperopt.start()
|
||||||
|
|
||||||
|
parallel.assert_called_once()
|
||||||
|
|
||||||
|
out, err = capsys.readouterr()
|
||||||
|
assert '{"minimal_roi":{},"stoploss":null}' in out
|
||||||
|
assert dumper.called
|
||||||
|
# Should be called twice, once for tickerdata, once to save evaluations
|
||||||
|
assert dumper.call_count == 2
|
||||||
|
@ -91,7 +91,7 @@ def test_fiat_convert_unsupported_crypto(mocker, caplog):
|
|||||||
mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._cryptomap', return_value=[])
|
mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._cryptomap', return_value=[])
|
||||||
fiat_convert = CryptoToFiatConverter()
|
fiat_convert = CryptoToFiatConverter()
|
||||||
assert fiat_convert._find_price(crypto_symbol='CRYPTO_123', fiat_symbol='EUR') == 0.0
|
assert fiat_convert._find_price(crypto_symbol='CRYPTO_123', fiat_symbol='EUR') == 0.0
|
||||||
assert log_has('unsupported crypto-symbol CRYPTO_123 - returning 0.0', caplog.record_tuples)
|
assert log_has('unsupported crypto-symbol CRYPTO_123 - returning 0.0', caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_fiat_convert_get_price(mocker):
|
def test_fiat_convert_get_price(mocker):
|
||||||
@ -190,7 +190,7 @@ def test_fiat_invalid_response(mocker, caplog):
|
|||||||
length_cryptomap = len(fiat_convert._cryptomap)
|
length_cryptomap = len(fiat_convert._cryptomap)
|
||||||
assert length_cryptomap == 0
|
assert length_cryptomap == 0
|
||||||
assert log_has('Could not load FIAT Cryptocurrency map for the following problem: TypeError',
|
assert log_has('Could not load FIAT Cryptocurrency map for the following problem: TypeError',
|
||||||
caplog.record_tuples)
|
caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_convert_amount(mocker):
|
def test_convert_amount(mocker):
|
||||||
|
@ -44,7 +44,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None:
|
|||||||
with pytest.raises(RPCException, match=r'.*no active trade*'):
|
with pytest.raises(RPCException, match=r'.*no active trade*'):
|
||||||
rpc._rpc_trade_status()
|
rpc._rpc_trade_status()
|
||||||
|
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trades()
|
||||||
results = rpc._rpc_trade_status()
|
results = rpc._rpc_trade_status()
|
||||||
assert {
|
assert {
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
@ -116,7 +116,7 @@ def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None:
|
|||||||
with pytest.raises(RPCException, match=r'.*no active order*'):
|
with pytest.raises(RPCException, match=r'.*no active order*'):
|
||||||
rpc._rpc_status_table()
|
rpc._rpc_status_table()
|
||||||
|
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trades()
|
||||||
result = rpc._rpc_status_table()
|
result = rpc._rpc_status_table()
|
||||||
assert 'instantly' in result['Since'].all()
|
assert 'instantly' in result['Since'].all()
|
||||||
assert 'ETH/BTC' in result['Pair'].all()
|
assert 'ETH/BTC' in result['Pair'].all()
|
||||||
@ -151,7 +151,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
|
|||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
rpc._fiat_converter = CryptoToFiatConverter()
|
rpc._fiat_converter = CryptoToFiatConverter()
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trades()
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
assert trade
|
assert trade
|
||||||
|
|
||||||
@ -208,7 +208,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
|||||||
rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trades()
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
# Simulate fulfilled LIMIT_BUY order for trade
|
# Simulate fulfilled LIMIT_BUY order for trade
|
||||||
trade.update(limit_buy_order)
|
trade.update(limit_buy_order)
|
||||||
@ -222,7 +222,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
|||||||
trade.close_date = datetime.utcnow()
|
trade.close_date = datetime.utcnow()
|
||||||
trade.is_open = False
|
trade.is_open = False
|
||||||
|
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trades()
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
# Simulate fulfilled LIMIT_BUY order for trade
|
# Simulate fulfilled LIMIT_BUY order for trade
|
||||||
trade.update(limit_buy_order)
|
trade.update(limit_buy_order)
|
||||||
@ -292,7 +292,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets,
|
|||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trades()
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
# Simulate fulfilled LIMIT_BUY order for trade
|
# Simulate fulfilled LIMIT_BUY order for trade
|
||||||
trade.update(limit_buy_order)
|
trade.update(limit_buy_order)
|
||||||
@ -536,7 +536,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
|
|||||||
msg = rpc._rpc_forcesell('all')
|
msg = rpc._rpc_forcesell('all')
|
||||||
assert msg == {'result': 'Created sell orders for all open trades.'}
|
assert msg == {'result': 'Created sell orders for all open trades.'}
|
||||||
|
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trades()
|
||||||
msg = rpc._rpc_forcesell('all')
|
msg = rpc._rpc_forcesell('all')
|
||||||
assert msg == {'result': 'Created sell orders for all open trades.'}
|
assert msg == {'result': 'Created sell orders for all open trades.'}
|
||||||
|
|
||||||
@ -570,7 +570,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
|
|||||||
assert cancel_order_mock.call_count == 1
|
assert cancel_order_mock.call_count == 1
|
||||||
assert trade.amount == filled_amount
|
assert trade.amount == filled_amount
|
||||||
|
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trades()
|
||||||
trade = Trade.query.filter(Trade.id == '2').first()
|
trade = Trade.query.filter(Trade.id == '2').first()
|
||||||
amount = trade.amount
|
amount = trade.amount
|
||||||
# make an limit-buy open trade, if there is no 'filled', don't sell it
|
# make an limit-buy open trade, if there is no 'filled', don't sell it
|
||||||
@ -589,7 +589,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
|
|||||||
assert cancel_order_mock.call_count == 2
|
assert cancel_order_mock.call_count == 2
|
||||||
assert trade.amount == amount
|
assert trade.amount == amount
|
||||||
|
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trades()
|
||||||
# make an limit-sell open trade
|
# make an limit-sell open trade
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
'freqtrade.exchange.Exchange.get_order',
|
'freqtrade.exchange.Exchange.get_order',
|
||||||
@ -622,7 +622,7 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
|||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trades()
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
assert trade
|
assert trade
|
||||||
|
|
||||||
@ -660,7 +660,7 @@ def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None:
|
|||||||
assert counts["current"] == 0
|
assert counts["current"] == 0
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trades()
|
||||||
counts = rpc._rpc_count()
|
counts = rpc._rpc_count()
|
||||||
assert counts["current"] == 1
|
assert counts["current"] == 1
|
||||||
|
|
||||||
|
@ -148,8 +148,8 @@ def test_api_run(default_conf, mocker, caplog):
|
|||||||
assert isinstance(server_mock.call_args_list[0][0][2], Flask)
|
assert isinstance(server_mock.call_args_list[0][0][2], Flask)
|
||||||
assert hasattr(apiserver, "srv")
|
assert hasattr(apiserver, "srv")
|
||||||
|
|
||||||
assert log_has("Starting HTTP Server at 127.0.0.1:8080", caplog.record_tuples)
|
assert log_has("Starting HTTP Server at 127.0.0.1:8080", caplog)
|
||||||
assert log_has("Starting Local Rest Server.", caplog.record_tuples)
|
assert log_has("Starting Local Rest Server.", caplog)
|
||||||
|
|
||||||
# Test binding to public
|
# Test binding to public
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
@ -165,22 +165,20 @@ def test_api_run(default_conf, mocker, caplog):
|
|||||||
assert server_mock.call_args_list[0][0][0] == "0.0.0.0"
|
assert server_mock.call_args_list[0][0][0] == "0.0.0.0"
|
||||||
assert server_mock.call_args_list[0][0][1] == "8089"
|
assert server_mock.call_args_list[0][0][1] == "8089"
|
||||||
assert isinstance(server_mock.call_args_list[0][0][2], Flask)
|
assert isinstance(server_mock.call_args_list[0][0][2], Flask)
|
||||||
assert log_has("Starting HTTP Server at 0.0.0.0:8089", caplog.record_tuples)
|
assert log_has("Starting HTTP Server at 0.0.0.0:8089", caplog)
|
||||||
assert log_has("Starting Local Rest Server.", caplog.record_tuples)
|
assert log_has("Starting Local Rest Server.", caplog)
|
||||||
assert log_has("SECURITY WARNING - Local Rest Server listening to external connections",
|
assert log_has("SECURITY WARNING - Local Rest Server listening to external connections",
|
||||||
caplog.record_tuples)
|
caplog)
|
||||||
assert log_has("SECURITY WARNING - This is insecure please set to your loopback,"
|
assert log_has("SECURITY WARNING - This is insecure please set to your loopback,"
|
||||||
"e.g 127.0.0.1 in config.json",
|
"e.g 127.0.0.1 in config.json", caplog)
|
||||||
caplog.record_tuples)
|
|
||||||
assert log_has("SECURITY WARNING - No password for local REST Server defined. "
|
assert log_has("SECURITY WARNING - No password for local REST Server defined. "
|
||||||
"Please make sure that this is intentional!",
|
"Please make sure that this is intentional!", caplog)
|
||||||
caplog.record_tuples)
|
|
||||||
|
|
||||||
# Test crashing flask
|
# Test crashing flask
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
mocker.patch('freqtrade.rpc.api_server.make_server', MagicMock(side_effect=Exception))
|
mocker.patch('freqtrade.rpc.api_server.make_server', MagicMock(side_effect=Exception))
|
||||||
apiserver.run()
|
apiserver.run()
|
||||||
assert log_has("Api server failed to start.", caplog.record_tuples)
|
assert log_has("Api server failed to start.", caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_api_cleanup(default_conf, mocker, caplog):
|
def test_api_cleanup(default_conf, mocker, caplog):
|
||||||
@ -199,7 +197,7 @@ def test_api_cleanup(default_conf, mocker, caplog):
|
|||||||
|
|
||||||
apiserver.cleanup()
|
apiserver.cleanup()
|
||||||
assert stop_mock.shutdown.call_count == 1
|
assert stop_mock.shutdown.call_count == 1
|
||||||
assert log_has("Stopping API Server", caplog.record_tuples)
|
assert log_has("Stopping API Server", caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_api_reloadconf(botclient):
|
def test_api_reloadconf(botclient):
|
||||||
@ -277,7 +275,7 @@ def test_api_count(botclient, mocker, ticker, fee, markets):
|
|||||||
assert rc.json["max"] == 1.0
|
assert rc.json["max"] == 1.0
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
ftbot.create_trade()
|
ftbot.create_trades()
|
||||||
rc = client_get(client, f"{BASE_URI}/count")
|
rc = client_get(client, f"{BASE_URI}/count")
|
||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
assert rc.json["current"] == 1.0
|
assert rc.json["current"] == 1.0
|
||||||
@ -331,7 +329,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li
|
|||||||
assert len(rc.json) == 1
|
assert len(rc.json) == 1
|
||||||
assert rc.json == {"error": "Error querying _profit: no closed trade"}
|
assert rc.json == {"error": "Error querying _profit: no closed trade"}
|
||||||
|
|
||||||
ftbot.create_trade()
|
ftbot.create_trades()
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_BUY order for trade
|
# Simulate fulfilled LIMIT_BUY order for trade
|
||||||
@ -420,7 +418,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
|
|||||||
assert_response(rc, 502)
|
assert_response(rc, 502)
|
||||||
assert rc.json == {'error': 'Error querying _status: no active trade'}
|
assert rc.json == {'error': 'Error querying _status: no active trade'}
|
||||||
|
|
||||||
ftbot.create_trade()
|
ftbot.create_trades()
|
||||||
rc = client_get(client, f"{BASE_URI}/status")
|
rc = client_get(client, f"{BASE_URI}/status")
|
||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
assert len(rc.json) == 1
|
assert len(rc.json) == 1
|
||||||
@ -550,7 +548,7 @@ def test_api_forcesell(botclient, mocker, ticker, fee, markets):
|
|||||||
assert_response(rc, 502)
|
assert_response(rc, 502)
|
||||||
assert rc.json == {"error": "Error querying _forcesell: invalid argument"}
|
assert rc.json == {"error": "Error querying _forcesell: invalid argument"}
|
||||||
|
|
||||||
ftbot.create_trade()
|
ftbot.create_trades()
|
||||||
|
|
||||||
rc = client_post(client, f"{BASE_URI}/forcesell",
|
rc = client_post(client, f"{BASE_URI}/forcesell",
|
||||||
data='{"tradeid": "1"}')
|
data='{"tradeid": "1"}')
|
||||||
|
@ -19,7 +19,7 @@ def test_init_telegram_disabled(mocker, default_conf, caplog) -> None:
|
|||||||
default_conf['telegram']['enabled'] = False
|
default_conf['telegram']['enabled'] = False
|
||||||
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
||||||
|
|
||||||
assert not log_has('Enabling rpc.telegram ...', caplog.record_tuples)
|
assert not log_has('Enabling rpc.telegram ...', caplog)
|
||||||
assert rpc_manager.registered_modules == []
|
assert rpc_manager.registered_modules == []
|
||||||
|
|
||||||
|
|
||||||
@ -28,7 +28,7 @@ def test_init_telegram_enabled(mocker, default_conf, caplog) -> None:
|
|||||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||||
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
||||||
|
|
||||||
assert log_has('Enabling rpc.telegram ...', caplog.record_tuples)
|
assert log_has('Enabling rpc.telegram ...', caplog)
|
||||||
len_modules = len(rpc_manager.registered_modules)
|
len_modules = len(rpc_manager.registered_modules)
|
||||||
assert len_modules == 1
|
assert len_modules == 1
|
||||||
assert 'telegram' in [mod.name for mod in rpc_manager.registered_modules]
|
assert 'telegram' in [mod.name for mod in rpc_manager.registered_modules]
|
||||||
@ -43,7 +43,7 @@ def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None:
|
|||||||
rpc_manager = RPCManager(freqtradebot)
|
rpc_manager = RPCManager(freqtradebot)
|
||||||
rpc_manager.cleanup()
|
rpc_manager.cleanup()
|
||||||
|
|
||||||
assert not log_has('Cleaning up rpc.telegram ...', caplog.record_tuples)
|
assert not log_has('Cleaning up rpc.telegram ...', caplog)
|
||||||
assert telegram_mock.call_count == 0
|
assert telegram_mock.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
@ -59,7 +59,7 @@ def test_cleanup_telegram_enabled(mocker, default_conf, caplog) -> None:
|
|||||||
assert 'telegram' in [mod.name for mod in rpc_manager.registered_modules]
|
assert 'telegram' in [mod.name for mod in rpc_manager.registered_modules]
|
||||||
|
|
||||||
rpc_manager.cleanup()
|
rpc_manager.cleanup()
|
||||||
assert log_has('Cleaning up rpc.telegram ...', caplog.record_tuples)
|
assert log_has('Cleaning up rpc.telegram ...', caplog)
|
||||||
assert 'telegram' not in [mod.name for mod in rpc_manager.registered_modules]
|
assert 'telegram' not in [mod.name for mod in rpc_manager.registered_modules]
|
||||||
assert telegram_mock.call_count == 1
|
assert telegram_mock.call_count == 1
|
||||||
|
|
||||||
@ -75,7 +75,7 @@ def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None:
|
|||||||
'status': 'test'
|
'status': 'test'
|
||||||
})
|
})
|
||||||
|
|
||||||
assert log_has("Sending rpc message: {'type': status, 'status': 'test'}", caplog.record_tuples)
|
assert log_has("Sending rpc message: {'type': status, 'status': 'test'}", caplog)
|
||||||
assert telegram_mock.call_count == 0
|
assert telegram_mock.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
@ -90,7 +90,7 @@ def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None:
|
|||||||
'status': 'test'
|
'status': 'test'
|
||||||
})
|
})
|
||||||
|
|
||||||
assert log_has("Sending rpc message: {'type': status, 'status': 'test'}", caplog.record_tuples)
|
assert log_has("Sending rpc message: {'type': status, 'status': 'test'}", caplog)
|
||||||
assert telegram_mock.call_count == 1
|
assert telegram_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
@ -100,7 +100,7 @@ def test_init_webhook_disabled(mocker, default_conf, caplog) -> None:
|
|||||||
default_conf['webhook'] = {'enabled': False}
|
default_conf['webhook'] = {'enabled': False}
|
||||||
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
||||||
|
|
||||||
assert not log_has('Enabling rpc.webhook ...', caplog.record_tuples)
|
assert not log_has('Enabling rpc.webhook ...', caplog)
|
||||||
assert rpc_manager.registered_modules == []
|
assert rpc_manager.registered_modules == []
|
||||||
|
|
||||||
|
|
||||||
@ -110,7 +110,7 @@ def test_init_webhook_enabled(mocker, default_conf, caplog) -> None:
|
|||||||
default_conf['webhook'] = {'enabled': True, 'url': "https://DEADBEEF.com"}
|
default_conf['webhook'] = {'enabled': True, 'url': "https://DEADBEEF.com"}
|
||||||
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
||||||
|
|
||||||
assert log_has('Enabling rpc.webhook ...', caplog.record_tuples)
|
assert log_has('Enabling rpc.webhook ...', caplog)
|
||||||
assert len(rpc_manager.registered_modules) == 1
|
assert len(rpc_manager.registered_modules) == 1
|
||||||
assert 'webhook' in [mod.name for mod in rpc_manager.registered_modules]
|
assert 'webhook' in [mod.name for mod in rpc_manager.registered_modules]
|
||||||
|
|
||||||
@ -144,7 +144,7 @@ def test_init_apiserver_disabled(mocker, default_conf, caplog) -> None:
|
|||||||
default_conf['telegram']['enabled'] = False
|
default_conf['telegram']['enabled'] = False
|
||||||
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
||||||
|
|
||||||
assert not log_has('Enabling rpc.api_server', caplog.record_tuples)
|
assert not log_has('Enabling rpc.api_server', caplog)
|
||||||
assert rpc_manager.registered_modules == []
|
assert rpc_manager.registered_modules == []
|
||||||
assert run_mock.call_count == 0
|
assert run_mock.call_count == 0
|
||||||
|
|
||||||
@ -160,7 +160,7 @@ def test_init_apiserver_enabled(mocker, default_conf, caplog) -> None:
|
|||||||
"listen_port": "8080"}
|
"listen_port": "8080"}
|
||||||
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
||||||
|
|
||||||
assert log_has('Enabling rpc.api_server', caplog.record_tuples)
|
assert log_has('Enabling rpc.api_server', caplog)
|
||||||
assert len(rpc_manager.registered_modules) == 1
|
assert len(rpc_manager.registered_modules) == 1
|
||||||
assert 'apiserver' in [mod.name for mod in rpc_manager.registered_modules]
|
assert 'apiserver' in [mod.name for mod in rpc_manager.registered_modules]
|
||||||
assert run_mock.call_count == 1
|
assert run_mock.call_count == 1
|
||||||
|
@ -76,7 +76,7 @@ def test_init(default_conf, mocker, caplog) -> None:
|
|||||||
"['performance'], ['daily'], ['count'], ['reload_conf'], " \
|
"['performance'], ['daily'], ['count'], ['reload_conf'], " \
|
||||||
"['stopbuy'], ['whitelist'], ['blacklist'], ['edge'], ['help'], ['version']]"
|
"['stopbuy'], ['whitelist'], ['blacklist'], ['edge'], ['help'], ['version']]"
|
||||||
|
|
||||||
assert log_has(message_str, caplog.record_tuples)
|
assert log_has(message_str, caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_cleanup(default_conf, mocker) -> None:
|
def test_cleanup(default_conf, mocker) -> None:
|
||||||
@ -102,18 +102,9 @@ def test_authorized_only(default_conf, mocker, caplog) -> None:
|
|||||||
dummy = DummyCls(bot)
|
dummy = DummyCls(bot)
|
||||||
dummy.dummy_handler(bot=MagicMock(), update=update)
|
dummy.dummy_handler(bot=MagicMock(), update=update)
|
||||||
assert dummy.state['called'] is True
|
assert dummy.state['called'] is True
|
||||||
assert log_has(
|
assert log_has('Executing handler: dummy_handler for chat_id: 0', caplog)
|
||||||
'Executing handler: dummy_handler for chat_id: 0',
|
assert not log_has('Rejected unauthorized message from: 0', caplog)
|
||||||
caplog.record_tuples
|
assert not log_has('Exception occurred within Telegram module', caplog)
|
||||||
)
|
|
||||||
assert not log_has(
|
|
||||||
'Rejected unauthorized message from: 0',
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
assert not log_has(
|
|
||||||
'Exception occurred within Telegram module',
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None:
|
def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None:
|
||||||
@ -128,18 +119,9 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None:
|
|||||||
dummy = DummyCls(bot)
|
dummy = DummyCls(bot)
|
||||||
dummy.dummy_handler(bot=MagicMock(), update=update)
|
dummy.dummy_handler(bot=MagicMock(), update=update)
|
||||||
assert dummy.state['called'] is False
|
assert dummy.state['called'] is False
|
||||||
assert not log_has(
|
assert not log_has('Executing handler: dummy_handler for chat_id: 3735928559', caplog)
|
||||||
'Executing handler: dummy_handler for chat_id: 3735928559',
|
assert log_has('Rejected unauthorized message from: 3735928559', caplog)
|
||||||
caplog.record_tuples
|
assert not log_has('Exception occurred within Telegram module', caplog)
|
||||||
)
|
|
||||||
assert log_has(
|
|
||||||
'Rejected unauthorized message from: 3735928559',
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
assert not log_has(
|
|
||||||
'Exception occurred within Telegram module',
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_authorized_only_exception(default_conf, mocker, caplog) -> None:
|
def test_authorized_only_exception(default_conf, mocker, caplog) -> None:
|
||||||
@ -156,18 +138,9 @@ def test_authorized_only_exception(default_conf, mocker, caplog) -> None:
|
|||||||
|
|
||||||
dummy.dummy_exception(bot=MagicMock(), update=update)
|
dummy.dummy_exception(bot=MagicMock(), update=update)
|
||||||
assert dummy.state['called'] is False
|
assert dummy.state['called'] is False
|
||||||
assert not log_has(
|
assert not log_has('Executing handler: dummy_handler for chat_id: 0', caplog)
|
||||||
'Executing handler: dummy_handler for chat_id: 0',
|
assert not log_has('Rejected unauthorized message from: 0', caplog)
|
||||||
caplog.record_tuples
|
assert log_has('Exception occurred within Telegram module', caplog)
|
||||||
)
|
|
||||||
assert not log_has(
|
|
||||||
'Rejected unauthorized message from: 0',
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
assert log_has(
|
|
||||||
'Exception occurred within Telegram module',
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
|
def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
|
||||||
@ -219,7 +192,7 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
|
|||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
for _ in range(3):
|
for _ in range(3):
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trades()
|
||||||
|
|
||||||
telegram._status(bot=MagicMock(), update=update)
|
telegram._status(bot=MagicMock(), update=update)
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
@ -267,7 +240,7 @@ def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> No
|
|||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trades()
|
||||||
# Trigger status while we have a fulfilled order for the open trade
|
# Trigger status while we have a fulfilled order for the open trade
|
||||||
telegram._status(bot=MagicMock(), update=update)
|
telegram._status(bot=MagicMock(), update=update)
|
||||||
|
|
||||||
@ -319,7 +292,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker)
|
|||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trades()
|
||||||
|
|
||||||
telegram._status_table(bot=MagicMock(), update=update)
|
telegram._status_table(bot=MagicMock(), update=update)
|
||||||
|
|
||||||
@ -335,6 +308,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker)
|
|||||||
def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
|
def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||||
limit_sell_order, markets, mocker) -> None:
|
limit_sell_order, markets, mocker) -> None:
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
|
default_conf['max_open_trades'] = 1
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price',
|
'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price',
|
||||||
return_value=15000.0
|
return_value=15000.0
|
||||||
@ -358,7 +332,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
|
|||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trades()
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
assert trade
|
assert trade
|
||||||
|
|
||||||
@ -384,9 +358,9 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
|
|||||||
|
|
||||||
# Reset msg_mock
|
# Reset msg_mock
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
|
freqtradebot.config['max_open_trades'] = 2
|
||||||
# Add two other trades
|
# Add two other trades
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trades()
|
||||||
freqtradebot.create_trade()
|
|
||||||
|
|
||||||
trades = Trade.query.all()
|
trades = Trade.query.all()
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
@ -465,7 +439,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
|||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trades()
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_BUY order for trade
|
# Simulate fulfilled LIMIT_BUY order for trade
|
||||||
@ -760,7 +734,7 @@ def test_forcesell_handle(default_conf, update, ticker, fee,
|
|||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trades()
|
||||||
|
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
assert trade
|
assert trade
|
||||||
@ -811,7 +785,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee,
|
|||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trades()
|
||||||
|
|
||||||
# Decrease the price and sell it
|
# Decrease the price and sell it
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -859,14 +833,13 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker
|
|||||||
markets=PropertyMock(return_value=markets),
|
markets=PropertyMock(return_value=markets),
|
||||||
validate_pairs=MagicMock(return_value={})
|
validate_pairs=MagicMock(return_value={})
|
||||||
)
|
)
|
||||||
|
default_conf['max_open_trades'] = 4
|
||||||
freqtradebot = FreqtradeBot(default_conf)
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
patch_get_signal(freqtradebot, (True, False))
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
for _ in range(4):
|
freqtradebot.create_trades()
|
||||||
freqtradebot.create_trade()
|
|
||||||
rpc_mock.reset_mock()
|
rpc_mock.reset_mock()
|
||||||
|
|
||||||
update.message.text = '/forcesell all'
|
update.message.text = '/forcesell all'
|
||||||
@ -1010,7 +983,7 @@ def test_performance_handle(default_conf, update, ticker, fee,
|
|||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trades()
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
assert trade
|
assert trade
|
||||||
|
|
||||||
@ -1055,7 +1028,7 @@ def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> Non
|
|||||||
freqtradebot.state = State.RUNNING
|
freqtradebot.state = State.RUNNING
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trades()
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
telegram._count(bot=MagicMock(), update=update)
|
telegram._count(bot=MagicMock(), update=update)
|
||||||
|
|
||||||
@ -1440,7 +1413,4 @@ def test__send_msg_network_error(default_conf, mocker, caplog) -> None:
|
|||||||
|
|
||||||
# Bot should've tried to send it twice
|
# Bot should've tried to send it twice
|
||||||
assert len(bot.method_calls) == 2
|
assert len(bot.method_calls) == 2
|
||||||
assert log_has(
|
assert log_has('Telegram NetworkError: Oh snap! Trying one more time.', caplog)
|
||||||
'Telegram NetworkError: Oh snap! Trying one more time.',
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
|
@ -115,7 +115,7 @@ def test_exception_send_msg(default_conf, mocker, caplog):
|
|||||||
webhook = Webhook(get_patched_freqtradebot(mocker, default_conf))
|
webhook = Webhook(get_patched_freqtradebot(mocker, default_conf))
|
||||||
webhook.send_msg({'type': RPCMessageType.BUY_NOTIFICATION})
|
webhook.send_msg({'type': RPCMessageType.BUY_NOTIFICATION})
|
||||||
assert log_has(f"Message type {RPCMessageType.BUY_NOTIFICATION} not configured for webhooks",
|
assert log_has(f"Message type {RPCMessageType.BUY_NOTIFICATION} not configured for webhooks",
|
||||||
caplog.record_tuples)
|
caplog)
|
||||||
|
|
||||||
default_conf["webhook"] = get_webhook_dict()
|
default_conf["webhook"] = get_webhook_dict()
|
||||||
default_conf["webhook"]["webhookbuy"]["value1"] = "{DEADBEEF:8f}"
|
default_conf["webhook"]["webhookbuy"]["value1"] = "{DEADBEEF:8f}"
|
||||||
@ -135,7 +135,7 @@ def test_exception_send_msg(default_conf, mocker, caplog):
|
|||||||
}
|
}
|
||||||
webhook.send_msg(msg)
|
webhook.send_msg(msg)
|
||||||
assert log_has("Problem calling Webhook. Please check your webhook configuration. "
|
assert log_has("Problem calling Webhook. Please check your webhook configuration. "
|
||||||
"Exception: 'DEADBEEF'", caplog.record_tuples)
|
"Exception: 'DEADBEEF'", caplog)
|
||||||
|
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
||||||
@ -164,4 +164,4 @@ def test__send_msg(default_conf, mocker, caplog):
|
|||||||
post = MagicMock(side_effect=RequestException)
|
post = MagicMock(side_effect=RequestException)
|
||||||
mocker.patch("freqtrade.rpc.webhook.post", post)
|
mocker.patch("freqtrade.rpc.webhook.post", post)
|
||||||
webhook._send_msg(msg)
|
webhook._send_msg(msg)
|
||||||
assert log_has('Could not call webhook url. Exception: ', caplog.record_tuples)
|
assert log_has('Could not call webhook url. Exception: ', caplog)
|
||||||
|
@ -19,13 +19,13 @@ _STRATEGY = DefaultStrategy(config={})
|
|||||||
|
|
||||||
def test_returns_latest_buy_signal(mocker, default_conf, ticker_history):
|
def test_returns_latest_buy_signal(mocker, default_conf, ticker_history):
|
||||||
mocker.patch.object(
|
mocker.patch.object(
|
||||||
_STRATEGY, 'analyze_ticker',
|
_STRATEGY, '_analyze_ticker_internal',
|
||||||
return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}])
|
return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}])
|
||||||
)
|
)
|
||||||
assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (True, False)
|
assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (True, False)
|
||||||
|
|
||||||
mocker.patch.object(
|
mocker.patch.object(
|
||||||
_STRATEGY, 'analyze_ticker',
|
_STRATEGY, '_analyze_ticker_internal',
|
||||||
return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}])
|
return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}])
|
||||||
)
|
)
|
||||||
assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (False, True)
|
assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (False, True)
|
||||||
@ -33,14 +33,14 @@ def test_returns_latest_buy_signal(mocker, default_conf, ticker_history):
|
|||||||
|
|
||||||
def test_returns_latest_sell_signal(mocker, default_conf, ticker_history):
|
def test_returns_latest_sell_signal(mocker, default_conf, ticker_history):
|
||||||
mocker.patch.object(
|
mocker.patch.object(
|
||||||
_STRATEGY, 'analyze_ticker',
|
_STRATEGY, '_analyze_ticker_internal',
|
||||||
return_value=DataFrame([{'sell': 1, 'buy': 0, 'date': arrow.utcnow()}])
|
return_value=DataFrame([{'sell': 1, 'buy': 0, 'date': arrow.utcnow()}])
|
||||||
)
|
)
|
||||||
|
|
||||||
assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (False, True)
|
assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (False, True)
|
||||||
|
|
||||||
mocker.patch.object(
|
mocker.patch.object(
|
||||||
_STRATEGY, 'analyze_ticker',
|
_STRATEGY, '_analyze_ticker_internal',
|
||||||
return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}])
|
return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}])
|
||||||
)
|
)
|
||||||
assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (True, False)
|
assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (True, False)
|
||||||
@ -49,34 +49,34 @@ def test_returns_latest_sell_signal(mocker, default_conf, ticker_history):
|
|||||||
def test_get_signal_empty(default_conf, mocker, caplog):
|
def test_get_signal_empty(default_conf, mocker, caplog):
|
||||||
assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'],
|
assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'],
|
||||||
DataFrame())
|
DataFrame())
|
||||||
assert log_has('Empty ticker history for pair foo', caplog.record_tuples)
|
assert log_has('Empty ticker history for pair foo', caplog)
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
|
||||||
assert (False, False) == _STRATEGY.get_signal('bar', default_conf['ticker_interval'],
|
assert (False, False) == _STRATEGY.get_signal('bar', default_conf['ticker_interval'],
|
||||||
[])
|
[])
|
||||||
assert log_has('Empty ticker history for pair bar', caplog.record_tuples)
|
assert log_has('Empty ticker history for pair bar', caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ticker_history):
|
def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ticker_history):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
mocker.patch.object(
|
mocker.patch.object(
|
||||||
_STRATEGY, 'analyze_ticker',
|
_STRATEGY, '_analyze_ticker_internal',
|
||||||
side_effect=ValueError('xyz')
|
side_effect=ValueError('xyz')
|
||||||
)
|
)
|
||||||
assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'],
|
assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'],
|
||||||
ticker_history)
|
ticker_history)
|
||||||
assert log_has('Unable to analyze ticker for pair foo: xyz', caplog.record_tuples)
|
assert log_has('Unable to analyze ticker for pair foo: xyz', caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ticker_history):
|
def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ticker_history):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
mocker.patch.object(
|
mocker.patch.object(
|
||||||
_STRATEGY, 'analyze_ticker',
|
_STRATEGY, '_analyze_ticker_internal',
|
||||||
return_value=DataFrame([])
|
return_value=DataFrame([])
|
||||||
)
|
)
|
||||||
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'],
|
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'],
|
||||||
ticker_history)
|
ticker_history)
|
||||||
assert log_has('Empty dataframe for pair xyz', caplog.record_tuples)
|
assert log_has('Empty dataframe for pair xyz', caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_get_signal_old_dataframe(default_conf, mocker, caplog, ticker_history):
|
def test_get_signal_old_dataframe(default_conf, mocker, caplog, ticker_history):
|
||||||
@ -86,15 +86,12 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ticker_history):
|
|||||||
oldtime = arrow.utcnow().shift(minutes=-16)
|
oldtime = arrow.utcnow().shift(minutes=-16)
|
||||||
ticks = DataFrame([{'buy': 1, 'date': oldtime}])
|
ticks = DataFrame([{'buy': 1, 'date': oldtime}])
|
||||||
mocker.patch.object(
|
mocker.patch.object(
|
||||||
_STRATEGY, 'analyze_ticker',
|
_STRATEGY, '_analyze_ticker_internal',
|
||||||
return_value=DataFrame(ticks)
|
return_value=DataFrame(ticks)
|
||||||
)
|
)
|
||||||
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'],
|
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'],
|
||||||
ticker_history)
|
ticker_history)
|
||||||
assert log_has(
|
assert log_has('Outdated history for pair xyz. Last tick is 16 minutes old', caplog)
|
||||||
'Outdated history for pair xyz. Last tick is 16 minutes old',
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_signal_handles_exceptions(mocker, default_conf):
|
def test_get_signal_handles_exceptions(mocker, default_conf):
|
||||||
@ -237,9 +234,8 @@ def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None:
|
|||||||
assert buy_mock.call_count == 1
|
assert buy_mock.call_count == 1
|
||||||
assert buy_mock.call_count == 1
|
assert buy_mock.call_count == 1
|
||||||
|
|
||||||
assert log_has('TA Analysis Launched', caplog.record_tuples)
|
assert log_has('TA Analysis Launched', caplog)
|
||||||
assert not log_has('Skipping TA Analysis for already analyzed candle',
|
assert not log_has('Skipping TA Analysis for already analyzed candle', caplog)
|
||||||
caplog.record_tuples)
|
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
|
||||||
strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'})
|
strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'})
|
||||||
@ -247,12 +243,11 @@ def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None:
|
|||||||
assert ind_mock.call_count == 2
|
assert ind_mock.call_count == 2
|
||||||
assert buy_mock.call_count == 2
|
assert buy_mock.call_count == 2
|
||||||
assert buy_mock.call_count == 2
|
assert buy_mock.call_count == 2
|
||||||
assert log_has('TA Analysis Launched', caplog.record_tuples)
|
assert log_has('TA Analysis Launched', caplog)
|
||||||
assert not log_has('Skipping TA Analysis for already analyzed candle',
|
assert not log_has('Skipping TA Analysis for already analyzed candle', caplog)
|
||||||
caplog.record_tuples)
|
|
||||||
|
|
||||||
|
|
||||||
def test_analyze_ticker_skip_analyze(ticker_history, mocker, caplog) -> None:
|
def test__analyze_ticker_internal_skip_analyze(ticker_history, mocker, caplog) -> None:
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
ind_mock = MagicMock(side_effect=lambda x, meta: x)
|
ind_mock = MagicMock(side_effect=lambda x, meta: x)
|
||||||
buy_mock = MagicMock(side_effect=lambda x, meta: x)
|
buy_mock = MagicMock(side_effect=lambda x, meta: x)
|
||||||
@ -267,7 +262,7 @@ def test_analyze_ticker_skip_analyze(ticker_history, mocker, caplog) -> None:
|
|||||||
strategy = DefaultStrategy({})
|
strategy = DefaultStrategy({})
|
||||||
strategy.process_only_new_candles = True
|
strategy.process_only_new_candles = True
|
||||||
|
|
||||||
ret = strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'})
|
ret = strategy._analyze_ticker_internal(ticker_history, {'pair': 'ETH/BTC'})
|
||||||
assert 'high' in ret.columns
|
assert 'high' in ret.columns
|
||||||
assert 'low' in ret.columns
|
assert 'low' in ret.columns
|
||||||
assert 'close' in ret.columns
|
assert 'close' in ret.columns
|
||||||
@ -275,12 +270,11 @@ def test_analyze_ticker_skip_analyze(ticker_history, mocker, caplog) -> None:
|
|||||||
assert ind_mock.call_count == 1
|
assert ind_mock.call_count == 1
|
||||||
assert buy_mock.call_count == 1
|
assert buy_mock.call_count == 1
|
||||||
assert buy_mock.call_count == 1
|
assert buy_mock.call_count == 1
|
||||||
assert log_has('TA Analysis Launched', caplog.record_tuples)
|
assert log_has('TA Analysis Launched', caplog)
|
||||||
assert not log_has('Skipping TA Analysis for already analyzed candle',
|
assert not log_has('Skipping TA Analysis for already analyzed candle', caplog)
|
||||||
caplog.record_tuples)
|
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
|
||||||
ret = strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'})
|
ret = strategy._analyze_ticker_internal(ticker_history, {'pair': 'ETH/BTC'})
|
||||||
# No analysis happens as process_only_new_candles is true
|
# No analysis happens as process_only_new_candles is true
|
||||||
assert ind_mock.call_count == 1
|
assert ind_mock.call_count == 1
|
||||||
assert buy_mock.call_count == 1
|
assert buy_mock.call_count == 1
|
||||||
@ -290,6 +284,21 @@ def test_analyze_ticker_skip_analyze(ticker_history, mocker, caplog) -> None:
|
|||||||
assert 'sell' in ret.columns
|
assert 'sell' in ret.columns
|
||||||
assert ret['buy'].sum() == 0
|
assert ret['buy'].sum() == 0
|
||||||
assert ret['sell'].sum() == 0
|
assert ret['sell'].sum() == 0
|
||||||
assert not log_has('TA Analysis Launched', caplog.record_tuples)
|
assert not log_has('TA Analysis Launched', caplog)
|
||||||
assert log_has('Skipping TA Analysis for already analyzed candle',
|
assert log_has('Skipping TA Analysis for already analyzed candle', caplog)
|
||||||
caplog.record_tuples)
|
|
||||||
|
|
||||||
|
def test_is_pair_locked(default_conf):
|
||||||
|
strategy = DefaultStrategy(default_conf)
|
||||||
|
# dict should be empty
|
||||||
|
assert not strategy._pair_locked_until
|
||||||
|
|
||||||
|
pair = 'ETH/BTC'
|
||||||
|
assert not strategy.is_pair_locked(pair)
|
||||||
|
strategy.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime)
|
||||||
|
# ETH/BTC locked for 4 minutes
|
||||||
|
assert strategy.is_pair_locked(pair)
|
||||||
|
|
||||||
|
# XRP/BTC should not be locked now
|
||||||
|
pair = 'XRP/BTC'
|
||||||
|
assert not strategy.is_pair_locked(pair)
|
||||||
|
@ -15,7 +15,7 @@ from freqtrade.resolvers import StrategyResolver
|
|||||||
from freqtrade.strategy import import_strategy
|
from freqtrade.strategy import import_strategy
|
||||||
from freqtrade.strategy.default_strategy import DefaultStrategy
|
from freqtrade.strategy.default_strategy import DefaultStrategy
|
||||||
from freqtrade.strategy.interface import IStrategy
|
from freqtrade.strategy.interface import IStrategy
|
||||||
from freqtrade.tests.conftest import log_has_re
|
from freqtrade.tests.conftest import log_has, log_has_re
|
||||||
|
|
||||||
|
|
||||||
def test_import_strategy(caplog):
|
def test_import_strategy(caplog):
|
||||||
@ -35,12 +35,8 @@ def test_import_strategy(caplog):
|
|||||||
assert imported_strategy.__module__ == 'freqtrade.strategy'
|
assert imported_strategy.__module__ == 'freqtrade.strategy'
|
||||||
assert imported_strategy.some_method() == 42
|
assert imported_strategy.some_method() == 42
|
||||||
|
|
||||||
assert (
|
assert log_has('Imported strategy freqtrade.strategy.default_strategy.DefaultStrategy '
|
||||||
'freqtrade.strategy',
|
'as freqtrade.strategy.DefaultStrategy', caplog)
|
||||||
logging.DEBUG,
|
|
||||||
'Imported strategy freqtrade.strategy.default_strategy.DefaultStrategy '
|
|
||||||
'as freqtrade.strategy.DefaultStrategy',
|
|
||||||
) in caplog.record_tuples
|
|
||||||
|
|
||||||
|
|
||||||
def test_search_strategy():
|
def test_search_strategy():
|
||||||
@ -64,63 +60,65 @@ def test_search_strategy():
|
|||||||
assert s is None
|
assert s is None
|
||||||
|
|
||||||
|
|
||||||
def test_load_strategy(result):
|
def test_load_strategy(default_conf, result):
|
||||||
resolver = StrategyResolver({'strategy': 'TestStrategy'})
|
default_conf.update({'strategy': 'TestStrategy'})
|
||||||
|
resolver = StrategyResolver(default_conf)
|
||||||
assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
|
assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
|
||||||
|
|
||||||
|
|
||||||
def test_load_strategy_base64(result, caplog):
|
def test_load_strategy_base64(result, caplog, default_conf):
|
||||||
with open("user_data/strategies/test_strategy.py", "rb") as file:
|
with open("user_data/strategies/test_strategy.py", "rb") as file:
|
||||||
encoded_string = urlsafe_b64encode(file.read()).decode("utf-8")
|
encoded_string = urlsafe_b64encode(file.read()).decode("utf-8")
|
||||||
resolver = StrategyResolver({'strategy': 'TestStrategy:{}'.format(encoded_string)})
|
default_conf.update({'strategy': 'TestStrategy:{}'.format(encoded_string)})
|
||||||
|
|
||||||
|
resolver = StrategyResolver(default_conf)
|
||||||
assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
|
assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
|
||||||
# Make sure strategy was loaded from base64 (using temp directory)!!
|
# Make sure strategy was loaded from base64 (using temp directory)!!
|
||||||
assert log_has_re(r"Using resolved strategy TestStrategy from '"
|
assert log_has_re(r"Using resolved strategy TestStrategy from '"
|
||||||
+ tempfile.gettempdir() + r"/.*/TestStrategy\.py'\.\.\.",
|
+ tempfile.gettempdir() + r"/.*/TestStrategy\.py'\.\.\.", caplog)
|
||||||
caplog.record_tuples)
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_strategy_invalid_directory(result, caplog):
|
def test_load_strategy_invalid_directory(result, caplog, default_conf):
|
||||||
resolver = StrategyResolver()
|
resolver = StrategyResolver(default_conf)
|
||||||
extra_dir = Path.cwd() / 'some/path'
|
extra_dir = Path.cwd() / 'some/path'
|
||||||
resolver._load_strategy('TestStrategy', config={}, extra_dir=extra_dir)
|
resolver._load_strategy('TestStrategy', config=default_conf, extra_dir=extra_dir)
|
||||||
|
|
||||||
assert log_has_re(r'Path .*' + r'some.*path.*' + r'.* does not exist', caplog.record_tuples)
|
assert log_has_re(r'Path .*' + r'some.*path.*' + r'.* does not exist', caplog)
|
||||||
|
|
||||||
assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
|
assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
|
||||||
|
|
||||||
|
|
||||||
def test_load_not_found_strategy():
|
def test_load_not_found_strategy(default_conf):
|
||||||
strategy = StrategyResolver()
|
strategy = StrategyResolver(default_conf)
|
||||||
with pytest.raises(OperationalException,
|
with pytest.raises(OperationalException,
|
||||||
match=r"Impossible to load Strategy 'NotFoundStrategy'. "
|
match=r"Impossible to load Strategy 'NotFoundStrategy'. "
|
||||||
r"This class does not exist or contains Python code errors."):
|
r"This class does not exist or contains Python code errors."):
|
||||||
strategy._load_strategy(strategy_name='NotFoundStrategy', config={})
|
strategy._load_strategy(strategy_name='NotFoundStrategy', config=default_conf)
|
||||||
|
|
||||||
|
|
||||||
def test_load_staticmethod_importerror(mocker, caplog):
|
def test_load_staticmethod_importerror(mocker, caplog, default_conf):
|
||||||
mocker.patch("freqtrade.resolvers.strategy_resolver.import_strategy", Mock(
|
mocker.patch("freqtrade.resolvers.strategy_resolver.import_strategy", Mock(
|
||||||
side_effect=TypeError("can't pickle staticmethod objects")))
|
side_effect=TypeError("can't pickle staticmethod objects")))
|
||||||
with pytest.raises(OperationalException,
|
with pytest.raises(OperationalException,
|
||||||
match=r"Impossible to load Strategy 'DefaultStrategy'. "
|
match=r"Impossible to load Strategy 'DefaultStrategy'. "
|
||||||
r"This class does not exist or contains Python code errors."):
|
r"This class does not exist or contains Python code errors."):
|
||||||
StrategyResolver()
|
StrategyResolver(default_conf)
|
||||||
assert log_has_re(r".*Error: can't pickle staticmethod objects", caplog.record_tuples)
|
assert log_has_re(r".*Error: can't pickle staticmethod objects", caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_strategy(result):
|
def test_strategy(result, default_conf):
|
||||||
config = {'strategy': 'DefaultStrategy'}
|
default_conf.update({'strategy': 'DefaultStrategy'})
|
||||||
|
|
||||||
resolver = StrategyResolver(config)
|
resolver = StrategyResolver(default_conf)
|
||||||
metadata = {'pair': 'ETH/BTC'}
|
metadata = {'pair': 'ETH/BTC'}
|
||||||
assert resolver.strategy.minimal_roi[0] == 0.04
|
assert resolver.strategy.minimal_roi[0] == 0.04
|
||||||
assert config["minimal_roi"]['0'] == 0.04
|
assert default_conf["minimal_roi"]['0'] == 0.04
|
||||||
|
|
||||||
assert resolver.strategy.stoploss == -0.10
|
assert resolver.strategy.stoploss == -0.10
|
||||||
assert config['stoploss'] == -0.10
|
assert default_conf['stoploss'] == -0.10
|
||||||
|
|
||||||
assert resolver.strategy.ticker_interval == '5m'
|
assert resolver.strategy.ticker_interval == '5m'
|
||||||
assert config['ticker_interval'] == '5m'
|
assert default_conf['ticker_interval'] == '5m'
|
||||||
|
|
||||||
df_indicators = resolver.strategy.advise_indicators(result, metadata=metadata)
|
df_indicators = resolver.strategy.advise_indicators(result, metadata=metadata)
|
||||||
assert 'adx' in df_indicators
|
assert 'adx' in df_indicators
|
||||||
@ -132,112 +130,95 @@ def test_strategy(result):
|
|||||||
assert 'sell' in dataframe.columns
|
assert 'sell' in dataframe.columns
|
||||||
|
|
||||||
|
|
||||||
def test_strategy_override_minimal_roi(caplog):
|
def test_strategy_override_minimal_roi(caplog, default_conf):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
config = {
|
default_conf.update({
|
||||||
'strategy': 'DefaultStrategy',
|
'strategy': 'DefaultStrategy',
|
||||||
'minimal_roi': {
|
'minimal_roi': {
|
||||||
"0": 0.5
|
"0": 0.5
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
resolver = StrategyResolver(config)
|
resolver = StrategyResolver(default_conf)
|
||||||
|
|
||||||
assert resolver.strategy.minimal_roi[0] == 0.5
|
assert resolver.strategy.minimal_roi[0] == 0.5
|
||||||
assert ('freqtrade.resolvers.strategy_resolver',
|
assert log_has("Override strategy 'minimal_roi' with value in config file: {'0': 0.5}.", caplog)
|
||||||
logging.INFO,
|
|
||||||
"Override strategy 'minimal_roi' with value in config file: {'0': 0.5}."
|
|
||||||
) in caplog.record_tuples
|
|
||||||
|
|
||||||
|
|
||||||
def test_strategy_override_stoploss(caplog):
|
def test_strategy_override_stoploss(caplog, default_conf):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
config = {
|
default_conf.update({
|
||||||
'strategy': 'DefaultStrategy',
|
'strategy': 'DefaultStrategy',
|
||||||
'stoploss': -0.5
|
'stoploss': -0.5
|
||||||
}
|
})
|
||||||
resolver = StrategyResolver(config)
|
resolver = StrategyResolver(default_conf)
|
||||||
|
|
||||||
assert resolver.strategy.stoploss == -0.5
|
assert resolver.strategy.stoploss == -0.5
|
||||||
assert ('freqtrade.resolvers.strategy_resolver',
|
assert log_has("Override strategy 'stoploss' with value in config file: -0.5.", caplog)
|
||||||
logging.INFO,
|
|
||||||
"Override strategy 'stoploss' with value in config file: -0.5."
|
|
||||||
) in caplog.record_tuples
|
|
||||||
|
|
||||||
|
|
||||||
def test_strategy_override_trailing_stop(caplog):
|
def test_strategy_override_trailing_stop(caplog, default_conf):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
config = {
|
default_conf.update({
|
||||||
'strategy': 'DefaultStrategy',
|
'strategy': 'DefaultStrategy',
|
||||||
'trailing_stop': True
|
'trailing_stop': True
|
||||||
}
|
})
|
||||||
resolver = StrategyResolver(config)
|
resolver = StrategyResolver(default_conf)
|
||||||
|
|
||||||
assert resolver.strategy.trailing_stop
|
assert resolver.strategy.trailing_stop
|
||||||
assert isinstance(resolver.strategy.trailing_stop, bool)
|
assert isinstance(resolver.strategy.trailing_stop, bool)
|
||||||
assert ('freqtrade.resolvers.strategy_resolver',
|
assert log_has("Override strategy 'trailing_stop' with value in config file: True.", caplog)
|
||||||
logging.INFO,
|
|
||||||
"Override strategy 'trailing_stop' with value in config file: True."
|
|
||||||
) in caplog.record_tuples
|
|
||||||
|
|
||||||
|
|
||||||
def test_strategy_override_trailing_stop_positive(caplog):
|
def test_strategy_override_trailing_stop_positive(caplog, default_conf):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
config = {
|
default_conf.update({
|
||||||
'strategy': 'DefaultStrategy',
|
'strategy': 'DefaultStrategy',
|
||||||
'trailing_stop_positive': -0.1,
|
'trailing_stop_positive': -0.1,
|
||||||
'trailing_stop_positive_offset': -0.2
|
'trailing_stop_positive_offset': -0.2
|
||||||
|
|
||||||
}
|
})
|
||||||
resolver = StrategyResolver(config)
|
resolver = StrategyResolver(default_conf)
|
||||||
|
|
||||||
assert resolver.strategy.trailing_stop_positive == -0.1
|
assert resolver.strategy.trailing_stop_positive == -0.1
|
||||||
assert ('freqtrade.resolvers.strategy_resolver',
|
assert log_has("Override strategy 'trailing_stop_positive' with value in config file: -0.1.",
|
||||||
logging.INFO,
|
caplog)
|
||||||
"Override strategy 'trailing_stop_positive' with value in config file: -0.1."
|
|
||||||
) in caplog.record_tuples
|
|
||||||
|
|
||||||
assert resolver.strategy.trailing_stop_positive_offset == -0.2
|
assert resolver.strategy.trailing_stop_positive_offset == -0.2
|
||||||
assert ('freqtrade.resolvers.strategy_resolver',
|
assert log_has("Override strategy 'trailing_stop_positive' with value in config file: -0.1.",
|
||||||
logging.INFO,
|
caplog)
|
||||||
"Override strategy 'trailing_stop_positive' with value in config file: -0.1."
|
|
||||||
) in caplog.record_tuples
|
|
||||||
|
|
||||||
|
|
||||||
def test_strategy_override_ticker_interval(caplog):
|
def test_strategy_override_ticker_interval(caplog, default_conf):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
|
|
||||||
config = {
|
default_conf.update({
|
||||||
'strategy': 'DefaultStrategy',
|
'strategy': 'DefaultStrategy',
|
||||||
'ticker_interval': 60,
|
'ticker_interval': 60,
|
||||||
'stake_currency': 'ETH'
|
'stake_currency': 'ETH'
|
||||||
}
|
})
|
||||||
resolver = StrategyResolver(config)
|
resolver = StrategyResolver(default_conf)
|
||||||
|
|
||||||
assert resolver.strategy.ticker_interval == 60
|
assert resolver.strategy.ticker_interval == 60
|
||||||
assert resolver.strategy.stake_currency == 'ETH'
|
assert resolver.strategy.stake_currency == 'ETH'
|
||||||
assert ('freqtrade.resolvers.strategy_resolver',
|
assert log_has("Override strategy 'ticker_interval' with value in config file: 60.",
|
||||||
logging.INFO,
|
caplog)
|
||||||
"Override strategy 'ticker_interval' with value in config file: 60."
|
|
||||||
) in caplog.record_tuples
|
|
||||||
|
|
||||||
|
|
||||||
def test_strategy_override_process_only_new_candles(caplog):
|
def test_strategy_override_process_only_new_candles(caplog, default_conf):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
|
|
||||||
config = {
|
default_conf.update({
|
||||||
'strategy': 'DefaultStrategy',
|
'strategy': 'DefaultStrategy',
|
||||||
'process_only_new_candles': True
|
'process_only_new_candles': True
|
||||||
}
|
})
|
||||||
resolver = StrategyResolver(config)
|
resolver = StrategyResolver(default_conf)
|
||||||
|
|
||||||
assert resolver.strategy.process_only_new_candles
|
assert resolver.strategy.process_only_new_candles
|
||||||
assert ('freqtrade.resolvers.strategy_resolver',
|
assert log_has("Override strategy 'process_only_new_candles' with value in config file: True.",
|
||||||
logging.INFO,
|
caplog)
|
||||||
"Override strategy 'process_only_new_candles' with value in config file: True."
|
|
||||||
) in caplog.record_tuples
|
|
||||||
|
|
||||||
|
|
||||||
def test_strategy_override_order_types(caplog):
|
def test_strategy_override_order_types(caplog, default_conf):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
|
|
||||||
order_types = {
|
order_types = {
|
||||||
@ -246,36 +227,32 @@ def test_strategy_override_order_types(caplog):
|
|||||||
'stoploss': 'limit',
|
'stoploss': 'limit',
|
||||||
'stoploss_on_exchange': True,
|
'stoploss_on_exchange': True,
|
||||||
}
|
}
|
||||||
|
default_conf.update({
|
||||||
config = {
|
|
||||||
'strategy': 'DefaultStrategy',
|
'strategy': 'DefaultStrategy',
|
||||||
'order_types': order_types
|
'order_types': order_types
|
||||||
}
|
})
|
||||||
resolver = StrategyResolver(config)
|
resolver = StrategyResolver(default_conf)
|
||||||
|
|
||||||
assert resolver.strategy.order_types
|
assert resolver.strategy.order_types
|
||||||
for method in ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']:
|
for method in ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']:
|
||||||
assert resolver.strategy.order_types[method] == order_types[method]
|
assert resolver.strategy.order_types[method] == order_types[method]
|
||||||
|
|
||||||
assert ('freqtrade.resolvers.strategy_resolver',
|
assert log_has("Override strategy 'order_types' with value in config file:"
|
||||||
logging.INFO,
|
|
||||||
"Override strategy 'order_types' with value in config file:"
|
|
||||||
" {'buy': 'market', 'sell': 'limit', 'stoploss': 'limit',"
|
" {'buy': 'market', 'sell': 'limit', 'stoploss': 'limit',"
|
||||||
" 'stoploss_on_exchange': True}."
|
" 'stoploss_on_exchange': True}.", caplog)
|
||||||
) in caplog.record_tuples
|
|
||||||
|
|
||||||
config = {
|
default_conf.update({
|
||||||
'strategy': 'DefaultStrategy',
|
'strategy': 'DefaultStrategy',
|
||||||
'order_types': {'buy': 'market'}
|
'order_types': {'buy': 'market'}
|
||||||
}
|
})
|
||||||
# Raise error for invalid configuration
|
# Raise error for invalid configuration
|
||||||
with pytest.raises(ImportError,
|
with pytest.raises(ImportError,
|
||||||
match=r"Impossible to load Strategy 'DefaultStrategy'. "
|
match=r"Impossible to load Strategy 'DefaultStrategy'. "
|
||||||
r"Order-types mapping is incomplete."):
|
r"Order-types mapping is incomplete."):
|
||||||
StrategyResolver(config)
|
StrategyResolver(default_conf)
|
||||||
|
|
||||||
|
|
||||||
def test_strategy_override_order_tif(caplog):
|
def test_strategy_override_order_tif(caplog, default_conf):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
|
|
||||||
order_time_in_force = {
|
order_time_in_force = {
|
||||||
@ -283,94 +260,86 @@ def test_strategy_override_order_tif(caplog):
|
|||||||
'sell': 'gtc',
|
'sell': 'gtc',
|
||||||
}
|
}
|
||||||
|
|
||||||
config = {
|
default_conf.update({
|
||||||
'strategy': 'DefaultStrategy',
|
'strategy': 'DefaultStrategy',
|
||||||
'order_time_in_force': order_time_in_force
|
'order_time_in_force': order_time_in_force
|
||||||
}
|
})
|
||||||
resolver = StrategyResolver(config)
|
resolver = StrategyResolver(default_conf)
|
||||||
|
|
||||||
assert resolver.strategy.order_time_in_force
|
assert resolver.strategy.order_time_in_force
|
||||||
for method in ['buy', 'sell']:
|
for method in ['buy', 'sell']:
|
||||||
assert resolver.strategy.order_time_in_force[method] == order_time_in_force[method]
|
assert resolver.strategy.order_time_in_force[method] == order_time_in_force[method]
|
||||||
|
|
||||||
assert ('freqtrade.resolvers.strategy_resolver',
|
assert log_has("Override strategy 'order_time_in_force' with value in config file:"
|
||||||
logging.INFO,
|
" {'buy': 'fok', 'sell': 'gtc'}.", caplog)
|
||||||
"Override strategy 'order_time_in_force' with value in config file:"
|
|
||||||
" {'buy': 'fok', 'sell': 'gtc'}."
|
|
||||||
) in caplog.record_tuples
|
|
||||||
|
|
||||||
config = {
|
default_conf.update({
|
||||||
'strategy': 'DefaultStrategy',
|
'strategy': 'DefaultStrategy',
|
||||||
'order_time_in_force': {'buy': 'fok'}
|
'order_time_in_force': {'buy': 'fok'}
|
||||||
}
|
})
|
||||||
# Raise error for invalid configuration
|
# Raise error for invalid configuration
|
||||||
with pytest.raises(ImportError,
|
with pytest.raises(ImportError,
|
||||||
match=r"Impossible to load Strategy 'DefaultStrategy'. "
|
match=r"Impossible to load Strategy 'DefaultStrategy'. "
|
||||||
r"Order-time-in-force mapping is incomplete."):
|
r"Order-time-in-force mapping is incomplete."):
|
||||||
StrategyResolver(config)
|
StrategyResolver(default_conf)
|
||||||
|
|
||||||
|
|
||||||
def test_strategy_override_use_sell_signal(caplog):
|
def test_strategy_override_use_sell_signal(caplog, default_conf):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
config = {
|
default_conf.update({
|
||||||
'strategy': 'DefaultStrategy',
|
'strategy': 'DefaultStrategy',
|
||||||
}
|
})
|
||||||
resolver = StrategyResolver(config)
|
resolver = StrategyResolver(default_conf)
|
||||||
assert not resolver.strategy.use_sell_signal
|
assert not resolver.strategy.use_sell_signal
|
||||||
assert isinstance(resolver.strategy.use_sell_signal, bool)
|
assert isinstance(resolver.strategy.use_sell_signal, bool)
|
||||||
# must be inserted to configuration
|
# must be inserted to configuration
|
||||||
assert 'use_sell_signal' in config['experimental']
|
assert 'use_sell_signal' in default_conf['experimental']
|
||||||
assert not config['experimental']['use_sell_signal']
|
assert not default_conf['experimental']['use_sell_signal']
|
||||||
|
|
||||||
config = {
|
default_conf.update({
|
||||||
'strategy': 'DefaultStrategy',
|
'strategy': 'DefaultStrategy',
|
||||||
'experimental': {
|
'experimental': {
|
||||||
'use_sell_signal': True,
|
'use_sell_signal': True,
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
resolver = StrategyResolver(config)
|
resolver = StrategyResolver(default_conf)
|
||||||
|
|
||||||
assert resolver.strategy.use_sell_signal
|
assert resolver.strategy.use_sell_signal
|
||||||
assert isinstance(resolver.strategy.use_sell_signal, bool)
|
assert isinstance(resolver.strategy.use_sell_signal, bool)
|
||||||
assert ('freqtrade.resolvers.strategy_resolver',
|
assert log_has("Override strategy 'use_sell_signal' with value in config file: True.", caplog)
|
||||||
logging.INFO,
|
|
||||||
"Override strategy 'use_sell_signal' with value in config file: True."
|
|
||||||
) in caplog.record_tuples
|
|
||||||
|
|
||||||
|
|
||||||
def test_strategy_override_use_sell_profit_only(caplog):
|
def test_strategy_override_use_sell_profit_only(caplog, default_conf):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
config = {
|
default_conf.update({
|
||||||
'strategy': 'DefaultStrategy',
|
'strategy': 'DefaultStrategy',
|
||||||
}
|
})
|
||||||
resolver = StrategyResolver(config)
|
resolver = StrategyResolver(default_conf)
|
||||||
assert not resolver.strategy.sell_profit_only
|
assert not resolver.strategy.sell_profit_only
|
||||||
assert isinstance(resolver.strategy.sell_profit_only, bool)
|
assert isinstance(resolver.strategy.sell_profit_only, bool)
|
||||||
# must be inserted to configuration
|
# must be inserted to configuration
|
||||||
assert 'sell_profit_only' in config['experimental']
|
assert 'sell_profit_only' in default_conf['experimental']
|
||||||
assert not config['experimental']['sell_profit_only']
|
assert not default_conf['experimental']['sell_profit_only']
|
||||||
|
|
||||||
config = {
|
default_conf.update({
|
||||||
'strategy': 'DefaultStrategy',
|
'strategy': 'DefaultStrategy',
|
||||||
'experimental': {
|
'experimental': {
|
||||||
'sell_profit_only': True,
|
'sell_profit_only': True,
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
resolver = StrategyResolver(config)
|
resolver = StrategyResolver(default_conf)
|
||||||
|
|
||||||
assert resolver.strategy.sell_profit_only
|
assert resolver.strategy.sell_profit_only
|
||||||
assert isinstance(resolver.strategy.sell_profit_only, bool)
|
assert isinstance(resolver.strategy.sell_profit_only, bool)
|
||||||
assert ('freqtrade.resolvers.strategy_resolver',
|
assert log_has("Override strategy 'sell_profit_only' with value in config file: True.", caplog)
|
||||||
logging.INFO,
|
|
||||||
"Override strategy 'sell_profit_only' with value in config file: True."
|
|
||||||
) in caplog.record_tuples
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.filterwarnings("ignore:deprecated")
|
@pytest.mark.filterwarnings("ignore:deprecated")
|
||||||
def test_deprecate_populate_indicators(result):
|
def test_deprecate_populate_indicators(result, default_conf):
|
||||||
default_location = path.join(path.dirname(path.realpath(__file__)))
|
default_location = path.join(path.dirname(path.realpath(__file__)))
|
||||||
resolver = StrategyResolver({'strategy': 'TestStrategyLegacy',
|
default_conf.update({'strategy': 'TestStrategyLegacy',
|
||||||
'strategy_path': default_location})
|
'strategy_path': default_location})
|
||||||
|
resolver = StrategyResolver(default_conf)
|
||||||
with warnings.catch_warnings(record=True) as w:
|
with warnings.catch_warnings(record=True) as w:
|
||||||
# Cause all warnings to always be triggered.
|
# Cause all warnings to always be triggered.
|
||||||
warnings.simplefilter("always")
|
warnings.simplefilter("always")
|
||||||
@ -400,10 +369,11 @@ def test_deprecate_populate_indicators(result):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.filterwarnings("ignore:deprecated")
|
@pytest.mark.filterwarnings("ignore:deprecated")
|
||||||
def test_call_deprecated_function(result, monkeypatch):
|
def test_call_deprecated_function(result, monkeypatch, default_conf):
|
||||||
default_location = path.join(path.dirname(path.realpath(__file__)))
|
default_location = path.join(path.dirname(path.realpath(__file__)))
|
||||||
resolver = StrategyResolver({'strategy': 'TestStrategyLegacy',
|
default_conf.update({'strategy': 'TestStrategyLegacy',
|
||||||
'strategy_path': default_location})
|
'strategy_path': default_location})
|
||||||
|
resolver = StrategyResolver(default_conf)
|
||||||
metadata = {'pair': 'ETH/BTC'}
|
metadata = {'pair': 'ETH/BTC'}
|
||||||
|
|
||||||
# Make sure we are using a legacy function
|
# Make sure we are using a legacy function
|
||||||
|
@ -3,8 +3,8 @@ import argparse
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from freqtrade.configuration import Arguments, TimeRange
|
from freqtrade.configuration import Arguments
|
||||||
from freqtrade.configuration.arguments import ARGS_DOWNLOADER, ARGS_PLOT_DATAFRAME
|
from freqtrade.configuration.arguments import ARGS_PLOT_DATAFRAME
|
||||||
from freqtrade.configuration.cli_options import check_int_positive
|
from freqtrade.configuration.cli_options import check_int_positive
|
||||||
|
|
||||||
|
|
||||||
@ -50,10 +50,10 @@ def test_parse_args_verbose() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_common_scripts_options() -> None:
|
def test_common_scripts_options() -> None:
|
||||||
arguments = Arguments(['-p', 'ETH/BTC'], '')
|
args = Arguments(['download-data', '-p', 'ETH/BTC', 'XRP/BTC'], '').get_parsed_arg()
|
||||||
arguments._build_args(ARGS_DOWNLOADER)
|
|
||||||
args = arguments._parse_args()
|
assert args.pairs == ['ETH/BTC', 'XRP/BTC']
|
||||||
assert args.pairs == 'ETH/BTC'
|
assert hasattr(args, "func")
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_version() -> None:
|
def test_parse_args_version() -> None:
|
||||||
@ -86,30 +86,6 @@ def test_parse_args_strategy_path_invalid() -> None:
|
|||||||
Arguments(['--strategy-path'], '').get_parsed_arg()
|
Arguments(['--strategy-path'], '').get_parsed_arg()
|
||||||
|
|
||||||
|
|
||||||
def test_parse_timerange_incorrect() -> None:
|
|
||||||
assert TimeRange(None, 'line', 0, -200) == Arguments.parse_timerange('-200')
|
|
||||||
assert TimeRange('line', None, 200, 0) == Arguments.parse_timerange('200-')
|
|
||||||
assert TimeRange('index', 'index', 200, 500) == Arguments.parse_timerange('200-500')
|
|
||||||
|
|
||||||
assert TimeRange('date', None, 1274486400, 0) == Arguments.parse_timerange('20100522-')
|
|
||||||
assert TimeRange(None, 'date', 0, 1274486400) == Arguments.parse_timerange('-20100522')
|
|
||||||
timerange = Arguments.parse_timerange('20100522-20150730')
|
|
||||||
assert timerange == TimeRange('date', 'date', 1274486400, 1438214400)
|
|
||||||
|
|
||||||
# Added test for unix timestamp - BTC genesis date
|
|
||||||
assert TimeRange('date', None, 1231006505, 0) == Arguments.parse_timerange('1231006505-')
|
|
||||||
assert TimeRange(None, 'date', 0, 1233360000) == Arguments.parse_timerange('-1233360000')
|
|
||||||
timerange = Arguments.parse_timerange('1231006505-1233360000')
|
|
||||||
assert TimeRange('date', 'date', 1231006505, 1233360000) == timerange
|
|
||||||
|
|
||||||
# TODO: Find solution for the following case (passing timestamp in ms)
|
|
||||||
timerange = Arguments.parse_timerange('1231006505000-1233360000000')
|
|
||||||
assert TimeRange('date', 'date', 1231006505, 1233360000) != timerange
|
|
||||||
|
|
||||||
with pytest.raises(Exception, match=r'Incorrect syntax.*'):
|
|
||||||
Arguments.parse_timerange('-')
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_backtesting_invalid() -> None:
|
def test_parse_args_backtesting_invalid() -> None:
|
||||||
with pytest.raises(SystemExit, match=r'2'):
|
with pytest.raises(SystemExit, match=r'2'):
|
||||||
Arguments(['backtesting --ticker-interval'], '').get_parsed_arg()
|
Arguments(['backtesting --ticker-interval'], '').get_parsed_arg()
|
||||||
@ -122,7 +98,6 @@ def test_parse_args_backtesting_custom() -> None:
|
|||||||
args = [
|
args = [
|
||||||
'-c', 'test_conf.json',
|
'-c', 'test_conf.json',
|
||||||
'backtesting',
|
'backtesting',
|
||||||
'--live',
|
|
||||||
'--ticker-interval', '1m',
|
'--ticker-interval', '1m',
|
||||||
'--refresh-pairs-cached',
|
'--refresh-pairs-cached',
|
||||||
'--strategy-list',
|
'--strategy-list',
|
||||||
@ -131,7 +106,6 @@ def test_parse_args_backtesting_custom() -> None:
|
|||||||
]
|
]
|
||||||
call_args = Arguments(args, '').get_parsed_arg()
|
call_args = Arguments(args, '').get_parsed_arg()
|
||||||
assert call_args.config == ['test_conf.json']
|
assert call_args.config == ['test_conf.json']
|
||||||
assert call_args.live is True
|
|
||||||
assert call_args.verbosity == 0
|
assert call_args.verbosity == 0
|
||||||
assert call_args.subparser == 'backtesting'
|
assert call_args.subparser == 'backtesting'
|
||||||
assert call_args.func is not None
|
assert call_args.func is not None
|
||||||
@ -159,14 +133,14 @@ def test_parse_args_hyperopt_custom() -> None:
|
|||||||
|
|
||||||
def test_download_data_options() -> None:
|
def test_download_data_options() -> None:
|
||||||
args = [
|
args = [
|
||||||
'--pairs-file', 'file_with_pairs',
|
|
||||||
'--datadir', 'datadir/directory',
|
'--datadir', 'datadir/directory',
|
||||||
|
'download-data',
|
||||||
|
'--pairs-file', 'file_with_pairs',
|
||||||
'--days', '30',
|
'--days', '30',
|
||||||
'--exchange', 'binance'
|
'--exchange', 'binance'
|
||||||
]
|
]
|
||||||
arguments = Arguments(args, '')
|
args = Arguments(args, '').get_parsed_arg()
|
||||||
arguments._build_args(ARGS_DOWNLOADER)
|
|
||||||
args = arguments._parse_args()
|
|
||||||
assert args.pairs_file == 'file_with_pairs'
|
assert args.pairs_file == 'file_with_pairs'
|
||||||
assert args.datadir == 'datadir/directory'
|
assert args.datadir == 'datadir/directory'
|
||||||
assert args.days == 30
|
assert args.days == 30
|
||||||
@ -186,7 +160,7 @@ def test_plot_dataframe_options() -> None:
|
|||||||
assert pargs.indicators1 == "sma10,sma100"
|
assert pargs.indicators1 == "sma10,sma100"
|
||||||
assert pargs.indicators2 == "macd,fastd,fastk"
|
assert pargs.indicators2 == "macd,fastd,fastk"
|
||||||
assert pargs.plot_limit == 30
|
assert pargs.plot_limit == 30
|
||||||
assert pargs.pairs == "UNITTEST/BTC"
|
assert pargs.pairs == ["UNITTEST/BTC"]
|
||||||
|
|
||||||
|
|
||||||
def test_check_int_positive() -> None:
|
def test_check_int_positive() -> None:
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import warnings
|
import warnings
|
||||||
from argparse import Namespace
|
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
@ -11,10 +10,12 @@ import pytest
|
|||||||
from jsonschema import Draft4Validator, ValidationError, validate
|
from jsonschema import Draft4Validator, ValidationError, validate
|
||||||
|
|
||||||
from freqtrade import OperationalException, constants
|
from freqtrade import OperationalException, constants
|
||||||
from freqtrade.configuration import Arguments, Configuration
|
from freqtrade.configuration import Arguments, Configuration, validate_config_consistency
|
||||||
from freqtrade.configuration.check_exchange import check_exchange
|
from freqtrade.configuration.check_exchange import check_exchange
|
||||||
from freqtrade.configuration.create_datadir import create_datadir
|
from freqtrade.configuration.config_validation import validate_config_schema
|
||||||
from freqtrade.configuration.json_schema import validate_config_schema
|
from freqtrade.configuration.directory_operations import (create_datadir,
|
||||||
|
create_userdata_dir)
|
||||||
|
from freqtrade.configuration.load_config import load_config_file
|
||||||
from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL
|
from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL
|
||||||
from freqtrade.loggers import _set_loggers
|
from freqtrade.loggers import _set_loggers
|
||||||
from freqtrade.state import RunMode
|
from freqtrade.state import RunMode
|
||||||
@ -26,8 +27,7 @@ from freqtrade.tests.conftest import (log_has, log_has_re,
|
|||||||
def all_conf():
|
def all_conf():
|
||||||
config_file = Path(__file__).parents[2] / "config_full.json.example"
|
config_file = Path(__file__).parents[2] / "config_full.json.example"
|
||||||
print(config_file)
|
print(config_file)
|
||||||
configuration = Configuration(Namespace())
|
conf = load_config_file(str(config_file))
|
||||||
conf = configuration._load_config_file(str(config_file))
|
|
||||||
return conf
|
return conf
|
||||||
|
|
||||||
|
|
||||||
@ -53,12 +53,12 @@ def test_load_config_incorrect_stake_amount(default_conf) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_load_config_file(default_conf, mocker, caplog) -> None:
|
def test_load_config_file(default_conf, mocker, caplog) -> None:
|
||||||
file_mock = mocker.patch('freqtrade.configuration.configuration.open', mocker.mock_open(
|
del default_conf['user_data_dir']
|
||||||
|
file_mock = mocker.patch('freqtrade.configuration.load_config.open', mocker.mock_open(
|
||||||
read_data=json.dumps(default_conf)
|
read_data=json.dumps(default_conf)
|
||||||
))
|
))
|
||||||
|
|
||||||
configuration = Configuration(Namespace())
|
validated_conf = load_config_file('somefile')
|
||||||
validated_conf = configuration._load_config_file('somefile')
|
|
||||||
assert file_mock.call_count == 1
|
assert file_mock.call_count == 1
|
||||||
assert validated_conf.items() >= default_conf.items()
|
assert validated_conf.items() >= default_conf.items()
|
||||||
|
|
||||||
@ -73,7 +73,7 @@ def test__args_to_config(caplog):
|
|||||||
# No warnings ...
|
# No warnings ...
|
||||||
configuration._args_to_config(config, argname="strategy_path", logstring="DeadBeef")
|
configuration._args_to_config(config, argname="strategy_path", logstring="DeadBeef")
|
||||||
assert len(w) == 0
|
assert len(w) == 0
|
||||||
assert log_has("DeadBeef", caplog.record_tuples)
|
assert log_has("DeadBeef", caplog)
|
||||||
assert config['strategy_path'] == "TestTest"
|
assert config['strategy_path'] == "TestTest"
|
||||||
|
|
||||||
configuration = Configuration(args)
|
configuration = Configuration(args)
|
||||||
@ -85,7 +85,7 @@ def test__args_to_config(caplog):
|
|||||||
assert len(w) == 1
|
assert len(w) == 1
|
||||||
assert issubclass(w[-1].category, DeprecationWarning)
|
assert issubclass(w[-1].category, DeprecationWarning)
|
||||||
assert "DEPRECATED: Going away soon!" in str(w[-1].message)
|
assert "DEPRECATED: Going away soon!" in str(w[-1].message)
|
||||||
assert log_has("DeadBeef", caplog.record_tuples)
|
assert log_has("DeadBeef", caplog)
|
||||||
assert config['strategy_path'] == "TestTest"
|
assert config['strategy_path'] == "TestTest"
|
||||||
|
|
||||||
|
|
||||||
@ -99,7 +99,7 @@ def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None:
|
|||||||
|
|
||||||
assert validated_conf['max_open_trades'] == 0
|
assert validated_conf['max_open_trades'] == 0
|
||||||
assert 'internals' in validated_conf
|
assert 'internals' in validated_conf
|
||||||
assert log_has('Validating configuration ...', caplog.record_tuples)
|
assert log_has('Validating configuration ...', caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None:
|
def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None:
|
||||||
@ -114,7 +114,7 @@ def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None:
|
|||||||
|
|
||||||
configsmock = MagicMock(side_effect=config_files)
|
configsmock = MagicMock(side_effect=config_files)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
'freqtrade.configuration.configuration.Configuration._load_config_file',
|
'freqtrade.configuration.configuration.load_config_file',
|
||||||
configsmock
|
configsmock
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -131,7 +131,36 @@ def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None:
|
|||||||
assert validated_conf['exchange']['pair_whitelist'] == conf2['exchange']['pair_whitelist']
|
assert validated_conf['exchange']['pair_whitelist'] == conf2['exchange']['pair_whitelist']
|
||||||
|
|
||||||
assert 'internals' in validated_conf
|
assert 'internals' in validated_conf
|
||||||
assert log_has('Validating configuration ...', caplog.record_tuples)
|
assert log_has('Validating configuration ...', caplog)
|
||||||
|
|
||||||
|
|
||||||
|
def test_from_config(default_conf, mocker, caplog) -> None:
|
||||||
|
conf1 = deepcopy(default_conf)
|
||||||
|
conf2 = deepcopy(default_conf)
|
||||||
|
del conf1['exchange']['key']
|
||||||
|
del conf1['exchange']['secret']
|
||||||
|
del conf2['exchange']['name']
|
||||||
|
conf2['exchange']['pair_whitelist'] += ['NANO/BTC']
|
||||||
|
conf2['fiat_display_currency'] = "EUR"
|
||||||
|
config_files = [conf1, conf2]
|
||||||
|
|
||||||
|
configsmock = MagicMock(side_effect=config_files)
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.configuration.configuration.load_config_file',
|
||||||
|
configsmock
|
||||||
|
)
|
||||||
|
|
||||||
|
validated_conf = Configuration.from_files(['test_conf.json', 'test2_conf.json'])
|
||||||
|
|
||||||
|
exchange_conf = default_conf['exchange']
|
||||||
|
assert validated_conf['exchange']['name'] == exchange_conf['name']
|
||||||
|
assert validated_conf['exchange']['key'] == exchange_conf['key']
|
||||||
|
assert validated_conf['exchange']['secret'] == exchange_conf['secret']
|
||||||
|
assert validated_conf['exchange']['pair_whitelist'] != conf1['exchange']['pair_whitelist']
|
||||||
|
assert validated_conf['exchange']['pair_whitelist'] == conf2['exchange']['pair_whitelist']
|
||||||
|
assert validated_conf['fiat_display_currency'] == "EUR"
|
||||||
|
assert 'internals' in validated_conf
|
||||||
|
assert log_has('Validating configuration ...', caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_load_config_max_open_trades_minus_one(default_conf, mocker, caplog) -> None:
|
def test_load_config_max_open_trades_minus_one(default_conf, mocker, caplog) -> None:
|
||||||
@ -144,7 +173,7 @@ def test_load_config_max_open_trades_minus_one(default_conf, mocker, caplog) ->
|
|||||||
|
|
||||||
assert validated_conf['max_open_trades'] > 999999999
|
assert validated_conf['max_open_trades'] > 999999999
|
||||||
assert validated_conf['max_open_trades'] == float('inf')
|
assert validated_conf['max_open_trades'] == float('inf')
|
||||||
assert log_has('Validating configuration ...', caplog.record_tuples)
|
assert log_has('Validating configuration ...', caplog)
|
||||||
assert "runmode" in validated_conf
|
assert "runmode" in validated_conf
|
||||||
assert validated_conf['runmode'] == RunMode.DRY_RUN
|
assert validated_conf['runmode'] == RunMode.DRY_RUN
|
||||||
|
|
||||||
@ -154,10 +183,9 @@ def test_load_config_file_exception(mocker) -> None:
|
|||||||
'freqtrade.configuration.configuration.open',
|
'freqtrade.configuration.configuration.open',
|
||||||
MagicMock(side_effect=FileNotFoundError('File not found'))
|
MagicMock(side_effect=FileNotFoundError('File not found'))
|
||||||
)
|
)
|
||||||
configuration = Configuration(Namespace())
|
|
||||||
|
|
||||||
with pytest.raises(OperationalException, match=r'.*Config file "somefile" not found!*'):
|
with pytest.raises(OperationalException, match=r'.*Config file "somefile" not found!*'):
|
||||||
configuration._load_config_file('somefile')
|
load_config_file('somefile')
|
||||||
|
|
||||||
|
|
||||||
def test_load_config(default_conf, mocker) -> None:
|
def test_load_config(default_conf, mocker) -> None:
|
||||||
@ -282,8 +310,8 @@ def test_show_info(default_conf, mocker, caplog) -> None:
|
|||||||
configuration = Configuration(args)
|
configuration = Configuration(args)
|
||||||
configuration.get_config()
|
configuration.get_config()
|
||||||
|
|
||||||
assert log_has('Using DB: "sqlite:///tmp/testdb"', caplog.record_tuples)
|
assert log_has('Using DB: "sqlite:///tmp/testdb"', caplog)
|
||||||
assert log_has('Dry run is enabled', caplog.record_tuples)
|
assert log_has('Dry run is enabled', caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
||||||
@ -305,40 +333,39 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
|
|||||||
assert 'exchange' in config
|
assert 'exchange' in config
|
||||||
assert 'pair_whitelist' in config['exchange']
|
assert 'pair_whitelist' in config['exchange']
|
||||||
assert 'datadir' in config
|
assert 'datadir' in config
|
||||||
assert log_has(
|
assert 'user_data_dir' in config
|
||||||
'Using data directory: {} ...'.format(config['datadir']),
|
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
assert 'ticker_interval' in config
|
assert 'ticker_interval' in config
|
||||||
assert not log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
|
assert not log_has('Parameter -i/--ticker-interval detected ...', caplog)
|
||||||
|
|
||||||
assert 'live' not in config
|
|
||||||
assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
|
||||||
|
|
||||||
assert 'position_stacking' not in config
|
assert 'position_stacking' not in config
|
||||||
assert not log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples)
|
assert not log_has('Parameter --enable-position-stacking detected ...', caplog)
|
||||||
|
|
||||||
assert 'refresh_pairs' not in config
|
assert 'refresh_pairs' not in config
|
||||||
assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog)
|
||||||
|
|
||||||
assert 'timerange' not in config
|
assert 'timerange' not in config
|
||||||
assert 'export' not in config
|
assert 'export' not in config
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.filterwarnings("ignore:DEPRECATED")
|
||||||
def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None:
|
def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None:
|
||||||
patched_configuration_load_config_file(mocker, default_conf)
|
patched_configuration_load_config_file(mocker, default_conf)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
'freqtrade.configuration.configuration.create_datadir',
|
'freqtrade.configuration.configuration.create_datadir',
|
||||||
lambda c, x: x
|
lambda c, x: x
|
||||||
)
|
)
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.configuration.configuration.create_userdata_dir',
|
||||||
|
lambda x, *args, **kwargs: Path(x)
|
||||||
|
)
|
||||||
arglist = [
|
arglist = [
|
||||||
'--config', 'config.json',
|
'--config', 'config.json',
|
||||||
'--strategy', 'DefaultStrategy',
|
'--strategy', 'DefaultStrategy',
|
||||||
'--datadir', '/foo/bar',
|
'--datadir', '/foo/bar',
|
||||||
|
'--userdir', "/tmp/freqtrade",
|
||||||
'backtesting',
|
'backtesting',
|
||||||
'--ticker-interval', '1m',
|
'--ticker-interval', '1m',
|
||||||
'--live',
|
|
||||||
'--enable-position-stacking',
|
'--enable-position-stacking',
|
||||||
'--disable-max-market-positions',
|
'--disable-max-market-positions',
|
||||||
'--refresh-pairs-cached',
|
'--refresh-pairs-cached',
|
||||||
@ -356,37 +383,28 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
|||||||
assert 'exchange' in config
|
assert 'exchange' in config
|
||||||
assert 'pair_whitelist' in config['exchange']
|
assert 'pair_whitelist' in config['exchange']
|
||||||
assert 'datadir' in config
|
assert 'datadir' in config
|
||||||
assert log_has(
|
assert log_has('Using data directory: {} ...'.format("/foo/bar"), caplog)
|
||||||
'Using data directory: {} ...'.format(config['datadir']),
|
assert log_has('Using user-data directory: {} ...'.format("/tmp/freqtrade"), caplog)
|
||||||
caplog.record_tuples
|
assert 'user_data_dir' in config
|
||||||
)
|
|
||||||
assert 'ticker_interval' in config
|
assert 'ticker_interval' in config
|
||||||
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
||||||
caplog.record_tuples)
|
caplog)
|
||||||
|
|
||||||
assert 'live' in config
|
|
||||||
assert log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
|
||||||
|
|
||||||
assert 'position_stacking'in config
|
assert 'position_stacking'in config
|
||||||
assert log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples)
|
assert log_has('Parameter --enable-position-stacking detected ...', caplog)
|
||||||
|
|
||||||
assert 'use_max_market_positions' in config
|
assert 'use_max_market_positions' in config
|
||||||
assert log_has('Parameter --disable-max-market-positions detected ...', caplog.record_tuples)
|
assert log_has('Parameter --disable-max-market-positions detected ...', caplog)
|
||||||
assert log_has('max_open_trades set to unlimited ...', caplog.record_tuples)
|
assert log_has('max_open_trades set to unlimited ...', caplog)
|
||||||
|
|
||||||
assert 'refresh_pairs'in config
|
assert 'refresh_pairs'in config
|
||||||
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog)
|
||||||
assert 'timerange' in config
|
assert 'timerange' in config
|
||||||
assert log_has(
|
assert log_has('Parameter --timerange detected: {} ...'.format(config['timerange']), caplog)
|
||||||
'Parameter --timerange detected: {} ...'.format(config['timerange']),
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
|
|
||||||
assert 'export' in config
|
assert 'export' in config
|
||||||
assert log_has(
|
assert log_has('Parameter --export detected: {} ...'.format(config['export']), caplog)
|
||||||
'Parameter --export detected: {} ...'.format(config['export']),
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> None:
|
def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> None:
|
||||||
@ -416,16 +434,13 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non
|
|||||||
assert 'exchange' in config
|
assert 'exchange' in config
|
||||||
assert 'pair_whitelist' in config['exchange']
|
assert 'pair_whitelist' in config['exchange']
|
||||||
assert 'datadir' in config
|
assert 'datadir' in config
|
||||||
assert log_has(
|
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
||||||
'Using data directory: {} ...'.format(config['datadir']),
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
assert 'ticker_interval' in config
|
assert 'ticker_interval' in config
|
||||||
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
||||||
caplog.record_tuples)
|
caplog)
|
||||||
|
|
||||||
assert 'strategy_list' in config
|
assert 'strategy_list' in config
|
||||||
assert log_has('Using strategy list of 2 Strategies', caplog.record_tuples)
|
assert log_has('Using strategy list of 2 Strategies', caplog)
|
||||||
|
|
||||||
assert 'position_stacking' not in config
|
assert 'position_stacking' not in config
|
||||||
|
|
||||||
@ -434,10 +449,7 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non
|
|||||||
assert 'timerange' not in config
|
assert 'timerange' not in config
|
||||||
|
|
||||||
assert 'export' in config
|
assert 'export' in config
|
||||||
assert log_has(
|
assert log_has('Parameter --export detected: {} ...'.format(config['export']), caplog)
|
||||||
'Parameter --export detected: {} ...'.format(config['export']),
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
|
def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
|
||||||
@ -456,11 +468,11 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
|
|||||||
assert 'epochs' in config
|
assert 'epochs' in config
|
||||||
assert int(config['epochs']) == 10
|
assert int(config['epochs']) == 10
|
||||||
assert log_has('Parameter --epochs detected ... Will run Hyperopt with for 10 epochs ...',
|
assert log_has('Parameter --epochs detected ... Will run Hyperopt with for 10 epochs ...',
|
||||||
caplog.record_tuples)
|
caplog)
|
||||||
|
|
||||||
assert 'spaces' in config
|
assert 'spaces' in config
|
||||||
assert config['spaces'] == ['all']
|
assert config['spaces'] == ['all']
|
||||||
assert log_has('Parameter -s/--spaces detected: [\'all\']', caplog.record_tuples)
|
assert log_has('Parameter -s/--spaces detected: [\'all\']', caplog)
|
||||||
assert "runmode" in config
|
assert "runmode" in config
|
||||||
assert config['runmode'] == RunMode.HYPEROPT
|
assert config['runmode'] == RunMode.HYPEROPT
|
||||||
|
|
||||||
@ -470,38 +482,35 @@ def test_check_exchange(default_conf, caplog) -> None:
|
|||||||
default_conf.get('exchange').update({'name': 'BITTREX'})
|
default_conf.get('exchange').update({'name': 'BITTREX'})
|
||||||
assert check_exchange(default_conf)
|
assert check_exchange(default_conf)
|
||||||
assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.",
|
assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.",
|
||||||
caplog.record_tuples)
|
caplog)
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
|
||||||
# Test an officially supported by Freqtrade team exchange
|
# Test an officially supported by Freqtrade team exchange
|
||||||
default_conf.get('exchange').update({'name': 'binance'})
|
default_conf.get('exchange').update({'name': 'binance'})
|
||||||
assert check_exchange(default_conf)
|
assert check_exchange(default_conf)
|
||||||
assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.",
|
assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.",
|
||||||
caplog.record_tuples)
|
caplog)
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
|
||||||
# Test an available exchange, supported by ccxt
|
# Test an available exchange, supported by ccxt
|
||||||
default_conf.get('exchange').update({'name': 'kraken'})
|
default_conf.get('exchange').update({'name': 'kraken'})
|
||||||
assert check_exchange(default_conf)
|
assert check_exchange(default_conf)
|
||||||
assert log_has_re(r"Exchange .* is supported by ccxt and .* not officially supported "
|
assert log_has_re(r"Exchange .* is supported by ccxt and .* not officially supported "
|
||||||
r"by the Freqtrade development team\. .*",
|
r"by the Freqtrade development team\. .*", caplog)
|
||||||
caplog.record_tuples)
|
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
|
||||||
# Test a 'bad' exchange, which known to have serious problems
|
# Test a 'bad' exchange, which known to have serious problems
|
||||||
default_conf.get('exchange').update({'name': 'bitmex'})
|
default_conf.get('exchange').update({'name': 'bitmex'})
|
||||||
assert not check_exchange(default_conf)
|
with pytest.raises(OperationalException,
|
||||||
assert log_has_re(r"Exchange .* is known to not work with the bot yet\. "
|
match=r"Exchange .* is known to not work with the bot yet.*"):
|
||||||
r"Use it only for development and testing purposes\.",
|
check_exchange(default_conf)
|
||||||
caplog.record_tuples)
|
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
|
||||||
# Test a 'bad' exchange with check_for_bad=False
|
# Test a 'bad' exchange with check_for_bad=False
|
||||||
default_conf.get('exchange').update({'name': 'bitmex'})
|
default_conf.get('exchange').update({'name': 'bitmex'})
|
||||||
assert check_exchange(default_conf, False)
|
assert check_exchange(default_conf, False)
|
||||||
assert log_has_re(r"Exchange .* is supported by ccxt and .* not officially supported "
|
assert log_has_re(r"Exchange .* is supported by ccxt and .* not officially supported "
|
||||||
r"by the Freqtrade development team\. .*",
|
r"by the Freqtrade development team\. .*", caplog)
|
||||||
caplog.record_tuples)
|
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
|
||||||
# Test an invalid exchange
|
# Test an invalid exchange
|
||||||
@ -527,7 +536,7 @@ def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None:
|
|||||||
validated_conf = configuration.load_config()
|
validated_conf = configuration.load_config()
|
||||||
|
|
||||||
assert validated_conf.get('verbosity') == 3
|
assert validated_conf.get('verbosity') == 3
|
||||||
assert log_has('Verbosity set to 3', caplog.record_tuples)
|
assert log_has('Verbosity set to 3', caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_set_loggers() -> None:
|
def test_set_loggers() -> None:
|
||||||
@ -593,7 +602,7 @@ def test_load_config_warn_forcebuy(default_conf, mocker, caplog) -> None:
|
|||||||
validated_conf = configuration.load_config()
|
validated_conf = configuration.load_config()
|
||||||
|
|
||||||
assert validated_conf.get('forcebuy_enable')
|
assert validated_conf.get('forcebuy_enable')
|
||||||
assert log_has('`forcebuy` RPC message enabled.', caplog.record_tuples)
|
assert log_has('`forcebuy` RPC message enabled.', caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_validate_default_conf(default_conf) -> None:
|
def test_validate_default_conf(default_conf) -> None:
|
||||||
@ -606,10 +615,45 @@ def test_create_datadir(mocker, default_conf, caplog) -> None:
|
|||||||
|
|
||||||
create_datadir(default_conf, '/foo/bar')
|
create_datadir(default_conf, '/foo/bar')
|
||||||
assert md.call_args[1]['parents'] is True
|
assert md.call_args[1]['parents'] is True
|
||||||
assert log_has('Created data directory: /foo/bar', caplog.record_tuples)
|
assert log_has('Created data directory: /foo/bar', caplog)
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_userdata_dir(mocker, default_conf, caplog) -> None:
|
||||||
|
mocker.patch.object(Path, "is_dir", MagicMock(return_value=False))
|
||||||
|
md = mocker.patch.object(Path, 'mkdir', MagicMock())
|
||||||
|
|
||||||
|
x = create_userdata_dir('/tmp/bar', create_dir=True)
|
||||||
|
assert md.call_count == 7
|
||||||
|
assert md.call_args[1]['parents'] is False
|
||||||
|
assert log_has('Created user-data directory: /tmp/bar', caplog)
|
||||||
|
assert isinstance(x, Path)
|
||||||
|
assert str(x) == "/tmp/bar"
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_userdata_dir_exists(mocker, default_conf, caplog) -> None:
|
||||||
|
mocker.patch.object(Path, "is_dir", MagicMock(return_value=True))
|
||||||
|
md = mocker.patch.object(Path, 'mkdir', MagicMock())
|
||||||
|
|
||||||
|
create_userdata_dir('/tmp/bar')
|
||||||
|
assert md.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_userdata_dir_exists_exception(mocker, default_conf, caplog) -> None:
|
||||||
|
mocker.patch.object(Path, "is_dir", MagicMock(return_value=False))
|
||||||
|
md = mocker.patch.object(Path, 'mkdir', MagicMock())
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException, match=r'Directory `/tmp/bar` does not exist.*'):
|
||||||
|
create_userdata_dir('/tmp/bar', create_dir=False)
|
||||||
|
assert md.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
def test_validate_tsl(default_conf):
|
def test_validate_tsl(default_conf):
|
||||||
|
default_conf['stoploss'] = 0.0
|
||||||
|
with pytest.raises(OperationalException, match='The config stoploss needs to be different '
|
||||||
|
'from 0 to avoid problems with sell orders.'):
|
||||||
|
validate_config_consistency(default_conf)
|
||||||
|
default_conf['stoploss'] = -0.10
|
||||||
|
|
||||||
default_conf['trailing_stop'] = True
|
default_conf['trailing_stop'] = True
|
||||||
default_conf['trailing_stop_positive'] = 0
|
default_conf['trailing_stop_positive'] = 0
|
||||||
default_conf['trailing_stop_positive_offset'] = 0
|
default_conf['trailing_stop_positive_offset'] = 0
|
||||||
@ -618,21 +662,54 @@ def test_validate_tsl(default_conf):
|
|||||||
with pytest.raises(OperationalException,
|
with pytest.raises(OperationalException,
|
||||||
match=r'The config trailing_only_offset_is_reached needs '
|
match=r'The config trailing_only_offset_is_reached needs '
|
||||||
'trailing_stop_positive_offset to be more than 0 in your config.'):
|
'trailing_stop_positive_offset to be more than 0 in your config.'):
|
||||||
configuration = Configuration(Namespace())
|
validate_config_consistency(default_conf)
|
||||||
configuration._validate_config_consistency(default_conf)
|
|
||||||
|
|
||||||
default_conf['trailing_stop_positive_offset'] = 0.01
|
default_conf['trailing_stop_positive_offset'] = 0.01
|
||||||
default_conf['trailing_stop_positive'] = 0.015
|
default_conf['trailing_stop_positive'] = 0.015
|
||||||
with pytest.raises(OperationalException,
|
with pytest.raises(OperationalException,
|
||||||
match=r'The config trailing_stop_positive_offset needs '
|
match=r'The config trailing_stop_positive_offset needs '
|
||||||
'to be greater than trailing_stop_positive_offset in your config.'):
|
'to be greater than trailing_stop_positive in your config.'):
|
||||||
configuration = Configuration(Namespace())
|
validate_config_consistency(default_conf)
|
||||||
configuration._validate_config_consistency(default_conf)
|
|
||||||
|
|
||||||
default_conf['trailing_stop_positive'] = 0.01
|
default_conf['trailing_stop_positive'] = 0.01
|
||||||
default_conf['trailing_stop_positive_offset'] = 0.015
|
default_conf['trailing_stop_positive_offset'] = 0.015
|
||||||
Configuration(Namespace())
|
validate_config_consistency(default_conf)
|
||||||
configuration._validate_config_consistency(default_conf)
|
|
||||||
|
# 0 trailing stop positive - results in "Order would trigger immediately"
|
||||||
|
default_conf['trailing_stop_positive'] = 0
|
||||||
|
default_conf['trailing_stop_positive_offset'] = 0.02
|
||||||
|
default_conf['trailing_only_offset_is_reached'] = False
|
||||||
|
with pytest.raises(OperationalException,
|
||||||
|
match='The config trailing_stop_positive needs to be different from 0 '
|
||||||
|
'to avoid problems with sell orders'):
|
||||||
|
validate_config_consistency(default_conf)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_edge(edge_conf):
|
||||||
|
edge_conf.update({"pairlist": {
|
||||||
|
"method": "VolumePairList",
|
||||||
|
}})
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException,
|
||||||
|
match="Edge and VolumePairList are incompatible, "
|
||||||
|
"Edge will override whatever pairs VolumePairlist selects."):
|
||||||
|
validate_config_consistency(edge_conf)
|
||||||
|
|
||||||
|
edge_conf.update({"pairlist": {
|
||||||
|
"method": "StaticPairList",
|
||||||
|
}})
|
||||||
|
validate_config_consistency(edge_conf)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_config_test_comments() -> None:
|
||||||
|
"""
|
||||||
|
Load config with comments
|
||||||
|
"""
|
||||||
|
config_file = Path(__file__).parents[0] / "config_test_comments.json"
|
||||||
|
print(config_file)
|
||||||
|
conf = load_config_file(str(config_file))
|
||||||
|
|
||||||
|
assert conf
|
||||||
|
|
||||||
|
|
||||||
def test_load_config_default_exchange(all_conf) -> None:
|
def test_load_config_default_exchange(all_conf) -> None:
|
||||||
@ -686,3 +763,111 @@ def test_load_config_default_subkeys(all_conf, keys) -> None:
|
|||||||
validate_config_schema(all_conf)
|
validate_config_schema(all_conf)
|
||||||
assert subkey in all_conf[key]
|
assert subkey in all_conf[key]
|
||||||
assert all_conf[key][subkey] == keys[2]
|
assert all_conf[key][subkey] == keys[2]
|
||||||
|
|
||||||
|
|
||||||
|
def test_pairlist_resolving():
|
||||||
|
arglist = [
|
||||||
|
'download-data',
|
||||||
|
'--pairs', 'ETH/BTC', 'XRP/BTC',
|
||||||
|
'--exchange', 'binance'
|
||||||
|
]
|
||||||
|
|
||||||
|
args = Arguments(arglist, '').get_parsed_arg()
|
||||||
|
|
||||||
|
configuration = Configuration(args)
|
||||||
|
config = configuration.get_config()
|
||||||
|
|
||||||
|
assert config['pairs'] == ['ETH/BTC', 'XRP/BTC']
|
||||||
|
assert config['exchange']['name'] == 'binance'
|
||||||
|
|
||||||
|
|
||||||
|
def test_pairlist_resolving_with_config(mocker, default_conf):
|
||||||
|
patched_configuration_load_config_file(mocker, default_conf)
|
||||||
|
arglist = [
|
||||||
|
'--config', 'config.json',
|
||||||
|
'download-data',
|
||||||
|
]
|
||||||
|
|
||||||
|
args = Arguments(arglist, '').get_parsed_arg()
|
||||||
|
|
||||||
|
configuration = Configuration(args)
|
||||||
|
config = configuration.get_config()
|
||||||
|
|
||||||
|
assert config['pairs'] == default_conf['exchange']['pair_whitelist']
|
||||||
|
assert config['exchange']['name'] == default_conf['exchange']['name']
|
||||||
|
|
||||||
|
# Override pairs
|
||||||
|
arglist = [
|
||||||
|
'--config', 'config.json',
|
||||||
|
'download-data',
|
||||||
|
'--pairs', 'ETH/BTC', 'XRP/BTC',
|
||||||
|
]
|
||||||
|
|
||||||
|
args = Arguments(arglist, '').get_parsed_arg()
|
||||||
|
|
||||||
|
configuration = Configuration(args)
|
||||||
|
config = configuration.get_config()
|
||||||
|
|
||||||
|
assert config['pairs'] == ['ETH/BTC', 'XRP/BTC']
|
||||||
|
assert config['exchange']['name'] == default_conf['exchange']['name']
|
||||||
|
|
||||||
|
|
||||||
|
def test_pairlist_resolving_with_config_pl(mocker, default_conf):
|
||||||
|
patched_configuration_load_config_file(mocker, default_conf)
|
||||||
|
load_mock = mocker.patch("freqtrade.configuration.configuration.json_load",
|
||||||
|
MagicMock(return_value=['XRP/BTC', 'ETH/BTC']))
|
||||||
|
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
|
||||||
|
mocker.patch.object(Path, "open", MagicMock(return_value=MagicMock()))
|
||||||
|
|
||||||
|
arglist = [
|
||||||
|
'--config', 'config.json',
|
||||||
|
'download-data',
|
||||||
|
'--pairs-file', 'pairs.json',
|
||||||
|
]
|
||||||
|
|
||||||
|
args = Arguments(arglist, '').get_parsed_arg()
|
||||||
|
|
||||||
|
configuration = Configuration(args)
|
||||||
|
config = configuration.get_config()
|
||||||
|
|
||||||
|
assert load_mock.call_count == 1
|
||||||
|
assert config['pairs'] == ['ETH/BTC', 'XRP/BTC']
|
||||||
|
assert config['exchange']['name'] == default_conf['exchange']['name']
|
||||||
|
|
||||||
|
|
||||||
|
def test_pairlist_resolving_with_config_pl_not_exists(mocker, default_conf):
|
||||||
|
patched_configuration_load_config_file(mocker, default_conf)
|
||||||
|
mocker.patch("freqtrade.configuration.configuration.json_load",
|
||||||
|
MagicMock(return_value=['XRP/BTC', 'ETH/BTC']))
|
||||||
|
mocker.patch.object(Path, "exists", MagicMock(return_value=False))
|
||||||
|
|
||||||
|
arglist = [
|
||||||
|
'--config', 'config.json',
|
||||||
|
'download-data',
|
||||||
|
'--pairs-file', 'pairs.json',
|
||||||
|
]
|
||||||
|
|
||||||
|
args = Arguments(arglist, '').get_parsed_arg()
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException, match=r"No pairs file found with path.*"):
|
||||||
|
configuration = Configuration(args)
|
||||||
|
configuration.get_config()
|
||||||
|
|
||||||
|
|
||||||
|
def test_pairlist_resolving_fallback(mocker):
|
||||||
|
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
|
||||||
|
mocker.patch.object(Path, "open", MagicMock(return_value=MagicMock()))
|
||||||
|
mocker.patch("freqtrade.configuration.configuration.json_load",
|
||||||
|
MagicMock(return_value=['XRP/BTC', 'ETH/BTC']))
|
||||||
|
arglist = [
|
||||||
|
'download-data',
|
||||||
|
'--exchange', 'binance'
|
||||||
|
]
|
||||||
|
|
||||||
|
args = Arguments(arglist, '').get_parsed_arg()
|
||||||
|
|
||||||
|
configuration = Configuration(args)
|
||||||
|
config = configuration.get_config()
|
||||||
|
|
||||||
|
assert config['pairs'] == ['ETH/BTC', 'XRP/BTC']
|
||||||
|
assert config['exchange']['name'] == 'binance'
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
|||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring
|
||||||
|
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock, PropertyMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@ -21,13 +21,13 @@ def test_parse_args_backtesting(mocker) -> None:
|
|||||||
further argument parsing is done in test_arguments.py
|
further argument parsing is done in test_arguments.py
|
||||||
"""
|
"""
|
||||||
backtesting_mock = mocker.patch('freqtrade.optimize.start_backtesting', MagicMock())
|
backtesting_mock = mocker.patch('freqtrade.optimize.start_backtesting', MagicMock())
|
||||||
|
backtesting_mock.__name__ = PropertyMock("start_backtesting")
|
||||||
# it's sys.exit(0) at the end of backtesting
|
# it's sys.exit(0) at the end of backtesting
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
main(['backtesting'])
|
main(['backtesting'])
|
||||||
assert backtesting_mock.call_count == 1
|
assert backtesting_mock.call_count == 1
|
||||||
call_args = backtesting_mock.call_args[0][0]
|
call_args = backtesting_mock.call_args[0][0]
|
||||||
assert call_args.config == ['config.json']
|
assert call_args.config == ['config.json']
|
||||||
assert call_args.live is False
|
|
||||||
assert call_args.verbosity == 0
|
assert call_args.verbosity == 0
|
||||||
assert call_args.subparser == 'backtesting'
|
assert call_args.subparser == 'backtesting'
|
||||||
assert call_args.func is not None
|
assert call_args.func is not None
|
||||||
@ -36,6 +36,7 @@ def test_parse_args_backtesting(mocker) -> None:
|
|||||||
|
|
||||||
def test_main_start_hyperopt(mocker) -> None:
|
def test_main_start_hyperopt(mocker) -> None:
|
||||||
hyperopt_mock = mocker.patch('freqtrade.optimize.start_hyperopt', MagicMock())
|
hyperopt_mock = mocker.patch('freqtrade.optimize.start_hyperopt', MagicMock())
|
||||||
|
hyperopt_mock.__name__ = PropertyMock("start_hyperopt")
|
||||||
# it's sys.exit(0) at the end of hyperopt
|
# it's sys.exit(0) at the end of hyperopt
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
main(['hyperopt'])
|
main(['hyperopt'])
|
||||||
@ -60,8 +61,8 @@ def test_main_fatal_exception(mocker, default_conf, caplog) -> None:
|
|||||||
# Test Main + the KeyboardInterrupt exception
|
# Test Main + the KeyboardInterrupt exception
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
main(args)
|
main(args)
|
||||||
assert log_has('Using config: config.json.example ...', caplog.record_tuples)
|
assert log_has('Using config: config.json.example ...', caplog)
|
||||||
assert log_has('Fatal exception!', caplog.record_tuples)
|
assert log_has('Fatal exception!', caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None:
|
def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None:
|
||||||
@ -77,8 +78,8 @@ def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None:
|
|||||||
# Test Main + the KeyboardInterrupt exception
|
# Test Main + the KeyboardInterrupt exception
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
main(args)
|
main(args)
|
||||||
assert log_has('Using config: config.json.example ...', caplog.record_tuples)
|
assert log_has('Using config: config.json.example ...', caplog)
|
||||||
assert log_has('SIGINT received, aborting ...', caplog.record_tuples)
|
assert log_has('SIGINT received, aborting ...', caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_main_operational_exception(mocker, default_conf, caplog) -> None:
|
def test_main_operational_exception(mocker, default_conf, caplog) -> None:
|
||||||
@ -97,8 +98,8 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None:
|
|||||||
# Test Main + the KeyboardInterrupt exception
|
# Test Main + the KeyboardInterrupt exception
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
main(args)
|
main(args)
|
||||||
assert log_has('Using config: config.json.example ...', caplog.record_tuples)
|
assert log_has('Using config: config.json.example ...', caplog)
|
||||||
assert log_has('Oh snap!', caplog.record_tuples)
|
assert log_has('Oh snap!', caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_main_reload_conf(mocker, default_conf, caplog) -> None:
|
def test_main_reload_conf(mocker, default_conf, caplog) -> None:
|
||||||
@ -121,7 +122,7 @@ def test_main_reload_conf(mocker, default_conf, caplog) -> None:
|
|||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
main(['-c', 'config.json.example'])
|
main(['-c', 'config.json.example'])
|
||||||
|
|
||||||
assert log_has('Using config: config.json.example ...', caplog.record_tuples)
|
assert log_has('Using config: config.json.example ...', caplog)
|
||||||
assert worker_mock.call_count == 4
|
assert worker_mock.call_count == 4
|
||||||
assert reconfigure_mock.call_count == 1
|
assert reconfigure_mock.call_count == 1
|
||||||
assert isinstance(worker.freqtrade, FreqtradeBot)
|
assert isinstance(worker.freqtrade, FreqtradeBot)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# pragma pylint: disable=missing-docstring,C0103
|
# pragma pylint: disable=missing-docstring,C0103
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
from pathlib import Path
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
from freqtrade.data.converter import parse_ticker_dataframe
|
from freqtrade.data.converter import parse_ticker_dataframe
|
||||||
@ -34,12 +35,12 @@ def test_datesarray_to_datetimearray(ticker_history_list):
|
|||||||
def test_file_dump_json(mocker) -> None:
|
def test_file_dump_json(mocker) -> None:
|
||||||
file_open = mocker.patch('freqtrade.misc.open', MagicMock())
|
file_open = mocker.patch('freqtrade.misc.open', MagicMock())
|
||||||
json_dump = mocker.patch('rapidjson.dump', MagicMock())
|
json_dump = mocker.patch('rapidjson.dump', MagicMock())
|
||||||
file_dump_json('somefile', [1, 2, 3])
|
file_dump_json(Path('somefile'), [1, 2, 3])
|
||||||
assert file_open.call_count == 1
|
assert file_open.call_count == 1
|
||||||
assert json_dump.call_count == 1
|
assert json_dump.call_count == 1
|
||||||
file_open = mocker.patch('freqtrade.misc.gzip.open', MagicMock())
|
file_open = mocker.patch('freqtrade.misc.gzip.open', MagicMock())
|
||||||
json_dump = mocker.patch('rapidjson.dump', MagicMock())
|
json_dump = mocker.patch('rapidjson.dump', MagicMock())
|
||||||
file_dump_json('somefile', [1, 2, 3], True)
|
file_dump_json(Path('somefile'), [1, 2, 3], True)
|
||||||
assert file_open.call_count == 1
|
assert file_open.call_count == 1
|
||||||
assert json_dump.call_count == 1
|
assert json_dump.call_count == 1
|
||||||
|
|
||||||
|
@ -151,7 +151,7 @@ def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee, caplog):
|
|||||||
assert trade.close_date is None
|
assert trade.close_date is None
|
||||||
assert log_has("LIMIT_BUY has been fulfilled for Trade(id=2, "
|
assert log_has("LIMIT_BUY has been fulfilled for Trade(id=2, "
|
||||||
"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001099, open_since=closed).",
|
"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001099, open_since=closed).",
|
||||||
caplog.record_tuples)
|
caplog)
|
||||||
|
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
trade.open_order_id = 'something'
|
trade.open_order_id = 'something'
|
||||||
@ -162,7 +162,7 @@ def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee, caplog):
|
|||||||
assert trade.close_date is not None
|
assert trade.close_date is not None
|
||||||
assert log_has("LIMIT_SELL has been fulfilled for Trade(id=2, "
|
assert log_has("LIMIT_SELL has been fulfilled for Trade(id=2, "
|
||||||
"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001099, open_since=closed).",
|
"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001099, open_since=closed).",
|
||||||
caplog.record_tuples)
|
caplog)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
@ -184,7 +184,7 @@ def test_update_market_order(market_buy_order, market_sell_order, fee, caplog):
|
|||||||
assert trade.close_date is None
|
assert trade.close_date is None
|
||||||
assert log_has("MARKET_BUY has been fulfilled for Trade(id=1, "
|
assert log_has("MARKET_BUY has been fulfilled for Trade(id=1, "
|
||||||
"pair=ETH/BTC, amount=91.99181073, open_rate=0.00004099, open_since=closed).",
|
"pair=ETH/BTC, amount=91.99181073, open_rate=0.00004099, open_since=closed).",
|
||||||
caplog.record_tuples)
|
caplog)
|
||||||
|
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
trade.open_order_id = 'something'
|
trade.open_order_id = 'something'
|
||||||
@ -195,7 +195,7 @@ def test_update_market_order(market_buy_order, market_sell_order, fee, caplog):
|
|||||||
assert trade.close_date is not None
|
assert trade.close_date is not None
|
||||||
assert log_has("MARKET_SELL has been fulfilled for Trade(id=1, "
|
assert log_has("MARKET_SELL has been fulfilled for Trade(id=1, "
|
||||||
"pair=ETH/BTC, amount=91.99181073, open_rate=0.00004099, open_since=closed).",
|
"pair=ETH/BTC, amount=91.99181073, open_rate=0.00004099, open_since=closed).",
|
||||||
caplog.record_tuples)
|
caplog)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
@ -558,10 +558,9 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
|
|||||||
assert trade.ticker_interval is None
|
assert trade.ticker_interval is None
|
||||||
assert trade.stoploss_order_id is None
|
assert trade.stoploss_order_id is None
|
||||||
assert trade.stoploss_last_update is None
|
assert trade.stoploss_last_update is None
|
||||||
assert log_has("trying trades_bak1", caplog.record_tuples)
|
assert log_has("trying trades_bak1", caplog)
|
||||||
assert log_has("trying trades_bak2", caplog.record_tuples)
|
assert log_has("trying trades_bak2", caplog)
|
||||||
assert log_has("Running database migration - backup available as trades_bak2",
|
assert log_has("Running database migration - backup available as trades_bak2", caplog)
|
||||||
caplog.record_tuples)
|
|
||||||
|
|
||||||
|
|
||||||
def test_migrate_mid_state(mocker, default_conf, fee, caplog):
|
def test_migrate_mid_state(mocker, default_conf, fee, caplog):
|
||||||
@ -621,9 +620,8 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog):
|
|||||||
assert trade.max_rate == 0.0
|
assert trade.max_rate == 0.0
|
||||||
assert trade.stop_loss == 0.0
|
assert trade.stop_loss == 0.0
|
||||||
assert trade.initial_stop_loss == 0.0
|
assert trade.initial_stop_loss == 0.0
|
||||||
assert log_has("trying trades_bak0", caplog.record_tuples)
|
assert log_has("trying trades_bak0", caplog)
|
||||||
assert log_has("Running database migration - backup available as trades_bak0",
|
assert log_has("Running database migration - backup available as trades_bak0", caplog)
|
||||||
caplog.record_tuples)
|
|
||||||
|
|
||||||
|
|
||||||
def test_adjust_stop_loss(fee):
|
def test_adjust_stop_loss(fee):
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
|
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
from pathlib import Path
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import plotly.graph_objects as go
|
import plotly.graph_objects as go
|
||||||
from plotly.subplots import make_subplots
|
from plotly.subplots import make_subplots
|
||||||
|
|
||||||
from freqtrade.configuration import Arguments, TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.data import history
|
from freqtrade.data import history
|
||||||
from freqtrade.data.btanalysis import create_cum_profit, load_backtest_data
|
from freqtrade.data.btanalysis import create_cum_profit, load_backtest_data
|
||||||
from freqtrade.plot.plotting import (add_indicators, add_profit,
|
from freqtrade.plot.plotting import (add_indicators, add_profit,
|
||||||
@ -50,7 +51,7 @@ def test_init_plotscript(default_conf, mocker):
|
|||||||
assert "pairs" in ret
|
assert "pairs" in ret
|
||||||
assert "strategy" in ret
|
assert "strategy" in ret
|
||||||
|
|
||||||
default_conf['pairs'] = "POWR/BTC,XLM/BTC"
|
default_conf['pairs'] = ["POWR/BTC", "XLM/BTC"]
|
||||||
ret = init_plotscript(default_conf)
|
ret = init_plotscript(default_conf)
|
||||||
assert "tickers" in ret
|
assert "tickers" in ret
|
||||||
assert "POWR/BTC" in ret["tickers"]
|
assert "POWR/BTC" in ret["tickers"]
|
||||||
@ -87,7 +88,7 @@ def test_add_indicators(default_conf, caplog):
|
|||||||
# No indicator found
|
# No indicator found
|
||||||
fig3 = add_indicators(fig=deepcopy(fig), row=3, indicators=['no_indicator'], data=data)
|
fig3 = add_indicators(fig=deepcopy(fig), row=3, indicators=['no_indicator'], data=data)
|
||||||
assert fig == fig3
|
assert fig == fig3
|
||||||
assert log_has_re(r'Indicator "no_indicator" ignored\..*', caplog.record_tuples)
|
assert log_has_re(r'Indicator "no_indicator" ignored\..*', caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_plot_trades(caplog):
|
def test_plot_trades(caplog):
|
||||||
@ -95,7 +96,7 @@ def test_plot_trades(caplog):
|
|||||||
# nothing happens when no trades are available
|
# nothing happens when no trades are available
|
||||||
fig = plot_trades(fig1, None)
|
fig = plot_trades(fig1, None)
|
||||||
assert fig == fig1
|
assert fig == fig1
|
||||||
assert log_has("No trades found.", caplog.record_tuples)
|
assert log_has("No trades found.", caplog)
|
||||||
pair = "ADA/BTC"
|
pair = "ADA/BTC"
|
||||||
filename = history.make_testdata_path(None) / "backtest-result_test.json"
|
filename = history.make_testdata_path(None) / "backtest-result_test.json"
|
||||||
trades = load_backtest_data(filename)
|
trades = load_backtest_data(filename)
|
||||||
@ -150,8 +151,8 @@ def test_generate_candlestick_graph_no_signals_no_trades(default_conf, mocker, c
|
|||||||
assert row_mock.call_count == 2
|
assert row_mock.call_count == 2
|
||||||
assert trades_mock.call_count == 1
|
assert trades_mock.call_count == 1
|
||||||
|
|
||||||
assert log_has("No buy-signals found.", caplog.record_tuples)
|
assert log_has("No buy-signals found.", caplog)
|
||||||
assert log_has("No sell-signals found.", caplog.record_tuples)
|
assert log_has("No sell-signals found.", caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_generate_candlestick_graph_no_trades(default_conf, mocker):
|
def test_generate_candlestick_graph_no_trades(default_conf, mocker):
|
||||||
@ -209,18 +210,21 @@ def test_generate_Plot_filename():
|
|||||||
def test_generate_plot_file(mocker, caplog):
|
def test_generate_plot_file(mocker, caplog):
|
||||||
fig = generage_empty_figure()
|
fig = generage_empty_figure()
|
||||||
plot_mock = mocker.patch("freqtrade.plot.plotting.plot", MagicMock())
|
plot_mock = mocker.patch("freqtrade.plot.plotting.plot", MagicMock())
|
||||||
store_plot_file(fig, filename="freqtrade-plot-UNITTEST_BTC-5m.html")
|
store_plot_file(fig, filename="freqtrade-plot-UNITTEST_BTC-5m.html",
|
||||||
|
directory=Path("user_data/plots"))
|
||||||
|
|
||||||
assert plot_mock.call_count == 1
|
assert plot_mock.call_count == 1
|
||||||
assert plot_mock.call_args[0][0] == fig
|
assert plot_mock.call_args[0][0] == fig
|
||||||
assert (plot_mock.call_args_list[0][1]['filename']
|
assert (plot_mock.call_args_list[0][1]['filename']
|
||||||
== "user_data/plots/freqtrade-plot-UNITTEST_BTC-5m.html")
|
== "user_data/plots/freqtrade-plot-UNITTEST_BTC-5m.html")
|
||||||
|
assert log_has("Stored plot as user_data/plots/freqtrade-plot-UNITTEST_BTC-5m.html",
|
||||||
|
caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_add_profit():
|
def test_add_profit():
|
||||||
filename = history.make_testdata_path(None) / "backtest-result_test.json"
|
filename = history.make_testdata_path(None) / "backtest-result_test.json"
|
||||||
bt_data = load_backtest_data(filename)
|
bt_data = load_backtest_data(filename)
|
||||||
timerange = Arguments.parse_timerange("20180110-20180112")
|
timerange = TimeRange.parse_timerange("20180110-20180112")
|
||||||
|
|
||||||
df = history.load_pair_history(pair="POWR/BTC", ticker_interval='5m',
|
df = history.load_pair_history(pair="POWR/BTC", ticker_interval='5m',
|
||||||
datadir=None, timerange=timerange)
|
datadir=None, timerange=timerange)
|
||||||
@ -240,7 +244,7 @@ def test_add_profit():
|
|||||||
def test_generate_profit_graph():
|
def test_generate_profit_graph():
|
||||||
filename = history.make_testdata_path(None) / "backtest-result_test.json"
|
filename = history.make_testdata_path(None) / "backtest-result_test.json"
|
||||||
trades = load_backtest_data(filename)
|
trades = load_backtest_data(filename)
|
||||||
timerange = Arguments.parse_timerange("20180110-20180112")
|
timerange = TimeRange.parse_timerange("20180110-20180112")
|
||||||
pairs = ["POWR/BTC", "XLM/BTC"]
|
pairs = ["POWR/BTC", "XLM/BTC"]
|
||||||
|
|
||||||
tickers = history.load_data(datadir=None,
|
tickers = history.load_data(datadir=None,
|
||||||
|
28
freqtrade/tests/test_timerange.py
Normal file
28
freqtrade/tests/test_timerange.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring, C0103
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from freqtrade.configuration import TimeRange
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_timerange_incorrect() -> None:
|
||||||
|
assert TimeRange(None, 'line', 0, -200) == TimeRange.parse_timerange('-200')
|
||||||
|
assert TimeRange('line', None, 200, 0) == TimeRange.parse_timerange('200-')
|
||||||
|
assert TimeRange('index', 'index', 200, 500) == TimeRange.parse_timerange('200-500')
|
||||||
|
|
||||||
|
assert TimeRange('date', None, 1274486400, 0) == TimeRange.parse_timerange('20100522-')
|
||||||
|
assert TimeRange(None, 'date', 0, 1274486400) == TimeRange.parse_timerange('-20100522')
|
||||||
|
timerange = TimeRange.parse_timerange('20100522-20150730')
|
||||||
|
assert timerange == TimeRange('date', 'date', 1274486400, 1438214400)
|
||||||
|
|
||||||
|
# Added test for unix timestamp - BTC genesis date
|
||||||
|
assert TimeRange('date', None, 1231006505, 0) == TimeRange.parse_timerange('1231006505-')
|
||||||
|
assert TimeRange(None, 'date', 0, 1233360000) == TimeRange.parse_timerange('-1233360000')
|
||||||
|
timerange = TimeRange.parse_timerange('1231006505-1233360000')
|
||||||
|
assert TimeRange('date', 'date', 1231006505, 1233360000) == timerange
|
||||||
|
|
||||||
|
# TODO: Find solution for the following case (passing timestamp in ms)
|
||||||
|
timerange = TimeRange.parse_timerange('1231006505000-1233360000000')
|
||||||
|
assert TimeRange('date', 'date', 1231006505, 1233360000) != timerange
|
||||||
|
|
||||||
|
with pytest.raises(Exception, match=r'Incorrect syntax.*'):
|
||||||
|
TimeRange.parse_timerange('-')
|
@ -1,8 +1,13 @@
|
|||||||
from freqtrade.utils import setup_utils_configuration, start_list_exchanges
|
|
||||||
from freqtrade.tests.conftest import get_args
|
|
||||||
from freqtrade.state import RunMode
|
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import MagicMock, PropertyMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from freqtrade.state import RunMode
|
||||||
|
from freqtrade.tests.conftest import get_args, log_has, patch_exchange
|
||||||
|
from freqtrade.utils import (setup_utils_configuration, start_create_userdir,
|
||||||
|
start_download_data, start_list_exchanges)
|
||||||
|
|
||||||
|
|
||||||
def test_setup_utils_configuration():
|
def test_setup_utils_configuration():
|
||||||
@ -40,3 +45,110 @@ def test_list_exchanges(capsys):
|
|||||||
assert not re.match(r"Exchanges supported by ccxt and available.*", captured.out)
|
assert not re.match(r"Exchanges supported by ccxt and available.*", captured.out)
|
||||||
assert re.search(r"^binance$", captured.out, re.MULTILINE)
|
assert re.search(r"^binance$", captured.out, re.MULTILINE)
|
||||||
assert re.search(r"^bittrex$", captured.out, re.MULTILINE)
|
assert re.search(r"^bittrex$", captured.out, re.MULTILINE)
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_datadir_failed(caplog):
|
||||||
|
|
||||||
|
args = [
|
||||||
|
"create-userdir",
|
||||||
|
]
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
start_create_userdir(get_args(args))
|
||||||
|
assert log_has("`create-userdir` requires --userdir to be set.", caplog)
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_datadir(caplog, mocker):
|
||||||
|
cud = mocker.patch("freqtrade.utils.create_userdata_dir", MagicMock())
|
||||||
|
args = [
|
||||||
|
"create-userdir",
|
||||||
|
"--userdir",
|
||||||
|
"/temp/freqtrade/test"
|
||||||
|
]
|
||||||
|
start_create_userdir(get_args(args))
|
||||||
|
|
||||||
|
assert cud.call_count == 1
|
||||||
|
assert len(caplog.record_tuples) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_download_data(mocker, markets, caplog):
|
||||||
|
dl_mock = mocker.patch('freqtrade.utils.download_pair_history', MagicMock())
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)
|
||||||
|
)
|
||||||
|
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
|
||||||
|
mocker.patch.object(Path, "unlink", MagicMock())
|
||||||
|
|
||||||
|
args = [
|
||||||
|
"download-data",
|
||||||
|
"--exchange", "binance",
|
||||||
|
"--pairs", "ETH/BTC", "XRP/BTC",
|
||||||
|
"--erase",
|
||||||
|
]
|
||||||
|
start_download_data(get_args(args))
|
||||||
|
|
||||||
|
assert dl_mock.call_count == 4
|
||||||
|
assert dl_mock.call_args[1]['timerange'].starttype is None
|
||||||
|
assert dl_mock.call_args[1]['timerange'].stoptype is None
|
||||||
|
assert log_has("Deleting existing data for pair ETH/BTC, interval 1m.", caplog)
|
||||||
|
assert log_has("Downloading pair ETH/BTC, interval 1m.", caplog)
|
||||||
|
|
||||||
|
|
||||||
|
def test_download_data_days(mocker, markets, caplog):
|
||||||
|
dl_mock = mocker.patch('freqtrade.utils.download_pair_history', MagicMock())
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)
|
||||||
|
)
|
||||||
|
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
|
||||||
|
mocker.patch.object(Path, "unlink", MagicMock())
|
||||||
|
|
||||||
|
args = [
|
||||||
|
"download-data",
|
||||||
|
"--exchange", "binance",
|
||||||
|
"--pairs", "ETH/BTC", "XRP/BTC",
|
||||||
|
"--days", "20",
|
||||||
|
]
|
||||||
|
|
||||||
|
start_download_data(get_args(args))
|
||||||
|
|
||||||
|
assert dl_mock.call_count == 4
|
||||||
|
assert dl_mock.call_args[1]['timerange'].starttype == 'date'
|
||||||
|
|
||||||
|
assert log_has("Downloading pair ETH/BTC, interval 1m.", caplog)
|
||||||
|
|
||||||
|
|
||||||
|
def test_download_data_no_markets(mocker, caplog):
|
||||||
|
dl_mock = mocker.patch('freqtrade.utils.download_pair_history', MagicMock())
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})
|
||||||
|
)
|
||||||
|
args = [
|
||||||
|
"download-data",
|
||||||
|
"--exchange", "binance",
|
||||||
|
"--pairs", "ETH/BTC", "XRP/BTC",
|
||||||
|
]
|
||||||
|
start_download_data(get_args(args))
|
||||||
|
|
||||||
|
assert dl_mock.call_count == 0
|
||||||
|
assert log_has("Skipping pair ETH/BTC...", caplog)
|
||||||
|
assert log_has("Pairs [ETH/BTC,XRP/BTC] not available on exchange binance.", caplog)
|
||||||
|
|
||||||
|
|
||||||
|
def test_download_data_keyboardInterrupt(mocker, caplog, markets):
|
||||||
|
dl_mock = mocker.patch('freqtrade.utils.download_pair_history',
|
||||||
|
MagicMock(side_effect=KeyboardInterrupt))
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)
|
||||||
|
)
|
||||||
|
args = [
|
||||||
|
"download-data",
|
||||||
|
"--exchange", "binance",
|
||||||
|
"--pairs", "ETH/BTC", "XRP/BTC",
|
||||||
|
]
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
start_download_data(get_args(args))
|
||||||
|
|
||||||
|
assert dl_mock.call_count == 1
|
||||||
|
@ -1,11 +1,17 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
from freqtrade.configuration import Configuration
|
import arrow
|
||||||
from freqtrade.exchange import available_exchanges
|
|
||||||
from freqtrade.state import RunMode
|
|
||||||
|
|
||||||
|
from freqtrade.configuration import Configuration, TimeRange
|
||||||
|
from freqtrade.configuration.directory_operations import create_userdata_dir
|
||||||
|
from freqtrade.data.history import download_pair_history
|
||||||
|
from freqtrade.exchange import available_exchanges
|
||||||
|
from freqtrade.resolvers import ExchangeResolver
|
||||||
|
from freqtrade.state import RunMode
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -17,7 +23,7 @@ def setup_utils_configuration(args: Namespace, method: RunMode) -> Dict[str, Any
|
|||||||
:return: Configuration
|
:return: Configuration
|
||||||
"""
|
"""
|
||||||
configuration = Configuration(args, method)
|
configuration = Configuration(args, method)
|
||||||
config = configuration.load_config()
|
config = configuration.get_config()
|
||||||
|
|
||||||
config['exchange']['dry_run'] = True
|
config['exchange']['dry_run'] = True
|
||||||
# Ensure we do not use Exchange credentials
|
# Ensure we do not use Exchange credentials
|
||||||
@ -39,3 +45,69 @@ def start_list_exchanges(args: Namespace) -> None:
|
|||||||
else:
|
else:
|
||||||
print(f"Exchanges supported by ccxt and available for Freqtrade: "
|
print(f"Exchanges supported by ccxt and available for Freqtrade: "
|
||||||
f"{', '.join(available_exchanges())}")
|
f"{', '.join(available_exchanges())}")
|
||||||
|
|
||||||
|
|
||||||
|
def start_create_userdir(args: Namespace) -> None:
|
||||||
|
"""
|
||||||
|
Create "user_data" directory to contain user data strategies, hyperopts, ...)
|
||||||
|
:param args: Cli args from Arguments()
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
if "user_data_dir" in args and args.user_data_dir:
|
||||||
|
create_userdata_dir(args.user_data_dir, create_dir=True)
|
||||||
|
else:
|
||||||
|
logger.warning("`create-userdir` requires --userdir to be set.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def start_download_data(args: Namespace) -> None:
|
||||||
|
"""
|
||||||
|
Download data (former download_backtest_data.py script)
|
||||||
|
"""
|
||||||
|
config = setup_utils_configuration(args, RunMode.OTHER)
|
||||||
|
|
||||||
|
timerange = TimeRange()
|
||||||
|
if 'days' in config:
|
||||||
|
time_since = arrow.utcnow().shift(days=-config['days']).strftime("%Y%m%d")
|
||||||
|
timerange = TimeRange.parse_timerange(f'{time_since}-')
|
||||||
|
|
||||||
|
dl_path = Path(config['datadir'])
|
||||||
|
logger.info(f'About to download pairs: {config["pairs"]}, '
|
||||||
|
f'intervals: {config["timeframes"]} to {dl_path}')
|
||||||
|
|
||||||
|
pairs_not_available = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Init exchange
|
||||||
|
exchange = ExchangeResolver(config['exchange']['name'], config).exchange
|
||||||
|
|
||||||
|
for pair in config["pairs"]:
|
||||||
|
if pair not in exchange.markets:
|
||||||
|
pairs_not_available.append(pair)
|
||||||
|
logger.info(f"Skipping pair {pair}...")
|
||||||
|
continue
|
||||||
|
for ticker_interval in config["timeframes"]:
|
||||||
|
pair_print = pair.replace('/', '_')
|
||||||
|
filename = f'{pair_print}-{ticker_interval}.json'
|
||||||
|
dl_file = dl_path.joinpath(filename)
|
||||||
|
if config.get("erase") and dl_file.exists():
|
||||||
|
logger.info(
|
||||||
|
f'Deleting existing data for pair {pair}, interval {ticker_interval}.')
|
||||||
|
dl_file.unlink()
|
||||||
|
|
||||||
|
logger.info(f'Downloading pair {pair}, interval {ticker_interval}.')
|
||||||
|
download_pair_history(datadir=dl_path, exchange=exchange,
|
||||||
|
pair=pair, ticker_interval=str(ticker_interval),
|
||||||
|
timerange=timerange)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
sys.exit("SIGINT received, aborting ...")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if pairs_not_available:
|
||||||
|
logger.info(
|
||||||
|
f"Pairs [{','.join(pairs_not_available)}] not available "
|
||||||
|
f"on exchange {config['exchange']['name']}.")
|
||||||
|
|
||||||
|
# configuration.resolve_pairs_list()
|
||||||
|
print(config)
|
||||||
|
@ -127,11 +127,10 @@ class Worker(object):
|
|||||||
time.sleep(duration)
|
time.sleep(duration)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _process(self) -> bool:
|
def _process(self) -> None:
|
||||||
logger.debug("========================================")
|
logger.debug("========================================")
|
||||||
state_changed = False
|
|
||||||
try:
|
try:
|
||||||
state_changed = self.freqtrade.process()
|
self.freqtrade.process()
|
||||||
except TemporaryError as error:
|
except TemporaryError as error:
|
||||||
logger.warning(f"Error: {error}, retrying in {constants.RETRY_TIMEOUT} seconds...")
|
logger.warning(f"Error: {error}, retrying in {constants.RETRY_TIMEOUT} seconds...")
|
||||||
time.sleep(constants.RETRY_TIMEOUT)
|
time.sleep(constants.RETRY_TIMEOUT)
|
||||||
@ -144,10 +143,6 @@ class Worker(object):
|
|||||||
})
|
})
|
||||||
logger.exception('OperationalException. Stopping trader ...')
|
logger.exception('OperationalException. Stopping trader ...')
|
||||||
self.freqtrade.state = State.STOPPED
|
self.freqtrade.state = State.STOPPED
|
||||||
# TODO: The return value of _process() is not used apart tests
|
|
||||||
# and should (could) be eliminated later. See PR #1689.
|
|
||||||
# state_changed = True
|
|
||||||
return state_changed
|
|
||||||
|
|
||||||
def _reconfigure(self) -> None:
|
def _reconfigure(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
# requirements without requirements installable via conda
|
# requirements without requirements installable via conda
|
||||||
# mainly used for Raspberry pi installs
|
# mainly used for Raspberry pi installs
|
||||||
ccxt==1.18.992
|
ccxt==1.18.1068
|
||||||
SQLAlchemy==1.3.6
|
SQLAlchemy==1.3.7
|
||||||
python-telegram-bot==11.1.0
|
python-telegram-bot==11.1.0
|
||||||
arrow==0.14.3
|
arrow==0.14.5
|
||||||
cachetools==3.1.1
|
cachetools==3.1.1
|
||||||
requests==2.22.0
|
requests==2.22.0
|
||||||
urllib3==1.24.2 # pyup: ignore
|
urllib3==1.25.3
|
||||||
wrapt==1.11.2
|
wrapt==1.11.2
|
||||||
scikit-learn==0.21.2
|
scikit-learn==0.21.3
|
||||||
joblib==0.13.2
|
joblib==0.13.2
|
||||||
jsonschema==3.0.1
|
jsonschema==3.0.2
|
||||||
TA-Lib==0.4.17
|
TA-Lib==0.4.17
|
||||||
tabulate==0.8.3
|
tabulate==0.8.3
|
||||||
coinmarketcap==5.0.3
|
coinmarketcap==5.0.3
|
||||||
@ -20,13 +20,16 @@ scikit-optimize==0.5.2
|
|||||||
filelock==3.0.12
|
filelock==3.0.12
|
||||||
|
|
||||||
# find first, C search in arrays
|
# find first, C search in arrays
|
||||||
py_find_1st==1.1.3
|
py_find_1st==1.1.4
|
||||||
|
|
||||||
#Load ticker files 30% faster
|
#Load ticker files 30% faster
|
||||||
python-rapidjson==0.7.2
|
python-rapidjson==0.8.0
|
||||||
|
|
||||||
# Notify systemd
|
# Notify systemd
|
||||||
sdnotify==0.3.2
|
sdnotify==0.3.2
|
||||||
|
|
||||||
# Api server
|
# Api server
|
||||||
flask==1.1.1
|
flask==1.1.1
|
||||||
|
|
||||||
|
# Support for colorized terminal output
|
||||||
|
colorama==0.4.1
|
||||||
|
@ -2,13 +2,13 @@
|
|||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
-r requirements-plot.txt
|
-r requirements-plot.txt
|
||||||
|
|
||||||
|
coveralls==1.8.2
|
||||||
flake8==3.7.8
|
flake8==3.7.8
|
||||||
flake8-type-annotations==0.1.0
|
flake8-type-annotations==0.1.0
|
||||||
flake8-tidy-imports==2.0.0
|
flake8-tidy-imports==2.0.0
|
||||||
pytest==5.0.1
|
mypy==0.720
|
||||||
pytest-mock==1.10.4
|
pytest==5.1.0
|
||||||
pytest-asyncio==0.10.0
|
pytest-asyncio==0.10.0
|
||||||
pytest-cov==2.7.1
|
pytest-cov==2.7.1
|
||||||
|
pytest-mock==1.10.4
|
||||||
pytest-random-order==1.0.4
|
pytest-random-order==1.0.4
|
||||||
coveralls==1.8.1
|
|
||||||
mypy==0.720
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Include all requirements to run the bot.
|
# Include all requirements to run the bot.
|
||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
|
|
||||||
plotly==4.0.0
|
plotly==4.1.0
|
||||||
|
|
||||||
|
@ -3,4 +3,4 @@
|
|||||||
|
|
||||||
numpy==1.17.0
|
numpy==1.17.0
|
||||||
pandas==0.25.0
|
pandas==0.25.0
|
||||||
scipy==1.3.0
|
scipy==1.3.1
|
||||||
|
@ -1,143 +1,11 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
|
||||||
This script generates json files with pairs history data
|
|
||||||
"""
|
|
||||||
import arrow
|
|
||||||
import json
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any, Dict, List
|
|
||||||
|
|
||||||
from freqtrade.configuration import Arguments, TimeRange
|
|
||||||
from freqtrade.configuration import Configuration
|
|
||||||
from freqtrade.configuration.arguments import ARGS_DOWNLOADER
|
|
||||||
from freqtrade.configuration.check_exchange import check_exchange
|
|
||||||
from freqtrade.data.history import download_pair_history
|
|
||||||
from freqtrade.exchange import Exchange
|
|
||||||
from freqtrade.misc import deep_merge_dicts
|
|
||||||
|
|
||||||
import logging
|
print("This script has been integrated into freqtrade "
|
||||||
|
"and its functionality is available by calling `freqtrade download-data`.")
|
||||||
|
print("Please check the documentation on https://www.freqtrade.io/en/latest/backtesting/ "
|
||||||
|
"for details.")
|
||||||
|
|
||||||
logger = logging.getLogger('download_backtest_data')
|
sys.exit(1)
|
||||||
|
|
||||||
DEFAULT_DL_PATH = 'user_data/data'
|
|
||||||
|
|
||||||
# Do not read the default config if config is not specified
|
|
||||||
# in the command line options explicitely
|
|
||||||
arguments = Arguments(sys.argv[1:], 'Download backtest data',
|
|
||||||
no_default_config=True)
|
|
||||||
arguments._build_args(optionlist=ARGS_DOWNLOADER)
|
|
||||||
args = arguments._parse_args()
|
|
||||||
|
|
||||||
# Use bittrex as default exchange
|
|
||||||
exchange_name = args.exchange or 'bittrex'
|
|
||||||
|
|
||||||
pairs: List = []
|
|
||||||
|
|
||||||
configuration = Configuration(args)
|
|
||||||
config: Dict[str, Any] = {}
|
|
||||||
|
|
||||||
if args.config:
|
|
||||||
# Now expecting a list of config filenames here, not a string
|
|
||||||
for path in args.config:
|
|
||||||
logger.info(f"Using config: {path}...")
|
|
||||||
# Merge config options, overwriting old values
|
|
||||||
config = deep_merge_dicts(configuration._load_config_file(path), config)
|
|
||||||
|
|
||||||
config['stake_currency'] = ''
|
|
||||||
# Ensure we do not use Exchange credentials
|
|
||||||
config['exchange']['dry_run'] = True
|
|
||||||
config['exchange']['key'] = ''
|
|
||||||
config['exchange']['secret'] = ''
|
|
||||||
|
|
||||||
pairs = config['exchange']['pair_whitelist']
|
|
||||||
|
|
||||||
if config.get('ticker_interval'):
|
|
||||||
timeframes = args.timeframes or [config.get('ticker_interval')]
|
|
||||||
else:
|
|
||||||
timeframes = args.timeframes or ['1m', '5m']
|
|
||||||
|
|
||||||
else:
|
|
||||||
config = {
|
|
||||||
'stake_currency': '',
|
|
||||||
'dry_run': True,
|
|
||||||
'exchange': {
|
|
||||||
'name': exchange_name,
|
|
||||||
'key': '',
|
|
||||||
'secret': '',
|
|
||||||
'pair_whitelist': [],
|
|
||||||
'ccxt_async_config': {
|
|
||||||
'enableRateLimit': True,
|
|
||||||
'rateLimit': 200
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
timeframes = args.timeframes or ['1m', '5m']
|
|
||||||
|
|
||||||
configuration._process_logging_options(config)
|
|
||||||
|
|
||||||
if args.config and args.exchange:
|
|
||||||
logger.warning("The --exchange option is ignored, "
|
|
||||||
"using exchange settings from the configuration file.")
|
|
||||||
|
|
||||||
# Check if the exchange set by the user is supported
|
|
||||||
check_exchange(config)
|
|
||||||
|
|
||||||
configuration._process_datadir_options(config)
|
|
||||||
|
|
||||||
dl_path = Path(config['datadir'])
|
|
||||||
|
|
||||||
pairs_file = Path(args.pairs_file) if args.pairs_file else dl_path.joinpath('pairs.json')
|
|
||||||
|
|
||||||
if not pairs or args.pairs_file:
|
|
||||||
logger.info(f'Reading pairs file "{pairs_file}".')
|
|
||||||
# Download pairs from the pairs file if no config is specified
|
|
||||||
# or if pairs file is specified explicitely
|
|
||||||
if not pairs_file.exists():
|
|
||||||
sys.exit(f'No pairs file found with path "{pairs_file}".')
|
|
||||||
|
|
||||||
with pairs_file.open() as file:
|
|
||||||
pairs = list(set(json.load(file)))
|
|
||||||
|
|
||||||
pairs.sort()
|
|
||||||
|
|
||||||
timerange = TimeRange()
|
|
||||||
if args.days:
|
|
||||||
time_since = arrow.utcnow().shift(days=-args.days).strftime("%Y%m%d")
|
|
||||||
timerange = arguments.parse_timerange(f'{time_since}-')
|
|
||||||
|
|
||||||
logger.info(f'About to download pairs: {pairs}, intervals: {timeframes} to {dl_path}')
|
|
||||||
|
|
||||||
pairs_not_available = []
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Init exchange
|
|
||||||
exchange = Exchange(config)
|
|
||||||
|
|
||||||
for pair in pairs:
|
|
||||||
if pair not in exchange._api.markets:
|
|
||||||
pairs_not_available.append(pair)
|
|
||||||
logger.info(f"Skipping pair {pair}...")
|
|
||||||
continue
|
|
||||||
for ticker_interval in timeframes:
|
|
||||||
pair_print = pair.replace('/', '_')
|
|
||||||
filename = f'{pair_print}-{ticker_interval}.json'
|
|
||||||
dl_file = dl_path.joinpath(filename)
|
|
||||||
if args.erase and dl_file.exists():
|
|
||||||
logger.info(
|
|
||||||
f'Deleting existing data for pair {pair}, interval {ticker_interval}.')
|
|
||||||
dl_file.unlink()
|
|
||||||
|
|
||||||
logger.info(f'Downloading pair {pair}, interval {ticker_interval}.')
|
|
||||||
download_pair_history(datadir=dl_path, exchange=exchange,
|
|
||||||
pair=pair, ticker_interval=str(ticker_interval),
|
|
||||||
timerange=timerange)
|
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
sys.exit("SIGINT received, aborting ...")
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if pairs_not_available:
|
|
||||||
logger.info(
|
|
||||||
f"Pairs [{','.join(pairs_not_available)}] not available "
|
|
||||||
f"on exchange {config['exchange']['name']}.")
|
|
||||||
|
@ -16,8 +16,6 @@ import logging
|
|||||||
import sys
|
import sys
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
from freqtrade.configuration import Arguments
|
from freqtrade.configuration import Arguments
|
||||||
from freqtrade.configuration.arguments import ARGS_PLOT_DATAFRAME
|
from freqtrade.configuration.arguments import ARGS_PLOT_DATAFRAME
|
||||||
from freqtrade.data.btanalysis import extract_trades_of_period
|
from freqtrade.data.btanalysis import extract_trades_of_period
|
||||||
@ -30,20 +28,6 @@ from freqtrade.state import RunMode
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def generate_dataframe(strategy, tickers, pair) -> pd.DataFrame:
|
|
||||||
"""
|
|
||||||
Get tickers then Populate strategy indicators and signals, then return the full dataframe
|
|
||||||
:return: the DataFrame of a pair
|
|
||||||
"""
|
|
||||||
|
|
||||||
dataframes = strategy.tickerdata_to_dataframe(tickers)
|
|
||||||
dataframe = dataframes[pair]
|
|
||||||
dataframe = strategy.advise_buy(dataframe, {'pair': pair})
|
|
||||||
dataframe = strategy.advise_sell(dataframe, {'pair': pair})
|
|
||||||
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
|
|
||||||
def analyse_and_plot_pairs(config: Dict[str, Any]):
|
def analyse_and_plot_pairs(config: Dict[str, Any]):
|
||||||
"""
|
"""
|
||||||
From arguments provided in cli:
|
From arguments provided in cli:
|
||||||
@ -57,6 +41,7 @@ def analyse_and_plot_pairs(config: Dict[str, Any]):
|
|||||||
"""
|
"""
|
||||||
plot_elements = init_plotscript(config)
|
plot_elements = init_plotscript(config)
|
||||||
trades = plot_elements['trades']
|
trades = plot_elements['trades']
|
||||||
|
strategy = plot_elements["strategy"]
|
||||||
|
|
||||||
pair_counter = 0
|
pair_counter = 0
|
||||||
for pair, data in plot_elements["tickers"].items():
|
for pair, data in plot_elements["tickers"].items():
|
||||||
@ -64,7 +49,8 @@ def analyse_and_plot_pairs(config: Dict[str, Any]):
|
|||||||
logger.info("analyse pair %s", pair)
|
logger.info("analyse pair %s", pair)
|
||||||
tickers = {}
|
tickers = {}
|
||||||
tickers[pair] = data
|
tickers[pair] = data
|
||||||
dataframe = generate_dataframe(plot_elements["strategy"], tickers, pair)
|
|
||||||
|
dataframe = strategy.analyze_ticker(tickers[pair], {'pair': pair})
|
||||||
|
|
||||||
trades_pair = trades.loc[trades['pair'] == pair]
|
trades_pair = trades.loc[trades['pair'] == pair]
|
||||||
trades_pair = extract_trades_of_period(dataframe, trades_pair)
|
trades_pair = extract_trades_of_period(dataframe, trades_pair)
|
||||||
@ -77,7 +63,8 @@ def analyse_and_plot_pairs(config: Dict[str, Any]):
|
|||||||
indicators2=config["indicators2"].split(",")
|
indicators2=config["indicators2"].split(",")
|
||||||
)
|
)
|
||||||
|
|
||||||
store_plot_file(fig, generate_plot_filename(pair, config['ticker_interval']))
|
store_plot_file(fig, filename=generate_plot_filename(pair, config['ticker_interval']),
|
||||||
|
directory=config['user_data_dir'] / "plot")
|
||||||
|
|
||||||
logger.info('End of ploting process %s plots generated', pair_counter)
|
logger.info('End of ploting process %s plots generated', pair_counter)
|
||||||
|
|
||||||
|
@ -32,7 +32,8 @@ def plot_profit(config: Dict[str, Any]) -> None:
|
|||||||
# Create an average close price of all the pairs that were involved.
|
# Create an average close price of all the pairs that were involved.
|
||||||
# this could be useful to gauge the overall market trend
|
# this could be useful to gauge the overall market trend
|
||||||
fig = generate_profit_graph(plot_elements["pairs"], plot_elements["tickers"], trades)
|
fig = generate_profit_graph(plot_elements["pairs"], plot_elements["tickers"], trades)
|
||||||
store_plot_file(fig, filename='freqtrade-profit-plot.html', auto_open=True)
|
store_plot_file(fig, filename='freqtrade-profit-plot.html',
|
||||||
|
directory=config['user_data_dir'] / "plot", auto_open=True)
|
||||||
|
|
||||||
|
|
||||||
def plot_parse_args(args: List[str]) -> Dict[str, Any]:
|
def plot_parse_args(args: List[str]) -> Dict[str, Any]:
|
||||||
|
48
setup.py
48
setup.py
@ -8,6 +8,30 @@ if version_info.major == 3 and version_info.minor < 6 or \
|
|||||||
|
|
||||||
from freqtrade import __version__
|
from freqtrade import __version__
|
||||||
|
|
||||||
|
# Requirements used for submodules
|
||||||
|
api = ['flask']
|
||||||
|
plot = ['plotly>=4.0']
|
||||||
|
|
||||||
|
develop = [
|
||||||
|
'coveralls',
|
||||||
|
'flake8',
|
||||||
|
'flake8-type-annotations',
|
||||||
|
'flake8-tidy-imports',
|
||||||
|
'mypy',
|
||||||
|
'pytest',
|
||||||
|
'pytest-asyncio',
|
||||||
|
'pytest-cov',
|
||||||
|
'pytest-mock',
|
||||||
|
'pytest-random-order',
|
||||||
|
]
|
||||||
|
|
||||||
|
jupyter = [
|
||||||
|
'jupyter',
|
||||||
|
'nbstripout',
|
||||||
|
'ipykernel',
|
||||||
|
]
|
||||||
|
|
||||||
|
all_extra = api + plot + develop + jupyter
|
||||||
|
|
||||||
setup(name='freqtrade',
|
setup(name='freqtrade',
|
||||||
version=__version__,
|
version=__version__,
|
||||||
@ -20,26 +44,40 @@ setup(name='freqtrade',
|
|||||||
setup_requires=['pytest-runner', 'numpy'],
|
setup_requires=['pytest-runner', 'numpy'],
|
||||||
tests_require=['pytest', 'pytest-mock', 'pytest-cov'],
|
tests_require=['pytest', 'pytest-mock', 'pytest-cov'],
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'ccxt',
|
# from requirements-common.txt
|
||||||
|
'ccxt>=1.18',
|
||||||
'SQLAlchemy',
|
'SQLAlchemy',
|
||||||
'python-telegram-bot',
|
'python-telegram-bot',
|
||||||
'arrow',
|
'arrow',
|
||||||
|
'cachetools',
|
||||||
'requests',
|
'requests',
|
||||||
'urllib3',
|
'urllib3',
|
||||||
'wrapt',
|
'wrapt',
|
||||||
'pandas',
|
|
||||||
'scikit-learn',
|
'scikit-learn',
|
||||||
'scipy',
|
|
||||||
'joblib',
|
'joblib',
|
||||||
'jsonschema',
|
'jsonschema',
|
||||||
'TA-Lib',
|
'TA-Lib',
|
||||||
'tabulate',
|
'tabulate',
|
||||||
'cachetools',
|
|
||||||
'coinmarketcap',
|
'coinmarketcap',
|
||||||
'scikit-optimize',
|
'scikit-optimize',
|
||||||
|
'filelock',
|
||||||
|
'py_find_1st',
|
||||||
'python-rapidjson',
|
'python-rapidjson',
|
||||||
'py_find_1st'
|
'sdnotify',
|
||||||
|
'colorama',
|
||||||
|
# from requirements.txt
|
||||||
|
'numpy',
|
||||||
|
'pandas',
|
||||||
|
'scipy',
|
||||||
],
|
],
|
||||||
|
extras_require={
|
||||||
|
'api': api,
|
||||||
|
'dev': all_extra,
|
||||||
|
'plot': plot,
|
||||||
|
'all': all_extra,
|
||||||
|
'jupyter': jupyter,
|
||||||
|
|
||||||
|
},
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
entry_points={
|
entry_points={
|
||||||
|
39
setup.sh
39
setup.sh
@ -11,6 +11,12 @@ function check_installed_pip() {
|
|||||||
|
|
||||||
# Check which python version is installed
|
# Check which python version is installed
|
||||||
function check_installed_python() {
|
function check_installed_python() {
|
||||||
|
if [ -n "${VIRTUAL_ENV}" ]; then
|
||||||
|
echo "Please deactivate your virtual environment before running setup.sh."
|
||||||
|
echo "You can do this by running 'deactivate'."
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
which python3.7
|
which python3.7
|
||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
echo "using Python 3.7"
|
echo "using Python 3.7"
|
||||||
@ -37,17 +43,19 @@ function updateenv() {
|
|||||||
echo "-------------------------"
|
echo "-------------------------"
|
||||||
echo "Updating your virtual env"
|
echo "Updating your virtual env"
|
||||||
echo "-------------------------"
|
echo "-------------------------"
|
||||||
|
if [ ! -f .env/bin/activate ]; then
|
||||||
|
echo "Something went wrong, no virtual environment found."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
source .env/bin/activate
|
source .env/bin/activate
|
||||||
echo "pip install in-progress. Please wait..."
|
echo "pip install in-progress. Please wait..."
|
||||||
# Install numpy first to have py_find_1st install clean
|
${PYTHON} -m pip install --upgrade pip
|
||||||
${PYTHON} -m pip install --upgrade pip numpy
|
|
||||||
${PYTHON} -m pip install --upgrade -r requirements.txt
|
|
||||||
|
|
||||||
read -p "Do you want to install dependencies for dev [y/N]? "
|
read -p "Do you want to install dependencies for dev [y/N]? "
|
||||||
if [[ $REPLY =~ ^[Yy]$ ]]
|
if [[ $REPLY =~ ^[Yy]$ ]]
|
||||||
then
|
then
|
||||||
${PYTHON} -m pip install --upgrade -r requirements-dev.txt
|
${PYTHON} -m pip install --upgrade -r requirements-dev.txt
|
||||||
else
|
else
|
||||||
|
${PYTHON} -m pip install --upgrade -r requirements.txt
|
||||||
echo "Dev dependencies ignored."
|
echo "Dev dependencies ignored."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@ -70,6 +78,10 @@ function install_talib() {
|
|||||||
./configure --prefix=/usr/local
|
./configure --prefix=/usr/local
|
||||||
make
|
make
|
||||||
sudo make install
|
sudo make install
|
||||||
|
if [ -x "$(command -v apt-get)" ]; then
|
||||||
|
echo "Updating library path using ldconfig"
|
||||||
|
sudo ldconfig
|
||||||
|
fi
|
||||||
cd .. && rm -rf ./ta-lib/
|
cd .. && rm -rf ./ta-lib/
|
||||||
cd ..
|
cd ..
|
||||||
}
|
}
|
||||||
@ -90,7 +102,7 @@ function install_macos() {
|
|||||||
# Install bot Debian_ubuntu
|
# Install bot Debian_ubuntu
|
||||||
function install_debian() {
|
function install_debian() {
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install build-essential autoconf libtool pkg-config make wget git
|
sudo apt-get install -y build-essential autoconf libtool pkg-config make wget git
|
||||||
install_talib
|
install_talib
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,12 +117,12 @@ function reset() {
|
|||||||
echo "----------------------------"
|
echo "----------------------------"
|
||||||
echo "Reseting branch and virtual env"
|
echo "Reseting branch and virtual env"
|
||||||
echo "----------------------------"
|
echo "----------------------------"
|
||||||
|
|
||||||
if [ "1" == $(git branch -vv |grep -cE "\* develop|\* master") ]
|
if [ "1" == $(git branch -vv |grep -cE "\* develop|\* master") ]
|
||||||
then
|
then
|
||||||
if [ -d ".env" ]; then
|
|
||||||
echo "- Delete your previous virtual env"
|
read -p "Reset git branch? (This will remove all changes you made!) [y/N]? "
|
||||||
rm -rf .env
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
fi
|
|
||||||
|
|
||||||
git fetch -a
|
git fetch -a
|
||||||
|
|
||||||
@ -123,12 +135,21 @@ function reset() {
|
|||||||
echo "- Hard resetting of 'master' branch."
|
echo "- Hard resetting of 'master' branch."
|
||||||
git reset --hard origin/master
|
git reset --hard origin/master
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
echo "Reset ignored because you are not on 'master' or 'develop'."
|
echo "Reset ignored because you are not on 'master' or 'develop'."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ -d ".env" ]; then
|
||||||
|
echo "- Delete your previous virtual env"
|
||||||
|
rm -rf .env
|
||||||
|
fi
|
||||||
echo
|
echo
|
||||||
${PYTHON} -m venv .env
|
${PYTHON} -m venv .env
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Could not create virtual environment. Leaving now"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
updateenv
|
updateenv
|
||||||
}
|
}
|
||||||
|
|
||||||
|
0
user_data/backtest_results/.gitkeep
Normal file
0
user_data/backtest_results/.gitkeep
Normal file
@ -1,11 +1,10 @@
|
|||||||
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
||||||
|
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from math import exp
|
|
||||||
from typing import Any, Callable, Dict, List
|
from typing import Any, Callable, Dict, List
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import numpy as np# noqa F401
|
import numpy as np
|
||||||
import talib.abstract as ta
|
import talib.abstract as ta
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
from skopt.space import Categorical, Dimension, Integer, Real
|
from skopt.space import Categorical, Dimension, Integer, Real
|
||||||
@ -14,48 +13,67 @@ import freqtrade.vendor.qtpylib.indicators as qtpylib
|
|||||||
from freqtrade.optimize.hyperopt_interface import IHyperOpt
|
from freqtrade.optimize.hyperopt_interface import IHyperOpt
|
||||||
|
|
||||||
|
|
||||||
# This class is a sample. Feel free to customize it.
|
|
||||||
class SampleHyperOpts(IHyperOpt):
|
class SampleHyperOpts(IHyperOpt):
|
||||||
"""
|
"""
|
||||||
This is a test hyperopt to inspire you.
|
This is a sample Hyperopt to inspire you.
|
||||||
More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md
|
Feel free to customize it.
|
||||||
You can:
|
|
||||||
- Rename the class name (Do not forget to update class_name)
|
|
||||||
- Add any methods you want to build your hyperopt
|
|
||||||
- Add any lib you need to build your hyperopt
|
|
||||||
You must keep:
|
|
||||||
- the prototype for the methods: populate_indicators, indicator_space, buy_strategy_generator,
|
|
||||||
roi_space, generate_roi_table, stoploss_space
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md
|
||||||
|
|
||||||
|
You should:
|
||||||
|
- Rename the class name to some unique name.
|
||||||
|
- Add any methods you want to build your hyperopt.
|
||||||
|
- Add any lib you need to build your hyperopt.
|
||||||
|
|
||||||
|
You must keep:
|
||||||
|
- The prototypes for the methods: populate_indicators, indicator_space, buy_strategy_generator.
|
||||||
|
|
||||||
|
The roi_space, generate_roi_table, stoploss_space methods are no longer required to be
|
||||||
|
copied in every custom hyperopt. However, you may override them if you need the
|
||||||
|
'roi' and the 'stoploss' spaces that differ from the defaults offered by Freqtrade.
|
||||||
|
Sample implementation of these methods can be found in
|
||||||
|
https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_advanced.py
|
||||||
|
"""
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Add several indicators needed for buy and sell strategies defined below.
|
||||||
|
"""
|
||||||
|
# ADX
|
||||||
dataframe['adx'] = ta.ADX(dataframe)
|
dataframe['adx'] = ta.ADX(dataframe)
|
||||||
|
# MACD
|
||||||
macd = ta.MACD(dataframe)
|
macd = ta.MACD(dataframe)
|
||||||
dataframe['macd'] = macd['macd']
|
dataframe['macd'] = macd['macd']
|
||||||
dataframe['macdsignal'] = macd['macdsignal']
|
dataframe['macdsignal'] = macd['macdsignal']
|
||||||
|
# MFI
|
||||||
dataframe['mfi'] = ta.MFI(dataframe)
|
dataframe['mfi'] = ta.MFI(dataframe)
|
||||||
|
# RSI
|
||||||
dataframe['rsi'] = ta.RSI(dataframe)
|
dataframe['rsi'] = ta.RSI(dataframe)
|
||||||
|
# Stochastic Fast
|
||||||
stoch_fast = ta.STOCHF(dataframe)
|
stoch_fast = ta.STOCHF(dataframe)
|
||||||
dataframe['fastd'] = stoch_fast['fastd']
|
dataframe['fastd'] = stoch_fast['fastd']
|
||||||
|
# Minus-DI
|
||||||
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
||||||
# Bollinger bands
|
# Bollinger bands
|
||||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||||
dataframe['bb_lowerband'] = bollinger['lower']
|
dataframe['bb_lowerband'] = bollinger['lower']
|
||||||
dataframe['bb_upperband'] = bollinger['upper']
|
dataframe['bb_upperband'] = bollinger['upper']
|
||||||
|
# SAR
|
||||||
dataframe['sar'] = ta.SAR(dataframe)
|
dataframe['sar'] = ta.SAR(dataframe)
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
|
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
|
||||||
"""
|
"""
|
||||||
Define the buy strategy parameters to be used by hyperopt
|
Define the buy strategy parameters to be used by Hyperopt.
|
||||||
"""
|
"""
|
||||||
def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Buy strategy Hyperopt will build and use
|
Buy strategy Hyperopt will build and use.
|
||||||
"""
|
"""
|
||||||
conditions = []
|
conditions = []
|
||||||
|
|
||||||
# GUARDS AND TRENDS
|
# GUARDS AND TRENDS
|
||||||
if 'mfi-enabled' in params and params['mfi-enabled']:
|
if 'mfi-enabled' in params and params['mfi-enabled']:
|
||||||
conditions.append(dataframe['mfi'] < params['mfi-value'])
|
conditions.append(dataframe['mfi'] < params['mfi-value'])
|
||||||
@ -91,7 +109,7 @@ class SampleHyperOpts(IHyperOpt):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def indicator_space() -> List[Dimension]:
|
def indicator_space() -> List[Dimension]:
|
||||||
"""
|
"""
|
||||||
Define your Hyperopt space for searching strategy parameters
|
Define your Hyperopt space for searching buy strategy parameters.
|
||||||
"""
|
"""
|
||||||
return [
|
return [
|
||||||
Integer(10, 25, name='mfi-value'),
|
Integer(10, 25, name='mfi-value'),
|
||||||
@ -108,14 +126,14 @@ class SampleHyperOpts(IHyperOpt):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def sell_strategy_generator(params: Dict[str, Any]) -> Callable:
|
def sell_strategy_generator(params: Dict[str, Any]) -> Callable:
|
||||||
"""
|
"""
|
||||||
Define the sell strategy parameters to be used by hyperopt
|
Define the sell strategy parameters to be used by Hyperopt.
|
||||||
"""
|
"""
|
||||||
def populate_sell_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_sell_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Sell strategy Hyperopt will build and use
|
Sell strategy Hyperopt will build and use.
|
||||||
"""
|
"""
|
||||||
# print(params)
|
|
||||||
conditions = []
|
conditions = []
|
||||||
|
|
||||||
# GUARDS AND TRENDS
|
# GUARDS AND TRENDS
|
||||||
if 'sell-mfi-enabled' in params and params['sell-mfi-enabled']:
|
if 'sell-mfi-enabled' in params and params['sell-mfi-enabled']:
|
||||||
conditions.append(dataframe['mfi'] > params['sell-mfi-value'])
|
conditions.append(dataframe['mfi'] > params['sell-mfi-value'])
|
||||||
@ -151,7 +169,7 @@ class SampleHyperOpts(IHyperOpt):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def sell_indicator_space() -> List[Dimension]:
|
def sell_indicator_space() -> List[Dimension]:
|
||||||
"""
|
"""
|
||||||
Define your Hyperopt space for searching sell strategy parameters
|
Define your Hyperopt space for searching sell strategy parameters.
|
||||||
"""
|
"""
|
||||||
return [
|
return [
|
||||||
Integer(75, 100, name='sell-mfi-value'),
|
Integer(75, 100, name='sell-mfi-value'),
|
||||||
@ -167,47 +185,11 @@ class SampleHyperOpts(IHyperOpt):
|
|||||||
'sell-sar_reversal'], name='sell-trigger')
|
'sell-sar_reversal'], name='sell-trigger')
|
||||||
]
|
]
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def generate_roi_table(params: Dict) -> Dict[int, float]:
|
|
||||||
"""
|
|
||||||
Generate the ROI table that will be used by Hyperopt
|
|
||||||
"""
|
|
||||||
roi_table = {}
|
|
||||||
roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3']
|
|
||||||
roi_table[params['roi_t3']] = params['roi_p1'] + params['roi_p2']
|
|
||||||
roi_table[params['roi_t3'] + params['roi_t2']] = params['roi_p1']
|
|
||||||
roi_table[params['roi_t3'] + params['roi_t2'] + params['roi_t1']] = 0
|
|
||||||
|
|
||||||
return roi_table
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def stoploss_space() -> List[Dimension]:
|
|
||||||
"""
|
|
||||||
Stoploss Value to search
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
Real(-0.5, -0.02, name='stoploss'),
|
|
||||||
]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def roi_space() -> List[Dimension]:
|
|
||||||
"""
|
|
||||||
Values to search for each ROI steps
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
Integer(10, 120, name='roi_t1'),
|
|
||||||
Integer(10, 60, name='roi_t2'),
|
|
||||||
Integer(10, 40, name='roi_t3'),
|
|
||||||
Real(0.01, 0.04, name='roi_p1'),
|
|
||||||
Real(0.01, 0.07, name='roi_p2'),
|
|
||||||
Real(0.01, 0.20, name='roi_p3'),
|
|
||||||
]
|
|
||||||
|
|
||||||
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Based on TA indicators. Should be a copy of from strategy
|
Based on TA indicators. Should be a copy of same method from strategy.
|
||||||
must align to populate_indicators in this file
|
Must align to populate_indicators in this file.
|
||||||
Only used when --spaces does not include buy
|
Only used when --spaces does not include buy space.
|
||||||
"""
|
"""
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
(
|
(
|
||||||
@ -222,9 +204,9 @@ class SampleHyperOpts(IHyperOpt):
|
|||||||
|
|
||||||
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Based on TA indicators. Should be a copy of from strategy
|
Based on TA indicators. Should be a copy of same method from strategy.
|
||||||
must align to populate_indicators in this file
|
Must align to populate_indicators in this file.
|
||||||
Only used when --spaces does not include sell
|
Only used when --spaces does not include sell space.
|
||||||
"""
|
"""
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
(
|
(
|
||||||
@ -234,4 +216,5 @@ class SampleHyperOpts(IHyperOpt):
|
|||||||
(dataframe['fastd'] > 54)
|
(dataframe['fastd'] > 54)
|
||||||
),
|
),
|
||||||
'sell'] = 1
|
'sell'] = 1
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user