mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-10 10:21:59 +00:00
Merge pull request #1957 from freqtrade/new_release
New release - 2019.6
This commit is contained in:
commit
21f6493b02
|
@ -11,7 +11,10 @@ update: all
|
||||||
# allowed: True, False
|
# allowed: True, False
|
||||||
pin: True
|
pin: True
|
||||||
|
|
||||||
schedule: "every day"
|
# update schedule
|
||||||
|
# default: empty
|
||||||
|
# allowed: "every day", "every week", ..
|
||||||
|
schedule: "every week"
|
||||||
|
|
||||||
|
|
||||||
search: False
|
search: False
|
||||||
|
@ -22,7 +25,7 @@ requirements:
|
||||||
- requirements.txt
|
- requirements.txt
|
||||||
- requirements-dev.txt
|
- requirements-dev.txt
|
||||||
- requirements-plot.txt
|
- requirements-plot.txt
|
||||||
- requirements-pi.txt
|
- requirements-common.txt
|
||||||
|
|
||||||
|
|
||||||
# configure the branch prefix the bot is using
|
# configure the branch prefix the bot is using
|
||||||
|
|
|
@ -11,7 +11,7 @@ Few pointers for contributions:
|
||||||
- Create your PR against the `develop` branch, not `master`.
|
- Create your PR against the `develop` branch, not `master`.
|
||||||
- New features need to contain unit tests and must be PEP8 conformant (max-line-length = 100).
|
- New features need to contain unit tests and must be PEP8 conformant (max-line-length = 100).
|
||||||
|
|
||||||
If you are unsure, discuss the feature on our [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE)
|
If you are unsure, discuss the feature on our [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LWEyODBiNzkzNzcyNzU0MWYyYzE5NjIyOTQxMzBmMGUxOTIzM2YyN2Y4NWY1YTEwZDgwYTRmMzE2NmM5ZmY2MTg)
|
||||||
or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR.
|
or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR.
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
|
@ -16,7 +16,7 @@ RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib*
|
||||||
ENV LD_LIBRARY_PATH /usr/local/lib
|
ENV LD_LIBRARY_PATH /usr/local/lib
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
COPY requirements.txt /freqtrade/
|
COPY requirements.txt requirements-common.txt /freqtrade/
|
||||||
RUN pip install numpy --no-cache-dir \
|
RUN pip install numpy --no-cache-dir \
|
||||||
&& pip install -r requirements.txt --no-cache-dir
|
&& pip install -r requirements.txt --no-cache-dir
|
||||||
|
|
||||||
|
|
|
@ -27,9 +27,9 @@ RUN wget https://github.com/jjhelmus/berryconda/releases/download/v2.0.0/Berryco
|
||||||
&& rm Berryconda3-2.0.0-Linux-armv7l.sh
|
&& rm Berryconda3-2.0.0-Linux-armv7l.sh
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
COPY requirements-pi.txt /freqtrade/
|
COPY requirements-common.txt /freqtrade/
|
||||||
RUN ~/berryconda3/bin/conda install -y numpy pandas scipy \
|
RUN ~/berryconda3/bin/conda install -y numpy pandas scipy \
|
||||||
&& ~/berryconda3/bin/pip install -r requirements-pi.txt --no-cache-dir
|
&& ~/berryconda3/bin/pip install -r requirements-common.txt --no-cache-dir
|
||||||
|
|
||||||
# Install and execute
|
# Install and execute
|
||||||
COPY . /freqtrade/
|
COPY . /freqtrade/
|
||||||
|
|
|
@ -3,4 +3,4 @@ FROM freqtradeorg/freqtrade:develop
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get -y install git \
|
&& apt-get -y install git \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& pip install git+https://github.com/berlinguyinca/technical
|
&& pip install git+https://github.com/freqtrade/technical
|
||||||
|
|
|
@ -129,7 +129,6 @@ The project is currently setup in two main branches:
|
||||||
- `master` - This branch contains the latest stable release. The bot 'should' be stable on this branch, and is generally well tested.
|
- `master` - This branch contains the latest stable release. The bot 'should' be stable on this branch, and is generally well tested.
|
||||||
- `feat/*` - These are feature branches, which are being worked on heavily. Please don't use these unless you want to test a specific feature.
|
- `feat/*` - These are feature branches, which are being worked on heavily. Please don't use these unless you want to test a specific feature.
|
||||||
|
|
||||||
|
|
||||||
## A note on Binance
|
## A note on Binance
|
||||||
|
|
||||||
For Binance, please add `"BNB/<STAKE>"` to your blacklist to avoid issues.
|
For Binance, please add `"BNB/<STAKE>"` to your blacklist to avoid issues.
|
||||||
|
@ -142,7 +141,7 @@ Accounts having BNB accounts use this to pay for fees - if your first trade happ
|
||||||
For any questions not covered by the documentation or for further
|
For any questions not covered by the documentation or for further
|
||||||
information about the bot, we encourage you to join our slack channel.
|
information about the bot, we encourage you to join our slack channel.
|
||||||
|
|
||||||
- [Click here to join Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE).
|
- [Click here to join Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LWEyODBiNzkzNzcyNzU0MWYyYzE5NjIyOTQxMzBmMGUxOTIzM2YyN2Y4NWY1YTEwZDgwYTRmMzE2NmM5ZmY2MTg).
|
||||||
|
|
||||||
### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
|
### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
|
||||||
|
|
||||||
|
@ -173,7 +172,7 @@ to understand the requirements before sending your pull-requests.
|
||||||
Coding is not a neccessity to contribute - maybe start with improving our documentation?
|
Coding is not a neccessity to contribute - maybe start with improving our documentation?
|
||||||
Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/good%20first%20issue) can be good first contributions, and will help get you familiar with the codebase.
|
Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/good%20first%20issue) can be good first contributions, and will help get you familiar with the codebase.
|
||||||
|
|
||||||
**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it.
|
**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LWEyODBiNzkzNzcyNzU0MWYyYzE5NjIyOTQxMzBmMGUxOTIzM2YyN2Y4NWY1YTEwZDgwYTRmMzE2NmM5ZmY2MTg). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it.
|
||||||
|
|
||||||
**Important:** Always create your PR against the `develop` branch, not `master`.
|
**Important:** Always create your PR against the `develop` branch, not `master`.
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
import warnings
|
||||||
|
|
||||||
from freqtrade.main import main, set_loggers
|
from freqtrade.main import main, set_loggers
|
||||||
|
|
||||||
set_loggers()
|
set_loggers()
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"Deprecated - To continue to run the bot like this, please run `pip install -e .` again.",
|
||||||
|
DeprecationWarning)
|
||||||
main(sys.argv[1:])
|
main(sys.argv[1:])
|
||||||
|
|
|
@ -11,8 +11,8 @@
|
||||||
"sell": 30
|
"sell": 30
|
||||||
},
|
},
|
||||||
"bid_strategy": {
|
"bid_strategy": {
|
||||||
"ask_last_balance": 0.0,
|
|
||||||
"use_order_book": false,
|
"use_order_book": false,
|
||||||
|
"ask_last_balance": 0.0,
|
||||||
"order_book_top": 1,
|
"order_book_top": 1,
|
||||||
"check_depth_of_market": {
|
"check_depth_of_market": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
|
|
|
@ -22,8 +22,8 @@
|
||||||
"sell": 30
|
"sell": 30
|
||||||
},
|
},
|
||||||
"bid_strategy": {
|
"bid_strategy": {
|
||||||
"ask_last_balance": 0.0,
|
|
||||||
"use_order_book": false,
|
"use_order_book": false,
|
||||||
|
"ask_last_balance": 0.0,
|
||||||
"order_book_top": 1,
|
"order_book_top": 1,
|
||||||
"check_depth_of_market": {
|
"check_depth_of_market": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
|
@ -56,8 +56,10 @@
|
||||||
},
|
},
|
||||||
"exchange": {
|
"exchange": {
|
||||||
"name": "bittrex",
|
"name": "bittrex",
|
||||||
|
"sandbox": false,
|
||||||
"key": "your_exchange_key",
|
"key": "your_exchange_key",
|
||||||
"secret": "your_exchange_secret",
|
"secret": "your_exchange_secret",
|
||||||
|
"password": "",
|
||||||
"ccxt_config": {"enableRateLimit": true},
|
"ccxt_config": {"enableRateLimit": true},
|
||||||
"ccxt_async_config": {
|
"ccxt_async_config": {
|
||||||
"enableRateLimit": false,
|
"enableRateLimit": false,
|
||||||
|
@ -107,6 +109,13 @@
|
||||||
"token": "your_telegram_token",
|
"token": "your_telegram_token",
|
||||||
"chat_id": "your_telegram_chat_id"
|
"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",
|
"db_url": "sqlite:///tradesv3.sqlite",
|
||||||
"initial_state": "running",
|
"initial_state": "running",
|
||||||
"forcebuy_enable": false,
|
"forcebuy_enable": false,
|
||||||
|
|
|
@ -5,15 +5,14 @@
|
||||||
"fiat_display_currency": "EUR",
|
"fiat_display_currency": "EUR",
|
||||||
"ticker_interval" : "5m",
|
"ticker_interval" : "5m",
|
||||||
"dry_run": true,
|
"dry_run": true,
|
||||||
"db_url": "sqlite:///tradesv3.dryrun.sqlite",
|
|
||||||
"trailing_stop": false,
|
"trailing_stop": false,
|
||||||
"unfilledtimeout": {
|
"unfilledtimeout": {
|
||||||
"buy": 10,
|
"buy": 10,
|
||||||
"sell": 30
|
"sell": 30
|
||||||
},
|
},
|
||||||
"bid_strategy": {
|
"bid_strategy": {
|
||||||
"ask_last_balance": 0.0,
|
|
||||||
"use_order_book": false,
|
"use_order_book": false,
|
||||||
|
"ask_last_balance": 0.0,
|
||||||
"order_book_top": 1,
|
"order_book_top": 1,
|
||||||
"check_depth_of_market": {
|
"check_depth_of_market": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
|
@ -60,8 +59,8 @@
|
||||||
},
|
},
|
||||||
"telegram": {
|
"telegram": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"token": "",
|
"token": "your_telegram_token",
|
||||||
"chat_id": ""
|
"chat_id": "your_telegram_chat_id"
|
||||||
},
|
},
|
||||||
"initial_state": "running",
|
"initial_state": "running",
|
||||||
"forcebuy_enable": false,
|
"forcebuy_enable": false,
|
||||||
|
|
|
@ -123,11 +123,12 @@ python scripts/download_backtest_data.py --exchange binance
|
||||||
|
|
||||||
This will download ticker data for all the currency pairs you defined in `pairs.json`.
|
This will download ticker data for all the currency pairs you defined in `pairs.json`.
|
||||||
|
|
||||||
- To use a different folder than the exchange specific default, use `--export user_data/data/some_directory`.
|
- To use a different folder 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 change the exchange used to download the tickers, use `--exchange`. Default is `bittrex`.
|
||||||
- To use `pairs.json` from some other folder, use `--pairs-file some_other_dir/pairs.json`.
|
- To use `pairs.json` from some other folder, use `--pairs-file some_other_dir/pairs.json`.
|
||||||
- To download ticker data for only 10 days, use `--days 10`.
|
- 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.
|
- 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).
|
For help about backtesting usage, please refer to [Backtesting commands](#backtesting-commands).
|
||||||
|
|
||||||
|
@ -220,24 +221,8 @@ strategies, your configuration, and the crypto-currency you have set up.
|
||||||
### Further backtest-result analysis
|
### Further backtest-result analysis
|
||||||
|
|
||||||
To further analyze your backtest results, you can [export the trades](#exporting-trades-to-file).
|
To further analyze your backtest results, you can [export the trades](#exporting-trades-to-file).
|
||||||
You can then load the trades to perform further analysis.
|
You can then load the trades to perform further analysis as shown in our [data analysis](data-analysis.md#backtesting) backtesting section.
|
||||||
|
|
||||||
A good way for this is using Jupyter (notebook or lab) - which provides an interactive environment to analyze the data.
|
|
||||||
|
|
||||||
Freqtrade provides an easy to load the backtest results, which is `load_backtest_data` - and takes a path to the backtest-results file.
|
|
||||||
|
|
||||||
``` python
|
|
||||||
from freqtrade.data.btanalysis import load_backtest_data
|
|
||||||
df = load_backtest_data("user_data/backtest-result.json")
|
|
||||||
|
|
||||||
# Show value-counts per pair
|
|
||||||
df.groupby("pair")["sell_reason"].value_counts()
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
This will allow you to drill deeper into your backtest results, and perform analysis which would make the regular backtest-output unreadable.
|
|
||||||
|
|
||||||
If you have some ideas for interesting / helpful backtest data analysis ideas, please submit a PR so the community can benefit from it.
|
|
||||||
|
|
||||||
## Backtesting multiple strategies
|
## Backtesting multiple strategies
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,8 @@ optional arguments:
|
||||||
--version show program's version number and exit
|
--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: None). Multiple
|
||||||
--config options may be used.
|
--config options may be used. Can be set to '-' to
|
||||||
|
read config from stdin.
|
||||||
-d PATH, --datadir PATH
|
-d PATH, --datadir PATH
|
||||||
Path to backtest data.
|
Path to backtest data.
|
||||||
-s NAME, --strategy NAME
|
-s NAME, --strategy NAME
|
||||||
|
@ -103,7 +104,7 @@ If the bot does not find your strategy file, it will display in an error
|
||||||
message the reason (File not found, or errors in your code).
|
message the reason (File not found, or errors in your code).
|
||||||
|
|
||||||
Learn more about strategy file in
|
Learn more about strategy file in
|
||||||
[optimize your bot](bot-optimization.md).
|
[Strategy Customization](strategy-customization.md).
|
||||||
|
|
||||||
### How to use **--strategy-path**?
|
### How to use **--strategy-path**?
|
||||||
|
|
||||||
|
@ -146,9 +147,11 @@ Backtesting also uses the config specified via `-c/--config`.
|
||||||
|
|
||||||
```
|
```
|
||||||
usage: freqtrade backtesting [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE]
|
usage: freqtrade backtesting [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE]
|
||||||
[--eps] [--dmmp] [-l] [-r]
|
[--max_open_trades MAX_OPEN_TRADES]
|
||||||
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
|
[--stake_amount STAKE_AMOUNT] [-r] [--eps] [--dmmp]
|
||||||
[--export EXPORT] [--export-filename PATH]
|
[-l]
|
||||||
|
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
|
||||||
|
[--export EXPORT] [--export-filename PATH]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
|
@ -156,6 +159,14 @@ optional arguments:
|
||||||
Specify ticker interval (1m, 5m, 30m, 1h, 1d).
|
Specify ticker interval (1m, 5m, 30m, 1h, 1d).
|
||||||
--timerange TIMERANGE
|
--timerange TIMERANGE
|
||||||
Specify what timerange of data to use.
|
Specify what timerange of data to use.
|
||||||
|
--max_open_trades MAX_OPEN_TRADES
|
||||||
|
Specify max_open_trades to use.
|
||||||
|
--stake_amount STAKE_AMOUNT
|
||||||
|
Specify stake_amount.
|
||||||
|
-r, --refresh-pairs-cached
|
||||||
|
Refresh the pairs files in tests/testdata with the
|
||||||
|
latest data from the exchange. Use it if you want to
|
||||||
|
run your optimization commands with up-to-date data.
|
||||||
--eps, --enable-position-stacking
|
--eps, --enable-position-stacking
|
||||||
Allow buying the same pair multiple times (position
|
Allow buying the same pair multiple times (position
|
||||||
stacking).
|
stacking).
|
||||||
|
@ -164,10 +175,6 @@ optional arguments:
|
||||||
(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.
|
-l, --live Use live data.
|
||||||
-r, --refresh-pairs-cached
|
|
||||||
Refresh the pairs files in tests/testdata with the
|
|
||||||
latest data from the exchange. Use it if you want to
|
|
||||||
run your backtesting with up-to-date data.
|
|
||||||
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
||||||
Provide a commaseparated list of strategies to
|
Provide a commaseparated list of strategies to
|
||||||
backtest Please note that ticker-interval needs to be
|
backtest Please note that ticker-interval needs to be
|
||||||
|
@ -188,7 +195,7 @@ optional arguments:
|
||||||
### How to use **--refresh-pairs-cached** parameter?
|
### How to use **--refresh-pairs-cached** parameter?
|
||||||
|
|
||||||
The first time your run Backtesting, it will take the pairs you have
|
The first time your run Backtesting, it will take the pairs you have
|
||||||
set in your config file and download data from Bittrex.
|
set in your config file and download data from the Exchange.
|
||||||
|
|
||||||
If for any reason you want to update your data set, you use
|
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.
|
`--refresh-pairs-cached` to force Backtesting to update the data it has.
|
||||||
|
@ -206,8 +213,11 @@ to find optimal parameter values for your stategy.
|
||||||
|
|
||||||
```
|
```
|
||||||
usage: freqtrade hyperopt [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE]
|
usage: freqtrade hyperopt [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE]
|
||||||
[--customhyperopt NAME] [--eps] [--dmmp] [-e INT]
|
[--max_open_trades MAX_OPEN_TRADES]
|
||||||
[-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]]
|
[--stake_amount STAKE_AMOUNT] [-r]
|
||||||
|
[--customhyperopt NAME] [--eps] [--dmmp] [-e INT]
|
||||||
|
[-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]]
|
||||||
|
[--print-all] [-j JOBS]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
|
@ -215,6 +225,14 @@ optional arguments:
|
||||||
Specify ticker interval (1m, 5m, 30m, 1h, 1d).
|
Specify ticker interval (1m, 5m, 30m, 1h, 1d).
|
||||||
--timerange TIMERANGE
|
--timerange TIMERANGE
|
||||||
Specify what timerange of data to use.
|
Specify what timerange of data to use.
|
||||||
|
--max_open_trades MAX_OPEN_TRADES
|
||||||
|
Specify max_open_trades to use.
|
||||||
|
--stake_amount STAKE_AMOUNT
|
||||||
|
Specify stake_amount.
|
||||||
|
-r, --refresh-pairs-cached
|
||||||
|
Refresh the pairs files in tests/testdata with the
|
||||||
|
latest data from the exchange. Use it if you want to
|
||||||
|
run your optimization commands with up-to-date data.
|
||||||
--customhyperopt NAME
|
--customhyperopt NAME
|
||||||
Specify hyperopt class name (default:
|
Specify hyperopt class name (default:
|
||||||
DefaultHyperOpts).
|
DefaultHyperOpts).
|
||||||
|
@ -229,7 +247,13 @@ optional arguments:
|
||||||
-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...], --spaces {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]
|
-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...], --spaces {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]
|
||||||
Specify which parameters to hyperopt. Space separate
|
Specify which parameters to hyperopt. Space separate
|
||||||
list. Default: all.
|
list. Default: all.
|
||||||
|
--print-all Print all results, not only the best ones.
|
||||||
|
-j JOBS, --job-workers JOBS
|
||||||
|
The number of concurrently running jobs for
|
||||||
|
hyperoptimization (hyperopt worker processes). If -1
|
||||||
|
(default), all CPUs are used, for -2, all CPUs but one
|
||||||
|
are used, etc. If 1 is given, no parallel computing
|
||||||
|
code is used at all.
|
||||||
```
|
```
|
||||||
|
|
||||||
## Edge commands
|
## Edge commands
|
||||||
|
@ -237,8 +261,10 @@ optional arguments:
|
||||||
To know your trade expectacny and winrate against historical data, you can use Edge.
|
To know your trade expectacny and winrate against historical data, you can use Edge.
|
||||||
|
|
||||||
```
|
```
|
||||||
usage: freqtrade edge [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] [-r]
|
usage: freqtrade edge [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE]
|
||||||
[--stoplosses STOPLOSS_RANGE]
|
[--max_open_trades MAX_OPEN_TRADES]
|
||||||
|
[--stake_amount STAKE_AMOUNT] [-r]
|
||||||
|
[--stoplosses STOPLOSS_RANGE]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
|
@ -246,10 +272,14 @@ optional arguments:
|
||||||
Specify ticker interval (1m, 5m, 30m, 1h, 1d).
|
Specify ticker interval (1m, 5m, 30m, 1h, 1d).
|
||||||
--timerange TIMERANGE
|
--timerange TIMERANGE
|
||||||
Specify what timerange of data to use.
|
Specify what timerange of data to use.
|
||||||
|
--max_open_trades MAX_OPEN_TRADES
|
||||||
|
Specify max_open_trades to use.
|
||||||
|
--stake_amount STAKE_AMOUNT
|
||||||
|
Specify stake_amount.
|
||||||
-r, --refresh-pairs-cached
|
-r, --refresh-pairs-cached
|
||||||
Refresh the pairs files in tests/testdata with the
|
Refresh the pairs files in tests/testdata with the
|
||||||
latest data from the exchange. Use it if you want to
|
latest data from the exchange. Use it if you want to
|
||||||
run your edge with up-to-date data.
|
run your optimization commands with up-to-date data.
|
||||||
--stoplosses STOPLOSS_RANGE
|
--stoplosses STOPLOSS_RANGE
|
||||||
Defines a range of stoploss against which edge will
|
Defines a range of stoploss against which edge will
|
||||||
assess the strategy the format is "min,max,step"
|
assess the strategy the format is "min,max,step"
|
||||||
|
@ -267,4 +297,4 @@ in [misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.
|
||||||
## Next step
|
## Next step
|
||||||
|
|
||||||
The optimal strategy of the bot will change with time depending of the market trends. The next step is to
|
The optimal strategy of the bot will change with time depending of the market trends. The next step is to
|
||||||
[optimize your bot](bot-optimization.md).
|
[Strategy Customization](strategy-customization.md).
|
||||||
|
|
|
@ -40,10 +40,10 @@ Mandatory Parameters are marked as **Required**.
|
||||||
| `ask_strategy.order_book_max` | 0 | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
|
| `ask_strategy.order_book_max` | 0 | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
|
||||||
| `order_types` | None | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy).
|
| `order_types` | None | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy).
|
||||||
| `order_time_in_force` | None | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy).
|
| `order_time_in_force` | None | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy).
|
||||||
| `exchange.name` | bittrex | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename).
|
| `exchange.name` | | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename).
|
||||||
| `exchange.sandbox` | false | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details.
|
| `exchange.sandbox` | false | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details.
|
||||||
| `exchange.key` | key | API key to use for the exchange. Only required when you are in production mode.
|
| `exchange.key` | '' | API key to use for the exchange. Only required when you are in production mode.
|
||||||
| `exchange.secret` | secret | API secret to use for the exchange. Only required when you are in production mode.
|
| `exchange.secret` | '' | API secret to use for the exchange. Only required when you are in production mode.
|
||||||
| `exchange.pair_whitelist` | [] | List of currency to use by the bot. Can be overrided with `--dynamic-whitelist` param.
|
| `exchange.pair_whitelist` | [] | List of currency to use by the bot. Can be overrided with `--dynamic-whitelist` param.
|
||||||
| `exchange.pair_blacklist` | [] | List of currency the bot must avoid. Useful when using `--dynamic-whitelist` param.
|
| `exchange.pair_blacklist` | [] | List of currency the bot must avoid. Useful when using `--dynamic-whitelist` param.
|
||||||
| `exchange.ccxt_config` | None | Additional CCXT parameters passed to the regular ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
|
| `exchange.ccxt_config` | None | Additional CCXT parameters passed to the regular ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
|
||||||
|
@ -131,17 +131,11 @@ If it is not set in either Strategy or Configuration, a default of 1000% `{"0":
|
||||||
|
|
||||||
### Understand stoploss
|
### Understand stoploss
|
||||||
|
|
||||||
The `stoploss` configuration parameter is loss in percentage that should trigger a sale.
|
Go to the [stoploss documentation](stoploss.md) for more details.
|
||||||
For example, value `-0.10` will cause immediate sell if the
|
|
||||||
profit dips below -10% for a given trade. This parameter is optional.
|
|
||||||
|
|
||||||
Most of the strategy files already include the optimal `stoploss`
|
|
||||||
value. This parameter is optional. If you use it in the configuration file, it will take over the
|
|
||||||
`stoploss` value from the strategy file.
|
|
||||||
|
|
||||||
### Understand trailing stoploss
|
### Understand trailing stoploss
|
||||||
|
|
||||||
Go to the [trailing stoploss Documentation](stoploss.md) for details on trailing stoploss.
|
Go to the [trailing stoploss Documentation](stoploss.md#trailing-stop-loss) for details on trailing stoploss.
|
||||||
|
|
||||||
### Understand initial_state
|
### Understand initial_state
|
||||||
|
|
||||||
|
@ -191,14 +185,28 @@ If this is configured, all 4 values (`buy`, `sell`, `stoploss` and
|
||||||
`stoploss_on_exchange`) need to be present, otherwise the bot will warn about it and fail to start.
|
`stoploss_on_exchange`) need to be present, otherwise the bot will warn about it and fail to start.
|
||||||
The below is the default which is used if this is not configured in either strategy or configuration file.
|
The below is the default which is used if this is not configured in either strategy or configuration file.
|
||||||
|
|
||||||
|
Syntax for Strategy:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
"order_types": {
|
order_types = {
|
||||||
"buy": "limit",
|
"buy": "limit",
|
||||||
"sell": "limit",
|
"sell": "limit",
|
||||||
"stoploss": "market",
|
"stoploss": "market",
|
||||||
"stoploss_on_exchange": False,
|
"stoploss_on_exchange": False,
|
||||||
"stoploss_on_exchange_interval": 60
|
"stoploss_on_exchange_interval": 60
|
||||||
},
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"order_types": {
|
||||||
|
"buy": "limit",
|
||||||
|
"sell": "limit",
|
||||||
|
"stoploss": "market",
|
||||||
|
"stoploss_on_exchange": false,
|
||||||
|
"stoploss_on_exchange_interval": 60
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
|
@ -287,8 +295,27 @@ This configuration enables binance, as well as rate limiting to avoid bans from
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Optimal settings for rate limiting depend on the exchange and the size of the whitelist, so an ideal parameter will vary on many other settings.
|
Optimal settings for rate limiting depend on the exchange and the size of the whitelist, so an ideal parameter will vary on many other settings.
|
||||||
We try to provide sensible defaults per exchange where possible, if you encounter bans please make sure that `"enableRateLimit"` is enabled and increase the `"rateLimit"` parameter step by step.
|
We try to provide sensible defaults per exchange where possible, if you encounter bans please make sure that `"enableRateLimit"` is enabled and increase the `"rateLimit"` parameter step by step.
|
||||||
|
|
||||||
|
#### Advanced FreqTrade Exchange configuration
|
||||||
|
|
||||||
|
Advanced options can be configured using the `_ft_has_params` setting, which will override Defaults and exchange-specific behaviours.
|
||||||
|
|
||||||
|
Available options are listed in the exchange-class as `_ft_has_default`.
|
||||||
|
|
||||||
|
For example, to test the order type `FOK` with Kraken, and modify candle_limit to 200 (so you only get 200 candles per call):
|
||||||
|
|
||||||
|
```json
|
||||||
|
"exchange": {
|
||||||
|
"name": "kraken",
|
||||||
|
"_ft_has_params": {
|
||||||
|
"order_time_in_force": ["gtc", "fok"],
|
||||||
|
"ohlcv_candle_limit": 200
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Warning
|
||||||
|
Please make sure to fully understand the impacts of these settings before modifying them.
|
||||||
|
|
||||||
### What values can be used for fiat_display_currency?
|
### What values can be used for fiat_display_currency?
|
||||||
|
|
||||||
|
|
42
docs/data-analysis.md
Normal file
42
docs/data-analysis.md
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
# Analyzing bot data
|
||||||
|
|
||||||
|
After performing backtests, or after running the bot for some time, it will be interesting to analyze the results your bot generated.
|
||||||
|
|
||||||
|
A good way for this is using Jupyter (notebook or lab) - which provides an interactive environment to analyze the data.
|
||||||
|
|
||||||
|
The following helpers will help you loading the data into Pandas DataFrames, and may also give you some starting points in analyzing the results.
|
||||||
|
|
||||||
|
## Backtesting
|
||||||
|
|
||||||
|
To analyze your backtest results, you can [export the trades](#exporting-trades-to-file).
|
||||||
|
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.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
from freqtrade.data.btanalysis import load_backtest_data
|
||||||
|
df = load_backtest_data("user_data/backtest-result.json")
|
||||||
|
|
||||||
|
# Show value-counts per pair
|
||||||
|
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.
|
||||||
|
|
||||||
|
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
|
||||||
|
from freqtrade.data.btanalysis import load_trades_from_db
|
||||||
|
|
||||||
|
df = load_trades_from_db("sqlite:///tradesv3.sqlite")
|
||||||
|
|
||||||
|
df.groupby("pair")["sell_reason"].value_counts()
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
This page is intended for developers of FreqTrade, people who want to contribute to the FreqTrade codebase or documentation, or people who want to understand the source code of the application they're running.
|
This page is intended for developers of FreqTrade, people who want to contribute to the FreqTrade codebase or documentation, or people who want to understand the source code of the application they're running.
|
||||||
|
|
||||||
All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel in [slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) where you can ask questions.
|
All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel in [slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LWEyODBiNzkzNzcyNzU0MWYyYzE5NjIyOTQxMzBmMGUxOTIzM2YyN2Y4NWY1YTEwZDgwYTRmMzE2NmM5ZmY2MTg) where you can ask questions.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
|
@ -81,6 +81,51 @@ Please also run `self._validate_whitelist(pairs)` and to check and remove pairs
|
||||||
This is a simple method used by `VolumePairList` - however serves as a good example.
|
This is a simple method used by `VolumePairList` - however serves as a good example.
|
||||||
It implements caching (`@cached(TTLCache(maxsize=1, ttl=1800))`) as well as a configuration option to allow different (but similar) strategies to work with the same PairListProvider.
|
It implements caching (`@cached(TTLCache(maxsize=1, ttl=1800))`) as well as a configuration option to allow different (but similar) strategies to work with the same PairListProvider.
|
||||||
|
|
||||||
|
## Implement a new Exchange (WIP)
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
This section is a Work in Progress and is not a complete guide on how to test a new exchange with FreqTrade.
|
||||||
|
|
||||||
|
Most exchanges supported by CCXT should work out of the box.
|
||||||
|
|
||||||
|
### Stoploss On Exchange
|
||||||
|
|
||||||
|
Check if the new exchange supports Stoploss on Exchange orders through their API.
|
||||||
|
|
||||||
|
Since CCXT does not provide unification for Stoploss On Exchange yet, we'll need to implement the exchange-specific parameters ourselfs. Best look at `binance.py` for an example implementation of this. You'll need to dig through the documentation of the Exchange's API on how exactly this can be done. [CCXT Issues](https://github.com/ccxt/ccxt/issues) may also provide great help, since others may have implemented something similar for their projects.
|
||||||
|
|
||||||
|
### Incomplete candles
|
||||||
|
|
||||||
|
While fetching OHLCV data, we're may end up getting incomplete candles (Depending on the exchange).
|
||||||
|
To demonstrate this, we'll use daily candles (`"1d"`) to keep things simple.
|
||||||
|
We query the api (`ct.fetch_ohlcv()`) for the timeframe and look at the date of the last entry. If this entry changes or shows the date of a "incomplete" candle, then we should drop this since having incomplete candles is problematic because indicators assume that only complete candles are passed to them, and will generate a lot of false buy signals. By default, we're therefore removing the last candle assuming it's incomplete.
|
||||||
|
|
||||||
|
To check how the new exchange behaves, you can use the following snippet:
|
||||||
|
|
||||||
|
``` python
|
||||||
|
import ccxt
|
||||||
|
from datetime import datetime
|
||||||
|
from freqtrade.data.converter import parse_ticker_dataframe
|
||||||
|
ct = ccxt.binance()
|
||||||
|
timeframe = "1d"
|
||||||
|
pair = "XLM/BTC" # Make sure to use a pair that exists on that exchange!
|
||||||
|
raw = ct.fetch_ohlcv(pair, timeframe=timeframe)
|
||||||
|
|
||||||
|
# convert to dataframe
|
||||||
|
df1 = parse_ticker_dataframe(raw, timeframe, pair=pair, drop_incomplete=False)
|
||||||
|
|
||||||
|
print(df1["date"].tail(1))
|
||||||
|
print(datetime.utcnow())
|
||||||
|
```
|
||||||
|
|
||||||
|
``` output
|
||||||
|
19 2019-06-08 00:00:00+00:00
|
||||||
|
2019-06-09 12:30:27.873327
|
||||||
|
```
|
||||||
|
|
||||||
|
The output will show the last entry from the Exchange as well as the current UTC date.
|
||||||
|
If the day shows the same day, then the last candle can be assumed as incomplete and should be dropped (leave the setting `"ohlcv_partial_candle"` from the exchange-class untouched / True). Otherwise, set `"ohlcv_partial_candle"` to `False` to not drop Candles (shown in the example above).
|
||||||
|
|
||||||
## Creating a release
|
## Creating a release
|
||||||
|
|
||||||
This part of the documentation is aimed at maintainers, and shows how to create a release.
|
This part of the documentation is aimed at maintainers, and shows how to create a release.
|
||||||
|
@ -95,9 +140,9 @@ git checkout develop
|
||||||
git checkout -b new_release
|
git checkout -b new_release
|
||||||
```
|
```
|
||||||
|
|
||||||
* edit `freqtrade/__init__.py` and add the desired version (for example `0.18.0`)
|
* Edit `freqtrade/__init__.py` and add the desired version (for example `0.18.0`)
|
||||||
* Commit this part
|
* Commit this part
|
||||||
* push that branch to the remote and create a PR
|
* push that branch to the remote and create a PR against the master branch
|
||||||
|
|
||||||
### create changelog from git commits
|
### create changelog from git commits
|
||||||
|
|
||||||
|
@ -108,10 +153,12 @@ git log --oneline --no-decorate --no-merges master..develop
|
||||||
|
|
||||||
### Create github release / tag
|
### Create github release / tag
|
||||||
|
|
||||||
|
* 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).
|
||||||
* use the above changelog as release comment (as codeblock)
|
* Use the above changelog as release comment (as codeblock)
|
||||||
|
|
||||||
### After-release
|
### After-release
|
||||||
|
|
||||||
* update version in develop to next valid version and postfix that with `-dev` (`0.18.0 -> 0.18.1-dev`)
|
* Update version in develop to next valid version and postfix that with `-dev` (`0.18.0 -> 0.18.1-dev`).
|
||||||
|
* Create a PR against develop to update that branch.
|
||||||
|
|
204
docs/docker.md
Normal file
204
docs/docker.md
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
# Using FreqTrade with Docker
|
||||||
|
|
||||||
|
## Install Docker
|
||||||
|
|
||||||
|
Start by downloading and installing Docker CE for your platform:
|
||||||
|
|
||||||
|
* [Mac](https://docs.docker.com/docker-for-mac/install/)
|
||||||
|
* [Windows](https://docs.docker.com/docker-for-windows/install/)
|
||||||
|
* [Linux](https://docs.docker.com/install/)
|
||||||
|
|
||||||
|
Once you have Docker installed, simply prepare the config file (e.g. `config.json`) and run the image for `freqtrade` as explained below.
|
||||||
|
|
||||||
|
## Download the official FreqTrade docker image
|
||||||
|
|
||||||
|
Pull the image from docker hub.
|
||||||
|
|
||||||
|
Branches / tags available can be checked out on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/tags/).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker pull freqtradeorg/freqtrade:develop
|
||||||
|
# Optionally tag the repository so the run-commands remain shorter
|
||||||
|
docker tag freqtradeorg/freqtrade:develop freqtrade
|
||||||
|
```
|
||||||
|
|
||||||
|
To update the image, simply run the above commands again and restart your running container.
|
||||||
|
|
||||||
|
Should you require additional libraries, please [build the image yourself](#build-your-own-docker-image).
|
||||||
|
|
||||||
|
### Prepare the configuration files
|
||||||
|
|
||||||
|
Even though you will use docker, you'll still need some files from the github repository.
|
||||||
|
|
||||||
|
#### Clone the git repository
|
||||||
|
|
||||||
|
Linux/Mac/Windows with WSL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/freqtrade/freqtrade.git
|
||||||
|
```
|
||||||
|
|
||||||
|
Windows with docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone --config core.autocrlf=input https://github.com/freqtrade/freqtrade.git
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Copy `config.json.example` to `config.json`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd freqtrade
|
||||||
|
cp -n config.json.example config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
> To understand the configuration options, please refer to the [Bot Configuration](configuration.md) page.
|
||||||
|
|
||||||
|
#### Create your database file
|
||||||
|
|
||||||
|
Production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
touch tradesv3.sqlite
|
||||||
|
````
|
||||||
|
|
||||||
|
Dry-Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
touch tradesv3.dryrun.sqlite
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
Make sure to use the path to this file when starting the bot in docker.
|
||||||
|
|
||||||
|
### Build your own Docker image
|
||||||
|
|
||||||
|
Best start by pulling the official docker image from dockerhub as explained [here](#download-the-official-docker-image) to speed up building.
|
||||||
|
|
||||||
|
To add additional libraries to your docker image, best check out [Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/Dockerfile.technical) which adds the [technical](https://github.com/freqtrade/technical) module to the image.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t freqtrade -f Dockerfile.technical .
|
||||||
|
```
|
||||||
|
|
||||||
|
If you are developing using Docker, use `Dockerfile.develop` to build a dev Docker image, which will also set up develop dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -f Dockerfile.develop -t freqtrade-dev .
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
For security reasons, your configuration file will not be included in the image, you will need to bind mount it. It is also advised to bind mount an SQLite database file (see the "5. Run a restartable docker image" section) to keep it between updates.
|
||||||
|
|
||||||
|
#### Verify the Docker image
|
||||||
|
|
||||||
|
After the build process you can verify that the image was created with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker images
|
||||||
|
```
|
||||||
|
|
||||||
|
The output should contain the freqtrade image.
|
||||||
|
|
||||||
|
### Run the Docker image
|
||||||
|
|
||||||
|
You can run a one-off container that is immediately deleted upon exiting with the following command (`config.json` must be in the current working directory):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Warning
|
||||||
|
In this example, the database will be created inside the docker instance and will be lost when you will refresh your image.
|
||||||
|
|
||||||
|
#### Adjust timezone
|
||||||
|
|
||||||
|
By default, the container will use UTC timezone.
|
||||||
|
Should you find this irritating please add the following to your docker commands:
|
||||||
|
|
||||||
|
##### Linux
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
-v /etc/timezone:/etc/timezone:ro
|
||||||
|
|
||||||
|
# Complete command:
|
||||||
|
docker run --rm -v /etc/timezone:/etc/timezone:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
|
||||||
|
```
|
||||||
|
|
||||||
|
##### MacOS
|
||||||
|
|
||||||
|
There is known issue in OSX Docker versions after 17.09.1, whereby `/etc/localtime` cannot be shared causing Docker to not start. A work-around for this is to start with the following cmd.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -e TZ=`ls -la /etc/localtime | cut -d/ -f8-9` -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
|
||||||
|
```
|
||||||
|
|
||||||
|
More information on this docker issue and work-around can be read [here](https://github.com/docker/for-mac/issues/2396).
|
||||||
|
|
||||||
|
### Run a restartable docker image
|
||||||
|
|
||||||
|
To run a restartable instance in the background (feel free to place your configuration and database files wherever it feels comfortable on your filesystem).
|
||||||
|
|
||||||
|
#### Move your config file and database
|
||||||
|
|
||||||
|
The following will assume that you place your configuration / database files to `~/.freqtrade`, which is a hidden folder in your home directory. Feel free to use a different folder and replace the folder in the upcomming commands.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir ~/.freqtrade
|
||||||
|
mv config.json ~/.freqtrade
|
||||||
|
mv tradesv3.sqlite ~/.freqtrade
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Run the docker image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name freqtrade \
|
||||||
|
-v ~/.freqtrade/config.json:/freqtrade/config.json \
|
||||||
|
-v ~/.freqtrade/user_data/:/freqtrade/user_data \
|
||||||
|
-v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
|
||||||
|
freqtrade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
db-url defaults to `sqlite:///tradesv3.sqlite` but it defaults to `sqlite://` if `dry_run=True` is being used.
|
||||||
|
To override this behaviour use a custom db-url value: i.e.: `--db-url sqlite:///tradesv3.dryrun.sqlite`
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
All available bot command line parameters can be added to the end of the `docker run` command.
|
||||||
|
|
||||||
|
### Monitor your Docker instance
|
||||||
|
|
||||||
|
You can use the following commands to monitor and manage your container:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker logs freqtrade
|
||||||
|
docker logs -f freqtrade
|
||||||
|
docker restart freqtrade
|
||||||
|
docker stop freqtrade
|
||||||
|
docker start freqtrade
|
||||||
|
```
|
||||||
|
|
||||||
|
For more information on how to operate Docker, please refer to the [official Docker documentation](https://docs.docker.com/).
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container.
|
||||||
|
|
||||||
|
### Backtest with docker
|
||||||
|
|
||||||
|
The following assumes that the download/setup of the docker image have been completed successfully.
|
||||||
|
Also, backtest-data should be available at `~/.freqtrade/user_data/`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name freqtrade \
|
||||||
|
-v /etc/localtime:/etc/localtime:ro \
|
||||||
|
-v ~/.freqtrade/config.json:/freqtrade/config.json \
|
||||||
|
-v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
|
||||||
|
-v ~/.freqtrade/user_data/:/freqtrade/user_data/ \
|
||||||
|
freqtrade --strategy AwsomelyProfitableStrategy backtesting
|
||||||
|
```
|
||||||
|
|
||||||
|
Head over to the [Backtesting Documentation](backtesting.md) for more details.
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
Additional bot command line parameters can be appended after the image name (`freqtrade` in the above example).
|
|
@ -12,7 +12,7 @@ and still take a long time.
|
||||||
## Prepare Hyperopting
|
## Prepare Hyperopting
|
||||||
|
|
||||||
Before we start digging into Hyperopt, we recommend you to take a look at
|
Before we start digging into Hyperopt, we recommend you to take a look at
|
||||||
an example hyperopt file located into [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/test_hyperopt.py)
|
an example hyperopt file located into [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt.py)
|
||||||
|
|
||||||
Configuring hyperopt is similar to writing your own strategy, and many tasks will be similar and a lot of code can be copied across from the strategy.
|
Configuring hyperopt is similar to writing your own strategy, and many tasks will be similar and a lot of code can be copied across from the strategy.
|
||||||
|
|
||||||
|
@ -71,6 +71,11 @@ Place the corresponding settings into the following methods
|
||||||
The configuration and rules are the same than for buy signals.
|
The configuration and rules are the same than for buy signals.
|
||||||
To avoid naming collisions in the search-space, please prefix all sell-spaces with `sell-`.
|
To avoid naming collisions in the search-space, please prefix all sell-spaces with `sell-`.
|
||||||
|
|
||||||
|
#### Using ticker-interval as part of the Strategy
|
||||||
|
|
||||||
|
The Strategy exposes the ticker-interval as `self.ticker_interval`. The same value is available as class-attribute `HyperoptName.ticker_interval`.
|
||||||
|
In the case of the linked sample-value this would be `SampleHyperOpts.ticker_interval`.
|
||||||
|
|
||||||
## Solving a Mystery
|
## Solving a Mystery
|
||||||
|
|
||||||
Let's say you are curious: should you use MACD crossings or lower Bollinger
|
Let's say you are curious: should you use MACD crossings or lower Bollinger
|
||||||
|
@ -122,9 +127,10 @@ So let's write the buy strategy using these values:
|
||||||
dataframe['macd'], dataframe['macdsignal']
|
dataframe['macd'], dataframe['macdsignal']
|
||||||
))
|
))
|
||||||
|
|
||||||
dataframe.loc[
|
if conditions:
|
||||||
reduce(lambda x, y: x & y, conditions),
|
dataframe.loc[
|
||||||
'buy'] = 1
|
reduce(lambda x, y: x & y, conditions),
|
||||||
|
'buy'] = 1
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
|
|
|
@ -21,8 +21,8 @@ Freqtrade is a cryptocurrency trading bot written in Python.
|
||||||
|
|
||||||
We strongly recommend you to have basic coding skills and Python knowledge. Do not hesitate to read the source code and understand the mechanisms of this bot, algorithms and techniques implemented in it.
|
We strongly recommend you to have basic coding skills and Python knowledge. Do not hesitate to read the source code and understand the mechanisms of this bot, algorithms and techniques implemented in it.
|
||||||
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Based on Python 3.6+: For botting on any operating system — Windows, macOS and Linux.
|
- Based on Python 3.6+: For botting on any operating system — Windows, macOS and Linux.
|
||||||
- Persistence: Persistence is achieved through sqlite database.
|
- Persistence: Persistence is achieved through sqlite database.
|
||||||
- Dry-run mode: Run the bot without playing money.
|
- Dry-run mode: Run the bot without playing money.
|
||||||
|
@ -31,17 +31,19 @@ Freqtrade is a cryptocurrency trading bot written in Python.
|
||||||
- Edge position sizing: Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market.
|
- Edge position sizing: Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market.
|
||||||
- Whitelist crypto-currencies: Select which crypto-currency you want to trade or use dynamic whitelists based on market (pair) trade volume.
|
- Whitelist crypto-currencies: Select which crypto-currency you want to trade or use dynamic whitelists based on market (pair) trade volume.
|
||||||
- Blacklist crypto-currencies: Select which crypto-currency you want to avoid.
|
- Blacklist crypto-currencies: Select which crypto-currency you want to avoid.
|
||||||
- Manageable via Telegram: Manage the bot with Telegram.
|
- Manageable via Telegram or REST APi: Manage the bot with Telegram or via the builtin REST API.
|
||||||
- Display profit/loss in fiat: Display your profit/loss in any of 33 fiat currencies supported.
|
- Display profit/loss in fiat: Display your profit/loss in any of 33 fiat currencies supported.
|
||||||
- Daily summary of profit/loss: Receive the daily summary of your profit/loss.
|
- Daily summary of profit/loss: Receive the daily summary of your profit/loss.
|
||||||
- Performance status report: Receive the performance status of your current trades.
|
- Performance status report: Receive the performance status of your current trades.
|
||||||
|
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
### Up to date clock
|
### Up to date clock
|
||||||
|
|
||||||
The clock on the system running the bot must be accurate, synchronized to a NTP server frequently enough to avoid problems with communication to the exchanges.
|
The clock on the system running the bot must be accurate, synchronized to a NTP server frequently enough to avoid problems with communication to the exchanges.
|
||||||
|
|
||||||
### Hardware requirements
|
### Hardware requirements
|
||||||
|
|
||||||
To run this bot we recommend you a cloud instance with a minimum of:
|
To run this bot we recommend you a cloud instance with a minimum of:
|
||||||
|
|
||||||
- 2GB RAM
|
- 2GB RAM
|
||||||
|
@ -49,6 +51,7 @@ To run this bot we recommend you a cloud instance with a minimum of:
|
||||||
- 2vCPU
|
- 2vCPU
|
||||||
|
|
||||||
### Software requirements
|
### Software requirements
|
||||||
|
|
||||||
- Python 3.6.x
|
- Python 3.6.x
|
||||||
- pip (pip3)
|
- pip (pip3)
|
||||||
- git
|
- git
|
||||||
|
@ -56,12 +59,13 @@ To run this bot we recommend you a cloud instance with a minimum of:
|
||||||
- virtualenv (Recommended)
|
- virtualenv (Recommended)
|
||||||
- Docker (Recommended)
|
- Docker (Recommended)
|
||||||
|
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
Help / Slack
|
Help / Slack
|
||||||
For any questions not covered by the documentation or for further information about the bot, we encourage you to join our Slack channel.
|
For any questions not covered by the documentation or for further information about the bot, we encourage you to join our Slack channel.
|
||||||
|
|
||||||
Click [here](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) to join Slack channel.
|
Click [here](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LWEyODBiNzkzNzcyNzU0MWYyYzE5NjIyOTQxMzBmMGUxOTIzM2YyN2Y4NWY1YTEwZDgwYTRmMzE2NmM5ZmY2MTg) to join Slack channel.
|
||||||
|
|
||||||
## Ready to try?
|
## Ready to try?
|
||||||
|
|
||||||
Begin by reading our installation guide [here](installation).
|
Begin by reading our installation guide [here](installation).
|
||||||
|
|
|
@ -1,58 +1,21 @@
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
This page explains how to prepare your environment for running the bot.
|
This page explains how to prepare your environment for running the bot.
|
||||||
|
|
||||||
## Prerequisite
|
## Prerequisite
|
||||||
|
|
||||||
Before running your bot in production you will need to setup few
|
Before running your bot in production you will need to setup few
|
||||||
external API. In production mode, the bot required valid Bittrex API
|
external API. In production mode, the bot will require valid Exchange API
|
||||||
credentials and a Telegram bot (optional but recommended).
|
credentials. We also recommend a [Telegram bot](telegram-usage.md#setup-your-telegram-bot) (optional but recommended).
|
||||||
|
|
||||||
- [Setup your exchange account](#setup-your-exchange-account)
|
- [Setup your exchange account](#setup-your-exchange-account)
|
||||||
- [Backtesting commands](#setup-your-telegram-bot)
|
|
||||||
|
|
||||||
### Setup your exchange account
|
### Setup your exchange account
|
||||||
*To be completed, please feel free to complete this section.*
|
|
||||||
|
|
||||||
### Setup your Telegram bot
|
You will need to create API Keys (Usually you get `key` and `secret`) from the Exchange website and insert this into the appropriate fields in the configuration or when asked by the installation script.
|
||||||
The only things you need is a working Telegram bot and its API token.
|
|
||||||
Below we explain how to create your Telegram Bot, and how to get your
|
|
||||||
Telegram user id.
|
|
||||||
|
|
||||||
### 1. Create your Telegram bot
|
|
||||||
|
|
||||||
**1.1. Start a chat with https://telegram.me/BotFather**
|
|
||||||
|
|
||||||
**1.2. Send the message `/newbot`. ** *BotFather response:*
|
|
||||||
```
|
|
||||||
Alright, a new bot. How are we going to call it? Please choose a name for your bot.
|
|
||||||
```
|
|
||||||
|
|
||||||
**1.3. Choose the public name of your bot (e.x. `Freqtrade bot`)**
|
|
||||||
*BotFather response:*
|
|
||||||
```
|
|
||||||
Good. Now let's choose a username for your bot. It must end in `bot`. Like this, for example: TetrisBot or tetris_bot.
|
|
||||||
```
|
|
||||||
**1.4. Choose the name id of your bot (e.x "`My_own_freqtrade_bot`")**
|
|
||||||
|
|
||||||
**1.5. Father bot will return you the token (API key)**<br/>
|
|
||||||
Copy it and keep it you will use it for the config parameter `token`.
|
|
||||||
*BotFather response:*
|
|
||||||
```hl_lines="4"
|
|
||||||
Done! Congratulations on your new bot. You will find it at t.me/My_own_freqtrade_bot. You can now add a description, about section and profile picture for your bot, see /help for a list of commands. By the way, when you've finished creating your cool bot, ping our Bot Support if you want a better username for it. Just make sure the bot is fully operational before you do this.
|
|
||||||
|
|
||||||
Use this token to access the HTTP API:
|
|
||||||
521095879:AAEcEZEL7ADJ56FtG_qD0bQJSKETbXCBCi0
|
|
||||||
|
|
||||||
For a description of the Bot API, see this page: https://core.telegram.org/bots/api
|
|
||||||
```
|
|
||||||
**1.6. Don't forget to start the conversation with your bot, by clicking /START button**
|
|
||||||
|
|
||||||
### 2. Get your user id
|
|
||||||
**2.1. Talk to https://telegram.me/userinfobot**
|
|
||||||
|
|
||||||
**2.2. Get your "Id", you will use it for the config parameter
|
|
||||||
`chat_id`.**
|
|
||||||
<hr/>
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
Freqtrade provides a Linux/MacOS script to install all dependencies and help you to configure the bot.
|
Freqtrade provides a Linux/MacOS script to install all dependencies and help you to configure the bot.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
@ -61,9 +24,10 @@ cd freqtrade
|
||||||
git checkout develop
|
git checkout develop
|
||||||
./setup.sh --install
|
./setup.sh --install
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Windows installation is explained [here](#windows).
|
Windows installation is explained [here](#windows).
|
||||||
<hr/>
|
|
||||||
## Easy Installation - Linux Script
|
## Easy Installation - Linux Script
|
||||||
|
|
||||||
If you are on Debian, Ubuntu or MacOS a freqtrade provides a script to Install, Update, Configure, and Reset your bot.
|
If you are on Debian, Ubuntu or MacOS a freqtrade provides a script to Install, Update, Configure, and Reset your bot.
|
||||||
|
@ -101,189 +65,6 @@ Config parameter is a `config.json` configurator. This script will ask you quest
|
||||||
|
|
||||||
------
|
------
|
||||||
|
|
||||||
## Automatic Installation - Docker
|
|
||||||
|
|
||||||
Start by downloading Docker for your platform:
|
|
||||||
|
|
||||||
* [Mac](https://www.docker.com/products/docker#/mac)
|
|
||||||
* [Windows](https://www.docker.com/products/docker#/windows)
|
|
||||||
* [Linux](https://www.docker.com/products/docker#/linux)
|
|
||||||
|
|
||||||
Once you have Docker installed, simply create the config file (e.g. `config.json`) and then create a Docker image for `freqtrade` using the Dockerfile in this repo.
|
|
||||||
|
|
||||||
### 1. Prepare the Bot
|
|
||||||
|
|
||||||
**1.1. Clone the git repository**
|
|
||||||
|
|
||||||
Linux/Mac/Windows with WSL
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/freqtrade/freqtrade.git
|
|
||||||
```
|
|
||||||
|
|
||||||
Windows with docker
|
|
||||||
```bash
|
|
||||||
git clone --config core.autocrlf=input https://github.com/freqtrade/freqtrade.git
|
|
||||||
```
|
|
||||||
|
|
||||||
**1.2. (Optional) Checkout the develop branch**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git checkout develop
|
|
||||||
```
|
|
||||||
|
|
||||||
**1.3. Go into the new directory**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd freqtrade
|
|
||||||
```
|
|
||||||
|
|
||||||
**1.4. Copy `config.json.example` to `config.json`**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp -n config.json.example config.json
|
|
||||||
```
|
|
||||||
|
|
||||||
> To edit the config please refer to the [Bot Configuration](configuration.md) page.
|
|
||||||
|
|
||||||
**1.5. Create your database file *(optional - the bot will create it if it is missing)**
|
|
||||||
|
|
||||||
Production
|
|
||||||
|
|
||||||
```bash
|
|
||||||
touch tradesv3.sqlite
|
|
||||||
````
|
|
||||||
|
|
||||||
Dry-Run
|
|
||||||
|
|
||||||
```bash
|
|
||||||
touch tradesv3.dryrun.sqlite
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Download or build the docker image
|
|
||||||
|
|
||||||
Either use the prebuilt image from docker hub - or build the image yourself if you would like more control on which version is used.
|
|
||||||
|
|
||||||
Branches / tags available can be checked out on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/tags/).
|
|
||||||
|
|
||||||
**2.1. Download the docker image**
|
|
||||||
|
|
||||||
Pull the image from docker hub and (optionally) change the name of the image
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker pull freqtradeorg/freqtrade:develop
|
|
||||||
# Optionally tag the repository so the run-commands remain shorter
|
|
||||||
docker tag freqtradeorg/freqtrade:develop freqtrade
|
|
||||||
```
|
|
||||||
|
|
||||||
To update the image, simply run the above commands again and restart your running container.
|
|
||||||
|
|
||||||
**2.2. Build the Docker image**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd freqtrade
|
|
||||||
docker build -t freqtrade .
|
|
||||||
```
|
|
||||||
|
|
||||||
If you are developing using Docker, use `Dockerfile.develop` to build a dev Docker image, which will also set up develop dependencies:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker build -f ./Dockerfile.develop -t freqtrade-dev .
|
|
||||||
```
|
|
||||||
|
|
||||||
For security reasons, your configuration file will not be included in the image, you will need to bind mount it. It is also advised to bind mount an SQLite database file (see the "5. Run a restartable docker image" section) to keep it between updates.
|
|
||||||
|
|
||||||
### 3. Verify the Docker image
|
|
||||||
|
|
||||||
After the build process you can verify that the image was created with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker images
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Run the Docker image
|
|
||||||
|
|
||||||
You can run a one-off container that is immediately deleted upon exiting with the following command (`config.json` must be in the current working directory):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run --rm -v /etc/localtime:/etc/localtime:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
|
|
||||||
```
|
|
||||||
|
|
||||||
There is known issue in OSX Docker versions after 17.09.1, whereby /etc/localtime cannot be shared causing Docker to not start. A work-around for this is to start with the following cmd.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run --rm -e TZ=`ls -la /etc/localtime | cut -d/ -f8-9` -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
|
|
||||||
```
|
|
||||||
|
|
||||||
More information on this docker issue and work-around can be read [here](https://github.com/docker/for-mac/issues/2396).
|
|
||||||
|
|
||||||
In this example, the database will be created inside the docker instance and will be lost when you will refresh your image.
|
|
||||||
|
|
||||||
### 5. Run a restartable docker image
|
|
||||||
|
|
||||||
To run a restartable instance in the background (feel free to place your configuration and database files wherever it feels comfortable on your filesystem).
|
|
||||||
|
|
||||||
**5.1. Move your config file and database**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir ~/.freqtrade
|
|
||||||
mv config.json ~/.freqtrade
|
|
||||||
mv tradesv3.sqlite ~/.freqtrade
|
|
||||||
```
|
|
||||||
|
|
||||||
**5.2. Run the docker image**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d \
|
|
||||||
--name freqtrade \
|
|
||||||
-v /etc/localtime:/etc/localtime:ro \
|
|
||||||
-v ~/.freqtrade/config.json:/freqtrade/config.json \
|
|
||||||
-v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
|
|
||||||
freqtrade --db-url sqlite:///tradesv3.sqlite
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
db-url defaults to `sqlite:///tradesv3.sqlite` but it defaults to `sqlite://` if `dry_run=True` is being used.
|
|
||||||
To override this behaviour use a custom db-url value: i.e.: `--db-url sqlite:///tradesv3.dryrun.sqlite`
|
|
||||||
|
|
||||||
### 6. Monitor your Docker instance
|
|
||||||
|
|
||||||
You can then use the following commands to monitor and manage your container:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker logs freqtrade
|
|
||||||
docker logs -f freqtrade
|
|
||||||
docker restart freqtrade
|
|
||||||
docker stop freqtrade
|
|
||||||
docker start freqtrade
|
|
||||||
```
|
|
||||||
|
|
||||||
For more information on how to operate Docker, please refer to the [official Docker documentation](https://docs.docker.com/).
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container.
|
|
||||||
|
|
||||||
### 7. Backtest with docker
|
|
||||||
|
|
||||||
The following assumes that the above steps (1-4) have been completed successfully.
|
|
||||||
Also, backtest-data should be available at `~/.freqtrade/user_data/`.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d \
|
|
||||||
--name freqtrade \
|
|
||||||
-v /etc/localtime:/etc/localtime:ro \
|
|
||||||
-v ~/.freqtrade/config.json:/freqtrade/config.json \
|
|
||||||
-v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
|
|
||||||
-v ~/.freqtrade/user_data/:/freqtrade/user_data/ \
|
|
||||||
freqtrade --strategy AwsomelyProfitableStrategy backtesting
|
|
||||||
```
|
|
||||||
|
|
||||||
Head over to the [Backtesting Documentation](backtesting.md) for more details.
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
Additional parameters can be appended after the image name (`freqtrade` in the above example).
|
|
||||||
|
|
||||||
------
|
|
||||||
|
|
||||||
## Custom Installation
|
## Custom Installation
|
||||||
|
|
||||||
We've included/collected install instructions for Ubuntu 16.04, MacOS, and Windows. These are guidelines and your success may vary with other distros.
|
We've included/collected install instructions for Ubuntu 16.04, MacOS, and Windows. These are guidelines and your success may vary with other distros.
|
||||||
|
@ -326,7 +107,7 @@ conda activate freqtrade
|
||||||
conda install scipy pandas numpy
|
conda install scipy pandas numpy
|
||||||
|
|
||||||
sudo apt install libffi-dev
|
sudo apt install libffi-dev
|
||||||
python3 -m pip install -r requirements-pi.txt
|
python3 -m pip install -r requirements-common.txt
|
||||||
python3 -m pip install -e .
|
python3 -m pip install -e .
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -409,7 +190,7 @@ If this is the first time you run the bot, ensure you are running it in Dry-run
|
||||||
python3.6 freqtrade -c config.json
|
python3.6 freqtrade -c config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
*Note*: If you run the bot on a server, you should consider using [Docker](#automatic-installation---docker) a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout.
|
*Note*: If you run the bot on a server, you should consider using [Docker](docker.md) or a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout.
|
||||||
|
|
||||||
#### 7. [Optional] Configure `freqtrade` as a `systemd` service
|
#### 7. [Optional] Configure `freqtrade` as a `systemd` service
|
||||||
|
|
||||||
|
@ -437,14 +218,13 @@ The `freqtrade.service.watchdog` file contains an example of the service unit co
|
||||||
as the watchdog.
|
as the watchdog.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
The sd_notify communication between the bot and the systemd service manager will not work if the bot runs in a
|
The sd_notify communication between the bot and the systemd service manager will not work if the bot runs in a Docker container.
|
||||||
Docker container.
|
|
||||||
|
|
||||||
------
|
------
|
||||||
|
|
||||||
## Windows
|
## Windows
|
||||||
|
|
||||||
We recommend that Windows users use [Docker](#docker) 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).
|
||||||
|
|
||||||
If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work.
|
If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work.
|
||||||
If that is not available on your system, feel free to try the instructions below, which led to success for some.
|
If that is not available on your system, feel free to try the instructions below, which led to success for some.
|
||||||
|
@ -488,7 +268,7 @@ error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++
|
||||||
|
|
||||||
Unfortunately, many packages requiring compilation don't provide a pre-build wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use.
|
Unfortunately, many packages requiring compilation don't provide a pre-build wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use.
|
||||||
|
|
||||||
The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building c code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or docker first.
|
The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building c code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker](docker.md) first.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -1,63 +1,83 @@
|
||||||
# Plotting
|
# Plotting
|
||||||
This page explains how to plot prices, indicator, profits.
|
|
||||||
|
This page explains how to plot prices, indicators and profits.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Plotting scripts use Plotly library. Install/upgrade it with:
|
Plotting scripts use Plotly library. Install/upgrade it with:
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
pip install -U -r requirements-plot.txt
|
||||||
```
|
```
|
||||||
pip install --upgrade plotly
|
|
||||||
```
|
|
||||||
|
|
||||||
At least version 2.3.0 is required.
|
|
||||||
|
|
||||||
## Plot price and indicators
|
## Plot price and indicators
|
||||||
|
|
||||||
Usage for the price plotter:
|
Usage for the price plotter:
|
||||||
|
|
||||||
```
|
``` bash
|
||||||
script/plot_dataframe.py [-h] [-p pairs] [--live]
|
python3 script/plot_dataframe.py [-h] [-p pairs] [--live]
|
||||||
```
|
```
|
||||||
|
|
||||||
Example
|
Example
|
||||||
```
|
|
||||||
python scripts/plot_dataframe.py -p BTC/ETH
|
``` bash
|
||||||
|
python3 scripts/plot_dataframe.py -p BTC/ETH
|
||||||
```
|
```
|
||||||
|
|
||||||
The `-p` pairs argument, can be used to specify
|
The `-p` pairs argument can be used to specify pairs you would like to plot.
|
||||||
pairs you would like to plot.
|
|
||||||
|
|
||||||
**Advanced use**
|
Specify custom indicators.
|
||||||
|
Use `--indicators1` for the main plot and `--indicators2` for the subplot below (if values are in a different range than prices).
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
python3 scripts/plot_dataframe.py -p BTC/ETH --indicators1 sma,ema --indicators2 macd
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced use
|
||||||
|
|
||||||
To plot multiple pairs, separate them with a comma:
|
To plot multiple pairs, separate them with a comma:
|
||||||
```
|
|
||||||
python scripts/plot_dataframe.py -p BTC/ETH,XRP/ETH
|
``` bash
|
||||||
|
python3 scripts/plot_dataframe.py -p BTC/ETH,XRP/ETH
|
||||||
```
|
```
|
||||||
|
|
||||||
To plot the current live price use the `--live` flag:
|
To plot the current live price use the `--live` flag:
|
||||||
```
|
|
||||||
python scripts/plot_dataframe.py -p BTC/ETH --live
|
``` 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
|
||||||
|
python3 scripts/plot_dataframe.py -p BTC/ETH --timerange=100-200
|
||||||
```
|
```
|
||||||
python scripts/plot_dataframe.py -p BTC/ETH --timerange=100-200
|
|
||||||
```
|
|
||||||
Timerange doesn't work with live data.
|
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:
|
||||||
```
|
|
||||||
python scripts/plot_dataframe.py --db-url sqlite:///tradesv3.dry_run.sqlite -p BTC/ETH
|
``` bash
|
||||||
|
python3 scripts/plot_dataframe.py --db-url sqlite:///tradesv3.dry_run.sqlite -p BTC/ETH --trade-source DB
|
||||||
```
|
```
|
||||||
|
|
||||||
To plot a test strategy the strategy should have first be backtested.
|
To plot trades from a backtesting result, use `--export-filename <filename>`
|
||||||
The results may then be plotted with the -s argument:
|
|
||||||
|
``` bash
|
||||||
|
python3 scripts/plot_dataframe.py --export-filename user_data/backtest_data/backtest-result.json -p BTC/ETH
|
||||||
```
|
```
|
||||||
python scripts/plot_dataframe.py -s Strategy_Name -p BTC/ETH --datadir user_data/data/<exchange_name>/
|
|
||||||
|
To plot a custom strategy the strategy should have first be backtested.
|
||||||
|
The results may then be plotted with the -s argument:
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
python3 scripts/plot_dataframe.py -s Strategy_Name -p BTC/ETH --datadir user_data/data/<exchange_name>/
|
||||||
```
|
```
|
||||||
|
|
||||||
## Plot profit
|
## Plot profit
|
||||||
|
|
||||||
The profit plotter show a picture with three plots:
|
The profit plotter shows a picture with three plots:
|
||||||
|
|
||||||
1) Average closing price for all pairs
|
1) Average closing price for all pairs
|
||||||
2) The summarized profit made by backtesting.
|
2) The summarized profit made by backtesting.
|
||||||
Note that this is not the real-world profit, but
|
Note that this is not the real-world profit, but
|
||||||
|
@ -67,7 +87,7 @@ The profit plotter show a picture with three plots:
|
||||||
The first graph is good to get a grip of how the overall market
|
The first graph is good to get a grip of how the overall market
|
||||||
progresses.
|
progresses.
|
||||||
|
|
||||||
The second graph will show how you algorithm works or doesnt.
|
The second graph will show how your algorithm works or doesn't.
|
||||||
Perhaps you want an algorithm that steadily makes small profits,
|
Perhaps you want an algorithm that steadily makes small profits,
|
||||||
or one that acts less seldom, but makes big swings.
|
or one that acts less seldom, but makes big swings.
|
||||||
|
|
||||||
|
@ -76,13 +96,14 @@ that makes profit spikes.
|
||||||
|
|
||||||
Usage for the profit plotter:
|
Usage for the profit plotter:
|
||||||
|
|
||||||
```
|
``` bash
|
||||||
script/plot_profit.py [-h] [-p pair] [--datadir directory] [--ticker_interval num]
|
python3 script/plot_profit.py [-h] [-p pair] [--datadir directory] [--ticker_interval num]
|
||||||
```
|
```
|
||||||
|
|
||||||
The `-p` pair argument, can be used to plot a single pair
|
The `-p` pair argument, can be used to plot a single pair
|
||||||
|
|
||||||
Example
|
Example
|
||||||
```
|
|
||||||
|
``` bash
|
||||||
python3 scripts/plot_profit.py --datadir ../freqtrade/freqtrade/tests/testdata-20171221/ -p LTC/BTC
|
python3 scripts/plot_profit.py --datadir ../freqtrade/freqtrade/tests/testdata-20171221/ -p LTC/BTC
|
||||||
```
|
```
|
||||||
|
|
193
docs/rest-api.md
Normal file
193
docs/rest-api.md
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
# REST API Usage
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Enable the rest API by adding the api_server section to your configuration and setting `api_server.enabled` to `true`.
|
||||||
|
|
||||||
|
Sample configuration:
|
||||||
|
|
||||||
|
``` json
|
||||||
|
"api_server": {
|
||||||
|
"enabled": true,
|
||||||
|
"listen_ip_address": "127.0.0.1",
|
||||||
|
"listen_port": 8080,
|
||||||
|
"username": "Freqtrader",
|
||||||
|
"password": "SuperSecret1!"
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Danger: Security warning
|
||||||
|
By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet and choose a strong, unique password, since others will potentially be able to control your bot.
|
||||||
|
|
||||||
|
!!! Danger: Password selection
|
||||||
|
Please make sure to select a very strong, unique password to protect your bot from unauthorized access.
|
||||||
|
|
||||||
|
You can then access the API by going to `http://127.0.0.1:8080/api/v1/version` to check if the API is running correctly.
|
||||||
|
|
||||||
|
To generate a secure password, either use a password manager, or use the below code snipped.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
import secrets
|
||||||
|
secrets.token_hex()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration with docker
|
||||||
|
|
||||||
|
If you run your bot using docker, you'll need to have the bot listen to incomming connections. The security is then handled by docker.
|
||||||
|
|
||||||
|
``` json
|
||||||
|
"api_server": {
|
||||||
|
"enabled": true,
|
||||||
|
"listen_ip_address": "0.0.0.0",
|
||||||
|
"listen_port": 8080
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
Add the following to your docker command:
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
-p 127.0.0.1:8080:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
A complete sample-command may then look as follows:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name freqtrade \
|
||||||
|
-v ~/.freqtrade/config.json:/freqtrade/config.json \
|
||||||
|
-v ~/.freqtrade/user_data/:/freqtrade/user_data \
|
||||||
|
-v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
|
||||||
|
-p 127.0.0.1:8080:8080 \
|
||||||
|
freqtrade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Danger "Security warning"
|
||||||
|
By using `-p 8080:8080` the API is available to everyone connecting to the server under the correct port, so others may be able to control your bot.
|
||||||
|
|
||||||
|
## Consuming the API
|
||||||
|
|
||||||
|
You can consume the API by using the script `scripts/rest_client.py`.
|
||||||
|
The client script only requires the `requests` module, so FreqTrade does not need to be installed on the system.
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
python3 scripts/rest_client.py <command> [optional parameters]
|
||||||
|
```
|
||||||
|
|
||||||
|
By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be used, however you can specify a configuration file to override this behaviour.
|
||||||
|
|
||||||
|
### Minimalistic client config
|
||||||
|
|
||||||
|
``` json
|
||||||
|
{
|
||||||
|
"api_server": {
|
||||||
|
"enabled": true,
|
||||||
|
"listen_ip_address": "0.0.0.0",
|
||||||
|
"listen_port": 8080
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
python3 scripts/rest_client.py --config rest_config.json <command> [optional parameters]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available commands
|
||||||
|
|
||||||
|
| Command | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `start` | | Starts the trader
|
||||||
|
| `stop` | | Stops the trader
|
||||||
|
| `stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
|
||||||
|
| `reload_conf` | | Reloads the configuration file
|
||||||
|
| `status` | | Lists all open trades
|
||||||
|
| `status table` | | List all open trades in a table format
|
||||||
|
| `count` | | Displays number of trades used and available
|
||||||
|
| `profit` | | Display a summary of your profit/loss from close trades and some stats about your performance
|
||||||
|
| `forcesell <trade_id>` | | Instantly sells the given trade (Ignoring `minimum_roi`).
|
||||||
|
| `forcesell all` | | Instantly sells all open trades (Ignoring `minimum_roi`).
|
||||||
|
| `forcebuy <pair> [rate]` | | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True)
|
||||||
|
| `performance` | | Show performance of each finished trade grouped by pair
|
||||||
|
| `balance` | | Show account balance per currency
|
||||||
|
| `daily <n>` | 7 | Shows profit or loss per day, over the last n days
|
||||||
|
| `whitelist` | | Show the current whitelist
|
||||||
|
| `blacklist [pair]` | | Show the current blacklist, or adds a pair to the blacklist.
|
||||||
|
| `edge` | | Show validated pairs by Edge if it is enabled.
|
||||||
|
| `version` | | Show version
|
||||||
|
|
||||||
|
Possible commands can be listed from the rest-client script using the `help` command.
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
python3 scripts/rest_client.py help
|
||||||
|
```
|
||||||
|
|
||||||
|
``` output
|
||||||
|
Possible commands:
|
||||||
|
balance
|
||||||
|
Get the account balance
|
||||||
|
:returns: json object
|
||||||
|
|
||||||
|
blacklist
|
||||||
|
Show the current blacklist
|
||||||
|
:param add: List of coins to add (example: "BNB/BTC")
|
||||||
|
:returns: json object
|
||||||
|
|
||||||
|
count
|
||||||
|
Returns the amount of open trades
|
||||||
|
:returns: json object
|
||||||
|
|
||||||
|
daily
|
||||||
|
Returns the amount of open trades
|
||||||
|
:returns: json object
|
||||||
|
|
||||||
|
edge
|
||||||
|
Returns information about edge
|
||||||
|
:returns: json object
|
||||||
|
|
||||||
|
forcebuy
|
||||||
|
Buy an asset
|
||||||
|
:param pair: Pair to buy (ETH/BTC)
|
||||||
|
:param price: Optional - price to buy
|
||||||
|
:returns: json object of the trade
|
||||||
|
|
||||||
|
forcesell
|
||||||
|
Force-sell a trade
|
||||||
|
:param tradeid: Id of the trade (can be received via status command)
|
||||||
|
:returns: json object
|
||||||
|
|
||||||
|
performance
|
||||||
|
Returns the performance of the different coins
|
||||||
|
:returns: json object
|
||||||
|
|
||||||
|
profit
|
||||||
|
Returns the profit summary
|
||||||
|
:returns: json object
|
||||||
|
|
||||||
|
reload_conf
|
||||||
|
Reload configuration
|
||||||
|
:returns: json object
|
||||||
|
|
||||||
|
start
|
||||||
|
Start the bot if it's in stopped state.
|
||||||
|
:returns: json object
|
||||||
|
|
||||||
|
status
|
||||||
|
Get the status of open trades
|
||||||
|
:returns: json object
|
||||||
|
|
||||||
|
stop
|
||||||
|
Stop the bot. Use start to restart
|
||||||
|
:returns: json object
|
||||||
|
|
||||||
|
stopbuy
|
||||||
|
Stop buying (but handle sells gracefully).
|
||||||
|
use reload_conf to reset
|
||||||
|
:returns: json object
|
||||||
|
|
||||||
|
version
|
||||||
|
Returns the version of the bot
|
||||||
|
:returns: json object containing the version
|
||||||
|
|
||||||
|
whitelist
|
||||||
|
Show the current whitelist
|
||||||
|
:returns: json object
|
||||||
|
```
|
|
@ -1,4 +1,13 @@
|
||||||
# Stop Loss support
|
# Stop Loss
|
||||||
|
|
||||||
|
The `stoploss` configuration parameter is loss in percentage that should trigger a sale.
|
||||||
|
For example, value `-0.10` will cause immediate sell if the profit dips below -10% for a given trade. This parameter is optional.
|
||||||
|
|
||||||
|
Most of the strategy files already include the optimal `stoploss`
|
||||||
|
value. This parameter is optional. If you use it in the configuration file, it will take over the
|
||||||
|
`stoploss` value from the strategy file.
|
||||||
|
|
||||||
|
## Stop Loss support
|
||||||
|
|
||||||
At this stage the bot contains the following stoploss support modes:
|
At this stage the bot contains the following stoploss support modes:
|
||||||
|
|
||||||
|
@ -16,13 +25,12 @@ In case of stoploss on exchange there is another parameter called `stoploss_on_e
|
||||||
!!! Note
|
!!! Note
|
||||||
Stoploss on exchange is only supported for Binance as of now.
|
Stoploss on exchange is only supported for Binance as of now.
|
||||||
|
|
||||||
|
|
||||||
## Static Stop Loss
|
## Static Stop Loss
|
||||||
|
|
||||||
This is very simple, basically you define a stop loss of x in your strategy file or alternative in the configuration, which
|
This is very simple, basically you define a stop loss of x in your strategy file or alternative in the configuration, which
|
||||||
will overwrite the strategy definition. This will basically try to sell your asset, the second the loss exceeds the defined loss.
|
will overwrite the strategy definition. This will basically try to sell your asset, the second the loss exceeds the defined loss.
|
||||||
|
|
||||||
## Trail Stop Loss
|
## Trailing Stop Loss
|
||||||
|
|
||||||
The initial value for this stop loss, is defined in your strategy or configuration. Just as you would define your Stop Loss normally.
|
The initial value for this stop loss, is defined in your strategy or configuration. Just as you would define your Stop Loss normally.
|
||||||
To enable this Feauture all you have to do is to define the configuration element:
|
To enable this Feauture all you have to do is to define the configuration element:
|
||||||
|
@ -63,3 +71,13 @@ The 0.01 would translate to a 1% stop loss, once you hit 1.1% profit.
|
||||||
You should also make sure to have this value (`trailing_stop_positive_offset`) lower than your minimal ROI, otherwise minimal ROI will apply first and sell your trade.
|
You should also make sure to have this value (`trailing_stop_positive_offset`) lower than your minimal ROI, otherwise minimal ROI will apply first and sell your trade.
|
||||||
|
|
||||||
If `"trailing_only_offset_is_reached": true` then the trailing stoploss is only activated once the offset is reached. Until then, the stoploss remains at the configured`stoploss`.
|
If `"trailing_only_offset_is_reached": true` then the trailing stoploss is only activated once the offset is reached. Until then, the stoploss remains at the configured`stoploss`.
|
||||||
|
|
||||||
|
## Changing stoploss on open trades
|
||||||
|
|
||||||
|
A stoploss on an open trade can be changed by changing the value in the configuration or strategy and use the `/reload_conf` command (alternatively, completely stopping and restarting the bot also works).
|
||||||
|
|
||||||
|
The new stoploss value will be applied to open trades (and corresponding log-messages will be generated).
|
||||||
|
|
||||||
|
### Limitations
|
||||||
|
|
||||||
|
Stoploss values cannot be changed if `trailing_stop` is enabled and the stoploss has already been adjusted, or if [Edge](edge.md) is enabled (since Edge would recalculate stoploss based on the current market situation).
|
||||||
|
|
|
@ -53,6 +53,12 @@ file as reference.**
|
||||||
It is therefore best to use vectorized operations (across the whole dataframe, not loops) and
|
It is therefore best to use vectorized operations (across the whole dataframe, not loops) and
|
||||||
avoid index referencing (`df.iloc[-1]`), but instead use `df.shift()` to get to the previous candle.
|
avoid index referencing (`df.iloc[-1]`), but instead use `df.shift()` to get to the previous candle.
|
||||||
|
|
||||||
|
!!! Warning Using future data
|
||||||
|
Since backtesting passes the full time interval to the `populate_*()` methods, the strategy author
|
||||||
|
needs to take care to avoid having the strategy utilize data from the future.
|
||||||
|
Samples for usage of future data are `dataframe.shift(-1)`, `dataframe.resample("1h")` (this uses the left border of the interval, so moves data from an hour to the start of the hour).
|
||||||
|
They all use data which is not available during regular operations, so these strategies will perform well during backtesting, but will fail / perform badly in dry-runs.
|
||||||
|
|
||||||
### Customize Indicators
|
### Customize Indicators
|
||||||
|
|
||||||
Buy and sell strategies need indicators. You can add more indicators by extending the list contained in the method `populate_indicators()` from your strategy file.
|
Buy and sell strategies need indicators. You can add more indicators by extending the list contained in the method `populate_indicators()` from your strategy file.
|
||||||
|
@ -212,9 +218,12 @@ stoploss = -0.10
|
||||||
```
|
```
|
||||||
|
|
||||||
This would signify a stoploss of -10%.
|
This would signify a stoploss of -10%.
|
||||||
|
|
||||||
|
For the full documentation on stoploss features, look at the dedicated [stoploss page](stoploss.md).
|
||||||
|
|
||||||
If your exchange supports it, it's recommended to also set `"stoploss_on_exchange"` in the order dict, so your stoploss is on the exchange and cannot be missed for network-problems (or other problems).
|
If your exchange supports it, it's recommended to also set `"stoploss_on_exchange"` in the order dict, so your stoploss is on the exchange and cannot be missed for network-problems (or other problems).
|
||||||
|
|
||||||
For more information on order_types please look [here](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md#understand-order_types).
|
For more information on order_types please look [here](configuration.md#understand-order_types).
|
||||||
|
|
||||||
### Ticker interval
|
### Ticker interval
|
||||||
|
|
||||||
|
@ -292,6 +301,18 @@ if self.dp:
|
||||||
!!! Warning Warning in hyperopt
|
!!! Warning Warning in hyperopt
|
||||||
This option cannot currently be used during hyperopt.
|
This option cannot currently be used during hyperopt.
|
||||||
|
|
||||||
|
#### Orderbook
|
||||||
|
|
||||||
|
``` python
|
||||||
|
if self.dp:
|
||||||
|
if self.dp.runmode in ('live', 'dry_run'):
|
||||||
|
ob = self.dp.orderbook(metadata['pair'], 1)
|
||||||
|
dataframe['best_bid'] = ob['bids'][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
|
||||||
|
method is used.
|
||||||
|
|
||||||
#### Available Pairs
|
#### Available Pairs
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
|
@ -300,6 +321,7 @@ 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.
|
||||||
|
@ -345,6 +367,30 @@ if self.wallets:
|
||||||
- `get_used(asset)` - currently tied up balance (open orders)
|
- `get_used(asset)` - currently tied up balance (open orders)
|
||||||
- `get_total(asset)` - total available balance - sum of the 2 above
|
- `get_total(asset)` - total available balance - sum of the 2 above
|
||||||
|
|
||||||
|
### Print created dataframe
|
||||||
|
|
||||||
|
To inspect the created dataframe, you can issue a print-statement in either `populate_buy_trend()` or `populate_sell_trend()`.
|
||||||
|
You may also want to print the pair so it's clear what data is currently shown.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
#>> whatever condition<<<
|
||||||
|
),
|
||||||
|
'buy'] = 1
|
||||||
|
|
||||||
|
# Print the Analyzed pair
|
||||||
|
print(f"result for {metadata['pair']}")
|
||||||
|
|
||||||
|
# Inspect the last 5 rows
|
||||||
|
print(dataframe.tail())
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
```
|
||||||
|
|
||||||
|
Printing more than a few rows is also possible (simply use `print(dataframe)` instead of `print(dataframe.tail())`), however not recommended, as that will be very verbose (~500 lines per pair every 5 seconds).
|
||||||
|
|
||||||
### Where is the default strategy?
|
### Where is the default strategy?
|
||||||
|
|
||||||
The default buy strategy is located in the file
|
The default buy strategy is located in the file
|
||||||
|
@ -364,7 +410,7 @@ To get additional Ideas for strategies, head over to our [strategy repository](h
|
||||||
Feel free to use any of them as inspiration for your own strategies.
|
Feel free to use any of them as inspiration for your own strategies.
|
||||||
We're happy to accept Pull Requests containing new Strategies to that repo.
|
We're happy to accept Pull Requests containing new Strategies to that repo.
|
||||||
|
|
||||||
We also got a *strategy-sharing* channel in our [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) which is a great place to get and/or share ideas.
|
We also got a *strategy-sharing* channel in our [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LWEyODBiNzkzNzcyNzU0MWYyYzE5NjIyOTQxMzBmMGUxOTIzM2YyN2Y4NWY1YTEwZDgwYTRmMzE2NmM5ZmY2MTg) which is a great place to get and/or share ideas.
|
||||||
|
|
||||||
## Next step
|
## Next step
|
||||||
|
|
|
@ -1,10 +1,45 @@
|
||||||
# Telegram usage
|
# Telegram usage
|
||||||
|
|
||||||
## Prerequisite
|
## Setup your Telegram bot
|
||||||
|
|
||||||
To control your bot with Telegram, you need first to
|
Below we explain how to create your Telegram Bot, and how to get your
|
||||||
[set up a Telegram bot](installation.md)
|
Telegram user id.
|
||||||
and add your Telegram API keys into your config file.
|
|
||||||
|
### 1. Create your Telegram bot
|
||||||
|
|
||||||
|
Start a chat with the [Telegram BotFather](https://telegram.me/BotFather)
|
||||||
|
|
||||||
|
Send the message `/newbot`.
|
||||||
|
|
||||||
|
*BotFather response:*
|
||||||
|
|
||||||
|
> Alright, a new bot. How are we going to call it? Please choose a name for your bot.
|
||||||
|
|
||||||
|
Choose the public name of your bot (e.x. `Freqtrade bot`)
|
||||||
|
|
||||||
|
*BotFather response:*
|
||||||
|
|
||||||
|
> Good. Now let's choose a username for your bot. It must end in `bot`. Like this, for example: TetrisBot or tetris_bot.
|
||||||
|
|
||||||
|
Choose the name id of your bot and send it to the BotFather (e.g. "`My_own_freqtrade_bot`")
|
||||||
|
|
||||||
|
*BotFather response:*
|
||||||
|
|
||||||
|
> Done! Congratulations on your new bot. You will find it at `t.me/yourbots_name_bot`. You can now add a description, about section and profile picture for your bot, see /help for a list of commands. By the way, when you've finished creating your cool bot, ping our Bot Support if you want a better username for it. Just make sure the bot is fully operational before you do this.
|
||||||
|
|
||||||
|
> Use this token to access the HTTP API: `22222222:APITOKEN`
|
||||||
|
|
||||||
|
> For a description of the Bot API, see this page: https://core.telegram.org/bots/api Father bot will return you the token (API key)
|
||||||
|
|
||||||
|
Copy the API Token (`22222222:APITOKEN` in the above example) and keep use it for the config parameter `token`.
|
||||||
|
|
||||||
|
Don't forget to start the conversation with your bot, by clicking `/START` button
|
||||||
|
|
||||||
|
### 2. Get your user id
|
||||||
|
|
||||||
|
Talk to the [userinfobot](https://telegram.me/userinfobot)
|
||||||
|
|
||||||
|
Get your "Id", you will use it for the config parameter `chat_id`.
|
||||||
|
|
||||||
## Telegram commands
|
## Telegram commands
|
||||||
|
|
||||||
|
@ -116,7 +151,7 @@ Return a summary of your profit/loss and performance.
|
||||||
|
|
||||||
### /forcebuy <pair>
|
### /forcebuy <pair>
|
||||||
|
|
||||||
> **BITTREX**: Buying ETH/BTC with limit `0.03400000` (`1.000000 ETH`, `225.290 USD`)
|
> **BITTREX:** Buying ETH/BTC with limit `0.03400000` (`1.000000 ETH`, `225.290 USD`)
|
||||||
|
|
||||||
Note that for this to work, `forcebuy_enable` needs to be set to true.
|
Note that for this to work, `forcebuy_enable` needs to be set to true.
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,7 @@ Possible parameters are:
|
||||||
* `stake_amount`
|
* `stake_amount`
|
||||||
* `stake_currency`
|
* `stake_currency`
|
||||||
* `fiat_currency`
|
* `fiat_currency`
|
||||||
|
* `order_type`
|
||||||
|
|
||||||
### Webhooksell
|
### Webhooksell
|
||||||
|
|
||||||
|
@ -61,6 +62,7 @@ Possible parameters are:
|
||||||
* `stake_currency`
|
* `stake_currency`
|
||||||
* `fiat_currency`
|
* `fiat_currency`
|
||||||
* `sell_reason`
|
* `sell_reason`
|
||||||
|
* `order_type`
|
||||||
|
|
||||||
### Webhookstatus
|
### Webhookstatus
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
""" FreqTrade bot """
|
""" FreqTrade bot """
|
||||||
__version__ = '0.18.5'
|
__version__ = '2019.6'
|
||||||
|
|
||||||
|
|
||||||
class DependencyException(BaseException):
|
class DependencyException(Exception):
|
||||||
"""
|
"""
|
||||||
Indicates that a assumed dependency is not met.
|
Indicates that an assumed dependency is not met.
|
||||||
This could happen when there is currently not enough money on the account.
|
This could happen when there is currently not enough money on the account.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class OperationalException(BaseException):
|
class OperationalException(Exception):
|
||||||
"""
|
"""
|
||||||
Requires manual intervention.
|
Requires manual intervention.
|
||||||
This happens when an exchange returns an unexpected error during runtime
|
This happens when an exchange returns an unexpected error during runtime
|
||||||
|
@ -17,7 +17,7 @@ class OperationalException(BaseException):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class InvalidOrderException(BaseException):
|
class InvalidOrderException(Exception):
|
||||||
"""
|
"""
|
||||||
This is returned when the order is not valid. Example:
|
This is returned when the order is not valid. Example:
|
||||||
If stoploss on exchange order is hit, then trying to cancel the order
|
If stoploss on exchange order is hit, then trying to cancel the order
|
||||||
|
@ -25,7 +25,7 @@ class InvalidOrderException(BaseException):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class TemporaryError(BaseException):
|
class TemporaryError(Exception):
|
||||||
"""
|
"""
|
||||||
Temporary network or exchange related error.
|
Temporary network or exchange related error.
|
||||||
This could happen when an exchange is congested, unavailable, or the user
|
This could happen when an exchange is congested, unavailable, or the user
|
||||||
|
|
|
@ -6,10 +6,7 @@ To launch Freqtrade as a module
|
||||||
> python -m freqtrade (with Python >= 3.6)
|
> python -m freqtrade (with Python >= 3.6)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from freqtrade import main
|
from freqtrade import main
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main.set_loggers()
|
main.main()
|
||||||
main.main(sys.argv[1:])
|
|
||||||
|
|
|
@ -27,13 +27,14 @@ class Arguments(object):
|
||||||
Arguments Class. Manage the arguments received by the cli
|
Arguments Class. Manage the arguments received by the cli
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, args: List[str], description: str) -> None:
|
def __init__(self, args: Optional[List[str]], description: str) -> None:
|
||||||
self.args = args
|
self.args = args
|
||||||
self.parsed_arg: Optional[argparse.Namespace] = None
|
self.parsed_arg: Optional[argparse.Namespace] = None
|
||||||
self.parser = argparse.ArgumentParser(description=description)
|
self.parser = argparse.ArgumentParser(description=description)
|
||||||
|
|
||||||
def _load_args(self) -> None:
|
def _load_args(self) -> None:
|
||||||
self.common_args_parser()
|
self.common_options()
|
||||||
|
self.main_options()
|
||||||
self._build_subcommands()
|
self._build_subcommands()
|
||||||
|
|
||||||
def get_parsed_arg(self) -> argparse.Namespace:
|
def get_parsed_arg(self) -> argparse.Namespace:
|
||||||
|
@ -47,7 +48,7 @@ class Arguments(object):
|
||||||
|
|
||||||
return self.parsed_arg
|
return self.parsed_arg
|
||||||
|
|
||||||
def parse_args(self) -> argparse.Namespace:
|
def parse_args(self, no_default_config: bool = False) -> argparse.Namespace:
|
||||||
"""
|
"""
|
||||||
Parses given arguments and returns an argparse Namespace instance.
|
Parses given arguments and returns an argparse Namespace instance.
|
||||||
"""
|
"""
|
||||||
|
@ -55,97 +56,140 @@ 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 parsed_arg.config is None:
|
if not no_default_config and parsed_arg.config is None:
|
||||||
parsed_arg.config = [constants.DEFAULT_CONFIG]
|
parsed_arg.config = [constants.DEFAULT_CONFIG]
|
||||||
|
|
||||||
return parsed_arg
|
return parsed_arg
|
||||||
|
|
||||||
def common_args_parser(self) -> None:
|
def common_options(self) -> None:
|
||||||
"""
|
"""
|
||||||
Parses given common arguments and returns them as a parsed object.
|
Parses arguments that are common for the main Freqtrade, all subcommands and scripts.
|
||||||
"""
|
"""
|
||||||
self.parser.add_argument(
|
parser = self.parser
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
'-v', '--verbose',
|
'-v', '--verbose',
|
||||||
help='Verbose mode (-vv for more, -vvv to get all messages).',
|
help='Verbose mode (-vv for more, -vvv to get all messages).',
|
||||||
action='count',
|
action='count',
|
||||||
dest='loglevel',
|
dest='loglevel',
|
||||||
default=0,
|
default=0,
|
||||||
)
|
)
|
||||||
self.parser.add_argument(
|
parser.add_argument(
|
||||||
'--logfile',
|
'--logfile',
|
||||||
help='Log to the file specified',
|
help='Log to the file specified.',
|
||||||
dest='logfile',
|
dest='logfile',
|
||||||
type=str,
|
metavar='FILE',
|
||||||
metavar='FILE'
|
|
||||||
)
|
)
|
||||||
self.parser.add_argument(
|
parser.add_argument(
|
||||||
'--version',
|
'--version',
|
||||||
action='version',
|
action='version',
|
||||||
version=f'%(prog)s {__version__}'
|
version=f'%(prog)s {__version__}'
|
||||||
)
|
)
|
||||||
self.parser.add_argument(
|
parser.add_argument(
|
||||||
'-c', '--config',
|
'-c', '--config',
|
||||||
help='Specify configuration file (default: %(default)s). '
|
help=f'Specify configuration file (default: `{constants.DEFAULT_CONFIG}`). '
|
||||||
'Multiple --config options may be used.',
|
f'Multiple --config options may be used. '
|
||||||
|
f'Can be set to `-` to read config from stdin.',
|
||||||
dest='config',
|
dest='config',
|
||||||
action='append',
|
action='append',
|
||||||
type=str,
|
|
||||||
metavar='PATH',
|
metavar='PATH',
|
||||||
)
|
)
|
||||||
self.parser.add_argument(
|
parser.add_argument(
|
||||||
'-d', '--datadir',
|
'-d', '--datadir',
|
||||||
help='Path to backtest data.',
|
help='Path to backtest data.',
|
||||||
dest='datadir',
|
dest='datadir',
|
||||||
default=None,
|
|
||||||
type=str,
|
|
||||||
metavar='PATH',
|
metavar='PATH',
|
||||||
)
|
)
|
||||||
self.parser.add_argument(
|
|
||||||
|
def main_options(self) -> None:
|
||||||
|
"""
|
||||||
|
Parses arguments for the main Freqtrade.
|
||||||
|
"""
|
||||||
|
parser = self.parser
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
'-s', '--strategy',
|
'-s', '--strategy',
|
||||||
help='Specify strategy class name (default: %(default)s).',
|
help='Specify strategy class name (default: `%(default)s`).',
|
||||||
dest='strategy',
|
dest='strategy',
|
||||||
default='DefaultStrategy',
|
default='DefaultStrategy',
|
||||||
type=str,
|
|
||||||
metavar='NAME',
|
metavar='NAME',
|
||||||
)
|
)
|
||||||
self.parser.add_argument(
|
parser.add_argument(
|
||||||
'--strategy-path',
|
'--strategy-path',
|
||||||
help='Specify additional strategy lookup path.',
|
help='Specify additional strategy lookup path.',
|
||||||
dest='strategy_path',
|
dest='strategy_path',
|
||||||
type=str,
|
|
||||||
metavar='PATH',
|
metavar='PATH',
|
||||||
)
|
)
|
||||||
self.parser.add_argument(
|
parser.add_argument(
|
||||||
'--dynamic-whitelist',
|
'--dynamic-whitelist',
|
||||||
help='Dynamically generate and update whitelist'
|
help='Dynamically generate and update whitelist '
|
||||||
' based on 24h BaseVolume (default: %(const)s).'
|
'based on 24h BaseVolume (default: %(const)s). '
|
||||||
' DEPRECATED.',
|
'DEPRECATED.',
|
||||||
dest='dynamic_whitelist',
|
dest='dynamic_whitelist',
|
||||||
const=constants.DYNAMIC_WHITELIST,
|
const=constants.DYNAMIC_WHITELIST,
|
||||||
type=int,
|
type=int,
|
||||||
metavar='INT',
|
metavar='INT',
|
||||||
nargs='?',
|
nargs='?',
|
||||||
)
|
)
|
||||||
self.parser.add_argument(
|
parser.add_argument(
|
||||||
'--db-url',
|
'--db-url',
|
||||||
help='Override trades database URL, this is useful if dry_run is enabled'
|
help=f'Override trades database URL, this is useful in custom deployments '
|
||||||
' or in custom deployments (default: %(default)s).',
|
f'(default: `{constants.DEFAULT_DB_PROD_URL}` for Live Run mode, '
|
||||||
|
f'`{constants.DEFAULT_DB_DRYRUN_URL}` for Dry Run).',
|
||||||
dest='db_url',
|
dest='db_url',
|
||||||
type=str,
|
|
||||||
metavar='PATH',
|
metavar='PATH',
|
||||||
)
|
)
|
||||||
self.parser.add_argument(
|
parser.add_argument(
|
||||||
'--sd-notify',
|
'--sd-notify',
|
||||||
help='Notify systemd service manager.',
|
help='Notify systemd service manager.',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
dest='sd_notify',
|
dest='sd_notify',
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
def common_optimize_options(self, subparser: argparse.ArgumentParser = None) -> None:
|
||||||
def backtesting_options(parser: argparse.ArgumentParser) -> None:
|
|
||||||
"""
|
"""
|
||||||
Parses given arguments for Backtesting scripts.
|
Parses arguments common for Backtesting, Edge and Hyperopt modules.
|
||||||
|
:param parser:
|
||||||
"""
|
"""
|
||||||
|
parser = subparser or self.parser
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'-i', '--ticker-interval',
|
||||||
|
help='Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`).',
|
||||||
|
dest='ticker_interval',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--timerange',
|
||||||
|
help='Specify what timerange of data to use.',
|
||||||
|
dest='timerange',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--max_open_trades',
|
||||||
|
help='Specify max_open_trades to use.',
|
||||||
|
type=int,
|
||||||
|
dest='max_open_trades',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--stake_amount',
|
||||||
|
help='Specify stake_amount.',
|
||||||
|
type=float,
|
||||||
|
dest='stake_amount',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-r', '--refresh-pairs-cached',
|
||||||
|
help='Refresh the pairs files in tests/testdata with the latest data from the '
|
||||||
|
'exchange. Use it if you want to run your optimization commands with '
|
||||||
|
'up-to-date data.',
|
||||||
|
action='store_true',
|
||||||
|
dest='refresh_pairs',
|
||||||
|
)
|
||||||
|
|
||||||
|
def backtesting_options(self, subparser: argparse.ArgumentParser = None) -> None:
|
||||||
|
"""
|
||||||
|
Parses given arguments for Backtesting module.
|
||||||
|
"""
|
||||||
|
parser = subparser or self.parser
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--eps', '--enable-position-stacking',
|
'--eps', '--enable-position-stacking',
|
||||||
help='Allow buying the same pair multiple times (position stacking).',
|
help='Allow buying the same pair multiple times (position stacking).',
|
||||||
|
@ -167,113 +211,57 @@ class Arguments(object):
|
||||||
action='store_true',
|
action='store_true',
|
||||||
dest='live',
|
dest='live',
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
|
||||||
'-r', '--refresh-pairs-cached',
|
|
||||||
help='Refresh the pairs files in tests/testdata with the latest data from the '
|
|
||||||
'exchange. Use it if you want to run your backtesting with up-to-date data.',
|
|
||||||
action='store_true',
|
|
||||||
dest='refresh_pairs',
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--strategy-list',
|
'--strategy-list',
|
||||||
help='Provide a commaseparated list of strategies to backtest '
|
help='Provide a comma-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 '
|
||||||
'(so backtest-data.json becomes backtest-data-DefaultStrategy.json',
|
'(so `backtest-data.json` becomes `backtest-data-DefaultStrategy.json`',
|
||||||
nargs='+',
|
nargs='+',
|
||||||
dest='strategy_list',
|
dest='strategy_list',
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--export',
|
'--export',
|
||||||
help='Export backtest results, argument are: trades. '
|
help='Export backtest results, argument are: trades. '
|
||||||
'Example --export=trades',
|
'Example: `--export=trades`',
|
||||||
type=str,
|
|
||||||
default=None,
|
|
||||||
dest='export',
|
dest='export',
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--export-filename',
|
'--export-filename',
|
||||||
help='Save backtest results to this filename \
|
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_data/backtest_today.json`',
|
||||||
(default: %(default)s)',
|
|
||||||
type=str,
|
|
||||||
default=os.path.join('user_data', 'backtest_data', 'backtest-result.json'),
|
default=os.path.join('user_data', 'backtest_data', 'backtest-result.json'),
|
||||||
dest='exportfilename',
|
dest='exportfilename',
|
||||||
metavar='PATH',
|
metavar='PATH',
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
def edge_options(self, subparser: argparse.ArgumentParser = None) -> None:
|
||||||
def edge_options(parser: argparse.ArgumentParser) -> None:
|
|
||||||
"""
|
"""
|
||||||
Parses given arguments for Backtesting scripts.
|
Parses given arguments for Edge module.
|
||||||
"""
|
"""
|
||||||
parser.add_argument(
|
parser = subparser or self.parser
|
||||||
'-r', '--refresh-pairs-cached',
|
|
||||||
help='Refresh the pairs files in tests/testdata with the latest data from the '
|
|
||||||
'exchange. Use it if you want to run your edge with up-to-date data.',
|
|
||||||
action='store_true',
|
|
||||||
dest='refresh_pairs',
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--stoplosses',
|
'--stoplosses',
|
||||||
help='Defines a range of stoploss against which edge will assess the strategy '
|
help='Defines a range of stoploss values against which edge will assess the strategy. '
|
||||||
'the format is "min,max,step" (without any space).'
|
'The format is "min,max,step" (without any space). '
|
||||||
'example: --stoplosses=-0.01,-0.1,-0.001',
|
'Example: `--stoplosses=-0.01,-0.1,-0.001`',
|
||||||
type=str,
|
|
||||||
dest='stoploss_range',
|
dest='stoploss_range',
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
def hyperopt_options(self, subparser: argparse.ArgumentParser = None) -> None:
|
||||||
def optimizer_shared_options(parser: argparse.ArgumentParser) -> None:
|
|
||||||
"""
|
"""
|
||||||
Parses given common arguments for Backtesting and Hyperopt scripts.
|
Parses given arguments for Hyperopt module.
|
||||||
:param parser:
|
|
||||||
:return:
|
|
||||||
"""
|
"""
|
||||||
parser.add_argument(
|
parser = subparser or self.parser
|
||||||
'-i', '--ticker-interval',
|
|
||||||
help='Specify ticker interval (1m, 5m, 30m, 1h, 1d).',
|
|
||||||
dest='ticker_interval',
|
|
||||||
type=str,
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
'--timerange',
|
|
||||||
help='Specify what timerange of data to use.',
|
|
||||||
default=None,
|
|
||||||
type=str,
|
|
||||||
dest='timerange',
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
'--max_open_trades',
|
|
||||||
help='Specify max_open_trades to use.',
|
|
||||||
default=None,
|
|
||||||
type=int,
|
|
||||||
dest='max_open_trades',
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
'--stake_amount',
|
|
||||||
help='Specify stake_amount.',
|
|
||||||
default=None,
|
|
||||||
type=float,
|
|
||||||
dest='stake_amount',
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def hyperopt_options(parser: argparse.ArgumentParser) -> None:
|
|
||||||
"""
|
|
||||||
Parses given arguments for Hyperopt scripts.
|
|
||||||
"""
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--customhyperopt',
|
'--customhyperopt',
|
||||||
help='Specify hyperopt class name (default: %(default)s).',
|
help='Specify hyperopt class name (default: `%(default)s`).',
|
||||||
dest='hyperopt',
|
dest='hyperopt',
|
||||||
default=constants.DEFAULT_HYPEROPT,
|
default=constants.DEFAULT_HYPEROPT,
|
||||||
type=str,
|
|
||||||
metavar='NAME',
|
metavar='NAME',
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
|
@ -283,7 +271,6 @@ class Arguments(object):
|
||||||
dest='position_stacking',
|
dest='position_stacking',
|
||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--dmmp', '--disable-max-market-positions',
|
'--dmmp', '--disable-max-market-positions',
|
||||||
help='Disable applying `max_open_trades` during backtest '
|
help='Disable applying `max_open_trades` during backtest '
|
||||||
|
@ -302,41 +289,97 @@ class Arguments(object):
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-s', '--spaces',
|
'-s', '--spaces',
|
||||||
help='Specify which parameters to hyperopt. Space separate list. \
|
help='Specify which parameters to hyperopt. Space-separated list. '
|
||||||
Default: %(default)s.',
|
'Default: `%(default)s`.',
|
||||||
choices=['all', 'buy', 'sell', 'roi', 'stoploss'],
|
choices=['all', 'buy', 'sell', 'roi', 'stoploss'],
|
||||||
default='all',
|
default='all',
|
||||||
nargs='+',
|
nargs='+',
|
||||||
dest='spaces',
|
dest='spaces',
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--print-all',
|
||||||
|
help='Print all results, not only the best ones.',
|
||||||
|
action='store_true',
|
||||||
|
dest='print_all',
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-j', '--job-workers',
|
||||||
|
help='The number of concurrently running jobs for hyperoptimization '
|
||||||
|
'(hyperopt worker processes). '
|
||||||
|
'If -1 (default), all CPUs are used, for -2, all CPUs but one are used, etc. '
|
||||||
|
'If 1 is given, no parallel computing code is used at all.',
|
||||||
|
dest='hyperopt_jobs',
|
||||||
|
default=-1,
|
||||||
|
type=int,
|
||||||
|
metavar='JOBS',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--random-state',
|
||||||
|
help='Set random state to some positive integer for reproducible hyperopt results.',
|
||||||
|
dest='hyperopt_random_state',
|
||||||
|
type=Arguments.check_int_positive,
|
||||||
|
metavar='INT',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--min-trades',
|
||||||
|
help="Set minimal desired number of trades for evaluations in the hyperopt "
|
||||||
|
"optimization path (default: 1).",
|
||||||
|
dest='hyperopt_min_trades',
|
||||||
|
default=1,
|
||||||
|
type=Arguments.check_int_positive,
|
||||||
|
metavar='INT',
|
||||||
|
)
|
||||||
|
|
||||||
|
def list_exchanges_options(self, subparser: argparse.ArgumentParser = None) -> None:
|
||||||
|
"""
|
||||||
|
Parses given arguments for the list-exchanges command.
|
||||||
|
"""
|
||||||
|
parser = subparser or self.parser
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'-1', '--one-column',
|
||||||
|
help='Print exchanges in one column.',
|
||||||
|
action='store_true',
|
||||||
|
dest='print_one_column',
|
||||||
|
)
|
||||||
|
|
||||||
def _build_subcommands(self) -> None:
|
def _build_subcommands(self) -> None:
|
||||||
"""
|
"""
|
||||||
Builds and attaches all subcommands
|
Builds and attaches all subcommands.
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
from freqtrade.optimize import backtesting, hyperopt, edge_cli
|
from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge
|
||||||
|
from freqtrade.utils import start_list_exchanges
|
||||||
|
|
||||||
subparsers = self.parser.add_subparsers(dest='subparser')
|
subparsers = self.parser.add_subparsers(dest='subparser')
|
||||||
|
|
||||||
# Add backtesting subcommand
|
# Add backtesting subcommand
|
||||||
backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.')
|
backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.')
|
||||||
backtesting_cmd.set_defaults(func=backtesting.start)
|
backtesting_cmd.set_defaults(func=start_backtesting)
|
||||||
self.optimizer_shared_options(backtesting_cmd)
|
self.common_optimize_options(backtesting_cmd)
|
||||||
self.backtesting_options(backtesting_cmd)
|
self.backtesting_options(backtesting_cmd)
|
||||||
|
|
||||||
# Add edge subcommand
|
# Add edge subcommand
|
||||||
edge_cmd = subparsers.add_parser('edge', help='Edge module.')
|
edge_cmd = subparsers.add_parser('edge', help='Edge module.')
|
||||||
edge_cmd.set_defaults(func=edge_cli.start)
|
edge_cmd.set_defaults(func=start_edge)
|
||||||
self.optimizer_shared_options(edge_cmd)
|
self.common_optimize_options(edge_cmd)
|
||||||
self.edge_options(edge_cmd)
|
self.edge_options(edge_cmd)
|
||||||
|
|
||||||
# Add hyperopt subcommand
|
# Add hyperopt subcommand
|
||||||
hyperopt_cmd = subparsers.add_parser('hyperopt', help='Hyperopt module.')
|
hyperopt_cmd = subparsers.add_parser('hyperopt', help='Hyperopt module.')
|
||||||
hyperopt_cmd.set_defaults(func=hyperopt.start)
|
hyperopt_cmd.set_defaults(func=start_hyperopt)
|
||||||
self.optimizer_shared_options(hyperopt_cmd)
|
self.common_optimize_options(hyperopt_cmd)
|
||||||
self.hyperopt_options(hyperopt_cmd)
|
self.hyperopt_options(hyperopt_cmd)
|
||||||
|
|
||||||
|
# Add list-exchanges subcommand
|
||||||
|
list_exchanges_cmd = subparsers.add_parser(
|
||||||
|
'list-exchanges',
|
||||||
|
help='Print available exchanges.'
|
||||||
|
)
|
||||||
|
list_exchanges_cmd.set_defaults(func=start_list_exchanges)
|
||||||
|
self.list_exchanges_options(list_exchanges_cmd)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_timerange(text: Optional[str]) -> TimeRange:
|
def parse_timerange(text: Optional[str]) -> TimeRange:
|
||||||
"""
|
"""
|
||||||
|
@ -379,78 +422,105 @@ class Arguments(object):
|
||||||
return TimeRange(stype[0], stype[1], start, stop)
|
return TimeRange(stype[0], stype[1], start, stop)
|
||||||
raise Exception('Incorrect syntax for timerange "%s"' % text)
|
raise Exception('Incorrect syntax for timerange "%s"' % text)
|
||||||
|
|
||||||
def scripts_options(self) -> None:
|
@staticmethod
|
||||||
|
def check_int_positive(value: str) -> int:
|
||||||
|
try:
|
||||||
|
uint = int(value)
|
||||||
|
if uint <= 0:
|
||||||
|
raise ValueError
|
||||||
|
except ValueError:
|
||||||
|
raise argparse.ArgumentTypeError(
|
||||||
|
f"{value} is invalid for this parameter, should be a positive integer value"
|
||||||
|
)
|
||||||
|
return uint
|
||||||
|
|
||||||
|
def common_scripts_options(self, subparser: argparse.ArgumentParser = None) -> None:
|
||||||
"""
|
"""
|
||||||
Parses given arguments for scripts.
|
Parses arguments common for scripts.
|
||||||
"""
|
"""
|
||||||
self.parser.add_argument(
|
parser = subparser or self.parser
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
'-p', '--pairs',
|
'-p', '--pairs',
|
||||||
help='Show profits for only this pairs. Pairs are comma-separated.',
|
help='Show profits for only these pairs. Pairs are comma-separated.',
|
||||||
dest='pairs',
|
dest='pairs',
|
||||||
default=None
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def testdata_dl_options(self) -> None:
|
def download_data_options(self) -> None:
|
||||||
"""
|
"""
|
||||||
Parses given arguments for testdata download
|
Parses given arguments for testdata download script
|
||||||
"""
|
"""
|
||||||
self.parser.add_argument(
|
parser = self.parser
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
'--pairs-file',
|
'--pairs-file',
|
||||||
help='File containing a list of pairs to download.',
|
help='File containing a list of pairs to download.',
|
||||||
dest='pairs_file',
|
dest='pairs_file',
|
||||||
default=None,
|
metavar='FILE',
|
||||||
metavar='PATH',
|
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
self.parser.add_argument(
|
|
||||||
'--export',
|
|
||||||
help='Export files to given dir.',
|
|
||||||
dest='export',
|
|
||||||
default=None,
|
|
||||||
metavar='PATH',
|
|
||||||
)
|
|
||||||
|
|
||||||
self.parser.add_argument(
|
|
||||||
'-c', '--config',
|
|
||||||
help='Specify configuration file (default: %(default)s). '
|
|
||||||
'Multiple --config options may be used.',
|
|
||||||
dest='config',
|
|
||||||
action='append',
|
|
||||||
type=str,
|
|
||||||
metavar='PATH',
|
|
||||||
)
|
|
||||||
|
|
||||||
self.parser.add_argument(
|
|
||||||
'--days',
|
'--days',
|
||||||
help='Download data for given number of days.',
|
help='Download data for given number of days.',
|
||||||
dest='days',
|
dest='days',
|
||||||
type=int,
|
type=Arguments.check_int_positive,
|
||||||
metavar='INT',
|
metavar='INT',
|
||||||
default=None
|
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
self.parser.add_argument(
|
|
||||||
'--exchange',
|
'--exchange',
|
||||||
help='Exchange name (default: %(default)s). Only valid if no config is provided.',
|
help=f'Exchange name (default: `{constants.DEFAULT_EXCHANGE}`). '
|
||||||
|
f'Only valid if no config is provided.',
|
||||||
dest='exchange',
|
dest='exchange',
|
||||||
type=str,
|
|
||||||
default='bittrex'
|
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
self.parser.add_argument(
|
|
||||||
'-t', '--timeframes',
|
'-t', '--timeframes',
|
||||||
help='Specify which tickers to download. Space separated list. \
|
help=f'Specify which tickers to download. Space-separated list. '
|
||||||
Default: %(default)s.',
|
f'Default: `{constants.DEFAULT_DOWNLOAD_TICKER_INTERVALS}`.',
|
||||||
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='+',
|
||||||
dest='timeframes',
|
dest='timeframes',
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
self.parser.add_argument(
|
|
||||||
'--erase',
|
'--erase',
|
||||||
help='Clean all existing data for the selected exchange/pairs/timeframes.',
|
help='Clean all existing data for the selected exchange/pairs/timeframes.',
|
||||||
dest='erase',
|
dest='erase',
|
||||||
action='store_true'
|
action='store_true'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def plot_dataframe_options(self) -> None:
|
||||||
|
"""
|
||||||
|
Parses given arguments for plot dataframe script
|
||||||
|
"""
|
||||||
|
parser = self.parser
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--indicators1',
|
||||||
|
help='Set indicators from your strategy you want in the first row of the graph. '
|
||||||
|
'Comma-separated list. Example: `ema3,ema5`. Default: `%(default)s`.',
|
||||||
|
default='sma,ema3,ema5',
|
||||||
|
dest='indicators1',
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--indicators2',
|
||||||
|
help='Set indicators from your strategy you want in the third row of the graph. '
|
||||||
|
'Comma-separated list. Example: `fastd,fastk`. Default: `%(default)s`.',
|
||||||
|
default='macd,macdsignal',
|
||||||
|
dest='indicators2',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--plot-limit',
|
||||||
|
help='Specify tick limit for plotting. Notice: too high values cause huge files. '
|
||||||
|
'Default: %(default)s.',
|
||||||
|
dest='plot_limit',
|
||||||
|
default=750,
|
||||||
|
type=int,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--trade-source',
|
||||||
|
help='Specify the source for trades (Can be DB or file (backtest file)) '
|
||||||
|
'Default: %(default)s',
|
||||||
|
dest='trade_source',
|
||||||
|
default="file",
|
||||||
|
choices=["DB", "file"]
|
||||||
|
)
|
||||||
|
|
|
@ -7,13 +7,14 @@ import os
|
||||||
import sys
|
import sys
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import RotatingFileHandler
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Callable, Dict, List, Optional
|
||||||
|
|
||||||
import ccxt
|
from jsonschema import Draft4Validator, validators
|
||||||
from jsonschema import Draft4Validator, validate
|
|
||||||
from jsonschema.exceptions import ValidationError, best_match
|
from jsonschema.exceptions import ValidationError, best_match
|
||||||
|
|
||||||
from freqtrade import OperationalException, constants
|
from freqtrade import OperationalException, constants
|
||||||
|
from freqtrade.exchange import (is_exchange_bad, is_exchange_available,
|
||||||
|
is_exchange_officially_supported, available_exchanges)
|
||||||
from freqtrade.misc import deep_merge_dicts
|
from freqtrade.misc import deep_merge_dicts
|
||||||
from freqtrade.state import RunMode
|
from freqtrade.state import RunMode
|
||||||
|
|
||||||
|
@ -33,6 +34,31 @@ def set_loggers(log_level: int = 0) -> None:
|
||||||
logging.getLogger('telegram').setLevel(logging.INFO)
|
logging.getLogger('telegram').setLevel(logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
class Configuration(object):
|
class Configuration(object):
|
||||||
"""
|
"""
|
||||||
Class to read and init the bot configuration
|
Class to read and init the bot configuration
|
||||||
|
@ -53,6 +79,7 @@ class Configuration(object):
|
||||||
# Now expecting a list of config filenames here, not a string
|
# Now expecting a list of config filenames here, not a string
|
||||||
for path in self.args.config:
|
for path in self.args.config:
|
||||||
logger.info('Using config: %s ...', path)
|
logger.info('Using config: %s ...', 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(self._load_config_file(path), config)
|
||||||
|
|
||||||
|
@ -73,14 +100,11 @@ class Configuration(object):
|
||||||
# Load Common configuration
|
# Load Common configuration
|
||||||
config = self._load_common_config(config)
|
config = self._load_common_config(config)
|
||||||
|
|
||||||
# Load Backtesting
|
# Load Optimize configurations
|
||||||
config = self._load_backtesting_config(config)
|
config = self._load_optimize_config(config)
|
||||||
|
|
||||||
# Load Edge
|
# Add plotting options if available
|
||||||
config = self._load_edge_config(config)
|
config = self._load_plot_config(config)
|
||||||
|
|
||||||
# Load Hyperopt
|
|
||||||
config = self._load_hyperopt_config(config)
|
|
||||||
|
|
||||||
# Set runmode
|
# Set runmode
|
||||||
if not self.runmode:
|
if not self.runmode:
|
||||||
|
@ -98,7 +122,8 @@ class Configuration(object):
|
||||||
:return: configuration as dictionary
|
:return: configuration as dictionary
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
with open(path) as file:
|
# Read config from stdin if requested in the options
|
||||||
|
with open(path) if path != '-' else sys.stdin as file:
|
||||||
conf = json.load(file)
|
conf = json.load(file)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
|
@ -107,12 +132,11 @@ class Configuration(object):
|
||||||
|
|
||||||
return conf
|
return conf
|
||||||
|
|
||||||
def _load_common_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
def _load_logging_config(self, config: Dict[str, Any]) -> None:
|
||||||
"""
|
"""
|
||||||
Extract information for sys.argv and load common configuration
|
Extract information for sys.argv and load logging configuration:
|
||||||
:return: configuration as dictionary
|
the --loglevel, --logfile options
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Log level
|
# Log level
|
||||||
if 'loglevel' in self.args and self.args.loglevel:
|
if 'loglevel' in self.args and self.args.loglevel:
|
||||||
config.update({'verbosity': self.args.loglevel})
|
config.update({'verbosity': self.args.loglevel})
|
||||||
|
@ -138,6 +162,13 @@ class Configuration(object):
|
||||||
set_loggers(config['verbosity'])
|
set_loggers(config['verbosity'])
|
||||||
logger.info('Verbosity set to %s', config['verbosity'])
|
logger.info('Verbosity set to %s', config['verbosity'])
|
||||||
|
|
||||||
|
def _load_common_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Extract information for sys.argv and load common configuration
|
||||||
|
:return: configuration as dictionary
|
||||||
|
"""
|
||||||
|
self._load_logging_config(config)
|
||||||
|
|
||||||
# Support for sd_notify
|
# Support for sd_notify
|
||||||
if self.args.sd_notify:
|
if self.args.sd_notify:
|
||||||
config['internals'].update({'sd_notify': True})
|
config['internals'].update({'sd_notify': True})
|
||||||
|
@ -194,30 +225,53 @@ class Configuration(object):
|
||||||
logger.info(f'Created data directory: {datadir}')
|
logger.info(f'Created data directory: {datadir}')
|
||||||
return datadir
|
return datadir
|
||||||
|
|
||||||
def _load_backtesting_config(self, config: Dict[str, Any]) -> Dict[str, Any]: # noqa: C901
|
def _args_to_config(self, config: Dict[str, Any], argname: str,
|
||||||
|
logstring: str, logfun: Optional[Callable] = None) -> None:
|
||||||
"""
|
"""
|
||||||
Extract information for sys.argv and load Backtesting configuration
|
:param config: Configuration dictionary
|
||||||
|
:param argname: Argumentname in self.args - will be copied to config dict.
|
||||||
|
:param logstring: Logging String
|
||||||
|
:param logfun: logfun is applied to the configuration entry before passing
|
||||||
|
that entry to the log string using .format().
|
||||||
|
sample: logfun=len (prints the length of the found
|
||||||
|
configuration instead of the content)
|
||||||
|
"""
|
||||||
|
if argname in self.args and getattr(self.args, argname):
|
||||||
|
|
||||||
|
config.update({argname: getattr(self.args, argname)})
|
||||||
|
if logfun:
|
||||||
|
logger.info(logstring.format(logfun(config[argname])))
|
||||||
|
else:
|
||||||
|
logger.info(logstring.format(config[argname]))
|
||||||
|
|
||||||
|
def _load_datadir_config(self, config: Dict[str, Any]) -> None:
|
||||||
|
"""
|
||||||
|
Extract information for sys.argv and load datadir configuration:
|
||||||
|
the --datadir option
|
||||||
|
"""
|
||||||
|
if 'datadir' in self.args and self.args.datadir:
|
||||||
|
config.update({'datadir': self._create_datadir(config, self.args.datadir)})
|
||||||
|
else:
|
||||||
|
config.update({'datadir': self._create_datadir(config, None)})
|
||||||
|
logger.info('Using data folder: %s ...', config.get('datadir'))
|
||||||
|
|
||||||
|
def _load_optimize_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Extract information for sys.argv and load Optimize configuration
|
||||||
:return: configuration as dictionary
|
:return: configuration as dictionary
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# If -i/--ticker-interval is used we override the configuration parameter
|
# This will override the strategy configuration
|
||||||
# (that will override the strategy configuration)
|
self._args_to_config(config, argname='ticker_interval',
|
||||||
if 'ticker_interval' in self.args and self.args.ticker_interval:
|
logstring='Parameter -i/--ticker-interval detected ... '
|
||||||
config.update({'ticker_interval': self.args.ticker_interval})
|
'Using ticker_interval: {} ...')
|
||||||
logger.info('Parameter -i/--ticker-interval detected ...')
|
|
||||||
logger.info('Using ticker_interval: %s ...', config.get('ticker_interval'))
|
|
||||||
|
|
||||||
# If -l/--live is used we add it to the configuration
|
self._args_to_config(config, argname='live',
|
||||||
if 'live' in self.args and self.args.live:
|
logstring='Parameter -l/--live detected ...')
|
||||||
config.update({'live': True})
|
|
||||||
logger.info('Parameter -l/--live detected ...')
|
|
||||||
|
|
||||||
# If --enable-position-stacking is used we add it to the configuration
|
self._args_to_config(config, argname='position_stacking',
|
||||||
if 'position_stacking' in self.args and self.args.position_stacking:
|
logstring='Parameter --enable-position-stacking detected ...')
|
||||||
config.update({'position_stacking': True})
|
|
||||||
logger.info('Parameter --enable-position-stacking detected ...')
|
|
||||||
|
|
||||||
# If --disable-max-market-positions or --max_open_trades is used we update configuration
|
|
||||||
if 'use_max_market_positions' in self.args and not self.args.use_max_market_positions:
|
if 'use_max_market_positions' in self.args and not self.args.use_max_market_positions:
|
||||||
config.update({'use_max_market_positions': False})
|
config.update({'use_max_market_positions': False})
|
||||||
logger.info('Parameter --disable-max-market-positions detected ...')
|
logger.info('Parameter --disable-max-market-positions detected ...')
|
||||||
|
@ -229,61 +283,31 @@ class Configuration(object):
|
||||||
else:
|
else:
|
||||||
logger.info('Using max_open_trades: %s ...', config.get('max_open_trades'))
|
logger.info('Using max_open_trades: %s ...', config.get('max_open_trades'))
|
||||||
|
|
||||||
# If --stake_amount is used we update configuration
|
self._args_to_config(config, argname='stake_amount',
|
||||||
if 'stake_amount' in self.args and self.args.stake_amount:
|
logstring='Parameter --stake_amount detected, '
|
||||||
config.update({'stake_amount': self.args.stake_amount})
|
'overriding stake_amount to: {} ...')
|
||||||
logger.info('Parameter --stake_amount detected, overriding stake_amount to: %s ...',
|
|
||||||
config.get('stake_amount'))
|
|
||||||
|
|
||||||
# If --timerange is used we add it to the configuration
|
self._args_to_config(config, argname='timerange',
|
||||||
if 'timerange' in self.args and self.args.timerange:
|
logstring='Parameter --timerange detected: {} ...')
|
||||||
config.update({'timerange': self.args.timerange})
|
|
||||||
logger.info('Parameter --timerange detected: %s ...', self.args.timerange)
|
|
||||||
|
|
||||||
# If --datadir is used we add it to the configuration
|
self._load_datadir_config(config)
|
||||||
if 'datadir' in self.args and self.args.datadir:
|
|
||||||
config.update({'datadir': self._create_datadir(config, self.args.datadir)})
|
|
||||||
else:
|
|
||||||
config.update({'datadir': self._create_datadir(config, None)})
|
|
||||||
logger.info('Using data folder: %s ...', config.get('datadir'))
|
|
||||||
|
|
||||||
# If -r/--refresh-pairs-cached is used we add it to the configuration
|
self._args_to_config(config, argname='refresh_pairs',
|
||||||
if 'refresh_pairs' in self.args and self.args.refresh_pairs:
|
logstring='Parameter -r/--refresh-pairs-cached detected ...')
|
||||||
config.update({'refresh_pairs': True})
|
|
||||||
logger.info('Parameter -r/--refresh-pairs-cached detected ...')
|
|
||||||
|
|
||||||
if 'strategy_list' in self.args and self.args.strategy_list:
|
self._args_to_config(config, argname='strategy_list',
|
||||||
config.update({'strategy_list': self.args.strategy_list})
|
logstring='Using strategy list of {} Strategies', logfun=len)
|
||||||
logger.info('Using strategy list of %s Strategies', len(self.args.strategy_list))
|
|
||||||
|
|
||||||
if 'ticker_interval' in self.args and self.args.ticker_interval:
|
self._args_to_config(config, argname='ticker_interval',
|
||||||
config.update({'ticker_interval': self.args.ticker_interval})
|
logstring='Overriding ticker interval with Command line argument')
|
||||||
logger.info('Overriding ticker interval with Command line argument')
|
|
||||||
|
|
||||||
# If --export is used we add it to the configuration
|
self._args_to_config(config, argname='export',
|
||||||
if 'export' in self.args and self.args.export:
|
logstring='Parameter --export detected: {} ...')
|
||||||
config.update({'export': self.args.export})
|
|
||||||
logger.info('Parameter --export detected: %s ...', self.args.export)
|
|
||||||
|
|
||||||
# If --export-filename is used we add it to the configuration
|
self._args_to_config(config, argname='exportfilename',
|
||||||
if 'export' in config and 'exportfilename' in self.args and self.args.exportfilename:
|
logstring='Storing backtest results to {} ...')
|
||||||
config.update({'exportfilename': self.args.exportfilename})
|
|
||||||
logger.info('Storing backtest results to %s ...', self.args.exportfilename)
|
|
||||||
|
|
||||||
return config
|
# Edge section:
|
||||||
|
|
||||||
def _load_edge_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Extract information for sys.argv and load Edge configuration
|
|
||||||
:return: configuration as dictionary
|
|
||||||
"""
|
|
||||||
|
|
||||||
# If --timerange is used we add it to the configuration
|
|
||||||
if 'timerange' in self.args and self.args.timerange:
|
|
||||||
config.update({'timerange': self.args.timerange})
|
|
||||||
logger.info('Parameter --timerange detected: %s ...', self.args.timerange)
|
|
||||||
|
|
||||||
# If --timerange is used we add it to the configuration
|
|
||||||
if 'stoploss_range' in self.args and self.args.stoploss_range:
|
if 'stoploss_range' in self.args and self.args.stoploss_range:
|
||||||
txt_range = eval(self.args.stoploss_range)
|
txt_range = eval(self.args.stoploss_range)
|
||||||
config['edge'].update({'stoploss_range_min': txt_range[0]})
|
config['edge'].update({'stoploss_range_min': txt_range[0]})
|
||||||
|
@ -291,34 +315,51 @@ class Configuration(object):
|
||||||
config['edge'].update({'stoploss_range_step': txt_range[2]})
|
config['edge'].update({'stoploss_range_step': txt_range[2]})
|
||||||
logger.info('Parameter --stoplosses detected: %s ...', self.args.stoploss_range)
|
logger.info('Parameter --stoplosses detected: %s ...', self.args.stoploss_range)
|
||||||
|
|
||||||
# If -r/--refresh-pairs-cached is used we add it to the configuration
|
# Hyperopt section
|
||||||
if 'refresh_pairs' in self.args and self.args.refresh_pairs:
|
self._args_to_config(config, argname='hyperopt',
|
||||||
config.update({'refresh_pairs': True})
|
logstring='Using Hyperopt file {}')
|
||||||
logger.info('Parameter -r/--refresh-pairs-cached detected ...')
|
|
||||||
|
self._args_to_config(config, argname='epochs',
|
||||||
|
logstring='Parameter --epochs detected ... '
|
||||||
|
'Will run Hyperopt with for {} epochs ...'
|
||||||
|
)
|
||||||
|
|
||||||
|
self._args_to_config(config, argname='spaces',
|
||||||
|
logstring='Parameter -s/--spaces detected: {}')
|
||||||
|
|
||||||
|
self._args_to_config(config, argname='print_all',
|
||||||
|
logstring='Parameter --print-all detected ...')
|
||||||
|
|
||||||
|
self._args_to_config(config, argname='hyperopt_jobs',
|
||||||
|
logstring='Parameter -j/--job-workers detected: {}')
|
||||||
|
|
||||||
|
self._args_to_config(config, argname='hyperopt_random_state',
|
||||||
|
logstring='Parameter --random-state detected: {}')
|
||||||
|
|
||||||
|
self._args_to_config(config, argname='hyperopt_min_trades',
|
||||||
|
logstring='Parameter --min-trades detected: {}')
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
def _load_hyperopt_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
def _load_plot_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Extract information for sys.argv and load Hyperopt configuration
|
Extract information for sys.argv Plotting configuration
|
||||||
:return: configuration as dictionary
|
:return: configuration as dictionary
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if "hyperopt" in self.args:
|
self._args_to_config(config, argname='pairs',
|
||||||
# Add the hyperopt file to use
|
logstring='Using pairs {}')
|
||||||
config.update({'hyperopt': self.args.hyperopt})
|
|
||||||
|
|
||||||
# If --epochs is used we add it to the configuration
|
self._args_to_config(config, argname='indicators1',
|
||||||
if 'epochs' in self.args and self.args.epochs:
|
logstring='Using indicators1: {}')
|
||||||
config.update({'epochs': self.args.epochs})
|
|
||||||
logger.info('Parameter --epochs detected ...')
|
|
||||||
logger.info('Will run Hyperopt with for %s epochs ...', config.get('epochs'))
|
|
||||||
|
|
||||||
# If --spaces is used we add it to the configuration
|
self._args_to_config(config, argname='indicators2',
|
||||||
if 'spaces' in self.args and self.args.spaces:
|
logstring='Using indicators2: {}')
|
||||||
config.update({'spaces': self.args.spaces})
|
|
||||||
logger.info('Parameter -s/--spaces detected: %s', config.get('spaces'))
|
|
||||||
|
|
||||||
|
self._args_to_config(config, argname='plot_limit',
|
||||||
|
logstring='Limiting plot to: {}')
|
||||||
|
self._args_to_config(config, argname='trade_source',
|
||||||
|
logstring='Using trades from: {}')
|
||||||
return config
|
return config
|
||||||
|
|
||||||
def _validate_config_schema(self, conf: Dict[str, Any]) -> Dict[str, Any]:
|
def _validate_config_schema(self, conf: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
@ -328,7 +369,7 @@ class Configuration(object):
|
||||||
:return: Returns the config if valid, otherwise throw an exception
|
:return: Returns the config if valid, otherwise throw an exception
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
validate(conf, constants.CONF_SCHEMA, Draft4Validator)
|
FreqtradeValidator(constants.CONF_SCHEMA).validate(conf)
|
||||||
return conf
|
return conf
|
||||||
except ValidationError as exception:
|
except ValidationError as exception:
|
||||||
logger.critical(
|
logger.critical(
|
||||||
|
@ -378,21 +419,40 @@ class Configuration(object):
|
||||||
|
|
||||||
return self.config
|
return self.config
|
||||||
|
|
||||||
def check_exchange(self, config: Dict[str, Any]) -> bool:
|
def check_exchange(self, config: Dict[str, Any], check_for_bad: bool = True) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if the exchange name in the config file is supported by Freqtrade
|
Check if the exchange name in the config file is supported by Freqtrade
|
||||||
:return: True or raised an exception if the exchange if not supported
|
:param check_for_bad: if True, check the exchange against the list of known 'bad'
|
||||||
|
exchanges
|
||||||
|
:return: False if exchange is 'bad', i.e. is known to work with the bot with
|
||||||
|
critical issues or does not work at all, crashes, etc. True otherwise.
|
||||||
|
raises an exception if the exchange if not supported by ccxt
|
||||||
|
and thus is not known for the Freqtrade at all.
|
||||||
"""
|
"""
|
||||||
|
logger.info("Checking exchange...")
|
||||||
|
|
||||||
exchange = config.get('exchange', {}).get('name').lower()
|
exchange = config.get('exchange', {}).get('name').lower()
|
||||||
if exchange not in ccxt.exchanges:
|
if not is_exchange_available(exchange):
|
||||||
|
|
||||||
exception_msg = f'Exchange "{exchange}" not supported.\n' \
|
|
||||||
f'The following exchanges are supported: {", ".join(ccxt.exchanges)}'
|
|
||||||
|
|
||||||
logger.critical(exception_msg)
|
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
exception_msg
|
f'Exchange "{exchange}" is not supported by ccxt '
|
||||||
|
f'and therefore not available for the bot.\n'
|
||||||
|
f'The following exchanges are supported by ccxt: '
|
||||||
|
f'{", ".join(available_exchanges())}'
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug('Exchange "%s" supported', exchange)
|
if check_for_bad and is_exchange_bad(exchange):
|
||||||
|
logger.warning(f'Exchange "{exchange}" is known to not work with the bot yet. '
|
||||||
|
f'Use it only for development and testing purposes.')
|
||||||
|
return False
|
||||||
|
|
||||||
|
if is_exchange_officially_supported(exchange):
|
||||||
|
logger.info(f'Exchange "{exchange}" is officially supported '
|
||||||
|
f'by the Freqtrade development team.')
|
||||||
|
else:
|
||||||
|
logger.warning(f'Exchange "{exchange}" is supported by ccxt '
|
||||||
|
f'and therefore available for the bot but not officially supported '
|
||||||
|
f'by the Freqtrade development team. '
|
||||||
|
f'It may work flawlessly (please report back) or have serious issues. '
|
||||||
|
f'Use it at your own discretion.')
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
bot constants
|
bot constants
|
||||||
"""
|
"""
|
||||||
DEFAULT_CONFIG = 'config.json'
|
DEFAULT_CONFIG = 'config.json'
|
||||||
|
DEFAULT_EXCHANGE = 'bittrex'
|
||||||
DYNAMIC_WHITELIST = 20 # pairs
|
DYNAMIC_WHITELIST = 20 # pairs
|
||||||
PROCESS_THROTTLE_SECS = 5 # sec
|
PROCESS_THROTTLE_SECS = 5 # sec
|
||||||
DEFAULT_TICKER_INTERVAL = 5 # min
|
DEFAULT_TICKER_INTERVAL = 5 # min
|
||||||
|
@ -21,6 +22,7 @@ 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',
|
||||||
|
@ -156,6 +158,21 @@ CONF_SCHEMA = {
|
||||||
'webhookstatus': {'type': 'object'},
|
'webhookstatus': {'type': 'object'},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'api_server': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'enabled': {'type': 'boolean'},
|
||||||
|
'listen_ip_address': {'format': 'ipv4'},
|
||||||
|
'listen_port': {
|
||||||
|
'type': 'integer',
|
||||||
|
"minimum": 1024,
|
||||||
|
"maximum": 65535
|
||||||
|
},
|
||||||
|
'username': {'type': 'string'},
|
||||||
|
'password': {'type': 'string'},
|
||||||
|
},
|
||||||
|
'required': ['enabled', 'listen_ip_address', 'listen_port', 'username', 'password']
|
||||||
|
},
|
||||||
'db_url': {'type': 'string'},
|
'db_url': {'type': 'string'},
|
||||||
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
|
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
|
||||||
'forcebuy_enable': {'type': 'boolean'},
|
'forcebuy_enable': {'type': 'boolean'},
|
||||||
|
@ -173,10 +190,10 @@ CONF_SCHEMA = {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
'name': {'type': 'string'},
|
'name': {'type': 'string'},
|
||||||
'sandbox': {'type': 'boolean'},
|
'sandbox': {'type': 'boolean', 'default': False},
|
||||||
'key': {'type': 'string'},
|
'key': {'type': 'string', 'default': ''},
|
||||||
'secret': {'type': 'string'},
|
'secret': {'type': 'string', 'default': ''},
|
||||||
'password': {'type': 'string'},
|
'password': {'type': 'string', 'default': ''},
|
||||||
'uid': {'type': 'string'},
|
'uid': {'type': 'string'},
|
||||||
'pair_whitelist': {
|
'pair_whitelist': {
|
||||||
'type': 'array',
|
'type': 'array',
|
||||||
|
@ -199,7 +216,7 @@ CONF_SCHEMA = {
|
||||||
'ccxt_config': {'type': 'object'},
|
'ccxt_config': {'type': 'object'},
|
||||||
'ccxt_async_config': {'type': 'object'}
|
'ccxt_async_config': {'type': 'object'}
|
||||||
},
|
},
|
||||||
'required': ['name', 'key', 'secret', 'pair_whitelist']
|
'required': ['name', 'pair_whitelist']
|
||||||
},
|
},
|
||||||
'edge': {
|
'edge': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
"""
|
"""
|
||||||
Helpers when analyzing backtest data
|
Helpers when analyzing backtest data
|
||||||
"""
|
"""
|
||||||
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
import pytz
|
||||||
|
|
||||||
|
from freqtrade import persistence
|
||||||
from freqtrade.misc import json_load
|
from freqtrade.misc import json_load
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# must align with columns in backtest.py
|
# must align with columns in backtest.py
|
||||||
BT_DATA_COLUMNS = ["pair", "profitperc", "open_time", "close_time", "index", "duration",
|
BT_DATA_COLUMNS = ["pair", "profitperc", "open_time", "close_time", "index", "duration",
|
||||||
|
@ -17,7 +23,7 @@ def load_backtest_data(filename) -> pd.DataFrame:
|
||||||
"""
|
"""
|
||||||
Load backtest data file.
|
Load backtest data file.
|
||||||
:param filename: pathlib.Path object, or string pointing to the file.
|
:param filename: pathlib.Path object, or string pointing to the file.
|
||||||
:return a dataframe with the analysis results
|
:return: a dataframe with the analysis results
|
||||||
"""
|
"""
|
||||||
if isinstance(filename, str):
|
if isinstance(filename, str):
|
||||||
filename = Path(filename)
|
filename = Path(filename)
|
||||||
|
@ -65,3 +71,41 @@ def evaluate_result_multi(results: pd.DataFrame, freq: str, max_open_trades: int
|
||||||
df2 = df2.set_index('date')
|
df2 = df2.set_index('date')
|
||||||
df_final = df2.resample(freq)[['pair']].count()
|
df_final = df2.resample(freq)[['pair']].count()
|
||||||
return df_final[df_final['pair'] > max_open_trades]
|
return df_final[df_final['pair'] > max_open_trades]
|
||||||
|
|
||||||
|
|
||||||
|
def load_trades_from_db(db_url: str) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Load trades from a DB (using dburl)
|
||||||
|
:param db_url: Sqlite url (default format sqlite:///tradesv3.dry-run.sqlite)
|
||||||
|
:return: Dataframe containing Trades
|
||||||
|
"""
|
||||||
|
trades: pd.DataFrame = pd.DataFrame([], columns=BT_DATA_COLUMNS)
|
||||||
|
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(),
|
||||||
|
t.open_date.replace(tzinfo=pytz.UTC),
|
||||||
|
t.close_date.replace(tzinfo=pytz.UTC) if t.close_date else None,
|
||||||
|
t.open_rate, t.close_rate,
|
||||||
|
t.close_date.timestamp() - t.open_date.timestamp()
|
||||||
|
if t.close_date else None,
|
||||||
|
t.sell_reason,
|
||||||
|
t.max_rate,
|
||||||
|
t.min_rate,
|
||||||
|
)
|
||||||
|
for t in Trade.query.all()],
|
||||||
|
columns=columns)
|
||||||
|
|
||||||
|
return trades
|
||||||
|
|
||||||
|
|
||||||
|
def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Compare trades and backtested pair DataFrames to get trades performed on backtested period
|
||||||
|
:return: the DataFrame of a trades of period
|
||||||
|
"""
|
||||||
|
trades = trades.loc[(trades['open_time'] >= dataframe.iloc[0]['date']) &
|
||||||
|
(trades['close_time'] <= dataframe.iloc[-1]['date'])]
|
||||||
|
return trades
|
||||||
|
|
|
@ -2,22 +2,25 @@
|
||||||
Functions to convert data from one format to another
|
Functions to convert data from one format to another
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from pandas import DataFrame, to_datetime
|
from pandas import DataFrame, to_datetime
|
||||||
from freqtrade.misc import timeframe_to_minutes
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def parse_ticker_dataframe(ticker: list, ticker_interval: str,
|
def parse_ticker_dataframe(ticker: list, ticker_interval: str, pair: str, *,
|
||||||
fill_missing: bool = True) -> DataFrame:
|
fill_missing: bool = True,
|
||||||
|
drop_incomplete: bool = True) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Converts a ticker-list (format ccxt.fetch_ohlcv) to a Dataframe
|
Converts a ticker-list (format ccxt.fetch_ohlcv) to a Dataframe
|
||||||
:param ticker: ticker list, as returned by exchange.async_get_candle_history
|
:param ticker: ticker list, as returned by exchange.async_get_candle_history
|
||||||
:param ticker_interval: ticker_interval (e.g. 5m). Used to fill up eventual missing data
|
:param ticker_interval: ticker_interval (e.g. 5m). Used to fill up eventual missing data
|
||||||
|
:param pair: Pair this data is for (used to warn if fillup was necessary)
|
||||||
:param fill_missing: fill up missing candles with 0 candles
|
:param fill_missing: fill up missing candles with 0 candles
|
||||||
(see ohlcv_fill_up_missing_data for details)
|
(see ohlcv_fill_up_missing_data for details)
|
||||||
|
:param drop_incomplete: Drop the last candle of the dataframe, assuming it's incomplete
|
||||||
:return: DataFrame
|
:return: DataFrame
|
||||||
"""
|
"""
|
||||||
logger.debug("Parsing tickerlist to dataframe")
|
logger.debug("Parsing tickerlist to dataframe")
|
||||||
|
@ -43,21 +46,25 @@ def parse_ticker_dataframe(ticker: list, ticker_interval: str,
|
||||||
'close': 'last',
|
'close': 'last',
|
||||||
'volume': 'max',
|
'volume': 'max',
|
||||||
})
|
})
|
||||||
frame.drop(frame.tail(1).index, inplace=True) # eliminate partial candle
|
# eliminate partial candle
|
||||||
logger.debug('Dropping last candle')
|
if drop_incomplete:
|
||||||
|
frame.drop(frame.tail(1).index, inplace=True)
|
||||||
|
logger.debug('Dropping last candle')
|
||||||
|
|
||||||
if fill_missing:
|
if fill_missing:
|
||||||
return ohlcv_fill_up_missing_data(frame, ticker_interval)
|
return ohlcv_fill_up_missing_data(frame, ticker_interval, pair)
|
||||||
else:
|
else:
|
||||||
return frame
|
return frame
|
||||||
|
|
||||||
|
|
||||||
def ohlcv_fill_up_missing_data(dataframe: DataFrame, ticker_interval: str) -> DataFrame:
|
def ohlcv_fill_up_missing_data(dataframe: DataFrame, ticker_interval: str, pair: str) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Fills up missing data with 0 volume rows,
|
Fills up missing data with 0 volume rows,
|
||||||
using the previous close as price for "open", "high" "low" and "close", volume is set to 0
|
using the previous close as price for "open", "high" "low" and "close", volume is set to 0
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from freqtrade.exchange import timeframe_to_minutes
|
||||||
|
|
||||||
ohlc_dict = {
|
ohlc_dict = {
|
||||||
'open': 'first',
|
'open': 'first',
|
||||||
'high': 'max',
|
'high': 'max',
|
||||||
|
@ -78,7 +85,10 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, ticker_interval: str) -> Da
|
||||||
'low': df['close'],
|
'low': df['close'],
|
||||||
})
|
})
|
||||||
df.reset_index(inplace=True)
|
df.reset_index(inplace=True)
|
||||||
logger.debug(f"Missing data fillup: before: {len(dataframe)} - after: {len(df)}")
|
len_before = len(dataframe)
|
||||||
|
len_after = len(df)
|
||||||
|
if len_before != len_after:
|
||||||
|
logger.info(f"Missing data fillup for {pair}: before: {len_before} - after: {len_after}")
|
||||||
return df
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -85,8 +85,7 @@ class DataProvider(object):
|
||||||
"""
|
"""
|
||||||
return latest orderbook data
|
return latest orderbook data
|
||||||
"""
|
"""
|
||||||
# TODO: Implement me
|
return self._exchange.get_order_book(pair, max)
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def runmode(self) -> RunMode:
|
def runmode(self) -> RunMode:
|
||||||
|
|
|
@ -1,23 +1,24 @@
|
||||||
"""
|
"""
|
||||||
Handle historic data (ohlcv).
|
Handle historic data (ohlcv).
|
||||||
includes:
|
|
||||||
|
Includes:
|
||||||
* load data for a pair (or a list of pairs) from disk
|
* load data for a pair (or a list of pairs) from disk
|
||||||
* download data from exchange and store to disk
|
* download data from exchange and store to disk
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import operator
|
||||||
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, List, Dict, Tuple, Any
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade import misc, OperationalException
|
from freqtrade import OperationalException, misc
|
||||||
from freqtrade.arguments import TimeRange
|
from freqtrade.arguments import TimeRange
|
||||||
from freqtrade.data.converter import parse_ticker_dataframe
|
from freqtrade.data.converter import parse_ticker_dataframe
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange, timeframe_to_minutes
|
||||||
from freqtrade.misc import timeframe_to_minutes
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -62,14 +63,10 @@ def load_tickerdata_file(
|
||||||
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
|
||||||
:return tickerlist or None if unsuccesful
|
:return: tickerlist or None if unsuccesful
|
||||||
"""
|
"""
|
||||||
path = make_testdata_path(datadir)
|
filename = pair_data_filename(datadir, pair, ticker_interval)
|
||||||
pair_s = pair.replace('/', '_')
|
pairdata = misc.file_load_json(filename)
|
||||||
file = path.joinpath(f'{pair_s}-{ticker_interval}.json')
|
|
||||||
|
|
||||||
pairdata = misc.file_load_json(file)
|
|
||||||
|
|
||||||
if not pairdata:
|
if not pairdata:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -84,20 +81,25 @@ def load_pair_history(pair: str,
|
||||||
timerange: TimeRange = TimeRange(None, None, 0, 0),
|
timerange: TimeRange = TimeRange(None, None, 0, 0),
|
||||||
refresh_pairs: bool = False,
|
refresh_pairs: bool = False,
|
||||||
exchange: Optional[Exchange] = None,
|
exchange: Optional[Exchange] = None,
|
||||||
fill_up_missing: bool = True
|
fill_up_missing: bool = True,
|
||||||
|
drop_incomplete: bool = True
|
||||||
) -> DataFrame:
|
) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Loads cached ticker history for the given pair.
|
Loads cached ticker history for the given pair.
|
||||||
|
:param pair: Pair to load data for
|
||||||
|
:param ticker_interval: Ticker-interval (e.g. "5m")
|
||||||
|
:param datadir: Path to the data storage location.
|
||||||
|
:param timerange: Limit data to be loaded to this timerange
|
||||||
|
:param refresh_pairs: Refresh pairs from exchange.
|
||||||
|
(Note: Requires exchange to be passed as well.)
|
||||||
|
:param exchange: Exchange object (needed when using "refresh_pairs")
|
||||||
|
:param fill_up_missing: Fill missing values with "No action"-candles
|
||||||
|
:param drop_incomplete: Drop last candle assuming it may be incomplete.
|
||||||
:return: DataFrame with ohlcv data
|
:return: DataFrame with ohlcv data
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# If the user force the refresh of pairs
|
# The user forced the refresh of pairs
|
||||||
if refresh_pairs:
|
if refresh_pairs:
|
||||||
if not exchange:
|
|
||||||
raise OperationalException("Exchange needs to be initialized when "
|
|
||||||
"calling load_data with refresh_pairs=True")
|
|
||||||
|
|
||||||
logger.info('Download data for pair and store them in %s', datadir)
|
|
||||||
download_pair_history(datadir=datadir,
|
download_pair_history(datadir=datadir,
|
||||||
exchange=exchange,
|
exchange=exchange,
|
||||||
pair=pair,
|
pair=pair,
|
||||||
|
@ -114,11 +116,15 @@ def load_pair_history(pair: str,
|
||||||
logger.warning('Missing data at end for pair %s, data ends at %s',
|
logger.warning('Missing data at end for pair %s, data ends at %s',
|
||||||
pair,
|
pair,
|
||||||
arrow.get(pairdata[-1][0] // 1000).strftime('%Y-%m-%d %H:%M:%S'))
|
arrow.get(pairdata[-1][0] // 1000).strftime('%Y-%m-%d %H:%M:%S'))
|
||||||
return parse_ticker_dataframe(pairdata, ticker_interval, fill_up_missing)
|
return parse_ticker_dataframe(pairdata, ticker_interval, pair=pair,
|
||||||
|
fill_missing=fill_up_missing,
|
||||||
|
drop_incomplete=drop_incomplete)
|
||||||
else:
|
else:
|
||||||
logger.warning('No data for pair: "%s", Interval: %s. '
|
logger.warning(
|
||||||
'Use --refresh-pairs-cached to download the data',
|
f'No history data for pair: "{pair}", interval: {ticker_interval}. '
|
||||||
pair, ticker_interval)
|
'Use --refresh-pairs-cached option or download_backtest_data.py '
|
||||||
|
'script to download the data'
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@ -128,21 +134,34 @@ def load_data(datadir: Optional[Path],
|
||||||
refresh_pairs: bool = False,
|
refresh_pairs: bool = False,
|
||||||
exchange: Optional[Exchange] = None,
|
exchange: Optional[Exchange] = None,
|
||||||
timerange: TimeRange = TimeRange(None, None, 0, 0),
|
timerange: TimeRange = TimeRange(None, None, 0, 0),
|
||||||
fill_up_missing: bool = True) -> Dict[str, DataFrame]:
|
fill_up_missing: bool = True,
|
||||||
|
live: bool = False
|
||||||
|
) -> Dict[str, DataFrame]:
|
||||||
"""
|
"""
|
||||||
Loads ticker history data for a list of pairs the given parameters
|
Loads ticker history data for a list of pairs the given parameters
|
||||||
:return: dict(<pair>:<tickerlist>)
|
:return: dict(<pair>:<tickerlist>)
|
||||||
"""
|
"""
|
||||||
result = {}
|
result: Dict[str, DataFrame] = {}
|
||||||
|
if live:
|
||||||
|
if exchange:
|
||||||
|
logger.info('Live: Downloading data for all defined pairs ...')
|
||||||
|
exchange.refresh_latest_ohlcv([(pair, ticker_interval) for pair in pairs])
|
||||||
|
result = {key[0]: value for key, value in exchange._klines.items() if value is not None}
|
||||||
|
else:
|
||||||
|
raise OperationalException(
|
||||||
|
"Exchange needs to be initialized when using live data."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.info('Using local backtesting data ...')
|
||||||
|
|
||||||
for pair in pairs:
|
for pair in pairs:
|
||||||
hist = load_pair_history(pair=pair, ticker_interval=ticker_interval,
|
hist = load_pair_history(pair=pair, ticker_interval=ticker_interval,
|
||||||
datadir=datadir, timerange=timerange,
|
datadir=datadir, timerange=timerange,
|
||||||
refresh_pairs=refresh_pairs,
|
refresh_pairs=refresh_pairs,
|
||||||
exchange=exchange,
|
exchange=exchange,
|
||||||
fill_up_missing=fill_up_missing)
|
fill_up_missing=fill_up_missing)
|
||||||
if hist is not None:
|
if hist is not None:
|
||||||
result[pair] = hist
|
result[pair] = hist
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@ -151,6 +170,13 @@ def make_testdata_path(datadir: Optional[Path]) -> Path:
|
||||||
return datadir or (Path(__file__).parent.parent / "tests" / "testdata").resolve()
|
return datadir or (Path(__file__).parent.parent / "tests" / "testdata").resolve()
|
||||||
|
|
||||||
|
|
||||||
|
def pair_data_filename(datadir: Optional[Path], pair: str, ticker_interval: str) -> Path:
|
||||||
|
path = make_testdata_path(datadir)
|
||||||
|
pair_s = pair.replace("/", "_")
|
||||||
|
filename = path.joinpath(f'{pair_s}-{ticker_interval}.json')
|
||||||
|
return filename
|
||||||
|
|
||||||
|
|
||||||
def load_cached_data_for_updating(filename: Path, ticker_interval: str,
|
def load_cached_data_for_updating(filename: Path, ticker_interval: str,
|
||||||
timerange: Optional[TimeRange]) -> Tuple[List[Any],
|
timerange: Optional[TimeRange]) -> Tuple[List[Any],
|
||||||
Optional[int]]:
|
Optional[int]]:
|
||||||
|
@ -190,7 +216,7 @@ def load_cached_data_for_updating(filename: Path, ticker_interval: str,
|
||||||
|
|
||||||
|
|
||||||
def download_pair_history(datadir: Optional[Path],
|
def download_pair_history(datadir: Optional[Path],
|
||||||
exchange: Exchange,
|
exchange: Optional[Exchange],
|
||||||
pair: str,
|
pair: str,
|
||||||
ticker_interval: str = '5m',
|
ticker_interval: str = '5m',
|
||||||
timerange: Optional[TimeRange] = None) -> bool:
|
timerange: Optional[TimeRange] = None) -> bool:
|
||||||
|
@ -201,18 +227,24 @@ def download_pair_history(datadir: Optional[Path],
|
||||||
the full data will be redownloaded
|
the full data will be redownloaded
|
||||||
|
|
||||||
Based on @Rybolov work: https://github.com/rybolov/freqtrade-data
|
Based on @Rybolov work: https://github.com/rybolov/freqtrade-data
|
||||||
|
|
||||||
:param pair: pair to download
|
:param pair: pair to download
|
||||||
:param ticker_interval: ticker interval
|
:param ticker_interval: ticker interval
|
||||||
:param timerange: range of time to download
|
:param timerange: range of time to download
|
||||||
:return: bool with success state
|
:return: bool with success state
|
||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
if not exchange:
|
||||||
path = make_testdata_path(datadir)
|
raise OperationalException(
|
||||||
filepair = pair.replace("/", "_")
|
"Exchange needs to be initialized when downloading pair history data"
|
||||||
filename = path.joinpath(f'{filepair}-{ticker_interval}.json')
|
)
|
||||||
|
|
||||||
logger.info('Download the pair: "%s", Interval: %s', pair, ticker_interval)
|
try:
|
||||||
|
filename = pair_data_filename(datadir, pair, ticker_interval)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f'Download history data for pair: "{pair}", interval: {ticker_interval} '
|
||||||
|
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(filename, ticker_interval, timerange)
|
||||||
|
|
||||||
|
@ -231,7 +263,46 @@ def download_pair_history(datadir: Optional[Path],
|
||||||
|
|
||||||
misc.file_dump_json(filename, data)
|
misc.file_dump_json(filename, data)
|
||||||
return True
|
return True
|
||||||
except BaseException:
|
|
||||||
logger.info('Failed to download the pair: "%s", Interval: %s',
|
except Exception as e:
|
||||||
pair, ticker_interval)
|
logger.error(
|
||||||
|
f'Failed to download history data for pair: "{pair}", interval: {ticker_interval}. '
|
||||||
|
f'Error: {e}'
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
|
||||||
|
"""
|
||||||
|
Get the maximum timeframe for the given backtest data
|
||||||
|
:param data: dictionary with preprocessed backtesting data
|
||||||
|
:return: tuple containing min_date, max_date
|
||||||
|
"""
|
||||||
|
timeframe = [
|
||||||
|
(arrow.get(frame['date'].min()), arrow.get(frame['date'].max()))
|
||||||
|
for frame in data.values()
|
||||||
|
]
|
||||||
|
return min(timeframe, key=operator.itemgetter(0))[0], \
|
||||||
|
max(timeframe, key=operator.itemgetter(1))[1]
|
||||||
|
|
||||||
|
|
||||||
|
def validate_backtest_data(data: DataFrame, pair: str, min_date: datetime,
|
||||||
|
max_date: datetime, ticker_interval_mins: int) -> bool:
|
||||||
|
"""
|
||||||
|
Validates preprocessed backtesting data for missing values and shows warnings about it that.
|
||||||
|
|
||||||
|
:param data: preprocessed backtesting data (as DataFrame)
|
||||||
|
:param pair: pair used for log output.
|
||||||
|
:param min_date: start-date of the data
|
||||||
|
:param max_date: end-date of the data
|
||||||
|
:param ticker_interval_mins: ticker interval in minutes
|
||||||
|
"""
|
||||||
|
# total difference in minutes / interval-minutes
|
||||||
|
expected_frames = int((max_date - min_date).total_seconds() // 60 // ticker_interval_mins)
|
||||||
|
found_missing = False
|
||||||
|
dflen = len(data)
|
||||||
|
if dflen < expected_frames:
|
||||||
|
found_missing = True
|
||||||
|
logger.warning("%s has missing frames: expected %s, got %s, that's %s missing values",
|
||||||
|
pair, expected_frames, dflen, expected_frames - dflen)
|
||||||
|
return found_missing
|
||||||
|
|
|
@ -13,7 +13,6 @@ from freqtrade import constants, OperationalException
|
||||||
from freqtrade.arguments import Arguments
|
from freqtrade.arguments import Arguments
|
||||||
from freqtrade.arguments import TimeRange
|
from freqtrade.arguments import TimeRange
|
||||||
from freqtrade.data import history
|
from freqtrade.data import history
|
||||||
from freqtrade.optimize import get_timeframe
|
|
||||||
from freqtrade.strategy.interface import SellType
|
from freqtrade.strategy.interface import SellType
|
||||||
|
|
||||||
|
|
||||||
|
@ -47,11 +46,6 @@ class Edge():
|
||||||
self.config = config
|
self.config = config
|
||||||
self.exchange = exchange
|
self.exchange = exchange
|
||||||
self.strategy = strategy
|
self.strategy = strategy
|
||||||
self.ticker_interval = self.strategy.ticker_interval
|
|
||||||
self.tickerdata_to_dataframe = self.strategy.tickerdata_to_dataframe
|
|
||||||
self.get_timeframe = get_timeframe
|
|
||||||
self.advise_sell = self.strategy.advise_sell
|
|
||||||
self.advise_buy = self.strategy.advise_buy
|
|
||||||
|
|
||||||
self.edge_config = self.config.get('edge', {})
|
self.edge_config = self.config.get('edge', {})
|
||||||
self._cached_pairs: Dict[str, Any] = {} # Keeps a list of pairs
|
self._cached_pairs: Dict[str, Any] = {} # Keeps a list of pairs
|
||||||
|
@ -102,7 +96,7 @@ class Edge():
|
||||||
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,
|
||||||
pairs=pairs,
|
pairs=pairs,
|
||||||
ticker_interval=self.ticker_interval,
|
ticker_interval=self.strategy.ticker_interval,
|
||||||
refresh_pairs=self._refresh_pairs,
|
refresh_pairs=self._refresh_pairs,
|
||||||
exchange=self.exchange,
|
exchange=self.exchange,
|
||||||
timerange=self._timerange
|
timerange=self._timerange
|
||||||
|
@ -114,10 +108,10 @@ class Edge():
|
||||||
logger.critical("No data found. Edge is stopped ...")
|
logger.critical("No data found. Edge is stopped ...")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
preprocessed = self.tickerdata_to_dataframe(data)
|
preprocessed = self.strategy.tickerdata_to_dataframe(data)
|
||||||
|
|
||||||
# Print timeframe
|
# Print timeframe
|
||||||
min_date, max_date = self.get_timeframe(preprocessed)
|
min_date, max_date = history.get_timeframe(preprocessed)
|
||||||
logger.info(
|
logger.info(
|
||||||
'Measuring data from %s up to %s (%s days) ...',
|
'Measuring data from %s up to %s (%s days) ...',
|
||||||
min_date.isoformat(),
|
min_date.isoformat(),
|
||||||
|
@ -132,13 +126,14 @@ class Edge():
|
||||||
pair_data = pair_data.sort_values(by=['date'])
|
pair_data = pair_data.sort_values(by=['date'])
|
||||||
pair_data = pair_data.reset_index(drop=True)
|
pair_data = pair_data.reset_index(drop=True)
|
||||||
|
|
||||||
ticker_data = self.advise_sell(
|
ticker_data = self.strategy.advise_sell(
|
||||||
self.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy()
|
self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy()
|
||||||
|
|
||||||
trades += self._find_trades_for_stoploss_range(ticker_data, pair, self._stoploss_range)
|
trades += self._find_trades_for_stoploss_range(ticker_data, pair, self._stoploss_range)
|
||||||
|
|
||||||
# If no trade found then exit
|
# If no trade found then exit
|
||||||
if len(trades) == 0:
|
if len(trades) == 0:
|
||||||
|
logger.info("No trades found.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Fill missing, calculable columns, profit, duration , abs etc.
|
# Fill missing, calculable columns, profit, duration , abs etc.
|
||||||
|
|
|
@ -1,3 +1,10 @@
|
||||||
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
|
||||||
|
is_exchange_available,
|
||||||
|
is_exchange_officially_supported,
|
||||||
|
available_exchanges)
|
||||||
|
from freqtrade.exchange.exchange import (timeframe_to_seconds, # noqa: F401
|
||||||
|
timeframe_to_minutes,
|
||||||
|
timeframe_to_msecs)
|
||||||
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
|
||||||
|
|
|
@ -1,23 +1,25 @@
|
||||||
# pragma pylint: disable=W0603
|
# pragma pylint: disable=W0603
|
||||||
""" Cryptocurrency Exchanges support """
|
"""
|
||||||
import logging
|
Cryptocurrency Exchanges support
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
import inspect
|
import inspect
|
||||||
from random import randint
|
import logging
|
||||||
from typing import List, Dict, Tuple, Any, Optional
|
from copy import deepcopy
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from math import floor, ceil
|
from math import ceil, floor
|
||||||
|
from random import randint
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import asyncio
|
|
||||||
import ccxt
|
import ccxt
|
||||||
import ccxt.async_support as ccxt_async
|
import ccxt.async_support as ccxt_async
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade import (constants, DependencyException, OperationalException,
|
from freqtrade import (DependencyException, InvalidOrderException,
|
||||||
TemporaryError, InvalidOrderException)
|
OperationalException, TemporaryError, constants)
|
||||||
from freqtrade.data.converter import parse_ticker_dataframe
|
from freqtrade.data.converter import parse_ticker_dataframe
|
||||||
from freqtrade.misc import timeframe_to_seconds, timeframe_to_msecs
|
from freqtrade.misc import deep_merge_dicts
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -67,12 +69,15 @@ class Exchange(object):
|
||||||
_params: Dict = {}
|
_params: Dict = {}
|
||||||
|
|
||||||
# Dict to specify which options each exchange implements
|
# Dict to specify which options each exchange implements
|
||||||
# TODO: this should be merged with attributes from subclasses
|
# This defines defaults, which can be selectively overridden by subclasses using _ft_has
|
||||||
# To avoid having to copy/paste this to all subclasses.
|
# or by specifying them in the configuration.
|
||||||
_ft_has: Dict = {
|
_ft_has_default: Dict = {
|
||||||
"stoploss_on_exchange": False,
|
"stoploss_on_exchange": False,
|
||||||
"order_time_in_force": ["gtc"],
|
"order_time_in_force": ["gtc"],
|
||||||
|
"ohlcv_candle_limit": 500,
|
||||||
|
"ohlcv_partial_candle": True,
|
||||||
}
|
}
|
||||||
|
_ft_has: Dict = {}
|
||||||
|
|
||||||
def __init__(self, config: dict) -> None:
|
def __init__(self, config: dict) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -99,6 +104,19 @@ class Exchange(object):
|
||||||
logger.info('Instance is running with dry_run enabled')
|
logger.info('Instance is running with dry_run enabled')
|
||||||
|
|
||||||
exchange_config = config['exchange']
|
exchange_config = config['exchange']
|
||||||
|
|
||||||
|
# Deep merge ft_has with default ft_has options
|
||||||
|
self._ft_has = deep_merge_dicts(self._ft_has, deepcopy(self._ft_has_default))
|
||||||
|
if exchange_config.get("_ft_has_params"):
|
||||||
|
self._ft_has = deep_merge_dicts(exchange_config.get("_ft_has_params"),
|
||||||
|
self._ft_has)
|
||||||
|
logger.info("Overriding exchange._ft_has with config params, result: %s", self._ft_has)
|
||||||
|
|
||||||
|
# Assign this directly for easy access
|
||||||
|
self._ohlcv_candle_limit = self._ft_has['ohlcv_candle_limit']
|
||||||
|
self._ohlcv_partial_candle = self._ft_has['ohlcv_partial_candle']
|
||||||
|
|
||||||
|
# Initialize ccxt objects
|
||||||
self._api: ccxt.Exchange = self._init_ccxt(
|
self._api: ccxt.Exchange = self._init_ccxt(
|
||||||
exchange_config, ccxt_kwargs=exchange_config.get('ccxt_config'))
|
exchange_config, ccxt_kwargs=exchange_config.get('ccxt_config'))
|
||||||
self._api_async: ccxt_async.Exchange = self._init_ccxt(
|
self._api_async: ccxt_async.Exchange = self._init_ccxt(
|
||||||
|
@ -138,8 +156,8 @@ class Exchange(object):
|
||||||
# Find matching class for the given exchange name
|
# Find matching class for the given exchange name
|
||||||
name = exchange_config['name']
|
name = exchange_config['name']
|
||||||
|
|
||||||
if name not in ccxt_module.exchanges:
|
if not is_exchange_available(name, ccxt_module):
|
||||||
raise OperationalException(f'Exchange {name} is not supported')
|
raise OperationalException(f'Exchange {name} is not supported by ccxt')
|
||||||
|
|
||||||
ex_config = {
|
ex_config = {
|
||||||
'apiKey': exchange_config.get('key'),
|
'apiKey': exchange_config.get('key'),
|
||||||
|
@ -221,8 +239,11 @@ class Exchange(object):
|
||||||
> arrow.utcnow().timestamp):
|
> arrow.utcnow().timestamp):
|
||||||
return None
|
return None
|
||||||
logger.debug("Performing scheduled market reload..")
|
logger.debug("Performing scheduled market reload..")
|
||||||
self._api.load_markets(reload=True)
|
try:
|
||||||
self._last_markets_refresh = arrow.utcnow().timestamp
|
self._api.load_markets(reload=True)
|
||||||
|
self._last_markets_refresh = arrow.utcnow().timestamp
|
||||||
|
except ccxt.BaseError:
|
||||||
|
logger.exception("Could not reload markets.")
|
||||||
|
|
||||||
def validate_pairs(self, pairs: List[str]) -> None:
|
def validate_pairs(self, pairs: List[str]) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -502,11 +523,13 @@ class Exchange(object):
|
||||||
async def _async_get_history(self, pair: str,
|
async def _async_get_history(self, pair: str,
|
||||||
ticker_interval: str,
|
ticker_interval: str,
|
||||||
since_ms: int) -> List:
|
since_ms: int) -> List:
|
||||||
# Assume exchange returns 500 candles
|
|
||||||
_LIMIT = 500
|
|
||||||
|
|
||||||
one_call = timeframe_to_msecs(ticker_interval) * _LIMIT
|
one_call = timeframe_to_msecs(ticker_interval) * self._ohlcv_candle_limit
|
||||||
logger.debug("one_call: %s msecs", one_call)
|
logger.debug(
|
||||||
|
"one_call: %s msecs (%s)",
|
||||||
|
one_call,
|
||||||
|
arrow.utcnow().shift(seconds=one_call // 1000).humanize(only_distance=True)
|
||||||
|
)
|
||||||
input_coroutines = [self._async_get_candle_history(
|
input_coroutines = [self._async_get_candle_history(
|
||||||
pair, ticker_interval, since) for since in
|
pair, ticker_interval, since) for since in
|
||||||
range(since_ms, arrow.utcnow().timestamp * 1000, one_call)]
|
range(since_ms, arrow.utcnow().timestamp * 1000, one_call)]
|
||||||
|
@ -537,7 +560,10 @@ class Exchange(object):
|
||||||
or self._now_is_time_to_refresh(pair, ticker_interval)):
|
or self._now_is_time_to_refresh(pair, ticker_interval)):
|
||||||
input_coroutines.append(self._async_get_candle_history(pair, ticker_interval))
|
input_coroutines.append(self._async_get_candle_history(pair, ticker_interval))
|
||||||
else:
|
else:
|
||||||
logger.debug("Using cached ohlcv data for %s, %s ...", pair, ticker_interval)
|
logger.debug(
|
||||||
|
"Using cached ohlcv data for pair %s, interval %s ...",
|
||||||
|
pair, ticker_interval
|
||||||
|
)
|
||||||
|
|
||||||
tickers = asyncio.get_event_loop().run_until_complete(
|
tickers = asyncio.get_event_loop().run_until_complete(
|
||||||
asyncio.gather(*input_coroutines, return_exceptions=True))
|
asyncio.gather(*input_coroutines, return_exceptions=True))
|
||||||
|
@ -555,7 +581,8 @@ class Exchange(object):
|
||||||
self._pairs_last_refresh_time[(pair, ticker_interval)] = ticks[-1][0] // 1000
|
self._pairs_last_refresh_time[(pair, ticker_interval)] = ticks[-1][0] // 1000
|
||||||
# keeping parsed dataframe in cache
|
# keeping parsed dataframe in cache
|
||||||
self._klines[(pair, ticker_interval)] = parse_ticker_dataframe(
|
self._klines[(pair, ticker_interval)] = parse_ticker_dataframe(
|
||||||
ticks, ticker_interval, fill_missing=True)
|
ticks, ticker_interval, pair=pair, fill_missing=True,
|
||||||
|
drop_incomplete=self._ohlcv_partial_candle)
|
||||||
return tickers
|
return tickers
|
||||||
|
|
||||||
def _now_is_time_to_refresh(self, pair: str, ticker_interval: str) -> bool:
|
def _now_is_time_to_refresh(self, pair: str, ticker_interval: str) -> bool:
|
||||||
|
@ -574,7 +601,11 @@ class Exchange(object):
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# fetch ohlcv asynchronously
|
# fetch ohlcv asynchronously
|
||||||
logger.debug("fetching %s, %s since %s ...", pair, ticker_interval, since_ms)
|
s = '(' + arrow.get(since_ms // 1000).isoformat() + ') ' if since_ms is not None else ''
|
||||||
|
logger.debug(
|
||||||
|
"Fetching pair %s, interval %s, since %s %s...",
|
||||||
|
pair, ticker_interval, since_ms, s
|
||||||
|
)
|
||||||
|
|
||||||
data = await self._api_async.fetch_ohlcv(pair, timeframe=ticker_interval,
|
data = await self._api_async.fetch_ohlcv(pair, timeframe=ticker_interval,
|
||||||
since=since_ms)
|
since=since_ms)
|
||||||
|
@ -589,7 +620,7 @@ class Exchange(object):
|
||||||
except IndexError:
|
except IndexError:
|
||||||
logger.exception("Error loading %s. Result was %s.", pair, data)
|
logger.exception("Error loading %s. Result was %s.", pair, data)
|
||||||
return pair, ticker_interval, []
|
return pair, ticker_interval, []
|
||||||
logger.debug("done fetching %s, %s ...", pair, ticker_interval)
|
logger.debug("Done fetching pair %s, interval %s ...", pair, ticker_interval)
|
||||||
return pair, ticker_interval, data
|
return pair, ticker_interval, data
|
||||||
|
|
||||||
except ccxt.NotSupported as e:
|
except ccxt.NotSupported as e:
|
||||||
|
@ -689,3 +720,42 @@ class Exchange(object):
|
||||||
f'Could not get fee info due to {e.__class__.__name__}. Message: {e}')
|
f'Could not get fee info due to {e.__class__.__name__}. Message: {e}')
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e)
|
raise OperationalException(e)
|
||||||
|
|
||||||
|
|
||||||
|
def is_exchange_bad(exchange: str) -> bool:
|
||||||
|
return exchange in ['bitmex']
|
||||||
|
|
||||||
|
|
||||||
|
def is_exchange_available(exchange: str, ccxt_module=None) -> bool:
|
||||||
|
return exchange in available_exchanges(ccxt_module)
|
||||||
|
|
||||||
|
|
||||||
|
def is_exchange_officially_supported(exchange: str) -> bool:
|
||||||
|
return exchange in ['bittrex', 'binance']
|
||||||
|
|
||||||
|
|
||||||
|
def available_exchanges(ccxt_module=None) -> List[str]:
|
||||||
|
return ccxt_module.exchanges if ccxt_module is not None else ccxt.exchanges
|
||||||
|
|
||||||
|
|
||||||
|
def timeframe_to_seconds(ticker_interval: str) -> int:
|
||||||
|
"""
|
||||||
|
Translates the timeframe interval value written in the human readable
|
||||||
|
form ('1m', '5m', '1h', '1d', '1w', etc.) to the number
|
||||||
|
of seconds for one timeframe interval.
|
||||||
|
"""
|
||||||
|
return ccxt.Exchange.parse_timeframe(ticker_interval)
|
||||||
|
|
||||||
|
|
||||||
|
def timeframe_to_minutes(ticker_interval: str) -> int:
|
||||||
|
"""
|
||||||
|
Same as above, but returns minutes.
|
||||||
|
"""
|
||||||
|
return ccxt.Exchange.parse_timeframe(ticker_interval) // 60
|
||||||
|
|
||||||
|
|
||||||
|
def timeframe_to_msecs(ticker_interval: str) -> int:
|
||||||
|
"""
|
||||||
|
Same as above, but returns milliseconds.
|
||||||
|
"""
|
||||||
|
return ccxt.Exchange.parse_timeframe(ticker_interval) * 1000
|
||||||
|
|
|
@ -16,7 +16,7 @@ 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.misc import timeframe_to_minutes
|
from freqtrade.exchange import timeframe_to_minutes
|
||||||
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
|
||||||
|
@ -53,8 +53,7 @@ class FreqtradeBot(object):
|
||||||
|
|
||||||
self.rpc: RPCManager = RPCManager(self)
|
self.rpc: RPCManager = RPCManager(self)
|
||||||
|
|
||||||
exchange_name = self.config.get('exchange', {}).get('name', 'bittrex').title()
|
self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange
|
||||||
self.exchange = ExchangeResolver(exchange_name, self.config).exchange
|
|
||||||
|
|
||||||
self.wallets = Wallets(self.config, self.exchange)
|
self.wallets = Wallets(self.config, self.exchange)
|
||||||
self.dataprovider = DataProvider(self.config, self.exchange)
|
self.dataprovider = DataProvider(self.config, self.exchange)
|
||||||
|
@ -73,7 +72,8 @@ class FreqtradeBot(object):
|
||||||
|
|
||||||
self.active_pair_whitelist: List[str] = self.config['exchange']['pair_whitelist']
|
self.active_pair_whitelist: List[str] = self.config['exchange']['pair_whitelist']
|
||||||
|
|
||||||
persistence.init(self.config)
|
persistence.init(self.config.get('db_url', None),
|
||||||
|
clean_open_orders=self.config.get('dry_run', 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')
|
||||||
|
@ -89,6 +89,16 @@ class FreqtradeBot(object):
|
||||||
self.rpc.cleanup()
|
self.rpc.cleanup()
|
||||||
persistence.cleanup()
|
persistence.cleanup()
|
||||||
|
|
||||||
|
def startup(self) -> None:
|
||||||
|
"""
|
||||||
|
Called on startup and after reloading the bot - triggers notifications and
|
||||||
|
performs startup tasks
|
||||||
|
"""
|
||||||
|
self.rpc.startup_messages(self.config, self.pairlists)
|
||||||
|
if not self.edge:
|
||||||
|
# Adjust stoploss if it was changed
|
||||||
|
Trade.stoploss_reinitialization(self.strategy.stoploss)
|
||||||
|
|
||||||
def process(self) -> bool:
|
def process(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Queries the persistence layer for open trades and handles them,
|
Queries the persistence layer for open trades and handles them,
|
||||||
|
@ -194,19 +204,19 @@ class FreqtradeBot(object):
|
||||||
else:
|
else:
|
||||||
stake_amount = self.config['stake_amount']
|
stake_amount = self.config['stake_amount']
|
||||||
|
|
||||||
avaliable_amount = self.wallets.get_free(self.config['stake_currency'])
|
available_amount = self.wallets.get_free(self.config['stake_currency'])
|
||||||
|
|
||||||
if stake_amount == constants.UNLIMITED_STAKE_AMOUNT:
|
if stake_amount == constants.UNLIMITED_STAKE_AMOUNT:
|
||||||
open_trades = len(Trade.get_open_trades())
|
open_trades = len(Trade.get_open_trades())
|
||||||
if open_trades >= self.config['max_open_trades']:
|
if open_trades >= self.config['max_open_trades']:
|
||||||
logger.warning('Can\'t open a new trade: max number of trades is reached')
|
logger.warning('Can\'t open a new trade: max number of trades is reached')
|
||||||
return None
|
return None
|
||||||
return avaliable_amount / (self.config['max_open_trades'] - open_trades)
|
return available_amount / (self.config['max_open_trades'] - open_trades)
|
||||||
|
|
||||||
# Check if stake_amount is fulfilled
|
# Check if stake_amount is fulfilled
|
||||||
if avaliable_amount < stake_amount:
|
if available_amount < stake_amount:
|
||||||
raise DependencyException(
|
raise DependencyException(
|
||||||
f"Available balance({avaliable_amount} {self.config['stake_currency']}) is "
|
f"Available balance({available_amount} {self.config['stake_currency']}) is "
|
||||||
f"lower than stake amount({stake_amount} {self.config['stake_currency']})"
|
f"lower than stake amount({stake_amount} {self.config['stake_currency']})"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -334,8 +344,8 @@ class FreqtradeBot(object):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
amount = stake_amount / buy_limit_requested
|
amount = stake_amount / buy_limit_requested
|
||||||
|
order_type = self.strategy.order_types['buy']
|
||||||
order = self.exchange.buy(pair=pair, ordertype=self.strategy.order_types['buy'],
|
order = self.exchange.buy(pair=pair, ordertype=order_type,
|
||||||
amount=amount, rate=buy_limit_requested,
|
amount=amount, rate=buy_limit_requested,
|
||||||
time_in_force=time_in_force)
|
time_in_force=time_in_force)
|
||||||
order_id = order['id']
|
order_id = order['id']
|
||||||
|
@ -345,7 +355,6 @@ class FreqtradeBot(object):
|
||||||
buy_limit_filled_price = buy_limit_requested
|
buy_limit_filled_price = buy_limit_requested
|
||||||
|
|
||||||
if order_status == 'expired' or order_status == 'rejected':
|
if order_status == 'expired' or order_status == 'rejected':
|
||||||
order_type = self.strategy.order_types['buy']
|
|
||||||
order_tif = self.strategy.order_time_in_force['buy']
|
order_tif = self.strategy.order_time_in_force['buy']
|
||||||
|
|
||||||
# return false if the order is not filled
|
# return false if the order is not filled
|
||||||
|
@ -379,6 +388,7 @@ class FreqtradeBot(object):
|
||||||
'exchange': self.exchange.name.capitalize(),
|
'exchange': self.exchange.name.capitalize(),
|
||||||
'pair': pair_s,
|
'pair': pair_s,
|
||||||
'limit': buy_limit_filled_price,
|
'limit': buy_limit_filled_price,
|
||||||
|
'order_type': order_type,
|
||||||
'stake_amount': stake_amount,
|
'stake_amount': stake_amount,
|
||||||
'stake_currency': stake_currency,
|
'stake_currency': stake_currency,
|
||||||
'fiat_currency': fiat_currency
|
'fiat_currency': fiat_currency
|
||||||
|
@ -460,7 +470,7 @@ class FreqtradeBot(object):
|
||||||
def get_real_amount(self, trade: Trade, order: Dict) -> float:
|
def get_real_amount(self, trade: Trade, order: Dict) -> float:
|
||||||
"""
|
"""
|
||||||
Get real amount for the trade
|
Get real amount for the trade
|
||||||
Necessary for self.exchanges which charge fees in base currency (e.g. binance)
|
Necessary for exchanges which charge fees in base currency (e.g. binance)
|
||||||
"""
|
"""
|
||||||
order_amount = order['amount']
|
order_amount = order['amount']
|
||||||
# Only run for closed orders
|
# Only run for closed orders
|
||||||
|
@ -522,6 +532,10 @@ class FreqtradeBot(object):
|
||||||
|
|
||||||
trade.update(order)
|
trade.update(order)
|
||||||
|
|
||||||
|
# Updating wallets when order is closed
|
||||||
|
if not trade.is_open:
|
||||||
|
self.wallets.update()
|
||||||
|
|
||||||
def get_sell_rate(self, pair: str, refresh: bool) -> float:
|
def get_sell_rate(self, pair: str, refresh: bool) -> float:
|
||||||
"""
|
"""
|
||||||
Get sell rate - either using get-ticker bid or first bid based on orderbook
|
Get sell rate - either using get-ticker bid or first bid based on orderbook
|
||||||
|
@ -676,13 +690,22 @@ class FreqtradeBot(object):
|
||||||
# cancelling the current stoploss on exchange first
|
# cancelling the current stoploss on exchange first
|
||||||
logger.info('Trailing stoploss: cancelling current stoploss on exchange (id:{%s})'
|
logger.info('Trailing stoploss: cancelling current stoploss on exchange (id:{%s})'
|
||||||
'in order to add another one ...', order['id'])
|
'in order to add another one ...', order['id'])
|
||||||
if self.exchange.cancel_order(order['id'], trade.pair):
|
try:
|
||||||
|
self.exchange.cancel_order(order['id'], trade.pair)
|
||||||
|
except InvalidOrderException:
|
||||||
|
logger.exception(f"Could not cancel stoploss order {order['id']} "
|
||||||
|
f"for pair {trade.pair}")
|
||||||
|
|
||||||
|
try:
|
||||||
# creating the new one
|
# creating the new one
|
||||||
stoploss_order_id = self.exchange.stoploss_limit(
|
stoploss_order_id = self.exchange.stoploss_limit(
|
||||||
pair=trade.pair, amount=trade.amount,
|
pair=trade.pair, amount=trade.amount,
|
||||||
stop_price=trade.stop_loss, rate=trade.stop_loss * 0.99
|
stop_price=trade.stop_loss, rate=trade.stop_loss * 0.99
|
||||||
)['id']
|
)['id']
|
||||||
trade.stoploss_order_id = str(stoploss_order_id)
|
trade.stoploss_order_id = str(stoploss_order_id)
|
||||||
|
except DependencyException:
|
||||||
|
logger.exception(f"Could create trailing stoploss order "
|
||||||
|
f"for pair {trade.pair}.")
|
||||||
|
|
||||||
def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool:
|
def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool:
|
||||||
if self.edge:
|
if self.edge:
|
||||||
|
@ -828,7 +851,10 @@ class FreqtradeBot(object):
|
||||||
|
|
||||||
# First cancelling stoploss on exchange ...
|
# First cancelling stoploss on exchange ...
|
||||||
if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id:
|
if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id:
|
||||||
self.exchange.cancel_order(trade.stoploss_order_id, trade.pair)
|
try:
|
||||||
|
self.exchange.cancel_order(trade.stoploss_order_id, trade.pair)
|
||||||
|
except InvalidOrderException:
|
||||||
|
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_id = self.exchange.sell(pair=str(trade.pair),
|
||||||
|
@ -860,6 +886,7 @@ class FreqtradeBot(object):
|
||||||
'pair': trade.pair,
|
'pair': trade.pair,
|
||||||
'gain': gain,
|
'gain': gain,
|
||||||
'limit': trade.close_rate_requested,
|
'limit': trade.close_rate_requested,
|
||||||
|
'order_type': self.strategy.order_types['sell'],
|
||||||
'amount': trade.amount,
|
'amount': trade.amount,
|
||||||
'open_rate': trade.open_rate,
|
'open_rate': trade.open_rate,
|
||||||
'current_rate': current_rate,
|
'current_rate': current_rate,
|
||||||
|
|
|
@ -3,10 +3,16 @@
|
||||||
Main Freqtrade bot script.
|
Main Freqtrade bot script.
|
||||||
Read the documentation to know what cli arguments you need.
|
Read the documentation to know what cli arguments you need.
|
||||||
"""
|
"""
|
||||||
import logging
|
|
||||||
import sys
|
import sys
|
||||||
|
# check min. python version
|
||||||
|
if sys.version_info < (3, 6):
|
||||||
|
sys.exit("Freqtrade requires Python version >= 3.6")
|
||||||
|
|
||||||
|
# flake8: noqa E402
|
||||||
|
import logging
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from typing import List
|
from typing import Any, List
|
||||||
|
|
||||||
from freqtrade import OperationalException
|
from freqtrade import OperationalException
|
||||||
from freqtrade.arguments import Arguments
|
from freqtrade.arguments import Arguments
|
||||||
|
@ -17,37 +23,43 @@ from freqtrade.worker import Worker
|
||||||
logger = logging.getLogger('freqtrade')
|
logger = logging.getLogger('freqtrade')
|
||||||
|
|
||||||
|
|
||||||
def main(sysargv: List[str]) -> None:
|
def main(sysargv: List[str] = None) -> None:
|
||||||
"""
|
"""
|
||||||
This function will initiate the bot and start the trading loop.
|
This function will initiate the bot and start the trading loop.
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
arguments = Arguments(
|
|
||||||
sysargv,
|
|
||||||
'Free, open source crypto trading bot'
|
|
||||||
)
|
|
||||||
args: Namespace = arguments.get_parsed_arg()
|
|
||||||
|
|
||||||
# A subcommand has been issued.
|
|
||||||
# Means if Backtesting or Hyperopt have been called we exit the bot
|
|
||||||
if hasattr(args, 'func'):
|
|
||||||
args.func(args)
|
|
||||||
return
|
|
||||||
|
|
||||||
|
return_code: Any = 1
|
||||||
worker = None
|
worker = None
|
||||||
return_code = 1
|
|
||||||
try:
|
try:
|
||||||
# Load and run worker
|
set_loggers()
|
||||||
worker = Worker(args)
|
|
||||||
worker.run()
|
|
||||||
|
|
||||||
|
arguments = Arguments(
|
||||||
|
sysargv,
|
||||||
|
'Free, open source crypto trading bot'
|
||||||
|
)
|
||||||
|
args: Namespace = arguments.get_parsed_arg()
|
||||||
|
|
||||||
|
# A subcommand has been issued.
|
||||||
|
# Means if Backtesting or Hyperopt have been called we exit the bot
|
||||||
|
if hasattr(args, 'func'):
|
||||||
|
args.func(args)
|
||||||
|
# TODO: fetch return_code as returned by the command function here
|
||||||
|
return_code = 0
|
||||||
|
else:
|
||||||
|
# Load and run worker
|
||||||
|
worker = Worker(args)
|
||||||
|
worker.run()
|
||||||
|
|
||||||
|
except SystemExit as e:
|
||||||
|
return_code = e
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logger.info('SIGINT received, aborting ...')
|
logger.info('SIGINT received, aborting ...')
|
||||||
return_code = 0
|
return_code = 0
|
||||||
except OperationalException as e:
|
except OperationalException as e:
|
||||||
logger.error(str(e))
|
logger.error(str(e))
|
||||||
return_code = 2
|
return_code = 2
|
||||||
except BaseException:
|
except Exception:
|
||||||
logger.exception('Fatal exception!')
|
logger.exception('Fatal exception!')
|
||||||
finally:
|
finally:
|
||||||
if worker:
|
if worker:
|
||||||
|
@ -56,5 +68,4 @@ def main(sysargv: List[str]) -> None:
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
set_loggers()
|
main()
|
||||||
main(sys.argv[1:])
|
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
"""
|
"""
|
||||||
Various tool function for Freqtrade and scripts
|
Various tool function for Freqtrade and scripts
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import gzip
|
import gzip
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
from ccxt import Exchange
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
import rapidjson
|
import rapidjson
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -118,6 +117,8 @@ def format_ms_time(date: int) -> str:
|
||||||
|
|
||||||
def deep_merge_dicts(source, destination):
|
def deep_merge_dicts(source, destination):
|
||||||
"""
|
"""
|
||||||
|
Values from Source override destination, destination is returned (and modified!!)
|
||||||
|
Sample:
|
||||||
>>> a = { 'first' : { 'rows' : { 'pass' : 'dog', 'number' : '1' } } }
|
>>> a = { 'first' : { 'rows' : { 'pass' : 'dog', 'number' : '1' } } }
|
||||||
>>> b = { 'first' : { 'rows' : { 'fail' : 'cat', 'number' : '5' } } }
|
>>> b = { 'first' : { 'rows' : { 'fail' : 'cat', 'number' : '5' } } }
|
||||||
>>> merge(b, a) == { 'first' : { 'rows' : { 'pass' : 'dog', 'fail' : 'cat', 'number' : '5' } } }
|
>>> merge(b, a) == { 'first' : { 'rows' : { 'pass' : 'dog', 'fail' : 'cat', 'number' : '5' } } }
|
||||||
|
@ -132,26 +133,3 @@ def deep_merge_dicts(source, destination):
|
||||||
destination[key] = value
|
destination[key] = value
|
||||||
|
|
||||||
return destination
|
return destination
|
||||||
|
|
||||||
|
|
||||||
def timeframe_to_seconds(ticker_interval: str) -> int:
|
|
||||||
"""
|
|
||||||
Translates the timeframe interval value written in the human readable
|
|
||||||
form ('1m', '5m', '1h', '1d', '1w', etc.) to the number
|
|
||||||
of seconds for one timeframe interval.
|
|
||||||
"""
|
|
||||||
return Exchange.parse_timeframe(ticker_interval)
|
|
||||||
|
|
||||||
|
|
||||||
def timeframe_to_minutes(ticker_interval: str) -> int:
|
|
||||||
"""
|
|
||||||
Same as above, but returns minutes.
|
|
||||||
"""
|
|
||||||
return Exchange.parse_timeframe(ticker_interval) // 60
|
|
||||||
|
|
||||||
|
|
||||||
def timeframe_to_msecs(ticker_interval: str) -> int:
|
|
||||||
"""
|
|
||||||
Same as above, but returns milliseconds.
|
|
||||||
"""
|
|
||||||
return Exchange.parse_timeframe(ticker_interval) * 1000
|
|
||||||
|
|
|
@ -1,49 +1,111 @@
|
||||||
# pragma pylint: disable=missing-docstring
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from argparse import Namespace
|
||||||
from typing import Dict, Tuple
|
from typing import Any, Dict
|
||||||
import operator
|
|
||||||
|
|
||||||
import arrow
|
from filelock import FileLock, Timeout
|
||||||
from pandas import DataFrame
|
|
||||||
|
from freqtrade import DependencyException, constants
|
||||||
|
from freqtrade.state import RunMode
|
||||||
|
from freqtrade.utils import setup_utils_configuration
|
||||||
|
|
||||||
from freqtrade.optimize.default_hyperopt import DefaultHyperOpts # noqa: F401
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
|
def setup_configuration(args: Namespace, method: RunMode) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Get the maximum timeframe for the given backtest data
|
Prepare the configuration for the Hyperopt module
|
||||||
:param data: dictionary with preprocessed backtesting data
|
:param args: Cli args from Arguments()
|
||||||
:return: tuple containing min_date, max_date
|
:return: Configuration
|
||||||
"""
|
"""
|
||||||
timeframe = [
|
config = setup_utils_configuration(args, method)
|
||||||
(arrow.get(frame['date'].min()), arrow.get(frame['date'].max()))
|
|
||||||
for frame in data.values()
|
if method == RunMode.BACKTEST:
|
||||||
]
|
if config['stake_amount'] == constants.UNLIMITED_STAKE_AMOUNT:
|
||||||
return min(timeframe, key=operator.itemgetter(0))[0], \
|
raise DependencyException('stake amount could not be "%s" for backtesting' %
|
||||||
max(timeframe, key=operator.itemgetter(1))[1]
|
constants.UNLIMITED_STAKE_AMOUNT)
|
||||||
|
|
||||||
|
if method == RunMode.HYPEROPT:
|
||||||
|
# Special cases for Hyperopt
|
||||||
|
if config.get('strategy') and config.get('strategy') != 'DefaultStrategy':
|
||||||
|
logger.error("Please don't use --strategy for hyperopt.")
|
||||||
|
logger.error(
|
||||||
|
"Read the documentation at "
|
||||||
|
"https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md "
|
||||||
|
"to understand how to configure hyperopt.")
|
||||||
|
raise DependencyException("--strategy configured but not supported for hyperopt")
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
def validate_backtest_data(data: Dict[str, DataFrame], min_date: datetime,
|
def start_backtesting(args: Namespace) -> None:
|
||||||
max_date: datetime, ticker_interval_mins: int) -> bool:
|
|
||||||
"""
|
"""
|
||||||
Validates preprocessed backtesting data for missing values and shows warnings about it that.
|
Start Backtesting script
|
||||||
|
:param args: Cli args from Arguments()
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
# Import here to avoid loading backtesting module when it's not used
|
||||||
|
from freqtrade.optimize.backtesting import Backtesting
|
||||||
|
|
||||||
:param data: dictionary with preprocessed backtesting data
|
# Initialize configuration
|
||||||
:param min_date: start-date of the data
|
config = setup_configuration(args, RunMode.BACKTEST)
|
||||||
:param max_date: end-date of the data
|
|
||||||
:param ticker_interval_mins: ticker interval in minutes
|
logger.info('Starting freqtrade in Backtesting mode')
|
||||||
|
|
||||||
|
# Initialize backtesting object
|
||||||
|
backtesting = Backtesting(config)
|
||||||
|
backtesting.start()
|
||||||
|
|
||||||
|
|
||||||
|
def start_hyperopt(args: Namespace) -> None:
|
||||||
"""
|
"""
|
||||||
# total difference in minutes / interval-minutes
|
Start hyperopt script
|
||||||
expected_frames = int((max_date - min_date).total_seconds() // 60 // ticker_interval_mins)
|
:param args: Cli args from Arguments()
|
||||||
found_missing = False
|
:return: None
|
||||||
for pair, df in data.items():
|
"""
|
||||||
dflen = len(df)
|
# Import here to avoid loading hyperopt module when it's not used
|
||||||
if dflen < expected_frames:
|
from freqtrade.optimize.hyperopt import Hyperopt, HYPEROPT_LOCKFILE
|
||||||
found_missing = True
|
|
||||||
logger.warning("%s has missing frames: expected %s, got %s, that's %s missing values",
|
# Initialize configuration
|
||||||
pair, expected_frames, dflen, expected_frames - dflen)
|
config = setup_configuration(args, RunMode.HYPEROPT)
|
||||||
return found_missing
|
|
||||||
|
logger.info('Starting freqtrade in Hyperopt mode')
|
||||||
|
|
||||||
|
lock = FileLock(HYPEROPT_LOCKFILE)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with lock.acquire(timeout=1):
|
||||||
|
|
||||||
|
# Remove noisy log messages
|
||||||
|
logging.getLogger('hyperopt.tpe').setLevel(logging.WARNING)
|
||||||
|
logging.getLogger('filelock').setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
# Initialize backtesting object
|
||||||
|
hyperopt = Hyperopt(config)
|
||||||
|
hyperopt.start()
|
||||||
|
|
||||||
|
except Timeout:
|
||||||
|
logger.info("Another running instance of freqtrade Hyperopt detected.")
|
||||||
|
logger.info("Simultaneous execution of multiple Hyperopt commands is not supported. "
|
||||||
|
"Hyperopt module is resource hungry. Please run your Hyperopts sequentially "
|
||||||
|
"or on separate machines.")
|
||||||
|
logger.info("Quitting now.")
|
||||||
|
# TODO: return False here in order to help freqtrade to exit
|
||||||
|
# with non-zero exit code...
|
||||||
|
# Same in Edge and Backtesting start() functions.
|
||||||
|
|
||||||
|
|
||||||
|
def start_edge(args: Namespace) -> None:
|
||||||
|
"""
|
||||||
|
Start Edge script
|
||||||
|
:param args: Cli args from Arguments()
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
from freqtrade.optimize.edge_cli import EdgeCli
|
||||||
|
# Initialize configuration
|
||||||
|
config = setup_configuration(args, RunMode.EDGE)
|
||||||
|
logger.info('Starting freqtrade in Edge mode')
|
||||||
|
|
||||||
|
# Initialize Edge object
|
||||||
|
edge_cli = EdgeCli(config)
|
||||||
|
edge_cli.start()
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
This module contains the backtesting logic
|
This module contains the backtesting logic
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from argparse import Namespace
|
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
@ -13,17 +12,15 @@ from typing import Any, Dict, List, NamedTuple, Optional
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
|
|
||||||
from freqtrade import optimize
|
|
||||||
from freqtrade import DependencyException, constants
|
|
||||||
from freqtrade.arguments import Arguments
|
from freqtrade.arguments import Arguments
|
||||||
from freqtrade.configuration import Configuration
|
|
||||||
from freqtrade.data import history
|
from freqtrade.data import history
|
||||||
from freqtrade.data.dataprovider import DataProvider
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
from freqtrade.misc import file_dump_json, timeframe_to_minutes
|
from freqtrade.exchange import timeframe_to_minutes
|
||||||
|
from freqtrade.misc import file_dump_json
|
||||||
from freqtrade.persistence import Trade
|
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 SellType, IStrategy
|
from freqtrade.strategy.interface import IStrategy, SellType
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -66,8 +63,7 @@ class Backtesting(object):
|
||||||
self.config['dry_run'] = True
|
self.config['dry_run'] = True
|
||||||
self.strategylist: List[IStrategy] = []
|
self.strategylist: List[IStrategy] = []
|
||||||
|
|
||||||
exchange_name = self.config.get('exchange', {}).get('name', 'bittrex').title()
|
self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange
|
||||||
self.exchange = ExchangeResolver(exchange_name, self.config).exchange
|
|
||||||
self.fee = self.exchange.get_fee()
|
self.fee = self.exchange.get_fee()
|
||||||
|
|
||||||
if self.config.get('runmode') != RunMode.HYPEROPT:
|
if self.config.get('runmode') != RunMode.HYPEROPT:
|
||||||
|
@ -75,18 +71,16 @@ class Backtesting(object):
|
||||||
IStrategy.dp = self.dataprovider
|
IStrategy.dp = self.dataprovider
|
||||||
|
|
||||||
if self.config.get('strategy_list', None):
|
if self.config.get('strategy_list', None):
|
||||||
# Force one interval
|
|
||||||
self.ticker_interval = str(self.config.get('ticker_interval'))
|
|
||||||
self.ticker_interval_mins = timeframe_to_minutes(self.ticker_interval)
|
|
||||||
for strat in list(self.config['strategy_list']):
|
for strat in list(self.config['strategy_list']):
|
||||||
stratconf = deepcopy(self.config)
|
stratconf = deepcopy(self.config)
|
||||||
stratconf['strategy'] = strat
|
stratconf['strategy'] = strat
|
||||||
self.strategylist.append(StrategyResolver(stratconf).strategy)
|
self.strategylist.append(StrategyResolver(stratconf).strategy)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# only one strategy
|
# No strategy list specified, only one strategy
|
||||||
self.strategylist.append(StrategyResolver(self.config).strategy)
|
self.strategylist.append(StrategyResolver(self.config).strategy)
|
||||||
# Load one strategy
|
|
||||||
|
# Load one (first) strategy
|
||||||
self._set_strategy(self.strategylist[0])
|
self._set_strategy(self.strategylist[0])
|
||||||
|
|
||||||
def _set_strategy(self, strategy):
|
def _set_strategy(self, strategy):
|
||||||
|
@ -97,7 +91,6 @@ class Backtesting(object):
|
||||||
|
|
||||||
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)
|
||||||
self.tickerdata_to_dataframe = strategy.tickerdata_to_dataframe
|
|
||||||
self.advise_buy = strategy.advise_buy
|
self.advise_buy = strategy.advise_buy
|
||||||
self.advise_sell = strategy.advise_sell
|
self.advise_sell = strategy.advise_sell
|
||||||
# Set stoploss_on_exchange to false for backtesting,
|
# Set stoploss_on_exchange to false for backtesting,
|
||||||
|
@ -238,10 +231,9 @@ class Backtesting(object):
|
||||||
|
|
||||||
def _get_sell_trade_entry(
|
def _get_sell_trade_entry(
|
||||||
self, pair: str, buy_row: DataFrame,
|
self, pair: str, buy_row: DataFrame,
|
||||||
partial_ticker: List, trade_count_lock: Dict, args: Dict) -> Optional[BacktestResult]:
|
partial_ticker: List, trade_count_lock: Dict,
|
||||||
|
stake_amount: float, max_open_trades: int) -> Optional[BacktestResult]:
|
||||||
|
|
||||||
stake_amount = args['stake_amount']
|
|
||||||
max_open_trades = args.get('max_open_trades', 0)
|
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
open_rate=buy_row.open,
|
open_rate=buy_row.open,
|
||||||
open_date=buy_row.date,
|
open_date=buy_row.date,
|
||||||
|
@ -257,8 +249,7 @@ class Backtesting(object):
|
||||||
# Increase trade_count_lock for every iteration
|
# Increase trade_count_lock for every iteration
|
||||||
trade_count_lock[sell_row.date] = trade_count_lock.get(sell_row.date, 0) + 1
|
trade_count_lock[sell_row.date] = trade_count_lock.get(sell_row.date, 0) + 1
|
||||||
|
|
||||||
buy_signal = sell_row.buy
|
sell = self.strategy.should_sell(trade, sell_row.open, sell_row.date, sell_row.buy,
|
||||||
sell = self.strategy.should_sell(trade, sell_row.open, sell_row.date, buy_signal,
|
|
||||||
sell_row.sell, low=sell_row.low, high=sell_row.high)
|
sell_row.sell, low=sell_row.low, high=sell_row.high)
|
||||||
if sell.sell_flag:
|
if sell.sell_flag:
|
||||||
|
|
||||||
|
@ -331,6 +322,7 @@ class Backtesting(object):
|
||||||
:return: DataFrame
|
:return: DataFrame
|
||||||
"""
|
"""
|
||||||
processed = args['processed']
|
processed = args['processed']
|
||||||
|
stake_amount = args['stake_amount']
|
||||||
max_open_trades = args.get('max_open_trades', 0)
|
max_open_trades = args.get('max_open_trades', 0)
|
||||||
position_stacking = args.get('position_stacking', False)
|
position_stacking = args.get('position_stacking', False)
|
||||||
start_date = args['start_date']
|
start_date = args['start_date']
|
||||||
|
@ -357,7 +349,7 @@ class Backtesting(object):
|
||||||
row = ticker[pair][indexes[pair]]
|
row = ticker[pair][indexes[pair]]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
# missing Data for one pair at the end.
|
# missing Data for one pair at the end.
|
||||||
# Warnings for this are shown by `validate_backtest_data`
|
# Warnings for this are shown during data loading
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Waits until the time-counter reaches the start of the data for this pair.
|
# Waits until the time-counter reaches the start of the data for this pair.
|
||||||
|
@ -381,7 +373,8 @@ class Backtesting(object):
|
||||||
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]:],
|
trade_entry = self._get_sell_trade_entry(pair, row, ticker[pair][indexes[pair]:],
|
||||||
trade_count_lock, args)
|
trade_count_lock, stake_amount,
|
||||||
|
max_open_trades)
|
||||||
|
|
||||||
if trade_entry:
|
if trade_entry:
|
||||||
lock_pair_until[pair] = trade_entry.close_time
|
lock_pair_until[pair] = trade_entry.close_time
|
||||||
|
@ -404,24 +397,17 @@ 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'])
|
||||||
|
|
||||||
if self.config.get('live'):
|
timerange = Arguments.parse_timerange(None if self.config.get(
|
||||||
logger.info('Downloading data for all pairs in whitelist ...')
|
'timerange') is None else str(self.config.get('timerange')))
|
||||||
self.exchange.refresh_latest_ohlcv([(pair, self.ticker_interval) for pair in pairs])
|
data = history.load_data(
|
||||||
data = {key[0]: value for key, value in self.exchange._klines.items()}
|
datadir=Path(self.config['datadir']) if self.config.get('datadir') else None,
|
||||||
|
pairs=pairs,
|
||||||
else:
|
ticker_interval=self.ticker_interval,
|
||||||
logger.info('Using local backtesting data (using whitelist in given config) ...')
|
refresh_pairs=self.config.get('refresh_pairs', False),
|
||||||
|
exchange=self.exchange,
|
||||||
timerange = Arguments.parse_timerange(None if self.config.get(
|
timerange=timerange,
|
||||||
'timerange') is None else str(self.config.get('timerange')))
|
live=self.config.get('live', False)
|
||||||
data = history.load_data(
|
)
|
||||||
datadir=Path(self.config['datadir']) if self.config.get('datadir') else None,
|
|
||||||
pairs=pairs,
|
|
||||||
ticker_interval=self.ticker_interval,
|
|
||||||
refresh_pairs=self.config.get('refresh_pairs', False),
|
|
||||||
exchange=self.exchange,
|
|
||||||
timerange=timerange
|
|
||||||
)
|
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
logger.critical("No data found. Terminating.")
|
logger.critical("No data found. Terminating.")
|
||||||
|
@ -434,20 +420,19 @@ class Backtesting(object):
|
||||||
max_open_trades = 0
|
max_open_trades = 0
|
||||||
all_results = {}
|
all_results = {}
|
||||||
|
|
||||||
|
min_date, max_date = history.get_timeframe(data)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
'Backtesting with data from %s up to %s (%s days)..',
|
||||||
|
min_date.isoformat(),
|
||||||
|
max_date.isoformat(),
|
||||||
|
(max_date - min_date).days
|
||||||
|
)
|
||||||
|
|
||||||
for strat in self.strategylist:
|
for strat in self.strategylist:
|
||||||
logger.info("Running backtesting for Strategy %s", strat.get_strategy_name())
|
logger.info("Running backtesting for Strategy %s", strat.get_strategy_name())
|
||||||
self._set_strategy(strat)
|
self._set_strategy(strat)
|
||||||
|
|
||||||
min_date, max_date = optimize.get_timeframe(data)
|
|
||||||
# Validate dataframe for missing values (mainly at start and end, as fillup is called)
|
|
||||||
optimize.validate_backtest_data(data, min_date, max_date,
|
|
||||||
timeframe_to_minutes(self.ticker_interval))
|
|
||||||
logger.info(
|
|
||||||
'Measuring data from %s up to %s (%s days)..',
|
|
||||||
min_date.isoformat(),
|
|
||||||
max_date.isoformat(),
|
|
||||||
(max_date - min_date).days
|
|
||||||
)
|
|
||||||
# need to reprocess data every time to populate signals
|
# need to reprocess data every time to populate signals
|
||||||
preprocessed = self.strategy.tickerdata_to_dataframe(data)
|
preprocessed = self.strategy.tickerdata_to_dataframe(data)
|
||||||
|
|
||||||
|
@ -484,38 +469,3 @@ class Backtesting(object):
|
||||||
print(' Strategy Summary '.center(133, '='))
|
print(' Strategy Summary '.center(133, '='))
|
||||||
print(self._generate_text_table_strategy(all_results))
|
print(self._generate_text_table_strategy(all_results))
|
||||||
print('\nFor more details, please look at the detail tables above')
|
print('\nFor more details, please look at the detail tables above')
|
||||||
|
|
||||||
|
|
||||||
def setup_configuration(args: Namespace) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Prepare the configuration for the backtesting
|
|
||||||
:param args: Cli args from Arguments()
|
|
||||||
:return: Configuration
|
|
||||||
"""
|
|
||||||
configuration = Configuration(args, RunMode.BACKTEST)
|
|
||||||
config = configuration.get_config()
|
|
||||||
|
|
||||||
# Ensure we do not use Exchange credentials
|
|
||||||
config['exchange']['key'] = ''
|
|
||||||
config['exchange']['secret'] = ''
|
|
||||||
|
|
||||||
if config['stake_amount'] == constants.UNLIMITED_STAKE_AMOUNT:
|
|
||||||
raise DependencyException('stake amount could not be "%s" for backtesting' %
|
|
||||||
constants.UNLIMITED_STAKE_AMOUNT)
|
|
||||||
|
|
||||||
return config
|
|
||||||
|
|
||||||
|
|
||||||
def start(args: Namespace) -> None:
|
|
||||||
"""
|
|
||||||
Start Backtesting script
|
|
||||||
:param args: Cli args from Arguments()
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
# Initialize configuration
|
|
||||||
config = setup_configuration(args)
|
|
||||||
logger.info('Starting freqtrade in Backtesting mode')
|
|
||||||
|
|
||||||
# Initialize backtesting object
|
|
||||||
backtesting = Backtesting(config)
|
|
||||||
backtesting.start()
|
|
||||||
|
|
|
@ -70,9 +70,10 @@ class DefaultHyperOpts(IHyperOpt):
|
||||||
dataframe['close'], dataframe['sar']
|
dataframe['close'], dataframe['sar']
|
||||||
))
|
))
|
||||||
|
|
||||||
dataframe.loc[
|
if conditions:
|
||||||
reduce(lambda x, y: x & y, conditions),
|
dataframe.loc[
|
||||||
'buy'] = 1
|
reduce(lambda x, y: x & y, conditions),
|
||||||
|
'buy'] = 1
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
|
@ -129,9 +130,10 @@ class DefaultHyperOpts(IHyperOpt):
|
||||||
dataframe['sar'], dataframe['close']
|
dataframe['sar'], dataframe['close']
|
||||||
))
|
))
|
||||||
|
|
||||||
dataframe.loc[
|
if conditions:
|
||||||
reduce(lambda x, y: x & y, conditions),
|
dataframe.loc[
|
||||||
'sell'] = 1
|
reduce(lambda x, y: x & y, conditions),
|
||||||
|
'sell'] = 1
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
|
|
|
@ -4,16 +4,14 @@
|
||||||
This module contains the edge backtesting interface
|
This module contains the edge backtesting interface
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from argparse import Namespace
|
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
|
from freqtrade import constants
|
||||||
from freqtrade.edge import Edge
|
from freqtrade.edge import Edge
|
||||||
|
|
||||||
from freqtrade.arguments import Arguments
|
from freqtrade.arguments import Arguments
|
||||||
from freqtrade.configuration import Configuration
|
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.resolvers import StrategyResolver
|
from freqtrade.resolvers import StrategyResolver
|
||||||
from freqtrade.state import RunMode
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -35,6 +33,7 @@ class EdgeCli(object):
|
||||||
self.config['exchange']['secret'] = ''
|
self.config['exchange']['secret'] = ''
|
||||||
self.config['exchange']['password'] = ''
|
self.config['exchange']['password'] = ''
|
||||||
self.config['exchange']['uid'] = ''
|
self.config['exchange']['uid'] = ''
|
||||||
|
self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
|
||||||
self.config['dry_run'] = True
|
self.config['dry_run'] = True
|
||||||
self.exchange = Exchange(self.config)
|
self.exchange = Exchange(self.config)
|
||||||
self.strategy = StrategyResolver(self.config).strategy
|
self.strategy = StrategyResolver(self.config).strategy
|
||||||
|
@ -73,37 +72,7 @@ class EdgeCli(object):
|
||||||
floatfmt=floatfmt, tablefmt="pipe")
|
floatfmt=floatfmt, tablefmt="pipe")
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
self.edge.calculate()
|
result = self.edge.calculate()
|
||||||
print('') # blank like for readability
|
if result:
|
||||||
print(self._generate_edge_table(self.edge._cached_pairs))
|
print('') # blank line for readability
|
||||||
|
print(self._generate_edge_table(self.edge._cached_pairs))
|
||||||
|
|
||||||
def setup_configuration(args: Namespace) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Prepare the configuration for edge backtesting
|
|
||||||
:param args: Cli args from Arguments()
|
|
||||||
:return: Configuration
|
|
||||||
"""
|
|
||||||
configuration = Configuration(args, RunMode.EDGECLI)
|
|
||||||
config = configuration.get_config()
|
|
||||||
|
|
||||||
# Ensure we do not use Exchange credentials
|
|
||||||
config['exchange']['key'] = ''
|
|
||||||
config['exchange']['secret'] = ''
|
|
||||||
|
|
||||||
return config
|
|
||||||
|
|
||||||
|
|
||||||
def start(args: Namespace) -> None:
|
|
||||||
"""
|
|
||||||
Start Edge script
|
|
||||||
:param args: Cli args from Arguments()
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
# Initialize configuration
|
|
||||||
config = setup_configuration(args)
|
|
||||||
logger.info('Starting freqtrade in Edge mode')
|
|
||||||
|
|
||||||
# Initialize Edge object
|
|
||||||
edge_cli = EdgeCli(config)
|
|
||||||
edge_cli.start()
|
|
||||||
|
|
|
@ -5,33 +5,33 @@ This module contains the hyperopt logic
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import multiprocessing
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from argparse import Namespace
|
|
||||||
from math import exp
|
from math import exp
|
||||||
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
|
||||||
|
|
||||||
from joblib import Parallel, delayed, dump, load, wrap_non_picklable_objects
|
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.arguments import Arguments
|
from freqtrade.arguments import Arguments
|
||||||
from freqtrade.configuration import Configuration
|
from freqtrade.data.history import load_data, get_timeframe
|
||||||
from freqtrade.data.history import load_data
|
|
||||||
from freqtrade.optimize import get_timeframe
|
|
||||||
from freqtrade.optimize.backtesting import Backtesting
|
from freqtrade.optimize.backtesting import Backtesting
|
||||||
from freqtrade.state import RunMode
|
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver
|
||||||
from freqtrade.resolvers import HyperOptResolver
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
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')
|
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(Backtesting):
|
||||||
|
@ -44,7 +44,6 @@ class Hyperopt(Backtesting):
|
||||||
"""
|
"""
|
||||||
def __init__(self, config: Dict[str, Any]) -> None:
|
def __init__(self, config: Dict[str, Any]) -> None:
|
||||||
super().__init__(config)
|
super().__init__(config)
|
||||||
self.config = config
|
|
||||||
self.custom_hyperopt = HyperOptResolver(self.config).hyperopt
|
self.custom_hyperopt = HyperOptResolver(self.config).hyperopt
|
||||||
|
|
||||||
# set TARGET_TRADES to suit your number concurrent trades so its realistic
|
# set TARGET_TRADES to suit your number concurrent trades so its realistic
|
||||||
|
@ -57,13 +56,15 @@ class Hyperopt(Backtesting):
|
||||||
# if eval ends with higher value, we consider it a failed eval
|
# if eval ends with higher value, we consider it a failed eval
|
||||||
self.max_accepted_trade_duration = 300
|
self.max_accepted_trade_duration = 300
|
||||||
|
|
||||||
# this is expexted avg profit * expected trade count
|
# This is assumed to be expected avg profit * expected trade count.
|
||||||
# for example 3.5%, 1100 trades, self.expected_max_profit = 3.85
|
# For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades,
|
||||||
# check that the reported Σ% values do not exceed this!
|
# self.expected_max_profit = 3.85
|
||||||
|
# Check that the reported Σ% values do not exceed this!
|
||||||
|
# Note, this is ratio. 3.85 stated above means 385Σ%.
|
||||||
self.expected_max_profit = 3.0
|
self.expected_max_profit = 3.0
|
||||||
|
|
||||||
# Previous evaluations
|
# Previous evaluations
|
||||||
self.trials_file = os.path.join('user_data', 'hyperopt_results.pickle')
|
self.trials_file = TRIALSDATA_PICKLE
|
||||||
self.trials: List = []
|
self.trials: List = []
|
||||||
|
|
||||||
def get_args(self, params):
|
def get_args(self, params):
|
||||||
|
@ -115,14 +116,20 @@ class Hyperopt(Backtesting):
|
||||||
"""
|
"""
|
||||||
Log results if it is better than any previous evaluation
|
Log results if it is better than any previous evaluation
|
||||||
"""
|
"""
|
||||||
if results['loss'] < self.current_best_loss:
|
print_all = self.config.get('print_all', False)
|
||||||
current = results['current_tries']
|
if print_all or results['loss'] < self.current_best_loss:
|
||||||
|
# Output human-friendly index here (starting from 1)
|
||||||
|
current = results['current_tries'] + 1
|
||||||
total = results['total_tries']
|
total = results['total_tries']
|
||||||
res = results['result']
|
res = results['result']
|
||||||
loss = results['loss']
|
loss = results['loss']
|
||||||
self.current_best_loss = results['loss']
|
self.current_best_loss = results['loss']
|
||||||
log_msg = f'\n{current:5d}/{total}: {res}. Loss {loss:.5f}'
|
log_msg = f'{current:5d}/{total}: {res} Objective: {loss:.5f}'
|
||||||
print(log_msg)
|
log_msg = f'*{log_msg}' if results['initial_point'] else f' {log_msg}'
|
||||||
|
if print_all:
|
||||||
|
print(log_msg)
|
||||||
|
else:
|
||||||
|
print('\n' + log_msg)
|
||||||
else:
|
else:
|
||||||
print('.', end='')
|
print('.', end='')
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
@ -199,7 +206,11 @@ class Hyperopt(Backtesting):
|
||||||
trade_count = len(results.index)
|
trade_count = len(results.index)
|
||||||
trade_duration = results.trade_duration.mean()
|
trade_duration = results.trade_duration.mean()
|
||||||
|
|
||||||
if trade_count == 0:
|
# If this evaluation contains too short amount of trades to be
|
||||||
|
# interesting -- consider it as 'bad' (assigned max. loss value)
|
||||||
|
# in order to cast this hyperspace point away from optimization
|
||||||
|
# path. We do not want to optimize 'hodl' strategies.
|
||||||
|
if trade_count < self.config['hyperopt_min_trades']:
|
||||||
return {
|
return {
|
||||||
'loss': MAX_LOSS,
|
'loss': MAX_LOSS,
|
||||||
'params': params,
|
'params': params,
|
||||||
|
@ -222,20 +233,21 @@ class Hyperopt(Backtesting):
|
||||||
avg_profit = results.profit_percent.mean() * 100.0
|
avg_profit = results.profit_percent.mean() * 100.0
|
||||||
total_profit = results.profit_abs.sum()
|
total_profit = results.profit_abs.sum()
|
||||||
stake_cur = self.config['stake_currency']
|
stake_cur = self.config['stake_currency']
|
||||||
profit = results.profit_percent.sum()
|
profit = results.profit_percent.sum() * 100.0
|
||||||
duration = results.trade_duration.mean()
|
duration = results.trade_duration.mean()
|
||||||
|
|
||||||
return (f'{trades:6d} trades. Avg profit {avg_profit: 5.2f}%. '
|
return (f'{trades:6d} trades. Avg profit {avg_profit: 5.2f}%. '
|
||||||
f'Total profit {total_profit: 11.8f} {stake_cur} '
|
f'Total profit {total_profit: 11.8f} {stake_cur} '
|
||||||
f'({profit:.4f}Σ%). Avg duration {duration:5.1f} mins.')
|
f'({profit: 7.2f}Σ%). Avg duration {duration:5.1f} mins.')
|
||||||
|
|
||||||
def get_optimizer(self, cpu_count) -> Optimizer:
|
def get_optimizer(self, cpu_count) -> Optimizer:
|
||||||
return Optimizer(
|
return Optimizer(
|
||||||
self.hyperopt_space(),
|
self.hyperopt_space(),
|
||||||
base_estimator="ET",
|
base_estimator="ET",
|
||||||
acq_optimizer="auto",
|
acq_optimizer="auto",
|
||||||
n_initial_points=30,
|
n_initial_points=INITIAL_POINTS,
|
||||||
acq_optimizer_kwargs={'n_jobs': cpu_count}
|
acq_optimizer_kwargs={'n_jobs': cpu_count},
|
||||||
|
random_state=self.config.get('hyperopt_random_state', None)
|
||||||
)
|
)
|
||||||
|
|
||||||
def run_optimizer_parallel(self, parallel, asked) -> List:
|
def run_optimizer_parallel(self, parallel, asked) -> List:
|
||||||
|
@ -258,69 +270,68 @@ class Hyperopt(Backtesting):
|
||||||
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.ticker_interval,
|
||||||
|
refresh_pairs=self.config.get('refresh_pairs', False),
|
||||||
|
exchange=self.exchange,
|
||||||
timerange=timerange
|
timerange=timerange
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
logger.critical("No data found. Terminating.")
|
||||||
|
return
|
||||||
|
|
||||||
|
min_date, max_date = get_timeframe(data)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
'Hyperopting with data from %s up to %s (%s days)..',
|
||||||
|
min_date.isoformat(),
|
||||||
|
max_date.isoformat(),
|
||||||
|
(max_date - min_date).days
|
||||||
|
)
|
||||||
|
|
||||||
if self.has_space('buy') or self.has_space('sell'):
|
if self.has_space('buy') or self.has_space('sell'):
|
||||||
self.strategy.advise_indicators = \
|
self.strategy.advise_indicators = \
|
||||||
self.custom_hyperopt.populate_indicators # type: ignore
|
self.custom_hyperopt.populate_indicators # type: ignore
|
||||||
dump(self.strategy.tickerdata_to_dataframe(data), TICKERDATA_PICKLE)
|
|
||||||
|
preprocessed = self.strategy.tickerdata_to_dataframe(data)
|
||||||
|
|
||||||
|
dump(preprocessed, TICKERDATA_PICKLE)
|
||||||
|
|
||||||
|
# We don't need exchange instance anymore while running hyperopt
|
||||||
self.exchange = None # type: ignore
|
self.exchange = None # type: ignore
|
||||||
|
|
||||||
self.load_previous_results()
|
self.load_previous_results()
|
||||||
|
|
||||||
cpus = multiprocessing.cpu_count()
|
cpus = cpu_count()
|
||||||
logger.info(f'Found {cpus} CPU cores. Let\'s make them scream!')
|
logger.info(f'Found {cpus} CPU cores. Let\'s make them scream!')
|
||||||
|
config_jobs = self.config.get('hyperopt_jobs', -1)
|
||||||
|
logger.info(f'Number of parallel jobs set as: {config_jobs}')
|
||||||
|
|
||||||
opt = self.get_optimizer(cpus)
|
opt = self.get_optimizer(config_jobs)
|
||||||
EVALS = max(self.total_tries // cpus, 1)
|
|
||||||
try:
|
try:
|
||||||
with Parallel(n_jobs=cpus) as parallel:
|
with Parallel(n_jobs=config_jobs) as parallel:
|
||||||
|
jobs = parallel._effective_n_jobs()
|
||||||
|
logger.info(f'Effective number of parallel workers used: {jobs}')
|
||||||
|
EVALS = max(self.total_tries // jobs, 1)
|
||||||
for i in range(EVALS):
|
for i in range(EVALS):
|
||||||
asked = opt.ask(n_points=cpus)
|
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, [i['loss'] for i in f_val])
|
||||||
|
|
||||||
self.trials += f_val
|
self.trials += f_val
|
||||||
for j in range(cpus):
|
for j in range(jobs):
|
||||||
|
current = i * jobs + j
|
||||||
self.log_results({
|
self.log_results({
|
||||||
'loss': f_val[j]['loss'],
|
'loss': f_val[j]['loss'],
|
||||||
'current_tries': i * cpus + j,
|
'current_tries': current,
|
||||||
|
'initial_point': current < INITIAL_POINTS,
|
||||||
'total_tries': self.total_tries,
|
'total_tries': self.total_tries,
|
||||||
'result': f_val[j]['result'],
|
'result': f_val[j]['result'],
|
||||||
})
|
})
|
||||||
|
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..')
|
||||||
|
|
||||||
self.save_trials()
|
self.save_trials()
|
||||||
self.log_trials_result()
|
self.log_trials_result()
|
||||||
|
|
||||||
|
|
||||||
def start(args: Namespace) -> None:
|
|
||||||
"""
|
|
||||||
Start Backtesting script
|
|
||||||
:param args: Cli args from Arguments()
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Remove noisy log messages
|
|
||||||
logging.getLogger('hyperopt.tpe').setLevel(logging.WARNING)
|
|
||||||
|
|
||||||
# Initialize configuration
|
|
||||||
# Monkey patch the configuration with hyperopt_conf.py
|
|
||||||
configuration = Configuration(args, RunMode.HYPEROPT)
|
|
||||||
logger.info('Starting freqtrade in Hyperopt mode')
|
|
||||||
config = configuration.load_config()
|
|
||||||
|
|
||||||
config['exchange']['key'] = ''
|
|
||||||
config['exchange']['secret'] = ''
|
|
||||||
|
|
||||||
if config.get('strategy') and config.get('strategy') != 'DefaultStrategy':
|
|
||||||
logger.error("Please don't use --strategy for hyperopt.")
|
|
||||||
logger.error(
|
|
||||||
"Read the documentation at "
|
|
||||||
"https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md "
|
|
||||||
"to understand how to configure hyperopt.")
|
|
||||||
raise ValueError("--strategy configured but not supported for hyperopt")
|
|
||||||
# Initialize backtesting object
|
|
||||||
hyperopt = Hyperopt(config)
|
|
||||||
hyperopt.start()
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ class IHyperOpt(ABC):
|
||||||
stoploss -> float: optimal stoploss designed for the strategy
|
stoploss -> float: optimal stoploss designed for the strategy
|
||||||
ticker_interval -> int: value of the ticker interval to use for the strategy
|
ticker_interval -> int: value of the ticker interval to use for the strategy
|
||||||
"""
|
"""
|
||||||
|
ticker_interval: str
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
|
|
@ -25,15 +25,16 @@ _DECL_BASE: Any = declarative_base()
|
||||||
_SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls'
|
_SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls'
|
||||||
|
|
||||||
|
|
||||||
def init(config: Dict) -> None:
|
def init(db_url: str, clean_open_orders: bool = False) -> None:
|
||||||
"""
|
"""
|
||||||
Initializes this module with the given config,
|
Initializes this module with the given config,
|
||||||
registers all known command handlers
|
registers all known command handlers
|
||||||
and starts polling for message updates
|
and starts polling for message updates
|
||||||
:param config: config to use
|
:param db_url: Database to use
|
||||||
|
:param clean_open_orders: Remove open orders from the database.
|
||||||
|
Useful for dry-run or if all orders have been reset on the exchange.
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
db_url = config.get('db_url', None)
|
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
|
|
||||||
# Take care of thread ownership if in-memory db
|
# Take care of thread ownership if in-memory db
|
||||||
|
@ -57,7 +58,7 @@ def init(config: Dict) -> None:
|
||||||
check_migrate(engine)
|
check_migrate(engine)
|
||||||
|
|
||||||
# Clean dry_run DB if the db is not in-memory
|
# Clean dry_run DB if the db is not in-memory
|
||||||
if config.get('dry_run', False) and db_url != 'sqlite://':
|
if clean_open_orders and db_url != 'sqlite://':
|
||||||
clean_dry_run_db()
|
clean_dry_run_db()
|
||||||
|
|
||||||
|
|
||||||
|
@ -213,11 +214,31 @@ class Trade(_DECL_BASE):
|
||||||
return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, '
|
return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, '
|
||||||
f'open_rate={self.open_rate:.8f}, open_since={open_since})')
|
f'open_rate={self.open_rate:.8f}, open_since={open_since})')
|
||||||
|
|
||||||
|
def to_json(self) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
'trade_id': self.id,
|
||||||
|
'pair': self.pair,
|
||||||
|
'open_date_hum': arrow.get(self.open_date).humanize(),
|
||||||
|
'open_date': self.open_date.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
'close_date_hum': (arrow.get(self.close_date).humanize()
|
||||||
|
if self.close_date else None),
|
||||||
|
'close_date': (self.close_date.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
if self.close_date else None),
|
||||||
|
'open_rate': self.open_rate,
|
||||||
|
'close_rate': self.close_rate,
|
||||||
|
'amount': round(self.amount, 8),
|
||||||
|
'stake_amount': round(self.stake_amount, 8),
|
||||||
|
'stop_loss': self.stop_loss,
|
||||||
|
'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None,
|
||||||
|
'initial_stop_loss': self.initial_stop_loss,
|
||||||
|
'initial_stop_loss_pct': (self.initial_stop_loss_pct * 100
|
||||||
|
if self.initial_stop_loss_pct else None),
|
||||||
|
}
|
||||||
|
|
||||||
def adjust_min_max_rates(self, current_price: float):
|
def adjust_min_max_rates(self, current_price: float):
|
||||||
"""
|
"""
|
||||||
Adjust the max_rate and min_rate.
|
Adjust the max_rate and min_rate.
|
||||||
"""
|
"""
|
||||||
logger.debug("Adjusting min/max rates")
|
|
||||||
self.max_rate = max(current_price, self.max_rate or self.open_rate)
|
self.max_rate = max(current_price, self.max_rate or self.open_rate)
|
||||||
self.min_rate = min(current_price, self.min_rate or self.open_rate)
|
self.min_rate = min(current_price, self.min_rate or self.open_rate)
|
||||||
|
|
||||||
|
@ -401,3 +422,22 @@ class Trade(_DECL_BASE):
|
||||||
Query trades from persistence layer
|
Query trades from persistence layer
|
||||||
"""
|
"""
|
||||||
return Trade.query.filter(Trade.is_open.is_(True)).all()
|
return Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def stoploss_reinitialization(desired_stoploss):
|
||||||
|
"""
|
||||||
|
Adjust initial Stoploss to desired stoploss for all open trades.
|
||||||
|
"""
|
||||||
|
for trade in Trade.get_open_trades():
|
||||||
|
logger.info("Found open trade: %s", trade)
|
||||||
|
|
||||||
|
# skip case if trailing-stop changed the stoploss already.
|
||||||
|
if (trade.stop_loss == trade.initial_stop_loss
|
||||||
|
and trade.initial_stop_loss_pct != desired_stoploss):
|
||||||
|
# Stoploss value got changed
|
||||||
|
|
||||||
|
logger.info(f"Stoploss for {trade} needs adjustment.")
|
||||||
|
# Force reset of stoploss
|
||||||
|
trade.stop_loss = None
|
||||||
|
trade.adjust_stop_loss(trade.open_rate, desired_stoploss)
|
||||||
|
logger.info(f"new stoploss: {trade.stop_loss}, ")
|
||||||
|
|
0
freqtrade/plot/__init__.py
Normal file
0
freqtrade/plot/__init__.py
Normal file
223
freqtrade/plot/plotting.py
Normal file
223
freqtrade/plot/plotting.py
Normal file
|
@ -0,0 +1,223 @@
|
||||||
|
import logging
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from plotly import tools
|
||||||
|
from plotly.offline import plot
|
||||||
|
import plotly.graph_objs as go
|
||||||
|
except ImportError:
|
||||||
|
logger.exception("Module plotly not found \n Please install using `pip install plotly`")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_row(fig, row, indicators: List[str], data: pd.DataFrame) -> tools.make_subplots:
|
||||||
|
"""
|
||||||
|
Generator all the indicator selected by the user for a specific row
|
||||||
|
:param fig: Plot figure to append to
|
||||||
|
:param row: row number for this plot
|
||||||
|
:param indicators: List of indicators present in the dataframe
|
||||||
|
:param data: candlestick DataFrame
|
||||||
|
"""
|
||||||
|
for indicator in indicators:
|
||||||
|
if indicator in data:
|
||||||
|
# TODO: Figure out why scattergl causes problems
|
||||||
|
scattergl = go.Scatter(
|
||||||
|
x=data['date'],
|
||||||
|
y=data[indicator].values,
|
||||||
|
mode='lines',
|
||||||
|
name=indicator
|
||||||
|
)
|
||||||
|
fig.append_trace(scattergl, row, 1)
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
'Indicator "%s" ignored. Reason: This indicator is not found '
|
||||||
|
'in your strategy.',
|
||||||
|
indicator
|
||||||
|
)
|
||||||
|
|
||||||
|
return fig
|
||||||
|
|
||||||
|
|
||||||
|
def plot_trades(fig, trades: pd.DataFrame):
|
||||||
|
"""
|
||||||
|
Plot trades to "fig"
|
||||||
|
"""
|
||||||
|
# Trades can be empty
|
||||||
|
if trades is not None and len(trades) > 0:
|
||||||
|
trade_buys = go.Scatter(
|
||||||
|
x=trades["open_time"],
|
||||||
|
y=trades["open_rate"],
|
||||||
|
mode='markers',
|
||||||
|
name='trade_buy',
|
||||||
|
marker=dict(
|
||||||
|
symbol='square-open',
|
||||||
|
size=11,
|
||||||
|
line=dict(width=2),
|
||||||
|
color='green'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Create description for sell summarizing the trade
|
||||||
|
desc = trades.apply(lambda row: f"{round(row['profitperc'], 3)}%, {row['sell_reason']}, "
|
||||||
|
f"{row['duration']}min",
|
||||||
|
axis=1)
|
||||||
|
trade_sells = go.Scatter(
|
||||||
|
x=trades["close_time"],
|
||||||
|
y=trades["close_rate"],
|
||||||
|
text=desc,
|
||||||
|
mode='markers',
|
||||||
|
name='trade_sell',
|
||||||
|
marker=dict(
|
||||||
|
symbol='square-open',
|
||||||
|
size=11,
|
||||||
|
line=dict(width=2),
|
||||||
|
color='red'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
fig.append_trace(trade_buys, 1, 1)
|
||||||
|
fig.append_trace(trade_sells, 1, 1)
|
||||||
|
else:
|
||||||
|
logger.warning("No trades found.")
|
||||||
|
return fig
|
||||||
|
|
||||||
|
|
||||||
|
def generate_graph(
|
||||||
|
pair: str,
|
||||||
|
data: pd.DataFrame,
|
||||||
|
trades: pd.DataFrame = None,
|
||||||
|
indicators1: List[str] = [],
|
||||||
|
indicators2: List[str] = [],
|
||||||
|
) -> go.Figure:
|
||||||
|
"""
|
||||||
|
Generate the graph from the data generated by Backtesting or from DB
|
||||||
|
Volume will always be ploted in row2, so Row 1 and 3 are to our disposal for custom indicators
|
||||||
|
:param pair: Pair to Display on the graph
|
||||||
|
:param data: OHLCV DataFrame containing indicators and buy/sell signals
|
||||||
|
:param trades: All trades created
|
||||||
|
:param indicators1: List containing Main plot indicators
|
||||||
|
:param indicators2: List containing Sub plot indicators
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Define the graph
|
||||||
|
fig = tools.make_subplots(
|
||||||
|
rows=3,
|
||||||
|
cols=1,
|
||||||
|
shared_xaxes=True,
|
||||||
|
row_width=[1, 1, 4],
|
||||||
|
vertical_spacing=0.0001,
|
||||||
|
)
|
||||||
|
fig['layout'].update(title=pair)
|
||||||
|
fig['layout']['yaxis1'].update(title='Price')
|
||||||
|
fig['layout']['yaxis2'].update(title='Volume')
|
||||||
|
fig['layout']['yaxis3'].update(title='Other')
|
||||||
|
fig['layout']['xaxis']['rangeslider'].update(visible=False)
|
||||||
|
|
||||||
|
# Common information
|
||||||
|
candles = go.Candlestick(
|
||||||
|
x=data.date,
|
||||||
|
open=data.open,
|
||||||
|
high=data.high,
|
||||||
|
low=data.low,
|
||||||
|
close=data.close,
|
||||||
|
name='Price'
|
||||||
|
)
|
||||||
|
fig.append_trace(candles, 1, 1)
|
||||||
|
|
||||||
|
if 'buy' in data.columns:
|
||||||
|
df_buy = data[data['buy'] == 1]
|
||||||
|
if len(df_buy) > 0:
|
||||||
|
buys = go.Scatter(
|
||||||
|
x=df_buy.date,
|
||||||
|
y=df_buy.close,
|
||||||
|
mode='markers',
|
||||||
|
name='buy',
|
||||||
|
marker=dict(
|
||||||
|
symbol='triangle-up-dot',
|
||||||
|
size=9,
|
||||||
|
line=dict(width=1),
|
||||||
|
color='green',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
fig.append_trace(buys, 1, 1)
|
||||||
|
else:
|
||||||
|
logger.warning("No buy-signals found.")
|
||||||
|
|
||||||
|
if 'sell' in data.columns:
|
||||||
|
df_sell = data[data['sell'] == 1]
|
||||||
|
if len(df_sell) > 0:
|
||||||
|
sells = go.Scatter(
|
||||||
|
x=df_sell.date,
|
||||||
|
y=df_sell.close,
|
||||||
|
mode='markers',
|
||||||
|
name='sell',
|
||||||
|
marker=dict(
|
||||||
|
symbol='triangle-down-dot',
|
||||||
|
size=9,
|
||||||
|
line=dict(width=1),
|
||||||
|
color='red',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
fig.append_trace(sells, 1, 1)
|
||||||
|
else:
|
||||||
|
logger.warning("No sell-signals found.")
|
||||||
|
|
||||||
|
if 'bb_lowerband' in data and 'bb_upperband' in data:
|
||||||
|
bb_lower = go.Scattergl(
|
||||||
|
x=data.date,
|
||||||
|
y=data.bb_lowerband,
|
||||||
|
name='BB lower',
|
||||||
|
line={'color': 'rgba(255,255,255,0)'},
|
||||||
|
)
|
||||||
|
bb_upper = go.Scattergl(
|
||||||
|
x=data.date,
|
||||||
|
y=data.bb_upperband,
|
||||||
|
name='BB upper',
|
||||||
|
fill="tonexty",
|
||||||
|
fillcolor="rgba(0,176,246,0.2)",
|
||||||
|
line={'color': 'rgba(255,255,255,0)'},
|
||||||
|
)
|
||||||
|
fig.append_trace(bb_lower, 1, 1)
|
||||||
|
fig.append_trace(bb_upper, 1, 1)
|
||||||
|
|
||||||
|
# Add indicators to main plot
|
||||||
|
fig = generate_row(fig=fig, row=1, indicators=indicators1, data=data)
|
||||||
|
|
||||||
|
fig = plot_trades(fig, trades)
|
||||||
|
|
||||||
|
# Volume goes to row 2
|
||||||
|
volume = go.Bar(
|
||||||
|
x=data['date'],
|
||||||
|
y=data['volume'],
|
||||||
|
name='Volume'
|
||||||
|
)
|
||||||
|
fig.append_trace(volume, 2, 1)
|
||||||
|
|
||||||
|
# Add indicators to seperate row
|
||||||
|
fig = generate_row(fig=fig, row=3, indicators=indicators2, data=data)
|
||||||
|
|
||||||
|
return fig
|
||||||
|
|
||||||
|
|
||||||
|
def generate_plot_file(fig, pair, ticker_interval) -> None:
|
||||||
|
"""
|
||||||
|
Generate a plot html file from pre populated fig plotly object
|
||||||
|
:param fig: Plotly Figure to plot
|
||||||
|
:param pair: Pair to plot (used as filename and Plot title)
|
||||||
|
:param ticker_interval: Used as part of the filename
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
logger.info('Generate plot file for %s', pair)
|
||||||
|
|
||||||
|
pair_name = pair.replace("/", "_")
|
||||||
|
file_name = 'freqtrade-plot-' + pair_name + '-' + ticker_interval + '.html'
|
||||||
|
|
||||||
|
Path("user_data/plots").mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
plot(fig, filename=str(Path('user_data/plots').joinpath(file_name)),
|
||||||
|
auto_open=False)
|
|
@ -1,5 +1,6 @@
|
||||||
from freqtrade.resolvers.iresolver import IResolver # noqa: F401
|
from freqtrade.resolvers.iresolver import IResolver # noqa: F401
|
||||||
from freqtrade.resolvers.exchange_resolver import ExchangeResolver # noqa: F401
|
from freqtrade.resolvers.exchange_resolver import ExchangeResolver # noqa: F401
|
||||||
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver # noqa: F401
|
# Don't import HyperoptResolver to avoid loading the whole Optimize tree
|
||||||
|
# from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver # noqa: F401
|
||||||
from freqtrade.resolvers.pairlist_resolver import PairListResolver # noqa: F401
|
from freqtrade.resolvers.pairlist_resolver import PairListResolver # noqa: F401
|
||||||
from freqtrade.resolvers.strategy_resolver import StrategyResolver # noqa: F401
|
from freqtrade.resolvers.strategy_resolver import StrategyResolver # noqa: F401
|
||||||
|
|
|
@ -22,6 +22,7 @@ class ExchangeResolver(IResolver):
|
||||||
Load the custom class from config parameter
|
Load the custom class from config parameter
|
||||||
:param config: configuration dictionary
|
:param config: configuration dictionary
|
||||||
"""
|
"""
|
||||||
|
exchange_name = exchange_name.title()
|
||||||
try:
|
try:
|
||||||
self.exchange = self._load_exchange(exchange_name, kwargs={'config': config})
|
self.exchange = self._load_exchange(exchange_name, kwargs={'config': config})
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|
|
@ -32,6 +32,9 @@ class HyperOptResolver(IResolver):
|
||||||
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, extra_dir=config.get('hyperopt_path'))
|
||||||
|
|
||||||
|
# Assign ticker_interval to be used in hyperopt
|
||||||
|
self.hyperopt.__class__.ticker_interval = str(config['ticker_interval'])
|
||||||
|
|
||||||
if not hasattr(self.hyperopt, 'populate_buy_trend'):
|
if not hasattr(self.hyperopt, 'populate_buy_trend'):
|
||||||
logger.warning("Custom Hyperopt does not provide populate_buy_trend. "
|
logger.warning("Custom Hyperopt does not provide populate_buy_trend. "
|
||||||
"Using populate_buy_trend from DefaultStrategy.")
|
"Using populate_buy_trend from DefaultStrategy.")
|
||||||
|
|
375
freqtrade/rpc/api_server.py
Normal file
375
freqtrade/rpc/api_server.py
Normal file
|
@ -0,0 +1,375 @@
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
from datetime import date, datetime
|
||||||
|
from ipaddress import IPv4Address
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from arrow import Arrow
|
||||||
|
from flask import Flask, jsonify, request
|
||||||
|
from flask.json import JSONEncoder
|
||||||
|
from werkzeug.serving import make_server
|
||||||
|
|
||||||
|
from freqtrade.__init__ import __version__
|
||||||
|
from freqtrade.rpc.rpc import RPC, RPCException
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
BASE_URI = "/api/v1"
|
||||||
|
|
||||||
|
|
||||||
|
class ArrowJSONEncoder(JSONEncoder):
|
||||||
|
def default(self, obj):
|
||||||
|
try:
|
||||||
|
if isinstance(obj, Arrow):
|
||||||
|
return obj.for_json()
|
||||||
|
elif isinstance(obj, date):
|
||||||
|
return obj.strftime("%Y-%m-%d")
|
||||||
|
elif isinstance(obj, datetime):
|
||||||
|
return obj.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
iterable = iter(obj)
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
return list(iterable)
|
||||||
|
return JSONEncoder.default(self, obj)
|
||||||
|
|
||||||
|
|
||||||
|
class ApiServer(RPC):
|
||||||
|
"""
|
||||||
|
This class runs api server and provides rpc.rpc functionality to it
|
||||||
|
|
||||||
|
This class starts a none blocking thread the api server runs within
|
||||||
|
"""
|
||||||
|
|
||||||
|
def rpc_catch_errors(func):
|
||||||
|
|
||||||
|
def func_wrapper(self, *args, **kwargs):
|
||||||
|
|
||||||
|
try:
|
||||||
|
return func(self, *args, **kwargs)
|
||||||
|
except RPCException as e:
|
||||||
|
logger.exception("API Error calling %s: %s", func.__name__, e)
|
||||||
|
return self.rest_error(f"Error querying {func.__name__}: {e}")
|
||||||
|
|
||||||
|
return func_wrapper
|
||||||
|
|
||||||
|
def check_auth(self, username, password):
|
||||||
|
return (username == self._config['api_server'].get('username') and
|
||||||
|
password == self._config['api_server'].get('password'))
|
||||||
|
|
||||||
|
def require_login(func):
|
||||||
|
|
||||||
|
def func_wrapper(self, *args, **kwargs):
|
||||||
|
|
||||||
|
auth = request.authorization
|
||||||
|
if auth and self.check_auth(auth.username, auth.password):
|
||||||
|
return func(self, *args, **kwargs)
|
||||||
|
else:
|
||||||
|
return jsonify({"error": "Unauthorized"}), 401
|
||||||
|
|
||||||
|
return func_wrapper
|
||||||
|
|
||||||
|
def __init__(self, freqtrade) -> None:
|
||||||
|
"""
|
||||||
|
Init the api server, and init the super class RPC
|
||||||
|
:param freqtrade: Instance of a freqtrade bot
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
super().__init__(freqtrade)
|
||||||
|
|
||||||
|
self._config = freqtrade.config
|
||||||
|
self.app = Flask(__name__)
|
||||||
|
self.app.json_encoder = ArrowJSONEncoder
|
||||||
|
|
||||||
|
# Register application handling
|
||||||
|
self.register_rest_rpc_urls()
|
||||||
|
|
||||||
|
thread = threading.Thread(target=self.run, daemon=True)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
def cleanup(self) -> None:
|
||||||
|
logger.info("Stopping API Server")
|
||||||
|
self.srv.shutdown()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""
|
||||||
|
Method that runs flask app in its own thread forever.
|
||||||
|
Section to handle configuration and running of the Rest server
|
||||||
|
also to check and warn if not bound to a loopback, warn on security risk.
|
||||||
|
"""
|
||||||
|
rest_ip = self._config['api_server']['listen_ip_address']
|
||||||
|
rest_port = self._config['api_server']['listen_port']
|
||||||
|
|
||||||
|
logger.info(f'Starting HTTP Server at {rest_ip}:{rest_port}')
|
||||||
|
if not IPv4Address(rest_ip).is_loopback:
|
||||||
|
logger.warning("SECURITY WARNING - Local Rest Server listening to external connections")
|
||||||
|
logger.warning("SECURITY WARNING - This is insecure please set to your loopback,"
|
||||||
|
"e.g 127.0.0.1 in config.json")
|
||||||
|
|
||||||
|
if not self._config['api_server'].get('password'):
|
||||||
|
logger.warning("SECURITY WARNING - No password for local REST Server defined. "
|
||||||
|
"Please make sure that this is intentional!")
|
||||||
|
|
||||||
|
# Run the Server
|
||||||
|
logger.info('Starting Local Rest Server.')
|
||||||
|
try:
|
||||||
|
self.srv = make_server(rest_ip, rest_port, self.app)
|
||||||
|
self.srv.serve_forever()
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Api server failed to start.")
|
||||||
|
logger.info('Local Rest Server started.')
|
||||||
|
|
||||||
|
def send_msg(self, msg: Dict[str, str]) -> None:
|
||||||
|
"""
|
||||||
|
We don't push to endpoints at the moment.
|
||||||
|
Take a look at webhooks for that functionality.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def rest_dump(self, return_value):
|
||||||
|
""" Helper function to jsonify object for a webserver """
|
||||||
|
return jsonify(return_value)
|
||||||
|
|
||||||
|
def rest_error(self, error_msg):
|
||||||
|
return jsonify({"error": error_msg}), 502
|
||||||
|
|
||||||
|
def register_rest_rpc_urls(self):
|
||||||
|
"""
|
||||||
|
Registers flask app URLs that are calls to functonality in rpc.rpc.
|
||||||
|
|
||||||
|
First two arguments passed are /URL and 'Label'
|
||||||
|
Label can be used as a shortcut when refactoring
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
self.app.register_error_handler(404, self.page_not_found)
|
||||||
|
|
||||||
|
# Actions to control the bot
|
||||||
|
self.app.add_url_rule(f'{BASE_URI}/start', 'start',
|
||||||
|
view_func=self._start, methods=['POST'])
|
||||||
|
self.app.add_url_rule(f'{BASE_URI}/stop', 'stop', view_func=self._stop, methods=['POST'])
|
||||||
|
self.app.add_url_rule(f'{BASE_URI}/stopbuy', 'stopbuy',
|
||||||
|
view_func=self._stopbuy, methods=['POST'])
|
||||||
|
self.app.add_url_rule(f'{BASE_URI}/reload_conf', 'reload_conf',
|
||||||
|
view_func=self._reload_conf, methods=['POST'])
|
||||||
|
# Info commands
|
||||||
|
self.app.add_url_rule(f'{BASE_URI}/balance', 'balance',
|
||||||
|
view_func=self._balance, methods=['GET'])
|
||||||
|
self.app.add_url_rule(f'{BASE_URI}/count', 'count', view_func=self._count, methods=['GET'])
|
||||||
|
self.app.add_url_rule(f'{BASE_URI}/daily', 'daily', view_func=self._daily, methods=['GET'])
|
||||||
|
self.app.add_url_rule(f'{BASE_URI}/edge', 'edge', view_func=self._edge, methods=['GET'])
|
||||||
|
self.app.add_url_rule(f'{BASE_URI}/profit', 'profit',
|
||||||
|
view_func=self._profit, methods=['GET'])
|
||||||
|
self.app.add_url_rule(f'{BASE_URI}/performance', 'performance',
|
||||||
|
view_func=self._performance, methods=['GET'])
|
||||||
|
self.app.add_url_rule(f'{BASE_URI}/status', 'status',
|
||||||
|
view_func=self._status, methods=['GET'])
|
||||||
|
self.app.add_url_rule(f'{BASE_URI}/version', 'version',
|
||||||
|
view_func=self._version, methods=['GET'])
|
||||||
|
|
||||||
|
# Combined actions and infos
|
||||||
|
self.app.add_url_rule(f'{BASE_URI}/blacklist', 'blacklist', view_func=self._blacklist,
|
||||||
|
methods=['GET', 'POST'])
|
||||||
|
self.app.add_url_rule(f'{BASE_URI}/whitelist', 'whitelist', view_func=self._whitelist,
|
||||||
|
methods=['GET'])
|
||||||
|
self.app.add_url_rule(f'{BASE_URI}/forcebuy', 'forcebuy',
|
||||||
|
view_func=self._forcebuy, methods=['POST'])
|
||||||
|
self.app.add_url_rule(f'{BASE_URI}/forcesell', 'forcesell', view_func=self._forcesell,
|
||||||
|
methods=['POST'])
|
||||||
|
|
||||||
|
# TODO: Implement the following
|
||||||
|
# help (?)
|
||||||
|
|
||||||
|
@require_login
|
||||||
|
def page_not_found(self, error):
|
||||||
|
"""
|
||||||
|
Return "404 not found", 404.
|
||||||
|
"""
|
||||||
|
return self.rest_dump({
|
||||||
|
'status': 'error',
|
||||||
|
'reason': f"There's no API call for {request.base_url}.",
|
||||||
|
'code': 404
|
||||||
|
}), 404
|
||||||
|
|
||||||
|
@require_login
|
||||||
|
@rpc_catch_errors
|
||||||
|
def _start(self):
|
||||||
|
"""
|
||||||
|
Handler for /start.
|
||||||
|
Starts TradeThread in bot if stopped.
|
||||||
|
"""
|
||||||
|
msg = self._rpc_start()
|
||||||
|
return self.rest_dump(msg)
|
||||||
|
|
||||||
|
@require_login
|
||||||
|
@rpc_catch_errors
|
||||||
|
def _stop(self):
|
||||||
|
"""
|
||||||
|
Handler for /stop.
|
||||||
|
Stops TradeThread in bot if running
|
||||||
|
"""
|
||||||
|
msg = self._rpc_stop()
|
||||||
|
return self.rest_dump(msg)
|
||||||
|
|
||||||
|
@require_login
|
||||||
|
@rpc_catch_errors
|
||||||
|
def _stopbuy(self):
|
||||||
|
"""
|
||||||
|
Handler for /stopbuy.
|
||||||
|
Sets max_open_trades to 0 and gracefully sells all open trades
|
||||||
|
"""
|
||||||
|
msg = self._rpc_stopbuy()
|
||||||
|
return self.rest_dump(msg)
|
||||||
|
|
||||||
|
@require_login
|
||||||
|
@rpc_catch_errors
|
||||||
|
def _version(self):
|
||||||
|
"""
|
||||||
|
Prints the bot's version
|
||||||
|
"""
|
||||||
|
return self.rest_dump({"version": __version__})
|
||||||
|
|
||||||
|
@require_login
|
||||||
|
@rpc_catch_errors
|
||||||
|
def _reload_conf(self):
|
||||||
|
"""
|
||||||
|
Handler for /reload_conf.
|
||||||
|
Triggers a config file reload
|
||||||
|
"""
|
||||||
|
msg = self._rpc_reload_conf()
|
||||||
|
return self.rest_dump(msg)
|
||||||
|
|
||||||
|
@require_login
|
||||||
|
@rpc_catch_errors
|
||||||
|
def _count(self):
|
||||||
|
"""
|
||||||
|
Handler for /count.
|
||||||
|
Returns the number of trades running
|
||||||
|
"""
|
||||||
|
msg = self._rpc_count()
|
||||||
|
return self.rest_dump(msg)
|
||||||
|
|
||||||
|
@require_login
|
||||||
|
@rpc_catch_errors
|
||||||
|
def _daily(self):
|
||||||
|
"""
|
||||||
|
Returns the last X days trading stats summary.
|
||||||
|
|
||||||
|
:return: stats
|
||||||
|
"""
|
||||||
|
timescale = request.args.get('timescale', 7)
|
||||||
|
timescale = int(timescale)
|
||||||
|
|
||||||
|
stats = self._rpc_daily_profit(timescale,
|
||||||
|
self._config['stake_currency'],
|
||||||
|
self._config['fiat_display_currency']
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.rest_dump(stats)
|
||||||
|
|
||||||
|
@require_login
|
||||||
|
@rpc_catch_errors
|
||||||
|
def _edge(self):
|
||||||
|
"""
|
||||||
|
Returns information related to Edge.
|
||||||
|
:return: edge stats
|
||||||
|
"""
|
||||||
|
stats = self._rpc_edge()
|
||||||
|
|
||||||
|
return self.rest_dump(stats)
|
||||||
|
|
||||||
|
@require_login
|
||||||
|
@rpc_catch_errors
|
||||||
|
def _profit(self):
|
||||||
|
"""
|
||||||
|
Handler for /profit.
|
||||||
|
|
||||||
|
Returns a cumulative profit statistics
|
||||||
|
:return: stats
|
||||||
|
"""
|
||||||
|
logger.info("LocalRPC - Profit Command Called")
|
||||||
|
|
||||||
|
stats = self._rpc_trade_statistics(self._config['stake_currency'],
|
||||||
|
self._config['fiat_display_currency']
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.rest_dump(stats)
|
||||||
|
|
||||||
|
@require_login
|
||||||
|
@rpc_catch_errors
|
||||||
|
def _performance(self):
|
||||||
|
"""
|
||||||
|
Handler for /performance.
|
||||||
|
|
||||||
|
Returns a cumulative performance statistics
|
||||||
|
:return: stats
|
||||||
|
"""
|
||||||
|
logger.info("LocalRPC - performance Command Called")
|
||||||
|
|
||||||
|
stats = self._rpc_performance()
|
||||||
|
|
||||||
|
return self.rest_dump(stats)
|
||||||
|
|
||||||
|
@require_login
|
||||||
|
@rpc_catch_errors
|
||||||
|
def _status(self):
|
||||||
|
"""
|
||||||
|
Handler for /status.
|
||||||
|
|
||||||
|
Returns the current status of the trades in json format
|
||||||
|
"""
|
||||||
|
results = self._rpc_trade_status()
|
||||||
|
return self.rest_dump(results)
|
||||||
|
|
||||||
|
@require_login
|
||||||
|
@rpc_catch_errors
|
||||||
|
def _balance(self):
|
||||||
|
"""
|
||||||
|
Handler for /balance.
|
||||||
|
|
||||||
|
Returns the current status of the trades in json format
|
||||||
|
"""
|
||||||
|
results = self._rpc_balance(self._config.get('fiat_display_currency', ''))
|
||||||
|
return self.rest_dump(results)
|
||||||
|
|
||||||
|
@require_login
|
||||||
|
@rpc_catch_errors
|
||||||
|
def _whitelist(self):
|
||||||
|
"""
|
||||||
|
Handler for /whitelist.
|
||||||
|
"""
|
||||||
|
results = self._rpc_whitelist()
|
||||||
|
return self.rest_dump(results)
|
||||||
|
|
||||||
|
@require_login
|
||||||
|
@rpc_catch_errors
|
||||||
|
def _blacklist(self):
|
||||||
|
"""
|
||||||
|
Handler for /blacklist.
|
||||||
|
"""
|
||||||
|
add = request.json.get("blacklist", None) if request.method == 'POST' else None
|
||||||
|
results = self._rpc_blacklist(add)
|
||||||
|
return self.rest_dump(results)
|
||||||
|
|
||||||
|
@require_login
|
||||||
|
@rpc_catch_errors
|
||||||
|
def _forcebuy(self):
|
||||||
|
"""
|
||||||
|
Handler for /forcebuy.
|
||||||
|
"""
|
||||||
|
asset = request.json.get("pair")
|
||||||
|
price = request.json.get("price", None)
|
||||||
|
trade = self._rpc_forcebuy(asset, price)
|
||||||
|
if trade:
|
||||||
|
return self.rest_dump(trade.to_json())
|
||||||
|
else:
|
||||||
|
return self.rest_dump({"status": f"Error buying pair {asset}."})
|
||||||
|
|
||||||
|
@require_login
|
||||||
|
@rpc_catch_errors
|
||||||
|
def _forcesell(self):
|
||||||
|
"""
|
||||||
|
Handler for /forcesell.
|
||||||
|
"""
|
||||||
|
tradeid = request.json.get("tradeid")
|
||||||
|
results = self._rpc_forcesell(tradeid)
|
||||||
|
return self.rest_dump(results)
|
|
@ -48,6 +48,11 @@ class RPCException(Exception):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.message
|
return self.message
|
||||||
|
|
||||||
|
def __json__(self):
|
||||||
|
return {
|
||||||
|
'msg': self.message
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class RPC(object):
|
class RPC(object):
|
||||||
"""
|
"""
|
||||||
|
@ -100,28 +105,17 @@ class RPC(object):
|
||||||
current_profit = trade.calc_profit_percent(current_rate)
|
current_profit = trade.calc_profit_percent(current_rate)
|
||||||
fmt_close_profit = (f'{round(trade.close_profit * 100, 2):.2f}%'
|
fmt_close_profit = (f'{round(trade.close_profit * 100, 2):.2f}%'
|
||||||
if trade.close_profit else None)
|
if trade.close_profit else None)
|
||||||
results.append(dict(
|
trade_dict = trade.to_json()
|
||||||
trade_id=trade.id,
|
trade_dict.update(dict(
|
||||||
pair=trade.pair,
|
|
||||||
base_currency=self._freqtrade.config['stake_currency'],
|
base_currency=self._freqtrade.config['stake_currency'],
|
||||||
date=arrow.get(trade.open_date),
|
|
||||||
open_rate=trade.open_rate,
|
|
||||||
close_rate=trade.close_rate,
|
|
||||||
current_rate=current_rate,
|
|
||||||
amount=round(trade.amount, 8),
|
|
||||||
stake_amount=round(trade.stake_amount, 8),
|
|
||||||
close_profit=fmt_close_profit,
|
close_profit=fmt_close_profit,
|
||||||
|
current_rate=current_rate,
|
||||||
current_profit=round(current_profit * 100, 2),
|
current_profit=round(current_profit * 100, 2),
|
||||||
stop_loss=trade.stop_loss,
|
|
||||||
stop_loss_pct=(trade.stop_loss_pct * 100)
|
|
||||||
if trade.stop_loss_pct else None,
|
|
||||||
initial_stop_loss=trade.initial_stop_loss,
|
|
||||||
initial_stop_loss_pct=(trade.initial_stop_loss_pct * 100)
|
|
||||||
if trade.initial_stop_loss_pct else None,
|
|
||||||
open_order='({} {} rem={:.8f})'.format(
|
open_order='({} {} rem={:.8f})'.format(
|
||||||
order['type'], order['side'], order['remaining']
|
order['type'], order['side'], order['remaining']
|
||||||
) if order else None,
|
) if order else None,
|
||||||
))
|
))
|
||||||
|
results.append(trade_dict)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def _rpc_status_table(self) -> DataFrame:
|
def _rpc_status_table(self) -> DataFrame:
|
||||||
|
@ -287,11 +281,12 @@ class RPC(object):
|
||||||
rate = 1.0
|
rate = 1.0
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
if coin == 'USDT':
|
if coin in('USDT', 'USD', 'EUR'):
|
||||||
rate = 1.0 / self._freqtrade.get_sell_rate('BTC/USDT', False)
|
rate = 1.0 / self._freqtrade.get_sell_rate('BTC/' + coin, False)
|
||||||
else:
|
else:
|
||||||
rate = self._freqtrade.get_sell_rate(coin + '/BTC', False)
|
rate = self._freqtrade.get_sell_rate(coin + '/BTC', False)
|
||||||
except (TemporaryError, DependencyException):
|
except (TemporaryError, DependencyException):
|
||||||
|
logger.warning(f" Could not get rate for pair {coin}.")
|
||||||
continue
|
continue
|
||||||
est_btc: float = rate * balance['total']
|
est_btc: float = rate * balance['total']
|
||||||
total = total + est_btc
|
total = total + est_btc
|
||||||
|
@ -346,7 +341,7 @@ class RPC(object):
|
||||||
|
|
||||||
return {'status': 'No more buy will occur from now. Run /reload_conf to reset.'}
|
return {'status': 'No more buy will occur from now. Run /reload_conf to reset.'}
|
||||||
|
|
||||||
def _rpc_forcesell(self, trade_id) -> None:
|
def _rpc_forcesell(self, trade_id) -> Dict[str, str]:
|
||||||
"""
|
"""
|
||||||
Handler for forcesell <id>.
|
Handler for forcesell <id>.
|
||||||
Sells the given trade at current price
|
Sells the given trade at current price
|
||||||
|
@ -386,7 +381,7 @@ class RPC(object):
|
||||||
for trade in Trade.get_open_trades():
|
for trade in Trade.get_open_trades():
|
||||||
_exec_forcesell(trade)
|
_exec_forcesell(trade)
|
||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
return
|
return {'result': 'Created sell orders for all open trades.'}
|
||||||
|
|
||||||
# Query for trade
|
# Query for trade
|
||||||
trade = Trade.query.filter(
|
trade = Trade.query.filter(
|
||||||
|
@ -401,6 +396,7 @@ class RPC(object):
|
||||||
|
|
||||||
_exec_forcesell(trade)
|
_exec_forcesell(trade)
|
||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
|
return {'result': f'Created sell order for trade {trade_id}.'}
|
||||||
|
|
||||||
def _rpc_forcebuy(self, pair: str, price: Optional[float]) -> Optional[Trade]:
|
def _rpc_forcebuy(self, pair: str, price: Optional[float]) -> Optional[Trade]:
|
||||||
"""
|
"""
|
||||||
|
@ -474,7 +470,7 @@ class RPC(object):
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def _rpc_blacklist(self, add: List[str]) -> Dict:
|
def _rpc_blacklist(self, add: List[str] = None) -> Dict:
|
||||||
""" Returns the currently active blacklist"""
|
""" Returns the currently active blacklist"""
|
||||||
if add:
|
if add:
|
||||||
stake_currency = self._freqtrade.config.get('stake_currency')
|
stake_currency = self._freqtrade.config.get('stake_currency')
|
||||||
|
|
|
@ -29,6 +29,12 @@ class RPCManager(object):
|
||||||
from freqtrade.rpc.webhook import Webhook
|
from freqtrade.rpc.webhook import Webhook
|
||||||
self.registered_modules.append(Webhook(freqtrade))
|
self.registered_modules.append(Webhook(freqtrade))
|
||||||
|
|
||||||
|
# Enable local rest api server for cmd line control
|
||||||
|
if freqtrade.config.get('api_server', {}).get('enabled', False):
|
||||||
|
logger.info('Enabling rpc.api_server')
|
||||||
|
from freqtrade.rpc.api_server import ApiServer
|
||||||
|
self.registered_modules.append(ApiServer(freqtrade))
|
||||||
|
|
||||||
def cleanup(self) -> None:
|
def cleanup(self) -> None:
|
||||||
""" Stops all enabled rpc modules """
|
""" Stops all enabled rpc modules """
|
||||||
logger.info('Cleaning up rpc modules ...')
|
logger.info('Cleaning up rpc modules ...')
|
||||||
|
|
|
@ -132,7 +132,7 @@ class Telegram(RPC):
|
||||||
msg['stake_amount_fiat'] = 0
|
msg['stake_amount_fiat'] = 0
|
||||||
|
|
||||||
message = ("*{exchange}:* Buying {pair}\n"
|
message = ("*{exchange}:* Buying {pair}\n"
|
||||||
"with limit `{limit:.8f}\n"
|
"at rate `{limit:.8f}\n"
|
||||||
"({stake_amount:.6f} {stake_currency}").format(**msg)
|
"({stake_amount:.6f} {stake_currency}").format(**msg)
|
||||||
|
|
||||||
if msg.get('fiat_currency', None):
|
if msg.get('fiat_currency', None):
|
||||||
|
@ -144,7 +144,7 @@ class Telegram(RPC):
|
||||||
msg['profit_percent'] = round(msg['profit_percent'] * 100, 2)
|
msg['profit_percent'] = round(msg['profit_percent'] * 100, 2)
|
||||||
|
|
||||||
message = ("*{exchange}:* Selling {pair}\n"
|
message = ("*{exchange}:* Selling {pair}\n"
|
||||||
"*Limit:* `{limit:.8f}`\n"
|
"*Rate:* `{limit:.8f}`\n"
|
||||||
"*Amount:* `{amount:.8f}`\n"
|
"*Amount:* `{amount:.8f}`\n"
|
||||||
"*Open Rate:* `{open_rate:.8f}`\n"
|
"*Open Rate:* `{open_rate:.8f}`\n"
|
||||||
"*Current Rate:* `{current_rate:.8f}`\n"
|
"*Current Rate:* `{current_rate:.8f}`\n"
|
||||||
|
@ -193,14 +193,11 @@ class Telegram(RPC):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
results = self._rpc_trade_status()
|
results = self._rpc_trade_status()
|
||||||
# pre format data
|
|
||||||
for result in results:
|
|
||||||
result['date'] = result['date'].humanize()
|
|
||||||
|
|
||||||
messages = []
|
messages = []
|
||||||
for r in results:
|
for r in results:
|
||||||
lines = [
|
lines = [
|
||||||
"*Trade ID:* `{trade_id}` `(since {date})`",
|
"*Trade ID:* `{trade_id}` `(since {open_date_hum})`",
|
||||||
"*Current Pair:* {pair}",
|
"*Current Pair:* {pair}",
|
||||||
"*Amount:* `{amount} ({stake_amount} {base_currency})`",
|
"*Amount:* `{amount} ({stake_amount} {base_currency})`",
|
||||||
"*Open Rate:* `{open_rate:.8f}`",
|
"*Open Rate:* `{open_rate:.8f}`",
|
||||||
|
@ -413,7 +410,9 @@ class Telegram(RPC):
|
||||||
|
|
||||||
trade_id = update.message.text.replace('/forcesell', '').strip()
|
trade_id = update.message.text.replace('/forcesell', '').strip()
|
||||||
try:
|
try:
|
||||||
self._rpc_forcesell(trade_id)
|
msg = self._rpc_forcesell(trade_id)
|
||||||
|
self._send_msg('Forcesell Result: `{result}`'.format(**msg), bot=bot)
|
||||||
|
|
||||||
except RPCException as e:
|
except RPCException as e:
|
||||||
self._send_msg(str(e), bot=bot)
|
self._send_msg(str(e), bot=bot)
|
||||||
|
|
||||||
|
|
|
@ -18,11 +18,11 @@ class State(Enum):
|
||||||
class RunMode(Enum):
|
class RunMode(Enum):
|
||||||
"""
|
"""
|
||||||
Bot running mode (backtest, hyperopt, ...)
|
Bot running mode (backtest, hyperopt, ...)
|
||||||
can be "live", "dry-run", "backtest", "edgecli", "hyperopt".
|
can be "live", "dry-run", "backtest", "edge", "hyperopt".
|
||||||
"""
|
"""
|
||||||
LIVE = "live"
|
LIVE = "live"
|
||||||
DRY_RUN = "dry_run"
|
DRY_RUN = "dry_run"
|
||||||
BACKTEST = "backtest"
|
BACKTEST = "backtest"
|
||||||
EDGECLI = "edgecli"
|
EDGE = "edge"
|
||||||
HYPEROPT = "hyperopt"
|
HYPEROPT = "hyperopt"
|
||||||
OTHER = "other" # Used for plotting scripts and test
|
OTHER = "other" # Used for plotting scripts and test
|
||||||
|
|
|
@ -6,6 +6,7 @@ from freqtrade.strategy.interface import IStrategy
|
||||||
# Import Default-Strategy to have hyperopt correctly resolve
|
# Import Default-Strategy to have hyperopt correctly resolve
|
||||||
from freqtrade.strategy.default_strategy import DefaultStrategy # noqa: F401
|
from freqtrade.strategy.default_strategy import DefaultStrategy # noqa: F401
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,7 +17,6 @@ def import_strategy(strategy: IStrategy, config: dict) -> IStrategy:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Copy all attributes from base class and class
|
# Copy all attributes from base class and class
|
||||||
|
|
||||||
comb = {**strategy.__class__.__dict__, **strategy.__dict__}
|
comb = {**strategy.__class__.__dict__, **strategy.__dict__}
|
||||||
|
|
||||||
# Delete '_abc_impl' from dict as deepcopy fails on 3.7 with
|
# Delete '_abc_impl' from dict as deepcopy fails on 3.7 with
|
||||||
|
@ -26,6 +26,7 @@ def import_strategy(strategy: IStrategy, config: dict) -> IStrategy:
|
||||||
del comb['_abc_impl']
|
del comb['_abc_impl']
|
||||||
|
|
||||||
attr = deepcopy(comb)
|
attr = deepcopy(comb)
|
||||||
|
|
||||||
# Adjust module name
|
# Adjust module name
|
||||||
attr['__module__'] = 'freqtrade.strategy'
|
attr['__module__'] = 'freqtrade.strategy'
|
||||||
|
|
||||||
|
|
|
@ -13,10 +13,11 @@ import arrow
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.data.dataprovider import DataProvider
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
from freqtrade.misc import timeframe_to_minutes
|
from freqtrade.exchange import timeframe_to_minutes
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.wallets import Wallets
|
from freqtrade.wallets import Wallets
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -157,7 +158,7 @@ class IStrategy(ABC):
|
||||||
"""
|
"""
|
||||||
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
|
||||||
:return DataFrame with ticker data and indicator data
|
:return: DataFrame with ticker data and indicator data
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pair = str(metadata.get('pair'))
|
pair = str(metadata.get('pair'))
|
||||||
|
@ -307,14 +308,16 @@ class IStrategy(ABC):
|
||||||
|
|
||||||
if trailing_stop:
|
if trailing_stop:
|
||||||
# trailing stoploss handling
|
# trailing stoploss handling
|
||||||
|
|
||||||
sl_offset = self.config.get('trailing_stop_positive_offset') or 0.0
|
sl_offset = self.config.get('trailing_stop_positive_offset') or 0.0
|
||||||
tsl_only_offset = self.config.get('trailing_only_offset_is_reached', False)
|
tsl_only_offset = self.config.get('trailing_only_offset_is_reached', False)
|
||||||
|
|
||||||
|
# Make sure current_profit is calculated using high for backtesting.
|
||||||
|
high_profit = current_profit if not high else trade.calc_profit_percent(high)
|
||||||
|
|
||||||
# Don't update stoploss if trailing_only_offset_is_reached is true.
|
# Don't update stoploss if trailing_only_offset_is_reached is true.
|
||||||
if not (tsl_only_offset and current_profit < sl_offset):
|
if not (tsl_only_offset and high_profit < sl_offset):
|
||||||
# Specific handling for trailing_stop_positive
|
# Specific handling for trailing_stop_positive
|
||||||
if 'trailing_stop_positive' in self.config and current_profit > sl_offset:
|
if 'trailing_stop_positive' in self.config and high_profit > sl_offset:
|
||||||
# Ignore mypy error check in configuration that this is a float
|
# Ignore mypy error check in configuration that this is a float
|
||||||
stop_loss_value = self.config.get('trailing_stop_positive') # type: ignore
|
stop_loss_value = self.config.get('trailing_stop_positive') # type: ignore
|
||||||
logger.debug(f"using positive stop loss: {stop_loss_value} "
|
logger.debug(f"using positive stop loss: {stop_loss_value} "
|
||||||
|
@ -328,8 +331,9 @@ class IStrategy(ABC):
|
||||||
(not self.order_types.get('stoploss_on_exchange'))):
|
(not self.order_types.get('stoploss_on_exchange'))):
|
||||||
|
|
||||||
selltype = SellType.STOP_LOSS
|
selltype = SellType.STOP_LOSS
|
||||||
# If Trailing stop (and max-rate did move above open rate)
|
|
||||||
if trailing_stop and trade.open_rate != trade.max_rate:
|
# If initial stoploss is not the same as current one then it is trailing.
|
||||||
|
if trade.initial_stop_loss != trade.stop_loss:
|
||||||
selltype = SellType.TRAILING_STOP_LOSS
|
selltype = SellType.TRAILING_STOP_LOSS
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"HIT STOP: current price at {current_rate:.6f}, "
|
f"HIT STOP: current price at {current_rate:.6f}, "
|
||||||
|
@ -347,7 +351,7 @@ class IStrategy(ABC):
|
||||||
"""
|
"""
|
||||||
Based an earlier trade and current price and ROI configuration, decides whether bot should
|
Based an earlier trade and current price and ROI configuration, decides whether bot should
|
||||||
sell. Requires current_profit to be in percent!!
|
sell. Requires current_profit to be in percent!!
|
||||||
:return True if bot should sell at current rate
|
:return: True if bot should sell at current rate
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Check if time matches and current rate is above threshold
|
# Check if time matches and current rate is above threshold
|
||||||
|
@ -376,6 +380,7 @@ class IStrategy(ABC):
|
||||||
:param metadata: Additional information, like the currently traded pair
|
:param metadata: Additional information, like the currently traded pair
|
||||||
:return: a Dataframe with all mandatory indicators for the strategies
|
:return: a Dataframe with all mandatory indicators for the strategies
|
||||||
"""
|
"""
|
||||||
|
logger.debug(f"Populating indicators for pair {metadata.get('pair')}.")
|
||||||
if self._populate_fun_len == 2:
|
if self._populate_fun_len == 2:
|
||||||
warnings.warn("deprecated - check out the Sample strategy to see "
|
warnings.warn("deprecated - check out the Sample strategy to see "
|
||||||
"the current function headers!", DeprecationWarning)
|
"the current function headers!", DeprecationWarning)
|
||||||
|
@ -391,6 +396,7 @@ class IStrategy(ABC):
|
||||||
:param pair: Additional information, like the currently traded pair
|
:param pair: Additional information, like the currently traded pair
|
||||||
:return: DataFrame with buy column
|
:return: DataFrame with buy column
|
||||||
"""
|
"""
|
||||||
|
logger.debug(f"Populating buy signals for pair {metadata.get('pair')}.")
|
||||||
if self._buy_fun_len == 2:
|
if self._buy_fun_len == 2:
|
||||||
warnings.warn("deprecated - check out the Sample strategy to see "
|
warnings.warn("deprecated - check out the Sample strategy to see "
|
||||||
"the current function headers!", DeprecationWarning)
|
"the current function headers!", DeprecationWarning)
|
||||||
|
@ -406,6 +412,7 @@ class IStrategy(ABC):
|
||||||
:param pair: Additional information, like the currently traded pair
|
:param pair: Additional information, like the currently traded pair
|
||||||
:return: DataFrame with sell column
|
:return: DataFrame with sell column
|
||||||
"""
|
"""
|
||||||
|
logger.debug(f"Populating sell signals for pair {metadata.get('pair')}.")
|
||||||
if self._sell_fun_len == 2:
|
if self._sell_fun_len == 2:
|
||||||
warnings.warn("deprecated - check out the Sample strategy to see "
|
warnings.warn("deprecated - check out the Sample strategy to see "
|
||||||
"the current function headers!", DeprecationWarning)
|
"the current function headers!", DeprecationWarning)
|
||||||
|
|
|
@ -2,15 +2,19 @@
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
from copy import deepcopy
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
from unittest.mock import MagicMock, PropertyMock
|
from unittest.mock import MagicMock, PropertyMock
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import pytest
|
import pytest
|
||||||
from telegram import Chat, Message, Update
|
from telegram import Chat, Message, Update
|
||||||
|
|
||||||
from freqtrade import constants
|
from freqtrade import constants, persistence
|
||||||
|
from freqtrade.arguments import Arguments
|
||||||
from freqtrade.data.converter import parse_ticker_dataframe
|
from freqtrade.data.converter import parse_ticker_dataframe
|
||||||
from freqtrade.edge import Edge, PairInfo
|
from freqtrade.edge import Edge, PairInfo
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
|
@ -35,6 +39,10 @@ def log_has_re(line, logs):
|
||||||
False)
|
False)
|
||||||
|
|
||||||
|
|
||||||
|
def get_args(args) -> List[str]:
|
||||||
|
return Arguments(args, '').get_parsed_arg()
|
||||||
|
|
||||||
|
|
||||||
def patch_exchange(mocker, api_mock=None, id='bittrex') -> None:
|
def patch_exchange(mocker, api_mock=None, id='bittrex') -> None:
|
||||||
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
||||||
|
@ -53,7 +61,7 @@ def get_patched_exchange(mocker, config, api_mock=None, id='bittrex') -> Exchang
|
||||||
patch_exchange(mocker, api_mock, id)
|
patch_exchange(mocker, api_mock, id)
|
||||||
config["exchange"]["name"] = id
|
config["exchange"]["name"] = id
|
||||||
try:
|
try:
|
||||||
exchange = ExchangeResolver(id.title(), config).exchange
|
exchange = ExchangeResolver(id, config).exchange
|
||||||
except ImportError:
|
except ImportError:
|
||||||
exchange = Exchange(config)
|
exchange = Exchange(config)
|
||||||
return exchange
|
return exchange
|
||||||
|
@ -96,22 +104,44 @@ def patch_freqtradebot(mocker, config) -> None:
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
|
persistence.init(config['db_url'])
|
||||||
patch_exchange(mocker, None)
|
patch_exchange(mocker, None)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock())
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock())
|
||||||
|
|
||||||
|
|
||||||
def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
|
def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
|
||||||
|
"""
|
||||||
|
This function patches _init_modules() to not call dependencies
|
||||||
|
:param mocker: a Mocker object to apply patches
|
||||||
|
:param config: Config to pass to the bot
|
||||||
|
:return: FreqtradeBot
|
||||||
|
"""
|
||||||
patch_freqtradebot(mocker, config)
|
patch_freqtradebot(mocker, config)
|
||||||
return FreqtradeBot(config)
|
return FreqtradeBot(config)
|
||||||
|
|
||||||
|
|
||||||
def get_patched_worker(mocker, config) -> Worker:
|
def get_patched_worker(mocker, config) -> Worker:
|
||||||
|
"""
|
||||||
|
This function patches _init_modules() to not call dependencies
|
||||||
|
:param mocker: a Mocker object to apply patches
|
||||||
|
:param config: Config to pass to the bot
|
||||||
|
:return: Worker
|
||||||
|
"""
|
||||||
patch_freqtradebot(mocker, config)
|
patch_freqtradebot(mocker, config)
|
||||||
return Worker(args=None, config=config)
|
return Worker(args=None, config=config)
|
||||||
|
|
||||||
|
|
||||||
|
def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None:
|
||||||
|
"""
|
||||||
|
:param mocker: mocker to patch IStrategy class
|
||||||
|
:param value: which value IStrategy.get_signal() must return
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
freqtrade.strategy.get_signal = lambda e, s, t: value
|
||||||
|
freqtrade.exchange.refresh_latest_ohlcv = lambda p: None
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def patch_coinmarketcap(mocker) -> None:
|
def patch_coinmarketcap(mocker) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -134,6 +164,11 @@ def patch_coinmarketcap(mocker) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='function')
|
||||||
|
def init_persistence(default_conf):
|
||||||
|
persistence.init(default_conf['db_url'], default_conf['dry_run'])
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def default_conf():
|
def default_conf():
|
||||||
""" Returns validated configuration suitable for most tests """
|
""" Returns validated configuration suitable for most tests """
|
||||||
|
@ -639,7 +674,7 @@ def ticker_history_list():
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def ticker_history(ticker_history_list):
|
def ticker_history(ticker_history_list):
|
||||||
return parse_ticker_dataframe(ticker_history_list, "5m", True)
|
return parse_ticker_dataframe(ticker_history_list, "5m", pair="UNITTEST/BTC", fill_missing=True)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
@ -843,8 +878,9 @@ def tickers():
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def result():
|
def result():
|
||||||
with open('freqtrade/tests/testdata/UNITTEST_BTC-1m.json') as data_file:
|
with Path('freqtrade/tests/testdata/UNITTEST_BTC-1m.json').open('r') as data_file:
|
||||||
return parse_ticker_dataframe(json.load(data_file), '1m', True)
|
return parse_ticker_dataframe(json.load(data_file), '1m', pair="UNITTEST/BTC",
|
||||||
|
fill_missing=True)
|
||||||
|
|
||||||
# FIX:
|
# FIX:
|
||||||
# Create an fixture/function
|
# Create an fixture/function
|
||||||
|
@ -942,9 +978,10 @@ def buy_order_fee():
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def edge_conf(default_conf):
|
def edge_conf(default_conf):
|
||||||
default_conf['max_open_trades'] = -1
|
conf = deepcopy(default_conf)
|
||||||
default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
|
conf['max_open_trades'] = -1
|
||||||
default_conf['edge'] = {
|
conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
|
||||||
|
conf['edge'] = {
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"process_throttle_secs": 1800,
|
"process_throttle_secs": 1800,
|
||||||
"calculate_since_number_of_days": 14,
|
"calculate_since_number_of_days": 14,
|
||||||
|
@ -960,4 +997,40 @@ def edge_conf(default_conf):
|
||||||
"remove_pumps": False
|
"remove_pumps": False
|
||||||
}
|
}
|
||||||
|
|
||||||
return default_conf
|
return conf
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def rpc_balance():
|
||||||
|
return {
|
||||||
|
'BTC': {
|
||||||
|
'total': 12.0,
|
||||||
|
'free': 12.0,
|
||||||
|
'used': 0.0
|
||||||
|
},
|
||||||
|
'ETH': {
|
||||||
|
'total': 0.0,
|
||||||
|
'free': 0.0,
|
||||||
|
'used': 0.0
|
||||||
|
},
|
||||||
|
'USDT': {
|
||||||
|
'total': 10000.0,
|
||||||
|
'free': 10000.0,
|
||||||
|
'used': 0.0
|
||||||
|
},
|
||||||
|
'LTC': {
|
||||||
|
'total': 10.0,
|
||||||
|
'free': 10.0,
|
||||||
|
'used': 0.0
|
||||||
|
},
|
||||||
|
'XRP': {
|
||||||
|
'total': 1.0,
|
||||||
|
'free': 1.0,
|
||||||
|
'used': 0.0
|
||||||
|
},
|
||||||
|
'EUR': {
|
||||||
|
'total': 10.0,
|
||||||
|
'free': 10.0,
|
||||||
|
'used': 0.0
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,15 @@
|
||||||
import pytest
|
from unittest.mock import MagicMock
|
||||||
from pandas import DataFrame
|
|
||||||
|
|
||||||
from freqtrade.data.btanalysis import BT_DATA_COLUMNS, load_backtest_data
|
from arrow import Arrow
|
||||||
from freqtrade.data.history import make_testdata_path
|
import pytest
|
||||||
|
from pandas import DataFrame, to_datetime
|
||||||
|
|
||||||
|
from freqtrade.arguments import TimeRange
|
||||||
|
from freqtrade.data.btanalysis import (BT_DATA_COLUMNS,
|
||||||
|
extract_trades_of_period,
|
||||||
|
load_backtest_data, load_trades_from_db)
|
||||||
|
from freqtrade.data.history import load_pair_history, make_testdata_path
|
||||||
|
from freqtrade.tests.test_persistence import create_mock_trades
|
||||||
|
|
||||||
|
|
||||||
def test_load_backtest_data():
|
def test_load_backtest_data():
|
||||||
|
@ -19,3 +26,51 @@ def test_load_backtest_data():
|
||||||
|
|
||||||
with pytest.raises(ValueError, match=r"File .* does not exist\."):
|
with pytest.raises(ValueError, match=r"File .* does not exist\."):
|
||||||
load_backtest_data(str("filename") + "nofile")
|
load_backtest_data(str("filename") + "nofile")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
|
def test_load_trades_db(default_conf, fee, mocker):
|
||||||
|
|
||||||
|
create_mock_trades(fee)
|
||||||
|
# remove init so it does not init again
|
||||||
|
init_mock = mocker.patch('freqtrade.persistence.init', MagicMock())
|
||||||
|
|
||||||
|
trades = load_trades_from_db(db_url=default_conf['db_url'])
|
||||||
|
assert init_mock.call_count == 1
|
||||||
|
assert len(trades) == 3
|
||||||
|
assert isinstance(trades, DataFrame)
|
||||||
|
assert "pair" in trades.columns
|
||||||
|
assert "open_time" in trades.columns
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_trades_of_period():
|
||||||
|
pair = "UNITTEST/BTC"
|
||||||
|
timerange = TimeRange(None, 'line', 0, -1000)
|
||||||
|
|
||||||
|
data = load_pair_history(pair=pair, ticker_interval='1m',
|
||||||
|
datadir=None, timerange=timerange)
|
||||||
|
|
||||||
|
# timerange = 2017-11-14 06:07 - 2017-11-14 22:58:00
|
||||||
|
trades = DataFrame(
|
||||||
|
{'pair': [pair, pair, pair, pair],
|
||||||
|
'profit_percent': [0.0, 0.1, -0.2, -0.5],
|
||||||
|
'profit_abs': [0.0, 1, -2, -5],
|
||||||
|
'open_time': to_datetime([Arrow(2017, 11, 13, 15, 40, 0).datetime,
|
||||||
|
Arrow(2017, 11, 14, 9, 41, 0).datetime,
|
||||||
|
Arrow(2017, 11, 14, 14, 20, 0).datetime,
|
||||||
|
Arrow(2017, 11, 15, 3, 40, 0).datetime,
|
||||||
|
], utc=True
|
||||||
|
),
|
||||||
|
'close_time': to_datetime([Arrow(2017, 11, 13, 16, 40, 0).datetime,
|
||||||
|
Arrow(2017, 11, 14, 10, 41, 0).datetime,
|
||||||
|
Arrow(2017, 11, 14, 15, 25, 0).datetime,
|
||||||
|
Arrow(2017, 11, 15, 3, 55, 0).datetime,
|
||||||
|
], utc=True)
|
||||||
|
})
|
||||||
|
trades1 = extract_trades_of_period(data, trades)
|
||||||
|
# First and last trade are dropped as they are out of range
|
||||||
|
assert len(trades1) == 2
|
||||||
|
assert trades1.iloc[0].open_time == Arrow(2017, 11, 14, 9, 41, 0).datetime
|
||||||
|
assert trades1.iloc[0].close_time == Arrow(2017, 11, 14, 10, 41, 0).datetime
|
||||||
|
assert trades1.iloc[-1].open_time == Arrow(2017, 11, 14, 14, 20, 0).datetime
|
||||||
|
assert trades1.iloc[-1].close_time == Arrow(2017, 11, 14, 15, 25, 0).datetime
|
||||||
|
|
|
@ -2,8 +2,7 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from freqtrade.data.converter import parse_ticker_dataframe, ohlcv_fill_up_missing_data
|
from freqtrade.data.converter import parse_ticker_dataframe, ohlcv_fill_up_missing_data
|
||||||
from freqtrade.data.history import load_pair_history
|
from freqtrade.data.history import load_pair_history, validate_backtest_data, get_timeframe
|
||||||
from freqtrade.optimize import validate_backtest_data, get_timeframe
|
|
||||||
from freqtrade.tests.conftest import log_has
|
from freqtrade.tests.conftest import log_has
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,7 +15,8 @@ def test_parse_ticker_dataframe(ticker_history_list, caplog):
|
||||||
|
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
# Test file with BV data
|
# Test file with BV data
|
||||||
dataframe = parse_ticker_dataframe(ticker_history_list, '5m', fill_missing=True)
|
dataframe = parse_ticker_dataframe(ticker_history_list, '5m',
|
||||||
|
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.record_tuples)
|
||||||
|
|
||||||
|
@ -28,18 +28,19 @@ def test_ohlcv_fill_up_missing_data(caplog):
|
||||||
pair='UNITTEST/BTC',
|
pair='UNITTEST/BTC',
|
||||||
fill_up_missing=False)
|
fill_up_missing=False)
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
data2 = ohlcv_fill_up_missing_data(data, '1m')
|
data2 = ohlcv_fill_up_missing_data(data, '1m', 'UNITTEST/BTC')
|
||||||
assert len(data2) > len(data)
|
assert len(data2) > len(data)
|
||||||
# Column names should not change
|
# Column names should not change
|
||||||
assert (data.columns == data2.columns).all()
|
assert (data.columns == data2.columns).all()
|
||||||
|
|
||||||
assert log_has(f"Missing data fillup: before: {len(data)} - after: {len(data2)}",
|
assert log_has(f"Missing data fillup for UNITTEST/BTC: before: "
|
||||||
|
f"{len(data)} - after: {len(data2)}",
|
||||||
caplog.record_tuples)
|
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})
|
||||||
assert validate_backtest_data({'UNITTEST/BTC': data}, min_date, max_date, 1)
|
assert validate_backtest_data(data, 'UNITTEST/BTC', min_date, max_date, 1)
|
||||||
assert not validate_backtest_data({'UNITTEST/BTC': data2}, min_date, max_date, 1)
|
assert not validate_backtest_data(data2, 'UNITTEST/BTC', min_date, max_date, 1)
|
||||||
|
|
||||||
|
|
||||||
def test_ohlcv_fill_up_missing_data2(caplog):
|
def test_ohlcv_fill_up_missing_data2(caplog):
|
||||||
|
@ -79,10 +80,10 @@ def test_ohlcv_fill_up_missing_data2(caplog):
|
||||||
]
|
]
|
||||||
|
|
||||||
# Generate test-data without filling missing
|
# Generate test-data without filling missing
|
||||||
data = parse_ticker_dataframe(ticks, ticker_interval, fill_missing=False)
|
data = parse_ticker_dataframe(ticks, ticker_interval, pair="UNITTEST/BTC", fill_missing=False)
|
||||||
assert len(data) == 3
|
assert len(data) == 3
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
data2 = ohlcv_fill_up_missing_data(data, ticker_interval)
|
data2 = ohlcv_fill_up_missing_data(data, ticker_interval, "UNITTEST/BTC")
|
||||||
assert len(data2) == 4
|
assert len(data2) == 4
|
||||||
# 3rd candle has been filled
|
# 3rd candle has been filled
|
||||||
row = data2.loc[2, :]
|
row = data2.loc[2, :]
|
||||||
|
@ -95,5 +96,55 @@ def test_ohlcv_fill_up_missing_data2(caplog):
|
||||||
# Column names should not change
|
# Column names should not change
|
||||||
assert (data.columns == data2.columns).all()
|
assert (data.columns == data2.columns).all()
|
||||||
|
|
||||||
assert log_has(f"Missing data fillup: before: {len(data)} - after: {len(data2)}",
|
assert log_has(f"Missing data fillup for UNITTEST/BTC: before: "
|
||||||
|
f"{len(data)} - after: {len(data2)}",
|
||||||
caplog.record_tuples)
|
caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_ohlcv_drop_incomplete(caplog):
|
||||||
|
ticker_interval = '1d'
|
||||||
|
ticks = [[
|
||||||
|
1559750400000, # 2019-06-04
|
||||||
|
8.794e-05, # open
|
||||||
|
8.948e-05, # high
|
||||||
|
8.794e-05, # low
|
||||||
|
8.88e-05, # close
|
||||||
|
2255, # volume (in quote currency)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1559836800000, # 2019-06-05
|
||||||
|
8.88e-05,
|
||||||
|
8.942e-05,
|
||||||
|
8.88e-05,
|
||||||
|
8.893e-05,
|
||||||
|
9911,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1559923200000, # 2019-06-06
|
||||||
|
8.891e-05,
|
||||||
|
8.893e-05,
|
||||||
|
8.875e-05,
|
||||||
|
8.877e-05,
|
||||||
|
2251
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1560009600000, # 2019-06-07
|
||||||
|
8.877e-05,
|
||||||
|
8.883e-05,
|
||||||
|
8.895e-05,
|
||||||
|
8.817e-05,
|
||||||
|
123551
|
||||||
|
]
|
||||||
|
]
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
data = parse_ticker_dataframe(ticks, ticker_interval, pair="UNITTEST/BTC",
|
||||||
|
fill_missing=False, drop_incomplete=False)
|
||||||
|
assert len(data) == 4
|
||||||
|
assert not log_has("Dropping last candle", caplog.record_tuples)
|
||||||
|
|
||||||
|
# Drop last candle
|
||||||
|
data = parse_ticker_dataframe(ticks, ticker_interval, pair="UNITTEST/BTC",
|
||||||
|
fill_missing=False, drop_incomplete=True)
|
||||||
|
assert len(data) == 3
|
||||||
|
|
||||||
|
assert log_has("Dropping last candle", caplog.record_tuples)
|
||||||
|
|
|
@ -2,24 +2,27 @@
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
|
||||||
import uuid
|
import uuid
|
||||||
|
from pathlib import Path
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
from pandas import DataFrame
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade import OperationalException
|
from freqtrade import OperationalException
|
||||||
from freqtrade.arguments import TimeRange
|
from freqtrade.arguments import TimeRange
|
||||||
from freqtrade.data import history
|
from freqtrade.data import history
|
||||||
from freqtrade.data.history import (download_pair_history,
|
from freqtrade.data.history import (download_pair_history,
|
||||||
load_cached_data_for_updating,
|
load_cached_data_for_updating,
|
||||||
load_tickerdata_file,
|
load_tickerdata_file, make_testdata_path,
|
||||||
make_testdata_path,
|
|
||||||
trim_tickerlist)
|
trim_tickerlist)
|
||||||
|
from freqtrade.exchange import timeframe_to_minutes
|
||||||
from freqtrade.misc import file_dump_json
|
from freqtrade.misc import file_dump_json
|
||||||
from freqtrade.tests.conftest import get_patched_exchange, log_has
|
from freqtrade.strategy.default_strategy import DefaultStrategy
|
||||||
|
from freqtrade.tests.conftest import (get_patched_exchange, log_has,
|
||||||
|
patch_exchange)
|
||||||
|
|
||||||
# Change this if modifying UNITTEST/BTC testdatafile
|
# Change this if modifying UNITTEST/BTC testdatafile
|
||||||
_BTC_UNITTEST_LENGTH = 13681
|
_BTC_UNITTEST_LENGTH = 13681
|
||||||
|
@ -59,7 +62,11 @@ def _clean_test_file(file: str) -> None:
|
||||||
def test_load_data_30min_ticker(mocker, caplog, default_conf) -> None:
|
def test_load_data_30min_ticker(mocker, caplog, default_conf) -> None:
|
||||||
ld = history.load_pair_history(pair='UNITTEST/BTC', ticker_interval='30m', datadir=None)
|
ld = history.load_pair_history(pair='UNITTEST/BTC', ticker_interval='30m', datadir=None)
|
||||||
assert isinstance(ld, DataFrame)
|
assert isinstance(ld, DataFrame)
|
||||||
assert not log_has('Download the pair: "UNITTEST/BTC", Interval: 30m', caplog.record_tuples)
|
assert not log_has(
|
||||||
|
'Download history data for pair: "UNITTEST/BTC", interval: 30m '
|
||||||
|
'and store in None.',
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_load_data_7min_ticker(mocker, caplog, default_conf) -> None:
|
def test_load_data_7min_ticker(mocker, caplog, default_conf) -> None:
|
||||||
|
@ -67,8 +74,11 @@ def test_load_data_7min_ticker(mocker, caplog, default_conf) -> None:
|
||||||
assert not isinstance(ld, DataFrame)
|
assert not isinstance(ld, DataFrame)
|
||||||
assert ld is None
|
assert ld is None
|
||||||
assert log_has(
|
assert log_has(
|
||||||
'No data for pair: "UNITTEST/BTC", Interval: 7m. '
|
'No history data for pair: "UNITTEST/BTC", interval: 7m. '
|
||||||
'Use --refresh-pairs-cached to download the data', caplog.record_tuples)
|
'Use --refresh-pairs-cached option or download_backtest_data.py '
|
||||||
|
'script to download the data',
|
||||||
|
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:
|
||||||
|
@ -77,7 +87,11 @@ def test_load_data_1min_ticker(ticker_history, mocker, caplog) -> None:
|
||||||
_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('Download the pair: "UNITTEST/BTC", Interval: 1m', caplog.record_tuples)
|
assert not log_has(
|
||||||
|
'Download history data for pair: "UNITTEST/BTC", interval: 1m '
|
||||||
|
'and store in None.',
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
_clean_test_file(file)
|
_clean_test_file(file)
|
||||||
|
|
||||||
|
|
||||||
|
@ -96,9 +110,12 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, defau
|
||||||
refresh_pairs=False,
|
refresh_pairs=False,
|
||||||
pair='MEME/BTC')
|
pair='MEME/BTC')
|
||||||
assert os.path.isfile(file) is False
|
assert os.path.isfile(file) is False
|
||||||
assert log_has('No data for pair: "MEME/BTC", Interval: 1m. '
|
assert log_has(
|
||||||
'Use --refresh-pairs-cached to download the data',
|
'No history data for pair: "MEME/BTC", interval: 1m. '
|
||||||
caplog.record_tuples)
|
'Use --refresh-pairs-cached option or download_backtest_data.py '
|
||||||
|
'script to download the data',
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
# download a new pair if refresh_pairs is set
|
# download a new pair if refresh_pairs is set
|
||||||
history.load_pair_history(datadir=None,
|
history.load_pair_history(datadir=None,
|
||||||
|
@ -107,7 +124,11 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, defau
|
||||||
exchange=exchange,
|
exchange=exchange,
|
||||||
pair='MEME/BTC')
|
pair='MEME/BTC')
|
||||||
assert os.path.isfile(file) is True
|
assert os.path.isfile(file) is True
|
||||||
assert log_has('Download the pair: "MEME/BTC", Interval: 1m', caplog.record_tuples)
|
assert log_has(
|
||||||
|
'Download history data for pair: "MEME/BTC", interval: 1m '
|
||||||
|
'and store in None.',
|
||||||
|
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,
|
||||||
ticker_interval='1m',
|
ticker_interval='1m',
|
||||||
|
@ -117,6 +138,31 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, defau
|
||||||
_clean_test_file(file)
|
_clean_test_file(file)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_data_live(default_conf, mocker, caplog) -> None:
|
||||||
|
refresh_mock = MagicMock()
|
||||||
|
mocker.patch("freqtrade.exchange.Exchange.refresh_latest_ohlcv", refresh_mock)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
|
||||||
|
history.load_data(datadir=None, ticker_interval='5m',
|
||||||
|
pairs=['UNITTEST/BTC', 'UNITTEST2/BTC'],
|
||||||
|
live=True,
|
||||||
|
exchange=exchange)
|
||||||
|
assert refresh_mock.call_count == 1
|
||||||
|
assert len(refresh_mock.call_args_list[0][0][0]) == 2
|
||||||
|
assert log_has('Live: Downloading data for all defined pairs ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_data_live_noexchange(default_conf, mocker, caplog) -> None:
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException,
|
||||||
|
match=r'Exchange needs to be initialized when using live data.'):
|
||||||
|
history.load_data(datadir=None, ticker_interval='5m',
|
||||||
|
pairs=['UNITTEST/BTC', 'UNITTEST2/BTC'],
|
||||||
|
exchange=None,
|
||||||
|
live=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_testdata_path() -> None:
|
def test_testdata_path() -> None:
|
||||||
assert str(Path('freqtrade') / 'tests' / 'testdata') in str(make_testdata_path(None))
|
assert str(Path('freqtrade') / 'tests' / 'testdata') in str(make_testdata_path(None))
|
||||||
|
|
||||||
|
@ -287,7 +333,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_history',
|
||||||
side_effect=BaseException('File Error'))
|
side_effect=Exception('File Error'))
|
||||||
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
|
||||||
|
@ -302,7 +348,11 @@ def test_download_backtesting_data_exception(ticker_history, mocker, caplog, def
|
||||||
# clean files freshly downloaded
|
# clean files freshly downloaded
|
||||||
_clean_test_file(file1_1)
|
_clean_test_file(file1_1)
|
||||||
_clean_test_file(file1_5)
|
_clean_test_file(file1_5)
|
||||||
assert log_has('Failed to download the pair: "MEME/BTC", Interval: 1m', caplog.record_tuples)
|
assert log_has(
|
||||||
|
'Failed to download history data for pair: "MEME/BTC", interval: 1m. '
|
||||||
|
'Error: File Error',
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_load_tickerdata_file() -> None:
|
def test_load_tickerdata_file() -> None:
|
||||||
|
@ -473,3 +523,62 @@ def test_file_dump_json_tofile() -> None:
|
||||||
|
|
||||||
# Remove the file
|
# Remove the file
|
||||||
_clean_test_file(file)
|
_clean_test_file(file)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_timeframe(default_conf, mocker) -> None:
|
||||||
|
patch_exchange(mocker)
|
||||||
|
strategy = DefaultStrategy(default_conf)
|
||||||
|
|
||||||
|
data = strategy.tickerdata_to_dataframe(
|
||||||
|
history.load_data(
|
||||||
|
datadir=None,
|
||||||
|
ticker_interval='1m',
|
||||||
|
pairs=['UNITTEST/BTC']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
min_date, max_date = history.get_timeframe(data)
|
||||||
|
assert min_date.isoformat() == '2017-11-04T23:02:00+00:00'
|
||||||
|
assert max_date.isoformat() == '2017-11-14T22:58:00+00:00'
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_backtest_data_warn(default_conf, mocker, caplog) -> None:
|
||||||
|
patch_exchange(mocker)
|
||||||
|
strategy = DefaultStrategy(default_conf)
|
||||||
|
|
||||||
|
data = strategy.tickerdata_to_dataframe(
|
||||||
|
history.load_data(
|
||||||
|
datadir=None,
|
||||||
|
ticker_interval='1m',
|
||||||
|
pairs=['UNITTEST/BTC'],
|
||||||
|
fill_up_missing=False
|
||||||
|
)
|
||||||
|
)
|
||||||
|
min_date, max_date = history.get_timeframe(data)
|
||||||
|
caplog.clear()
|
||||||
|
assert history.validate_backtest_data(data['UNITTEST/BTC'], 'UNITTEST/BTC',
|
||||||
|
min_date, max_date, timeframe_to_minutes('1m'))
|
||||||
|
assert len(caplog.record_tuples) == 1
|
||||||
|
assert log_has(
|
||||||
|
"UNITTEST/BTC has missing frames: expected 14396, got 13680, that's 716 missing values",
|
||||||
|
caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_backtest_data(default_conf, mocker, caplog) -> None:
|
||||||
|
patch_exchange(mocker)
|
||||||
|
strategy = DefaultStrategy(default_conf)
|
||||||
|
|
||||||
|
timerange = TimeRange('index', 'index', 200, 250)
|
||||||
|
data = strategy.tickerdata_to_dataframe(
|
||||||
|
history.load_data(
|
||||||
|
datadir=None,
|
||||||
|
ticker_interval='5m',
|
||||||
|
pairs=['UNITTEST/BTC'],
|
||||||
|
timerange=timerange
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
min_date, max_date = history.get_timeframe(data)
|
||||||
|
caplog.clear()
|
||||||
|
assert not history.validate_backtest_data(data['UNITTEST/BTC'], 'UNITTEST/BTC',
|
||||||
|
min_date, max_date, timeframe_to_minutes('5m'))
|
||||||
|
assert len(caplog.record_tuples) == 0
|
||||||
|
|
|
@ -10,10 +10,11 @@ import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
from pandas import DataFrame, to_datetime
|
from pandas import DataFrame, to_datetime
|
||||||
|
|
||||||
|
from freqtrade import OperationalException
|
||||||
from freqtrade.data.converter import parse_ticker_dataframe
|
from freqtrade.data.converter import parse_ticker_dataframe
|
||||||
from freqtrade.edge import Edge, PairInfo
|
from freqtrade.edge import Edge, PairInfo
|
||||||
from freqtrade.strategy.interface import SellType
|
from freqtrade.strategy.interface import SellType
|
||||||
from freqtrade.tests.conftest import get_patched_freqtradebot
|
from freqtrade.tests.conftest import get_patched_freqtradebot, log_has
|
||||||
from freqtrade.tests.optimize import (BTContainer, BTrade,
|
from freqtrade.tests.optimize import (BTContainer, BTrade,
|
||||||
_build_backtest_dataframe,
|
_build_backtest_dataframe,
|
||||||
_get_frame_time_from_offset)
|
_get_frame_time_from_offset)
|
||||||
|
@ -30,7 +31,50 @@ ticker_start_time = arrow.get(2018, 10, 3)
|
||||||
ticker_interval_in_minute = 60
|
ticker_interval_in_minute = 60
|
||||||
_ohlc = {'date': 0, 'buy': 1, 'open': 2, 'high': 3, 'low': 4, 'close': 5, 'sell': 6, 'volume': 7}
|
_ohlc = {'date': 0, 'buy': 1, 'open': 2, 'high': 3, 'low': 4, 'close': 5, 'sell': 6, 'volume': 7}
|
||||||
|
|
||||||
|
# Helpers for this test file
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_ohlc(buy_ohlc_sell_matrice):
|
||||||
|
for index, ohlc in enumerate(buy_ohlc_sell_matrice):
|
||||||
|
# if not high < open < low or not high < close < low
|
||||||
|
if not ohlc[3] >= ohlc[2] >= ohlc[4] or not ohlc[3] >= ohlc[5] >= ohlc[4]:
|
||||||
|
raise Exception('Line ' + str(index + 1) + ' of ohlc has invalid values!')
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _build_dataframe(buy_ohlc_sell_matrice):
|
||||||
|
_validate_ohlc(buy_ohlc_sell_matrice)
|
||||||
|
tickers = []
|
||||||
|
for ohlc in buy_ohlc_sell_matrice:
|
||||||
|
ticker = {
|
||||||
|
'date': ticker_start_time.shift(
|
||||||
|
minutes=(
|
||||||
|
ohlc[0] *
|
||||||
|
ticker_interval_in_minute)).timestamp *
|
||||||
|
1000,
|
||||||
|
'buy': ohlc[1],
|
||||||
|
'open': ohlc[2],
|
||||||
|
'high': ohlc[3],
|
||||||
|
'low': ohlc[4],
|
||||||
|
'close': ohlc[5],
|
||||||
|
'sell': ohlc[6]}
|
||||||
|
tickers.append(ticker)
|
||||||
|
|
||||||
|
frame = DataFrame(tickers)
|
||||||
|
frame['date'] = to_datetime(frame['date'],
|
||||||
|
unit='ms',
|
||||||
|
utc=True,
|
||||||
|
infer_datetime_format=True)
|
||||||
|
|
||||||
|
return frame
|
||||||
|
|
||||||
|
|
||||||
|
def _time_on_candle(number):
|
||||||
|
return np.datetime64(ticker_start_time.shift(
|
||||||
|
minutes=(number * ticker_interval_in_minute)).timestamp * 1000, 'ms')
|
||||||
|
|
||||||
|
|
||||||
|
# End helper functions
|
||||||
# Open trade should be removed from the end
|
# Open trade should be removed from the end
|
||||||
tc0 = BTContainer(data=[
|
tc0 = BTContainer(data=[
|
||||||
# D O H L C V B S
|
# D O H L C V B S
|
||||||
|
@ -203,46 +247,6 @@ def test_nonexisting_stake_amount(mocker, edge_conf):
|
||||||
assert edge.stake_amount('N/O', 1, 2, 1) == 0.15
|
assert edge.stake_amount('N/O', 1, 2, 1) == 0.15
|
||||||
|
|
||||||
|
|
||||||
def _validate_ohlc(buy_ohlc_sell_matrice):
|
|
||||||
for index, ohlc in enumerate(buy_ohlc_sell_matrice):
|
|
||||||
# if not high < open < low or not high < close < low
|
|
||||||
if not ohlc[3] >= ohlc[2] >= ohlc[4] or not ohlc[3] >= ohlc[5] >= ohlc[4]:
|
|
||||||
raise Exception('Line ' + str(index + 1) + ' of ohlc has invalid values!')
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def _build_dataframe(buy_ohlc_sell_matrice):
|
|
||||||
_validate_ohlc(buy_ohlc_sell_matrice)
|
|
||||||
tickers = []
|
|
||||||
for ohlc in buy_ohlc_sell_matrice:
|
|
||||||
ticker = {
|
|
||||||
'date': ticker_start_time.shift(
|
|
||||||
minutes=(
|
|
||||||
ohlc[0] *
|
|
||||||
ticker_interval_in_minute)).timestamp *
|
|
||||||
1000,
|
|
||||||
'buy': ohlc[1],
|
|
||||||
'open': ohlc[2],
|
|
||||||
'high': ohlc[3],
|
|
||||||
'low': ohlc[4],
|
|
||||||
'close': ohlc[5],
|
|
||||||
'sell': ohlc[6]}
|
|
||||||
tickers.append(ticker)
|
|
||||||
|
|
||||||
frame = DataFrame(tickers)
|
|
||||||
frame['date'] = to_datetime(frame['date'],
|
|
||||||
unit='ms',
|
|
||||||
utc=True,
|
|
||||||
infer_datetime_format=True)
|
|
||||||
|
|
||||||
return frame
|
|
||||||
|
|
||||||
|
|
||||||
def _time_on_candle(number):
|
|
||||||
return np.datetime64(ticker_start_time.shift(
|
|
||||||
minutes=(number * ticker_interval_in_minute)).timestamp * 1000, 'ms')
|
|
||||||
|
|
||||||
|
|
||||||
def test_edge_heartbeat_calculate(mocker, edge_conf):
|
def test_edge_heartbeat_calculate(mocker, edge_conf):
|
||||||
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||||
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||||
|
@ -259,7 +263,7 @@ def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=Fals
|
||||||
hz = 0.1
|
hz = 0.1
|
||||||
base = 0.001
|
base = 0.001
|
||||||
|
|
||||||
ETHBTC = [
|
NEOBTC = [
|
||||||
[
|
[
|
||||||
ticker_start_time.shift(minutes=(x * ticker_interval_in_minute)).timestamp * 1000,
|
ticker_start_time.shift(minutes=(x * ticker_interval_in_minute)).timestamp * 1000,
|
||||||
math.sin(x * hz) / 1000 + base,
|
math.sin(x * hz) / 1000 + base,
|
||||||
|
@ -281,8 +285,8 @@ def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=Fals
|
||||||
123.45
|
123.45
|
||||||
] for x in range(0, 500)]
|
] for x in range(0, 500)]
|
||||||
|
|
||||||
pairdata = {'NEO/BTC': parse_ticker_dataframe(ETHBTC, '1h', fill_missing=True),
|
pairdata = {'NEO/BTC': parse_ticker_dataframe(NEOBTC, '1h', pair="NEO/BTC", fill_missing=True),
|
||||||
'LTC/BTC': parse_ticker_dataframe(LTCBTC, '1h', fill_missing=True)}
|
'LTC/BTC': parse_ticker_dataframe(LTCBTC, '1h', pair="LTC/BTC", fill_missing=True)}
|
||||||
return pairdata
|
return pairdata
|
||||||
|
|
||||||
|
|
||||||
|
@ -298,6 +302,40 @@ def test_edge_process_downloaded_data(mocker, edge_conf):
|
||||||
assert edge._last_updated <= arrow.utcnow().timestamp + 2
|
assert edge._last_updated <= arrow.utcnow().timestamp + 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_process_no_data(mocker, edge_conf, caplog):
|
||||||
|
edge_conf['datadir'] = None
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001))
|
||||||
|
mocker.patch('freqtrade.data.history.load_data', MagicMock(return_value={}))
|
||||||
|
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||||
|
|
||||||
|
assert not edge.calculate()
|
||||||
|
assert len(edge._cached_pairs) == 0
|
||||||
|
assert log_has("No data found. Edge is stopped ...", caplog.record_tuples)
|
||||||
|
assert edge._last_updated == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_process_no_trades(mocker, edge_conf, caplog):
|
||||||
|
edge_conf['datadir'] = None
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001))
|
||||||
|
mocker.patch('freqtrade.data.history.load_data', mocked_load_data)
|
||||||
|
# Return empty
|
||||||
|
mocker.patch('freqtrade.edge.Edge._find_trades_for_stoploss_range', MagicMock(return_value=[]))
|
||||||
|
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||||
|
|
||||||
|
assert not edge.calculate()
|
||||||
|
assert len(edge._cached_pairs) == 0
|
||||||
|
assert log_has("No trades found.", caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_init_error(mocker, edge_conf,):
|
||||||
|
edge_conf['stake_amount'] = 0.5
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001))
|
||||||
|
with pytest.raises(OperationalException, match='Edge works only with unlimited stake amount'):
|
||||||
|
get_patched_freqtradebot(mocker, edge_conf)
|
||||||
|
|
||||||
|
|
||||||
def test_process_expectancy(mocker, edge_conf):
|
def test_process_expectancy(mocker, edge_conf):
|
||||||
edge_conf['edge']['min_trade_number'] = 2
|
edge_conf['edge']['min_trade_number'] = 2
|
||||||
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||||
|
@ -360,3 +398,11 @@ def test_process_expectancy(mocker, edge_conf):
|
||||||
assert round(final['TEST/BTC'].risk_reward_ratio, 10) == 306.5384615384
|
assert round(final['TEST/BTC'].risk_reward_ratio, 10) == 306.5384615384
|
||||||
assert round(final['TEST/BTC'].required_risk_reward, 10) == 2.0
|
assert round(final['TEST/BTC'].required_risk_reward, 10) == 2.0
|
||||||
assert round(final['TEST/BTC'].expectancy, 10) == 101.5128205128
|
assert round(final['TEST/BTC'].expectancy, 10) == 101.5128205128
|
||||||
|
|
||||||
|
# Pop last item so no trade is profitable
|
||||||
|
trades.pop()
|
||||||
|
trades_df = DataFrame(trades)
|
||||||
|
trades_df = edge._fill_calculable_fields(trades_df)
|
||||||
|
final = edge._process_expectancy(trades_df)
|
||||||
|
assert len(final) == 0
|
||||||
|
assert isinstance(final, dict)
|
||||||
|
|
|
@ -124,14 +124,14 @@ def test_exchange_resolver(default_conf, mocker, caplog):
|
||||||
caplog.record_tuples)
|
caplog.record_tuples)
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
|
||||||
exchange = ExchangeResolver('Kraken', default_conf).exchange
|
exchange = ExchangeResolver('kraken', default_conf).exchange
|
||||||
assert isinstance(exchange, Exchange)
|
assert isinstance(exchange, Exchange)
|
||||||
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.record_tuples)
|
||||||
|
|
||||||
exchange = ExchangeResolver('Binance', default_conf).exchange
|
exchange = ExchangeResolver('binance', default_conf).exchange
|
||||||
assert isinstance(exchange, Exchange)
|
assert isinstance(exchange, Exchange)
|
||||||
assert isinstance(exchange, Binance)
|
assert isinstance(exchange, Binance)
|
||||||
assert not isinstance(exchange, Kraken)
|
assert not isinstance(exchange, Kraken)
|
||||||
|
@ -301,6 +301,20 @@ def test__reload_markets(default_conf, mocker, caplog):
|
||||||
assert log_has('Performing scheduled market reload..', caplog.record_tuples)
|
assert log_has('Performing scheduled market reload..', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test__reload_markets_exception(default_conf, mocker, caplog):
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.load_markets = MagicMock(side_effect=ccxt.NetworkError)
|
||||||
|
default_conf['exchange']['markets_refresh_interval'] = 10
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance")
|
||||||
|
|
||||||
|
# less than 10 minutes have passed, no reload
|
||||||
|
exchange._reload_markets()
|
||||||
|
assert exchange._last_markets_refresh == 0
|
||||||
|
assert log_has_re(r"Could not reload markets.*", caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
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={
|
||||||
|
@ -1002,7 +1016,7 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
|
||||||
exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m')])
|
exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m')])
|
||||||
|
|
||||||
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 {pairs[0][0]}, {pairs[0][1]} ...",
|
assert log_has(f"Using cached ohlcv data for pair {pairs[0][0]}, interval {pairs[0][1]} ...",
|
||||||
caplog.record_tuples)
|
caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1421,3 +1435,30 @@ def test_stoploss_limit_order_dry_run(default_conf, mocker):
|
||||||
assert order['type'] == order_type
|
assert order['type'] == order_type
|
||||||
assert order['price'] == 220
|
assert order['price'] == 220
|
||||||
assert order['amount'] == 1
|
assert order['amount'] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_ft_has_dict(default_conf, mocker):
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=MagicMock()))
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
|
ex = Exchange(default_conf)
|
||||||
|
assert ex._ft_has == Exchange._ft_has_default
|
||||||
|
|
||||||
|
ex = Kraken(default_conf)
|
||||||
|
assert ex._ft_has == Exchange._ft_has_default
|
||||||
|
|
||||||
|
# Binance defines different values
|
||||||
|
ex = Binance(default_conf)
|
||||||
|
assert ex._ft_has != Exchange._ft_has_default
|
||||||
|
assert ex._ft_has['stoploss_on_exchange']
|
||||||
|
assert ex._ft_has['order_time_in_force'] == ['gtc', 'fok', 'ioc']
|
||||||
|
|
||||||
|
conf = copy.deepcopy(default_conf)
|
||||||
|
conf['exchange']['_ft_has_params'] = {"DeadBeef": 20,
|
||||||
|
"stoploss_on_exchange": False}
|
||||||
|
# Use settings from configuration (overriding stoploss_on_exchange)
|
||||||
|
ex = Binance(conf)
|
||||||
|
assert ex._ft_has != Exchange._ft_has_default
|
||||||
|
assert not ex._ft_has['stoploss_on_exchange']
|
||||||
|
assert ex._ft_has['DeadBeef'] == 20
|
||||||
|
|
|
@ -3,7 +3,7 @@ from typing import NamedTuple, List
|
||||||
import arrow
|
import arrow
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.misc import timeframe_to_minutes
|
from freqtrade.exchange import timeframe_to_minutes
|
||||||
from freqtrade.strategy.interface import SellType
|
from freqtrade.strategy.interface import SellType
|
||||||
|
|
||||||
ticker_start_time = arrow.get(2018, 10, 3)
|
ticker_start_time = arrow.get(2018, 10, 3)
|
||||||
|
@ -29,6 +29,10 @@ class BTContainer(NamedTuple):
|
||||||
trades: List[BTrade]
|
trades: List[BTrade]
|
||||||
profit_perc: float
|
profit_perc: float
|
||||||
trailing_stop: bool = False
|
trailing_stop: bool = False
|
||||||
|
trailing_only_offset_is_reached: bool = False
|
||||||
|
trailing_stop_positive: float = None
|
||||||
|
trailing_stop_positive_offset: float = 0.0
|
||||||
|
use_sell_signal: bool = False
|
||||||
|
|
||||||
|
|
||||||
def _get_frame_time_from_offset(offset):
|
def _get_frame_time_from_offset(offset):
|
||||||
|
|
|
@ -2,17 +2,32 @@
|
||||||
import logging
|
import logging
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
from pandas import DataFrame
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.data.history import get_timeframe
|
||||||
from freqtrade.optimize import get_timeframe
|
|
||||||
from freqtrade.optimize.backtesting import Backtesting
|
from freqtrade.optimize.backtesting import Backtesting
|
||||||
from freqtrade.strategy.interface import SellType
|
from freqtrade.strategy.interface import SellType
|
||||||
from freqtrade.tests.optimize import (BTrade, BTContainer, _build_backtest_dataframe,
|
|
||||||
_get_frame_time_from_offset, tests_ticker_interval)
|
|
||||||
from freqtrade.tests.conftest import patch_exchange
|
from freqtrade.tests.conftest import patch_exchange
|
||||||
|
from freqtrade.tests.optimize import (BTContainer, BTrade,
|
||||||
|
_build_backtest_dataframe,
|
||||||
|
_get_frame_time_from_offset,
|
||||||
|
tests_ticker_interval)
|
||||||
|
|
||||||
|
# Test 0 Sell signal sell
|
||||||
|
# Test with Stop-loss at 1%
|
||||||
|
# TC0: Sell signal in candle 3
|
||||||
|
tc0 = BTContainer(data=[
|
||||||
|
# D O H L C V B S
|
||||||
|
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||||
|
[1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle)
|
||||||
|
[2, 4987, 5012, 4986, 4600, 6172, 0, 0], # exit with stoploss hit
|
||||||
|
[3, 5010, 5000, 4980, 5010, 6172, 0, 1],
|
||||||
|
[4, 5010, 4987, 4977, 4995, 6172, 0, 0],
|
||||||
|
[5, 4995, 4995, 4995, 4950, 6172, 0, 0]],
|
||||||
|
stop_loss=-0.01, roi=1, profit_perc=0.002, use_sell_signal=True,
|
||||||
|
trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4)]
|
||||||
|
)
|
||||||
|
|
||||||
# Test 1 Minus 8% Close
|
# Test 1 Minus 8% Close
|
||||||
# Test with Stop-loss at 1%
|
# Test with Stop-loss at 1%
|
||||||
|
@ -146,7 +161,7 @@ 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
|
# Candle Data for test 9
|
||||||
# Set stop-loss at 10%, ROI at 10% (should not apply)
|
# Set stop-loss at 10%, ROI at 10% (should not apply)
|
||||||
# TC9: Trailing stoploss - stoploss should be adjusted candle 2
|
# 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],
|
||||||
|
@ -158,7 +173,59 @@ 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
|
||||||
|
# without applying trailing_stop_positive since stoploss_offset is at 10%.
|
||||||
|
# Set stop-loss at 10%, ROI at 10% (should not apply)
|
||||||
|
# TC10: Trailing stoploss - stoploss should be adjusted candle 2
|
||||||
|
tc10 = BTContainer(data=[
|
||||||
|
# D O H L C V B S
|
||||||
|
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
||||||
|
[1, 5000, 5050, 4950, 5100, 6172, 0, 0],
|
||||||
|
[2, 5100, 5251, 5100, 5100, 6172, 0, 0],
|
||||||
|
[3, 4850, 5050, 4650, 4750, 6172, 0, 0],
|
||||||
|
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
|
||||||
|
stop_loss=-0.10, roi=0.10, profit_perc=-0.1, trailing_stop=True,
|
||||||
|
trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.10,
|
||||||
|
trailing_stop_positive=0.03,
|
||||||
|
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
|
||||||
|
# applying a positive trailing stop of 3% since stop_positive_offset is reached.
|
||||||
|
# Set stop-loss at 10%, ROI at 10% (should not apply)
|
||||||
|
# TC11: Trailing stoploss - stoploss should be adjusted candle 2,
|
||||||
|
tc11 = BTContainer(data=[
|
||||||
|
# D O H L C V B S
|
||||||
|
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
||||||
|
[1, 5000, 5050, 4950, 5100, 6172, 0, 0],
|
||||||
|
[2, 5100, 5251, 5100, 5100, 6172, 0, 0],
|
||||||
|
[3, 4850, 5050, 4650, 4750, 6172, 0, 0],
|
||||||
|
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
|
||||||
|
stop_loss=-0.10, roi=0.10, profit_perc=0.019, trailing_stop=True,
|
||||||
|
trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05,
|
||||||
|
trailing_stop_positive=0.03,
|
||||||
|
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
|
||||||
|
# applying a positive trailing stop of 3% since stop_positive_offset is reached.
|
||||||
|
# Set stop-loss at 10%, ROI at 10% (should not apply)
|
||||||
|
# TC12: Trailing stoploss - stoploss should be adjusted candle 2,
|
||||||
|
tc12 = BTContainer(data=[
|
||||||
|
# D O H L C V B S
|
||||||
|
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
||||||
|
[1, 5000, 5050, 4950, 5100, 6172, 0, 0],
|
||||||
|
[2, 5100, 5251, 4650, 5100, 6172, 0, 0],
|
||||||
|
[3, 4850, 5050, 4650, 4750, 6172, 0, 0],
|
||||||
|
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
|
||||||
|
stop_loss=-0.10, roi=0.10, profit_perc=0.019, trailing_stop=True,
|
||||||
|
trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05,
|
||||||
|
trailing_stop_positive=0.03,
|
||||||
|
trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=2)]
|
||||||
|
)
|
||||||
|
|
||||||
TESTS = [
|
TESTS = [
|
||||||
|
tc0,
|
||||||
tc1,
|
tc1,
|
||||||
tc2,
|
tc2,
|
||||||
tc3,
|
tc3,
|
||||||
|
@ -168,6 +235,9 @@ TESTS = [
|
||||||
tc7,
|
tc7,
|
||||||
tc8,
|
tc8,
|
||||||
tc9,
|
tc9,
|
||||||
|
tc10,
|
||||||
|
tc11,
|
||||||
|
tc12,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -180,6 +250,13 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
|
||||||
default_conf["minimal_roi"] = {"0": data.roi}
|
default_conf["minimal_roi"] = {"0": data.roi}
|
||||||
default_conf["ticker_interval"] = tests_ticker_interval
|
default_conf["ticker_interval"] = tests_ticker_interval
|
||||||
default_conf["trailing_stop"] = data.trailing_stop
|
default_conf["trailing_stop"] = data.trailing_stop
|
||||||
|
default_conf["trailing_only_offset_is_reached"] = data.trailing_only_offset_is_reached
|
||||||
|
# Only add this to configuration If it's necessary
|
||||||
|
if data.trailing_stop_positive:
|
||||||
|
default_conf["trailing_stop_positive"] = data.trailing_stop_positive
|
||||||
|
default_conf["trailing_stop_positive_offset"] = data.trailing_stop_positive_offset
|
||||||
|
default_conf["experimental"] = {"use_sell_signal": data.use_sell_signal}
|
||||||
|
|
||||||
mocker.patch("freqtrade.exchange.Exchange.get_fee", MagicMock(return_value=0.0))
|
mocker.patch("freqtrade.exchange.Exchange.get_fee", MagicMock(return_value=0.0))
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
frame = _build_backtest_dataframe(data.data)
|
frame = _build_backtest_dataframe(data.data)
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
import json
|
import json
|
||||||
import math
|
import math
|
||||||
import random
|
import random
|
||||||
from typing import List
|
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
@ -12,28 +11,24 @@ import pytest
|
||||||
from arrow import Arrow
|
from arrow import Arrow
|
||||||
|
|
||||||
from freqtrade import DependencyException, constants
|
from freqtrade import DependencyException, constants
|
||||||
from freqtrade.arguments import Arguments, TimeRange
|
from freqtrade.arguments 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
|
||||||
from freqtrade.data.converter import parse_ticker_dataframe
|
from freqtrade.data.converter import parse_ticker_dataframe
|
||||||
from freqtrade.data.dataprovider import DataProvider
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
from freqtrade.optimize import get_timeframe
|
from freqtrade.data.history import get_timeframe
|
||||||
from freqtrade.optimize.backtesting import (Backtesting, setup_configuration,
|
from freqtrade.optimize import setup_configuration, start_backtesting
|
||||||
start)
|
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 log_has, patch_exchange
|
from freqtrade.tests.conftest import get_args, log_has, log_has_re, patch_exchange
|
||||||
|
|
||||||
|
|
||||||
def get_args(args) -> List[str]:
|
|
||||||
return Arguments(args, '').get_parsed_arg()
|
|
||||||
|
|
||||||
|
|
||||||
def trim_dictlist(dict_list, num):
|
def trim_dictlist(dict_list, num):
|
||||||
new = {}
|
new = {}
|
||||||
for pair, pair_data in dict_list.items():
|
for pair, pair_data in dict_list.items():
|
||||||
new[pair] = pair_data[num:]
|
new[pair] = pair_data[num:].reset_index()
|
||||||
return new
|
return new
|
||||||
|
|
||||||
|
|
||||||
|
@ -78,7 +73,8 @@ def load_data_test(what):
|
||||||
pair[x][5] # Keep old volume
|
pair[x][5] # Keep old volume
|
||||||
] for x in range(0, datalen)
|
] for x in range(0, datalen)
|
||||||
]
|
]
|
||||||
return {'UNITTEST/BTC': parse_ticker_dataframe(data, '1m', fill_missing=True)}
|
return {'UNITTEST/BTC': parse_ticker_dataframe(data, '1m', pair="UNITTEST/BTC",
|
||||||
|
fill_missing=True)}
|
||||||
|
|
||||||
|
|
||||||
def simple_backtest(config, contour, num_results, mocker) -> None:
|
def simple_backtest(config, contour, num_results, mocker) -> None:
|
||||||
|
@ -105,9 +101,10 @@ def simple_backtest(config, contour, num_results, mocker) -> None:
|
||||||
|
|
||||||
|
|
||||||
def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=False,
|
def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=False,
|
||||||
timerange=None, exchange=None):
|
timerange=None, exchange=None, live=False):
|
||||||
tickerdata = history.load_tickerdata_file(datadir, 'UNITTEST/BTC', '1m', timerange=timerange)
|
tickerdata = history.load_tickerdata_file(datadir, 'UNITTEST/BTC', '1m', timerange=timerange)
|
||||||
pairdata = {'UNITTEST/BTC': parse_ticker_dataframe(tickerdata, '1m', fill_missing=True)}
|
pairdata = {'UNITTEST/BTC': parse_ticker_dataframe(tickerdata, '1m', pair="UNITTEST/BTC",
|
||||||
|
fill_missing=True)}
|
||||||
return pairdata
|
return pairdata
|
||||||
|
|
||||||
|
|
||||||
|
@ -178,7 +175,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
|
||||||
'backtesting'
|
'backtesting'
|
||||||
]
|
]
|
||||||
|
|
||||||
config = setup_configuration(get_args(args))
|
config = setup_configuration(get_args(args), RunMode.BACKTEST)
|
||||||
assert 'max_open_trades' in config
|
assert 'max_open_trades' in config
|
||||||
assert 'stake_currency' in config
|
assert 'stake_currency' in config
|
||||||
assert 'stake_amount' in config
|
assert 'stake_amount' in config
|
||||||
|
@ -190,7 +187,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
|
||||||
caplog.record_tuples
|
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_re('Parameter -i/--ticker-interval detected .*', caplog.record_tuples)
|
||||||
|
|
||||||
assert 'live' not in config
|
assert 'live' not in config
|
||||||
assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
||||||
|
@ -228,7 +225,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) ->
|
||||||
'--export-filename', 'foo_bar.json'
|
'--export-filename', 'foo_bar.json'
|
||||||
]
|
]
|
||||||
|
|
||||||
config = setup_configuration(get_args(args))
|
config = setup_configuration(get_args(args), RunMode.BACKTEST)
|
||||||
assert 'max_open_trades' in config
|
assert 'max_open_trades' in config
|
||||||
assert 'stake_currency' in config
|
assert 'stake_currency' in config
|
||||||
assert 'stake_amount' in config
|
assert 'stake_amount' in config
|
||||||
|
@ -242,11 +239,8 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) ->
|
||||||
caplog.record_tuples
|
caplog.record_tuples
|
||||||
)
|
)
|
||||||
assert 'ticker_interval' in config
|
assert 'ticker_interval' in config
|
||||||
assert log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
|
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
||||||
assert log_has(
|
caplog.record_tuples)
|
||||||
'Using ticker_interval: 1m ...',
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
|
|
||||||
assert 'live' in config
|
assert 'live' in config
|
||||||
assert log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
assert log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
||||||
|
@ -260,6 +254,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, 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.record_tuples)
|
||||||
|
|
||||||
assert 'timerange' in config
|
assert 'timerange' in config
|
||||||
assert log_has(
|
assert log_has(
|
||||||
'Parameter --timerange detected: {} ...'.format(config['timerange']),
|
'Parameter --timerange detected: {} ...'.format(config['timerange']),
|
||||||
|
@ -292,7 +287,7 @@ def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog
|
||||||
]
|
]
|
||||||
|
|
||||||
with pytest.raises(DependencyException, match=r'.*stake amount.*'):
|
with pytest.raises(DependencyException, match=r'.*stake amount.*'):
|
||||||
setup_configuration(get_args(args))
|
setup_configuration(get_args(args), RunMode.BACKTEST)
|
||||||
|
|
||||||
|
|
||||||
def test_start(mocker, fee, default_conf, caplog) -> None:
|
def test_start(mocker, fee, default_conf, caplog) -> None:
|
||||||
|
@ -309,7 +304,7 @@ def test_start(mocker, fee, default_conf, caplog) -> None:
|
||||||
'backtesting'
|
'backtesting'
|
||||||
]
|
]
|
||||||
args = get_args(args)
|
args = get_args(args)
|
||||||
start(args)
|
start_backtesting(args)
|
||||||
assert log_has(
|
assert log_has(
|
||||||
'Starting freqtrade in Backtesting mode',
|
'Starting freqtrade in Backtesting mode',
|
||||||
caplog.record_tuples
|
caplog.record_tuples
|
||||||
|
@ -357,7 +352,8 @@ 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)
|
||||||
tick = history.load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
|
tick = history.load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
|
||||||
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', fill_missing=True)}
|
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', pair="UNITTEST/BTC",
|
||||||
|
fill_missing=True)}
|
||||||
|
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
data = backtesting.strategy.tickerdata_to_dataframe(tickerlist)
|
data = backtesting.strategy.tickerdata_to_dataframe(tickerlist)
|
||||||
|
@ -474,7 +470,7 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
|
||||||
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
|
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
|
||||||
|
|
||||||
mocker.patch('freqtrade.data.history.load_data', mocked_load_data)
|
mocker.patch('freqtrade.data.history.load_data', mocked_load_data)
|
||||||
mocker.patch('freqtrade.optimize.get_timeframe', get_timeframe)
|
mocker.patch('freqtrade.data.history.get_timeframe', get_timeframe)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock())
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
|
@ -494,10 +490,9 @@ def test_backtesting_start(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
|
||||||
exists = [
|
exists = [
|
||||||
'Using local backtesting data (using whitelist in given config) ...',
|
|
||||||
'Using stake_currency: BTC ...',
|
'Using stake_currency: BTC ...',
|
||||||
'Using stake_amount: 0.001 ...',
|
'Using stake_amount: 0.001 ...',
|
||||||
'Measuring data from 2017-11-14T21:17:00+00:00 '
|
'Backtesting with data from 2017-11-14T21:17:00+00:00 '
|
||||||
'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:
|
||||||
|
@ -509,7 +504,7 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None:
|
||||||
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
|
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
|
||||||
|
|
||||||
mocker.patch('freqtrade.data.history.load_data', MagicMock(return_value={}))
|
mocker.patch('freqtrade.data.history.load_data', MagicMock(return_value={}))
|
||||||
mocker.patch('freqtrade.optimize.get_timeframe', get_timeframe)
|
mocker.patch('freqtrade.data.history.get_timeframe', get_timeframe)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock())
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
|
@ -710,7 +705,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair):
|
||||||
data = trim_dictlist(data, -500)
|
data = trim_dictlist(data, -500)
|
||||||
|
|
||||||
# Remove data for one pair from the beginning of the data
|
# Remove data for one pair from the beginning of the data
|
||||||
data[pair] = data[pair][tres:]
|
data[pair] = data[pair][tres:].reset_index()
|
||||||
# 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}
|
||||||
default_conf['ticker_interval'] = '5m'
|
default_conf['ticker_interval'] = '5m'
|
||||||
|
@ -849,19 +844,19 @@ def test_backtest_start_live(default_conf, mocker, caplog):
|
||||||
'--disable-max-market-positions'
|
'--disable-max-market-positions'
|
||||||
]
|
]
|
||||||
args = get_args(args)
|
args = get_args(args)
|
||||||
start(args)
|
start_backtesting(args)
|
||||||
# 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 ...',
|
'Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
||||||
'Using ticker_interval: 1m ...',
|
|
||||||
'Parameter -l/--live detected ...',
|
'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 folder: freqtrade/tests/testdata ...',
|
'Using data folder: freqtrade/tests/testdata ...',
|
||||||
'Using stake_currency: BTC ...',
|
'Using stake_currency: BTC ...',
|
||||||
'Using stake_amount: 0.001 ...',
|
'Using stake_amount: 0.001 ...',
|
||||||
'Downloading data for all pairs in whitelist ...',
|
'Live: Downloading data for all defined pairs ...',
|
||||||
'Measuring data from 2017-11-14T19:31:00+00:00 up to 2017-11-14T22:58:00+00:00 (0 days)..',
|
'Backtesting with data from 2017-11-14T19:31:00+00:00 '
|
||||||
|
'up to 2017-11-14T22:58:00+00:00 (0 days)..',
|
||||||
'Parameter --enable-position-stacking detected ...'
|
'Parameter --enable-position-stacking detected ...'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -903,7 +898,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog):
|
||||||
'TestStrategy',
|
'TestStrategy',
|
||||||
]
|
]
|
||||||
args = get_args(args)
|
args = get_args(args)
|
||||||
start(args)
|
start_backtesting(args)
|
||||||
# 2 backtests, 4 tables
|
# 2 backtests, 4 tables
|
||||||
assert backtestmock.call_count == 2
|
assert backtestmock.call_count == 2
|
||||||
assert gen_table_mock.call_count == 4
|
assert gen_table_mock.call_count == 4
|
||||||
|
@ -911,16 +906,16 @@ 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 ...',
|
'Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
||||||
'Using ticker_interval: 1m ...',
|
|
||||||
'Parameter -l/--live detected ...',
|
'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 folder: freqtrade/tests/testdata ...',
|
'Using data folder: freqtrade/tests/testdata ...',
|
||||||
'Using stake_currency: BTC ...',
|
'Using stake_currency: BTC ...',
|
||||||
'Using stake_amount: 0.001 ...',
|
'Using stake_amount: 0.001 ...',
|
||||||
'Downloading data for all pairs in whitelist ...',
|
'Live: Downloading data for all defined pairs ...',
|
||||||
'Measuring data from 2017-11-14T19:31:00+00:00 up to 2017-11-14T22:58:00+00:00 (0 days)..',
|
'Backtesting with data from 2017-11-14T19:31:00+00:00 '
|
||||||
|
'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',
|
||||||
'Running backtesting for Strategy TestStrategy',
|
'Running backtesting for Strategy TestStrategy',
|
||||||
|
|
|
@ -1,18 +1,14 @@
|
||||||
# pragma pylint: disable=missing-docstring, C0103, C0330
|
# pragma pylint: disable=missing-docstring, C0103, C0330
|
||||||
# pragma pylint: disable=protected-access, too-many-lines, invalid-name, too-many-arguments
|
# pragma pylint: disable=protected-access, too-many-lines, invalid-name, too-many-arguments
|
||||||
|
|
||||||
from unittest.mock import MagicMock
|
|
||||||
import json
|
import json
|
||||||
from typing import List
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
from freqtrade.edge import PairInfo
|
from freqtrade.edge import PairInfo
|
||||||
from freqtrade.arguments import Arguments
|
from freqtrade.optimize import setup_configuration, start_edge
|
||||||
from freqtrade.optimize.edge_cli import (EdgeCli, setup_configuration, start)
|
from freqtrade.optimize.edge_cli import EdgeCli
|
||||||
from freqtrade.state import RunMode
|
from freqtrade.state import RunMode
|
||||||
from freqtrade.tests.conftest import log_has, patch_exchange
|
from freqtrade.tests.conftest import get_args, log_has, log_has_re, patch_exchange
|
||||||
|
|
||||||
|
|
||||||
def get_args(args) -> List[str]:
|
|
||||||
return Arguments(args, '').get_parsed_arg()
|
|
||||||
|
|
||||||
|
|
||||||
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
||||||
|
@ -26,8 +22,8 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
|
||||||
'edge'
|
'edge'
|
||||||
]
|
]
|
||||||
|
|
||||||
config = setup_configuration(get_args(args))
|
config = setup_configuration(get_args(args), RunMode.EDGE)
|
||||||
assert config['runmode'] == RunMode.EDGECLI
|
assert config['runmode'] == RunMode.EDGE
|
||||||
|
|
||||||
assert 'max_open_trades' in config
|
assert 'max_open_trades' in config
|
||||||
assert 'stake_currency' in config
|
assert 'stake_currency' in config
|
||||||
|
@ -40,7 +36,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
|
||||||
caplog.record_tuples
|
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_re('Parameter -i/--ticker-interval detected .*', caplog.record_tuples)
|
||||||
|
|
||||||
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.record_tuples)
|
||||||
|
@ -66,24 +62,21 @@ def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> N
|
||||||
'--stoplosses=-0.01,-0.10,-0.001'
|
'--stoplosses=-0.01,-0.10,-0.001'
|
||||||
]
|
]
|
||||||
|
|
||||||
config = setup_configuration(get_args(args))
|
config = setup_configuration(get_args(args), RunMode.EDGE)
|
||||||
assert 'max_open_trades' in config
|
assert 'max_open_trades' in config
|
||||||
assert 'stake_currency' in config
|
assert 'stake_currency' in config
|
||||||
assert 'stake_amount' in config
|
assert 'stake_amount' in config
|
||||||
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 config['runmode'] == RunMode.EDGECLI
|
assert config['runmode'] == RunMode.EDGE
|
||||||
assert log_has(
|
assert log_has(
|
||||||
'Using data folder: {} ...'.format(config['datadir']),
|
'Using data folder: {} ...'.format(config['datadir']),
|
||||||
caplog.record_tuples
|
caplog.record_tuples
|
||||||
)
|
)
|
||||||
assert 'ticker_interval' in config
|
assert 'ticker_interval' in config
|
||||||
assert log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
|
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
||||||
assert log_has(
|
caplog.record_tuples)
|
||||||
'Using ticker_interval: 1m ...',
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
|
|
||||||
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.record_tuples)
|
||||||
|
@ -108,7 +101,7 @@ def test_start(mocker, fee, edge_conf, caplog) -> None:
|
||||||
'edge'
|
'edge'
|
||||||
]
|
]
|
||||||
args = get_args(args)
|
args = get_args(args)
|
||||||
start(args)
|
start_edge(args)
|
||||||
assert log_has(
|
assert log_has(
|
||||||
'Starting freqtrade in Edge mode',
|
'Starting freqtrade in Edge mode',
|
||||||
caplog.record_tuples
|
caplog.record_tuples
|
||||||
|
@ -118,8 +111,10 @@ def test_start(mocker, fee, edge_conf, caplog) -> None:
|
||||||
|
|
||||||
def test_edge_init(mocker, edge_conf) -> None:
|
def test_edge_init(mocker, edge_conf) -> None:
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
|
edge_conf['stake_amount'] = 20
|
||||||
edge_cli = EdgeCli(edge_conf)
|
edge_cli = EdgeCli(edge_conf)
|
||||||
assert edge_cli.config == edge_conf
|
assert edge_cli.config == edge_conf
|
||||||
|
assert edge_cli.config['stake_amount'] == 'unlimited'
|
||||||
assert callable(edge_cli.edge.calculate)
|
assert callable(edge_cli.edge.calculate)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,22 @@
|
||||||
# pragma pylint: disable=missing-docstring,W0212,C0103
|
# pragma pylint: disable=missing-docstring,W0212,C0103
|
||||||
from datetime import datetime
|
import json
|
||||||
import os
|
import os
|
||||||
|
from datetime import datetime
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
from filelock import Timeout
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from freqtrade import DependencyException
|
||||||
from freqtrade.data.converter import parse_ticker_dataframe
|
from freqtrade.data.converter import parse_ticker_dataframe
|
||||||
from freqtrade.data.history import load_tickerdata_file
|
from freqtrade.data.history import load_tickerdata_file
|
||||||
from freqtrade.optimize.hyperopt import Hyperopt, start
|
|
||||||
from freqtrade.optimize.default_hyperopt import DefaultHyperOpts
|
from freqtrade.optimize.default_hyperopt import DefaultHyperOpts
|
||||||
from freqtrade.resolvers import StrategyResolver, HyperOptResolver
|
from freqtrade.optimize.hyperopt import Hyperopt, HYPEROPT_LOCKFILE
|
||||||
from freqtrade.tests.conftest import log_has, patch_exchange
|
from freqtrade.optimize import setup_configuration, start_hyperopt
|
||||||
from freqtrade.tests.optimize.test_backtesting import get_args
|
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver
|
||||||
|
from freqtrade.state import RunMode
|
||||||
|
from freqtrade.tests.conftest import get_args, log_has, log_has_re, patch_exchange
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function')
|
@pytest.fixture(scope='function')
|
||||||
|
@ -39,6 +43,110 @@ def create_trials(mocker, hyperopt) -> None:
|
||||||
return [{'loss': 1, 'result': 'foo', 'params': {}}]
|
return [{'loss': 1, 'result': 'foo', 'params': {}}]
|
||||||
|
|
||||||
|
|
||||||
|
def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(default_conf)
|
||||||
|
))
|
||||||
|
|
||||||
|
args = [
|
||||||
|
'--config', 'config.json',
|
||||||
|
'hyperopt'
|
||||||
|
]
|
||||||
|
|
||||||
|
config = setup_configuration(get_args(args), RunMode.HYPEROPT)
|
||||||
|
assert 'max_open_trades' in config
|
||||||
|
assert 'stake_currency' in config
|
||||||
|
assert 'stake_amount' in config
|
||||||
|
assert 'exchange' in config
|
||||||
|
assert 'pair_whitelist' in config['exchange']
|
||||||
|
assert 'datadir' in config
|
||||||
|
assert log_has(
|
||||||
|
'Using data folder: {} ...'.format(config['datadir']),
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
assert 'ticker_interval' in config
|
||||||
|
assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'live' not in config
|
||||||
|
assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'position_stacking' not in config
|
||||||
|
assert not log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'refresh_pairs' not in config
|
||||||
|
assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'timerange' not in config
|
||||||
|
assert 'runmode' in config
|
||||||
|
assert config['runmode'] == RunMode.HYPEROPT
|
||||||
|
|
||||||
|
|
||||||
|
def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplog) -> None:
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(default_conf)
|
||||||
|
))
|
||||||
|
mocker.patch('freqtrade.configuration.Configuration._create_datadir', lambda s, c, x: x)
|
||||||
|
|
||||||
|
args = [
|
||||||
|
'--config', 'config.json',
|
||||||
|
'--datadir', '/foo/bar',
|
||||||
|
'hyperopt',
|
||||||
|
'--ticker-interval', '1m',
|
||||||
|
'--timerange', ':100',
|
||||||
|
'--refresh-pairs-cached',
|
||||||
|
'--enable-position-stacking',
|
||||||
|
'--disable-max-market-positions',
|
||||||
|
'--epochs', '1000',
|
||||||
|
'--spaces', 'all',
|
||||||
|
'--print-all'
|
||||||
|
]
|
||||||
|
|
||||||
|
config = setup_configuration(get_args(args), RunMode.HYPEROPT)
|
||||||
|
assert 'max_open_trades' in config
|
||||||
|
assert 'stake_currency' in config
|
||||||
|
assert 'stake_amount' in config
|
||||||
|
assert 'exchange' in config
|
||||||
|
assert 'pair_whitelist' in config['exchange']
|
||||||
|
assert 'datadir' in config
|
||||||
|
assert config['runmode'] == RunMode.HYPEROPT
|
||||||
|
|
||||||
|
assert log_has(
|
||||||
|
'Using data folder: {} ...'.format(config['datadir']),
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
assert 'ticker_interval' in config
|
||||||
|
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
||||||
|
caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'position_stacking' in config
|
||||||
|
assert log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'use_max_market_positions' in config
|
||||||
|
assert log_has('Parameter --disable-max-market-positions detected ...', caplog.record_tuples)
|
||||||
|
assert log_has('max_open_trades set to unlimited ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'refresh_pairs' in config
|
||||||
|
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'timerange' in config
|
||||||
|
assert log_has(
|
||||||
|
'Parameter --timerange detected: {} ...'.format(config['timerange']),
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
|
assert 'epochs' in config
|
||||||
|
assert log_has('Parameter --epochs detected ... Will run Hyperopt with for 1000 epochs ...',
|
||||||
|
caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'spaces' in config
|
||||||
|
assert log_has(
|
||||||
|
'Parameter -s/--spaces detected: {}'.format(config['spaces']),
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
assert 'print_all' in config
|
||||||
|
assert log_has('Parameter --print-all detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_hyperoptresolver(mocker, default_conf, caplog) -> None:
|
def test_hyperoptresolver(mocker, default_conf, caplog) -> None:
|
||||||
|
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
|
@ -59,6 +167,7 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None:
|
||||||
"Using populate_sell_trend from DefaultStrategy.", caplog.record_tuples)
|
"Using populate_sell_trend from DefaultStrategy.", caplog.record_tuples)
|
||||||
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.record_tuples)
|
||||||
|
assert hasattr(x, "ticker_interval")
|
||||||
|
|
||||||
|
|
||||||
def test_start(mocker, default_conf, caplog) -> None:
|
def test_start(mocker, default_conf, caplog) -> None:
|
||||||
|
@ -72,13 +181,11 @@ def test_start(mocker, default_conf, caplog) -> None:
|
||||||
|
|
||||||
args = [
|
args = [
|
||||||
'--config', 'config.json',
|
'--config', 'config.json',
|
||||||
'--strategy', 'DefaultStrategy',
|
|
||||||
'hyperopt',
|
'hyperopt',
|
||||||
'--epochs', '5'
|
'--epochs', '5'
|
||||||
]
|
]
|
||||||
args = get_args(args)
|
args = get_args(args)
|
||||||
StrategyResolver({'strategy': 'DefaultStrategy'})
|
start_hyperopt(args)
|
||||||
start(args)
|
|
||||||
|
|
||||||
import pprint
|
import pprint
|
||||||
pprint.pprint(caplog.record_tuples)
|
pprint.pprint(caplog.record_tuples)
|
||||||
|
@ -90,6 +197,33 @@ def test_start(mocker, default_conf, caplog) -> None:
|
||||||
assert start_mock.call_count == 1
|
assert start_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_start_no_data(mocker, default_conf, caplog) -> None:
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.configuration.Configuration._load_config_file',
|
||||||
|
lambda *args, **kwargs: default_conf
|
||||||
|
)
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock(return_value={}))
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.optimize.hyperopt.get_timeframe',
|
||||||
|
MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13)))
|
||||||
|
)
|
||||||
|
|
||||||
|
patch_exchange(mocker)
|
||||||
|
|
||||||
|
args = [
|
||||||
|
'--config', 'config.json',
|
||||||
|
'hyperopt',
|
||||||
|
'--epochs', '5'
|
||||||
|
]
|
||||||
|
args = get_args(args)
|
||||||
|
start_hyperopt(args)
|
||||||
|
|
||||||
|
import pprint
|
||||||
|
pprint.pprint(caplog.record_tuples)
|
||||||
|
|
||||||
|
assert log_has('No data found. Terminating.', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_start_failure(mocker, default_conf, caplog) -> None:
|
def test_start_failure(mocker, default_conf, caplog) -> None:
|
||||||
start_mock = MagicMock()
|
start_mock = MagicMock()
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
|
@ -106,17 +240,37 @@ def test_start_failure(mocker, default_conf, caplog) -> None:
|
||||||
'--epochs', '5'
|
'--epochs', '5'
|
||||||
]
|
]
|
||||||
args = get_args(args)
|
args = get_args(args)
|
||||||
StrategyResolver({'strategy': 'DefaultStrategy'})
|
with pytest.raises(DependencyException):
|
||||||
with pytest.raises(ValueError):
|
start_hyperopt(args)
|
||||||
start(args)
|
|
||||||
assert log_has(
|
assert log_has(
|
||||||
"Please don't use --strategy for hyperopt.",
|
"Please don't use --strategy for hyperopt.",
|
||||||
caplog.record_tuples
|
caplog.record_tuples
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_start_filelock(mocker, default_conf, caplog) -> None:
|
||||||
|
start_mock = MagicMock(side_effect=Timeout(HYPEROPT_LOCKFILE))
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.configuration.Configuration._load_config_file',
|
||||||
|
lambda *args, **kwargs: default_conf
|
||||||
|
)
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
|
||||||
|
args = [
|
||||||
|
'--config', 'config.json',
|
||||||
|
'hyperopt',
|
||||||
|
'--epochs', '5'
|
||||||
|
]
|
||||||
|
args = get_args(args)
|
||||||
|
start_hyperopt(args)
|
||||||
|
assert log_has(
|
||||||
|
"Another running instance of freqtrade Hyperopt detected.",
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_loss_calculation_prefer_correct_trade_count(hyperopt) -> None:
|
def test_loss_calculation_prefer_correct_trade_count(hyperopt) -> None:
|
||||||
StrategyResolver({'strategy': 'DefaultStrategy'})
|
|
||||||
|
|
||||||
correct = hyperopt.calculate_loss(1, hyperopt.target_trades, 20)
|
correct = hyperopt.calculate_loss(1, hyperopt.target_trades, 20)
|
||||||
over = hyperopt.calculate_loss(1, hyperopt.target_trades + 100, 20)
|
over = hyperopt.calculate_loss(1, hyperopt.target_trades + 100, 20)
|
||||||
|
@ -146,11 +300,12 @@ def test_log_results_if_loss_improves(hyperopt, capsys) -> None:
|
||||||
'loss': 1,
|
'loss': 1,
|
||||||
'current_tries': 1,
|
'current_tries': 1,
|
||||||
'total_tries': 2,
|
'total_tries': 2,
|
||||||
'result': 'foo'
|
'result': 'foo.',
|
||||||
|
'initial_point': False
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
out, err = capsys.readouterr()
|
out, err = capsys.readouterr()
|
||||||
assert ' 1/2: foo. Loss 1.00000' in out
|
assert ' 2/2: foo. Objective: 1.00000' in out
|
||||||
|
|
||||||
|
|
||||||
def test_no_log_if_loss_does_not_improve(hyperopt, caplog) -> None:
|
def test_no_log_if_loss_does_not_improve(hyperopt, caplog) -> None:
|
||||||
|
@ -206,26 +361,32 @@ def test_roi_table_generation(hyperopt) -> None:
|
||||||
def test_start_calls_optimizer(mocker, default_conf, caplog) -> None:
|
def test_start_calls_optimizer(mocker, default_conf, caplog) -> 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('freqtrade.optimize.hyperopt.multiprocessing.cpu_count', MagicMock(return_value=1))
|
mocker.patch(
|
||||||
|
'freqtrade.optimize.hyperopt.get_timeframe',
|
||||||
|
MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13)))
|
||||||
|
)
|
||||||
|
|
||||||
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, 'result': 'foo result', 'params': {}}])
|
||||||
)
|
)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
|
|
||||||
default_conf.update({'config': 'config.json.example'})
|
default_conf.update({'config': 'config.json.example',
|
||||||
default_conf.update({'epochs': 1})
|
'epochs': 1,
|
||||||
default_conf.update({'timerange': None})
|
'timerange': None,
|
||||||
default_conf.update({'spaces': 'all'})
|
'spaces': 'all',
|
||||||
|
'hyperopt_jobs': 1, })
|
||||||
|
|
||||||
hyperopt = Hyperopt(default_conf)
|
hyperopt = Hyperopt(default_conf)
|
||||||
hyperopt.strategy.tickerdata_to_dataframe = MagicMock()
|
hyperopt.strategy.tickerdata_to_dataframe = MagicMock()
|
||||||
|
|
||||||
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)
|
||||||
assert 'Best result:\nfoo result\nwith values:\n\n' in caplog.text
|
|
||||||
assert dumper.called
|
assert dumper.called
|
||||||
|
# Should be called twice, once for tickerdata, once to save evaluations
|
||||||
|
assert dumper.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
def test_format_results(hyperopt):
|
def test_format_results(hyperopt):
|
||||||
|
@ -266,7 +427,8 @@ def test_has_space(hyperopt):
|
||||||
|
|
||||||
def test_populate_indicators(hyperopt) -> None:
|
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', fill_missing=True)}
|
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', pair="UNITTEST/BTC",
|
||||||
|
fill_missing=True)}
|
||||||
dataframes = hyperopt.strategy.tickerdata_to_dataframe(tickerlist)
|
dataframes = hyperopt.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'})
|
||||||
|
@ -279,7 +441,8 @@ def test_populate_indicators(hyperopt) -> None:
|
||||||
|
|
||||||
def test_buy_strategy_generator(hyperopt) -> None:
|
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', fill_missing=True)}
|
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', pair="UNITTEST/BTC",
|
||||||
|
fill_missing=True)}
|
||||||
dataframes = hyperopt.strategy.tickerdata_to_dataframe(tickerlist)
|
dataframes = hyperopt.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'})
|
||||||
|
@ -307,6 +470,7 @@ def test_generate_optimizer(mocker, default_conf) -> None:
|
||||||
default_conf.update({'config': 'config.json.example'})
|
default_conf.update({'config': 'config.json.example'})
|
||||||
default_conf.update({'timerange': None})
|
default_conf.update({'timerange': None})
|
||||||
default_conf.update({'spaces': 'all'})
|
default_conf.update({'spaces': 'all'})
|
||||||
|
default_conf.update({'hyperopt_min_trades': 1})
|
||||||
|
|
||||||
trades = [
|
trades = [
|
||||||
('POWR/BTC', 0.023117, 0.000233, 100)
|
('POWR/BTC', 0.023117, 0.000233, 100)
|
||||||
|
@ -355,7 +519,7 @@ 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 '
|
'result': ' 1 trades. Avg profit 2.31%. Total profit 0.00023300 BTC '
|
||||||
'(0.0231Σ%). Avg duration 100.0 mins.',
|
'( 2.31Σ%). Avg duration 100.0 mins.',
|
||||||
'params': optimizer_param
|
'params': optimizer_param
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,66 +0,0 @@
|
||||||
# pragma pylint: disable=missing-docstring, protected-access, C0103
|
|
||||||
from freqtrade import optimize
|
|
||||||
from freqtrade.arguments import TimeRange
|
|
||||||
from freqtrade.data import history
|
|
||||||
from freqtrade.misc import timeframe_to_minutes
|
|
||||||
from freqtrade.strategy.default_strategy import DefaultStrategy
|
|
||||||
from freqtrade.tests.conftest import log_has, patch_exchange
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_timeframe(default_conf, mocker) -> None:
|
|
||||||
patch_exchange(mocker)
|
|
||||||
strategy = DefaultStrategy(default_conf)
|
|
||||||
|
|
||||||
data = strategy.tickerdata_to_dataframe(
|
|
||||||
history.load_data(
|
|
||||||
datadir=None,
|
|
||||||
ticker_interval='1m',
|
|
||||||
pairs=['UNITTEST/BTC']
|
|
||||||
)
|
|
||||||
)
|
|
||||||
min_date, max_date = optimize.get_timeframe(data)
|
|
||||||
assert min_date.isoformat() == '2017-11-04T23:02:00+00:00'
|
|
||||||
assert max_date.isoformat() == '2017-11-14T22:58:00+00:00'
|
|
||||||
|
|
||||||
|
|
||||||
def test_validate_backtest_data_warn(default_conf, mocker, caplog) -> None:
|
|
||||||
patch_exchange(mocker)
|
|
||||||
strategy = DefaultStrategy(default_conf)
|
|
||||||
|
|
||||||
data = strategy.tickerdata_to_dataframe(
|
|
||||||
history.load_data(
|
|
||||||
datadir=None,
|
|
||||||
ticker_interval='1m',
|
|
||||||
pairs=['UNITTEST/BTC'],
|
|
||||||
fill_up_missing=False
|
|
||||||
)
|
|
||||||
)
|
|
||||||
min_date, max_date = optimize.get_timeframe(data)
|
|
||||||
caplog.clear()
|
|
||||||
assert optimize.validate_backtest_data(data, min_date, max_date,
|
|
||||||
timeframe_to_minutes('1m'))
|
|
||||||
assert len(caplog.record_tuples) == 1
|
|
||||||
assert log_has(
|
|
||||||
"UNITTEST/BTC has missing frames: expected 14396, got 13680, that's 716 missing values",
|
|
||||||
caplog.record_tuples)
|
|
||||||
|
|
||||||
|
|
||||||
def test_validate_backtest_data(default_conf, mocker, caplog) -> None:
|
|
||||||
patch_exchange(mocker)
|
|
||||||
strategy = DefaultStrategy(default_conf)
|
|
||||||
|
|
||||||
timerange = TimeRange('index', 'index', 200, 250)
|
|
||||||
data = strategy.tickerdata_to_dataframe(
|
|
||||||
history.load_data(
|
|
||||||
datadir=None,
|
|
||||||
ticker_interval='5m',
|
|
||||||
pairs=['UNITTEST/BTC'],
|
|
||||||
timerange=timerange
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
min_date, max_date = optimize.get_timeframe(data)
|
|
||||||
caplog.clear()
|
|
||||||
assert not optimize.validate_backtest_data(data, min_date, max_date,
|
|
||||||
timeframe_to_minutes('5m'))
|
|
||||||
assert len(caplog.record_tuples) == 0
|
|
|
@ -14,8 +14,7 @@ from freqtrade.persistence import Trade
|
||||||
from freqtrade.rpc import RPC, RPCException
|
from freqtrade.rpc import RPC, RPCException
|
||||||
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
||||||
from freqtrade.state import State
|
from freqtrade.state import State
|
||||||
from freqtrade.tests.conftest import patch_exchange
|
from freqtrade.tests.conftest import patch_exchange, patch_get_signal
|
||||||
from freqtrade.tests.test_freqtradebot import patch_get_signal
|
|
||||||
|
|
||||||
|
|
||||||
# Functions for recurrent object patching
|
# Functions for recurrent object patching
|
||||||
|
@ -47,12 +46,14 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None:
|
||||||
|
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trade()
|
||||||
results = rpc._rpc_trade_status()
|
results = rpc._rpc_trade_status()
|
||||||
|
|
||||||
assert {
|
assert {
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'base_currency': 'BTC',
|
'base_currency': 'BTC',
|
||||||
'date': ANY,
|
'open_date': ANY,
|
||||||
|
'open_date_hum': ANY,
|
||||||
|
'close_date': None,
|
||||||
|
'close_date_hum': None,
|
||||||
'open_rate': 1.099e-05,
|
'open_rate': 1.099e-05,
|
||||||
'close_rate': None,
|
'close_rate': None,
|
||||||
'current_rate': 1.098e-05,
|
'current_rate': 1.098e-05,
|
||||||
|
@ -78,7 +79,10 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None:
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'base_currency': 'BTC',
|
'base_currency': 'BTC',
|
||||||
'date': ANY,
|
'open_date': ANY,
|
||||||
|
'open_date_hum': ANY,
|
||||||
|
'close_date': None,
|
||||||
|
'close_date_hum': None,
|
||||||
'open_rate': 1.099e-05,
|
'open_rate': 1.099e-05,
|
||||||
'close_rate': None,
|
'close_rate': None,
|
||||||
'current_rate': ANY,
|
'current_rate': ANY,
|
||||||
|
@ -114,7 +118,7 @@ def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None:
|
||||||
|
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trade()
|
||||||
result = rpc._rpc_status_table()
|
result = rpc._rpc_status_table()
|
||||||
assert 'just now' in result['Since'].all()
|
assert 'instantly' in result['Since'].all()
|
||||||
assert 'ETH/BTC' in result['Pair'].all()
|
assert 'ETH/BTC' in result['Pair'].all()
|
||||||
assert '-0.59%' in result['Profit'].all()
|
assert '-0.59%' in result['Profit'].all()
|
||||||
|
|
||||||
|
@ -123,7 +127,7 @@ def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None:
|
||||||
# invalidate ticker cache
|
# invalidate ticker cache
|
||||||
rpc._freqtrade.exchange._cached_ticker = {}
|
rpc._freqtrade.exchange._cached_ticker = {}
|
||||||
result = rpc._rpc_status_table()
|
result = rpc._rpc_status_table()
|
||||||
assert 'just now' in result['Since'].all()
|
assert 'instantly' in result['Since'].all()
|
||||||
assert 'ETH/BTC' in result['Pair'].all()
|
assert 'ETH/BTC' in result['Pair'].all()
|
||||||
assert 'nan%' in result['Profit'].all()
|
assert 'nan%' in result['Profit'].all()
|
||||||
|
|
||||||
|
@ -463,12 +467,15 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
|
||||||
with pytest.raises(RPCException, match=r'.*invalid argument*'):
|
with pytest.raises(RPCException, match=r'.*invalid argument*'):
|
||||||
rpc._rpc_forcesell(None)
|
rpc._rpc_forcesell(None)
|
||||||
|
|
||||||
rpc._rpc_forcesell('all')
|
msg = rpc._rpc_forcesell('all')
|
||||||
|
assert msg == {'result': 'Created sell orders for all open trades.'}
|
||||||
|
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trade()
|
||||||
rpc._rpc_forcesell('all')
|
msg = rpc._rpc_forcesell('all')
|
||||||
|
assert msg == {'result': 'Created sell orders for all open trades.'}
|
||||||
|
|
||||||
rpc._rpc_forcesell('1')
|
msg = rpc._rpc_forcesell('1')
|
||||||
|
assert msg == {'result': 'Created sell order for trade 1.'}
|
||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
with pytest.raises(RPCException, match=r'.*trader is not running*'):
|
with pytest.raises(RPCException, match=r'.*trader is not running*'):
|
||||||
|
@ -511,7 +518,8 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
# check that the trade is called, which is done by ensuring exchange.cancel_order is called
|
# check that the trade is called, which is done by ensuring exchange.cancel_order is called
|
||||||
rpc._rpc_forcesell('2')
|
msg = rpc._rpc_forcesell('2')
|
||||||
|
assert msg == {'result': 'Created sell order for trade 2.'}
|
||||||
assert cancel_order_mock.call_count == 2
|
assert cancel_order_mock.call_count == 2
|
||||||
assert trade.amount == amount
|
assert trade.amount == amount
|
||||||
|
|
||||||
|
@ -525,7 +533,8 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
|
||||||
'side': 'sell'
|
'side': 'sell'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
rpc._rpc_forcesell('3')
|
msg = rpc._rpc_forcesell('3')
|
||||||
|
assert msg == {'result': 'Created sell order for trade 3.'}
|
||||||
# status quo, no exchange calls
|
# status quo, no exchange calls
|
||||||
assert cancel_order_mock.call_count == 2
|
assert cancel_order_mock.call_count == 2
|
||||||
|
|
||||||
|
|
556
freqtrade/tests/rpc/test_rpc_apiserver.py
Normal file
556
freqtrade/tests/rpc/test_rpc_apiserver.py
Normal file
|
@ -0,0 +1,556 @@
|
||||||
|
"""
|
||||||
|
Unit test file for rpc/api_server.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from unittest.mock import ANY, MagicMock, PropertyMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from flask import Flask
|
||||||
|
from requests.auth import _basic_auth_str
|
||||||
|
|
||||||
|
from freqtrade.__init__ import __version__
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.rpc.api_server import BASE_URI, ApiServer
|
||||||
|
from freqtrade.state import State
|
||||||
|
from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has,
|
||||||
|
patch_get_signal)
|
||||||
|
|
||||||
|
|
||||||
|
_TEST_USER = "FreqTrader"
|
||||||
|
_TEST_PASS = "SuperSecurePassword1!"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def botclient(default_conf, mocker):
|
||||||
|
default_conf.update({"api_server": {"enabled": True,
|
||||||
|
"listen_ip_address": "127.0.0.1",
|
||||||
|
"listen_port": "8080",
|
||||||
|
"username": _TEST_USER,
|
||||||
|
"password": _TEST_PASS,
|
||||||
|
}})
|
||||||
|
|
||||||
|
ftbot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
mocker.patch('freqtrade.rpc.api_server.ApiServer.run', MagicMock())
|
||||||
|
apiserver = ApiServer(ftbot)
|
||||||
|
yield ftbot, apiserver.app.test_client()
|
||||||
|
# Cleanup ... ?
|
||||||
|
|
||||||
|
|
||||||
|
def client_post(client, url, data={}):
|
||||||
|
return client.post(url,
|
||||||
|
content_type="application/json",
|
||||||
|
data=data,
|
||||||
|
headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS)})
|
||||||
|
|
||||||
|
|
||||||
|
def client_get(client, url):
|
||||||
|
return client.get(url, headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS)})
|
||||||
|
|
||||||
|
|
||||||
|
def assert_response(response, expected_code=200):
|
||||||
|
assert response.status_code == expected_code
|
||||||
|
assert response.content_type == "application/json"
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_not_found(botclient):
|
||||||
|
ftbot, client = botclient
|
||||||
|
|
||||||
|
rc = client_post(client, f"{BASE_URI}/invalid_url")
|
||||||
|
assert_response(rc, 404)
|
||||||
|
assert rc.json == {"status": "error",
|
||||||
|
"reason": f"There's no API call for http://localhost{BASE_URI}/invalid_url.",
|
||||||
|
"code": 404
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_unauthorized(botclient):
|
||||||
|
ftbot, client = botclient
|
||||||
|
# Don't send user/pass information
|
||||||
|
rc = client.get(f"{BASE_URI}/version")
|
||||||
|
assert_response(rc, 401)
|
||||||
|
assert rc.json == {'error': 'Unauthorized'}
|
||||||
|
|
||||||
|
# Change only username
|
||||||
|
ftbot.config['api_server']['username'] = "Ftrader"
|
||||||
|
rc = client_get(client, f"{BASE_URI}/version")
|
||||||
|
assert_response(rc, 401)
|
||||||
|
assert rc.json == {'error': 'Unauthorized'}
|
||||||
|
|
||||||
|
# Change only password
|
||||||
|
ftbot.config['api_server']['username'] = _TEST_USER
|
||||||
|
ftbot.config['api_server']['password'] = "WrongPassword"
|
||||||
|
rc = client_get(client, f"{BASE_URI}/version")
|
||||||
|
assert_response(rc, 401)
|
||||||
|
assert rc.json == {'error': 'Unauthorized'}
|
||||||
|
|
||||||
|
ftbot.config['api_server']['username'] = "Ftrader"
|
||||||
|
ftbot.config['api_server']['password'] = "WrongPassword"
|
||||||
|
|
||||||
|
rc = client_get(client, f"{BASE_URI}/version")
|
||||||
|
assert_response(rc, 401)
|
||||||
|
assert rc.json == {'error': 'Unauthorized'}
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_stop_workflow(botclient):
|
||||||
|
ftbot, client = botclient
|
||||||
|
assert ftbot.state == State.RUNNING
|
||||||
|
rc = client_post(client, f"{BASE_URI}/stop")
|
||||||
|
assert_response(rc)
|
||||||
|
assert rc.json == {'status': 'stopping trader ...'}
|
||||||
|
assert ftbot.state == State.STOPPED
|
||||||
|
|
||||||
|
# Stop bot again
|
||||||
|
rc = client_post(client, f"{BASE_URI}/stop")
|
||||||
|
assert_response(rc)
|
||||||
|
assert rc.json == {'status': 'already stopped'}
|
||||||
|
|
||||||
|
# Start bot
|
||||||
|
rc = client_post(client, f"{BASE_URI}/start")
|
||||||
|
assert_response(rc)
|
||||||
|
assert rc.json == {'status': 'starting trader ...'}
|
||||||
|
assert ftbot.state == State.RUNNING
|
||||||
|
|
||||||
|
# Call start again
|
||||||
|
rc = client_post(client, f"{BASE_URI}/start")
|
||||||
|
assert_response(rc)
|
||||||
|
assert rc.json == {'status': 'already running'}
|
||||||
|
|
||||||
|
|
||||||
|
def test_api__init__(default_conf, mocker):
|
||||||
|
"""
|
||||||
|
Test __init__() method
|
||||||
|
"""
|
||||||
|
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock())
|
||||||
|
mocker.patch('freqtrade.rpc.api_server.ApiServer.run', MagicMock())
|
||||||
|
|
||||||
|
apiserver = ApiServer(get_patched_freqtradebot(mocker, default_conf))
|
||||||
|
assert apiserver._config == default_conf
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_run(default_conf, mocker, caplog):
|
||||||
|
default_conf.update({"api_server": {"enabled": True,
|
||||||
|
"listen_ip_address": "127.0.0.1",
|
||||||
|
"listen_port": "8080"}})
|
||||||
|
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock())
|
||||||
|
mocker.patch('freqtrade.rpc.api_server.threading.Thread', MagicMock())
|
||||||
|
|
||||||
|
server_mock = MagicMock()
|
||||||
|
mocker.patch('freqtrade.rpc.api_server.make_server', server_mock)
|
||||||
|
|
||||||
|
apiserver = ApiServer(get_patched_freqtradebot(mocker, default_conf))
|
||||||
|
|
||||||
|
assert apiserver._config == default_conf
|
||||||
|
apiserver.run()
|
||||||
|
assert server_mock.call_count == 1
|
||||||
|
assert server_mock.call_args_list[0][0][0] == "127.0.0.1"
|
||||||
|
assert server_mock.call_args_list[0][0][1] == "8080"
|
||||||
|
assert isinstance(server_mock.call_args_list[0][0][2], Flask)
|
||||||
|
assert hasattr(apiserver, "srv")
|
||||||
|
|
||||||
|
assert log_has("Starting HTTP Server at 127.0.0.1:8080", caplog.record_tuples)
|
||||||
|
assert log_has("Starting Local Rest Server.", caplog.record_tuples)
|
||||||
|
|
||||||
|
# Test binding to public
|
||||||
|
caplog.clear()
|
||||||
|
server_mock.reset_mock()
|
||||||
|
apiserver._config.update({"api_server": {"enabled": True,
|
||||||
|
"listen_ip_address": "0.0.0.0",
|
||||||
|
"listen_port": "8089",
|
||||||
|
"password": "",
|
||||||
|
}})
|
||||||
|
apiserver.run()
|
||||||
|
|
||||||
|
assert server_mock.call_count == 1
|
||||||
|
assert server_mock.call_args_list[0][0][0] == "0.0.0.0"
|
||||||
|
assert server_mock.call_args_list[0][0][1] == "8089"
|
||||||
|
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 Local Rest Server.", caplog.record_tuples)
|
||||||
|
assert log_has("SECURITY WARNING - Local Rest Server listening to external connections",
|
||||||
|
caplog.record_tuples)
|
||||||
|
assert log_has("SECURITY WARNING - This is insecure please set to your loopback,"
|
||||||
|
"e.g 127.0.0.1 in config.json",
|
||||||
|
caplog.record_tuples)
|
||||||
|
assert log_has("SECURITY WARNING - No password for local REST Server defined. "
|
||||||
|
"Please make sure that this is intentional!",
|
||||||
|
caplog.record_tuples)
|
||||||
|
|
||||||
|
# Test crashing flask
|
||||||
|
caplog.clear()
|
||||||
|
mocker.patch('freqtrade.rpc.api_server.make_server', MagicMock(side_effect=Exception))
|
||||||
|
apiserver.run()
|
||||||
|
assert log_has("Api server failed to start.", caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_cleanup(default_conf, mocker, caplog):
|
||||||
|
default_conf.update({"api_server": {"enabled": True,
|
||||||
|
"listen_ip_address": "127.0.0.1",
|
||||||
|
"listen_port": "8080"}})
|
||||||
|
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock())
|
||||||
|
mocker.patch('freqtrade.rpc.api_server.threading.Thread', MagicMock())
|
||||||
|
mocker.patch('freqtrade.rpc.api_server.make_server', MagicMock())
|
||||||
|
|
||||||
|
apiserver = ApiServer(get_patched_freqtradebot(mocker, default_conf))
|
||||||
|
apiserver.run()
|
||||||
|
stop_mock = MagicMock()
|
||||||
|
stop_mock.shutdown = MagicMock()
|
||||||
|
apiserver.srv = stop_mock
|
||||||
|
|
||||||
|
apiserver.cleanup()
|
||||||
|
assert stop_mock.shutdown.call_count == 1
|
||||||
|
assert log_has("Stopping API Server", caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_reloadconf(botclient):
|
||||||
|
ftbot, client = botclient
|
||||||
|
|
||||||
|
rc = client_post(client, f"{BASE_URI}/reload_conf")
|
||||||
|
assert_response(rc)
|
||||||
|
assert rc.json == {'status': 'reloading config ...'}
|
||||||
|
assert ftbot.state == State.RELOAD_CONF
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_stopbuy(botclient):
|
||||||
|
ftbot, client = botclient
|
||||||
|
assert ftbot.config['max_open_trades'] != 0
|
||||||
|
|
||||||
|
rc = client_post(client, f"{BASE_URI}/stopbuy")
|
||||||
|
assert_response(rc)
|
||||||
|
assert rc.json == {'status': 'No more buy will occur from now. Run /reload_conf to reset.'}
|
||||||
|
assert ftbot.config['max_open_trades'] == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_balance(botclient, mocker, rpc_balance):
|
||||||
|
ftbot, client = botclient
|
||||||
|
|
||||||
|
def mock_ticker(symbol, refresh):
|
||||||
|
if symbol == 'BTC/USDT':
|
||||||
|
return {
|
||||||
|
'bid': 10000.00,
|
||||||
|
'ask': 10000.00,
|
||||||
|
'last': 10000.00,
|
||||||
|
}
|
||||||
|
elif symbol == 'XRP/BTC':
|
||||||
|
return {
|
||||||
|
'bid': 0.00001,
|
||||||
|
'ask': 0.00001,
|
||||||
|
'last': 0.00001,
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
'bid': 0.1,
|
||||||
|
'ask': 0.1,
|
||||||
|
'last': 0.1,
|
||||||
|
}
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker)
|
||||||
|
|
||||||
|
rc = client_get(client, f"{BASE_URI}/balance")
|
||||||
|
assert_response(rc)
|
||||||
|
assert "currencies" in rc.json
|
||||||
|
assert len(rc.json["currencies"]) == 5
|
||||||
|
assert rc.json['currencies'][0] == {
|
||||||
|
'currency': 'BTC',
|
||||||
|
'available': 12.0,
|
||||||
|
'balance': 12.0,
|
||||||
|
'pending': 0.0,
|
||||||
|
'est_btc': 12.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_count(botclient, mocker, ticker, fee, markets):
|
||||||
|
ftbot, client = botclient
|
||||||
|
patch_get_signal(ftbot, (True, False))
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_balances=MagicMock(return_value=ticker),
|
||||||
|
get_ticker=ticker,
|
||||||
|
get_fee=fee,
|
||||||
|
markets=PropertyMock(return_value=markets)
|
||||||
|
)
|
||||||
|
rc = client_get(client, f"{BASE_URI}/count")
|
||||||
|
assert_response(rc)
|
||||||
|
|
||||||
|
assert rc.json["current"] == 0
|
||||||
|
assert rc.json["max"] == 1.0
|
||||||
|
|
||||||
|
# Create some test data
|
||||||
|
ftbot.create_trade()
|
||||||
|
rc = client_get(client, f"{BASE_URI}/count")
|
||||||
|
assert_response(rc)
|
||||||
|
assert rc.json["current"] == 1.0
|
||||||
|
assert rc.json["max"] == 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_daily(botclient, mocker, ticker, fee, markets):
|
||||||
|
ftbot, client = botclient
|
||||||
|
patch_get_signal(ftbot, (True, False))
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_balances=MagicMock(return_value=ticker),
|
||||||
|
get_ticker=ticker,
|
||||||
|
get_fee=fee,
|
||||||
|
markets=PropertyMock(return_value=markets)
|
||||||
|
)
|
||||||
|
rc = client_get(client, f"{BASE_URI}/daily")
|
||||||
|
assert_response(rc)
|
||||||
|
assert len(rc.json) == 7
|
||||||
|
assert rc.json[0][0] == str(datetime.utcnow().date())
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
|
||||||
|
ftbot, client = botclient
|
||||||
|
patch_get_signal(ftbot, (True, False))
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_balances=MagicMock(return_value=ticker),
|
||||||
|
get_ticker=ticker,
|
||||||
|
get_fee=fee,
|
||||||
|
markets=PropertyMock(return_value=markets)
|
||||||
|
)
|
||||||
|
rc = client_get(client, f"{BASE_URI}/edge")
|
||||||
|
assert_response(rc, 502)
|
||||||
|
assert rc.json == {"error": "Error querying _edge: Edge is not enabled."}
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, limit_sell_order):
|
||||||
|
ftbot, client = botclient
|
||||||
|
patch_get_signal(ftbot, (True, False))
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_balances=MagicMock(return_value=ticker),
|
||||||
|
get_ticker=ticker,
|
||||||
|
get_fee=fee,
|
||||||
|
markets=PropertyMock(return_value=markets)
|
||||||
|
)
|
||||||
|
|
||||||
|
rc = client_get(client, f"{BASE_URI}/profit")
|
||||||
|
assert_response(rc, 502)
|
||||||
|
assert len(rc.json) == 1
|
||||||
|
assert rc.json == {"error": "Error querying _profit: no closed trade"}
|
||||||
|
|
||||||
|
ftbot.create_trade()
|
||||||
|
trade = Trade.query.first()
|
||||||
|
|
||||||
|
# Simulate fulfilled LIMIT_BUY order for trade
|
||||||
|
trade.update(limit_buy_order)
|
||||||
|
rc = client_get(client, f"{BASE_URI}/profit")
|
||||||
|
assert_response(rc, 502)
|
||||||
|
assert rc.json == {"error": "Error querying _profit: no closed trade"}
|
||||||
|
|
||||||
|
trade.update(limit_sell_order)
|
||||||
|
|
||||||
|
trade.close_date = datetime.utcnow()
|
||||||
|
trade.is_open = False
|
||||||
|
|
||||||
|
rc = client_get(client, f"{BASE_URI}/profit")
|
||||||
|
assert_response(rc)
|
||||||
|
assert rc.json == {'avg_duration': '0:00:00',
|
||||||
|
'best_pair': 'ETH/BTC',
|
||||||
|
'best_rate': 6.2,
|
||||||
|
'first_trade_date': 'just now',
|
||||||
|
'latest_trade_date': 'just now',
|
||||||
|
'profit_all_coin': 6.217e-05,
|
||||||
|
'profit_all_fiat': 0,
|
||||||
|
'profit_all_percent': 6.2,
|
||||||
|
'profit_closed_coin': 6.217e-05,
|
||||||
|
'profit_closed_fiat': 0,
|
||||||
|
'profit_closed_percent': 6.2,
|
||||||
|
'trade_count': 1
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_performance(botclient, mocker, ticker, fee):
|
||||||
|
ftbot, client = botclient
|
||||||
|
patch_get_signal(ftbot, (True, False))
|
||||||
|
|
||||||
|
trade = Trade(
|
||||||
|
pair='LTC/ETH',
|
||||||
|
amount=1,
|
||||||
|
exchange='binance',
|
||||||
|
stake_amount=1,
|
||||||
|
open_rate=0.245441,
|
||||||
|
open_order_id="123456",
|
||||||
|
is_open=False,
|
||||||
|
fee_close=fee.return_value,
|
||||||
|
fee_open=fee.return_value,
|
||||||
|
close_rate=0.265441,
|
||||||
|
|
||||||
|
)
|
||||||
|
trade.close_profit = trade.calc_profit_percent()
|
||||||
|
Trade.session.add(trade)
|
||||||
|
|
||||||
|
trade = Trade(
|
||||||
|
pair='XRP/ETH',
|
||||||
|
amount=5,
|
||||||
|
stake_amount=1,
|
||||||
|
exchange='binance',
|
||||||
|
open_rate=0.412,
|
||||||
|
open_order_id="123456",
|
||||||
|
is_open=False,
|
||||||
|
fee_close=fee.return_value,
|
||||||
|
fee_open=fee.return_value,
|
||||||
|
close_rate=0.391
|
||||||
|
)
|
||||||
|
trade.close_profit = trade.calc_profit_percent()
|
||||||
|
Trade.session.add(trade)
|
||||||
|
Trade.session.flush()
|
||||||
|
|
||||||
|
rc = client_get(client, f"{BASE_URI}/performance")
|
||||||
|
assert_response(rc)
|
||||||
|
assert len(rc.json) == 2
|
||||||
|
assert rc.json == [{'count': 1, 'pair': 'LTC/ETH', 'profit': 7.61},
|
||||||
|
{'count': 1, 'pair': 'XRP/ETH', 'profit': -5.57}]
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_status(botclient, mocker, ticker, fee, markets):
|
||||||
|
ftbot, client = botclient
|
||||||
|
patch_get_signal(ftbot, (True, False))
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_balances=MagicMock(return_value=ticker),
|
||||||
|
get_ticker=ticker,
|
||||||
|
get_fee=fee,
|
||||||
|
markets=PropertyMock(return_value=markets)
|
||||||
|
)
|
||||||
|
|
||||||
|
rc = client_get(client, f"{BASE_URI}/status")
|
||||||
|
assert_response(rc, 502)
|
||||||
|
assert rc.json == {'error': 'Error querying _status: no active trade'}
|
||||||
|
|
||||||
|
ftbot.create_trade()
|
||||||
|
rc = client_get(client, f"{BASE_URI}/status")
|
||||||
|
assert_response(rc)
|
||||||
|
assert len(rc.json) == 1
|
||||||
|
assert rc.json == [{'amount': 90.99181074,
|
||||||
|
'base_currency': 'BTC',
|
||||||
|
'close_date': None,
|
||||||
|
'close_date_hum': None,
|
||||||
|
'close_profit': None,
|
||||||
|
'close_rate': None,
|
||||||
|
'current_profit': -0.59,
|
||||||
|
'current_rate': 1.098e-05,
|
||||||
|
'initial_stop_loss': 0.0,
|
||||||
|
'initial_stop_loss_pct': None,
|
||||||
|
'open_date': ANY,
|
||||||
|
'open_date_hum': 'just now',
|
||||||
|
'open_order': '(limit buy rem=0.00000000)',
|
||||||
|
'open_rate': 1.099e-05,
|
||||||
|
'pair': 'ETH/BTC',
|
||||||
|
'stake_amount': 0.001,
|
||||||
|
'stop_loss': 0.0,
|
||||||
|
'stop_loss_pct': None,
|
||||||
|
'trade_id': 1}]
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_version(botclient):
|
||||||
|
ftbot, client = botclient
|
||||||
|
|
||||||
|
rc = client_get(client, f"{BASE_URI}/version")
|
||||||
|
assert_response(rc)
|
||||||
|
assert rc.json == {"version": __version__}
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_blacklist(botclient, mocker):
|
||||||
|
ftbot, client = botclient
|
||||||
|
|
||||||
|
rc = client_get(client, f"{BASE_URI}/blacklist")
|
||||||
|
assert_response(rc)
|
||||||
|
assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC"],
|
||||||
|
"length": 2,
|
||||||
|
"method": "StaticPairList"}
|
||||||
|
|
||||||
|
# Add ETH/BTC to blacklist
|
||||||
|
rc = client_post(client, f"{BASE_URI}/blacklist",
|
||||||
|
data='{"blacklist": ["ETH/BTC"]}')
|
||||||
|
assert_response(rc)
|
||||||
|
assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC", "ETH/BTC"],
|
||||||
|
"length": 3,
|
||||||
|
"method": "StaticPairList"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_whitelist(botclient):
|
||||||
|
ftbot, client = botclient
|
||||||
|
|
||||||
|
rc = client_get(client, f"{BASE_URI}/whitelist")
|
||||||
|
assert_response(rc)
|
||||||
|
assert rc.json == {"whitelist": ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC'],
|
||||||
|
"length": 4,
|
||||||
|
"method": "StaticPairList"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_forcebuy(botclient, mocker, fee):
|
||||||
|
ftbot, client = botclient
|
||||||
|
|
||||||
|
rc = client_post(client, f"{BASE_URI}/forcebuy",
|
||||||
|
data='{"pair": "ETH/BTC"}')
|
||||||
|
assert_response(rc, 502)
|
||||||
|
assert rc.json == {"error": "Error querying _forcebuy: Forcebuy not enabled."}
|
||||||
|
|
||||||
|
# enable forcebuy
|
||||||
|
ftbot.config["forcebuy_enable"] = True
|
||||||
|
|
||||||
|
fbuy_mock = MagicMock(return_value=None)
|
||||||
|
mocker.patch("freqtrade.rpc.RPC._rpc_forcebuy", fbuy_mock)
|
||||||
|
rc = client_post(client, f"{BASE_URI}/forcebuy",
|
||||||
|
data='{"pair": "ETH/BTC"}')
|
||||||
|
assert_response(rc)
|
||||||
|
assert rc.json == {"status": "Error buying pair ETH/BTC."}
|
||||||
|
|
||||||
|
# Test creating trae
|
||||||
|
fbuy_mock = MagicMock(return_value=Trade(
|
||||||
|
pair='ETH/ETH',
|
||||||
|
amount=1,
|
||||||
|
exchange='bittrex',
|
||||||
|
stake_amount=1,
|
||||||
|
open_rate=0.245441,
|
||||||
|
open_order_id="123456",
|
||||||
|
open_date=datetime.utcnow(),
|
||||||
|
is_open=False,
|
||||||
|
fee_close=fee.return_value,
|
||||||
|
fee_open=fee.return_value,
|
||||||
|
close_rate=0.265441,
|
||||||
|
))
|
||||||
|
mocker.patch("freqtrade.rpc.RPC._rpc_forcebuy", fbuy_mock)
|
||||||
|
|
||||||
|
rc = client_post(client, f"{BASE_URI}/forcebuy",
|
||||||
|
data='{"pair": "ETH/BTC"}')
|
||||||
|
assert_response(rc)
|
||||||
|
assert rc.json == {'amount': 1,
|
||||||
|
'close_date': None,
|
||||||
|
'close_date_hum': None,
|
||||||
|
'close_rate': 0.265441,
|
||||||
|
'initial_stop_loss': None,
|
||||||
|
'initial_stop_loss_pct': None,
|
||||||
|
'open_date': ANY,
|
||||||
|
'open_date_hum': 'just now',
|
||||||
|
'open_rate': 0.245441,
|
||||||
|
'pair': 'ETH/ETH',
|
||||||
|
'stake_amount': 1,
|
||||||
|
'stop_loss': None,
|
||||||
|
'stop_loss_pct': None,
|
||||||
|
'trade_id': None}
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_forcesell(botclient, mocker, ticker, fee, markets):
|
||||||
|
ftbot, client = botclient
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_balances=MagicMock(return_value=ticker),
|
||||||
|
get_ticker=ticker,
|
||||||
|
get_fee=fee,
|
||||||
|
markets=PropertyMock(return_value=markets)
|
||||||
|
)
|
||||||
|
patch_get_signal(ftbot, (True, False))
|
||||||
|
|
||||||
|
rc = client_post(client, f"{BASE_URI}/forcesell",
|
||||||
|
data='{"tradeid": "1"}')
|
||||||
|
assert_response(rc, 502)
|
||||||
|
assert rc.json == {"error": "Error querying _forcesell: invalid argument"}
|
||||||
|
|
||||||
|
ftbot.create_trade()
|
||||||
|
|
||||||
|
rc = client_post(client, f"{BASE_URI}/forcesell",
|
||||||
|
data='{"tradeid": "1"}')
|
||||||
|
assert_response(rc)
|
||||||
|
assert rc.json == {'result': 'Created sell order for trade 1.'}
|
|
@ -135,3 +135,32 @@ def test_startupmessages_telegram_enabled(mocker, default_conf, caplog) -> None:
|
||||||
rpc_manager.startup_messages(default_conf, freqtradebot.pairlists)
|
rpc_manager.startup_messages(default_conf, freqtradebot.pairlists)
|
||||||
assert telegram_mock.call_count == 3
|
assert telegram_mock.call_count == 3
|
||||||
assert "Dry run is enabled." in telegram_mock.call_args_list[0][0][0]['status']
|
assert "Dry run is enabled." in telegram_mock.call_args_list[0][0][0]['status']
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_apiserver_disabled(mocker, default_conf, caplog) -> None:
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
run_mock = MagicMock()
|
||||||
|
mocker.patch('freqtrade.rpc.api_server.ApiServer.run', run_mock)
|
||||||
|
default_conf['telegram']['enabled'] = False
|
||||||
|
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
||||||
|
|
||||||
|
assert not log_has('Enabling rpc.api_server', caplog.record_tuples)
|
||||||
|
assert rpc_manager.registered_modules == []
|
||||||
|
assert run_mock.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_apiserver_enabled(mocker, default_conf, caplog) -> None:
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
run_mock = MagicMock()
|
||||||
|
mocker.patch('freqtrade.rpc.api_server.ApiServer.run', run_mock)
|
||||||
|
|
||||||
|
default_conf["telegram"]["enabled"] = False
|
||||||
|
default_conf["api_server"] = {"enabled": True,
|
||||||
|
"listen_ip_address": "127.0.0.1",
|
||||||
|
"listen_port": "8080"}
|
||||||
|
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
||||||
|
|
||||||
|
assert log_has('Enabling rpc.api_server', caplog.record_tuples)
|
||||||
|
assert len(rpc_manager.registered_modules) == 1
|
||||||
|
assert 'apiserver' in [mod.name for mod in rpc_manager.registered_modules]
|
||||||
|
assert run_mock.call_count == 1
|
||||||
|
|
|
@ -22,8 +22,7 @@ from freqtrade.rpc.telegram import Telegram, authorized_only
|
||||||
from freqtrade.state import State
|
from freqtrade.state import State
|
||||||
from freqtrade.strategy.interface import SellType
|
from freqtrade.strategy.interface import SellType
|
||||||
from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has,
|
from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has,
|
||||||
patch_exchange)
|
patch_exchange, patch_get_signal)
|
||||||
from freqtrade.tests.test_freqtradebot import patch_get_signal
|
|
||||||
|
|
||||||
|
|
||||||
class DummyCls(Telegram):
|
class DummyCls(Telegram):
|
||||||
|
@ -192,7 +191,10 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'base_currency': 'BTC',
|
'base_currency': 'BTC',
|
||||||
'date': arrow.utcnow(),
|
'open_date': arrow.utcnow(),
|
||||||
|
'open_date_hum': arrow.utcnow().humanize,
|
||||||
|
'close_date': None,
|
||||||
|
'close_date_hum': None,
|
||||||
'open_rate': 1.099e-05,
|
'open_rate': 1.099e-05,
|
||||||
'close_rate': None,
|
'close_rate': None,
|
||||||
'current_rate': 1.098e-05,
|
'current_rate': 1.098e-05,
|
||||||
|
@ -493,34 +495,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
||||||
assert '*Best Performing:* `ETH/BTC: 6.20%`' in msg_mock.call_args_list[-1][0][0]
|
assert '*Best Performing:* `ETH/BTC: 6.20%`' in msg_mock.call_args_list[-1][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_telegram_balance_handle(default_conf, update, mocker) -> None:
|
def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance) -> None:
|
||||||
mock_balance = {
|
|
||||||
'BTC': {
|
|
||||||
'total': 12.0,
|
|
||||||
'free': 12.0,
|
|
||||||
'used': 0.0
|
|
||||||
},
|
|
||||||
'ETH': {
|
|
||||||
'total': 0.0,
|
|
||||||
'free': 0.0,
|
|
||||||
'used': 0.0
|
|
||||||
},
|
|
||||||
'USDT': {
|
|
||||||
'total': 10000.0,
|
|
||||||
'free': 10000.0,
|
|
||||||
'used': 0.0
|
|
||||||
},
|
|
||||||
'LTC': {
|
|
||||||
'total': 10.0,
|
|
||||||
'free': 10.0,
|
|
||||||
'used': 0.0
|
|
||||||
},
|
|
||||||
'XRP': {
|
|
||||||
'total': 1.0,
|
|
||||||
'free': 1.0,
|
|
||||||
'used': 0.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def mock_ticker(symbol, refresh):
|
def mock_ticker(symbol, refresh):
|
||||||
if symbol == 'BTC/USDT':
|
if symbol == 'BTC/USDT':
|
||||||
|
@ -541,7 +516,7 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None:
|
||||||
'last': 0.1,
|
'last': 0.1,
|
||||||
}
|
}
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=mock_balance)
|
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker)
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker)
|
||||||
|
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
|
@ -562,6 +537,7 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None:
|
||||||
assert '*BTC:*' in result
|
assert '*BTC:*' in result
|
||||||
assert '*ETH:*' not in result
|
assert '*ETH:*' not in result
|
||||||
assert '*USDT:*' in result
|
assert '*USDT:*' in result
|
||||||
|
assert '*EUR:*' in result
|
||||||
assert 'Balance:' in result
|
assert 'Balance:' in result
|
||||||
assert 'Est. BTC:' in result
|
assert 'Est. BTC:' in result
|
||||||
assert 'BTC: 12.00000000' in result
|
assert 'BTC: 12.00000000' in result
|
||||||
|
@ -780,6 +756,7 @@ def test_forcesell_handle(default_conf, update, ticker, fee,
|
||||||
'gain': 'profit',
|
'gain': 'profit',
|
||||||
'limit': 1.172e-05,
|
'limit': 1.172e-05,
|
||||||
'amount': 90.99181073703367,
|
'amount': 90.99181073703367,
|
||||||
|
'order_type': 'limit',
|
||||||
'open_rate': 1.099e-05,
|
'open_rate': 1.099e-05,
|
||||||
'current_rate': 1.172e-05,
|
'current_rate': 1.172e-05,
|
||||||
'profit_amount': 6.126e-05,
|
'profit_amount': 6.126e-05,
|
||||||
|
@ -834,6 +811,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee,
|
||||||
'gain': 'loss',
|
'gain': 'loss',
|
||||||
'limit': 1.044e-05,
|
'limit': 1.044e-05,
|
||||||
'amount': 90.99181073703367,
|
'amount': 90.99181073703367,
|
||||||
|
'order_type': 'limit',
|
||||||
'open_rate': 1.099e-05,
|
'open_rate': 1.099e-05,
|
||||||
'current_rate': 1.044e-05,
|
'current_rate': 1.044e-05,
|
||||||
'profit_amount': -5.492e-05,
|
'profit_amount': -5.492e-05,
|
||||||
|
@ -879,6 +857,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker
|
||||||
'gain': 'loss',
|
'gain': 'loss',
|
||||||
'limit': 1.098e-05,
|
'limit': 1.098e-05,
|
||||||
'amount': 90.99181073703367,
|
'amount': 90.99181073703367,
|
||||||
|
'order_type': 'limit',
|
||||||
'open_rate': 1.099e-05,
|
'open_rate': 1.099e-05,
|
||||||
'current_rate': 1.098e-05,
|
'current_rate': 1.098e-05,
|
||||||
'profit_amount': -5.91e-06,
|
'profit_amount': -5.91e-06,
|
||||||
|
@ -1212,6 +1191,7 @@ def test_send_msg_buy_notification(default_conf, mocker) -> None:
|
||||||
'exchange': 'Bittrex',
|
'exchange': 'Bittrex',
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'limit': 1.099e-05,
|
'limit': 1.099e-05,
|
||||||
|
'order_type': 'limit',
|
||||||
'stake_amount': 0.001,
|
'stake_amount': 0.001,
|
||||||
'stake_amount_fiat': 0.0,
|
'stake_amount_fiat': 0.0,
|
||||||
'stake_currency': 'BTC',
|
'stake_currency': 'BTC',
|
||||||
|
@ -1219,7 +1199,7 @@ def test_send_msg_buy_notification(default_conf, mocker) -> None:
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] \
|
assert msg_mock.call_args[0][0] \
|
||||||
== '*Bittrex:* Buying ETH/BTC\n' \
|
== '*Bittrex:* Buying ETH/BTC\n' \
|
||||||
'with limit `0.00001099\n' \
|
'at rate `0.00001099\n' \
|
||||||
'(0.001000 BTC,0.000 USD)`'
|
'(0.001000 BTC,0.000 USD)`'
|
||||||
|
|
||||||
|
|
||||||
|
@ -1241,6 +1221,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
||||||
'gain': 'loss',
|
'gain': 'loss',
|
||||||
'limit': 3.201e-05,
|
'limit': 3.201e-05,
|
||||||
'amount': 1333.3333333333335,
|
'amount': 1333.3333333333335,
|
||||||
|
'order_type': 'market',
|
||||||
'open_rate': 7.5e-05,
|
'open_rate': 7.5e-05,
|
||||||
'current_rate': 3.201e-05,
|
'current_rate': 3.201e-05,
|
||||||
'profit_amount': -0.05746268,
|
'profit_amount': -0.05746268,
|
||||||
|
@ -1251,7 +1232,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] \
|
assert msg_mock.call_args[0][0] \
|
||||||
== ('*Binance:* Selling KEY/ETH\n'
|
== ('*Binance:* Selling KEY/ETH\n'
|
||||||
'*Limit:* `0.00003201`\n'
|
'*Rate:* `0.00003201`\n'
|
||||||
'*Amount:* `1333.33333333`\n'
|
'*Amount:* `1333.33333333`\n'
|
||||||
'*Open Rate:* `0.00007500`\n'
|
'*Open Rate:* `0.00007500`\n'
|
||||||
'*Current Rate:* `0.00003201`\n'
|
'*Current Rate:* `0.00003201`\n'
|
||||||
|
@ -1266,6 +1247,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
||||||
'gain': 'loss',
|
'gain': 'loss',
|
||||||
'limit': 3.201e-05,
|
'limit': 3.201e-05,
|
||||||
'amount': 1333.3333333333335,
|
'amount': 1333.3333333333335,
|
||||||
|
'order_type': 'market',
|
||||||
'open_rate': 7.5e-05,
|
'open_rate': 7.5e-05,
|
||||||
'current_rate': 3.201e-05,
|
'current_rate': 3.201e-05,
|
||||||
'profit_amount': -0.05746268,
|
'profit_amount': -0.05746268,
|
||||||
|
@ -1275,7 +1257,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] \
|
assert msg_mock.call_args[0][0] \
|
||||||
== ('*Binance:* Selling KEY/ETH\n'
|
== ('*Binance:* Selling KEY/ETH\n'
|
||||||
'*Limit:* `0.00003201`\n'
|
'*Rate:* `0.00003201`\n'
|
||||||
'*Amount:* `1333.33333333`\n'
|
'*Amount:* `1333.33333333`\n'
|
||||||
'*Open Rate:* `0.00007500`\n'
|
'*Open Rate:* `0.00007500`\n'
|
||||||
'*Current Rate:* `0.00003201`\n'
|
'*Current Rate:* `0.00003201`\n'
|
||||||
|
@ -1363,6 +1345,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None:
|
||||||
'exchange': 'Bittrex',
|
'exchange': 'Bittrex',
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'limit': 1.099e-05,
|
'limit': 1.099e-05,
|
||||||
|
'order_type': 'limit',
|
||||||
'stake_amount': 0.001,
|
'stake_amount': 0.001,
|
||||||
'stake_amount_fiat': 0.0,
|
'stake_amount_fiat': 0.0,
|
||||||
'stake_currency': 'BTC',
|
'stake_currency': 'BTC',
|
||||||
|
@ -1370,7 +1353,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None:
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] \
|
assert msg_mock.call_args[0][0] \
|
||||||
== '*Bittrex:* Buying ETH/BTC\n' \
|
== '*Bittrex:* Buying ETH/BTC\n' \
|
||||||
'with limit `0.00001099\n' \
|
'at rate `0.00001099\n' \
|
||||||
'(0.001000 BTC)`'
|
'(0.001000 BTC)`'
|
||||||
|
|
||||||
|
|
||||||
|
@ -1391,6 +1374,7 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
|
||||||
'gain': 'loss',
|
'gain': 'loss',
|
||||||
'limit': 3.201e-05,
|
'limit': 3.201e-05,
|
||||||
'amount': 1333.3333333333335,
|
'amount': 1333.3333333333335,
|
||||||
|
'order_type': 'limit',
|
||||||
'open_rate': 7.5e-05,
|
'open_rate': 7.5e-05,
|
||||||
'current_rate': 3.201e-05,
|
'current_rate': 3.201e-05,
|
||||||
'profit_amount': -0.05746268,
|
'profit_amount': -0.05746268,
|
||||||
|
@ -1401,7 +1385,7 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] \
|
assert msg_mock.call_args[0][0] \
|
||||||
== '*Binance:* Selling KEY/ETH\n' \
|
== '*Binance:* Selling KEY/ETH\n' \
|
||||||
'*Limit:* `0.00003201`\n' \
|
'*Rate:* `0.00003201`\n' \
|
||||||
'*Amount:* `1333.33333333`\n' \
|
'*Amount:* `1333.33333333`\n' \
|
||||||
'*Open Rate:* `0.00007500`\n' \
|
'*Open Rate:* `0.00007500`\n' \
|
||||||
'*Current Rate:* `0.00003201`\n' \
|
'*Current Rate:* `0.00003201`\n' \
|
||||||
|
|
|
@ -74,6 +74,7 @@ def test_send_msg(default_conf, mocker):
|
||||||
'gain': "profit",
|
'gain': "profit",
|
||||||
'limit': 0.005,
|
'limit': 0.005,
|
||||||
'amount': 0.8,
|
'amount': 0.8,
|
||||||
|
'order_type': 'limit',
|
||||||
'open_rate': 0.004,
|
'open_rate': 0.004,
|
||||||
'current_rate': 0.005,
|
'current_rate': 0.005,
|
||||||
'profit_amount': 0.001,
|
'profit_amount': 0.001,
|
||||||
|
@ -126,6 +127,7 @@ def test_exception_send_msg(default_conf, mocker, caplog):
|
||||||
'exchange': 'Bittrex',
|
'exchange': 'Bittrex',
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'limit': 0.005,
|
'limit': 0.005,
|
||||||
|
'order_type': 'limit',
|
||||||
'stake_amount': 0.8,
|
'stake_amount': 0.8,
|
||||||
'stake_amount_fiat': 500,
|
'stake_amount_fiat': 500,
|
||||||
'stake_currency': 'BTC',
|
'stake_currency': 'BTC',
|
||||||
|
|
|
@ -10,7 +10,8 @@ from freqtrade.strategy.default_strategy import DefaultStrategy
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def result():
|
def result():
|
||||||
with open('freqtrade/tests/testdata/ETH_BTC-1m.json') as data_file:
|
with open('freqtrade/tests/testdata/ETH_BTC-1m.json') as data_file:
|
||||||
return parse_ticker_dataframe(json.load(data_file), '1m', fill_missing=True)
|
return parse_ticker_dataframe(json.load(data_file), '1m', pair="UNITTEST/BTC",
|
||||||
|
fill_missing=True)
|
||||||
|
|
||||||
|
|
||||||
def test_default_strategy_structure():
|
def test_default_strategy_structure():
|
||||||
|
|
|
@ -111,7 +111,8 @@ def test_tickerdata_to_dataframe(default_conf) -> None:
|
||||||
|
|
||||||
timerange = TimeRange(None, 'line', 0, -100)
|
timerange = TimeRange(None, 'line', 0, -100)
|
||||||
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
|
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
|
||||||
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', True)}
|
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', pair="UNITTEST/BTC",
|
||||||
|
fill_missing=True)}
|
||||||
data = strategy.tickerdata_to_dataframe(tickerlist)
|
data = strategy.tickerdata_to_dataframe(tickerlist)
|
||||||
assert len(data['UNITTEST/BTC']) == 102 # partial candle was removed
|
assert len(data['UNITTEST/BTC']) == 102 # partial candle was removed
|
||||||
|
|
||||||
|
|
|
@ -63,27 +63,22 @@ def test_search_strategy():
|
||||||
|
|
||||||
def test_load_strategy(result):
|
def test_load_strategy(result):
|
||||||
resolver = StrategyResolver({'strategy': 'TestStrategy'})
|
resolver = StrategyResolver({'strategy': 'TestStrategy'})
|
||||||
metadata = {'pair': 'ETH/BTC'}
|
assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
|
||||||
assert 'adx' in resolver.strategy.advise_indicators(result, metadata=metadata)
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_strategy_byte64(result):
|
def test_load_strategy_byte64(result):
|
||||||
with open("freqtrade/tests/strategy/test_strategy.py", "r") as file:
|
with open("freqtrade/tests/strategy/test_strategy.py", "r") as file:
|
||||||
encoded_string = urlsafe_b64encode(file.read().encode("utf-8")).decode("utf-8")
|
encoded_string = urlsafe_b64encode(file.read().encode("utf-8")).decode("utf-8")
|
||||||
resolver = StrategyResolver({'strategy': 'TestStrategy:{}'.format(encoded_string)})
|
resolver = StrategyResolver({'strategy': 'TestStrategy:{}'.format(encoded_string)})
|
||||||
assert 'adx' in resolver.strategy.advise_indicators(result, 'ETH/BTC')
|
assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
|
||||||
|
|
||||||
|
|
||||||
def test_load_strategy_invalid_directory(result, caplog):
|
def test_load_strategy_invalid_directory(result, caplog):
|
||||||
resolver = StrategyResolver()
|
resolver = StrategyResolver()
|
||||||
extra_dir = path.join('some', 'path')
|
extra_dir = Path.cwd() / 'some/path'
|
||||||
resolver._load_strategy('TestStrategy', config={}, extra_dir=extra_dir)
|
resolver._load_strategy('TestStrategy', config={}, extra_dir=extra_dir)
|
||||||
|
|
||||||
assert (
|
assert log_has_re(r'Path .*' + r'some.*path.*' + r'.* does not exist', caplog.record_tuples)
|
||||||
'freqtrade.resolvers.strategy_resolver',
|
|
||||||
logging.WARNING,
|
|
||||||
'Path "{}" does not exist'.format(extra_dir),
|
|
||||||
) in caplog.record_tuples
|
|
||||||
|
|
||||||
assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
|
assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
|
||||||
|
|
||||||
|
@ -371,7 +366,7 @@ def test_deprecate_populate_indicators(result):
|
||||||
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")
|
||||||
indicators = resolver.strategy.advise_indicators(result, 'ETH/BTC')
|
indicators = resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
|
||||||
assert len(w) == 1
|
assert len(w) == 1
|
||||||
assert issubclass(w[-1].category, DeprecationWarning)
|
assert issubclass(w[-1].category, DeprecationWarning)
|
||||||
assert "deprecated - check out the Sample strategy to see the current function headers!" \
|
assert "deprecated - check out the Sample strategy to see the current function headers!" \
|
||||||
|
@ -380,7 +375,7 @@ def test_deprecate_populate_indicators(result):
|
||||||
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")
|
||||||
resolver.strategy.advise_buy(indicators, 'ETH/BTC')
|
resolver.strategy.advise_buy(indicators, {'pair': 'ETH/BTC'})
|
||||||
assert len(w) == 1
|
assert len(w) == 1
|
||||||
assert issubclass(w[-1].category, DeprecationWarning)
|
assert issubclass(w[-1].category, DeprecationWarning)
|
||||||
assert "deprecated - check out the Sample strategy to see the current function headers!" \
|
assert "deprecated - check out the Sample strategy to see the current function headers!" \
|
||||||
|
@ -389,7 +384,7 @@ def test_deprecate_populate_indicators(result):
|
||||||
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")
|
||||||
resolver.strategy.advise_sell(indicators, 'ETH_BTC')
|
resolver.strategy.advise_sell(indicators, {'pair': 'ETH_BTC'})
|
||||||
assert len(w) == 1
|
assert len(w) == 1
|
||||||
assert issubclass(w[-1].category, DeprecationWarning)
|
assert issubclass(w[-1].category, DeprecationWarning)
|
||||||
assert "deprecated - check out the Sample strategy to see the current function headers!" \
|
assert "deprecated - check out the Sample strategy to see the current function headers!" \
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
# pragma pylint: disable=missing-docstring, C0103
|
# pragma pylint: disable=missing-docstring, C0103
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -48,9 +47,9 @@ def test_parse_args_verbose() -> None:
|
||||||
assert args.loglevel == 1
|
assert args.loglevel == 1
|
||||||
|
|
||||||
|
|
||||||
def test_scripts_options() -> None:
|
def test_common_scripts_options() -> None:
|
||||||
arguments = Arguments(['-p', 'ETH/BTC'], '')
|
arguments = Arguments(['-p', 'ETH/BTC'], '')
|
||||||
arguments.scripts_options()
|
arguments.common_scripts_options()
|
||||||
args = arguments.get_parsed_arg()
|
args = arguments.get_parsed_arg()
|
||||||
assert args.pairs == 'ETH/BTC'
|
assert args.pairs == 'ETH/BTC'
|
||||||
|
|
||||||
|
@ -171,17 +170,54 @@ def test_parse_args_hyperopt_custom() -> None:
|
||||||
assert call_args.func is not None
|
assert call_args.func is not None
|
||||||
|
|
||||||
|
|
||||||
def test_testdata_dl_options() -> None:
|
def test_download_data_options() -> None:
|
||||||
args = [
|
args = [
|
||||||
'--pairs-file', 'file_with_pairs',
|
'--pairs-file', 'file_with_pairs',
|
||||||
'--export', 'export/folder',
|
'--datadir', 'datadir/folder',
|
||||||
'--days', '30',
|
'--days', '30',
|
||||||
'--exchange', 'binance'
|
'--exchange', 'binance'
|
||||||
]
|
]
|
||||||
arguments = Arguments(args, '')
|
arguments = Arguments(args, '')
|
||||||
arguments.testdata_dl_options()
|
arguments.common_options()
|
||||||
|
arguments.download_data_options()
|
||||||
args = arguments.parse_args()
|
args = arguments.parse_args()
|
||||||
assert args.pairs_file == 'file_with_pairs'
|
assert args.pairs_file == 'file_with_pairs'
|
||||||
assert args.export == 'export/folder'
|
assert args.datadir == 'datadir/folder'
|
||||||
assert args.days == 30
|
assert args.days == 30
|
||||||
assert args.exchange == 'binance'
|
assert args.exchange == 'binance'
|
||||||
|
|
||||||
|
|
||||||
|
def test_plot_dataframe_options() -> None:
|
||||||
|
args = [
|
||||||
|
'--indicators1', 'sma10,sma100',
|
||||||
|
'--indicators2', 'macd,fastd,fastk',
|
||||||
|
'--plot-limit', '30',
|
||||||
|
'-p', 'UNITTEST/BTC',
|
||||||
|
]
|
||||||
|
arguments = Arguments(args, '')
|
||||||
|
arguments.common_scripts_options()
|
||||||
|
arguments.plot_dataframe_options()
|
||||||
|
pargs = arguments.parse_args(True)
|
||||||
|
assert pargs.indicators1 == "sma10,sma100"
|
||||||
|
assert pargs.indicators2 == "macd,fastd,fastk"
|
||||||
|
assert pargs.plot_limit == 30
|
||||||
|
assert pargs.pairs == "UNITTEST/BTC"
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_int_positive() -> None:
|
||||||
|
|
||||||
|
assert Arguments.check_int_positive("3") == 3
|
||||||
|
assert Arguments.check_int_positive("1") == 1
|
||||||
|
assert Arguments.check_int_positive("100") == 100
|
||||||
|
|
||||||
|
with pytest.raises(argparse.ArgumentTypeError):
|
||||||
|
Arguments.check_int_positive("-2")
|
||||||
|
|
||||||
|
with pytest.raises(argparse.ArgumentTypeError):
|
||||||
|
Arguments.check_int_positive("0")
|
||||||
|
|
||||||
|
with pytest.raises(argparse.ArgumentTypeError):
|
||||||
|
Arguments.check_int_positive("3.5")
|
||||||
|
|
||||||
|
with pytest.raises(argparse.ArgumentTypeError):
|
||||||
|
Arguments.check_int_positive("DeadBeef")
|
||||||
|
|
|
@ -15,7 +15,16 @@ from freqtrade.arguments import Arguments
|
||||||
from freqtrade.configuration import Configuration, set_loggers
|
from freqtrade.configuration import Configuration, set_loggers
|
||||||
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.state import RunMode
|
from freqtrade.state import RunMode
|
||||||
from freqtrade.tests.conftest import log_has
|
from freqtrade.tests.conftest import log_has, log_has_re
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def all_conf():
|
||||||
|
config_file = Path(__file__).parents[2] / "config_full.json.example"
|
||||||
|
print(config_file)
|
||||||
|
configuration = Configuration(Namespace())
|
||||||
|
conf = configuration._load_config_file(str(config_file))
|
||||||
|
return conf
|
||||||
|
|
||||||
|
|
||||||
def test_load_config_invalid_pair(default_conf) -> None:
|
def test_load_config_invalid_pair(default_conf) -> None:
|
||||||
|
@ -351,11 +360,8 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
||||||
caplog.record_tuples
|
caplog.record_tuples
|
||||||
)
|
)
|
||||||
assert 'ticker_interval' in config
|
assert 'ticker_interval' in config
|
||||||
assert log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
|
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
||||||
assert log_has(
|
caplog.record_tuples)
|
||||||
'Using ticker_interval: 1m ...',
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
|
|
||||||
assert 'live' in config
|
assert 'live' in config
|
||||||
assert log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
assert log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
||||||
|
@ -416,11 +422,8 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non
|
||||||
caplog.record_tuples
|
caplog.record_tuples
|
||||||
)
|
)
|
||||||
assert 'ticker_interval' in config
|
assert 'ticker_interval' in config
|
||||||
assert log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
|
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
||||||
assert log_has(
|
caplog.record_tuples)
|
||||||
'Using ticker_interval: 1m ...',
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
|
|
||||||
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.record_tuples)
|
||||||
|
@ -454,8 +457,8 @@ 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 ...', caplog.record_tuples)
|
assert log_has('Parameter --epochs detected ... Will run Hyperopt with for 10 epochs ...',
|
||||||
assert log_has('Will run Hyperopt with for 10 epochs ...', caplog.record_tuples)
|
caplog.record_tuples)
|
||||||
|
|
||||||
assert 'spaces' in config
|
assert 'spaces' in config
|
||||||
assert config['spaces'] == ['all']
|
assert config['spaces'] == ['all']
|
||||||
|
@ -467,21 +470,52 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
|
||||||
def test_check_exchange(default_conf, caplog) -> None:
|
def test_check_exchange(default_conf, caplog) -> None:
|
||||||
configuration = Configuration(Namespace())
|
configuration = Configuration(Namespace())
|
||||||
|
|
||||||
# Test a valid exchange
|
# Test an officially supported by Freqtrade team exchange
|
||||||
default_conf.get('exchange').update({'name': 'BITTREX'})
|
default_conf.get('exchange').update({'name': 'BITTREX'})
|
||||||
assert configuration.check_exchange(default_conf)
|
assert configuration.check_exchange(default_conf)
|
||||||
|
assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.",
|
||||||
|
caplog.record_tuples)
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
# Test a valid exchange
|
# Test an officially supported by Freqtrade team exchange
|
||||||
default_conf.get('exchange').update({'name': 'binance'})
|
default_conf.get('exchange').update({'name': 'binance'})
|
||||||
assert configuration.check_exchange(default_conf)
|
assert configuration.check_exchange(default_conf)
|
||||||
|
assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.",
|
||||||
|
caplog.record_tuples)
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
# Test a invalid exchange
|
# Test an available exchange, supported by ccxt
|
||||||
|
default_conf.get('exchange').update({'name': 'kraken'})
|
||||||
|
assert configuration.check_exchange(default_conf)
|
||||||
|
assert log_has_re(r"Exchange .* is supported by ccxt and .* not officially supported "
|
||||||
|
r"by the Freqtrade development team\. .*",
|
||||||
|
caplog.record_tuples)
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
|
# Test a 'bad' exchange, which known to have serious problems
|
||||||
|
default_conf.get('exchange').update({'name': 'bitmex'})
|
||||||
|
assert not configuration.check_exchange(default_conf)
|
||||||
|
assert log_has_re(r"Exchange .* is known to not work with the bot yet\. "
|
||||||
|
r"Use it only for development and testing purposes\.",
|
||||||
|
caplog.record_tuples)
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
|
# Test a 'bad' exchange with check_for_bad=False
|
||||||
|
default_conf.get('exchange').update({'name': 'bitmex'})
|
||||||
|
assert configuration.check_exchange(default_conf, False)
|
||||||
|
assert log_has_re(r"Exchange .* is supported by ccxt and .* not officially supported "
|
||||||
|
r"by the Freqtrade development team\. .*",
|
||||||
|
caplog.record_tuples)
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
|
# Test an invalid exchange
|
||||||
default_conf.get('exchange').update({'name': 'unknown_exchange'})
|
default_conf.get('exchange').update({'name': 'unknown_exchange'})
|
||||||
configuration.config = default_conf
|
configuration.config = default_conf
|
||||||
|
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
OperationalException,
|
OperationalException,
|
||||||
match=r'.*Exchange "unknown_exchange" not supported.*'
|
match=r'.*Exchange "unknown_exchange" is not supported by ccxt '
|
||||||
|
r'and therefore not available for the bot.*'
|
||||||
):
|
):
|
||||||
configuration.check_exchange(default_conf)
|
configuration.check_exchange(default_conf)
|
||||||
|
|
||||||
|
@ -608,3 +642,59 @@ def test_validate_tsl(default_conf):
|
||||||
default_conf['trailing_stop_positive_offset'] = 0.015
|
default_conf['trailing_stop_positive_offset'] = 0.015
|
||||||
Configuration(Namespace())
|
Configuration(Namespace())
|
||||||
configuration._validate_config_consistency(default_conf)
|
configuration._validate_config_consistency(default_conf)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_config_default_exchange(all_conf) -> None:
|
||||||
|
"""
|
||||||
|
config['exchange'] subtree has required options in it
|
||||||
|
so it cannot be omitted in the config
|
||||||
|
"""
|
||||||
|
del all_conf['exchange']
|
||||||
|
|
||||||
|
assert 'exchange' not in all_conf
|
||||||
|
|
||||||
|
with pytest.raises(ValidationError,
|
||||||
|
match=r'\'exchange\' is a required property'):
|
||||||
|
configuration = Configuration(Namespace())
|
||||||
|
configuration._validate_config_schema(all_conf)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_config_default_exchange_name(all_conf) -> None:
|
||||||
|
"""
|
||||||
|
config['exchange']['name'] option is required
|
||||||
|
so it cannot be omitted in the config
|
||||||
|
"""
|
||||||
|
del all_conf['exchange']['name']
|
||||||
|
|
||||||
|
assert 'name' not in all_conf['exchange']
|
||||||
|
|
||||||
|
with pytest.raises(ValidationError,
|
||||||
|
match=r'\'name\' is a required property'):
|
||||||
|
configuration = Configuration(Namespace())
|
||||||
|
configuration._validate_config_schema(all_conf)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("keys", [("exchange", "sandbox", False),
|
||||||
|
("exchange", "key", ""),
|
||||||
|
("exchange", "secret", ""),
|
||||||
|
("exchange", "password", ""),
|
||||||
|
])
|
||||||
|
def test_load_config_default_subkeys(all_conf, keys) -> None:
|
||||||
|
"""
|
||||||
|
Test for parameters with default values in sub-paths
|
||||||
|
so they can be omitted in the config and the default value
|
||||||
|
should is added to the config.
|
||||||
|
"""
|
||||||
|
# Get first level key
|
||||||
|
key = keys[0]
|
||||||
|
# get second level key
|
||||||
|
subkey = keys[1]
|
||||||
|
|
||||||
|
del all_conf[key][subkey]
|
||||||
|
|
||||||
|
assert subkey not in all_conf[key]
|
||||||
|
|
||||||
|
configuration = Configuration(Namespace())
|
||||||
|
configuration._validate_config_schema(all_conf)
|
||||||
|
assert subkey in all_conf[key]
|
||||||
|
assert all_conf[key][subkey] == keys[2]
|
||||||
|
|
|
@ -11,64 +11,21 @@ import arrow
|
||||||
import pytest
|
import pytest
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from freqtrade import (DependencyException, OperationalException,
|
from freqtrade import (DependencyException, InvalidOrderException,
|
||||||
TemporaryError, InvalidOrderException, constants)
|
OperationalException, TemporaryError, constants)
|
||||||
from freqtrade.data.dataprovider import DataProvider
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
from freqtrade.freqtradebot import FreqtradeBot
|
from freqtrade.freqtradebot import FreqtradeBot
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.rpc import RPCMessageType
|
from freqtrade.rpc import RPCMessageType
|
||||||
from freqtrade.state import State
|
from freqtrade.state import State
|
||||||
from freqtrade.strategy.interface import SellCheckTuple, SellType
|
from freqtrade.strategy.interface import SellCheckTuple, SellType
|
||||||
from freqtrade.tests.conftest import (log_has, log_has_re, patch_edge,
|
from freqtrade.tests.conftest import (get_patched_freqtradebot,
|
||||||
patch_exchange, patch_wallet)
|
get_patched_worker, log_has, log_has_re,
|
||||||
|
patch_edge, patch_exchange,
|
||||||
|
patch_get_signal, patch_wallet)
|
||||||
from freqtrade.worker import Worker
|
from freqtrade.worker import Worker
|
||||||
|
|
||||||
|
|
||||||
# Functions for recurrent object patching
|
|
||||||
def patch_freqtradebot(mocker, config) -> None:
|
|
||||||
"""
|
|
||||||
This function patches _init_modules() to not call dependencies
|
|
||||||
:param mocker: a Mocker object to apply patches
|
|
||||||
:param config: Config to pass to the bot
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
|
||||||
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
|
|
||||||
patch_exchange(mocker)
|
|
||||||
|
|
||||||
|
|
||||||
def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
|
|
||||||
"""
|
|
||||||
This function patches _init_modules() to not call dependencies
|
|
||||||
:param mocker: a Mocker object to apply patches
|
|
||||||
:param config: Config to pass to the bot
|
|
||||||
:return: FreqtradeBot
|
|
||||||
"""
|
|
||||||
patch_freqtradebot(mocker, config)
|
|
||||||
return FreqtradeBot(config)
|
|
||||||
|
|
||||||
|
|
||||||
def get_patched_worker(mocker, config) -> Worker:
|
|
||||||
"""
|
|
||||||
This function patches _init_modules() to not call dependencies
|
|
||||||
:param mocker: a Mocker object to apply patches
|
|
||||||
:param config: Config to pass to the bot
|
|
||||||
:return: Worker
|
|
||||||
"""
|
|
||||||
patch_freqtradebot(mocker, config)
|
|
||||||
return Worker(args=None, config=config)
|
|
||||||
|
|
||||||
|
|
||||||
def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None:
|
|
||||||
"""
|
|
||||||
:param mocker: mocker to patch IStrategy class
|
|
||||||
:param value: which value IStrategy.get_signal() must return
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
freqtrade.strategy.get_signal = lambda e, s, t: value
|
|
||||||
freqtrade.exchange.refresh_latest_ohlcv = lambda p: None
|
|
||||||
|
|
||||||
|
|
||||||
def patch_RPCManager(mocker) -> MagicMock:
|
def patch_RPCManager(mocker) -> MagicMock:
|
||||||
"""
|
"""
|
||||||
This function mock RPC manager to avoid repeating this code in almost every tests
|
This function mock RPC manager to avoid repeating this code in almost every tests
|
||||||
|
@ -114,6 +71,7 @@ def test_cleanup(mocker, default_conf, caplog) -> None:
|
||||||
def test_worker_running(mocker, default_conf, caplog) -> None:
|
def test_worker_running(mocker, default_conf, caplog) -> None:
|
||||||
mock_throttle = MagicMock()
|
mock_throttle = MagicMock()
|
||||||
mocker.patch('freqtrade.worker.Worker._throttle', mock_throttle)
|
mocker.patch('freqtrade.worker.Worker._throttle', mock_throttle)
|
||||||
|
mocker.patch('freqtrade.persistence.Trade.stoploss_reinitialization', MagicMock())
|
||||||
|
|
||||||
worker = get_patched_worker(mocker, default_conf)
|
worker = get_patched_worker(mocker, default_conf)
|
||||||
|
|
||||||
|
@ -1184,6 +1142,77 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
|
||||||
stop_price=0.00002344 * 0.95)
|
stop_price=0.00002344 * 0.95)
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, caplog,
|
||||||
|
markets, limit_buy_order,
|
||||||
|
limit_sell_order) -> None:
|
||||||
|
# When trailing stoploss is set
|
||||||
|
stoploss_limit = MagicMock(return_value={'id': 13434334})
|
||||||
|
patch_exchange(mocker)
|
||||||
|
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_ticker=MagicMock(return_value={
|
||||||
|
'bid': 0.00001172,
|
||||||
|
'ask': 0.00001173,
|
||||||
|
'last': 0.00001172
|
||||||
|
}),
|
||||||
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
|
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
||||||
|
get_fee=fee,
|
||||||
|
markets=PropertyMock(return_value=markets),
|
||||||
|
stoploss_limit=stoploss_limit
|
||||||
|
)
|
||||||
|
|
||||||
|
# enabling TSL
|
||||||
|
default_conf['trailing_stop'] = True
|
||||||
|
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
# enabling stoploss on exchange
|
||||||
|
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
||||||
|
|
||||||
|
# setting stoploss
|
||||||
|
freqtrade.strategy.stoploss = -0.05
|
||||||
|
|
||||||
|
# setting stoploss_on_exchange_interval to 60 seconds
|
||||||
|
freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 60
|
||||||
|
patch_get_signal(freqtrade)
|
||||||
|
freqtrade.create_trade()
|
||||||
|
trade = Trade.query.first()
|
||||||
|
trade.is_open = True
|
||||||
|
trade.open_order_id = None
|
||||||
|
trade.stoploss_order_id = "abcd"
|
||||||
|
trade.stop_loss = 0.2
|
||||||
|
trade.stoploss_last_update = arrow.utcnow().shift(minutes=-601).datetime.replace(tzinfo=None)
|
||||||
|
|
||||||
|
stoploss_order_hanging = {
|
||||||
|
'id': "abcd",
|
||||||
|
'status': 'open',
|
||||||
|
'type': 'stop_loss_limit',
|
||||||
|
'price': 3,
|
||||||
|
'average': 2,
|
||||||
|
'info': {
|
||||||
|
'stopPrice': '0.1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException())
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hanging)
|
||||||
|
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
|
||||||
|
assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*",
|
||||||
|
caplog.record_tuples)
|
||||||
|
|
||||||
|
# Still try to create order
|
||||||
|
assert stoploss_limit.call_count == 1
|
||||||
|
|
||||||
|
# Fail creating stoploss order
|
||||||
|
caplog.clear()
|
||||||
|
cancel_mock = mocker.patch("freqtrade.exchange.Exchange.cancel_order", MagicMock())
|
||||||
|
mocker.patch("freqtrade.exchange.Exchange.stoploss_limit", side_effect=DependencyException())
|
||||||
|
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
|
||||||
|
assert cancel_mock.call_count == 1
|
||||||
|
assert log_has_re(r"Could create trailing stoploss order for pair ETH/BTC\..*",
|
||||||
|
caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
|
def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
|
||||||
markets, limit_buy_order, limit_sell_order) -> None:
|
markets, limit_buy_order, limit_sell_order) -> None:
|
||||||
|
|
||||||
|
@ -1407,7 +1436,8 @@ def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_
|
||||||
amount=amount,
|
amount=amount,
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
open_rate=0.245441,
|
open_rate=0.245441,
|
||||||
open_order_id="123456"
|
open_order_id="123456",
|
||||||
|
is_open=True,
|
||||||
)
|
)
|
||||||
freqtrade.update_trade_state(trade, limit_buy_order)
|
freqtrade.update_trade_state(trade, limit_buy_order)
|
||||||
assert trade.amount != amount
|
assert trade.amount != amount
|
||||||
|
@ -1432,6 +1462,35 @@ def test_update_trade_state_exception(mocker, default_conf,
|
||||||
assert log_has('Could not update trade amount: ', caplog.record_tuples)
|
assert log_has('Could not update trade amount: ', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_order, mocker):
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
|
||||||
|
# get_order should not be called!!
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_order', MagicMock(side_effect=ValueError))
|
||||||
|
wallet_mock = MagicMock()
|
||||||
|
mocker.patch('freqtrade.wallets.Wallets.update', wallet_mock)
|
||||||
|
|
||||||
|
patch_exchange(mocker)
|
||||||
|
Trade.session = MagicMock()
|
||||||
|
amount = limit_sell_order["amount"]
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
wallet_mock.reset_mock()
|
||||||
|
trade = Trade(
|
||||||
|
pair='LTC/ETH',
|
||||||
|
amount=amount,
|
||||||
|
exchange='binance',
|
||||||
|
open_rate=0.245441,
|
||||||
|
fee_open=0.0025,
|
||||||
|
fee_close=0.0025,
|
||||||
|
open_order_id="123456",
|
||||||
|
is_open=True,
|
||||||
|
)
|
||||||
|
freqtrade.update_trade_state(trade, limit_sell_order)
|
||||||
|
assert trade.amount == limit_sell_order['amount']
|
||||||
|
# Wallet needs to be updated after closing a limit-sell order to reenable buying
|
||||||
|
assert wallet_mock.call_count == 1
|
||||||
|
assert not trade.is_open
|
||||||
|
|
||||||
|
|
||||||
def test_handle_trade(default_conf, limit_buy_order, limit_sell_order,
|
def test_handle_trade(default_conf, limit_buy_order, limit_sell_order,
|
||||||
fee, markets, mocker) -> None:
|
fee, markets, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
@ -1972,6 +2031,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, moc
|
||||||
'gain': 'profit',
|
'gain': 'profit',
|
||||||
'limit': 1.172e-05,
|
'limit': 1.172e-05,
|
||||||
'amount': 90.99181073703367,
|
'amount': 90.99181073703367,
|
||||||
|
'order_type': 'limit',
|
||||||
'open_rate': 1.099e-05,
|
'open_rate': 1.099e-05,
|
||||||
'current_rate': 1.172e-05,
|
'current_rate': 1.172e-05,
|
||||||
'profit_amount': 6.126e-05,
|
'profit_amount': 6.126e-05,
|
||||||
|
@ -2018,6 +2078,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets,
|
||||||
'gain': 'loss',
|
'gain': 'loss',
|
||||||
'limit': 1.044e-05,
|
'limit': 1.044e-05,
|
||||||
'amount': 90.99181073703367,
|
'amount': 90.99181073703367,
|
||||||
|
'order_type': 'limit',
|
||||||
'open_rate': 1.099e-05,
|
'open_rate': 1.099e-05,
|
||||||
'current_rate': 1.044e-05,
|
'current_rate': 1.044e-05,
|
||||||
'profit_amount': -5.492e-05,
|
'profit_amount': -5.492e-05,
|
||||||
|
@ -2072,6 +2133,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe
|
||||||
'gain': 'loss',
|
'gain': 'loss',
|
||||||
'limit': 1.08801e-05,
|
'limit': 1.08801e-05,
|
||||||
'amount': 90.99181073703367,
|
'amount': 90.99181073703367,
|
||||||
|
'order_type': 'limit',
|
||||||
'open_rate': 1.099e-05,
|
'open_rate': 1.099e-05,
|
||||||
'current_rate': 1.044e-05,
|
'current_rate': 1.044e-05,
|
||||||
'profit_amount': -1.498e-05,
|
'profit_amount': -1.498e-05,
|
||||||
|
@ -2083,6 +2145,36 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe
|
||||||
} == last_msg
|
} == last_msg
|
||||||
|
|
||||||
|
|
||||||
|
def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee,
|
||||||
|
markets, caplog) -> None:
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException())
|
||||||
|
sellmock = MagicMock()
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
_load_markets=MagicMock(return_value={}),
|
||||||
|
get_ticker=ticker,
|
||||||
|
get_fee=fee,
|
||||||
|
markets=PropertyMock(return_value=markets),
|
||||||
|
sell=sellmock
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
||||||
|
patch_get_signal(freqtrade)
|
||||||
|
freqtrade.create_trade()
|
||||||
|
|
||||||
|
trade = Trade.query.first()
|
||||||
|
Trade.session = MagicMock()
|
||||||
|
|
||||||
|
freqtrade.config['dry_run'] = False
|
||||||
|
trade.stoploss_order_id = "abcd"
|
||||||
|
|
||||||
|
freqtrade.execute_sell(trade=trade, limit=1234,
|
||||||
|
sell_reason=SellType.STOP_LOSS)
|
||||||
|
assert sellmock.call_count == 1
|
||||||
|
assert log_has('Could not cancel stoploss order abcd', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_execute_sell_with_stoploss_on_exchange(default_conf,
|
def test_execute_sell_with_stoploss_on_exchange(default_conf,
|
||||||
ticker, fee, ticker_sell_up,
|
ticker, fee, ticker_sell_up,
|
||||||
markets, mocker) -> None:
|
markets, mocker) -> None:
|
||||||
|
@ -2243,6 +2335,7 @@ def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee,
|
||||||
'gain': 'profit',
|
'gain': 'profit',
|
||||||
'limit': 1.172e-05,
|
'limit': 1.172e-05,
|
||||||
'amount': 90.99181073703367,
|
'amount': 90.99181073703367,
|
||||||
|
'order_type': 'limit',
|
||||||
'open_rate': 1.099e-05,
|
'open_rate': 1.099e-05,
|
||||||
'current_rate': 1.172e-05,
|
'current_rate': 1.172e-05,
|
||||||
'profit_amount': 6.126e-05,
|
'profit_amount': 6.126e-05,
|
||||||
|
@ -2290,6 +2383,7 @@ def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee,
|
||||||
'gain': 'loss',
|
'gain': 'loss',
|
||||||
'limit': 1.044e-05,
|
'limit': 1.044e-05,
|
||||||
'amount': 90.99181073703367,
|
'amount': 90.99181073703367,
|
||||||
|
'order_type': 'limit',
|
||||||
'open_rate': 1.099e-05,
|
'open_rate': 1.099e-05,
|
||||||
'current_rate': 1.044e-05,
|
'current_rate': 1.044e-05,
|
||||||
'profit_amount': -5.492e-05,
|
'profit_amount': -5.492e-05,
|
||||||
|
@ -2463,9 +2557,9 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, markets, caplog,
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
get_ticker=MagicMock(return_value={
|
get_ticker=MagicMock(return_value={
|
||||||
'bid': 0.00000102,
|
'bid': 0.00001099,
|
||||||
'ask': 0.00000103,
|
'ask': 0.00001099,
|
||||||
'last': 0.00000102
|
'last': 0.00001099
|
||||||
}),
|
}),
|
||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
|
@ -2477,15 +2571,33 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, markets, caplog,
|
||||||
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
|
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
|
||||||
|
|
||||||
freqtrade.create_trade()
|
freqtrade.create_trade()
|
||||||
|
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
trade.update(limit_buy_order)
|
assert freqtrade.handle_trade(trade) is False
|
||||||
trade.max_rate = trade.open_rate * 1.003
|
|
||||||
|
# Raise ticker above buy price
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker',
|
||||||
|
MagicMock(return_value={
|
||||||
|
'bid': 0.00001099 * 1.5,
|
||||||
|
'ask': 0.00001099 * 1.5,
|
||||||
|
'last': 0.00001099 * 1.5
|
||||||
|
}))
|
||||||
|
|
||||||
|
# Stoploss should be adjusted
|
||||||
|
assert freqtrade.handle_trade(trade) is False
|
||||||
|
|
||||||
|
# Price fell
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker',
|
||||||
|
MagicMock(return_value={
|
||||||
|
'bid': 0.00001099 * 1.1,
|
||||||
|
'ask': 0.00001099 * 1.1,
|
||||||
|
'last': 0.00001099 * 1.1
|
||||||
|
}))
|
||||||
|
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
# Sell as trailing-stop is reached
|
# Sell as trailing-stop is reached
|
||||||
assert freqtrade.handle_trade(trade) is True
|
assert freqtrade.handle_trade(trade) is True
|
||||||
assert log_has(
|
assert log_has(
|
||||||
f'HIT STOP: current price at 0.000001, stop loss is {trade.stop_loss:.6f}, '
|
f'HIT STOP: current price at 0.000012, stop loss is 0.000015, '
|
||||||
f'initial stop loss was at 0.000010, trade opened at 0.000011', caplog.record_tuples)
|
f'initial stop loss was at 0.000010, trade opened at 0.000011', caplog.record_tuples)
|
||||||
assert trade.sell_reason == SellType.TRAILING_STOP_LOSS.value
|
assert trade.sell_reason == SellType.TRAILING_STOP_LOSS.value
|
||||||
|
|
||||||
|
@ -3105,10 +3217,27 @@ def test_get_sell_rate(default_conf, mocker, ticker, order_book_l2) -> None:
|
||||||
assert rate == 0.043936
|
assert rate == 0.043936
|
||||||
|
|
||||||
|
|
||||||
def test_startup_messages(default_conf, mocker):
|
def test_startup_state(default_conf, mocker):
|
||||||
default_conf['pairlist'] = {'method': 'VolumePairList',
|
default_conf['pairlist'] = {'method': 'VolumePairList',
|
||||||
'config': {'number_assets': 20}
|
'config': {'number_assets': 20}
|
||||||
}
|
}
|
||||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||||
worker = get_patched_worker(mocker, default_conf)
|
worker = get_patched_worker(mocker, default_conf)
|
||||||
assert worker.state is State.RUNNING
|
assert worker.state is State.RUNNING
|
||||||
|
|
||||||
|
|
||||||
|
def test_startup_trade_reinit(default_conf, edge_conf, mocker):
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||||
|
reinit_mock = MagicMock()
|
||||||
|
mocker.patch('freqtrade.persistence.Trade.stoploss_reinitialization', reinit_mock)
|
||||||
|
|
||||||
|
ftbot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
ftbot.startup()
|
||||||
|
assert reinit_mock.call_count == 1
|
||||||
|
|
||||||
|
reinit_mock.reset_mock()
|
||||||
|
|
||||||
|
ftbot = get_patched_freqtradebot(mocker, edge_conf)
|
||||||
|
ftbot.startup()
|
||||||
|
assert reinit_mock.call_count == 0
|
||||||
|
|
|
@ -7,10 +7,11 @@ import pytest
|
||||||
|
|
||||||
from freqtrade import OperationalException
|
from freqtrade import OperationalException
|
||||||
from freqtrade.arguments import Arguments
|
from freqtrade.arguments import Arguments
|
||||||
from freqtrade.worker import Worker
|
from freqtrade.freqtradebot import FreqtradeBot
|
||||||
from freqtrade.main import main
|
from freqtrade.main import main
|
||||||
from freqtrade.state import State
|
from freqtrade.state import State
|
||||||
from freqtrade.tests.conftest import log_has, patch_exchange
|
from freqtrade.tests.conftest import log_has, patch_exchange
|
||||||
|
from freqtrade.worker import Worker
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_backtesting(mocker) -> None:
|
def test_parse_args_backtesting(mocker) -> None:
|
||||||
|
@ -18,8 +19,10 @@ def test_parse_args_backtesting(mocker) -> None:
|
||||||
Test that main() can start backtesting and also ensure we can pass some specific arguments
|
Test that main() can start backtesting and also ensure we can pass some specific arguments
|
||||||
further argument parsing is done in test_arguments.py
|
further argument parsing is done in test_arguments.py
|
||||||
"""
|
"""
|
||||||
backtesting_mock = mocker.patch('freqtrade.optimize.backtesting.start', MagicMock())
|
backtesting_mock = mocker.patch('freqtrade.optimize.start_backtesting', MagicMock())
|
||||||
main(['backtesting'])
|
# it's sys.exit(0) at the end of backtesting
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
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']
|
||||||
|
@ -31,8 +34,10 @@ 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.hyperopt.start', MagicMock())
|
hyperopt_mock = mocker.patch('freqtrade.optimize.start_hyperopt', MagicMock())
|
||||||
main(['hyperopt'])
|
# it's sys.exit(0) at the end of hyperopt
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
main(['hyperopt'])
|
||||||
assert hyperopt_mock.call_count == 1
|
assert hyperopt_mock.call_count == 1
|
||||||
call_args = hyperopt_mock.call_args[0][0]
|
call_args = hyperopt_mock.call_args[0][0]
|
||||||
assert call_args.config == ['config.json']
|
assert call_args.config == ['config.json']
|
||||||
|
@ -107,24 +112,30 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None:
|
||||||
def test_main_reload_conf(mocker, default_conf, caplog) -> None:
|
def test_main_reload_conf(mocker, default_conf, caplog) -> None:
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock())
|
||||||
mocker.patch('freqtrade.worker.Worker._worker', MagicMock(return_value=State.RELOAD_CONF))
|
# Simulate Running, reload, running workflow
|
||||||
|
worker_mock = MagicMock(side_effect=[State.RUNNING,
|
||||||
|
State.RELOAD_CONF,
|
||||||
|
State.RUNNING,
|
||||||
|
OperationalException("Oh snap!")])
|
||||||
|
mocker.patch('freqtrade.worker.Worker._worker', worker_mock)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
'freqtrade.configuration.Configuration._load_config_file',
|
'freqtrade.configuration.Configuration._load_config_file',
|
||||||
lambda *args, **kwargs: default_conf
|
lambda *args, **kwargs: default_conf
|
||||||
)
|
)
|
||||||
|
reconfigure_mock = mocker.patch('freqtrade.main.Worker._reconfigure', MagicMock())
|
||||||
|
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
|
||||||
|
|
||||||
# Raise exception as side effect to avoid endless loop
|
args = Arguments(['-c', 'config.json.example'], '').get_parsed_arg()
|
||||||
reconfigure_mock = mocker.patch(
|
worker = Worker(args=args, config=default_conf)
|
||||||
'freqtrade.main.Worker._reconfigure', MagicMock(side_effect=Exception)
|
|
||||||
)
|
|
||||||
|
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
main(['-c', 'config.json.example'])
|
main(['-c', 'config.json.example'])
|
||||||
|
|
||||||
assert reconfigure_mock.call_count == 1
|
|
||||||
assert log_has('Using config: config.json.example ...', caplog.record_tuples)
|
assert log_has('Using config: config.json.example ...', caplog.record_tuples)
|
||||||
|
assert worker_mock.call_count == 4
|
||||||
|
assert reconfigure_mock.call_count == 1
|
||||||
|
assert isinstance(worker.freqtrade, FreqtradeBot)
|
||||||
|
|
||||||
|
|
||||||
def test_reconfigure(mocker, default_conf) -> None:
|
def test_reconfigure(mocker, default_conf) -> None:
|
||||||
|
|
|
@ -6,7 +6,7 @@ from unittest.mock import MagicMock
|
||||||
from freqtrade.data.converter import parse_ticker_dataframe
|
from freqtrade.data.converter import parse_ticker_dataframe
|
||||||
from freqtrade.misc import (common_datearray, datesarray_to_datetimearray,
|
from freqtrade.misc import (common_datearray, datesarray_to_datetimearray,
|
||||||
file_dump_json, file_load_json, format_ms_time, shorten_date)
|
file_dump_json, file_load_json, format_ms_time, shorten_date)
|
||||||
from freqtrade.data.history import load_tickerdata_file, make_testdata_path
|
from freqtrade.data.history import load_tickerdata_file, pair_data_filename
|
||||||
from freqtrade.strategy.default_strategy import DefaultStrategy
|
from freqtrade.strategy.default_strategy import DefaultStrategy
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,7 +17,8 @@ def test_shorten_date() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_datesarray_to_datetimearray(ticker_history_list):
|
def test_datesarray_to_datetimearray(ticker_history_list):
|
||||||
dataframes = parse_ticker_dataframe(ticker_history_list, "5m", fill_missing=True)
|
dataframes = parse_ticker_dataframe(ticker_history_list, "5m", pair="UNITTEST/BTC",
|
||||||
|
fill_missing=True)
|
||||||
dates = datesarray_to_datetimearray(dataframes['date'])
|
dates = datesarray_to_datetimearray(dataframes['date'])
|
||||||
|
|
||||||
assert isinstance(dates[0], datetime.datetime)
|
assert isinstance(dates[0], datetime.datetime)
|
||||||
|
@ -34,7 +35,8 @@ def test_datesarray_to_datetimearray(ticker_history_list):
|
||||||
def test_common_datearray(default_conf) -> None:
|
def test_common_datearray(default_conf) -> None:
|
||||||
strategy = DefaultStrategy(default_conf)
|
strategy = DefaultStrategy(default_conf)
|
||||||
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
|
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
|
||||||
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, "1m", fill_missing=True)}
|
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, "1m", pair="UNITTEST/BTC",
|
||||||
|
fill_missing=True)}
|
||||||
dataframes = strategy.tickerdata_to_dataframe(tickerlist)
|
dataframes = strategy.tickerdata_to_dataframe(tickerlist)
|
||||||
|
|
||||||
dates = common_datearray(dataframes)
|
dates = common_datearray(dataframes)
|
||||||
|
@ -60,13 +62,13 @@ def test_file_dump_json(mocker) -> None:
|
||||||
def test_file_load_json(mocker) -> None:
|
def test_file_load_json(mocker) -> None:
|
||||||
|
|
||||||
# 7m .json does not exist
|
# 7m .json does not exist
|
||||||
ret = file_load_json(make_testdata_path(None).joinpath('UNITTEST_BTC-7m.json'))
|
ret = file_load_json(pair_data_filename(None, 'UNITTEST/BTC', '7m'))
|
||||||
assert not ret
|
assert not ret
|
||||||
# 1m json exists (but no .gz exists)
|
# 1m json exists (but no .gz exists)
|
||||||
ret = file_load_json(make_testdata_path(None).joinpath('UNITTEST_BTC-1m.json'))
|
ret = file_load_json(pair_data_filename(None, 'UNITTEST/BTC', '1m'))
|
||||||
assert ret
|
assert ret
|
||||||
# 8 .json is empty and will fail if it's loaded. .json.gz is a copy of 1.json
|
# 8 .json is empty and will fail if it's loaded. .json.gz is a copy of 1.json
|
||||||
ret = file_load_json(make_testdata_path(None).joinpath('UNITTEST_BTC-8m.json'))
|
ret = file_load_json(pair_data_filename(None, 'UNITTEST/BTC', '8m'))
|
||||||
assert ret
|
assert ret
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
# pragma pylint: disable=missing-docstring, C0103
|
# pragma pylint: disable=missing-docstring, C0103
|
||||||
from unittest.mock import MagicMock
|
|
||||||
import logging
|
import logging
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import arrow
|
||||||
import pytest
|
import pytest
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
|
|
||||||
|
@ -10,14 +11,53 @@ from freqtrade.persistence import Trade, clean_dry_run_db, init
|
||||||
from freqtrade.tests.conftest import log_has
|
from freqtrade.tests.conftest import log_has
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function')
|
def create_mock_trades(fee):
|
||||||
def init_persistence(default_conf):
|
"""
|
||||||
init(default_conf)
|
Create some fake trades ...
|
||||||
|
"""
|
||||||
|
# Simulate dry_run entries
|
||||||
|
trade = Trade(
|
||||||
|
pair='ETH/BTC',
|
||||||
|
stake_amount=0.001,
|
||||||
|
amount=123.0,
|
||||||
|
fee_open=fee.return_value,
|
||||||
|
fee_close=fee.return_value,
|
||||||
|
open_rate=0.123,
|
||||||
|
exchange='bittrex',
|
||||||
|
open_order_id='dry_run_buy_12345'
|
||||||
|
)
|
||||||
|
Trade.session.add(trade)
|
||||||
|
|
||||||
|
trade = Trade(
|
||||||
|
pair='ETC/BTC',
|
||||||
|
stake_amount=0.001,
|
||||||
|
amount=123.0,
|
||||||
|
fee_open=fee.return_value,
|
||||||
|
fee_close=fee.return_value,
|
||||||
|
open_rate=0.123,
|
||||||
|
exchange='bittrex',
|
||||||
|
is_open=False,
|
||||||
|
open_order_id='dry_run_sell_12345'
|
||||||
|
)
|
||||||
|
Trade.session.add(trade)
|
||||||
|
|
||||||
|
# Simulate prod entry
|
||||||
|
trade = Trade(
|
||||||
|
pair='ETC/BTC',
|
||||||
|
stake_amount=0.001,
|
||||||
|
amount=123.0,
|
||||||
|
fee_open=fee.return_value,
|
||||||
|
fee_close=fee.return_value,
|
||||||
|
open_rate=0.123,
|
||||||
|
exchange='bittrex',
|
||||||
|
open_order_id='prod_buy_12345'
|
||||||
|
)
|
||||||
|
Trade.session.add(trade)
|
||||||
|
|
||||||
|
|
||||||
def test_init_create_session(default_conf):
|
def test_init_create_session(default_conf):
|
||||||
# Check if init create a session
|
# Check if init create a session
|
||||||
init(default_conf)
|
init(default_conf['db_url'], default_conf['dry_run'])
|
||||||
assert hasattr(Trade, 'session')
|
assert hasattr(Trade, 'session')
|
||||||
assert 'Session' in type(Trade.session).__name__
|
assert 'Session' in type(Trade.session).__name__
|
||||||
|
|
||||||
|
@ -27,7 +67,7 @@ def test_init_custom_db_url(default_conf, mocker):
|
||||||
default_conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'})
|
default_conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'})
|
||||||
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
|
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
|
||||||
|
|
||||||
init(default_conf)
|
init(default_conf['db_url'], default_conf['dry_run'])
|
||||||
assert create_engine_mock.call_count == 1
|
assert create_engine_mock.call_count == 1
|
||||||
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tmp/freqtrade2_test.sqlite'
|
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tmp/freqtrade2_test.sqlite'
|
||||||
|
|
||||||
|
@ -36,7 +76,7 @@ def test_init_invalid_db_url(default_conf):
|
||||||
# Update path to a value other than default, but still in-memory
|
# Update path to a value other than default, but still in-memory
|
||||||
default_conf.update({'db_url': 'unknown:///some.url'})
|
default_conf.update({'db_url': 'unknown:///some.url'})
|
||||||
with pytest.raises(OperationalException, match=r'.*no valid database URL*'):
|
with pytest.raises(OperationalException, match=r'.*no valid database URL*'):
|
||||||
init(default_conf)
|
init(default_conf['db_url'], default_conf['dry_run'])
|
||||||
|
|
||||||
|
|
||||||
def test_init_prod_db(default_conf, mocker):
|
def test_init_prod_db(default_conf, mocker):
|
||||||
|
@ -45,7 +85,7 @@ def test_init_prod_db(default_conf, mocker):
|
||||||
|
|
||||||
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
|
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
|
||||||
|
|
||||||
init(default_conf)
|
init(default_conf['db_url'], default_conf['dry_run'])
|
||||||
assert create_engine_mock.call_count == 1
|
assert create_engine_mock.call_count == 1
|
||||||
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite'
|
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite'
|
||||||
|
|
||||||
|
@ -56,7 +96,7 @@ def test_init_dryrun_db(default_conf, mocker):
|
||||||
|
|
||||||
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
|
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
|
||||||
|
|
||||||
init(default_conf)
|
init(default_conf['db_url'], default_conf['dry_run'])
|
||||||
assert create_engine_mock.call_count == 1
|
assert create_engine_mock.call_count == 1
|
||||||
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite://'
|
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite://'
|
||||||
|
|
||||||
|
@ -335,8 +375,8 @@ def test_calc_profit_percent(limit_buy_order, limit_sell_order, fee):
|
||||||
assert trade.calc_profit_percent(fee=0.003) == 0.06147824
|
assert trade.calc_profit_percent(fee=0.003) == 0.06147824
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_clean_dry_run_db(default_conf, fee):
|
def test_clean_dry_run_db(default_conf, fee):
|
||||||
init(default_conf)
|
|
||||||
|
|
||||||
# Simulate dry_run entries
|
# Simulate dry_run entries
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
|
@ -423,7 +463,7 @@ def test_migrate_old(mocker, default_conf, fee):
|
||||||
engine.execute(create_table_old)
|
engine.execute(create_table_old)
|
||||||
engine.execute(insert_table_old)
|
engine.execute(insert_table_old)
|
||||||
# Run init to test migration
|
# Run init to test migration
|
||||||
init(default_conf)
|
init(default_conf['db_url'], default_conf['dry_run'])
|
||||||
|
|
||||||
assert len(Trade.query.filter(Trade.id == 1).all()) == 1
|
assert len(Trade.query.filter(Trade.id == 1).all()) == 1
|
||||||
trade = Trade.query.filter(Trade.id == 1).first()
|
trade = Trade.query.filter(Trade.id == 1).first()
|
||||||
|
@ -496,7 +536,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
|
||||||
|
|
||||||
engine.execute("create table trades_bak1 as select * from trades")
|
engine.execute("create table trades_bak1 as select * from trades")
|
||||||
# Run init to test migration
|
# Run init to test migration
|
||||||
init(default_conf)
|
init(default_conf['db_url'], default_conf['dry_run'])
|
||||||
|
|
||||||
assert len(Trade.query.filter(Trade.id == 1).all()) == 1
|
assert len(Trade.query.filter(Trade.id == 1).all()) == 1
|
||||||
trade = Trade.query.filter(Trade.id == 1).first()
|
trade = Trade.query.filter(Trade.id == 1).first()
|
||||||
|
@ -565,7 +605,7 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog):
|
||||||
engine.execute(insert_table_old)
|
engine.execute(insert_table_old)
|
||||||
|
|
||||||
# Run init to test migration
|
# Run init to test migration
|
||||||
init(default_conf)
|
init(default_conf['db_url'], default_conf['dry_run'])
|
||||||
|
|
||||||
assert len(Trade.query.filter(Trade.id == 1).all()) == 1
|
assert len(Trade.query.filter(Trade.id == 1).all()) == 1
|
||||||
trade = Trade.query.filter(Trade.id == 1).first()
|
trade = Trade.query.filter(Trade.id == 1).first()
|
||||||
|
@ -667,8 +707,15 @@ def test_adjust_min_max_rates(fee):
|
||||||
assert trade.min_rate == 0.96
|
assert trade.min_rate == 0.96
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_get_open(default_conf, fee):
|
def test_get_open(default_conf, fee):
|
||||||
init(default_conf)
|
|
||||||
|
create_mock_trades(fee)
|
||||||
|
assert len(Trade.get_open_trades()) == 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
|
def test_to_json(default_conf, fee):
|
||||||
|
|
||||||
# Simulate dry_run entries
|
# Simulate dry_run entries
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
|
@ -677,36 +724,117 @@ def test_get_open(default_conf, fee):
|
||||||
amount=123.0,
|
amount=123.0,
|
||||||
fee_open=fee.return_value,
|
fee_open=fee.return_value,
|
||||||
fee_close=fee.return_value,
|
fee_close=fee.return_value,
|
||||||
|
open_date=arrow.utcnow().shift(hours=-2).datetime,
|
||||||
open_rate=0.123,
|
open_rate=0.123,
|
||||||
exchange='bittrex',
|
exchange='bittrex',
|
||||||
open_order_id='dry_run_buy_12345'
|
open_order_id='dry_run_buy_12345'
|
||||||
)
|
)
|
||||||
Trade.session.add(trade)
|
result = trade.to_json()
|
||||||
|
assert isinstance(result, dict)
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
assert result == {'trade_id': None,
|
||||||
|
'pair': 'ETH/BTC',
|
||||||
|
'open_date_hum': '2 hours ago',
|
||||||
|
'open_date': trade.open_date.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
'close_date_hum': None,
|
||||||
|
'close_date': None,
|
||||||
|
'open_rate': 0.123,
|
||||||
|
'close_rate': None,
|
||||||
|
'amount': 123.0,
|
||||||
|
'stake_amount': 0.001,
|
||||||
|
'stop_loss': None,
|
||||||
|
'stop_loss_pct': None,
|
||||||
|
'initial_stop_loss': None,
|
||||||
|
'initial_stop_loss_pct': None}
|
||||||
|
|
||||||
|
# Simulate dry_run entries
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='ETC/BTC',
|
pair='XRP/BTC',
|
||||||
stake_amount=0.001,
|
stake_amount=0.001,
|
||||||
amount=123.0,
|
amount=100.0,
|
||||||
fee_open=fee.return_value,
|
fee_open=fee.return_value,
|
||||||
fee_close=fee.return_value,
|
fee_close=fee.return_value,
|
||||||
|
open_date=arrow.utcnow().shift(hours=-2).datetime,
|
||||||
|
close_date=arrow.utcnow().shift(hours=-1).datetime,
|
||||||
open_rate=0.123,
|
open_rate=0.123,
|
||||||
|
close_rate=0.125,
|
||||||
exchange='bittrex',
|
exchange='bittrex',
|
||||||
is_open=False,
|
|
||||||
open_order_id='dry_run_sell_12345'
|
|
||||||
)
|
)
|
||||||
Trade.session.add(trade)
|
result = trade.to_json()
|
||||||
|
assert isinstance(result, dict)
|
||||||
|
|
||||||
# Simulate prod entry
|
assert result == {'trade_id': None,
|
||||||
|
'pair': 'XRP/BTC',
|
||||||
|
'open_date_hum': '2 hours ago',
|
||||||
|
'open_date': trade.open_date.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
'close_date_hum': 'an hour ago',
|
||||||
|
'close_date': trade.close_date.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
'open_rate': 0.123,
|
||||||
|
'close_rate': 0.125,
|
||||||
|
'amount': 100.0,
|
||||||
|
'stake_amount': 0.001,
|
||||||
|
'stop_loss': None,
|
||||||
|
'stop_loss_pct': None,
|
||||||
|
'initial_stop_loss': None,
|
||||||
|
'initial_stop_loss_pct': None}
|
||||||
|
|
||||||
|
|
||||||
|
def test_stoploss_reinitialization(default_conf, fee):
|
||||||
|
init(default_conf['db_url'])
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='ETC/BTC',
|
pair='ETH/BTC',
|
||||||
stake_amount=0.001,
|
stake_amount=0.001,
|
||||||
amount=123.0,
|
|
||||||
fee_open=fee.return_value,
|
fee_open=fee.return_value,
|
||||||
|
open_date=arrow.utcnow().shift(hours=-2).datetime,
|
||||||
|
amount=10,
|
||||||
fee_close=fee.return_value,
|
fee_close=fee.return_value,
|
||||||
open_rate=0.123,
|
|
||||||
exchange='bittrex',
|
exchange='bittrex',
|
||||||
open_order_id='prod_buy_12345'
|
open_rate=1,
|
||||||
|
max_rate=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
trade.adjust_stop_loss(trade.open_rate, 0.05, True)
|
||||||
|
assert trade.stop_loss == 0.95
|
||||||
|
assert trade.stop_loss_pct == -0.05
|
||||||
|
assert trade.initial_stop_loss == 0.95
|
||||||
|
assert trade.initial_stop_loss_pct == -0.05
|
||||||
Trade.session.add(trade)
|
Trade.session.add(trade)
|
||||||
|
|
||||||
assert len(Trade.get_open_trades()) == 2
|
# Lower stoploss
|
||||||
|
Trade.stoploss_reinitialization(0.06)
|
||||||
|
|
||||||
|
trades = Trade.get_open_trades()
|
||||||
|
assert len(trades) == 1
|
||||||
|
trade_adj = trades[0]
|
||||||
|
assert trade_adj.stop_loss == 0.94
|
||||||
|
assert trade_adj.stop_loss_pct == -0.06
|
||||||
|
assert trade_adj.initial_stop_loss == 0.94
|
||||||
|
assert trade_adj.initial_stop_loss_pct == -0.06
|
||||||
|
|
||||||
|
# Raise stoploss
|
||||||
|
Trade.stoploss_reinitialization(0.04)
|
||||||
|
|
||||||
|
trades = Trade.get_open_trades()
|
||||||
|
assert len(trades) == 1
|
||||||
|
trade_adj = trades[0]
|
||||||
|
assert trade_adj.stop_loss == 0.96
|
||||||
|
assert trade_adj.stop_loss_pct == -0.04
|
||||||
|
assert trade_adj.initial_stop_loss == 0.96
|
||||||
|
assert trade_adj.initial_stop_loss_pct == -0.04
|
||||||
|
|
||||||
|
# Trailing stoploss (move stoplos up a bit)
|
||||||
|
trade.adjust_stop_loss(1.02, 0.04)
|
||||||
|
assert trade_adj.stop_loss == 0.9792
|
||||||
|
assert trade_adj.initial_stop_loss == 0.96
|
||||||
|
|
||||||
|
Trade.stoploss_reinitialization(0.04)
|
||||||
|
|
||||||
|
trades = Trade.get_open_trades()
|
||||||
|
assert len(trades) == 1
|
||||||
|
trade_adj = trades[0]
|
||||||
|
# Stoploss should not change in this case.
|
||||||
|
assert trade_adj.stop_loss == 0.9792
|
||||||
|
assert trade_adj.stop_loss_pct == -0.04
|
||||||
|
assert trade_adj.initial_stop_loss == 0.96
|
||||||
|
assert trade_adj.initial_stop_loss_pct == -0.04
|
||||||
|
|
189
freqtrade/tests/test_plotting.py
Normal file
189
freqtrade/tests/test_plotting.py
Normal file
|
@ -0,0 +1,189 @@
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from plotly import tools
|
||||||
|
import plotly.graph_objs as go
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
from freqtrade.arguments import TimeRange
|
||||||
|
from freqtrade.data import history
|
||||||
|
from freqtrade.data.btanalysis import load_backtest_data
|
||||||
|
from freqtrade.plot.plotting import (generate_graph, generate_plot_file,
|
||||||
|
generate_row, plot_trades)
|
||||||
|
from freqtrade.strategy.default_strategy import DefaultStrategy
|
||||||
|
from freqtrade.tests.conftest import log_has, log_has_re
|
||||||
|
|
||||||
|
|
||||||
|
def fig_generating_mock(fig, *args, **kwargs):
|
||||||
|
""" Return Fig - used to mock generate_row and plot_trades"""
|
||||||
|
return fig
|
||||||
|
|
||||||
|
|
||||||
|
def find_trace_in_fig_data(data, search_string: str):
|
||||||
|
matches = filter(lambda x: x.name == search_string, data)
|
||||||
|
return next(matches)
|
||||||
|
|
||||||
|
|
||||||
|
def generage_empty_figure():
|
||||||
|
return tools.make_subplots(
|
||||||
|
rows=3,
|
||||||
|
cols=1,
|
||||||
|
shared_xaxes=True,
|
||||||
|
row_width=[1, 1, 4],
|
||||||
|
vertical_spacing=0.0001,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_row(default_conf, caplog):
|
||||||
|
pair = "UNITTEST/BTC"
|
||||||
|
timerange = TimeRange(None, 'line', 0, -1000)
|
||||||
|
|
||||||
|
data = history.load_pair_history(pair=pair, ticker_interval='1m',
|
||||||
|
datadir=None, timerange=timerange)
|
||||||
|
indicators1 = ["ema10"]
|
||||||
|
indicators2 = ["macd"]
|
||||||
|
|
||||||
|
# Generate buy/sell signals and indicators
|
||||||
|
strat = DefaultStrategy(default_conf)
|
||||||
|
data = strat.analyze_ticker(data, {'pair': pair})
|
||||||
|
fig = generage_empty_figure()
|
||||||
|
|
||||||
|
# Row 1
|
||||||
|
fig1 = generate_row(fig=deepcopy(fig), row=1, indicators=indicators1, data=data)
|
||||||
|
figure = fig1.layout.figure
|
||||||
|
ema10 = find_trace_in_fig_data(figure.data, "ema10")
|
||||||
|
assert isinstance(ema10, go.Scatter)
|
||||||
|
assert ema10.yaxis == "y"
|
||||||
|
|
||||||
|
fig2 = generate_row(fig=deepcopy(fig), row=3, indicators=indicators2, data=data)
|
||||||
|
figure = fig2.layout.figure
|
||||||
|
macd = find_trace_in_fig_data(figure.data, "macd")
|
||||||
|
assert isinstance(macd, go.Scatter)
|
||||||
|
assert macd.yaxis == "y3"
|
||||||
|
|
||||||
|
# No indicator found
|
||||||
|
fig3 = generate_row(fig=deepcopy(fig), row=3, indicators=['no_indicator'], data=data)
|
||||||
|
assert fig == fig3
|
||||||
|
assert log_has_re(r'Indicator "no_indicator" ignored\..*', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_plot_trades(caplog):
|
||||||
|
fig1 = generage_empty_figure()
|
||||||
|
# nothing happens when no trades are available
|
||||||
|
fig = plot_trades(fig1, None)
|
||||||
|
assert fig == fig1
|
||||||
|
assert log_has("No trades found.", caplog.record_tuples)
|
||||||
|
pair = "ADA/BTC"
|
||||||
|
filename = history.make_testdata_path(None) / "backtest-result_test.json"
|
||||||
|
trades = load_backtest_data(filename)
|
||||||
|
trades = trades.loc[trades['pair'] == pair]
|
||||||
|
|
||||||
|
fig = plot_trades(fig, trades)
|
||||||
|
figure = fig1.layout.figure
|
||||||
|
|
||||||
|
# Check buys - color, should be in first graph, ...
|
||||||
|
trade_buy = find_trace_in_fig_data(figure.data, "trade_buy")
|
||||||
|
assert isinstance(trade_buy, go.Scatter)
|
||||||
|
assert trade_buy.yaxis == 'y'
|
||||||
|
assert len(trades) == len(trade_buy.x)
|
||||||
|
assert trade_buy.marker.color == 'green'
|
||||||
|
|
||||||
|
trade_sell = find_trace_in_fig_data(figure.data, "trade_sell")
|
||||||
|
assert isinstance(trade_sell, go.Scatter)
|
||||||
|
assert trade_sell.yaxis == 'y'
|
||||||
|
assert len(trades) == len(trade_sell.x)
|
||||||
|
assert trade_sell.marker.color == 'red'
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_graph_no_signals_no_trades(default_conf, mocker, caplog):
|
||||||
|
row_mock = mocker.patch('freqtrade.plot.plotting.generate_row',
|
||||||
|
MagicMock(side_effect=fig_generating_mock))
|
||||||
|
trades_mock = mocker.patch('freqtrade.plot.plotting.plot_trades',
|
||||||
|
MagicMock(side_effect=fig_generating_mock))
|
||||||
|
|
||||||
|
pair = "UNITTEST/BTC"
|
||||||
|
timerange = TimeRange(None, 'line', 0, -1000)
|
||||||
|
data = history.load_pair_history(pair=pair, ticker_interval='1m',
|
||||||
|
datadir=None, timerange=timerange)
|
||||||
|
data['buy'] = 0
|
||||||
|
data['sell'] = 0
|
||||||
|
|
||||||
|
indicators1 = []
|
||||||
|
indicators2 = []
|
||||||
|
fig = generate_graph(pair=pair, data=data, trades=None,
|
||||||
|
indicators1=indicators1, indicators2=indicators2)
|
||||||
|
assert isinstance(fig, go.Figure)
|
||||||
|
assert fig.layout.title.text == pair
|
||||||
|
figure = fig.layout.figure
|
||||||
|
|
||||||
|
assert len(figure.data) == 2
|
||||||
|
# Candlesticks are plotted first
|
||||||
|
candles = find_trace_in_fig_data(figure.data, "Price")
|
||||||
|
assert isinstance(candles, go.Candlestick)
|
||||||
|
|
||||||
|
volume = find_trace_in_fig_data(figure.data, "Volume")
|
||||||
|
assert isinstance(volume, go.Bar)
|
||||||
|
|
||||||
|
assert row_mock.call_count == 2
|
||||||
|
assert trades_mock.call_count == 1
|
||||||
|
|
||||||
|
assert log_has("No buy-signals found.", caplog.record_tuples)
|
||||||
|
assert log_has("No sell-signals found.", caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_graph_no_trades(default_conf, mocker):
|
||||||
|
row_mock = mocker.patch('freqtrade.plot.plotting.generate_row',
|
||||||
|
MagicMock(side_effect=fig_generating_mock))
|
||||||
|
trades_mock = mocker.patch('freqtrade.plot.plotting.plot_trades',
|
||||||
|
MagicMock(side_effect=fig_generating_mock))
|
||||||
|
pair = 'UNITTEST/BTC'
|
||||||
|
timerange = TimeRange(None, 'line', 0, -1000)
|
||||||
|
data = history.load_pair_history(pair=pair, ticker_interval='1m',
|
||||||
|
datadir=None, timerange=timerange)
|
||||||
|
|
||||||
|
# Generate buy/sell signals and indicators
|
||||||
|
strat = DefaultStrategy(default_conf)
|
||||||
|
data = strat.analyze_ticker(data, {'pair': pair})
|
||||||
|
|
||||||
|
indicators1 = []
|
||||||
|
indicators2 = []
|
||||||
|
fig = generate_graph(pair=pair, data=data, trades=None,
|
||||||
|
indicators1=indicators1, indicators2=indicators2)
|
||||||
|
assert isinstance(fig, go.Figure)
|
||||||
|
assert fig.layout.title.text == pair
|
||||||
|
figure = fig.layout.figure
|
||||||
|
|
||||||
|
assert len(figure.data) == 6
|
||||||
|
# Candlesticks are plotted first
|
||||||
|
candles = find_trace_in_fig_data(figure.data, "Price")
|
||||||
|
assert isinstance(candles, go.Candlestick)
|
||||||
|
|
||||||
|
volume = find_trace_in_fig_data(figure.data, "Volume")
|
||||||
|
assert isinstance(volume, go.Bar)
|
||||||
|
|
||||||
|
buy = find_trace_in_fig_data(figure.data, "buy")
|
||||||
|
assert isinstance(buy, go.Scatter)
|
||||||
|
# All buy-signals should be plotted
|
||||||
|
assert int(data.buy.sum()) == len(buy.x)
|
||||||
|
|
||||||
|
sell = find_trace_in_fig_data(figure.data, "sell")
|
||||||
|
assert isinstance(sell, go.Scatter)
|
||||||
|
# All buy-signals should be plotted
|
||||||
|
assert int(data.sell.sum()) == len(sell.x)
|
||||||
|
|
||||||
|
assert find_trace_in_fig_data(figure.data, "BB lower")
|
||||||
|
assert find_trace_in_fig_data(figure.data, "BB upper")
|
||||||
|
|
||||||
|
assert row_mock.call_count == 2
|
||||||
|
assert trades_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_plot_file(mocker, caplog):
|
||||||
|
fig = generage_empty_figure()
|
||||||
|
plot_mock = mocker.patch("freqtrade.plot.plotting.plot", MagicMock())
|
||||||
|
generate_plot_file(fig, "UNITTEST/BTC", "5m")
|
||||||
|
|
||||||
|
assert plot_mock.call_count == 1
|
||||||
|
assert plot_mock.call_args[0][0] == fig
|
||||||
|
assert (plot_mock.call_args_list[0][1]['filename']
|
||||||
|
== "user_data/plots/freqtrade-plot-UNITTEST_BTC-5m.html")
|
42
freqtrade/tests/test_utils.py
Normal file
42
freqtrade/tests/test_utils.py
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
from freqtrade.utils import setup_utils_configuration, start_list_exchanges
|
||||||
|
from freqtrade.tests.conftest import get_args
|
||||||
|
from freqtrade.state import RunMode
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def test_setup_utils_configuration():
|
||||||
|
args = [
|
||||||
|
'--config', 'config.json.example',
|
||||||
|
]
|
||||||
|
|
||||||
|
config = setup_utils_configuration(get_args(args), RunMode.OTHER)
|
||||||
|
assert "exchange" in config
|
||||||
|
assert config['exchange']['dry_run'] is True
|
||||||
|
assert config['exchange']['key'] == ''
|
||||||
|
assert config['exchange']['secret'] == ''
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_exchanges(capsys):
|
||||||
|
|
||||||
|
args = [
|
||||||
|
"list-exchanges",
|
||||||
|
]
|
||||||
|
|
||||||
|
start_list_exchanges(get_args(args))
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert re.match(r"Exchanges supported by ccxt and available.*", captured.out)
|
||||||
|
assert re.match(r".*binance,.*", captured.out)
|
||||||
|
assert re.match(r".*bittrex,.*", captured.out)
|
||||||
|
|
||||||
|
# Test with --one-column
|
||||||
|
args = [
|
||||||
|
"list-exchanges",
|
||||||
|
"--one-column",
|
||||||
|
]
|
||||||
|
|
||||||
|
start_list_exchanges(get_args(args))
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
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"^bittrex$", captured.out, re.MULTILINE)
|
41
freqtrade/utils.py
Normal file
41
freqtrade/utils.py
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import logging
|
||||||
|
from argparse import Namespace
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from freqtrade.configuration import Configuration
|
||||||
|
from freqtrade.exchange import available_exchanges
|
||||||
|
from freqtrade.state import RunMode
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_utils_configuration(args: Namespace, method: RunMode) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Prepare the configuration for utils subcommands
|
||||||
|
:param args: Cli args from Arguments()
|
||||||
|
:return: Configuration
|
||||||
|
"""
|
||||||
|
configuration = Configuration(args, method)
|
||||||
|
config = configuration.load_config()
|
||||||
|
|
||||||
|
config['exchange']['dry_run'] = True
|
||||||
|
# Ensure we do not use Exchange credentials
|
||||||
|
config['exchange']['key'] = ''
|
||||||
|
config['exchange']['secret'] = ''
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def start_list_exchanges(args: Namespace) -> None:
|
||||||
|
"""
|
||||||
|
Print available exchanges
|
||||||
|
:param args: Cli args from Arguments()
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
|
if args.print_one_column:
|
||||||
|
print('\n'.join(available_exchanges()))
|
||||||
|
else:
|
||||||
|
print(f"Exchanges supported by ccxt and available for Freqtrade: "
|
||||||
|
f"{', '.join(available_exchanges())}")
|
202
freqtrade/vendor/qtpylib/indicators.py
vendored
202
freqtrade/vendor/qtpylib/indicators.py
vendored
|
@ -4,13 +4,13 @@
|
||||||
# QTPyLib: Quantitative Trading Python Library
|
# QTPyLib: Quantitative Trading Python Library
|
||||||
# https://github.com/ranaroussi/qtpylib
|
# https://github.com/ranaroussi/qtpylib
|
||||||
#
|
#
|
||||||
# Copyright 2016 Ran Aroussi
|
# Copyright 2016-2018 Ran Aroussi
|
||||||
#
|
#
|
||||||
# Licensed under the GNU Lesser General Public License, v3.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
# You may obtain a copy of the License at
|
# You may obtain a copy of the License at
|
||||||
#
|
#
|
||||||
# https://www.gnu.org/licenses/lgpl-3.0.en.html
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
#
|
#
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@ -19,8 +19,8 @@
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
|
|
||||||
import sys
|
|
||||||
import warnings
|
import warnings
|
||||||
|
import sys
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
@ -62,19 +62,20 @@ def numpy_rolling_series(func):
|
||||||
|
|
||||||
@numpy_rolling_series
|
@numpy_rolling_series
|
||||||
def numpy_rolling_mean(data, window, as_source=False):
|
def numpy_rolling_mean(data, window, as_source=False):
|
||||||
return np.mean(numpy_rolling_window(data, window), -1)
|
return np.mean(numpy_rolling_window(data, window), axis=-1)
|
||||||
|
|
||||||
|
|
||||||
@numpy_rolling_series
|
@numpy_rolling_series
|
||||||
def numpy_rolling_std(data, window, as_source=False):
|
def numpy_rolling_std(data, window, as_source=False):
|
||||||
return np.std(numpy_rolling_window(data, window), -1)
|
return np.std(numpy_rolling_window(data, window), axis=-1, ddof=1)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def session(df, start='17:00', end='16:00'):
|
def session(df, start='17:00', end='16:00'):
|
||||||
""" remove previous globex day from df """
|
""" remove previous globex day from df """
|
||||||
if len(df) == 0:
|
if df.empty:
|
||||||
return df
|
return df
|
||||||
|
|
||||||
# get start/end/now as decimals
|
# get start/end/now as decimals
|
||||||
|
@ -103,47 +104,47 @@ def session(df, start='17:00', end='16:00'):
|
||||||
|
|
||||||
return df.copy()
|
return df.copy()
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def heikinashi(bars):
|
def heikinashi(bars):
|
||||||
bars = bars.copy()
|
bars = bars.copy()
|
||||||
bars['ha_close'] = (bars['open'] + bars['high'] +
|
bars['ha_close'] = (bars['open'] + bars['high'] +
|
||||||
bars['low'] + bars['close']) / 4
|
bars['low'] + bars['close']) / 4
|
||||||
|
|
||||||
bars['ha_open'] = (bars['open'].shift(1) + bars['close'].shift(1)) / 2
|
# ha open
|
||||||
bars.loc[:1, 'ha_open'] = bars['open'].values[0]
|
bars.at[0, 'ha_open'] = (bars.at[0, 'open'] + bars.at[0, 'close']) / 2
|
||||||
for x in range(2):
|
for i in range(1, len(bars)):
|
||||||
bars.loc[1:, 'ha_open'] = (
|
bars.at[i, 'ha_open'] = (bars.at[i - 1, 'ha_open'] + bars.at[i - 1, 'ha_close']) / 2
|
||||||
(bars['ha_open'].shift(1) + bars['ha_close'].shift(1)) / 2)[1:]
|
|
||||||
|
|
||||||
bars['ha_high'] = bars.loc[:, ['high', 'ha_open', 'ha_close']].max(axis=1)
|
bars['ha_high'] = bars.loc[:, ['high', 'ha_open', 'ha_close']].max(axis=1)
|
||||||
bars['ha_low'] = bars.loc[:, ['low', 'ha_open', 'ha_close']].min(axis=1)
|
bars['ha_low'] = bars.loc[:, ['low', 'ha_open', 'ha_close']].min(axis=1)
|
||||||
|
|
||||||
return pd.DataFrame(
|
return pd.DataFrame(index=bars.index,
|
||||||
index=bars.index,
|
data={'open': bars['ha_open'],
|
||||||
data={
|
'high': bars['ha_high'],
|
||||||
'open': bars['ha_open'],
|
'low': bars['ha_low'],
|
||||||
'high': bars['ha_high'],
|
'close': bars['ha_close']})
|
||||||
'low': bars['ha_low'],
|
|
||||||
'close': bars['ha_close']})
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------
|
# ---------------------------------------------
|
||||||
|
|
||||||
def tdi(series, rsi_len=13, bollinger_len=34, rsi_smoothing=2,
|
|
||||||
rsi_signal_len=7, bollinger_std=1.6185):
|
def tdi(series, rsi_lookback=13, rsi_smooth_len=2,
|
||||||
rsi_series = rsi(series, rsi_len)
|
rsi_signal_len=7, bb_lookback=34, bb_std=1.6185):
|
||||||
bb_series = bollinger_bands(rsi_series, bollinger_len, bollinger_std)
|
|
||||||
signal = sma(rsi_series, rsi_signal_len)
|
rsi_data = rsi(series, rsi_lookback)
|
||||||
rsi_series = sma(rsi_series, rsi_smoothing)
|
rsi_smooth = sma(rsi_data, rsi_smooth_len)
|
||||||
|
rsi_signal = sma(rsi_data, rsi_signal_len)
|
||||||
|
|
||||||
|
bb_series = bollinger_bands(rsi_data, bb_lookback, bb_std)
|
||||||
|
|
||||||
return pd.DataFrame(index=series.index, data={
|
return pd.DataFrame(index=series.index, data={
|
||||||
"rsi": rsi_series,
|
"rsi": rsi_data,
|
||||||
"signal": signal,
|
"rsi_signal": rsi_signal,
|
||||||
"bbupper": bb_series['upper'],
|
"rsi_smooth": rsi_smooth,
|
||||||
"bblower": bb_series['lower'],
|
"rsi_bb_upper": bb_series['upper'],
|
||||||
"bbmid": bb_series['mid']
|
"rsi_bb_lower": bb_series['lower'],
|
||||||
|
"rsi_bb_mid": bb_series['mid']
|
||||||
})
|
})
|
||||||
|
|
||||||
# ---------------------------------------------
|
# ---------------------------------------------
|
||||||
|
@ -163,8 +164,8 @@ def awesome_oscillator(df, weighted=False, fast=5, slow=34):
|
||||||
|
|
||||||
# ---------------------------------------------
|
# ---------------------------------------------
|
||||||
|
|
||||||
def nans(len=1):
|
def nans(length=1):
|
||||||
mtx = np.empty(len)
|
mtx = np.empty(length)
|
||||||
mtx[:] = np.nan
|
mtx[:] = np.nan
|
||||||
return mtx
|
return mtx
|
||||||
|
|
||||||
|
@ -222,7 +223,7 @@ def crossed(series1, series2, direction=None):
|
||||||
if isinstance(series1, np.ndarray):
|
if isinstance(series1, np.ndarray):
|
||||||
series1 = pd.Series(series1)
|
series1 = pd.Series(series1)
|
||||||
|
|
||||||
if isinstance(series2, int) or isinstance(series2, float) or isinstance(series2, np.ndarray):
|
if isinstance(series2, (float, int, np.ndarray)):
|
||||||
series2 = pd.Series(index=series1.index, data=series2)
|
series2 = pd.Series(index=series1.index, data=series2)
|
||||||
|
|
||||||
if direction is None or direction == "above":
|
if direction is None or direction == "above":
|
||||||
|
@ -256,7 +257,7 @@ def rolling_std(series, window=200, min_periods=None):
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
return series.rolling(window=window, min_periods=min_periods).std()
|
return series.rolling(window=window, min_periods=min_periods).std()
|
||||||
except BaseException:
|
except Exception as e: # noqa: F841
|
||||||
return pd.Series(series).rolling(window=window, min_periods=min_periods).std()
|
return pd.Series(series).rolling(window=window, min_periods=min_periods).std()
|
||||||
|
|
||||||
# ---------------------------------------------
|
# ---------------------------------------------
|
||||||
|
@ -269,7 +270,7 @@ def rolling_mean(series, window=200, min_periods=None):
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
return series.rolling(window=window, min_periods=min_periods).mean()
|
return series.rolling(window=window, min_periods=min_periods).mean()
|
||||||
except BaseException:
|
except Exception as e: # noqa: F841
|
||||||
return pd.Series(series).rolling(window=window, min_periods=min_periods).mean()
|
return pd.Series(series).rolling(window=window, min_periods=min_periods).mean()
|
||||||
|
|
||||||
# ---------------------------------------------
|
# ---------------------------------------------
|
||||||
|
@ -279,7 +280,7 @@ def rolling_min(series, window=14, min_periods=None):
|
||||||
min_periods = window if min_periods is None else min_periods
|
min_periods = window if min_periods is None else min_periods
|
||||||
try:
|
try:
|
||||||
return series.rolling(window=window, min_periods=min_periods).min()
|
return series.rolling(window=window, min_periods=min_periods).min()
|
||||||
except BaseException:
|
except Exception as e: # noqa: F841
|
||||||
return pd.Series(series).rolling(window=window, min_periods=min_periods).min()
|
return pd.Series(series).rolling(window=window, min_periods=min_periods).min()
|
||||||
|
|
||||||
|
|
||||||
|
@ -289,7 +290,7 @@ def rolling_max(series, window=14, min_periods=None):
|
||||||
min_periods = window if min_periods is None else min_periods
|
min_periods = window if min_periods is None else min_periods
|
||||||
try:
|
try:
|
||||||
return series.rolling(window=window, min_periods=min_periods).min()
|
return series.rolling(window=window, min_periods=min_periods).min()
|
||||||
except BaseException:
|
except Exception as e: # noqa: F841
|
||||||
return pd.Series(series).rolling(window=window, min_periods=min_periods).min()
|
return pd.Series(series).rolling(window=window, min_periods=min_periods).min()
|
||||||
|
|
||||||
|
|
||||||
|
@ -299,16 +300,17 @@ def rolling_weighted_mean(series, window=200, min_periods=None):
|
||||||
min_periods = window if min_periods is None else min_periods
|
min_periods = window if min_periods is None else min_periods
|
||||||
try:
|
try:
|
||||||
return series.ewm(span=window, min_periods=min_periods).mean()
|
return series.ewm(span=window, min_periods=min_periods).mean()
|
||||||
except BaseException:
|
except Exception as e: # noqa: F841
|
||||||
return pd.ewma(series, span=window, min_periods=min_periods)
|
return pd.ewma(series, span=window, min_periods=min_periods)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------
|
# ---------------------------------------------
|
||||||
|
|
||||||
def hull_moving_average(series, window=200):
|
def hull_moving_average(series, window=200, min_periods=None):
|
||||||
wma = (2 * rolling_weighted_mean(series, window=window / 2)) - \
|
min_periods = window if min_periods is None else min_periods
|
||||||
rolling_weighted_mean(series, window=window)
|
ma = (2 * rolling_weighted_mean(series, window / 2, min_periods)) - \
|
||||||
return rolling_weighted_mean(wma, window=np.sqrt(window))
|
rolling_weighted_mean(series, window, min_periods)
|
||||||
|
return rolling_weighted_mean(ma, np.sqrt(window), min_periods)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------
|
# ---------------------------------------------
|
||||||
|
@ -325,8 +327,8 @@ def wma(series, window=200, min_periods=None):
|
||||||
|
|
||||||
# ---------------------------------------------
|
# ---------------------------------------------
|
||||||
|
|
||||||
def hma(series, window=200):
|
def hma(series, window=200, min_periods=None):
|
||||||
return hull_moving_average(series, window=window)
|
return hull_moving_average(series, window=window, min_periods=min_periods)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------
|
# ---------------------------------------------
|
||||||
|
@ -361,7 +363,8 @@ def rolling_vwap(bars, window=200, min_periods=None):
|
||||||
min_periods=min_periods).sum()
|
min_periods=min_periods).sum()
|
||||||
right = volume.rolling(window=window, min_periods=min_periods).sum()
|
right = volume.rolling(window=window, min_periods=min_periods).sum()
|
||||||
|
|
||||||
return pd.Series(index=bars.index, data=(left / right))
|
return pd.Series(index=bars.index, data=(left / right)
|
||||||
|
).replace([np.inf, -np.inf], float('NaN')).ffill()
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------
|
# ---------------------------------------------
|
||||||
|
@ -370,6 +373,7 @@ def rsi(series, window=14):
|
||||||
"""
|
"""
|
||||||
compute the n period relative strength indicator
|
compute the n period relative strength indicator
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# 100-(100/relative_strength)
|
# 100-(100/relative_strength)
|
||||||
deltas = np.diff(series)
|
deltas = np.diff(series)
|
||||||
seed = deltas[:window + 1]
|
seed = deltas[:window + 1]
|
||||||
|
@ -406,13 +410,13 @@ def macd(series, fast=3, slow=10, smooth=16):
|
||||||
using a fast and slow exponential moving avg'
|
using a fast and slow exponential moving avg'
|
||||||
return value is emaslow, emafast, macd which are len(x) arrays
|
return value is emaslow, emafast, macd which are len(x) arrays
|
||||||
"""
|
"""
|
||||||
macd = rolling_weighted_mean(series, window=fast) - \
|
macd_line = rolling_weighted_mean(series, window=fast) - \
|
||||||
rolling_weighted_mean(series, window=slow)
|
rolling_weighted_mean(series, window=slow)
|
||||||
signal = rolling_weighted_mean(macd, window=smooth)
|
signal = rolling_weighted_mean(macd_line, window=smooth)
|
||||||
histogram = macd - signal
|
histogram = macd_line - signal
|
||||||
# return macd, signal, histogram
|
# return macd_line, signal, histogram
|
||||||
return pd.DataFrame(index=series.index, data={
|
return pd.DataFrame(index=series.index, data={
|
||||||
'macd': macd.values,
|
'macd': macd_line.values,
|
||||||
'signal': signal.values,
|
'signal': signal.values,
|
||||||
'histogram': histogram.values
|
'histogram': histogram.values
|
||||||
})
|
})
|
||||||
|
@ -421,14 +425,14 @@ def macd(series, fast=3, slow=10, smooth=16):
|
||||||
# ---------------------------------------------
|
# ---------------------------------------------
|
||||||
|
|
||||||
def bollinger_bands(series, window=20, stds=2):
|
def bollinger_bands(series, window=20, stds=2):
|
||||||
sma = rolling_mean(series, window=window)
|
ma = rolling_mean(series, window=window, min_periods=1)
|
||||||
std = rolling_std(series, window=window)
|
std = rolling_std(series, window=window, min_periods=1)
|
||||||
upper = sma + std * stds
|
upper = ma + std * stds
|
||||||
lower = sma - std * stds
|
lower = ma - std * stds
|
||||||
|
|
||||||
return pd.DataFrame(index=series.index, data={
|
return pd.DataFrame(index=series.index, data={
|
||||||
'upper': upper,
|
'upper': upper,
|
||||||
'mid': sma,
|
'mid': ma,
|
||||||
'lower': lower
|
'lower': lower
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -454,7 +458,7 @@ def returns(series):
|
||||||
try:
|
try:
|
||||||
res = (series / series.shift(1) -
|
res = (series / series.shift(1) -
|
||||||
1).replace([np.inf, -np.inf], float('NaN'))
|
1).replace([np.inf, -np.inf], float('NaN'))
|
||||||
except BaseException:
|
except Exception as e: # noqa: F841
|
||||||
res = nans(len(series))
|
res = nans(len(series))
|
||||||
|
|
||||||
return pd.Series(index=series.index, data=res)
|
return pd.Series(index=series.index, data=res)
|
||||||
|
@ -466,7 +470,7 @@ def log_returns(series):
|
||||||
try:
|
try:
|
||||||
res = np.log(series / series.shift(1)
|
res = np.log(series / series.shift(1)
|
||||||
).replace([np.inf, -np.inf], float('NaN'))
|
).replace([np.inf, -np.inf], float('NaN'))
|
||||||
except BaseException:
|
except Exception as e: # noqa: F841
|
||||||
res = nans(len(series))
|
res = nans(len(series))
|
||||||
|
|
||||||
return pd.Series(index=series.index, data=res)
|
return pd.Series(index=series.index, data=res)
|
||||||
|
@ -479,7 +483,7 @@ def implied_volatility(series, window=252):
|
||||||
logret = np.log(series / series.shift(1)
|
logret = np.log(series / series.shift(1)
|
||||||
).replace([np.inf, -np.inf], float('NaN'))
|
).replace([np.inf, -np.inf], float('NaN'))
|
||||||
res = numpy_rolling_std(logret, window) * np.sqrt(window)
|
res = numpy_rolling_std(logret, window) * np.sqrt(window)
|
||||||
except BaseException:
|
except Exception as e: # noqa: F841
|
||||||
res = nans(len(series))
|
res = nans(len(series))
|
||||||
|
|
||||||
return pd.Series(index=series.index, data=res)
|
return pd.Series(index=series.index, data=res)
|
||||||
|
@ -530,32 +534,55 @@ def stoch(df, window=14, d=3, k=3, fast=False):
|
||||||
compute the n period relative strength indicator
|
compute the n period relative strength indicator
|
||||||
http://excelta.blogspot.co.il/2013/09/stochastic-oscillator-technical.html
|
http://excelta.blogspot.co.il/2013/09/stochastic-oscillator-technical.html
|
||||||
"""
|
"""
|
||||||
highs_ma = pd.concat([df['high'].shift(i)
|
|
||||||
for i in np.arange(window)], 1).apply(list, 1)
|
|
||||||
highs_ma = highs_ma.T.max().T
|
|
||||||
|
|
||||||
lows_ma = pd.concat([df['low'].shift(i)
|
my_df = pd.DataFrame(index=df.index)
|
||||||
for i in np.arange(window)], 1).apply(list, 1)
|
|
||||||
lows_ma = lows_ma.T.min().T
|
|
||||||
|
|
||||||
fast_k = ((df['close'] - lows_ma) / (highs_ma - lows_ma)) * 100
|
my_df['rolling_max'] = df['high'].rolling(window).max()
|
||||||
fast_d = numpy_rolling_mean(fast_k, d)
|
my_df['rolling_min'] = df['low'].rolling(window).min()
|
||||||
|
|
||||||
|
my_df['fast_k'] = (
|
||||||
|
100 * (df['close'] - my_df['rolling_min']) /
|
||||||
|
(my_df['rolling_max'] - my_df['rolling_min'])
|
||||||
|
)
|
||||||
|
my_df['fast_d'] = my_df['fast_k'].rolling(d).mean()
|
||||||
|
|
||||||
if fast:
|
if fast:
|
||||||
data = {
|
return my_df.loc[:, ['fast_k', 'fast_d']]
|
||||||
'k': fast_k,
|
|
||||||
'd': fast_d
|
|
||||||
}
|
|
||||||
|
|
||||||
else:
|
my_df['slow_k'] = my_df['fast_k'].rolling(k).mean()
|
||||||
slow_k = numpy_rolling_mean(fast_k, k)
|
my_df['slow_d'] = my_df['slow_k'].rolling(d).mean()
|
||||||
slow_d = numpy_rolling_mean(slow_k, d)
|
|
||||||
data = {
|
|
||||||
'k': slow_k,
|
|
||||||
'd': slow_d
|
|
||||||
}
|
|
||||||
|
|
||||||
return pd.DataFrame(index=df.index, data=data)
|
return my_df.loc[:, ['slow_k', 'slow_d']]
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def zlma(series, window=20, min_periods=None, kind="ema"):
|
||||||
|
"""
|
||||||
|
John Ehlers' Zero lag (exponential) moving average
|
||||||
|
https://en.wikipedia.org/wiki/Zero_lag_exponential_moving_average
|
||||||
|
"""
|
||||||
|
min_periods = window if min_periods is None else min_periods
|
||||||
|
|
||||||
|
lag = (window - 1) // 2
|
||||||
|
series = 2 * series - series.shift(lag)
|
||||||
|
if kind in ['ewm', 'ema']:
|
||||||
|
return wma(series, lag, min_periods)
|
||||||
|
elif kind == "hma":
|
||||||
|
return hma(series, lag, min_periods)
|
||||||
|
return sma(series, lag, min_periods)
|
||||||
|
|
||||||
|
|
||||||
|
def zlema(series, window, min_periods=None):
|
||||||
|
return zlma(series, window, min_periods, kind="ema")
|
||||||
|
|
||||||
|
|
||||||
|
def zlsma(series, window, min_periods=None):
|
||||||
|
return zlma(series, window, min_periods, kind="sma")
|
||||||
|
|
||||||
|
|
||||||
|
def zlhma(series, window, min_periods=None):
|
||||||
|
return zlma(series, window, min_periods, kind="hma")
|
||||||
|
|
||||||
# ---------------------------------------------
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
@ -571,13 +598,13 @@ def zscore(bars, window=20, stds=1, col='close'):
|
||||||
|
|
||||||
def pvt(bars):
|
def pvt(bars):
|
||||||
""" Price Volume Trend """
|
""" Price Volume Trend """
|
||||||
pvt = ((bars['close'] - bars['close'].shift(1)) /
|
trend = ((bars['close'] - bars['close'].shift(1)) /
|
||||||
bars['close'].shift(1)) * bars['volume']
|
bars['close'].shift(1)) * bars['volume']
|
||||||
return pvt.cumsum()
|
return trend.cumsum()
|
||||||
|
|
||||||
|
|
||||||
# =============================================
|
# =============================================
|
||||||
|
|
||||||
|
|
||||||
PandasObject.session = session
|
PandasObject.session = session
|
||||||
PandasObject.atr = atr
|
PandasObject.atr = atr
|
||||||
PandasObject.bollinger_bands = bollinger_bands
|
PandasObject.bollinger_bands = bollinger_bands
|
||||||
|
@ -613,4 +640,11 @@ PandasObject.rolling_weighted_mean = rolling_weighted_mean
|
||||||
|
|
||||||
PandasObject.sma = sma
|
PandasObject.sma = sma
|
||||||
PandasObject.wma = wma
|
PandasObject.wma = wma
|
||||||
|
PandasObject.ema = wma
|
||||||
PandasObject.hma = hma
|
PandasObject.hma = hma
|
||||||
|
|
||||||
|
PandasObject.zlsma = zlsma
|
||||||
|
PandasObject.zlwma = zlema
|
||||||
|
PandasObject.zlema = zlema
|
||||||
|
PandasObject.zlhma = zlhma
|
||||||
|
PandasObject.zlma = zlma
|
||||||
|
|
|
@ -39,7 +39,7 @@ class Worker(object):
|
||||||
logger.debug("sd_notify: READY=1")
|
logger.debug("sd_notify: READY=1")
|
||||||
self._sd_notify.notify("READY=1")
|
self._sd_notify.notify("READY=1")
|
||||||
|
|
||||||
def _init(self, reconfig: bool):
|
def _init(self, reconfig: bool) -> None:
|
||||||
"""
|
"""
|
||||||
Also called from the _reconfigure() method (with reconfig=True).
|
Also called from the _reconfigure() method (with reconfig=True).
|
||||||
"""
|
"""
|
||||||
|
@ -63,17 +63,17 @@ class Worker(object):
|
||||||
return self.freqtrade.state
|
return self.freqtrade.state
|
||||||
|
|
||||||
@state.setter
|
@state.setter
|
||||||
def state(self, value: State):
|
def state(self, value: State) -> None:
|
||||||
self.freqtrade.state = value
|
self.freqtrade.state = value
|
||||||
|
|
||||||
def run(self):
|
def run(self) -> None:
|
||||||
state = None
|
state = None
|
||||||
while True:
|
while True:
|
||||||
state = self._worker(old_state=state)
|
state = self._worker(old_state=state)
|
||||||
if state == State.RELOAD_CONF:
|
if state == State.RELOAD_CONF:
|
||||||
self.freqtrade = self._reconfigure()
|
self._reconfigure()
|
||||||
|
|
||||||
def _worker(self, old_state: State, throttle_secs: Optional[float] = None) -> State:
|
def _worker(self, old_state: Optional[State], throttle_secs: Optional[float] = None) -> State:
|
||||||
"""
|
"""
|
||||||
Trading routine that must be run at each loop
|
Trading routine that must be run at each loop
|
||||||
:param old_state: the previous service state from the previous call
|
:param old_state: the previous service state from the previous call
|
||||||
|
@ -91,7 +91,7 @@ class Worker(object):
|
||||||
})
|
})
|
||||||
logger.info('Changing state to: %s', state.name)
|
logger.info('Changing state to: %s', state.name)
|
||||||
if state == State.RUNNING:
|
if state == State.RUNNING:
|
||||||
self.freqtrade.rpc.startup_messages(self._config, self.freqtrade.pairlists)
|
self.freqtrade.startup()
|
||||||
|
|
||||||
if state == State.STOPPED:
|
if state == State.STOPPED:
|
||||||
# Ping systemd watchdog before sleeping in the stopped state
|
# Ping systemd watchdog before sleeping in the stopped state
|
||||||
|
@ -148,7 +148,7 @@ class Worker(object):
|
||||||
# state_changed = True
|
# state_changed = True
|
||||||
return state_changed
|
return state_changed
|
||||||
|
|
||||||
def _reconfigure(self):
|
def _reconfigure(self) -> None:
|
||||||
"""
|
"""
|
||||||
Cleans up current freqtradebot instance, reloads the configuration and
|
Cleans up current freqtradebot instance, reloads the configuration and
|
||||||
replaces it with the new instance
|
replaces it with the new instance
|
||||||
|
@ -174,7 +174,7 @@ class Worker(object):
|
||||||
logger.debug("sd_notify: READY=1")
|
logger.debug("sd_notify: READY=1")
|
||||||
self._sd_notify.notify("READY=1")
|
self._sd_notify.notify("READY=1")
|
||||||
|
|
||||||
def exit(self):
|
def exit(self) -> None:
|
||||||
# Tell systemd that we are exiting now
|
# Tell systemd that we are exiting now
|
||||||
if self._sd_notify:
|
if self._sd_notify:
|
||||||
logger.debug("sd_notify: STOPPING=1")
|
logger.debug("sd_notify: STOPPING=1")
|
||||||
|
|
|
@ -2,19 +2,22 @@ site_name: Freqtrade
|
||||||
nav:
|
nav:
|
||||||
- About: index.md
|
- About: index.md
|
||||||
- Installation: installation.md
|
- Installation: installation.md
|
||||||
|
- Installation Docker: docker.md
|
||||||
- Configuration: configuration.md
|
- Configuration: configuration.md
|
||||||
- Custom Strategy: bot-optimization.md
|
- Strategy Customization: strategy-customization.md
|
||||||
- Stoploss: stoploss.md
|
- Stoploss: stoploss.md
|
||||||
- Start the bot: bot-usage.md
|
- Start the bot: bot-usage.md
|
||||||
- Control the bot:
|
- Control the bot:
|
||||||
- Telegram: telegram-usage.md
|
- Telegram: telegram-usage.md
|
||||||
- Web Hook: webhook-config.md
|
- Web Hook: webhook-config.md
|
||||||
|
- REST API: rest-api.md
|
||||||
- Backtesting: backtesting.md
|
- Backtesting: backtesting.md
|
||||||
- Hyperopt: hyperopt.md
|
- Hyperopt: hyperopt.md
|
||||||
- Edge positioning: edge.md
|
- Edge positioning: edge.md
|
||||||
- Plotting: plotting.md
|
- Plotting: plotting.md
|
||||||
- Deprecated features: deprecated.md
|
- Deprecated features: deprecated.md
|
||||||
- FAQ: faq.md
|
- FAQ: faq.md
|
||||||
|
- Data Analysis: data-analysis.md
|
||||||
- SQL Cheatsheet: sql_cheatsheet.md
|
- SQL Cheatsheet: sql_cheatsheet.md
|
||||||
- Sandbox testing: sandbox-testing.md
|
- Sandbox testing: sandbox-testing.md
|
||||||
- Contributors guide: developer.md
|
- Contributors guide: developer.md
|
||||||
|
|
32
requirements-common.txt
Normal file
32
requirements-common.txt
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# requirements without requirements installable via conda
|
||||||
|
# mainly used for Raspberry pi installs
|
||||||
|
ccxt==1.18.805
|
||||||
|
SQLAlchemy==1.3.5
|
||||||
|
python-telegram-bot==11.1.0
|
||||||
|
arrow==0.14.2
|
||||||
|
cachetools==3.1.1
|
||||||
|
requests==2.22.0
|
||||||
|
urllib3==1.24.2 # pyup: ignore
|
||||||
|
wrapt==1.11.2
|
||||||
|
scikit-learn==0.21.2
|
||||||
|
joblib==0.13.2
|
||||||
|
jsonschema==3.0.1
|
||||||
|
TA-Lib==0.4.17
|
||||||
|
tabulate==0.8.3
|
||||||
|
coinmarketcap==5.0.3
|
||||||
|
|
||||||
|
# Required for hyperopt
|
||||||
|
scikit-optimize==0.5.2
|
||||||
|
filelock==3.0.12
|
||||||
|
|
||||||
|
# find first, C search in arrays
|
||||||
|
py_find_1st==1.1.3
|
||||||
|
|
||||||
|
#Load ticker files 30% faster
|
||||||
|
python-rapidjson==0.7.2
|
||||||
|
|
||||||
|
# Notify systemd
|
||||||
|
sdnotify==0.3.2
|
||||||
|
|
||||||
|
# Api server
|
||||||
|
flask==1.0.3
|
|
@ -1,12 +1,13 @@
|
||||||
# Include all requirements to run the bot.
|
# Include all requirements to run the bot.
|
||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
|
-r requirements-plot.txt
|
||||||
|
|
||||||
flake8==3.7.7
|
flake8==3.7.7
|
||||||
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==4.4.1
|
pytest==4.6.3
|
||||||
pytest-mock==1.10.3
|
pytest-mock==1.10.4
|
||||||
pytest-asyncio==0.10.0
|
pytest-asyncio==0.10.0
|
||||||
pytest-cov==2.6.1
|
pytest-cov==2.7.1
|
||||||
coveralls==1.7.0
|
coveralls==1.8.1
|
||||||
mypy==0.701
|
mypy==0.710
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
ccxt==1.18.472
|
|
||||||
SQLAlchemy==1.3.3
|
|
||||||
python-telegram-bot==11.1.0
|
|
||||||
arrow==0.13.1
|
|
||||||
cachetools==3.1.0
|
|
||||||
requests==2.21.0
|
|
||||||
urllib3==1.24.1
|
|
||||||
wrapt==1.11.1
|
|
||||||
scikit-learn==0.20.3
|
|
||||||
joblib==0.13.2
|
|
||||||
jsonschema==3.0.1
|
|
||||||
TA-Lib==0.4.17
|
|
||||||
tabulate==0.8.3
|
|
||||||
coinmarketcap==5.0.3
|
|
||||||
|
|
||||||
# Required for hyperopt
|
|
||||||
scikit-optimize==0.5.2
|
|
||||||
|
|
||||||
# find first, C search in arrays
|
|
||||||
py_find_1st==1.1.3
|
|
||||||
|
|
||||||
#Load ticker files 30% faster
|
|
||||||
python-rapidjson==0.7.0
|
|
|
@ -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==3.8.0
|
plotly==3.10.0
|
||||||
|
|
||||||
|
|
|
@ -1,29 +1,6 @@
|
||||||
ccxt==1.18.472
|
# Load common requirements
|
||||||
SQLAlchemy==1.3.3
|
-r requirements-common.txt
|
||||||
python-telegram-bot==11.1.0
|
|
||||||
arrow==0.13.1
|
numpy==1.16.4
|
||||||
cachetools==3.1.0
|
|
||||||
requests==2.21.0
|
|
||||||
urllib3==1.24.1
|
|
||||||
wrapt==1.11.1
|
|
||||||
numpy==1.16.2
|
|
||||||
pandas==0.24.2
|
pandas==0.24.2
|
||||||
scikit-learn==0.20.3
|
scipy==1.3.0
|
||||||
joblib==0.13.2
|
|
||||||
scipy==1.2.1
|
|
||||||
jsonschema==3.0.1
|
|
||||||
TA-Lib==0.4.17
|
|
||||||
tabulate==0.8.3
|
|
||||||
coinmarketcap==5.0.3
|
|
||||||
|
|
||||||
# Required for hyperopt
|
|
||||||
scikit-optimize==0.5.2
|
|
||||||
|
|
||||||
# find first, C search in arrays
|
|
||||||
py_find_1st==1.1.3
|
|
||||||
|
|
||||||
# Load ticker files 30% faster
|
|
||||||
python-rapidjson==0.7.0
|
|
||||||
|
|
||||||
# Notify systemd
|
|
||||||
sdnotify==0.3.2
|
|
||||||
|
|
|
@ -1,55 +1,67 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
This script generates json data
|
This script generates json files with pairs history data
|
||||||
"""
|
"""
|
||||||
|
import arrow
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import arrow
|
from typing import Any, Dict, List
|
||||||
from typing import Any, Dict
|
|
||||||
|
|
||||||
from freqtrade.arguments import Arguments
|
from freqtrade.arguments import Arguments, TimeRange
|
||||||
from freqtrade.arguments import TimeRange
|
from freqtrade.configuration import Configuration
|
||||||
from freqtrade.exchange import Exchange
|
|
||||||
from freqtrade.data.history import download_pair_history
|
from freqtrade.data.history import download_pair_history
|
||||||
from freqtrade.configuration import Configuration, set_loggers
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.misc import deep_merge_dicts
|
from freqtrade.misc import deep_merge_dicts
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.INFO,
|
logger = logging.getLogger('download_backtest_data')
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
||||||
)
|
|
||||||
set_loggers(0)
|
|
||||||
|
|
||||||
DEFAULT_DL_PATH = 'user_data/data'
|
DEFAULT_DL_PATH = 'user_data/data'
|
||||||
|
|
||||||
arguments = Arguments(sys.argv[1:], 'download utility')
|
arguments = Arguments(sys.argv[1:], 'Download backtest data')
|
||||||
arguments.testdata_dl_options()
|
arguments.common_options()
|
||||||
args = arguments.parse_args()
|
arguments.download_data_options()
|
||||||
|
|
||||||
timeframes = args.timeframes
|
# Do not read the default config if config is not specified
|
||||||
|
# in the command line options explicitely
|
||||||
|
args = arguments.parse_args(no_default_config=True)
|
||||||
|
|
||||||
|
# Use bittrex as default exchange
|
||||||
|
exchange_name = args.exchange or 'bittrex'
|
||||||
|
|
||||||
|
pairs: List = []
|
||||||
|
|
||||||
|
configuration = Configuration(args)
|
||||||
|
config: Dict[str, Any] = {}
|
||||||
|
|
||||||
if args.config:
|
if args.config:
|
||||||
configuration = Configuration(args)
|
|
||||||
|
|
||||||
config: Dict[str, Any] = {}
|
|
||||||
# Now expecting a list of config filenames here, not a string
|
# Now expecting a list of config filenames here, not a string
|
||||||
for path in args.config:
|
for path in args.config:
|
||||||
print(f"Using config: {path}...")
|
logger.info(f"Using config: {path}...")
|
||||||
# Merge config options, overwriting old values
|
# Merge config options, overwriting old values
|
||||||
config = deep_merge_dicts(configuration._load_config_file(path), config)
|
config = deep_merge_dicts(configuration._load_config_file(path), config)
|
||||||
|
|
||||||
config['stake_currency'] = ''
|
config['stake_currency'] = ''
|
||||||
# Ensure we do not use Exchange credentials
|
# Ensure we do not use Exchange credentials
|
||||||
|
config['exchange']['dry_run'] = True
|
||||||
config['exchange']['key'] = ''
|
config['exchange']['key'] = ''
|
||||||
config['exchange']['secret'] = ''
|
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:
|
else:
|
||||||
config = {
|
config = {
|
||||||
'stake_currency': '',
|
'stake_currency': '',
|
||||||
'dry_run': True,
|
'dry_run': True,
|
||||||
'exchange': {
|
'exchange': {
|
||||||
'name': args.exchange,
|
'name': exchange_name,
|
||||||
'key': '',
|
'key': '',
|
||||||
'secret': '',
|
'secret': '',
|
||||||
'pair_whitelist': [],
|
'pair_whitelist': [],
|
||||||
|
@ -59,56 +71,72 @@ else:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
timeframes = args.timeframes or ['1m', '5m']
|
||||||
|
|
||||||
|
configuration._load_logging_config(config)
|
||||||
|
|
||||||
dl_path = Path(DEFAULT_DL_PATH).joinpath(config['exchange']['name'])
|
if args.config and args.exchange:
|
||||||
if args.export:
|
logger.warning("The --exchange option is ignored, "
|
||||||
dl_path = Path(args.export)
|
"using exchange settings from the configuration file.")
|
||||||
|
|
||||||
if not dl_path.is_dir():
|
# Check if the exchange set by the user is supported
|
||||||
sys.exit(f'Directory {dl_path} does not exist.')
|
configuration.check_exchange(config)
|
||||||
|
|
||||||
|
configuration._load_datadir_config(config)
|
||||||
|
|
||||||
|
dl_path = Path(config['datadir'])
|
||||||
|
|
||||||
pairs_file = Path(args.pairs_file) if args.pairs_file else dl_path.joinpath('pairs.json')
|
pairs_file = Path(args.pairs_file) if args.pairs_file else dl_path.joinpath('pairs.json')
|
||||||
if not pairs_file.exists():
|
|
||||||
sys.exit(f'No pairs file found with path {pairs_file}.')
|
|
||||||
|
|
||||||
with pairs_file.open() as file:
|
if not pairs or args.pairs_file:
|
||||||
PAIRS = list(set(json.load(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}".')
|
||||||
|
|
||||||
PAIRS.sort()
|
with pairs_file.open() as file:
|
||||||
|
pairs = list(set(json.load(file)))
|
||||||
|
|
||||||
|
pairs.sort()
|
||||||
|
|
||||||
timerange = TimeRange()
|
timerange = TimeRange()
|
||||||
if args.days:
|
if args.days:
|
||||||
time_since = arrow.utcnow().shift(days=-args.days).strftime("%Y%m%d")
|
time_since = arrow.utcnow().shift(days=-args.days).strftime("%Y%m%d")
|
||||||
timerange = arguments.parse_timerange(f'{time_since}-')
|
timerange = arguments.parse_timerange(f'{time_since}-')
|
||||||
|
|
||||||
|
logger.info(f'About to download pairs: {pairs}, intervals: {timeframes} to {dl_path}')
|
||||||
|
|
||||||
print(f'About to download pairs: {PAIRS} to {dl_path}')
|
|
||||||
|
|
||||||
# Init exchange
|
|
||||||
exchange = Exchange(config)
|
|
||||||
pairs_not_available = []
|
pairs_not_available = []
|
||||||
|
|
||||||
for pair in PAIRS:
|
try:
|
||||||
if pair not in exchange._api.markets:
|
# Init exchange
|
||||||
pairs_not_available.append(pair)
|
exchange = Exchange(config)
|
||||||
print(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():
|
|
||||||
print(f'Deleting existing data for pair {pair}, interval {ticker_interval}')
|
|
||||||
dl_file.unlink()
|
|
||||||
|
|
||||||
print(f'downloading pair {pair}, interval {ticker_interval}')
|
for pair in pairs:
|
||||||
download_pair_history(datadir=dl_path, exchange=exchange,
|
if pair not in exchange._api.markets:
|
||||||
pair=pair,
|
pairs_not_available.append(pair)
|
||||||
ticker_interval=ticker_interval,
|
logger.info(f"Skipping pair {pair}...")
|
||||||
timerange=timerange)
|
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)
|
||||||
|
|
||||||
if pairs_not_available:
|
except KeyboardInterrupt:
|
||||||
print(f"Pairs [{','.join(pairs_not_available)}] not availble.")
|
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']}.")
|
||||||
|
|
|
@ -26,145 +26,21 @@ Example of usage:
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from argparse import Namespace
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import plotly.graph_objs as go
|
|
||||||
import pytz
|
|
||||||
from plotly import tools
|
|
||||||
from plotly.offline import plot
|
|
||||||
|
|
||||||
from freqtrade import persistence
|
from freqtrade.arguments import Arguments
|
||||||
from freqtrade.arguments import Arguments, TimeRange
|
|
||||||
from freqtrade.data import history
|
from freqtrade.data import history
|
||||||
from freqtrade.data.btanalysis import BT_DATA_COLUMNS, load_backtest_data
|
from freqtrade.data.btanalysis import (extract_trades_of_period,
|
||||||
from freqtrade.exchange import Exchange
|
load_backtest_data, load_trades_from_db)
|
||||||
from freqtrade.optimize.backtesting import setup_configuration
|
from freqtrade.optimize import setup_configuration
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.plot.plotting import generate_graph, generate_plot_file
|
||||||
from freqtrade.resolvers import StrategyResolver
|
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||||
|
from freqtrade.state import RunMode
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
_CONF: Dict[str, Any] = {}
|
|
||||||
|
|
||||||
timeZone = pytz.UTC
|
|
||||||
|
|
||||||
|
|
||||||
def load_trades(args: Namespace, pair: str, timerange: TimeRange) -> pd.DataFrame:
|
|
||||||
trades: pd.DataFrame = pd.DataFrame()
|
|
||||||
if args.db_url:
|
|
||||||
persistence.init(_CONF)
|
|
||||||
columns = ["pair", "profit", "open_time", "close_time",
|
|
||||||
"open_rate", "close_rate", "duration"]
|
|
||||||
|
|
||||||
for x in Trade.query.all():
|
|
||||||
print("date: {}".format(x.open_date))
|
|
||||||
|
|
||||||
trades = pd.DataFrame([(t.pair, t.calc_profit(),
|
|
||||||
t.open_date.replace(tzinfo=timeZone),
|
|
||||||
t.close_date.replace(tzinfo=timeZone) if t.close_date else None,
|
|
||||||
t.open_rate, t.close_rate,
|
|
||||||
t.close_date.timestamp() - t.open_date.timestamp()
|
|
||||||
if t.close_date else None)
|
|
||||||
for t in Trade.query.filter(Trade.pair.is_(pair)).all()],
|
|
||||||
columns=columns)
|
|
||||||
|
|
||||||
elif args.exportfilename:
|
|
||||||
|
|
||||||
file = Path(args.exportfilename)
|
|
||||||
if file.exists():
|
|
||||||
load_backtest_data(file)
|
|
||||||
|
|
||||||
else:
|
|
||||||
trades = pd.DataFrame([], columns=BT_DATA_COLUMNS)
|
|
||||||
|
|
||||||
return trades
|
|
||||||
|
|
||||||
|
|
||||||
def generate_plot_file(fig, pair, ticker_interval, is_last) -> None:
|
|
||||||
"""
|
|
||||||
Generate a plot html file from pre populated fig plotly object
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
logger.info('Generate plot file for %s', pair)
|
|
||||||
|
|
||||||
pair_name = pair.replace("/", "_")
|
|
||||||
file_name = 'freqtrade-plot-' + pair_name + '-' + ticker_interval + '.html'
|
|
||||||
|
|
||||||
Path("user_data/plots").mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
plot(fig, filename=str(Path('user_data/plots').joinpath(file_name)), auto_open=False)
|
|
||||||
if is_last:
|
|
||||||
plot(fig, filename=str(Path('user_data').joinpath('freqtrade-plot.html')), auto_open=False)
|
|
||||||
|
|
||||||
|
|
||||||
def get_trading_env(args: Namespace):
|
|
||||||
"""
|
|
||||||
Initalize freqtrade Exchange and Strategy, split pairs recieved in parameter
|
|
||||||
:return: Strategy
|
|
||||||
"""
|
|
||||||
global _CONF
|
|
||||||
|
|
||||||
# Load the configuration
|
|
||||||
_CONF.update(setup_configuration(args))
|
|
||||||
print(_CONF)
|
|
||||||
|
|
||||||
pairs = args.pairs.split(',')
|
|
||||||
if pairs is None:
|
|
||||||
logger.critical('Parameter --pairs mandatory;. E.g --pairs ETH/BTC,XRP/BTC')
|
|
||||||
exit()
|
|
||||||
|
|
||||||
# Load the strategy
|
|
||||||
try:
|
|
||||||
strategy = StrategyResolver(_CONF).strategy
|
|
||||||
exchange = Exchange(_CONF)
|
|
||||||
except AttributeError:
|
|
||||||
logger.critical(
|
|
||||||
'Impossible to load the strategy. Please check the file "user_data/strategies/%s.py"',
|
|
||||||
args.strategy
|
|
||||||
)
|
|
||||||
exit()
|
|
||||||
|
|
||||||
return [strategy, exchange, pairs]
|
|
||||||
|
|
||||||
|
|
||||||
def get_tickers_data(strategy, exchange, pairs: List[str], args):
|
|
||||||
"""
|
|
||||||
Get tickers data for each pairs on live or local, option defined in args
|
|
||||||
:return: dictinnary of tickers. output format: {'pair': tickersdata}
|
|
||||||
"""
|
|
||||||
|
|
||||||
ticker_interval = strategy.ticker_interval
|
|
||||||
timerange = Arguments.parse_timerange(args.timerange)
|
|
||||||
|
|
||||||
tickers = {}
|
|
||||||
if args.live:
|
|
||||||
logger.info('Downloading pairs.')
|
|
||||||
exchange.refresh_latest_ohlcv([(pair, ticker_interval) for pair in pairs])
|
|
||||||
for pair in pairs:
|
|
||||||
tickers[pair] = exchange.klines((pair, ticker_interval))
|
|
||||||
else:
|
|
||||||
tickers = history.load_data(
|
|
||||||
datadir=Path(str(_CONF.get("datadir"))),
|
|
||||||
pairs=pairs,
|
|
||||||
ticker_interval=ticker_interval,
|
|
||||||
refresh_pairs=_CONF.get('refresh_pairs', False),
|
|
||||||
timerange=timerange,
|
|
||||||
exchange=Exchange(_CONF)
|
|
||||||
)
|
|
||||||
|
|
||||||
# No ticker found, impossible to download, len mismatch
|
|
||||||
for pair, data in tickers.copy().items():
|
|
||||||
logger.debug("checking tickers data of pair: %s", pair)
|
|
||||||
logger.debug("data.empty: %s", data.empty)
|
|
||||||
logger.debug("len(data): %s", len(data))
|
|
||||||
if data.empty:
|
|
||||||
del tickers[pair]
|
|
||||||
logger.info(
|
|
||||||
'An issue occured while retreiving datas of %s pair, please retry '
|
|
||||||
'using -l option for live or --refresh-pairs-cached', pair)
|
|
||||||
return tickers
|
|
||||||
|
|
||||||
|
|
||||||
def generate_dataframe(strategy, tickers, pair) -> pd.DataFrame:
|
def generate_dataframe(strategy, tickers, pair) -> pd.DataFrame:
|
||||||
|
@ -181,211 +57,7 @@ def generate_dataframe(strategy, tickers, pair) -> pd.DataFrame:
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
|
|
||||||
def extract_trades_of_period(dataframe, trades) -> pd.DataFrame:
|
def analyse_and_plot_pairs(config: Dict[str, Any]):
|
||||||
"""
|
|
||||||
Compare trades and backtested pair DataFrames to get trades performed on backtested period
|
|
||||||
:return: the DataFrame of a trades of period
|
|
||||||
"""
|
|
||||||
trades = trades.loc[trades['open_time'] >= dataframe.iloc[0]['date']]
|
|
||||||
return trades
|
|
||||||
|
|
||||||
|
|
||||||
def generate_graph(
|
|
||||||
pair: str,
|
|
||||||
trades: pd.DataFrame,
|
|
||||||
data: pd.DataFrame,
|
|
||||||
indicators1: str,
|
|
||||||
indicators2: str
|
|
||||||
) -> tools.make_subplots:
|
|
||||||
"""
|
|
||||||
Generate the graph from the data generated by Backtesting or from DB
|
|
||||||
:param pair: Pair to Display on the graph
|
|
||||||
:param trades: All trades created
|
|
||||||
:param data: Dataframe
|
|
||||||
:indicators1: String Main plot indicators
|
|
||||||
:indicators2: String Sub plot indicators
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Define the graph
|
|
||||||
fig = tools.make_subplots(
|
|
||||||
rows=3,
|
|
||||||
cols=1,
|
|
||||||
shared_xaxes=True,
|
|
||||||
row_width=[1, 1, 4],
|
|
||||||
vertical_spacing=0.0001,
|
|
||||||
)
|
|
||||||
fig['layout'].update(title=pair)
|
|
||||||
fig['layout']['yaxis1'].update(title='Price')
|
|
||||||
fig['layout']['yaxis2'].update(title='Volume')
|
|
||||||
fig['layout']['yaxis3'].update(title='Other')
|
|
||||||
fig['layout']['xaxis']['rangeslider'].update(visible=False)
|
|
||||||
|
|
||||||
# Common information
|
|
||||||
candles = go.Candlestick(
|
|
||||||
x=data.date,
|
|
||||||
open=data.open,
|
|
||||||
high=data.high,
|
|
||||||
low=data.low,
|
|
||||||
close=data.close,
|
|
||||||
name='Price'
|
|
||||||
)
|
|
||||||
|
|
||||||
df_buy = data[data['buy'] == 1]
|
|
||||||
buys = go.Scattergl(
|
|
||||||
x=df_buy.date,
|
|
||||||
y=df_buy.close,
|
|
||||||
mode='markers',
|
|
||||||
name='buy',
|
|
||||||
marker=dict(
|
|
||||||
symbol='triangle-up-dot',
|
|
||||||
size=9,
|
|
||||||
line=dict(width=1),
|
|
||||||
color='green',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
df_sell = data[data['sell'] == 1]
|
|
||||||
sells = go.Scattergl(
|
|
||||||
x=df_sell.date,
|
|
||||||
y=df_sell.close,
|
|
||||||
mode='markers',
|
|
||||||
name='sell',
|
|
||||||
marker=dict(
|
|
||||||
symbol='triangle-down-dot',
|
|
||||||
size=9,
|
|
||||||
line=dict(width=1),
|
|
||||||
color='red',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
trade_buys = go.Scattergl(
|
|
||||||
x=trades["open_time"],
|
|
||||||
y=trades["open_rate"],
|
|
||||||
mode='markers',
|
|
||||||
name='trade_buy',
|
|
||||||
marker=dict(
|
|
||||||
symbol='square-open',
|
|
||||||
size=11,
|
|
||||||
line=dict(width=2),
|
|
||||||
color='green'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
trade_sells = go.Scattergl(
|
|
||||||
x=trades["close_time"],
|
|
||||||
y=trades["close_rate"],
|
|
||||||
mode='markers',
|
|
||||||
name='trade_sell',
|
|
||||||
marker=dict(
|
|
||||||
symbol='square-open',
|
|
||||||
size=11,
|
|
||||||
line=dict(width=2),
|
|
||||||
color='red'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Row 1
|
|
||||||
fig.append_trace(candles, 1, 1)
|
|
||||||
|
|
||||||
if 'bb_lowerband' in data and 'bb_upperband' in data:
|
|
||||||
bb_lower = go.Scatter(
|
|
||||||
x=data.date,
|
|
||||||
y=data.bb_lowerband,
|
|
||||||
name='BB lower',
|
|
||||||
line={'color': 'rgba(255,255,255,0)'},
|
|
||||||
)
|
|
||||||
bb_upper = go.Scatter(
|
|
||||||
x=data.date,
|
|
||||||
y=data.bb_upperband,
|
|
||||||
name='BB upper',
|
|
||||||
fill="tonexty",
|
|
||||||
fillcolor="rgba(0,176,246,0.2)",
|
|
||||||
line={'color': 'rgba(255,255,255,0)'},
|
|
||||||
)
|
|
||||||
fig.append_trace(bb_lower, 1, 1)
|
|
||||||
fig.append_trace(bb_upper, 1, 1)
|
|
||||||
|
|
||||||
fig = generate_row(fig=fig, row=1, raw_indicators=indicators1, data=data)
|
|
||||||
fig.append_trace(buys, 1, 1)
|
|
||||||
fig.append_trace(sells, 1, 1)
|
|
||||||
fig.append_trace(trade_buys, 1, 1)
|
|
||||||
fig.append_trace(trade_sells, 1, 1)
|
|
||||||
|
|
||||||
# Row 2
|
|
||||||
volume = go.Bar(
|
|
||||||
x=data['date'],
|
|
||||||
y=data['volume'],
|
|
||||||
name='Volume'
|
|
||||||
)
|
|
||||||
fig.append_trace(volume, 2, 1)
|
|
||||||
|
|
||||||
# Row 3
|
|
||||||
fig = generate_row(fig=fig, row=3, raw_indicators=indicators2, data=data)
|
|
||||||
|
|
||||||
return fig
|
|
||||||
|
|
||||||
|
|
||||||
def generate_row(fig, row, raw_indicators, data) -> tools.make_subplots:
|
|
||||||
"""
|
|
||||||
Generator all the indicator selected by the user for a specific row
|
|
||||||
"""
|
|
||||||
for indicator in raw_indicators.split(','):
|
|
||||||
if indicator in data:
|
|
||||||
scattergl = go.Scattergl(
|
|
||||||
x=data['date'],
|
|
||||||
y=data[indicator],
|
|
||||||
name=indicator
|
|
||||||
)
|
|
||||||
fig.append_trace(scattergl, row, 1)
|
|
||||||
else:
|
|
||||||
logger.info(
|
|
||||||
'Indicator "%s" ignored. Reason: This indicator is not found '
|
|
||||||
'in your strategy.',
|
|
||||||
indicator
|
|
||||||
)
|
|
||||||
|
|
||||||
return fig
|
|
||||||
|
|
||||||
|
|
||||||
def plot_parse_args(args: List[str]) -> Namespace:
|
|
||||||
"""
|
|
||||||
Parse args passed to the script
|
|
||||||
:param args: Cli arguments
|
|
||||||
:return: args: Array with all arguments
|
|
||||||
"""
|
|
||||||
arguments = Arguments(args, 'Graph dataframe')
|
|
||||||
arguments.scripts_options()
|
|
||||||
arguments.parser.add_argument(
|
|
||||||
'--indicators1',
|
|
||||||
help='Set indicators from your strategy you want in the first row of the graph. Separate '
|
|
||||||
'them with a coma. E.g: ema3,ema5 (default: %(default)s)',
|
|
||||||
type=str,
|
|
||||||
default='sma,ema3,ema5',
|
|
||||||
dest='indicators1',
|
|
||||||
)
|
|
||||||
|
|
||||||
arguments.parser.add_argument(
|
|
||||||
'--indicators2',
|
|
||||||
help='Set indicators from your strategy you want in the third row of the graph. Separate '
|
|
||||||
'them with a coma. E.g: fastd,fastk (default: %(default)s)',
|
|
||||||
type=str,
|
|
||||||
default='macd,macdsignal',
|
|
||||||
dest='indicators2',
|
|
||||||
)
|
|
||||||
arguments.parser.add_argument(
|
|
||||||
'--plot-limit',
|
|
||||||
help='Specify tick limit for plotting - too high values cause huge files - '
|
|
||||||
'Default: %(default)s',
|
|
||||||
dest='plot_limit',
|
|
||||||
default=750,
|
|
||||||
type=int,
|
|
||||||
)
|
|
||||||
arguments.common_args_parser()
|
|
||||||
arguments.optimizer_shared_options(arguments.parser)
|
|
||||||
arguments.backtesting_options(arguments.parser)
|
|
||||||
return arguments.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def analyse_and_plot_pairs(args: Namespace):
|
|
||||||
"""
|
"""
|
||||||
From arguments provided in cli:
|
From arguments provided in cli:
|
||||||
-Initialise backtest env
|
-Initialise backtest env
|
||||||
|
@ -396,12 +68,28 @@ def analyse_and_plot_pairs(args: Namespace):
|
||||||
-Generate plot files
|
-Generate plot files
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
strategy, exchange, pairs = get_trading_env(args)
|
exchange = ExchangeResolver(config.get('exchange', {}).get('name'), config).exchange
|
||||||
|
|
||||||
|
strategy = StrategyResolver(config).strategy
|
||||||
|
if "pairs" in config:
|
||||||
|
pairs = config["pairs"].split(',')
|
||||||
|
else:
|
||||||
|
pairs = config["exchange"]["pair_whitelist"]
|
||||||
|
|
||||||
# Set timerange to use
|
# Set timerange to use
|
||||||
timerange = Arguments.parse_timerange(args.timerange)
|
timerange = Arguments.parse_timerange(config["timerange"])
|
||||||
ticker_interval = strategy.ticker_interval
|
ticker_interval = strategy.ticker_interval
|
||||||
|
|
||||||
tickers = get_tickers_data(strategy, exchange, pairs, args)
|
tickers = history.load_data(
|
||||||
|
datadir=Path(str(config.get("datadir"))),
|
||||||
|
pairs=pairs,
|
||||||
|
ticker_interval=config['ticker_interval'],
|
||||||
|
refresh_pairs=config.get('refresh_pairs', False),
|
||||||
|
timerange=timerange,
|
||||||
|
exchange=exchange,
|
||||||
|
live=config.get("live", False),
|
||||||
|
)
|
||||||
|
|
||||||
pair_counter = 0
|
pair_counter = 0
|
||||||
for pair, data in tickers.items():
|
for pair, data in tickers.items():
|
||||||
pair_counter += 1
|
pair_counter += 1
|
||||||
|
@ -409,24 +97,47 @@ def analyse_and_plot_pairs(args: Namespace):
|
||||||
tickers = {}
|
tickers = {}
|
||||||
tickers[pair] = data
|
tickers[pair] = data
|
||||||
dataframe = generate_dataframe(strategy, tickers, pair)
|
dataframe = generate_dataframe(strategy, tickers, pair)
|
||||||
|
if config["trade_source"] == "DB":
|
||||||
|
trades = load_trades_from_db(config["db_url"])
|
||||||
|
elif config["trade_source"] == "file":
|
||||||
|
trades = load_backtest_data(Path(config["exportfilename"]))
|
||||||
|
|
||||||
trades = load_trades(args, pair, timerange)
|
trades = trades.loc[trades['pair'] == pair]
|
||||||
trades = extract_trades_of_period(dataframe, trades)
|
trades = extract_trades_of_period(dataframe, trades)
|
||||||
|
|
||||||
fig = generate_graph(
|
fig = generate_graph(
|
||||||
pair=pair,
|
pair=pair,
|
||||||
trades=trades,
|
|
||||||
data=dataframe,
|
data=dataframe,
|
||||||
indicators1=args.indicators1,
|
trades=trades,
|
||||||
indicators2=args.indicators2
|
indicators1=config["indicators1"].split(","),
|
||||||
|
indicators2=config["indicators2"].split(",")
|
||||||
)
|
)
|
||||||
|
|
||||||
is_last = (False, True)[pair_counter == len(tickers)]
|
generate_plot_file(fig, pair, ticker_interval)
|
||||||
generate_plot_file(fig, pair, ticker_interval, is_last)
|
|
||||||
|
|
||||||
logger.info('End of ploting process %s plots generated', pair_counter)
|
logger.info('End of ploting process %s plots generated', pair_counter)
|
||||||
|
|
||||||
|
|
||||||
|
def plot_parse_args(args: List[str]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Parse args passed to the script
|
||||||
|
:param args: Cli arguments
|
||||||
|
:return: args: Array with all arguments
|
||||||
|
"""
|
||||||
|
arguments = Arguments(args, 'Graph dataframe')
|
||||||
|
arguments.common_options()
|
||||||
|
arguments.main_options()
|
||||||
|
arguments.common_optimize_options()
|
||||||
|
arguments.backtesting_options()
|
||||||
|
arguments.common_scripts_options()
|
||||||
|
arguments.plot_dataframe_options()
|
||||||
|
parsed_args = arguments.parse_args()
|
||||||
|
|
||||||
|
# Load the configuration
|
||||||
|
config = setup_configuration(parsed_args, RunMode.BACKTEST)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
def main(sysargv: List[str]) -> None:
|
def main(sysargv: List[str]) -> None:
|
||||||
"""
|
"""
|
||||||
This function will initiate the bot and start the trading loop.
|
This function will initiate the bot and start the trading loop.
|
||||||
|
|
|
@ -27,10 +27,12 @@ from plotly.offline import plot
|
||||||
from freqtrade.arguments import Arguments
|
from freqtrade.arguments import Arguments
|
||||||
from freqtrade.configuration import Configuration
|
from freqtrade.configuration import Configuration
|
||||||
from freqtrade.data import history
|
from freqtrade.data import history
|
||||||
from freqtrade.misc import common_datearray, timeframe_to_seconds
|
from freqtrade.exchange import timeframe_to_seconds
|
||||||
|
from freqtrade.misc import common_datearray
|
||||||
from freqtrade.resolvers import StrategyResolver
|
from freqtrade.resolvers import StrategyResolver
|
||||||
from freqtrade.state import RunMode
|
from freqtrade.state import RunMode
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -204,10 +206,11 @@ def plot_parse_args(args: List[str]) -> Namespace:
|
||||||
:return: args: Array with all arguments
|
:return: args: Array with all arguments
|
||||||
"""
|
"""
|
||||||
arguments = Arguments(args, 'Graph profits')
|
arguments = Arguments(args, 'Graph profits')
|
||||||
arguments.scripts_options()
|
arguments.common_options()
|
||||||
arguments.common_args_parser()
|
arguments.main_options()
|
||||||
arguments.optimizer_shared_options(arguments.parser)
|
arguments.common_optimize_options()
|
||||||
arguments.backtesting_options(arguments.parser)
|
arguments.backtesting_options()
|
||||||
|
arguments.common_scripts_options()
|
||||||
|
|
||||||
return arguments.parse_args()
|
return arguments.parse_args()
|
||||||
|
|
||||||
|
|
264
scripts/rest_client.py
Executable file
264
scripts/rest_client.py
Executable file
|
@ -0,0 +1,264 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Simple command line client into RPC commands
|
||||||
|
Can be used as an alternate to Telegram
|
||||||
|
|
||||||
|
Should not import anything from freqtrade,
|
||||||
|
so it can be used as a standalone script.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import inspect
|
||||||
|
from urllib.parse import urlencode, urlparse, urlunparse
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from requests.exceptions import ConnectionError
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
|
)
|
||||||
|
logger = logging.getLogger("ft_rest_client")
|
||||||
|
|
||||||
|
|
||||||
|
class FtRestClient():
|
||||||
|
|
||||||
|
def __init__(self, serverurl, username=None, password=None):
|
||||||
|
|
||||||
|
self._serverurl = serverurl
|
||||||
|
self._session = requests.Session()
|
||||||
|
self._session.auth = (username, password)
|
||||||
|
|
||||||
|
def _call(self, method, apipath, params: dict = None, data=None, files=None):
|
||||||
|
|
||||||
|
if str(method).upper() not in ('GET', 'POST', 'PUT', 'DELETE'):
|
||||||
|
raise ValueError('invalid method <{0}>'.format(method))
|
||||||
|
basepath = f"{self._serverurl}/api/v1/{apipath}"
|
||||||
|
|
||||||
|
hd = {"Accept": "application/json",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Split url
|
||||||
|
schema, netloc, path, par, query, fragment = urlparse(basepath)
|
||||||
|
# URLEncode query string
|
||||||
|
query = urlencode(params) if params else ""
|
||||||
|
# recombine url
|
||||||
|
url = urlunparse((schema, netloc, path, par, query, fragment))
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = self._session.request(method, url, headers=hd, data=json.dumps(data))
|
||||||
|
# return resp.text
|
||||||
|
return resp.json()
|
||||||
|
except ConnectionError:
|
||||||
|
logger.warning("Connection error")
|
||||||
|
|
||||||
|
def _get(self, apipath, params: dict = None):
|
||||||
|
return self._call("GET", apipath, params=params)
|
||||||
|
|
||||||
|
def _post(self, apipath, params: dict = None, data: dict = None):
|
||||||
|
return self._call("POST", apipath, params=params, data=data)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""
|
||||||
|
Start the bot if it's in stopped state.
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._post("start")
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""
|
||||||
|
Stop the bot. Use start to restart
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._post("stop")
|
||||||
|
|
||||||
|
def stopbuy(self):
|
||||||
|
"""
|
||||||
|
Stop buying (but handle sells gracefully).
|
||||||
|
use reload_conf to reset
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._post("stopbuy")
|
||||||
|
|
||||||
|
def reload_conf(self):
|
||||||
|
"""
|
||||||
|
Reload configuration
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._post("reload_conf")
|
||||||
|
|
||||||
|
def balance(self):
|
||||||
|
"""
|
||||||
|
Get the account balance
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._get("balance")
|
||||||
|
|
||||||
|
def count(self):
|
||||||
|
"""
|
||||||
|
Returns the amount of open trades
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._get("count")
|
||||||
|
|
||||||
|
def daily(self, days=None):
|
||||||
|
"""
|
||||||
|
Returns the amount of open trades
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._get("daily", params={"timescale": days} if days else None)
|
||||||
|
|
||||||
|
def edge(self):
|
||||||
|
"""
|
||||||
|
Returns information about edge
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._get("edge")
|
||||||
|
|
||||||
|
def profit(self):
|
||||||
|
"""
|
||||||
|
Returns the profit summary
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._get("profit")
|
||||||
|
|
||||||
|
def performance(self):
|
||||||
|
"""
|
||||||
|
Returns the performance of the different coins
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._get("performance")
|
||||||
|
|
||||||
|
def status(self):
|
||||||
|
"""
|
||||||
|
Get the status of open trades
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._get("status")
|
||||||
|
|
||||||
|
def version(self):
|
||||||
|
"""
|
||||||
|
Returns the version of the bot
|
||||||
|
:return: json object containing the version
|
||||||
|
"""
|
||||||
|
return self._get("version")
|
||||||
|
|
||||||
|
def whitelist(self):
|
||||||
|
"""
|
||||||
|
Show the current whitelist
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._get("whitelist")
|
||||||
|
|
||||||
|
def blacklist(self, *args):
|
||||||
|
"""
|
||||||
|
Show the current blacklist
|
||||||
|
:param add: List of coins to add (example: "BNB/BTC")
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
if not args:
|
||||||
|
return self._get("blacklist")
|
||||||
|
else:
|
||||||
|
return self._post("blacklist", data={"blacklist": args})
|
||||||
|
|
||||||
|
def forcebuy(self, pair, price=None):
|
||||||
|
"""
|
||||||
|
Buy an asset
|
||||||
|
:param pair: Pair to buy (ETH/BTC)
|
||||||
|
:param price: Optional - price to buy
|
||||||
|
:return: json object of the trade
|
||||||
|
"""
|
||||||
|
data = {"pair": pair,
|
||||||
|
"price": price
|
||||||
|
}
|
||||||
|
return self._post("forcebuy", data=data)
|
||||||
|
|
||||||
|
def forcesell(self, tradeid):
|
||||||
|
"""
|
||||||
|
Force-sell a trade
|
||||||
|
:param tradeid: Id of the trade (can be received via status command)
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self._post("forcesell", data={"tradeid": tradeid})
|
||||||
|
|
||||||
|
|
||||||
|
def add_arguments():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("command",
|
||||||
|
help="Positional argument defining the command to execute.")
|
||||||
|
|
||||||
|
parser.add_argument('--show',
|
||||||
|
help='Show possible methods with this client',
|
||||||
|
dest='show',
|
||||||
|
action='store_true',
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument('-c', '--config',
|
||||||
|
help='Specify configuration file (default: %(default)s). ',
|
||||||
|
dest='config',
|
||||||
|
type=str,
|
||||||
|
metavar='PATH',
|
||||||
|
default='config.json'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument("command_arguments",
|
||||||
|
help="Positional arguments for the parameters for [command]",
|
||||||
|
nargs="*",
|
||||||
|
default=[]
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
return vars(args)
|
||||||
|
|
||||||
|
|
||||||
|
def load_config(configfile):
|
||||||
|
file = Path(configfile)
|
||||||
|
if file.is_file():
|
||||||
|
with file.open("r") as f:
|
||||||
|
config = json.load(f)
|
||||||
|
return config
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def print_commands():
|
||||||
|
# Print dynamic help for the different commands using the commands doc-strings
|
||||||
|
client = FtRestClient(None)
|
||||||
|
print("Possible commands:")
|
||||||
|
for x, y in inspect.getmembers(client):
|
||||||
|
if not x.startswith('_'):
|
||||||
|
print(f"{x} {getattr(client, x).__doc__}")
|
||||||
|
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
|
||||||
|
if args.get("help"):
|
||||||
|
print_commands()
|
||||||
|
|
||||||
|
config = load_config(args["config"])
|
||||||
|
url = config.get("api_server", {}).get("server_url", "127.0.0.1")
|
||||||
|
port = config.get("api_server", {}).get("listen_port", "8080")
|
||||||
|
username = config.get("api_server", {}).get("username")
|
||||||
|
password = config.get("api_server", {}).get("password")
|
||||||
|
|
||||||
|
server_url = f"http://{url}:{port}"
|
||||||
|
client = FtRestClient(server_url, username, password)
|
||||||
|
|
||||||
|
m = [x for x, y in inspect.getmembers(client) if not x.startswith('_')]
|
||||||
|
command = args["command"]
|
||||||
|
if command not in m:
|
||||||
|
logger.error(f"Command {command} not defined")
|
||||||
|
print_commands()
|
||||||
|
return
|
||||||
|
|
||||||
|
print(getattr(client, command)(*args["command_arguments"]))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
args = add_arguments()
|
||||||
|
main(args)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user