diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 94d998310..ae5375f43 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -5,6 +5,7 @@ If it hasn't been reported, please create a new issue. ## Step 2: Describe your environment + * Operating system: ____ * Python Version: _____ (`python -V`) * CCXT version: _____ (`pip freeze | grep ccxt`) * Branch: Master | Develop diff --git a/.gitignore b/.gitignore index 9ed046c40..3a9df9852 100644 --- a/.gitignore +++ b/.gitignore @@ -81,7 +81,6 @@ target/ # Jupyter Notebook .ipynb_checkpoints -*.ipynb # pyenv .python-version @@ -93,3 +92,6 @@ target/ .pytest_cache/ .mypy_cache/ + +#exceptions +!user_data/noteboks/*example.ipynb diff --git a/.pyup.yml b/.pyup.yml index 01d4bba2a..b1b721113 100644 --- a/.pyup.yml +++ b/.pyup.yml @@ -11,7 +11,10 @@ update: all # allowed: True, False pin: True -schedule: "every day" +# update schedule +# default: empty +# allowed: "every day", "every week", .. +schedule: "every week" search: False @@ -22,6 +25,7 @@ requirements: - requirements.txt - requirements-dev.txt - requirements-plot.txt + - requirements-common.txt # configure the branch prefix the bot is using diff --git a/.travis.yml b/.travis.yml index d24ffcf1b..a452d245b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,16 +10,11 @@ services: env: global: - IMAGE_NAME=freqtradeorg/freqtrade -addons: - apt: - packages: - - libelf-dev - - libdw-dev - - binutils-dev install: -- cd build_helpers && ./install_ta-lib.sh; cd .. -- export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH -- pip install --upgrade pytest-random-order +- cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd .. +- export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH +- export TA_LIBRARY_PATH=${HOME}/dependencies/lib +- export TA_INCLUDE_PATH=${HOME}/dependencies/lib/include - pip install -r requirements-dev.txt - pip install -e . jobs: @@ -27,21 +22,21 @@ jobs: include: - stage: tests script: - - pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/ + - pytest --random-order --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/ # Allow failure for coveralls - - coveralls || true + - coveralls || true name: pytest - script: - cp config.json.example config.json - - python freqtrade/main.py --datadir freqtrade/tests/testdata backtesting + - freqtrade --datadir freqtrade/tests/testdata backtesting name: backtest - script: - cp config.json.example config.json - - python freqtrade/main.py --datadir freqtrade/tests/testdata hyperopt -e 5 + - freqtrade --datadir freqtrade/tests/testdata hyperopt -e 5 name: hyperopt - - script: flake8 freqtrade + - script: flake8 freqtrade scripts name: flake8 - - script: mypy freqtrade + - script: mypy freqtrade scripts name: mypy - stage: docker @@ -56,4 +51,4 @@ notifications: cache: pip: True directories: - - /usr/local/lib + - $HOME/dependencies diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3c511f44d..e15059f56 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,7 @@ Few pointers for contributions: - 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). -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. ## Getting started diff --git a/Dockerfile b/Dockerfile index aeefc0722..7a0298719 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.7.2-slim-stretch +FROM python:3.7.3-slim-stretch RUN apt-get update \ && apt-get -y install curl build-essential libssl-dev \ @@ -16,7 +16,7 @@ RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib* ENV LD_LIBRARY_PATH /usr/local/lib # Install dependencies -COPY requirements.txt /freqtrade/ +COPY requirements.txt requirements-common.txt /freqtrade/ RUN pip install numpy --no-cache-dir \ && pip install -r requirements.txt --no-cache-dir diff --git a/Dockerfile.pi b/Dockerfile.pi new file mode 100644 index 000000000..1b9c4c579 --- /dev/null +++ b/Dockerfile.pi @@ -0,0 +1,40 @@ +FROM balenalib/raspberrypi3-debian:stretch + +RUN [ "cross-build-start" ] + +RUN apt-get update \ + && apt-get -y install wget curl build-essential libssl-dev libffi-dev \ + && apt-get clean + +# Prepare environment +RUN mkdir /freqtrade +WORKDIR /freqtrade + +# Install TA-lib +COPY build_helpers/ta-lib-0.4.0-src.tar.gz /freqtrade/ +RUN tar -xzf /freqtrade/ta-lib-0.4.0-src.tar.gz \ + && cd /freqtrade/ta-lib/ \ + && ./configure \ + && make \ + && make install \ + && rm /freqtrade/ta-lib-0.4.0-src.tar.gz + +ENV LD_LIBRARY_PATH /usr/local/lib + +# Install berryconda +RUN wget https://github.com/jjhelmus/berryconda/releases/download/v2.0.0/Berryconda3-2.0.0-Linux-armv7l.sh \ + && bash ./Berryconda3-2.0.0-Linux-armv7l.sh -b \ + && rm Berryconda3-2.0.0-Linux-armv7l.sh + +# Install dependencies +COPY requirements-common.txt /freqtrade/ +RUN ~/berryconda3/bin/conda install -y numpy pandas scipy \ + && ~/berryconda3/bin/pip install -r requirements-common.txt --no-cache-dir + +# Install and execute +COPY . /freqtrade/ +RUN ~/berryconda3/bin/pip install -e . --no-cache-dir + +RUN [ "cross-build-end" ] + +ENTRYPOINT ["/root/berryconda3/bin/python","./freqtrade/main.py"] diff --git a/Dockerfile.technical b/Dockerfile.technical index 5339eb232..9431e72d0 100644 --- a/Dockerfile.technical +++ b/Dockerfile.technical @@ -3,4 +3,4 @@ FROM freqtradeorg/freqtrade:develop RUN apt-get update \ && apt-get -y install git \ && apt-get clean \ - && pip install git+https://github.com/berlinguyinca/technical + && pip install git+https://github.com/freqtrade/technical diff --git a/README.md b/README.md index ade62ce94..240b4f917 100644 --- a/README.md +++ b/README.md @@ -68,9 +68,9 @@ For any other type of installation please refer to [Installation doc](https://ww ### Bot commands ``` -usage: freqtrade [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME] - [--strategy-path PATH] [--dynamic-whitelist [INT]] - [--db-url PATH] +usage: freqtrade [-h] [-v] [--logfile FILE] [--version] [-c PATH] [-d PATH] + [-s NAME] [--strategy-path PATH] [--dynamic-whitelist [INT]] + [--db-url PATH] [--sd-notify] {backtesting,edge,hyperopt} ... Free, open source crypto trading bot @@ -84,6 +84,7 @@ positional arguments: optional arguments: -h, --help show this help message and exit -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). + --logfile FILE Log to the file specified --version show program's version number and exit -c PATH, --config PATH Specify configuration file (default: None). Multiple @@ -100,6 +101,7 @@ optional arguments: --db-url PATH Override trades database URL, this is useful if dry_run is enabled or in custom deployments (default: None). + --sd-notify Notify systemd service manager. ``` ### Telegram RPC commands @@ -127,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. - `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 For Binance, please add `"BNB/"` to your blacklist to avoid issues. @@ -140,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 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) @@ -171,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? 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`. diff --git a/bin/freqtrade b/bin/freqtrade index e7ae7a4ca..25c94fe98 100755 --- a/bin/freqtrade +++ b/bin/freqtrade @@ -1,7 +1,11 @@ #!/usr/bin/env python3 import sys +import warnings -from freqtrade.main import main, set_loggers -set_loggers() +from freqtrade.main import main + +warnings.warn( + "Deprecated - To continue to run the bot like this, please run `pip install -e .` again.", + DeprecationWarning) main(sys.argv[1:]) diff --git a/build_helpers/install_ta-lib.sh b/build_helpers/install_ta-lib.sh index 9fe341bba..cb86e5f64 100755 --- a/build_helpers/install_ta-lib.sh +++ b/build_helpers/install_ta-lib.sh @@ -1,8 +1,14 @@ -if [ ! -f "/usr/local/lib/libta_lib.a" ]; then +if [ -z "$1" ]; then + INSTALL_LOC=/usr/local +else + INSTALL_LOC=${1} +fi +echo "Installing to ${INSTALL_LOC}" +if [ ! -f "${INSTALL_LOC}/lib/libta_lib.a" ]; then tar zxvf ta-lib-0.4.0-src.tar.gz cd ta-lib \ && sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \ - && ./configure \ + && ./configure --prefix=${INSTALL_LOC}/ \ && make \ && which sudo && sudo make install || make install \ && cd .. diff --git a/config.json.example b/config.json.example index 323ff711e..94084434a 100644 --- a/config.json.example +++ b/config.json.example @@ -30,7 +30,8 @@ "secret": "your_exchange_secret", "ccxt_config": {"enableRateLimit": true}, "ccxt_async_config": { - "enableRateLimit": false + "enableRateLimit": true, + "rateLimit": 500 }, "pair_whitelist": [ "ETH/BTC", diff --git a/config_binance.json.example b/config_binance.json.example index 3d11f317a..1d492fc3c 100644 --- a/config_binance.json.example +++ b/config_binance.json.example @@ -11,8 +11,8 @@ "sell": 30 }, "bid_strategy": { - "ask_last_balance": 0.0, "use_order_book": false, + "ask_last_balance": 0.0, "order_book_top": 1, "check_depth_of_market": { "enabled": false, @@ -30,7 +30,8 @@ "secret": "your_exchange_secret", "ccxt_config": {"enableRateLimit": true}, "ccxt_async_config": { - "enableRateLimit": false + "enableRateLimit": true, + "rateLimit": 200 }, "pair_whitelist": [ "AST/BTC", diff --git a/config_full.json.example b/config_full.json.example index 357a8f525..b6451859c 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -22,8 +22,8 @@ "sell": 30 }, "bid_strategy": { - "ask_last_balance": 0.0, "use_order_book": false, + "ask_last_balance": 0.0, "order_book_top": 1, "check_depth_of_market": { "enabled": false, @@ -39,12 +39,12 @@ "buy": "limit", "sell": "limit", "stoploss": "market", - "stoploss_on_exchange": "false", + "stoploss_on_exchange": false, "stoploss_on_exchange_interval": 60 }, "order_time_in_force": { "buy": "gtc", - "sell": "gtc", + "sell": "gtc" }, "pairlist": { "method": "VolumePairList", @@ -56,11 +56,14 @@ }, "exchange": { "name": "bittrex", + "sandbox": false, "key": "your_exchange_key", "secret": "your_exchange_secret", + "password": "", "ccxt_config": {"enableRateLimit": true}, "ccxt_async_config": { "enableRateLimit": false, + "rateLimit": 500, "aiohttp_trust_env": false }, "pair_whitelist": [ @@ -106,6 +109,13 @@ "token": "your_telegram_token", "chat_id": "your_telegram_chat_id" }, + "api_server": { + "enabled": false, + "listen_ip_address": "127.0.0.1", + "listen_port": 8080, + "username": "freqtrader", + "password": "SuperSecurePassword" + }, "db_url": "sqlite:///tradesv3.sqlite", "initial_state": "running", "forcebuy_enable": false, @@ -113,5 +123,5 @@ "process_throttle_secs": 5 }, "strategy": "DefaultStrategy", - "strategy_path": "/some/folder/" + "strategy_path": "user_data/strategies/" } diff --git a/config_kraken.json.example b/config_kraken.json.example index 7a47b701f..ea3677b2d 100644 --- a/config_kraken.json.example +++ b/config_kraken.json.example @@ -5,15 +5,14 @@ "fiat_display_currency": "EUR", "ticker_interval" : "5m", "dry_run": true, - "db_url": "sqlite:///tradesv3.dryrun.sqlite", "trailing_stop": false, "unfilledtimeout": { "buy": 10, "sell": 30 }, "bid_strategy": { - "ask_last_balance": 0.0, "use_order_book": false, + "ask_last_balance": 0.0, "order_book_top": 1, "check_depth_of_market": { "enabled": false, @@ -60,8 +59,8 @@ }, "telegram": { "enabled": false, - "token": "", - "chat_id": "" + "token": "your_telegram_token", + "chat_id": "your_telegram_chat_id" }, "initial_state": "running", "forcebuy_enable": false, diff --git a/docs/backtesting.md b/docs/backtesting.md index 932783160..7e9f7ff53 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -13,7 +13,7 @@ Backtesting will use the crypto-currencies (pair) from your config file and load static tickers located in [/freqtrade/tests/testdata](https://github.com/freqtrade/freqtrade/tree/develop/freqtrade/tests/testdata). If the 5 min and 1 min ticker for the crypto-currencies to test is not -already in the `testdata` folder, backtesting will download them +already in the `testdata` directory, backtesting will download them automatically. Testdata files will not be updated until you specify it. The result of backtesting will confirm you if your bot has better odds of making a profit than a loss. @@ -24,53 +24,61 @@ The backtesting is very easy with freqtrade. #### With 5 min tickers (Per default) ```bash -python3 ./freqtrade/main.py backtesting +freqtrade backtesting ``` #### With 1 min tickers ```bash -python3 ./freqtrade/main.py backtesting --ticker-interval 1m +freqtrade backtesting --ticker-interval 1m ``` #### Update cached pairs with the latest data ```bash -python3 ./freqtrade/main.py backtesting --refresh-pairs-cached +freqtrade backtesting --refresh-pairs-cached ``` #### With live data (do not alter your testdata files) ```bash -python3 ./freqtrade/main.py backtesting --live +freqtrade backtesting --live ``` #### Using a different on-disk ticker-data source ```bash -python3 ./freqtrade/main.py backtesting --datadir freqtrade/tests/testdata-20180101 +freqtrade backtesting --datadir freqtrade/tests/testdata-20180101 ``` #### With a (custom) strategy file ```bash -python3 ./freqtrade/main.py -s TestStrategy backtesting +freqtrade -s TestStrategy backtesting ``` -Where `-s TestStrategy` refers to the class name within the strategy file `test_strategy.py` found in the `freqtrade/user_data/strategies` directory +Where `-s TestStrategy` refers to the class name within the strategy file `test_strategy.py` found in the `freqtrade/user_data/strategies` directory. + +#### Comparing multiple Strategies + +```bash +freqtrade backtesting --strategy-list TestStrategy1 AwesomeStrategy --ticker-interval 5m +``` + +Where `TestStrategy1` and `AwesomeStrategy` refer to class names of strategies. #### Exporting trades to file ```bash -python3 ./freqtrade/main.py backtesting --export trades +freqtrade backtesting --export trades ``` -The exported trades can be used for [further analysis](#further-backtest-result-analysis), or can be used by the plotting script `plot_dataframe.py` in the scripts folder. +The exported trades can be used for [further analysis](#further-backtest-result-analysis), or can be used by the plotting script `plot_dataframe.py` in the scripts directory. #### Exporting trades to file specifying a custom filename ```bash -python3 ./freqtrade/main.py backtesting --export trades --export-filename=backtest_teststrategy.json +freqtrade backtesting --export trades --export-filename=backtest_teststrategy.json ``` #### Running backtest with smaller testset @@ -81,7 +89,7 @@ you want to use. The last N ticks/timeframes will be used. Example: ```bash -python3 ./freqtrade/main.py backtesting --timerange=-200 +freqtrade backtesting --timerange=-200 ``` #### Advanced use of timerange @@ -107,7 +115,7 @@ To download new set of backtesting ticker data, you can use a download script. If you are using Binance for example: -- create a folder `user_data/data/binance` and copy `pairs.json` in that folder. +- create a directory `user_data/data/binance` and copy `pairs.json` in that directory. - update the `pairs.json` to contain the currency pairs you are interested in. ```bash @@ -123,11 +131,12 @@ python scripts/download_backtest_data.py --exchange binance This will download ticker data for all the currency pairs you defined in `pairs.json`. -- To use a different folder than the exchange specific default, use `--export user_data/data/some_directory`. +- To use a different directory than the exchange specific default, use `--datadir user_data/data/some_directory`. - To change the exchange used to download the tickers, use `--exchange`. Default is `bittrex`. -- To use `pairs.json` from some other folder, use `--pairs-file some_other_dir/pairs.json`. +- To use `pairs.json` from some other directory, use `--pairs-file some_other_dir/pairs.json`. - To download ticker data for only 10 days, use `--days 10`. - Use `--timeframes` to specify which tickers to download. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute tickers. +- To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with other options. For help about backtesting usage, please refer to [Backtesting commands](#backtesting-commands). @@ -220,24 +229,8 @@ strategies, your configuration, and the crypto-currency you have set up. ### Further backtest-result analysis 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 @@ -246,7 +239,7 @@ To backtest multiple strategies, a list of Strategies can be provided. This is limited to 1 ticker-interval per run, however, data is only loaded once from disk so if you have multiple strategies you'd like to compare, this should give a nice runtime boost. -All listed Strategies need to be in the same folder. +All listed Strategies need to be in the same directory. ``` bash freqtrade backtesting --timerange 20180401-20180410 --ticker-interval 5m --strategy-list Strategy001 Strategy002 --export trades diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 739a89c07..0ca2f3cc5 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -2,13 +2,16 @@ This page explains the different parameters of the bot and how to run it. +!Note: + If you've used `setup.sh`, don't forget to activate your virtual environment (`source .env/bin/activate`) before running freqtrade commands. + ## Bot commands ``` -usage: freqtrade [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME] - [--strategy-path PATH] [--dynamic-whitelist [INT]] - [--db-url PATH] +usage: freqtrade [-h] [-v] [--logfile FILE] [--version] [-c PATH] [-d PATH] + [-s NAME] [--strategy-path PATH] [--db-url PATH] + [--sd-notify] {backtesting,edge,hyperopt} ... Free, open source crypto trading bot @@ -22,19 +25,18 @@ positional arguments: optional arguments: -h, --help show this help message and exit -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). - --version show program's version number and exit + --logfile FILE Log to the file specified + -V, --version show program's version number and exit -c PATH, --config PATH 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 Path to backtest data. -s NAME, --strategy NAME Specify strategy class name (default: DefaultStrategy). --strategy-path PATH Specify additional strategy lookup path. - --dynamic-whitelist [INT] - Dynamically generate and update whitelist based on 24h - BaseVolume (default: 20). DEPRECATED. --db-url PATH Override trades database URL, this is useful if dry_run is enabled or in custom deployments (default: None). @@ -47,7 +49,7 @@ The bot allows you to select which configuration file you want to use. Per default, the bot will load the file `./config.json` ```bash -python3 ./freqtrade/main.py -c path/far/far/away/config.json +freqtrade -c path/far/far/away/config.json ``` ### How to use multiple configuration files? @@ -63,13 +65,13 @@ empty key and secrete values while running in the Dry Mode (which does not actua require them): ```bash -python3 ./freqtrade/main.py -c ./config.json +freqtrade -c ./config.json ``` and specify both configuration files when running in the normal Live Trade Mode: ```bash -python3 ./freqtrade/main.py -c ./config.json -c path/to/secrets/keys.config.json +freqtrade -c ./config.json -c path/to/secrets/keys.config.json ``` This could help you hide your private Exchange key and Exchange secrete on you local machine @@ -78,7 +80,7 @@ prevent unintended disclosure of sensitive private data when you publish example of your configuration in the project issues or in the Internet. See more details on this technique with examples in the documentation page on -[configuration](bot-configuration.md). +[configuration](configuration.md). ### How to use **--strategy**? @@ -95,39 +97,28 @@ In `user_data/strategies` you have a file `my_awesome_strategy.py` which has a strategy class called `AwesomeStrategy` to load it: ```bash -python3 ./freqtrade/main.py --strategy AwesomeStrategy +freqtrade --strategy AwesomeStrategy ``` 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). Learn more about strategy file in -[optimize your bot](bot-optimization.md). +[Strategy Customization](strategy-customization.md). ### How to use **--strategy-path**? This parameter allows you to add an additional strategy lookup path, which gets -checked before the default locations (The passed path must be a folder!): +checked before the default locations (The passed path must be a directory!): ```bash -python3 ./freqtrade/main.py --strategy AwesomeStrategy --strategy-path /some/folder +freqtrade --strategy AwesomeStrategy --strategy-path /some/directory ``` #### How to install a strategy? -This is very simple. Copy paste your strategy file into the folder +This is very simple. Copy paste your strategy file into the directory `user_data/strategies` or use `--strategy-path`. And voila, the bot is ready to use it. -### How to use **--dynamic-whitelist**? - -!!! danger "DEPRECATED" - This command line option is deprecated. Please move your configurations using it -to the configurations that utilize the `StaticPairList` or `VolumePairList` methods set -in the configuration file -as outlined [here](configuration/#dynamic-pairlists) - -Description of this deprecated feature was moved to [here](deprecated.md). -Please no longer use it. - ### How to use **--db-url**? When you run the bot in Dry-run mode, per default no transactions are @@ -136,7 +127,7 @@ using `--db-url`. This can also be used to specify a custom database in production mode. Example command: ```bash -python3 ./freqtrade/main.py -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite +freqtrade -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite ``` ## Backtesting commands @@ -145,9 +136,11 @@ Backtesting also uses the config specified via `-c/--config`. ``` usage: freqtrade backtesting [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] - [--eps] [--dmmp] [-l] [-r] - [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] - [--export EXPORT] [--export-filename PATH] + [--max_open_trades MAX_OPEN_TRADES] + [--stake_amount STAKE_AMOUNT] [-r] [--eps] [--dmmp] + [-l] + [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] + [--export EXPORT] [--export-filename PATH] optional arguments: -h, --help show this help message and exit @@ -155,6 +148,14 @@ optional arguments: Specify ticker interval (1m, 5m, 30m, 1h, 1d). --timerange TIMERANGE 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 Allow buying the same pair multiple times (position stacking). @@ -163,12 +164,8 @@ optional arguments: (same as setting `max_open_trades` to a very high number). -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 ...] - Provide a commaseparated list of strategies to + Provide a space-separated list of strategies to backtest Please note that ticker-interval needs to be set either in config or via command line. When using this together with --export trades, the strategy-name @@ -187,7 +184,7 @@ optional arguments: ### How to use **--refresh-pairs-cached** parameter? 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 `--refresh-pairs-cached` to force Backtesting to update the data it has. @@ -205,30 +202,66 @@ to find optimal parameter values for your stategy. ``` usage: freqtrade hyperopt [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] - [--customhyperopt NAME] [--eps] [--dmmp] [-e INT] + [--max_open_trades INT] + [--stake_amount STAKE_AMOUNT] [-r] + [--customhyperopt NAME] [--hyperopt-path PATH] + [--eps] [-e INT] [-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]] + [--dmmp] [--print-all] [-j JOBS] + [--random-state INT] [--min-trades INT] [--continue] + [--hyperopt-loss NAME] optional arguments: -h, --help show this help message and exit -i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL - Specify ticker interval (1m, 5m, 30m, 1h, 1d). + Specify ticker interval (`1m`, `5m`, `30m`, `1h`, + `1d`). --timerange TIMERANGE Specify what timerange of data to use. + --max_open_trades INT + 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 Specify hyperopt class name (default: - DefaultHyperOpts). + `DefaultHyperOpts`). + --hyperopt-path PATH Specify additional lookup path for Hyperopts and + Hyperopt Loss functions. --eps, --enable-position-stacking Allow buying the same pair multiple times (position stacking). + -e INT, --epochs INT Specify number of epochs (default: 100). + -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-separated + list. Default: `all`. --dmmp, --disable-max-market-positions Disable applying `max_open_trades` during backtest (same as setting `max_open_trades` to a very high number). - -e INT, --epochs INT Specify number of epochs (default: 100). - -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 - 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. + --random-state INT Set random state to some positive integer for + reproducible hyperopt results. + --min-trades INT Set minimal desired number of trades for evaluations + in the hyperopt optimization path (default: 1). + --continue Continue hyperopt from previous runs. By default, + temporary files will be removed and hyperopt will + start from scratch. + --hyperopt-loss NAME + Specify the class name of the hyperopt loss function + class (IHyperOptLoss). Different functions can + generate completely different results, since the + target for optimization is different. (default: + `DefaultHyperOptLoss`). ``` ## Edge commands @@ -236,8 +269,10 @@ optional arguments: To know your trade expectacny and winrate against historical data, you can use Edge. ``` -usage: freqtrade edge [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] [-r] - [--stoplosses STOPLOSS_RANGE] +usage: freqtrade edge [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] + [--max_open_trades MAX_OPEN_TRADES] + [--stake_amount STAKE_AMOUNT] [-r] + [--stoplosses STOPLOSS_RANGE] optional arguments: -h, --help show this help message and exit @@ -245,10 +280,14 @@ optional arguments: Specify ticker interval (1m, 5m, 30m, 1h, 1d). --timerange TIMERANGE 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 edge with up-to-date data. + run your optimization commands with up-to-date data. --stoplosses STOPLOSS_RANGE Defines a range of stoploss against which edge will assess the strategy the format is "min,max,step" @@ -258,12 +297,7 @@ optional arguments: To understand edge and how to read the results, please read the [edge documentation](edge.md). -## A parameter missing in the configuration? - -All parameters for `main.py`, `backtesting`, `hyperopt` are referenced -in [misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L84) - ## Next step 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). diff --git a/docs/configuration.md b/docs/configuration.md index e7ad9c9bf..f8dbbbbbb 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -14,12 +14,13 @@ Mandatory Parameters are marked as **Required**. | Command | Default | Description | |----------|---------|-------------| | `max_open_trades` | 3 | **Required.** Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades) -| `stake_currency` | BTC | **Required.** Crypto-currency used for trading. -| `stake_amount` | 0.05 | **Required.** Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged. Set it to `"unlimited"` to allow the bot to use all available balance. +| `stake_currency` | BTC | **Required.** Crypto-currency used for trading. [Strategy Override](#parameters-in-the-strategy). +| `stake_amount` | 0.05 | **Required.** Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged. Set it to `"unlimited"` to allow the bot to use all available balance. [Strategy Override](#parameters-in-the-strategy). | `amount_reserve_percent` | 0.05 | Reserve some amount in min pair stake amount. Default is 5%. The bot will reserve `amount_reserve_percent` + stop-loss value when calculating min pair stake amount in order to avoid possible trade refusals. | `ticker_interval` | [1m, 5m, 15m, 30m, 1h, 1d, ...] | The ticker interval to use (1min, 5 min, 15 min, 30 min, 1 hour or 1 day). Default is 5 minutes. [Strategy Override](#parameters-in-the-strategy). | `fiat_display_currency` | USD | **Required.** Fiat currency used to show your profits. More information below. | `dry_run` | true | **Required.** Define if the bot must be in Dry-run or production mode. +| `dry_run_wallet` | 999.9 | Overrides the default amount of 999.9 stake currency units in the wallet used by the bot running in the Dry Run mode if you need it for any reason. | `process_only_new_candles` | false | If set to true indicators are processed only once a new candle arrives. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy). | `minimal_roi` | See below | Set the threshold in percent the bot will use to sell a trade. More information below. [Strategy Override](#parameters-in-the-strategy). | `stoploss` | -0.10 | Value of the stoploss in percent used by the bot. More information below. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). @@ -39,13 +40,12 @@ 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. | `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). -| `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.key` | 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.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.ccxt_rate_limit` | True | DEPRECATED!! Have CCXT handle Exchange rate limits. Depending on the exchange, having this to false can lead to temporary bans from the exchange. +| `exchange.key` | '' | API key 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 pairs to use by the bot for trading and to check for potential trades during backtesting. Can be overriden by dynamic pairlists (see [below](#dynamic-pairlists)). +| `exchange.pair_blacklist` | [] | List of pairs the bot must absolutely avoid for trading and backtesting. Can be overriden by dynamic pairlists (see [below](#dynamic-pairlists)). | `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_async_config` | None | Additional CCXT parameters passed to the async 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.markets_refresh_interval` | 60 | The interval in minutes in which markets are reloaded. @@ -53,7 +53,7 @@ Mandatory Parameters are marked as **Required**. | `experimental.use_sell_signal` | false | Use your sell strategy in addition of the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy). | `experimental.sell_profit_only` | false | Waits until you have made a positive profit before taking a sell decision. [Strategy Override](#parameters-in-the-strategy). | `experimental.ignore_roi_if_buy_signal` | false | Does not sell if the buy-signal is still active. Takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy). -| `pairlist.method` | StaticPairList | Use Static whitelist. [More information below](#dynamic-pairlists). +| `pairlist.method` | StaticPairList | Use static or dynamic volume-based pairlist. [More information below](#dynamic-pairlists). | `pairlist.config` | None | Additional configuration for dynamic pairlists. [More information below](#dynamic-pairlists). | `telegram.enabled` | true | **Required.** Enable or not the usage of Telegram. | `telegram.token` | token | Your Telegram bot token. Only required if `telegram.enabled` is `true`. @@ -67,17 +67,20 @@ Mandatory Parameters are marked as **Required**. | `initial_state` | running | Defines the initial application state. More information below. | `forcebuy_enable` | false | Enables the RPC Commands to force a buy. More information below. | `strategy` | DefaultStrategy | Defines Strategy class to use. -| `strategy_path` | null | Adds an additional strategy lookup path (must be a folder). +| `strategy_path` | null | Adds an additional strategy lookup path (must be a directory). | `internals.process_throttle_secs` | 5 | **Required.** Set the process throttle. Value in second. | `internals.sd_notify` | false | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details. +| `logfile` | | Specify Logfile. Uses a rolling strategy of 10 files, with 1Mb per file. ### Parameters in the strategy The following parameters can be set in either configuration file or strategy. Values set in the configuration file always overwrite values set in the strategy. -* `minimal_roi` +* `stake_currency` +* `stake_amount` * `ticker_interval` +* `minimal_roi` * `stoploss` * `trailing_stop` * `trailing_stop_positive` @@ -128,17 +131,11 @@ If it is not set in either Strategy or Configuration, a default of 1000% `{"0": ### Understand stoploss -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. +Go to the [stoploss documentation](stoploss.md) for more details. ### 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 @@ -188,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. The below is the default which is used if this is not configured in either strategy or configuration file. +Syntax for Strategy: + ```python -"order_types": { +order_types = { "buy": "limit", "sell": "limit", "stoploss": "market", "stoploss_on_exchange": False, "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 @@ -208,7 +219,12 @@ The below is the default which is used if this is not configured in either strat unsure of what you are doing. For more information about how stoploss works please read [the stoploss documentation](stoploss.md). +!!! Note + In case of stoploss on exchange if the stoploss is cancelled manually then + the bot would recreate one. + ### Understand order_time_in_force + The `order_time_in_force` configuration parameter defines the policy by which the order is executed on the exchange. Three commonly used time in force are: @@ -244,9 +260,9 @@ The possible values are: `gtc` (default), `fok` or `ioc`. This is an ongoing work. For now it is supported only for binance and only for buy orders. Please don't change the default value unless you know what you are doing. -### What values for exchange.name? +### Exchange configuration -Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports 115 cryptocurrency +Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports over 100 cryptocurrency exchange markets and trading APIs. The complete up-to-date list can be found in the [CCXT repo homepage](https://github.com/ccxt/ccxt/tree/master/python). However, the bot was tested with only Bittrex and Binance. @@ -258,6 +274,49 @@ The bot was tested with the following exchanges: Feel free to test other exchanges and submit your PR to improve the bot. +#### Sample exchange configuration + +A exchange configuration for "binance" would look as follows: + +```json +"exchange": { + "name": "binance", + "key": "your_exchange_key", + "secret": "your_exchange_secret", + "ccxt_config": {"enableRateLimit": true}, + "ccxt_async_config": { + "enableRateLimit": true, + "rateLimit": 200 + }, +``` + +This configuration enables binance, as well as rate limiting to avoid bans from the exchange. +`"rateLimit": 200` defines a wait-event of 0.2s between each call. This can also be completely disabled by setting `"enableRateLimit"` to false. + +!!! 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. + 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? The `fiat_display_currency` configuration parameter sets the base currency to use for the @@ -321,8 +380,6 @@ section of the configuration. * `StaticPairList` * It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`. * `VolumePairList` - * Formerly available as `--dynamic-whitelist []`. This command line -option is deprecated and should no longer be used. * It selects `number_assets` top pairs based on `sort_key`, which can be one of `askVolume`, `bidVolume` and `quoteVolume`, defaults to `quoteVolume`. * There is a possibility to filter low-value coins that would not allow setting a stop loss diff --git a/docs/data-analysis.md b/docs/data-analysis.md new file mode 100644 index 000000000..ecd94445b --- /dev/null +++ b/docs/data-analysis.md @@ -0,0 +1,114 @@ +# Analyzing bot data + +You can analyze the results of backtests and trading history easily using Jupyter notebooks. A sample notebook is located at `user_data/notebooks/analysis_example.ipynb`. For usage instructions, see [jupyter.org](https://jupyter.org/documentation). + +*Pro tip - Don't forget to start a jupyter notbook server from within your conda or venv environment or use [nb_conda_kernels](https://github.com/Anaconda-Platform/nb_conda_kernels)* + +## Example snippets + +### Load backtest results into a pandas dataframe + +```python +from freqtrade.data.btanalysis import load_backtest_data +# Load backtest results +df = load_backtest_data("user_data/backtest_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. + +### Load live trading results into a pandas dataframe + +``` python +from freqtrade.data.btanalysis import load_trades_from_db + +# Fetch trades from database +df = load_trades_from_db("sqlite:///tradesv3.sqlite") + +# Display results +df.groupby("pair")["sell_reason"].value_counts() +``` + +## Strategy debugging example + +Debugging a strategy can be time-consuming. FreqTrade offers helper functions to visualize raw data. + +### Import requirements and define variables used in analyses + +```python +# Imports +from pathlib import Path +import os +from freqtrade.data.history import load_pair_history +from freqtrade.resolvers import StrategyResolver + +# You can override strategy settings as demonstrated below. +# Customize these according to your needs. + +# Define some constants +ticker_interval = "5m" +# Name of the strategy class +strategy_name = 'AwesomeStrategy' +# Path to user data +user_data_dir = 'user_data' +# Location of the strategy +strategy_location = Path(user_data_dir, 'strategies') +# Location of the data +data_location = Path(user_data_dir, 'data', 'binance') +# Pair to analyze +# Only use one pair here +pair = "BTC_USDT" +``` + +### Load exchange data + +```python +# Load data using values set above +bt_data = load_pair_history(datadir=Path(data_location), + ticker_interval=ticker_interval, + pair=pair) + +# Confirm success +print(f"Loaded {len(bt_data)} rows of data for {pair} from {data_location}") +``` + +### Load and run strategy + +* Rerun each time the strategy file is changed + +```python +# Load strategy using values set above +strategy = StrategyResolver({'strategy': strategy_name, + 'user_data_dir': user_data_dir, + 'strategy_path': strategy_location}).strategy + +# Generate buy/sell signals using strategy +df = strategy.analyze_ticker(bt_data, {'pair': pair}) +``` + +### Display the trade details + +* Note that using `data.head()` would also work, however most indicators have some "startup" data at the top of the dataframe. + +#### Some possible problems + +* Columns with NaN values at the end of the dataframe +* Columns used in `crossed*()` functions with completely different units + +#### Comparison with full backtest + +having 200 buy signals as output for one pair from `analyze_ticker()` does not necessarily mean that 200 trades will be made during backtesting. + +Assuming you use only one condition such as, `df['rsi'] < 30` as buy condition, this will generate multiple "buy" signals for each pair in sequence (until rsi returns > 29). +The bot will only buy on the first of these signals (and also only if a trade-slot ("max_open_trades") is still available), or on one of the middle signals, as soon as a "slot" becomes available. + +```python +# Report results +print(f"Generated {df['buy'].sum()} buy signals") +data = df.set_index('date', drop=True) +data.tail() +``` + +Feel free to submit an issue or Pull Request enhancing this document if you would like to share ideas on how to best analyze the data. diff --git a/docs/deprecated.md b/docs/deprecated.md index 25043d981..2bf655191 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -4,28 +4,16 @@ This page contains description of the command line arguments, configuration para and the bot features that were declared as DEPRECATED by the bot development team and are no longer supported. Please avoid their usage in your configuration. +### the `--live` command line option + +`--live` in the context of backtesting allows to download the latest tick data for backtesting. +Since this only downloads one set of data (by default 500 candles) - this is not really suitable for extendet backtesting, and has therefore been deprecated. + +This command was deprecated in `2019.6-dev` and will be removed after the next release. + +## Removed features + ### The **--dynamic-whitelist** command line option -Per default `--dynamic-whitelist` will retrieve the 20 currencies based -on BaseVolume. This value can be changed when you run the script. - -**By Default** -Get the 20 currencies based on BaseVolume. - -```bash -python3 ./freqtrade/main.py --dynamic-whitelist -``` - -**Customize the number of currencies to retrieve** -Get the 30 currencies based on BaseVolume. - -```bash -python3 ./freqtrade/main.py --dynamic-whitelist 30 -``` - -**Exception** -`--dynamic-whitelist` must be greater than 0. If you enter 0 or a -negative value (e.g -2), `--dynamic-whitelist` will use the default -value (20). - - +This command line option was deprecated in 2018 and removed freqtrade 2019.6-dev (develop branch) +and in freqtrade 2019.7 (master branch). diff --git a/docs/developer.md b/docs/developer.md index 6fbcdc812..28369bb73 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -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. -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 @@ -12,8 +12,8 @@ Special fields for the documentation (like Note boxes, ...) can be found [here]( ## Developer setup -To configure a development environment, use best use the `setup.sh` script and answer "y" when asked "Do you want to install dependencies for dev [y/N]? ". -Alternatively (if your system is not supported by the setup.sh script), follow the manual installation process and run `pip3 install -r requirements-dev.txt`. +To configure a development environment, best use the `setup.sh` script and answer "y" when asked "Do you want to install dependencies for dev [y/N]? ". +Alternatively (if your system is not supported by the setup.sh script), follow the manual installation process and run `pip3 install -e .[all]`. This will install all required tools for development, including `pytest`, `flake8`, `mypy`, and `coveralls`. @@ -81,11 +81,56 @@ 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. 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 This part of the documentation is aimed at maintainers, and shows how to create a release. -### create release branch +### Create release branch ``` bash # make sure you're in develop branch @@ -95,11 +140,14 @@ git checkout develop 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 version matching the current date (for example `2019.7` for July 2019). Minor versions can be `2019.7-1` should we need to do a second release that month. * 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 + +!!! Note + Make sure that both master and develop are up-todate!. ``` bash # Needs to be done before merging / pulling that branch. @@ -108,10 +156,14 @@ git log --oneline --no-decorate --no-merges master..develop ### Create github release / tag +Once the PR against master is merged (best right after merging): + +* Use the button "Draft a new release" in the Github UI (subsection releases) * Use the version-number specified as tag. * 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 -* update version in develop to next valid version and postfix that with `-dev` (`0.18.0 -> 0.18.1-dev`) +* Update version in develop by postfixing that with `-dev` (`2019.6 -> 2019.6-dev`). +* Create a PR against develop to update that branch. diff --git a/docs/docker.md b/docs/docker.md new file mode 100644 index 000000000..615d31796 --- /dev/null +++ b/docs/docker.md @@ -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 directory in your home directory. Feel free to use a different directory and replace the directory 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). diff --git a/docs/edge.md b/docs/edge.md index 7372e3373..9047758f4 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -3,7 +3,7 @@ This page explains how to use Edge Positioning module in your bot in order to enter into a trade only if the trade has a reasonable win rate and risk reward ratio, and consequently adjust your position size and stoploss. !!! Warning - Edge positioning is not compatible with dynamic whitelist. If enabled, it overrides the dynamic whitelist option. + Edge positioning is not compatible with dynamic (volume-based) whitelist. !!! Note Edge does not consider anything else than buy/sell/stoploss signals. So trailing stoploss, ROI, and everything else are ignored in its calculation. @@ -146,16 +146,19 @@ Percentage of allowed risk per trade. (defaults to 0.01 so 1%) #### stoploss_range_min + Minimum stoploss. (defaults to -0.01) #### stoploss_range_max + Maximum stoploss. (defaults to -0.10) #### stoploss_range_step + As an example if this is set to -0.01 then Edge will test the strategy for \[-0.01, -0,02, -0,03 ..., -0.09, -0.10\] ranges. Note than having a smaller step means having a bigger range which could lead to slow calculation. @@ -164,6 +167,7 @@ If you set this parameter to -0.001, you then slow down the Edge calculation by (defaults to -0.01) #### minimum_winrate + It filters out pairs which don't have at least minimum_winrate. This comes handy if you want to be conservative and don't comprise win rate in favour of risk reward ratio. @@ -171,6 +175,7 @@ This comes handy if you want to be conservative and don't comprise win rate in f (defaults to 0.60) #### minimum_expectancy + It filters out pairs which have the expectancy lower than this number. Having an expectancy of 0.20 means if you put 10$ on a trade you expect a 12$ return. @@ -178,6 +183,7 @@ Having an expectancy of 0.20 means if you put 10$ on a trade you expect a 12$ re (defaults to 0.20) #### min_trade_number + When calculating *W*, *R* and *E* (expectancy) against historical data, you always want to have a minimum number of trades. The more this number is the more Edge is reliable. Having a win rate of 100% on a single trade doesn't mean anything at all. But having a win rate of 70% over past 100 trades means clearly something. @@ -185,6 +191,7 @@ Having a win rate of 100% on a single trade doesn't mean anything at all. But ha (defaults to 10, it is highly recommended not to decrease this number) #### max_trade_duration_minute + Edge will filter out trades with long duration. If a trade is profitable after 1 month, it is hard to evaluate the strategy based on it. But if most of trades are profitable and they have maximum duration of 30 minutes, then it is clearly a good sign. **NOTICE:** While configuring this value, you should take into consideration your ticker interval. As an example filtering out trades having duration less than one day for a strategy which has 4h interval does not make sense. Default value is set assuming your strategy interval is relatively small (1m or 5m, etc.). @@ -192,15 +199,17 @@ Edge will filter out trades with long duration. If a trade is profitable after 1 (defaults to 1 day, i.e. to 60 * 24 = 1440 minutes) #### remove_pumps + Edge will remove sudden pumps in a given market while going through historical data. However, given that pumps happen very often in crypto markets, we recommend you keep this off. (defaults to false) - ## Running Edge independently + You can run Edge independently in order to see in details the result. Here is an example: + ```bash -python3 ./freqtrade/main.py edge +freqtrade edge ``` An example of its output: @@ -224,18 +233,21 @@ An example of its output: | NEBL/BTC | -0.03 | 0.63 | 1.29 | 0.58 | 0.44 | 19 | 59 | ### Update cached pairs with the latest data + ```bash -python3 ./freqtrade/main.py edge --refresh-pairs-cached +freqtrade edge --refresh-pairs-cached ``` ### Precising stoploss range + ```bash -python3 ./freqtrade/main.py edge --stoplosses=-0.01,-0.1,-0.001 #min,max,step +freqtrade edge --stoplosses=-0.01,-0.1,-0.001 #min,max,step ``` ### Advanced use of timerange + ```bash -python3 ./freqtrade/main.py edge --timerange=20181110-20181113 +freqtrade edge --timerange=20181110-20181113 ``` Doing `--timerange=-200` will get the last 200 timeframes from your inputdata. You can also specify specific dates, or a range span indexed by start and stop. diff --git a/docs/faq.md b/docs/faq.md index 127f69e9f..a441ffacd 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -1,14 +1,25 @@ # Freqtrade FAQ -### Freqtrade commons +## Freqtrade common issues -#### I have waited 5 minutes, why hasn't the bot made any trades yet?! +### The bot does not start + +Running the bot with `freqtrade --config config.json` does show the output `freqtrade: command not found`. + +This could have the following reasons: + +* The virtual environment is not active + * run `source .env/bin/activate` to activate the virtual environment +* The installation did not work correctly. + * Please check the [Installation documentation](installation.md). + +### I have waited 5 minutes, why hasn't the bot made any trades yet?! Depending on the buy strategy, the amount of whitelisted coins, the situation of the market etc, it can take up to hours to find good entry position for a trade. Be patient! -#### I have made 12 trades already, why is my total profit negative?! +### I have made 12 trades already, why is my total profit negative?! I understand your disappointment but unfortunately 12 trades is just not enough to say anything. If you run backtesting, you can see that our @@ -19,53 +30,64 @@ of course constantly aim to improve the bot but it will _always_ be a gamble, which should leave you with modest wins on monthly basis but you can't say much from few trades. -#### I’d like to change the stake amount. Can I just stop the bot with -/stop and then change the config.json and run it again? +### I’d like to change the stake amount. Can I just stop the bot with /stop and then change the config.json and run it again? Not quite. Trades are persisted to a database but the configuration is currently only read when the bot is killed and restarted. `/stop` more like pauses. You can stop your bot, adjust settings and start it again. -#### I want to improve the bot with a new strategy +### I want to improve the bot with a new strategy That's great. We have a nice backtesting and hyperoptimizing setup. See the tutorial [here|Testing-new-strategies-with-Hyperopt](bot-usage.md#hyperopt-commands). -#### Is there a setting to only SELL the coins being held and not -perform anymore BUYS? +### Is there a setting to only SELL the coins being held and not perform anymore BUYS? You can use the `/forcesell all` command from Telegram. -### Hyperopt module +### I get the message "RESTRICTED_MARKET" + +Currently known to happen for US Bittrex users. +Bittrex split its exchange into US and International versions. +The International version has more pairs available, however the API always returns all pairs, so there is currently no automated way to detect if you're affected by the restriction. + +If you have restricted pairs in your whitelist, you'll get a warning message in the log on FreqTrade startup for each restricted pair. +If you're an "International" Customer on the Bittrex exchange, then this warning will probably not impact you. +If you're a US customer, the bot will fail to create orders for these pairs, and you should remove them from your Whitelist. + +## Hyperopt module + +### How many epoch do I need to get a good Hyperopt result? -#### How many epoch do I need to get a good Hyperopt result? Per default Hyperopts without `-e` or `--epochs` parameter will only -run 100 epochs, means 100 evals of your triggers, guards, .... Too few +run 100 epochs, means 100 evals of your triggers, guards, ... Too few to find a great result (unless if you are very lucky), so you probably have to run it for 10.000 or more. But it will take an eternity to compute. We recommend you to run it at least 10.000 epochs: + ```bash -python3 ./freqtrade/main.py hyperopt -e 10000 +freqtrade hyperopt -e 10000 ``` or if you want intermediate result to see + ```bash -for i in {1..100}; do python3 ./freqtrade/main.py hyperopt -e 100; done +for i in {1..100}; do freqtrade hyperopt -e 100; done ``` -#### Why it is so long to run hyperopt? +### Why it is so long to run hyperopt? + Finding a great Hyperopt results takes time. If you wonder why it takes a while to find great hyperopt results -This answer was written during the under the release 0.15.1, when we had -: +This answer was written during the under the release 0.15.1, when we had: + - 8 triggers - 9 guards: let's say we evaluate even 10 values from each -- 1 stoploss calculation: let's say we want 10 values from that too to -be evaluated +- 1 stoploss calculation: let's say we want 10 values from that too to be evaluated The following calculation is still very rough and not very precise but it will give the idea. With only these triggers and guards there is @@ -73,17 +95,17 @@ already 8\*10^9\*10 evaluations. A roughly total of 80 billion evals. Did you run 100 000 evals? Congrats, you've done roughly 1 / 100 000 th of the search space. -### Edge module +## Edge module -#### Edge implements interesting approach for controlling position size, is there any theory behind it? +### Edge implements interesting approach for controlling position size, is there any theory behind it? The Edge module is mostly a result of brainstorming of [@mishaker](https://github.com/mishaker) and [@creslinux](https://github.com/creslinux) freqtrade team members. You can find further info on expectancy, winrate, risk management and position size in the following sources: -* https://www.tradeciety.com/ultimate-math-guide-for-traders/ -* http://www.vantharp.com/tharp-concepts/expectancy.asp -* https://samuraitradingacademy.com/trading-expectancy/ -* https://www.learningmarkets.com/determining-expectancy-in-your-trading/ -* http://www.lonestocktrader.com/make-money-trading-positive-expectancy/ -* https://www.babypips.com/trading/trade-expectancy-matter +- https://www.tradeciety.com/ultimate-math-guide-for-traders/ +- http://www.vantharp.com/tharp-concepts/expectancy.asp +- https://samuraitradingacademy.com/trading-expectancy/ +- https://www.learningmarkets.com/determining-expectancy-in-your-trading/ +- http://www.lonestocktrader.com/make-money-trading-positive-expectancy/ +- https://www.babypips.com/trading/trade-expectancy-matter diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 6e52be47f..6ef68d82f 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -12,29 +12,34 @@ and still take a long time. ## Prepare Hyperopting 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. ### Checklist on all tasks / possibilities in hyperopt -Depending on the space you want to optimize, only some of the below are required. +Depending on the space you want to optimize, only some of the below are required: * fill `populate_indicators` - probably a copy from your strategy * fill `buy_strategy_generator` - for buy signal optimization * fill `indicator_space` - for buy signal optimzation * fill `sell_strategy_generator` - for sell signal optimization * fill `sell_indicator_space` - for sell signal optimzation -* fill `roi_space` - for ROI optimization -* fill `generate_roi_table` - for ROI optimization (if you need more than 3 entries) -* fill `stoploss_space` - stoploss optimization -* Optional but recommended - * copy `populate_buy_trend` from your strategy - otherwise default-strategy will be used - * copy `populate_sell_trend` from your strategy - otherwise default-strategy will be used + +Optional, but recommended: + +* copy `populate_buy_trend` from your strategy - otherwise default-strategy will be used +* copy `populate_sell_trend` from your strategy - otherwise default-strategy will be used + +Rarely you may also need to override: + +* `roi_space` - for custom ROI optimization (if you need the ranges for the ROI parameters in the optimization hyperspace that differ from default) +* `generate_roi_table` - for custom ROI optimization (if you need more than 4 entries in the ROI table) +* `stoploss_space` - for custom stoploss optimization (if you need the range for the stoploss parameter in the optimization hyperspace that differs from default) ### 1. Install a Custom Hyperopt File -Put your hyperopt file into the folder`user_data/hyperopts`. +Put your hyperopt file into the directory `user_data/hyperopts`. Let assume you want a hyperopt file `awesome_hyperopt.py`: Copy the file `user_data/hyperopts/sample_hyperopt.py` into `user_data/hyperopts/awesome_hyperopt.py` @@ -62,7 +67,7 @@ If you have updated the buy strategy, ie. changed the contents of #### Sell optimization -Similar to the buy-signal above, sell-signals can also be optimized. +Similar to the buy-signal above, sell-signals can also be optimized. Place the corresponding settings into the following methods * Inside `sell_indicator_space()` - the parameters hyperopt shall be optimizing. @@ -71,6 +76,11 @@ Place the corresponding settings into the following methods 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-`. +#### 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 Let's say you are curious: should you use MACD crossings or lower Bollinger @@ -122,9 +132,10 @@ So let's write the buy strategy using these values: dataframe['macd'], dataframe['macdsignal'] )) - dataframe.loc[ - reduce(lambda x, y: x & y, conditions), - 'buy'] = 1 + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'buy'] = 1 return dataframe @@ -138,21 +149,90 @@ it will end with telling you which paramter combination produced the best profit The search for best parameters starts with a few random combinations and then uses a regressor algorithm (currently ExtraTreesRegressor) to quickly find a parameter combination -that minimizes the value of the objective function `calculate_loss` in `hyperopt.py`. +that minimizes the value of the [loss function](#loss-functions). The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators. When you want to test an indicator that isn't used by the bot currently, remember to add it to the `populate_indicators()` method in `hyperopt.py`. +## Loss-functions + +Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results. + +By default, FreqTrade uses a loss function, which has been with freqtrade since the beginning and optimizes mostly for short trade duration and avoiding losses. + +A different loss function can be specified by using the `--hyperopt-loss ` argument. +This class should be in its own file within the `user_data/hyperopts/` directory. + +Currently, the following loss functions are builtin: `DefaultHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function), `SharpeHyperOptLoss` (optimizes Sharpe Ratio calculated on the trade returns) and `OnlyProfitHyperOptLoss` (which takes only amount of profit into consideration). + +### Creating and using a custom loss function + +To use a custom loss function class, make sure that the function `hyperopt_loss_function` is defined in your custom hyperopt loss class. +For the sample below, you then need to add the command line parameter `--hyperopt-loss SuperDuperHyperOptLoss` to your hyperopt call so this fuction is being used. + +A sample of this can be found below, which is identical to the Default Hyperopt loss implementation. A full sample can be found [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_loss.py) + +``` python +from freqtrade.optimize.hyperopt import IHyperOptLoss + +TARGET_TRADES = 600 +EXPECTED_MAX_PROFIT = 3.0 +MAX_ACCEPTED_TRADE_DURATION = 300 + +class SuperDuperHyperOptLoss(IHyperOptLoss): + """ + Defines the default loss function for hyperopt + """ + + @staticmethod + def hyperopt_loss_function(results: DataFrame, trade_count: int, + min_date: datetime, max_date: datetime, + *args, **kwargs) -> float: + """ + Objective function, returns smaller number for better results + This is the legacy algorithm (used until now in freqtrade). + Weights are distributed as follows: + * 0.4 to trade duration + * 0.25: Avoiding trade loss + * 1.0 to total profit, compared to the expected value (`EXPECTED_MAX_PROFIT`) defined above + """ + total_profit = results.profit_percent.sum() + trade_duration = results.trade_duration.mean() + + trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8) + profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT) + duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1) + result = trade_loss + profit_loss + duration_loss + return result +``` + +Currently, the arguments are: + +* `results`: DataFrame containing the result + The following columns are available in results (corresponds to the output-file of backtesting when used with `--export trades`): + `pair, profit_percent, profit_abs, open_time, close_time, open_index, close_index, trade_duration, open_at_end, open_rate, close_rate, sell_reason` +* `trade_count`: Amount of trades (identical to `len(results)`) +* `min_date`: Start date of the hyperopting TimeFrame +* `min_date`: End date of the hyperopting TimeFrame + +This function needs to return a floating point number (`float`). Smaller numbers will be interpreted as better results. The parameters and balancing for this is up to you. + +!!! Note + This function is called once per iteration - so please make sure to have this as optimized as possible to not slow hyperopt down unnecessarily. + +!!! Note + Please keep the arguments `*args` and `**kwargs` in the interface to allow us to extend this interface later. + ## Execute Hyperopt Once you have updated your hyperopt configuration you can run it. -Because hyperopt tries a lot of combinations to find the best parameters it will take time you will have the result (more than 30 mins). +Because hyperopt tries a lot of combinations to find the best parameters it will take time to get a good result. More time usually results in better results. We strongly recommend to use `screen` or `tmux` to prevent any connection loss. ```bash -python3 ./freqtrade/main.py -c config.json hyperopt --customhyperopt -e 5000 --spaces all +freqtrade -c config.json hyperopt --customhyperopt -e 5000 --spaces all ``` Use `` as the name of the custom hyperopt used. @@ -162,8 +242,11 @@ running at least several thousand evaluations. The `--spaces all` flag determines that all possible parameters should be optimized. Possibilities are listed below. +!!! Note + By default, hyperopt will erase previous results and start from scratch. Continuation can be archived by using `--continue`. + !!! Warning -When switching parameters or changing configuration options, the file `user_data/hyperopt_results.pickle` should be removed. It's used to be able to continue interrupted calculations, but does not detect changes to settings or the hyperopt file. + When switching parameters or changing configuration options, make sure to not use the argument `--continue` so temporary results can be removed. ### Execute Hyperopt with Different Ticker-Data Source @@ -173,12 +256,11 @@ use data from directory `user_data/data`. ### Running Hyperopt with Smaller Testset -Use the `--timerange` argument to change how much of the testset -you want to use. The last N ticks/timeframes will be used. -Example: +Use the `--timerange` argument to change how much of the testset you want to use. +For example, to use one month of data, pass the following parameter to the hyperopt call: ```bash -python3 ./freqtrade/main.py hyperopt --timerange -200 +freqtrade hyperopt --timerange 20180401-20180501 ``` ### Running Hyperopt with Smaller Search Space @@ -191,12 +273,33 @@ new buy strategy you have. Legal values are: -- `all`: optimize everything -- `buy`: just search for a new buy strategy -- `sell`: just search for a new sell strategy -- `roi`: just optimize the minimal profit table for your strategy -- `stoploss`: search for the best stoploss value -- space-separated list of any of the above values for example `--spaces roi stoploss` +* `all`: optimize everything +* `buy`: just search for a new buy strategy +* `sell`: just search for a new sell strategy +* `roi`: just optimize the minimal profit table for your strategy +* `stoploss`: search for the best stoploss value +* space-separated list of any of the above values for example `--spaces roi stoploss` + +### Position stacking and disabling max market positions + +In some situations, you may need to run Hyperopt (and Backtesting) with the +`--eps`/`--enable-position-staking` and `--dmmp`/`--disable-max-market-positions` arguments. + +By default, hyperopt emulates the behavior of the Freqtrade Live Run/Dry Run, where only one +open trade is allowed for every traded pair. The total number of trades open for all pairs +is also limited by the `max_open_trades` setting. During Hyperopt/Backtesting this may lead to +some potential trades to be hidden (or masked) by previosly open trades. + +The `--eps`/`--enable-position-stacking` argument allows emulation of buying the same pair multiple times, +while `--dmmp`/`--disable-max-market-positions` disables applying `max_open_trades` +during Hyperopt/Backtesting (which is equal to setting `max_open_trades` to a very high +number). + +!!! Note + Dry/live runs will **NOT** use position stacking - therefore it does make sense to also validate the strategy without this as it's closer to reality. + +You can also enable position stacking in the configuration file by explicitly setting +`"position_stacking"=true`. ## Understand the Hyperopt Result @@ -205,8 +308,10 @@ Given the following result from hyperopt: ``` Best result: - 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. -with values: + + 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 + +Buy hyperspace params: { 'adx-value': 44, 'rsi-value': 29, 'adx-enabled': False, @@ -225,7 +330,7 @@ method, what those values match to. So for example you had `rsi-value: 29.0` so we would look at `rsi`-block, that translates to the following code block: -``` +``` python (dataframe['rsi'] < 29.0) ``` @@ -245,25 +350,19 @@ def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: ### Understand Hyperopt ROI results -If you are optimizing ROI, you're result will look as follows and include a ROI table. +If you are optimizing ROI (i.e. if optimization search-space contains 'all' or 'roi'), your result will look as follows and include a ROI table: ``` Best result: - 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. -with values: + + 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 + +Buy hyperspace params: { 'adx-value': 44, 'rsi-value': 29, - 'adx-enabled': false, + 'adx-enabled': False, 'rsi-enabled': True, - 'trigger': 'bb_lower', - 'roi_t1': 40, - 'roi_t2': 57, - 'roi_t3': 21, - 'roi_p1': 0.03634636907306948, - 'roi_p2': 0.055237357937802885, - 'roi_p3': 0.015163796015548354, - 'stoploss': -0.37996664668703606 -} + 'trigger': 'bb_lower'} ROI table: { 0: 0.10674752302642071, 21: 0.09158372701087236, @@ -274,27 +373,54 @@ ROI table: This would translate to the following ROI table: ``` python - minimal_roi = { +minimal_roi = { "118": 0, - "78": 0.0363463, + "78": 0.0363, "21": 0.0915, "0": 0.106 } ``` -### Validate backtest result +If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps) with the values that can vary in the following ranges: + +| # | minutes | ROI percentage | +|---|---|---| +| 1 | always 0 | 0.03...0.31 | +| 2 | 10...40 | 0.02...0.11 | +| 3 | 20...100 | 0.01...0.04 | +| 4 | 30...220 | always 0 | + +This structure of the ROI table is sufficient in most cases. Override the `roi_space()` method defining the ranges desired if you need components of the ROI tables to vary in other ranges. + +Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization in these methods if you need a different structure of the ROI table or other amount of rows (steps) in the ROI tables. + +### Understand Hyperopt Stoploss results + +If you are optimizing stoploss values (i.e. if optimization search-space contains 'all' or 'stoploss'), your result will look as follows and include stoploss: + +``` +Best result: + + 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 + +Buy hyperspace params: +{ 'adx-value': 44, + 'rsi-value': 29, + 'adx-enabled': False, + 'rsi-enabled': True, + 'trigger': 'bb_lower'} +Stoploss: -0.37996664668703606 +``` + +If you are optimizing stoploss values, Freqtrade creates the 'stoploss' optimization hyperspace for you. By default, the stoploss values in that hyperspace can vary in the range -0.5...-0.02, which is sufficient in most cases. + +Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. + +### Validate backtesting results Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected. -To archive the same results (number of trades, ...) than during hyperopt, please use the command line flags `--disable-max-market-positions` and `--enable-position-stacking` for backtesting. -This configuration is the default in hyperopt for performance reasons. - -You can overwrite position stacking in the configuration by explicitly setting `"position_stacking"=false` or by changing the relevant line in your hyperopt file [here](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L191). - -Enabling the market-position for hyperopt is currently not possible. - -!!! Note: -Dry/live runs will **NOT** use position stacking - therefore it does make sense to also validate the strategy without this as it's closer to reality. +To achieve same results (number of trades, their durations, profit, etc.) than during Hyperopt, please use same set of arguments `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting. ## Next Step diff --git a/docs/index.md b/docs/index.md index 9abc71747..63d6be75e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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. - ## Features + - Based on Python 3.6+: For botting on any operating system — Windows, macOS and Linux. - Persistence: Persistence is achieved through sqlite database. - 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. - 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. - - 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. - Daily summary of profit/loss: Receive the daily summary of your profit/loss. - Performance status report: Receive the performance status of your current trades. - ## Requirements + ### 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. ### Hardware requirements + To run this bot we recommend you a cloud instance with a minimum of: - 2GB RAM @@ -49,6 +51,7 @@ To run this bot we recommend you a cloud instance with a minimum of: - 2vCPU ### Software requirements + - Python 3.6.x - pip (pip3) - git @@ -56,12 +59,13 @@ To run this bot we recommend you a cloud instance with a minimum of: - virtualenv (Recommended) - Docker (Recommended) - ## Support + 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. -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? + Begin by reading our installation guide [here](installation). diff --git a/docs/installation.md b/docs/installation.md index bd6c50c5a..589d3fe7f 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,72 +1,49 @@ # Installation + This page explains how to prepare your environment for running the bot. ## Prerequisite -Before running your bot in production you will need to setup few -external API. In production mode, the bot required valid Bittrex API -credentials and a Telegram bot (optional but recommended). -- [Setup your exchange account](#setup-your-exchange-account) -- [Backtesting commands](#setup-your-telegram-bot) +### Requirements + +Click each one for install guide: + +* [Python >= 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/) +* [pip](https://pip.pypa.io/en/stable/installing/) +* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +* [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended) +* [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html) (install instructions below) + +### API keys + +Before running your bot in production you will need to setup few +external API. In production mode, the bot will require valid Exchange API +credentials. We also recommend a [Telegram bot](telegram-usage.md#setup-your-telegram-bot) (optional but recommended). ### Setup your exchange account -*To be completed, please feel free to complete this section.* -### Setup your Telegram bot -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. +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. -### 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)**
-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`.** -
## Quick start + Freqtrade provides a Linux/MacOS script to install all dependencies and help you to configure the bot. +!!! Note + Python3.6 or higher and the corresponding pip are assumed to be available. The install-script will warn and stop if that's not the case. + ```bash git clone git@github.com:freqtrade/freqtrade.git cd freqtrade git checkout develop ./setup.sh --install ``` + !!! Note Windows installation is explained [here](#windows). -
+ ## 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 freqtrade provides a script to Install, Update, Configure, and Reset your bot. ```bash $ ./setup.sh @@ -81,7 +58,7 @@ usage: This script will install everything you need to run the bot: -* Mandatory software as: `Python3`, `ta-lib`, `wget` +* Mandatory software as: `ta-lib` * Setup your virtualenv * Configure your `config.json` file @@ -101,212 +78,21 @@ 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 We've included/collected install instructions for Ubuntu 16.04, MacOS, and Windows. These are guidelines and your success may vary with other distros. OS Specific steps are listed first, the [Common](#common) section below is necessary for all systems. -### Requirements - -Click each one for install guide: - -* [Python >= 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/) -* [pip](https://pip.pypa.io/en/stable/installing/) -* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) -* [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended) -* [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html) +!!! Note + Python3.6 or higher and the corresponding pip are assumed to be available. ### Linux - Ubuntu 16.04 -#### Install Python 3.6, Git, and wget +#### Install necessary dependencies ```bash -sudo add-apt-repository ppa:jonathonf/python-3.6 sudo apt-get update -sudo apt-get install python3.6 python3.6-venv python3.6-dev build-essential autoconf libtool pkg-config make wget git +sudo apt-get install build-essential git ``` #### Raspberry Pi / Raspbian @@ -315,7 +101,6 @@ Before installing FreqTrade on a Raspberry Pi running the official Raspbian Imag The following assumes that miniconda3 is installed and available in your environment. Last miniconda3 installation file use python 3.4, we will update to python 3.6 on this installation. It's recommended to use (mini)conda for this as installation/compilation of `numpy`, `scipy` and `pandas` takes a long time. -If you have installed it from (mini)conda, you can remove `numpy`, `scipy`, and `pandas` from `requirements.txt` before you install it with `pip`. Additional package to install on your Raspbian, `libffi-dev` required by cryptography (from python-telegram-bot). @@ -327,18 +112,10 @@ conda activate freqtrade conda install scipy pandas numpy sudo apt install libffi-dev -python3 -m pip install -r requirements.txt +python3 -m pip install -r requirements-common.txt python3 -m pip install -e . ``` -### MacOS - -#### Install Python 3.6, git and wget - -```bash -brew install python3 git wget -``` - ### Common #### 1. Install TA-Lib @@ -379,7 +156,7 @@ git clone https://github.com/freqtrade/freqtrade.git ``` -Optionally checkout the stable/master branch: +Optionally checkout the master branch to get the latest stable release: ```bash git checkout master @@ -397,9 +174,9 @@ cp config.json.example config.json #### 5. Install python dependencies ``` bash -pip3 install --upgrade pip -pip3 install -r requirements.txt -pip3 install -e . +python3 -m pip install --upgrade pip +python3 -m pip install -r requirements.txt +python3 -m pip install -e . ``` #### 6. Run the Bot @@ -407,10 +184,10 @@ pip3 install -e . If this is the first time you run the bot, ensure you are running it in Dry-run `"dry_run": true,` otherwise it will start to buy and sell coins. ```bash -python3.6 ./freqtrade/main.py -c config.json +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 @@ -437,15 +214,25 @@ when it changes. The `freqtrade.service.watchdog` file contains an example of the service unit configuration file which uses systemd as the watchdog. -!!! Note: -The sd_notify communication between the bot and the systemd service manager will not work if the bot runs in a -Docker container. +!!! Note + The sd_notify communication between the bot and the systemd service manager will not work if the bot runs in a Docker container. ------ +## Using Conda + +Freqtrade can also be installed using Anaconda (or Miniconda). + +``` bash +conda env create -f environment.yml +``` + +!!! Note: + This requires the [ta-lib](#1-install-ta-lib) C-library to be installed first. + ## Windows -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 available on your system, feel free to try the instructions below, which led to success for some. @@ -458,8 +245,6 @@ If that is not available on your system, feel free to try the instructions below git clone https://github.com/freqtrade/freqtrade.git ``` -copy paste `config.json` to ``\path\freqtrade-develop\freqtrade` - #### Install ta-lib Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows). @@ -489,7 +274,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. -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. --- diff --git a/docs/plotting.md b/docs/plotting.md index a9b191e75..b8e041d61 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -1,63 +1,83 @@ # Plotting -This page explains how to plot prices, indicator, profits. + +This page explains how to plot prices, indicators and profits. ## Installation 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 + Usage for the price plotter: -``` -script/plot_dataframe.py [-h] [-p pairs] [--live] +``` bash +python3 script/plot_dataframe.py [-h] [-p pairs] [--live] ``` 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 -pairs you would like to plot. +The `-p` pairs argument can be used to specify 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: -``` -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: -``` -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): + +``` 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. 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. -The results may then be plotted with the -s argument: +To plot trades from a backtesting result, use `--export-filename ` + +``` 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// + +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// ``` ## 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 2) The summarized profit made by backtesting. 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 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, or one that acts less seldom, but makes big swings. @@ -76,13 +96,14 @@ that makes profit spikes. Usage for the profit plotter: -``` -script/plot_profit.py [-h] [-p pair] [--datadir directory] [--ticker_interval num] +``` bash +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 Example -``` -python3 scripts/plot_profit.py --datadir ../freqtrade/freqtrade/tests/testdata-20171221/ -p BTC_LTC + +``` bash +python3 scripts/plot_profit.py --datadir ../freqtrade/freqtrade/tests/testdata-20171221/ -p LTC/BTC ``` diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index c4d8c2cae..ce76d52e5 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1 +1 @@ -mkdocs-material==3.1.0 \ No newline at end of file +mkdocs-material==4.4.0 \ No newline at end of file diff --git a/docs/rest-api.md b/docs/rest-api.md new file mode 100644 index 000000000..afecc1d80 --- /dev/null +++ b/docs/rest-api.md @@ -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 [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 [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 ` | | Instantly sells the given trade (Ignoring `minimum_roi`). +| `forcesell all` | | Instantly sells all open trades (Ignoring `minimum_roi`). +| `forcebuy [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 ` | 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 +``` diff --git a/docs/sql_cheatsheet.md b/docs/sql_cheatsheet.md index e85aceec8..f41520bd9 100644 --- a/docs/sql_cheatsheet.md +++ b/docs/sql_cheatsheet.md @@ -1,5 +1,5 @@ # SQL Helper -This page constains some help if you want to edit your sqlite db. +This page contains some help if you want to edit your sqlite db. ## Install sqlite3 **Ubuntu/Debian installation** @@ -65,12 +65,12 @@ SELECT * FROM trades; ## Fix trade still open after a manual sell on the exchange -!!! Warning: - Manually selling on the exchange should not be done by default, since the bot does not detect this and will try to sell anyway. - /foresell should accomplish the same thing. +!!! Warning + Manually selling a pair on the exchange will not be detected by the bot and it will try to sell anyway. Whenever possible, forcesell should be used to accomplish the same thing. + It is strongly advised to backup your database file before making any manual changes. -!!! Note: - This should not be necessary after /forcesell, as forcesell orders are closed automatically by the bot on the next iteration. +!!! Note + This should not be necessary after /forcesell, as forcesell orders are closed automatically by the bot on the next iteration. ```sql UPDATE trades diff --git a/docs/stoploss.md b/docs/stoploss.md index cbe4fd3c4..f5e2f8df6 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -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: @@ -16,13 +25,12 @@ In case of stoploss on exchange there is another parameter called `stoploss_on_e !!! Note Stoploss on exchange is only supported for Binance as of now. - ## 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 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. 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. 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). diff --git a/docs/bot-optimization.md b/docs/strategy-customization.md similarity index 81% rename from docs/bot-optimization.md rename to docs/strategy-customization.md index 8592f6cca..15f44955b 100644 --- a/docs/bot-optimization.md +++ b/docs/strategy-customization.md @@ -5,8 +5,7 @@ indicators. ## Install a custom strategy file -This is very simple. Copy paste your strategy file into the folder -`user_data/strategies`. +This is very simple. Copy paste your strategy file into the directory `user_data/strategies`. Let assume you have a class called `AwesomeStrategy` in the file `awesome-strategy.py`: @@ -14,7 +13,7 @@ Let assume you have a class called `AwesomeStrategy` in the file `awesome-strate 2. Start the bot with the param `--strategy AwesomeStrategy` (the parameter is the class name) ```bash -python3 ./freqtrade/main.py --strategy AwesomeStrategy +freqtrade --strategy AwesomeStrategy ``` ## Change your strategy @@ -22,7 +21,7 @@ python3 ./freqtrade/main.py --strategy AwesomeStrategy The bot includes a default strategy file. However, we recommend you to use your own file to not have to lose your parameters every time the default strategy file will be updated on Github. Put your custom strategy file -into the folder `user_data/strategies`. +into the directory `user_data/strategies`. Best copy the test-strategy and modify this copy to avoid having bot-updates override your changes. `cp user_data/strategies/test_strategy.py user_data/strategies/awesome-strategy.py` @@ -41,18 +40,24 @@ The bot also include a sample strategy called `TestStrategy` you can update: `us You can test it with the parameter: `--strategy TestStrategy` ```bash -python3 ./freqtrade/main.py --strategy AwesomeStrategy +freqtrade --strategy AwesomeStrategy ``` **For the following section we will use the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py) file as reference.** -!!! Note: Strategies and Backtesting +!!! Note Strategies and Backtesting To avoid problems and unexpected differences between Backtesting and dry/live modes, please be aware that during backtesting the full time-interval is passed to the `populate_*()` methods at once. 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. +!!! 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 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 +217,12 @@ stoploss = -0.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). -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 @@ -250,22 +258,19 @@ class Awesomestrategy(IStrategy): self.cust_info[metadata["pair"]["crosstime"] = 1 ``` -!!! Warning: +!!! Warning The data is not persisted after a bot-restart (or config-reload). Also, the amount of data should be kept smallish (no DataFrames and such), otherwise the bot will start to consume a lot of memory and eventually run out of memory and crash. -!!! Note: +!!! Note If the data is pair-specific, make sure to use pair as one of the keys in the dictionary. ### Additional data (DataProvider) The strategy provides access to the `DataProvider`. This allows you to get additional data to use in your strategy. -!!!Note: - The DataProvier is currently not available during backtesting / hyperopt, but this is planned for the future. - All methods return `None` in case of failure (do not raise an exception). -Please always check if the `DataProvider` is available to avoid failures during backtesting. +Please always check the mode of operation to select the correct method to get data (samples see below). #### Possible options for DataProvider @@ -278,20 +283,35 @@ Please always check if the `DataProvider` is available to avoid failures during ``` python if self.dp: - if dp.runmode == 'live': - if ('ETH/BTC', ticker_interval) in self.dp.available_pairs: - data_eth = self.dp.ohlcv(pair='ETH/BTC', - ticker_interval=ticker_interval) + if self.dp.runmode in ('live', 'dry_run'): + if (f'{self.stake_currency}/BTC', self.ticker_interval) in self.dp.available_pairs: + data_eth = self.dp.ohlcv(pair='{self.stake_currency}/BTC', + ticker_interval=self.ticker_interval) else: # Get historic ohlcv data (cached on disk). - history_eth = self.dp.historic_ohlcv(pair='ETH/BTC', + history_eth = self.dp.historic_ohlcv(pair='{self.stake_currency}/BTC', ticker_interval='1h') ``` -!!! Warning: Warning about backtesting +!!! Warning Warning about backtesting Be carefull when using dataprovider in backtesting. `historic_ohlcv()` provides the full time-range in one go, so please be aware of it and make sure to not "look into the future" to avoid surprises when running in dry/live mode). +!!! Warning Warning in hyperopt + 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 ``` python @@ -300,6 +320,7 @@ if self.dp: print(f"available {pair}, {ticker}") ``` + #### Get data for non-tradeable pairs Data for additional, informative pairs (reference pairs) can be beneficial for some strategies. @@ -317,7 +338,7 @@ def informative_pairs(self): ] ``` -!!! Warning: +!!! Warning As these pairs will be refreshed as part of the regular whitelist refresh, it's best to keep this list short. All intervals and all pairs can be specified as long as they are available (and active) on the used exchange. It is however better to use resampling to longer time-intervals when possible @@ -327,7 +348,7 @@ def informative_pairs(self): The strategy provides access to the `Wallets` object. This contains the current balances on the exchange. -!!!NOTE: +!!! Note Wallets is not available during backtesting / hyperopt. Please always check if `Wallets` is available to avoid failures during backtesting. @@ -345,6 +366,30 @@ if self.wallets: - `get_used(asset)` - currently tied up balance (open orders) - `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? The default buy strategy is located in the file @@ -352,10 +397,10 @@ The default buy strategy is located in the file ### Specify custom strategy location -If you want to use a strategy from a different folder you can pass `--strategy-path` +If you want to use a strategy from a different directory you can pass `--strategy-path` ```bash -python3 ./freqtrade/main.py --strategy AwesomeStrategy --strategy-path /some/folder +freqtrade --strategy AwesomeStrategy --strategy-path /some/directory ``` ### Further strategy ideas @@ -364,7 +409,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. 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 diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index face22404..e06d4fdfc 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -1,13 +1,48 @@ # Telegram usage -This page explains how to command your bot with Telegram. +## Setup your Telegram bot -## Prerequisite -To control your bot with Telegram, you need first to -[set up a Telegram bot](installation.md) -and add your Telegram API keys into your config file. +Below we explain how to create your Telegram Bot, and how to get your +Telegram user id. + +### 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 + Per default, the Telegram bot shows predefined commands. Some commands are only available by sending them to the bot. The table below list the official commands. You can ask at any moment for help with `/help`. @@ -28,6 +63,9 @@ official commands. You can ask at any moment for help with `/help`. | `/performance` | | Show performance of each finished trade grouped by pair | `/balance` | | Show account balance per currency | `/daily ` | 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. | `/help` | | Show help message | `/version` | | Show version @@ -55,23 +93,21 @@ Once all positions are sold, run `/stop` to completely stop the bot. `/reload_conf` resets "max_open_trades" to the value set in the configuration and resets this command. -!!! warning: -The stop-buy signal is ONLY active while the bot is running, and is not persisted anyway, so restarting the bot will cause this to reset. +!!! warning + The stop-buy signal is ONLY active while the bot is running, and is not persisted anyway, so restarting the bot will cause this to reset. ### /status For each open trade, the bot will send you the following message. -> **Trade ID:** `123` -> **Current Pair:** CVC/BTC -> **Open Since:** `1 days ago` -> **Amount:** `26.64180098` -> **Open Rate:** `0.00007489` -> **Close Rate:** `None` -> **Current Rate:** `0.00007489` -> **Close Profit:** `None` -> **Current Profit:** `12.95%` -> **Open Order:** `None` +> **Trade ID:** `123` `(since 1 days ago)` +> **Current Pair:** CVC/BTC +> **Open Since:** `1 days ago` +> **Amount:** `26.64180098` +> **Open Rate:** `0.00007489` +> **Current Rate:** `0.00007489` +> **Current Profit:** `12.95%` +> **Stoploss:** `0.00007389 (-0.02%)` ### /status table @@ -96,18 +132,18 @@ current max Return a summary of your profit/loss and performance. -> **ROI:** Close trades -> ∙ `0.00485701 BTC (258.45%)` -> ∙ `62.968 USD` -> **ROI:** All trades -> ∙ `0.00255280 BTC (143.43%)` -> ∙ `33.095 EUR` -> -> **Total Trade Count:** `138` -> **First Trade opened:** `3 days ago` -> **Latest Trade opened:** `2 minutes ago` -> **Avg. Duration:** `2:33:45` -> **Best Performing:** `PAY/BTC: 50.23%` +> **ROI:** Close trades +> ∙ `0.00485701 BTC (258.45%)` +> ∙ `62.968 USD` +> **ROI:** All trades +> ∙ `0.00255280 BTC (143.43%)` +> ∙ `33.095 EUR` +> +> **Total Trade Count:** `138` +> **First Trade opened:** `3 days ago` +> **Latest Trade opened:** `2 minutes ago` +> **Avg. Duration:** `2:33:45` +> **Best Performing:** `PAY/BTC: 50.23%` ### /forcesell @@ -115,7 +151,7 @@ Return a summary of your profit/loss and performance. ### /forcebuy -> **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. @@ -125,26 +161,26 @@ Note that for this to work, `forcebuy_enable` needs to be set to true. Return the performance of each crypto-currency the bot has sold. > Performance: -> 1. `RCN/BTC 57.77%` -> 2. `PAY/BTC 56.91%` -> 3. `VIB/BTC 47.07%` -> 4. `SALT/BTC 30.24%` -> 5. `STORJ/BTC 27.24%` -> ... +> 1. `RCN/BTC 57.77%` +> 2. `PAY/BTC 56.91%` +> 3. `VIB/BTC 47.07%` +> 4. `SALT/BTC 30.24%` +> 5. `STORJ/BTC 27.24%` +> ... ### /balance Return the balance of all crypto-currency your have on the exchange. -> **Currency:** BTC -> **Available:** 3.05890234 -> **Balance:** 3.05890234 -> **Pending:** 0.0 +> **Currency:** BTC +> **Available:** 3.05890234 +> **Balance:** 3.05890234 +> **Pending:** 0.0 -> **Currency:** CVC -> **Available:** 86.64180098 -> **Balance:** 86.64180098 -> **Pending:** 0.0 +> **Currency:** CVC +> **Available:** 86.64180098 +> **Balance:** 86.64180098 +> **Pending:** 0.0 ### /daily @@ -160,6 +196,38 @@ Day Profit BTC Profit USD 2018-01-01 0.00269130 BTC 34.986 USD ``` +### /whitelist + +Shows the current whitelist + +> Using whitelist `StaticPairList` with 22 pairs +> `IOTA/BTC, NEO/BTC, TRX/BTC, VET/BTC, ADA/BTC, ETC/BTC, NCASH/BTC, DASH/BTC, XRP/BTC, XVG/BTC, EOS/BTC, LTC/BTC, OMG/BTC, BTG/BTC, LSK/BTC, ZEC/BTC, HOT/BTC, IOTX/BTC, XMR/BTC, AST/BTC, XLM/BTC, NANO/BTC` + +### /blacklist [pair] + +Shows the current blacklist. +If Pair is set, then this pair will be added to the pairlist. +Also supports multiple pairs, seperated by a space. +Use `/reload_conf` to reset the blacklist. + +> Using blacklist `StaticPairList` with 2 pairs +>`DODGE/BTC`, `HOT/BTC`. + +### /edge + +Shows pairs validated by Edge along with their corresponding winrate, expectancy and stoploss values. + +> **Edge only validated following pairs:** +``` +Pair Winrate Expectancy Stoploss +-------- --------- ------------ ---------- +DOCK/ETH 0.522727 0.881821 -0.03 +PHX/ETH 0.677419 0.560488 -0.03 +HOT/ETH 0.733333 0.490492 -0.03 +HC/ETH 0.588235 0.280988 -0.02 +ARDR/ETH 0.366667 0.143059 -0.01 +``` + ### /version > **Version:** `0.14.3` diff --git a/docs/webhook-config.md b/docs/webhook-config.md index 2b5365e32..112f8a77e 100644 --- a/docs/webhook-config.md +++ b/docs/webhook-config.md @@ -1,7 +1,5 @@ # Webhook usage -This page explains how to configure your bot to talk to webhooks. - ## Configuration Enable webhooks by adding a webhook-section to your configuration file, and setting `webhook.enabled` to `true`. @@ -39,32 +37,32 @@ Different payloads can be configured for different events. Not all fields are ne The fields in `webhook.webhookbuy` are filled when the bot executes a buy. Parameters are filled using string.format. Possible parameters are: -* exchange -* pair -* limit -* stake_amount -* stake_amount_fiat -* stake_currency -* fiat_currency +* `exchange` +* `pair` +* `limit` +* `stake_amount` +* `stake_currency` +* `fiat_currency` +* `order_type` ### Webhooksell The fields in `webhook.webhooksell` are filled when the bot sells a trade. Parameters are filled using string.format. Possible parameters are: -* exchange -* pair -* gain -* limit -* amount -* open_rate -* current_rate -* profit_amount -* profit_percent -* profit_fiat -* stake_currency -* fiat_currency -* sell_reason +* `exchange` +* `pair` +* `gain` +* `limit` +* `amount` +* `open_rate` +* `current_rate` +* `profit_amount` +* `profit_percent` +* `stake_currency` +* `fiat_currency` +* `sell_reason` +* `order_type` ### Webhookstatus diff --git a/environment.yml b/environment.yml new file mode 100644 index 000000000..cd3350fd5 --- /dev/null +++ b/environment.yml @@ -0,0 +1,59 @@ +name: freqtrade +channels: + - defaults + - conda-forge +dependencies: + # Required for app + - python>=3.6 + - pip + - wheel + - numpy + - pandas + - scipy + - SQLAlchemy + - scikit-learn + - arrow + - requests + - urllib3 + - wrapt + - joblib + - jsonschema + - tabulate + - python-rapidjson + - filelock + - flask + - python-dotenv + - cachetools + - scikit-optimize + - python-telegram-bot + # Optional for plotting + - plotly + # Optional for development + - flake8 + - pytest + - pytest-mock + - pytest-asyncio + - pytest-cov + - coveralls + - mypy + # Useful for jupyter + - jupyter + - ipykernel + - isort + - yapf + - pip: + # Required for app + - cython + - coinmarketcap + - ccxt + - TA-Lib + - py_find_1st + - sdnotify + # Optional for develpment + - flake8-tidy-imports + - flake8-type-annotations + - pytest-random-order + - -e . + + + diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 0d1ae9c26..14f0bb819 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,15 +1,15 @@ """ FreqTrade bot """ -__version__ = '0.18.2-dev' +__version__ = '2019.7-dev' -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. """ -class OperationalException(BaseException): +class OperationalException(Exception): """ Requires manual intervention. This happens when an exchange returns an unexpected error during runtime @@ -17,7 +17,15 @@ class OperationalException(BaseException): """ -class TemporaryError(BaseException): +class InvalidOrderException(Exception): + """ + This is returned when the order is not valid. Example: + If stoploss on exchange order is hit, then trying to cancel the order + should return this exception. + """ + + +class TemporaryError(Exception): """ Temporary network or exchange related error. This could happen when an exchange is congested, unavailable, or the user diff --git a/freqtrade/__main__.py b/freqtrade/__main__.py index 7d271dfd1..97ed9ae67 100644 --- a/freqtrade/__main__.py +++ b/freqtrade/__main__.py @@ -6,10 +6,7 @@ To launch Freqtrade as a module > python -m freqtrade (with Python >= 3.6) """ -import sys - from freqtrade import main if __name__ == '__main__': - main.set_loggers() - main.main(sys.argv[1:]) + main.main() diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py deleted file mode 100644 index 604386426..000000000 --- a/freqtrade/arguments.py +++ /dev/null @@ -1,433 +0,0 @@ -""" -This module contains the argument manager class -""" - -import argparse -import os -import re -from typing import List, NamedTuple, Optional -import arrow -from freqtrade import __version__, constants - - -class TimeRange(NamedTuple): - """ - NamedTuple Defining timerange inputs. - [start/stop]type defines if [start/stop]ts shall be used. - if *type is none, don't use corresponding startvalue. - """ - starttype: Optional[str] = None - stoptype: Optional[str] = None - startts: int = 0 - stopts: int = 0 - - -class Arguments(object): - """ - Arguments Class. Manage the arguments received by the cli - """ - - def __init__(self, args: List[str], description: str) -> None: - self.args = args - self.parsed_arg: Optional[argparse.Namespace] = None - self.parser = argparse.ArgumentParser(description=description) - - def _load_args(self) -> None: - self.common_args_parser() - self._build_subcommands() - - def get_parsed_arg(self) -> argparse.Namespace: - """ - Return the list of arguments - :return: List[str] List of arguments - """ - if self.parsed_arg is None: - self._load_args() - self.parsed_arg = self.parse_args() - - return self.parsed_arg - - def parse_args(self) -> argparse.Namespace: - """ - Parses given arguments and returns an argparse Namespace instance. - """ - parsed_arg = self.parser.parse_args(self.args) - - # Workaround issue in argparse with action='append' and default value - # (see https://bugs.python.org/issue16399) - if parsed_arg.config is None: - parsed_arg.config = [constants.DEFAULT_CONFIG] - - return parsed_arg - - def common_args_parser(self) -> None: - """ - Parses given common arguments and returns them as a parsed object. - """ - self.parser.add_argument( - '-v', '--verbose', - help='Verbose mode (-vv for more, -vvv to get all messages).', - action='count', - dest='loglevel', - default=0, - ) - self.parser.add_argument( - '--version', - action='version', - version=f'%(prog)s {__version__}' - ) - 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( - '-d', '--datadir', - help='Path to backtest data.', - dest='datadir', - default=None, - type=str, - metavar='PATH', - ) - self.parser.add_argument( - '-s', '--strategy', - help='Specify strategy class name (default: %(default)s).', - dest='strategy', - default='DefaultStrategy', - type=str, - metavar='NAME', - ) - self.parser.add_argument( - '--strategy-path', - help='Specify additional strategy lookup path.', - dest='strategy_path', - type=str, - metavar='PATH', - ) - self.parser.add_argument( - '--dynamic-whitelist', - help='Dynamically generate and update whitelist' - ' based on 24h BaseVolume (default: %(const)s).' - ' DEPRECATED.', - dest='dynamic_whitelist', - const=constants.DYNAMIC_WHITELIST, - type=int, - metavar='INT', - nargs='?', - ) - self.parser.add_argument( - '--db-url', - help='Override trades database URL, this is useful if dry_run is enabled' - ' or in custom deployments (default: %(default)s).', - dest='db_url', - type=str, - metavar='PATH', - ) - self.parser.add_argument( - '--sd-notify', - help='Notify systemd service manager.', - action='store_true', - dest='sd_notify', - ) - - @staticmethod - def backtesting_options(parser: argparse.ArgumentParser) -> None: - """ - Parses given arguments for Backtesting scripts. - """ - parser.add_argument( - '--eps', '--enable-position-stacking', - help='Allow buying the same pair multiple times (position stacking).', - action='store_true', - dest='position_stacking', - default=False - ) - parser.add_argument( - '--dmmp', '--disable-max-market-positions', - help='Disable applying `max_open_trades` during backtest ' - '(same as setting `max_open_trades` to a very high number).', - action='store_false', - dest='use_max_market_positions', - default=True - ) - parser.add_argument( - '-l', '--live', - help='Use live data.', - action='store_true', - 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( - '--strategy-list', - help='Provide a commaseparated list of strategies to backtest ' - 'Please note that ticker-interval needs to be set either in config ' - 'or via command line. When using this together with --export trades, ' - 'the strategy-name is injected into the filename ' - '(so backtest-data.json becomes backtest-data-DefaultStrategy.json', - nargs='+', - dest='strategy_list', - ) - parser.add_argument( - '--export', - help='Export backtest results, argument are: trades. ' - 'Example --export=trades', - type=str, - default=None, - dest='export', - ) - parser.add_argument( - '--export-filename', - help='Save backtest results to this filename \ - requires --export to be set as well\ - 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'), - dest='exportfilename', - metavar='PATH', - ) - - @staticmethod - def edge_options(parser: argparse.ArgumentParser) -> None: - """ - Parses given arguments for Backtesting scripts. - """ - 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 edge with up-to-date data.', - action='store_true', - dest='refresh_pairs', - ) - parser.add_argument( - '--stoplosses', - help='Defines a range of stoploss against which edge will assess the strategy ' - 'the format is "min,max,step" (without any space).' - 'example: --stoplosses=-0.01,-0.1,-0.001', - type=str, - dest='stoploss_range', - ) - - @staticmethod - def optimizer_shared_options(parser: argparse.ArgumentParser) -> None: - """ - Parses given common arguments for Backtesting and Hyperopt scripts. - :param parser: - :return: - """ - parser.add_argument( - '-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', - ) - - @staticmethod - def hyperopt_options(parser: argparse.ArgumentParser) -> None: - """ - Parses given arguments for Hyperopt scripts. - """ - parser.add_argument( - '--customhyperopt', - help='Specify hyperopt class name (default: %(default)s).', - dest='hyperopt', - default=constants.DEFAULT_HYPEROPT, - type=str, - metavar='NAME', - ) - parser.add_argument( - '--eps', '--enable-position-stacking', - help='Allow buying the same pair multiple times (position stacking).', - action='store_true', - dest='position_stacking', - default=False - ) - - parser.add_argument( - '--dmmp', '--disable-max-market-positions', - help='Disable applying `max_open_trades` during backtest ' - '(same as setting `max_open_trades` to a very high number).', - action='store_false', - dest='use_max_market_positions', - default=True - ) - parser.add_argument( - '-e', '--epochs', - help='Specify number of epochs (default: %(default)d).', - dest='epochs', - default=constants.HYPEROPT_EPOCH, - type=int, - metavar='INT', - ) - parser.add_argument( - '-s', '--spaces', - help='Specify which parameters to hyperopt. Space separate list. \ - Default: %(default)s.', - choices=['all', 'buy', 'sell', 'roi', 'stoploss'], - default='all', - nargs='+', - dest='spaces', - ) - - def _build_subcommands(self) -> None: - """ - Builds and attaches all subcommands - :return: None - """ - from freqtrade.optimize import backtesting, hyperopt, edge_cli - - subparsers = self.parser.add_subparsers(dest='subparser') - - # Add backtesting subcommand - backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.') - backtesting_cmd.set_defaults(func=backtesting.start) - self.optimizer_shared_options(backtesting_cmd) - self.backtesting_options(backtesting_cmd) - - # Add edge subcommand - edge_cmd = subparsers.add_parser('edge', help='Edge module.') - edge_cmd.set_defaults(func=edge_cli.start) - self.optimizer_shared_options(edge_cmd) - self.edge_options(edge_cmd) - - # Add hyperopt subcommand - hyperopt_cmd = subparsers.add_parser('hyperopt', help='Hyperopt module.') - hyperopt_cmd.set_defaults(func=hyperopt.start) - self.optimizer_shared_options(hyperopt_cmd) - self.hyperopt_options(hyperopt_cmd) - - @staticmethod - def parse_timerange(text: Optional[str]) -> TimeRange: - """ - Parse the value of the argument --timerange to determine what is the range desired - :param text: value from --timerange - :return: Start and End range period - """ - if text is None: - return TimeRange(None, None, 0, 0) - syntax = [(r'^-(\d{8})$', (None, 'date')), - (r'^(\d{8})-$', ('date', None)), - (r'^(\d{8})-(\d{8})$', ('date', 'date')), - (r'^-(\d{10})$', (None, 'date')), - (r'^(\d{10})-$', ('date', None)), - (r'^(\d{10})-(\d{10})$', ('date', 'date')), - (r'^(-\d+)$', (None, 'line')), - (r'^(\d+)-$', ('line', None)), - (r'^(\d+)-(\d+)$', ('index', 'index'))] - for rex, stype in syntax: - # Apply the regular expression to text - match = re.match(rex, text) - if match: # Regex has matched - rvals = match.groups() - index = 0 - start: int = 0 - stop: int = 0 - if stype[0]: - starts = rvals[index] - if stype[0] == 'date' and len(starts) == 8: - start = arrow.get(starts, 'YYYYMMDD').timestamp - else: - start = int(starts) - index += 1 - if stype[1]: - stops = rvals[index] - if stype[1] == 'date' and len(stops) == 8: - stop = arrow.get(stops, 'YYYYMMDD').timestamp - else: - stop = int(stops) - return TimeRange(stype[0], stype[1], start, stop) - raise Exception('Incorrect syntax for timerange "%s"' % text) - - def scripts_options(self) -> None: - """ - Parses given arguments for scripts. - """ - self.parser.add_argument( - '-p', '--pairs', - help='Show profits for only this pairs. Pairs are comma-separated.', - dest='pairs', - default=None - ) - - def testdata_dl_options(self) -> None: - """ - Parses given arguments for testdata download - """ - self.parser.add_argument( - '--pairs-file', - help='File containing a list of pairs to download.', - dest='pairs_file', - default=None, - metavar='PATH', - ) - - 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', - help='Download data for given number of days.', - dest='days', - type=int, - metavar='INT', - default=None - ) - - self.parser.add_argument( - '--exchange', - help='Exchange name (default: %(default)s). Only valid if no config is provided.', - dest='exchange', - type=str, - default='bittrex' - ) - - self.parser.add_argument( - '-t', '--timeframes', - help='Specify which tickers to download. Space separated list. \ - Default: %(default)s.', - choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', - '6h', '8h', '12h', '1d', '3d', '1w'], - default=['1m', '5m'], - nargs='+', - dest='timeframes', - ) - - self.parser.add_argument( - '--erase', - help='Clean all existing data for the selected exchange/pairs/timeframes.', - dest='erase', - action='store_true' - ) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py deleted file mode 100644 index ba7a0e200..000000000 --- a/freqtrade/configuration.py +++ /dev/null @@ -1,378 +0,0 @@ -""" -This module contains the configuration class -""" -import json -import logging -import os -from argparse import Namespace -from typing import Any, Dict, Optional - -import ccxt -from jsonschema import Draft4Validator, validate -from jsonschema.exceptions import ValidationError, best_match - -from freqtrade import OperationalException, constants -from freqtrade.state import RunMode -from freqtrade.misc import deep_merge_dicts - -logger = logging.getLogger(__name__) - - -def set_loggers(log_level: int = 0) -> None: - """ - Set the logger level for Third party libs - :return: None - """ - - logging.getLogger('requests').setLevel(logging.INFO if log_level <= 1 else logging.DEBUG) - logging.getLogger("urllib3").setLevel(logging.INFO if log_level <= 1 else logging.DEBUG) - logging.getLogger('ccxt.base.exchange').setLevel( - logging.INFO if log_level <= 2 else logging.DEBUG) - logging.getLogger('telegram').setLevel(logging.INFO) - - -class Configuration(object): - """ - Class to read and init the bot configuration - Reuse this class for the bot, backtesting, hyperopt and every script that required configuration - """ - - def __init__(self, args: Namespace, runmode: RunMode = None) -> None: - self.args = args - self.config: Optional[Dict[str, Any]] = None - self.runmode = runmode - - def load_config(self) -> Dict[str, Any]: - """ - Extract information for sys.argv and load the bot configuration - :return: Configuration dictionary - """ - config: Dict[str, Any] = {} - # Now expecting a list of config filenames here, not a string - for path in self.args.config: - logger.info('Using config: %s ...', path) - # Merge config options, overwriting old values - config = deep_merge_dicts(self._load_config_file(path), config) - - if 'internals' not in config: - config['internals'] = {} - - logger.info('Validating configuration ...') - self._validate_config_schema(config) - self._validate_config_consistency(config) - - # Set strategy if not specified in config and or if it's non default - if self.args.strategy != constants.DEFAULT_STRATEGY or not config.get('strategy'): - config.update({'strategy': self.args.strategy}) - - if self.args.strategy_path: - config.update({'strategy_path': self.args.strategy_path}) - - # Load Common configuration - config = self._load_common_config(config) - - # Load Backtesting - config = self._load_backtesting_config(config) - - # Load Edge - config = self._load_edge_config(config) - - # Load Hyperopt - config = self._load_hyperopt_config(config) - - # Set runmode - if not self.runmode: - # Handle real mode, infer dry/live from config - self.runmode = RunMode.DRY_RUN if config.get('dry_run', True) else RunMode.LIVE - - config.update({'runmode': self.runmode}) - - return config - - def _load_config_file(self, path: str) -> Dict[str, Any]: - """ - Loads a config file from the given path - :param path: path as str - :return: configuration as dictionary - """ - try: - with open(path) as file: - conf = json.load(file) - except FileNotFoundError: - raise OperationalException( - f'Config file "{path}" not found!' - ' Please create a config file or check whether it exists.') - - return conf - - 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 - """ - - # Log level - if 'loglevel' in self.args and self.args.loglevel: - config.update({'verbosity': self.args.loglevel}) - else: - config.update({'verbosity': 0}) - logging.basicConfig( - level=logging.INFO if config['verbosity'] < 1 else logging.DEBUG, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - ) - set_loggers(config['verbosity']) - logger.info('Verbosity set to %s', config['verbosity']) - - # Support for sd_notify - if self.args.sd_notify: - config['internals'].update({'sd_notify': True}) - - # Add dynamic_whitelist if found - if 'dynamic_whitelist' in self.args and self.args.dynamic_whitelist: - # Update to volumePairList (the previous default) - config['pairlist'] = {'method': 'VolumePairList', - 'config': {'number_assets': self.args.dynamic_whitelist} - } - logger.warning( - 'Parameter --dynamic-whitelist has been deprecated, ' - 'and will be completely replaced by the whitelist dict in the future. ' - 'For now: using dynamically generated whitelist based on VolumePairList. ' - '(not applicable with Backtesting and Hyperopt)' - ) - - if self.args.db_url and self.args.db_url != constants.DEFAULT_DB_PROD_URL: - config.update({'db_url': self.args.db_url}) - logger.info('Parameter --db-url detected ...') - - if config.get('dry_run', False): - logger.info('Dry run is enabled') - if config.get('db_url') in [None, constants.DEFAULT_DB_PROD_URL]: - # Default to in-memory db for dry_run if not specified - config['db_url'] = constants.DEFAULT_DB_DRYRUN_URL - else: - if not config.get('db_url', None): - config['db_url'] = constants.DEFAULT_DB_PROD_URL - logger.info('Dry run is disabled') - - if config.get('forcebuy_enable', False): - logger.warning('`forcebuy` RPC message enabled.') - - # Setting max_open_trades to infinite if -1 - if config.get('max_open_trades') == -1: - config['max_open_trades'] = float('inf') - - logger.info(f'Using DB: "{config["db_url"]}"') - - # Check if the exchange set by the user is supported - self.check_exchange(config) - - return config - - def _create_datadir(self, config: Dict[str, Any], datadir: Optional[str] = None) -> str: - if not datadir: - # set datadir - exchange_name = config.get('exchange', {}).get('name').lower() - datadir = os.path.join('user_data', 'data', exchange_name) - - if not os.path.isdir(datadir): - os.makedirs(datadir) - logger.info(f'Created data directory: {datadir}') - return datadir - - def _load_backtesting_config(self, config: Dict[str, Any]) -> Dict[str, Any]: - """ - Extract information for sys.argv and load Backtesting configuration - :return: configuration as dictionary - """ - - # If -i/--ticker-interval is used we override the configuration parameter - # (that will override the strategy configuration) - if 'ticker_interval' in self.args and self.args.ticker_interval: - config.update({'ticker_interval': self.args.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 - if 'live' in self.args and self.args.live: - config.update({'live': True}) - logger.info('Parameter -l/--live detected ...') - - # If --enable-position-stacking is used we add it to the configuration - if 'position_stacking' in self.args and self.args.position_stacking: - config.update({'position_stacking': True}) - logger.info('Parameter --enable-position-stacking detected ...') - - # If --disable-max-market-positions is used we add it to the configuration - if 'use_max_market_positions' in self.args and not self.args.use_max_market_positions: - config.update({'use_max_market_positions': False}) - logger.info('Parameter --disable-max-market-positions detected ...') - logger.info('max_open_trades set to unlimited ...') - else: - logger.info('Using max_open_trades: %s ...', config.get('max_open_trades')) - - # 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 --datadir is used we add it to the configuration - 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 - if 'refresh_pairs' in self.args and self.args.refresh_pairs: - config.update({'refresh_pairs': True}) - logger.info('Parameter -r/--refresh-pairs-cached detected ...') - - if 'strategy_list' in self.args and self.args.strategy_list: - config.update({'strategy_list': self.args.strategy_list}) - logger.info('Using strategy list of %s Strategies', len(self.args.strategy_list)) - - if 'ticker_interval' in self.args and self.args.ticker_interval: - config.update({'ticker_interval': self.args.ticker_interval}) - logger.info('Overriding ticker interval with Command line argument') - - # If --export is used we add it to the configuration - if 'export' in self.args and self.args.export: - 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 - if 'export' in config and 'exportfilename' in self.args and self.args.exportfilename: - config.update({'exportfilename': self.args.exportfilename}) - logger.info('Storing backtest results to %s ...', self.args.exportfilename) - - return config - - 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: - txt_range = eval(self.args.stoploss_range) - config['edge'].update({'stoploss_range_min': txt_range[0]}) - config['edge'].update({'stoploss_range_max': txt_range[1]}) - config['edge'].update({'stoploss_range_step': txt_range[2]}) - logger.info('Parameter --stoplosses detected: %s ...', self.args.stoploss_range) - - # If -r/--refresh-pairs-cached is used we add it to the configuration - if 'refresh_pairs' in self.args and self.args.refresh_pairs: - config.update({'refresh_pairs': True}) - logger.info('Parameter -r/--refresh-pairs-cached detected ...') - - return config - - def _load_hyperopt_config(self, config: Dict[str, Any]) -> Dict[str, Any]: - """ - Extract information for sys.argv and load Hyperopt configuration - :return: configuration as dictionary - """ - - if "hyperopt" in self.args: - # Add the hyperopt file to use - config.update({'hyperopt': self.args.hyperopt}) - - # If --epochs is used we add it to the configuration - if 'epochs' in self.args and self.args.epochs: - 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 - if 'spaces' in self.args and self.args.spaces: - config.update({'spaces': self.args.spaces}) - logger.info('Parameter -s/--spaces detected: %s', config.get('spaces')) - - return config - - def _validate_config_schema(self, conf: Dict[str, Any]) -> Dict[str, Any]: - """ - Validate the configuration follow the Config Schema - :param conf: Config in JSON format - :return: Returns the config if valid, otherwise throw an exception - """ - try: - validate(conf, constants.CONF_SCHEMA, Draft4Validator) - return conf - except ValidationError as exception: - logger.critical( - 'Invalid configuration. See config.json.example. Reason: %s', - exception - ) - raise ValidationError( - best_match(Draft4Validator(constants.CONF_SCHEMA).iter_errors(conf)).message - ) - - def _validate_config_consistency(self, conf: Dict[str, Any]) -> None: - """ - Validate the configuration consistency - :param conf: Config in JSON format - :return: Returns None if everything is ok, otherwise throw an OperationalException - """ - - # validating trailing stoploss - self._validate_trailing_stoploss(conf) - - def _validate_trailing_stoploss(self, conf: Dict[str, Any]) -> None: - # Skip if trailing stoploss is not activated - if not conf.get('trailing_stop', False): - return - - tsl_positive = float(conf.get('trailing_stop_positive', 0)) - tsl_offset = float(conf.get('trailing_stop_positive_offset', 0)) - tsl_only_offset = conf.get('trailing_only_offset_is_reached', False) - - if tsl_only_offset: - if tsl_positive == 0.0: - raise OperationalException( - f'The config trailing_only_offset_is_reached needs ' - 'trailing_stop_positive_offset to be more than 0 in your config.') - if tsl_positive > 0 and 0 < tsl_offset <= tsl_positive: - raise OperationalException( - f'The config trailing_stop_positive_offset needs ' - 'to be greater than trailing_stop_positive_offset in your config.') - - def get_config(self) -> Dict[str, Any]: - """ - Return the config. Use this method to get the bot config - :return: Dict: Bot config - """ - if self.config is None: - self.config = self.load_config() - - return self.config - - def check_exchange(self, config: Dict[str, Any]) -> bool: - """ - 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 - """ - exchange = config.get('exchange', {}).get('name').lower() - if exchange not in ccxt.exchanges: - - exception_msg = f'Exchange "{exchange}" not supported.\n' \ - f'The following exchanges are supported: {", ".join(ccxt.exchanges)}' - - logger.critical(exception_msg) - raise OperationalException( - exception_msg - ) - # Depreciation warning - if 'ccxt_rate_limit' in config.get('exchange', {}): - logger.warning("`ccxt_rate_limit` has been deprecated in favor of " - "`ccxt_config` and `ccxt_async_config` and will be removed " - "in a future version.") - - logger.debug('Exchange "%s" supported', exchange) - return True diff --git a/freqtrade/configuration/__init__.py b/freqtrade/configuration/__init__.py new file mode 100644 index 000000000..548b508a7 --- /dev/null +++ b/freqtrade/configuration/__init__.py @@ -0,0 +1,2 @@ +from freqtrade.configuration.arguments import Arguments, TimeRange # noqa: F401 +from freqtrade.configuration.configuration import Configuration # noqa: F401 diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py new file mode 100644 index 000000000..4f0c3d31b --- /dev/null +++ b/freqtrade/configuration/arguments.py @@ -0,0 +1,176 @@ +""" +This module contains the argument manager class +""" +import argparse +import re +from typing import List, NamedTuple, Optional + +import arrow +from freqtrade.configuration.cli_options import AVAILABLE_CLI_OPTIONS +from freqtrade import constants + +ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir"] + +ARGS_STRATEGY = ["strategy", "strategy_path"] + +ARGS_MAIN = ARGS_COMMON + ARGS_STRATEGY + ["db_url", "sd_notify"] + +ARGS_COMMON_OPTIMIZE = ["ticker_interval", "timerange", + "max_open_trades", "stake_amount", "refresh_pairs"] + +ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions", + "live", "strategy_list", "export", "exportfilename"] + +ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path", + "position_stacking", "epochs", "spaces", + "use_max_market_positions", "print_all", "hyperopt_jobs", + "hyperopt_random_state", "hyperopt_min_trades", + "hyperopt_continue", "hyperopt_loss"] + +ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"] + +ARGS_LIST_EXCHANGES = ["print_one_column"] + +ARGS_DOWNLOADER = ARGS_COMMON + ["pairs", "pairs_file", "days", "exchange", "timeframes", "erase"] + +ARGS_PLOT_DATAFRAME = (ARGS_COMMON + ARGS_STRATEGY + + ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", + "trade_source", "export", "exportfilename", "timerange", + "refresh_pairs", "live"]) + +ARGS_PLOT_PROFIT = (ARGS_COMMON + ARGS_STRATEGY + + ["pairs", "timerange", "export", "exportfilename", "db_url", "trade_source"]) + + +class TimeRange(NamedTuple): + """ + NamedTuple defining timerange inputs. + [start/stop]type defines if [start/stop]ts shall be used. + if *type is None, don't use corresponding startvalue. + """ + starttype: Optional[str] = None + stoptype: Optional[str] = None + startts: int = 0 + stopts: int = 0 + + +class Arguments(object): + """ + Arguments Class. Manage the arguments received by the cli + """ + def __init__(self, args: Optional[List[str]], description: str, + no_default_config: bool = False) -> None: + self.args = args + self._parsed_arg: Optional[argparse.Namespace] = None + self.parser = argparse.ArgumentParser(description=description) + self._no_default_config = no_default_config + + def _load_args(self) -> None: + self._build_args(optionlist=ARGS_MAIN) + self._build_subcommands() + + def get_parsed_arg(self) -> argparse.Namespace: + """ + Return the list of arguments + :return: List[str] List of arguments + """ + if self._parsed_arg is None: + self._load_args() + self._parsed_arg = self._parse_args() + + return self._parsed_arg + + def _parse_args(self) -> argparse.Namespace: + """ + Parses given arguments and returns an argparse Namespace instance. + """ + parsed_arg = self.parser.parse_args(self.args) + + # Workaround issue in argparse with action='append' and default value + # (see https://bugs.python.org/issue16399) + if not self._no_default_config and parsed_arg.config is None: + parsed_arg.config = [constants.DEFAULT_CONFIG] + + return parsed_arg + + def _build_args(self, optionlist, parser=None): + parser = parser or self.parser + + for val in optionlist: + opt = AVAILABLE_CLI_OPTIONS[val] + parser.add_argument(*opt.cli, dest=val, **opt.kwargs) + + def _build_subcommands(self) -> None: + """ + Builds and attaches all subcommands. + :return: None + """ + from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge + from freqtrade.utils import start_list_exchanges + + subparsers = self.parser.add_subparsers(dest='subparser') + + # Add backtesting subcommand + backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.') + backtesting_cmd.set_defaults(func=start_backtesting) + self._build_args(optionlist=ARGS_BACKTEST, parser=backtesting_cmd) + + # Add edge subcommand + edge_cmd = subparsers.add_parser('edge', help='Edge module.') + edge_cmd.set_defaults(func=start_edge) + self._build_args(optionlist=ARGS_EDGE, parser=edge_cmd) + + # Add hyperopt subcommand + hyperopt_cmd = subparsers.add_parser('hyperopt', help='Hyperopt module.') + hyperopt_cmd.set_defaults(func=start_hyperopt) + self._build_args(optionlist=ARGS_HYPEROPT, parser=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._build_args(optionlist=ARGS_LIST_EXCHANGES, parser=list_exchanges_cmd) + + @staticmethod + def parse_timerange(text: Optional[str]) -> TimeRange: + """ + Parse the value of the argument --timerange to determine what is the range desired + :param text: value from --timerange + :return: Start and End range period + """ + if text is None: + return TimeRange(None, None, 0, 0) + syntax = [(r'^-(\d{8})$', (None, 'date')), + (r'^(\d{8})-$', ('date', None)), + (r'^(\d{8})-(\d{8})$', ('date', 'date')), + (r'^-(\d{10})$', (None, 'date')), + (r'^(\d{10})-$', ('date', None)), + (r'^(\d{10})-(\d{10})$', ('date', 'date')), + (r'^(-\d+)$', (None, 'line')), + (r'^(\d+)-$', ('line', None)), + (r'^(\d+)-(\d+)$', ('index', 'index'))] + for rex, stype in syntax: + # Apply the regular expression to text + match = re.match(rex, text) + if match: # Regex has matched + rvals = match.groups() + index = 0 + start: int = 0 + stop: int = 0 + if stype[0]: + starts = rvals[index] + if stype[0] == 'date' and len(starts) == 8: + start = arrow.get(starts, 'YYYYMMDD').timestamp + else: + start = int(starts) + index += 1 + if stype[1]: + stops = rvals[index] + if stype[1] == 'date' and len(stops) == 8: + stop = arrow.get(stops, 'YYYYMMDD').timestamp + else: + stop = int(stops) + return TimeRange(stype[0], stype[1], start, stop) + raise Exception('Incorrect syntax for timerange "%s"' % text) diff --git a/freqtrade/configuration/check_exchange.py b/freqtrade/configuration/check_exchange.py new file mode 100644 index 000000000..8dae06f7a --- /dev/null +++ b/freqtrade/configuration/check_exchange.py @@ -0,0 +1,48 @@ +import logging +from typing import Any, Dict + +from freqtrade import OperationalException +from freqtrade.exchange import (is_exchange_bad, is_exchange_available, + is_exchange_officially_supported, available_exchanges) + + +logger = logging.getLogger(__name__) + + +def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool: + """ + Check if the exchange name in the config file is supported by Freqtrade + :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() + if not is_exchange_available(exchange): + raise OperationalException( + 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())}' + ) + + 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 diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py new file mode 100644 index 000000000..04554c386 --- /dev/null +++ b/freqtrade/configuration/cli_options.py @@ -0,0 +1,302 @@ +""" +Definition of cli arguments used in arguments.py +""" +import argparse +import os + +from freqtrade import __version__, constants + + +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 + + +class Arg: + # Optional CLI arguments + def __init__(self, *args, **kwargs): + self.cli = args + self.kwargs = kwargs + + +# List of available command line options +AVAILABLE_CLI_OPTIONS = { + # Common options + "verbosity": Arg( + '-v', '--verbose', + help='Verbose mode (-vv for more, -vvv to get all messages).', + action='count', + default=0, + ), + "logfile": Arg( + '--logfile', + help='Log to the file specified.', + metavar='FILE', + ), + "version": Arg( + '-V', '--version', + action='version', + version=f'%(prog)s {__version__}', + ), + "config": Arg( + '-c', '--config', + help=f'Specify configuration file (default: `{constants.DEFAULT_CONFIG}`). ' + f'Multiple --config options may be used. ' + f'Can be set to `-` to read config from stdin.', + action='append', + metavar='PATH', + ), + "datadir": Arg( + '-d', '--datadir', + help='Path to backtest data.', + metavar='PATH', + ), + # Main options + "strategy": Arg( + '-s', '--strategy', + help='Specify strategy class name (default: `%(default)s`).', + metavar='NAME', + default='DefaultStrategy', + ), + "strategy_path": Arg( + '--strategy-path', + help='Specify additional strategy lookup path.', + metavar='PATH', + ), + "db_url": Arg( + '--db-url', + help=f'Override trades database URL, this is useful in custom deployments ' + f'(default: `{constants.DEFAULT_DB_PROD_URL}` for Live Run mode, ' + f'`{constants.DEFAULT_DB_DRYRUN_URL}` for Dry Run).', + metavar='PATH', + ), + "sd_notify": Arg( + '--sd-notify', + help='Notify systemd service manager.', + action='store_true', + ), + # Optimize common + "ticker_interval": Arg( + '-i', '--ticker-interval', + help='Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`).', + ), + "timerange": Arg( + '--timerange', + help='Specify what timerange of data to use.', + ), + "max_open_trades": Arg( + '--max_open_trades', + help='Specify max_open_trades to use.', + type=int, + metavar='INT', + ), + "stake_amount": Arg( + '--stake_amount', + help='Specify stake_amount.', + type=float, + ), + "refresh_pairs": Arg( + '-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', + ), + # Backtesting + "position_stacking": Arg( + '--eps', '--enable-position-stacking', + help='Allow buying the same pair multiple times (position stacking).', + action='store_true', + default=False, + ), + "use_max_market_positions": Arg( + '--dmmp', '--disable-max-market-positions', + help='Disable applying `max_open_trades` during backtest ' + '(same as setting `max_open_trades` to a very high number).', + action='store_false', + default=True, + ), + "live": Arg( + '-l', '--live', + help='Use live data.', + action='store_true', + ), + "strategy_list": Arg( + '--strategy-list', + help='Provide a space-separated list of strategies to backtest. ' + 'Please note that ticker-interval needs to be set either in config ' + 'or via command line. When using this together with `--export trades`, ' + 'the strategy-name is injected into the filename ' + '(so `backtest-data.json` becomes `backtest-data-DefaultStrategy.json`', + nargs='+', + ), + "export": Arg( + '--export', + help='Export backtest results, argument are: trades. ' + 'Example: `--export=trades`', + ), + "exportfilename": Arg( + '--export-filename', + help='Save backtest results to the file with this filename (default: `%(default)s`). ' + 'Requires `--export` to be set as well. ' + 'Example: `--export-filename=user_data/backtest_data/backtest_today.json`', + metavar='PATH', + default=os.path.join('user_data', 'backtest_data', + 'backtest-result.json'), + ), + # Edge + "stoploss_range": Arg( + '--stoplosses', + help='Defines a range of stoploss values against which edge will assess the strategy. ' + 'The format is "min,max,step" (without any space). ' + 'Example: `--stoplosses=-0.01,-0.1,-0.001`', + ), + # Hyperopt + "hyperopt": Arg( + '--customhyperopt', + help='Specify hyperopt class name (default: `%(default)s`).', + metavar='NAME', + default=constants.DEFAULT_HYPEROPT, + ), + "hyperopt_path": Arg( + '--hyperopt-path', + help='Specify additional lookup path for Hyperopts and Hyperopt Loss functions.', + metavar='PATH', + ), + "epochs": Arg( + '-e', '--epochs', + help='Specify number of epochs (default: %(default)d).', + type=check_int_positive, + metavar='INT', + default=constants.HYPEROPT_EPOCH, + ), + "spaces": Arg( + '-s', '--spaces', + help='Specify which parameters to hyperopt. Space-separated list. ' + 'Default: `%(default)s`.', + choices=['all', 'buy', 'sell', 'roi', 'stoploss'], + nargs='+', + default='all', + ), + "print_all": Arg( + '--print-all', + help='Print all results, not only the best ones.', + action='store_true', + default=False, + ), + "hyperopt_jobs": Arg( + '-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.', + type=int, + metavar='JOBS', + default=-1, + ), + "hyperopt_random_state": Arg( + '--random-state', + help='Set random state to some positive integer for reproducible hyperopt results.', + type=check_int_positive, + metavar='INT', + ), + "hyperopt_min_trades": Arg( + '--min-trades', + help="Set minimal desired number of trades for evaluations in the hyperopt " + "optimization path (default: 1).", + type=check_int_positive, + metavar='INT', + default=1, + ), + "hyperopt_continue": Arg( + "--continue", + help="Continue hyperopt from previous runs. " + "By default, temporary files will be removed and hyperopt will start from scratch.", + default=False, + action='store_true', + ), + "hyperopt_loss": Arg( + '--hyperopt-loss', + help='Specify the class name of the hyperopt loss function class (IHyperOptLoss). ' + 'Different functions can generate completely different results, ' + 'since the target for optimization is different. (default: `%(default)s`).', + metavar='NAME', + default=constants.DEFAULT_HYPEROPT_LOSS, + ), + # List exchanges + "print_one_column": Arg( + '-1', '--one-column', + help='Print exchanges in one column.', + action='store_true', + ), + # Script options + "pairs": Arg( + '-p', '--pairs', + help='Show profits for only these pairs. Pairs are comma-separated.', + ), + # Download data + "pairs_file": Arg( + '--pairs-file', + help='File containing a list of pairs to download.', + metavar='FILE', + ), + "days": Arg( + '--days', + help='Download data for given number of days.', + type=check_int_positive, + metavar='INT', + ), + "exchange": Arg( + '--exchange', + help=f'Exchange name (default: `{constants.DEFAULT_EXCHANGE}`). ' + f'Only valid if no config is provided.', + ), + "timeframes": Arg( + '-t', '--timeframes', + help=f'Specify which tickers to download. Space-separated list. ' + f'Default: `{constants.DEFAULT_DOWNLOAD_TICKER_INTERVALS}`.', + choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', + '6h', '8h', '12h', '1d', '3d', '1w'], + nargs='+', + ), + "erase": Arg( + '--erase', + help='Clean all existing data for the selected exchange/pairs/timeframes.', + action='store_true', + ), + # Plot dataframe + "indicators1": Arg( + '--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', + ), + "indicators2": Arg( + '--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', + ), + "plot_limit": Arg( + '--plot-limit', + help='Specify tick limit for plotting. Notice: too high values cause huge files. ' + 'Default: %(default)s.', + type=check_int_positive, + metavar='INT', + default=750, + ), + "trade_source": Arg( + '--trade-source', + help='Specify the source for trades (Can be DB or file (backtest file)) ' + 'Default: %(default)s', + choices=["DB", "file"], + default="file", + ), +} diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py new file mode 100644 index 000000000..e564c79ce --- /dev/null +++ b/freqtrade/configuration/configuration.py @@ -0,0 +1,328 @@ +""" +This module contains the configuration class +""" +import logging +import warnings +from argparse import Namespace +from typing import Any, Callable, Dict, Optional + +from freqtrade import OperationalException, constants +from freqtrade.configuration.check_exchange import check_exchange +from freqtrade.configuration.create_datadir import create_datadir +from freqtrade.configuration.json_schema import validate_config_schema +from freqtrade.configuration.load_config import load_config_file +from freqtrade.loggers import setup_logging +from freqtrade.misc import deep_merge_dicts +from freqtrade.state import RunMode + +logger = logging.getLogger(__name__) + + +class Configuration(object): + """ + Class to read and init the bot configuration + Reuse this class for the bot, backtesting, hyperopt and every script that required configuration + """ + + def __init__(self, args: Namespace, runmode: RunMode = None) -> None: + self.args = args + self.config: Optional[Dict[str, Any]] = None + self.runmode = runmode + + def get_config(self) -> Dict[str, Any]: + """ + Return the config. Use this method to get the bot config + :return: Dict: Bot config + """ + if self.config is None: + self.config = self.load_config() + + return self.config + + def _load_config_files(self) -> Dict[str, Any]: + """ + Iterate through the config files passed in the args, + loading all of them and merging their contents. + """ + config: Dict[str, Any] = {} + + # We expect here a list of config filenames + for path in self.args.config: + logger.info('Using config: %s ...', path) + + # Merge config options, overwriting old values + config = deep_merge_dicts(load_config_file(path), config) + + return config + + def _normalize_config(self, config: Dict[str, Any]) -> None: + """ + Make config more canonical -- i.e. for example add missing parts that we expect + to be normally in it... + """ + if 'internals' not in config: + config['internals'] = {} + + def load_config(self) -> Dict[str, Any]: + """ + Extract information for sys.argv and load the bot configuration + :return: Configuration dictionary + """ + # Load all configs + config: Dict[str, Any] = self._load_config_files() + + # Make resulting config more canonical + self._normalize_config(config) + + logger.info('Validating configuration ...') + validate_config_schema(config) + + self._validate_config_consistency(config) + + self._process_common_options(config) + + self._process_optimize_options(config) + + self._process_plot_options(config) + + self._process_runmode(config) + + return config + + def _process_logging_options(self, config: Dict[str, Any]) -> None: + """ + Extract information for sys.argv and load logging configuration: + the -v/--verbose, --logfile options + """ + # Log level + if 'verbosity' in self.args and self.args.verbosity: + config.update({'verbosity': self.args.verbosity}) + else: + config.update({'verbosity': 0}) + + if 'logfile' in self.args and self.args.logfile: + config.update({'logfile': self.args.logfile}) + + setup_logging(config) + + def _process_strategy_options(self, config: Dict[str, Any]) -> None: + + # Set strategy if not specified in config and or if it's non default + if self.args.strategy != constants.DEFAULT_STRATEGY or not config.get('strategy'): + config.update({'strategy': self.args.strategy}) + + self._args_to_config(config, argname='strategy_path', + logstring='Using additional Strategy lookup path: {}') + + def _process_common_options(self, config: Dict[str, Any]) -> None: + + self._process_logging_options(config) + self._process_strategy_options(config) + + if ('db_url' in self.args and self.args.db_url and + self.args.db_url != constants.DEFAULT_DB_PROD_URL): + config.update({'db_url': self.args.db_url}) + logger.info('Parameter --db-url detected ...') + + if config.get('dry_run', False): + logger.info('Dry run is enabled') + if config.get('db_url') in [None, constants.DEFAULT_DB_PROD_URL]: + # Default to in-memory db for dry_run if not specified + config['db_url'] = constants.DEFAULT_DB_DRYRUN_URL + else: + if not config.get('db_url', None): + config['db_url'] = constants.DEFAULT_DB_PROD_URL + logger.info('Dry run is disabled') + + logger.info(f'Using DB: "{config["db_url"]}"') + + if config.get('forcebuy_enable', False): + logger.warning('`forcebuy` RPC message enabled.') + + # Setting max_open_trades to infinite if -1 + if config.get('max_open_trades') == -1: + config['max_open_trades'] = float('inf') + + # Support for sd_notify + if 'sd_notify' in self.args and self.args.sd_notify: + config['internals'].update({'sd_notify': True}) + + # Check if the exchange set by the user is supported + check_exchange(config) + + def _process_datadir_options(self, config: Dict[str, Any]) -> None: + """ + Extract information for sys.argv and load datadir configuration: + the --datadir option + """ + if 'datadir' in self.args and self.args.datadir: + config.update({'datadir': create_datadir(config, self.args.datadir)}) + else: + config.update({'datadir': create_datadir(config, None)}) + logger.info('Using data directory: %s ...', config.get('datadir')) + + def _process_optimize_options(self, config: Dict[str, Any]) -> None: + + # This will override the strategy configuration + self._args_to_config(config, argname='ticker_interval', + logstring='Parameter -i/--ticker-interval detected ... ' + 'Using ticker_interval: {} ...') + + self._args_to_config(config, argname='live', + logstring='Parameter -l/--live detected ...', + deprecated_msg='--live will be removed soon.') + + self._args_to_config(config, argname='position_stacking', + logstring='Parameter --enable-position-stacking detected ...') + + if 'use_max_market_positions' in self.args and not self.args.use_max_market_positions: + config.update({'use_max_market_positions': False}) + logger.info('Parameter --disable-max-market-positions detected ...') + logger.info('max_open_trades set to unlimited ...') + elif 'max_open_trades' in self.args and self.args.max_open_trades: + config.update({'max_open_trades': self.args.max_open_trades}) + logger.info('Parameter --max_open_trades detected, ' + 'overriding max_open_trades to: %s ...', config.get('max_open_trades')) + else: + logger.info('Using max_open_trades: %s ...', config.get('max_open_trades')) + + self._args_to_config(config, argname='stake_amount', + logstring='Parameter --stake_amount detected, ' + 'overriding stake_amount to: {} ...') + + self._args_to_config(config, argname='timerange', + logstring='Parameter --timerange detected: {} ...') + + self._process_datadir_options(config) + + self._args_to_config(config, argname='refresh_pairs', + logstring='Parameter -r/--refresh-pairs-cached detected ...') + + self._args_to_config(config, argname='strategy_list', + logstring='Using strategy list of {} Strategies', logfun=len) + + self._args_to_config(config, argname='ticker_interval', + logstring='Overriding ticker interval with Command line argument') + + self._args_to_config(config, argname='export', + logstring='Parameter --export detected: {} ...') + + self._args_to_config(config, argname='exportfilename', + logstring='Storing backtest results to {} ...') + + # Edge section: + if 'stoploss_range' in self.args and 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_max': txt_range[1]}) + config['edge'].update({'stoploss_range_step': txt_range[2]}) + logger.info('Parameter --stoplosses detected: %s ...', self.args.stoploss_range) + + # Hyperopt section + self._args_to_config(config, argname='hyperopt', + logstring='Using Hyperopt file {}') + + self._args_to_config(config, argname='hyperopt_path', + logstring='Using additional Hyperopt lookup path: {}') + + 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: {}') + + self._args_to_config(config, argname='hyperopt_continue', + logstring='Hyperopt continue: {}') + + self._args_to_config(config, argname='hyperopt_loss', + logstring='Using loss function: {}') + + def _process_plot_options(self, config: Dict[str, Any]) -> None: + + self._args_to_config(config, argname='pairs', + logstring='Using pairs {}') + + self._args_to_config(config, argname='indicators1', + logstring='Using indicators1: {}') + + self._args_to_config(config, argname='indicators2', + logstring='Using indicators2: {}') + + self._args_to_config(config, argname='plot_limit', + logstring='Limiting plot to: {}') + self._args_to_config(config, argname='trade_source', + logstring='Using trades from: {}') + + def _process_runmode(self, config: Dict[str, Any]) -> None: + + if not self.runmode: + # Handle real mode, infer dry/live from config + self.runmode = RunMode.DRY_RUN if config.get('dry_run', True) else RunMode.LIVE + logger.info("Runmode set to {self.runmode}.") + + config.update({'runmode': self.runmode}) + + def _validate_config_consistency(self, conf: Dict[str, Any]) -> None: + """ + Validate the configuration consistency + :param conf: Config in JSON format + :return: Returns None if everything is ok, otherwise throw an OperationalException + """ + # validating trailing stoploss + self._validate_trailing_stoploss(conf) + + def _validate_trailing_stoploss(self, conf: Dict[str, Any]) -> None: + + # Skip if trailing stoploss is not activated + if not conf.get('trailing_stop', False): + return + + tsl_positive = float(conf.get('trailing_stop_positive', 0)) + tsl_offset = float(conf.get('trailing_stop_positive_offset', 0)) + tsl_only_offset = conf.get('trailing_only_offset_is_reached', False) + + if tsl_only_offset: + if tsl_positive == 0.0: + raise OperationalException( + f'The config trailing_only_offset_is_reached needs ' + 'trailing_stop_positive_offset to be more than 0 in your config.') + if tsl_positive > 0 and 0 < tsl_offset <= tsl_positive: + raise OperationalException( + f'The config trailing_stop_positive_offset needs ' + 'to be greater than trailing_stop_positive_offset in your config.') + + def _args_to_config(self, config: Dict[str, Any], argname: str, + logstring: str, logfun: Optional[Callable] = None, + deprecated_msg: Optional[str] = None) -> None: + """ + :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])) + if deprecated_msg: + warnings.warn(f"DEPRECATED: {deprecated_msg}", DeprecationWarning) diff --git a/freqtrade/configuration/create_datadir.py b/freqtrade/configuration/create_datadir.py new file mode 100644 index 000000000..acc3a29ca --- /dev/null +++ b/freqtrade/configuration/create_datadir.py @@ -0,0 +1,20 @@ +import logging +from typing import Any, Dict, Optional +from pathlib import Path + + +logger = logging.getLogger(__name__) + + +def create_datadir(config: Dict[str, Any], datadir: Optional[str] = None) -> str: + + folder = Path(datadir) if datadir else Path('user_data/data') + if not datadir: + # set datadir + exchange_name = config.get('exchange', {}).get('name').lower() + folder = folder.joinpath(exchange_name) + + if not folder.is_dir(): + folder.mkdir(parents=True) + logger.info(f'Created data directory: {datadir}') + return str(folder) diff --git a/freqtrade/configuration/json_schema.py b/freqtrade/configuration/json_schema.py new file mode 100644 index 000000000..4c6f4a4a0 --- /dev/null +++ b/freqtrade/configuration/json_schema.py @@ -0,0 +1,53 @@ +import logging +from typing import Any, Dict + +from jsonschema import Draft4Validator, validators +from jsonschema.exceptions import ValidationError, best_match + +from freqtrade import constants + + +logger = logging.getLogger(__name__) + + +def _extend_validator(validator_class): + """ + Extended validator for the Freqtrade configuration JSON Schema. + Currently it only handles defaults for subschemas. + """ + validate_properties = validator_class.VALIDATORS['properties'] + + def set_defaults(validator, properties, instance, schema): + for prop, subschema in properties.items(): + if 'default' in subschema: + instance.setdefault(prop, subschema['default']) + + for error in validate_properties( + validator, properties, instance, schema, + ): + yield error + + return validators.extend( + validator_class, {'properties': set_defaults} + ) + + +FreqtradeValidator = _extend_validator(Draft4Validator) + + +def validate_config_schema(conf: Dict[str, Any]) -> Dict[str, Any]: + """ + Validate the configuration follow the Config Schema + :param conf: Config in JSON format + :return: Returns the config if valid, otherwise throw an exception + """ + try: + FreqtradeValidator(constants.CONF_SCHEMA).validate(conf) + return conf + except ValidationError as e: + logger.critical( + f"Invalid configuration. See config.json.example. Reason: {e}" + ) + raise ValidationError( + best_match(Draft4Validator(constants.CONF_SCHEMA).iter_errors(conf)).message + ) diff --git a/freqtrade/configuration/load_config.py b/freqtrade/configuration/load_config.py new file mode 100644 index 000000000..25504144f --- /dev/null +++ b/freqtrade/configuration/load_config.py @@ -0,0 +1,30 @@ +""" +This module contain functions to load the configuration file +""" +import json +import logging +import sys +from typing import Any, Dict + +from freqtrade import OperationalException + + +logger = logging.getLogger(__name__) + + +def load_config_file(path: str) -> Dict[str, Any]: + """ + Loads a config file from the given path + :param path: path as str + :return: configuration as dictionary + """ + try: + # Read config from stdin if requested in the options + with open(path) if path != '-' else sys.stdin as file: + config = json.load(file) + except FileNotFoundError: + raise OperationalException( + f'Config file "{path}" not found!' + ' Please create a config file or check whether it exists.') + + return config diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 02062acc4..9b73adcfe 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -4,13 +4,15 @@ bot constants """ DEFAULT_CONFIG = 'config.json' +DEFAULT_EXCHANGE = 'bittrex' DYNAMIC_WHITELIST = 20 # pairs PROCESS_THROTTLE_SECS = 5 # sec -TICKER_INTERVAL = 5 # min +DEFAULT_TICKER_INTERVAL = 5 # min HYPEROPT_EPOCH = 100 # epochs RETRY_TIMEOUT = 30 # sec DEFAULT_STRATEGY = 'DefaultStrategy' DEFAULT_HYPEROPT = 'DefaultHyperOpts' +DEFAULT_HYPEROPT_LOSS = 'DefaultHyperOptLoss' DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite' DEFAULT_DB_DRYRUN_URL = 'sqlite://' UNLIMITED_STAKE_AMOUNT = 'unlimited' @@ -21,23 +23,13 @@ ORDERTYPE_POSSIBILITIES = ['limit', 'market'] ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList'] DRY_RUN_WALLET = 999.9 +DEFAULT_DOWNLOAD_TICKER_INTERVALS = '1m 5m' -TICKER_INTERVAL_MINUTES = { - '1m': 1, - '3m': 3, - '5m': 5, - '15m': 15, - '30m': 30, - '1h': 60, - '2h': 120, - '4h': 240, - '6h': 360, - '8h': 480, - '12h': 720, - '1d': 1440, - '3d': 4320, - '1w': 10080, -} +TICKER_INTERVALS = [ + '1m', '3m', '5m', '15m', '30m', + '1h', '2h', '4h', '6h', '8h', '12h', + '1d', '3d', '1w', +] SUPPORTED_FIAT = [ "AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK", @@ -52,7 +44,7 @@ CONF_SCHEMA = { 'type': 'object', 'properties': { 'max_open_trades': {'type': 'integer', 'minimum': -1}, - 'ticker_interval': {'type': 'string', 'enum': list(TICKER_INTERVAL_MINUTES.keys())}, + 'ticker_interval': {'type': 'string', 'enum': TICKER_INTERVALS}, 'stake_currency': {'type': 'string', 'enum': ['BTC', 'XBT', 'ETH', 'USDT', 'EUR', 'USD']}, 'stake_amount': { "type": ["number", "string"], @@ -167,6 +159,21 @@ CONF_SCHEMA = { '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'}, 'initial_state': {'type': 'string', 'enum': ['running', 'stopped']}, 'forcebuy_enable': {'type': 'boolean'}, @@ -184,10 +191,10 @@ CONF_SCHEMA = { 'type': 'object', 'properties': { 'name': {'type': 'string'}, - 'sandbox': {'type': 'boolean'}, - 'key': {'type': 'string'}, - 'secret': {'type': 'string'}, - 'password': {'type': 'string'}, + 'sandbox': {'type': 'boolean', 'default': False}, + 'key': {'type': 'string', 'default': ''}, + 'secret': {'type': 'string', 'default': ''}, + 'password': {'type': 'string', 'default': ''}, 'uid': {'type': 'string'}, 'pair_whitelist': { 'type': 'array', @@ -210,7 +217,7 @@ CONF_SCHEMA = { 'ccxt_config': {'type': 'object'}, 'ccxt_async_config': {'type': 'object'} }, - 'required': ['name', 'key', 'secret', 'pair_whitelist'] + 'required': ['name', 'pair_whitelist'] }, 'edge': { 'type': 'object', diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 6fce4361b..36d8aedbb 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -1,12 +1,19 @@ """ Helpers when analyzing backtest data """ +import logging from pathlib import Path +from typing import Dict import numpy as np import pandas as pd +import pytz +from freqtrade import persistence from freqtrade.misc import json_load +from freqtrade.persistence import Trade + +logger = logging.getLogger(__name__) # must align with columns in backtest.py BT_DATA_COLUMNS = ["pair", "profitperc", "open_time", "close_time", "index", "duration", @@ -17,13 +24,13 @@ def load_backtest_data(filename) -> pd.DataFrame: """ Load backtest data 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): filename = Path(filename) if not filename.is_file(): - raise ValueError("File {filename} does not exist.") + raise ValueError(f"File {filename} does not exist.") with filename.open() as file: data = json_load(file) @@ -60,8 +67,99 @@ def evaluate_result_multi(results: pd.DataFrame, freq: str, max_open_trades: int dates = pd.Series(pd.concat(dates).values, name='date') df2 = pd.DataFrame(np.repeat(results.values, deltas, axis=0), columns=results.columns) - df2 = df2.astype(dtype={"open_time": "datetime64", "close_time": "datetime64"}) df2 = pd.concat([dates, df2], axis=1) df2 = df2.set_index('date') df_final = df2.resample(freq)[['pair']].count() 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", "open_time", "close_time", "profit", "profitperc", + "open_rate", "close_rate", "amount", "duration", "sell_reason", + "fee_open", "fee_close", "open_rate_requested", "close_rate_requested", + "stake_amount", "max_rate", "min_rate", "id", "exchange", + "stop_loss", "initial_stop_loss", "strategy", "ticker_interval"] + + trades = pd.DataFrame([(t.pair, + t.open_date.replace(tzinfo=pytz.UTC), + t.close_date.replace(tzinfo=pytz.UTC) if t.close_date else None, + t.calc_profit(), t.calc_profit_percent(), + t.open_rate, t.close_rate, t.amount, + (t.close_date.timestamp() - t.open_date.timestamp() + if t.close_date else None), + t.sell_reason, + t.fee_open, t.fee_close, + t.open_rate_requested, + t.close_rate_requested, + t.stake_amount, + t.max_rate, + t.min_rate, + t.id, t.exchange, + t.stop_loss, t.initial_stop_loss, + t.strategy, t.ticker_interval + ) + for t in Trade.query.all()], + columns=columns) + + return trades + + +def load_trades(config) -> pd.DataFrame: + """ + Based on configuration option "trade_source": + * loads data from DB (using `db_url`) + * loads data from backtestfile (using `exportfilename`) + """ + if config["trade_source"] == "DB": + return load_trades_from_db(config["db_url"]) + elif config["trade_source"] == "file": + return load_backtest_data(Path(config["exportfilename"])) + + +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 + + +def combine_tickers_with_mean(tickers: Dict[str, pd.DataFrame], column: str = "close"): + """ + Combine multiple dataframes "column" + :param tickers: Dict of Dataframes, dict key should be pair. + :param column: Column in the original dataframes to use + :return: DataFrame with the column renamed to the dict key, and a column + named mean, containing the mean of all pairs. + """ + df_comb = pd.concat([tickers[pair].set_index('date').rename( + {column: pair}, axis=1)[pair] for pair in tickers], axis=1) + + df_comb['mean'] = df_comb.mean(axis=1) + + return df_comb + + +def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str) -> pd.DataFrame: + """ + Adds a column `col_name` with the cumulative profit for the given trades array. + :param df: DataFrame with date index + :param trades: DataFrame containing trades (requires columns close_time and profitperc) + :return: Returns df with one additional column, col_name, containing the cumulative profit. + """ + df[col_name] = trades.set_index('close_time')['profitperc'].cumsum() + # Set first value to 0 + df.loc[df.iloc[0].name, col_name] = 0 + # FFill to get continuous + df[col_name] = df[col_name].ffill() + return df diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index c32338bbe..b530b3bce 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -2,22 +2,25 @@ Functions to convert data from one format to another """ import logging + import pandas as pd from pandas import DataFrame, to_datetime -from freqtrade.constants import TICKER_INTERVAL_MINUTES logger = logging.getLogger(__name__) -def parse_ticker_dataframe(ticker: list, ticker_interval: str, - fill_missing: bool = True) -> DataFrame: +def parse_ticker_dataframe(ticker: list, ticker_interval: str, pair: str, *, + fill_missing: bool = True, + drop_incomplete: bool = True) -> 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_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 (see ohlcv_fill_up_missing_data for details) + :param drop_incomplete: Drop the last candle of the dataframe, assuming it's incomplete :return: DataFrame """ logger.debug("Parsing tickerlist to dataframe") @@ -43,21 +46,25 @@ def parse_ticker_dataframe(ticker: list, ticker_interval: str, 'close': 'last', 'volume': 'max', }) - frame.drop(frame.tail(1).index, inplace=True) # eliminate partial candle - logger.debug('Dropping last candle') + # eliminate partial candle + if drop_incomplete: + frame.drop(frame.tail(1).index, inplace=True) + logger.debug('Dropping last candle') if fill_missing: - return ohlcv_fill_up_missing_data(frame, ticker_interval) + return ohlcv_fill_up_missing_data(frame, ticker_interval, pair) else: 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, 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 = { 'open': 'first', 'high': 'max', @@ -65,9 +72,9 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, ticker_interval: str) -> Da 'close': 'last', 'volume': 'sum' } - tick_mins = TICKER_INTERVAL_MINUTES[ticker_interval] + ticker_minutes = timeframe_to_minutes(ticker_interval) # Resample to create "NAN" values - df = dataframe.resample(f'{tick_mins}min', on='date').agg(ohlc_dict) + df = dataframe.resample(f'{ticker_minutes}min', on='date').agg(ohlc_dict) # Forwardfill close for missing columns df['close'] = df['close'].fillna(method='ffill') @@ -78,7 +85,10 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, ticker_interval: str) -> Da 'low': df['close'], }) 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 diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 375b8bf5b..b87589df7 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -17,7 +17,7 @@ from freqtrade.state import RunMode logger = logging.getLogger(__name__) -class DataProvider(object): +class DataProvider(): def __init__(self, config: dict, exchange: Exchange) -> None: self._config = config @@ -37,23 +37,23 @@ class DataProvider(object): @property def available_pairs(self) -> List[Tuple[str, str]]: """ - Return a list of tuples containing pair, tick_interval for which data is currently cached. + Return a list of tuples containing pair, ticker_interval for which data is currently cached. Should be whitelist + open trades. """ return list(self._exchange._klines.keys()) - def ohlcv(self, pair: str, tick_interval: str = None, copy: bool = True) -> DataFrame: + def ohlcv(self, pair: str, ticker_interval: str = None, copy: bool = True) -> DataFrame: """ get ohlcv data for the given pair as DataFrame Please check `available_pairs` to verify which pairs are currently cached. :param pair: pair to get the data for - :param tick_interval: ticker_interval to get pair for + :param ticker_interval: ticker_interval to get pair for :param copy: copy dataframe before returning. Use false only for RO operations (where the dataframe is not modified) """ if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE): - if tick_interval: - pairtick = (pair, tick_interval) + if ticker_interval: + pairtick = (pair, ticker_interval) else: pairtick = (pair, self._config['ticker_interval']) @@ -65,7 +65,7 @@ class DataProvider(object): """ get stored historic ohlcv data :param pair: pair to get the data for - :param tick_interval: ticker_interval to get pair for + :param ticker_interval: ticker_interval to get pair for """ return load_pair_history(pair=pair, ticker_interval=ticker_interval, @@ -81,12 +81,14 @@ class DataProvider(object): # TODO: Implement me pass - def orderbook(self, pair: str, max: int): + def orderbook(self, pair: str, maximum: int): """ return latest orderbook data + :param pair: pair to get the data for + :param maximum: Maximum number of orderbook entries to query + :return: dict including bids/asks with a total of `maximum` entries. """ - # TODO: Implement me - pass + return self._exchange.get_order_book(pair, maximum) @property def runmode(self) -> RunMode: diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 7d89f7ad6..f600615df 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -1,21 +1,24 @@ """ Handle historic data (ohlcv). -includes: + +Includes: * load data for a pair (or a list of pairs) from disk * download data from exchange and store to disk """ import logging +import operator +from datetime import datetime from pathlib import Path -from typing import Optional, List, Dict, Tuple, Any +from typing import Any, Dict, List, Optional, Tuple import arrow from pandas import DataFrame -from freqtrade import misc, constants, OperationalException +from freqtrade import OperationalException, misc +from freqtrade.configuration import TimeRange from freqtrade.data.converter import parse_ticker_dataframe -from freqtrade.exchange import Exchange -from freqtrade.arguments import TimeRange +from freqtrade.exchange import Exchange, timeframe_to_minutes logger = logging.getLogger(__name__) @@ -60,14 +63,10 @@ def load_tickerdata_file( timerange: Optional[TimeRange] = None) -> Optional[list]: """ 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) - pair_s = pair.replace('/', '_') - file = path.joinpath(f'{pair_s}-{ticker_interval}.json') - - pairdata = misc.file_load_json(file) - + filename = pair_data_filename(datadir, pair, ticker_interval) + pairdata = misc.file_load_json(filename) if not pairdata: return None @@ -82,24 +81,29 @@ def load_pair_history(pair: str, timerange: TimeRange = TimeRange(None, None, 0, 0), refresh_pairs: bool = False, exchange: Optional[Exchange] = None, - fill_up_missing: bool = True + fill_up_missing: bool = True, + drop_incomplete: bool = True ) -> DataFrame: """ 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 """ - # If the user force the refresh of pairs + # The user forced the refresh of 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, exchange=exchange, pair=pair, - tick_interval=ticker_interval, + ticker_interval=ticker_interval, timerange=timerange) pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange) @@ -112,11 +116,15 @@ def load_pair_history(pair: str, logger.warning('Missing data at end for pair %s, data ends at %s', pair, 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: - logger.warning('No data for pair: "%s", Interval: %s. ' - 'Use --refresh-pairs-cached to download the data', - pair, ticker_interval) + logger.warning( + f'No history data for pair: "{pair}", interval: {ticker_interval}. ' + 'Use --refresh-pairs-cached option or download_backtest_data.py ' + 'script to download the data' + ) return None @@ -126,21 +134,34 @@ def load_data(datadir: Optional[Path], refresh_pairs: bool = False, exchange: Optional[Exchange] = None, 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 :return: dict(:) """ - 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: - hist = load_pair_history(pair=pair, ticker_interval=ticker_interval, - datadir=datadir, timerange=timerange, - refresh_pairs=refresh_pairs, - exchange=exchange, - fill_up_missing=fill_up_missing) - if hist is not None: - result[pair] = hist + for pair in pairs: + hist = load_pair_history(pair=pair, ticker_interval=ticker_interval, + datadir=datadir, timerange=timerange, + refresh_pairs=refresh_pairs, + exchange=exchange, + fill_up_missing=fill_up_missing) + if hist is not None: + result[pair] = hist return result @@ -149,7 +170,14 @@ def make_testdata_path(datadir: Optional[Path]) -> Path: return datadir or (Path(__file__).parent.parent / "tests" / "testdata").resolve() -def load_cached_data_for_updating(filename: Path, tick_interval: str, +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, timerange: Optional[TimeRange]) -> Tuple[List[Any], Optional[int]]: """ @@ -163,7 +191,7 @@ def load_cached_data_for_updating(filename: Path, tick_interval: str, if timerange.starttype == 'date': since_ms = timerange.startts * 1000 elif timerange.stoptype == 'line': - num_minutes = timerange.stopts * constants.TICKER_INTERVAL_MINUTES[tick_interval] + num_minutes = timerange.stopts * timeframe_to_minutes(ticker_interval) since_ms = arrow.utcnow().shift(minutes=num_minutes).timestamp * 1000 # read the cached file @@ -188,9 +216,9 @@ def load_cached_data_for_updating(filename: Path, tick_interval: str, def download_pair_history(datadir: Optional[Path], - exchange: Exchange, + exchange: Optional[Exchange], pair: str, - tick_interval: str = '5m', + ticker_interval: str = '5m', timerange: Optional[TimeRange] = None) -> bool: """ Download the latest ticker intervals from the exchange for the pair passed in parameters @@ -199,26 +227,32 @@ def download_pair_history(datadir: Optional[Path], the full data will be redownloaded Based on @Rybolov work: https://github.com/rybolov/freqtrade-data + :param pair: pair to download - :param tick_interval: ticker interval + :param ticker_interval: ticker interval :param timerange: range of time to download :return: bool with success state - """ + if not exchange: + raise OperationalException( + "Exchange needs to be initialized when downloading pair history data" + ) + try: - path = make_testdata_path(datadir) - filepair = pair.replace("/", "_") - filename = path.joinpath(f'{filepair}-{tick_interval}.json') + filename = pair_data_filename(datadir, pair, ticker_interval) - logger.info('Download the pair: "%s", Interval: %s', pair, tick_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, tick_interval, timerange) + data, since_ms = load_cached_data_for_updating(filename, ticker_interval, timerange) logger.debug("Current Start: %s", misc.format_ms_time(data[1][0]) if data else 'None') logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None') # Default since_ms to 30 days if nothing is given - new_data = exchange.get_history(pair=pair, tick_interval=tick_interval, + new_data = exchange.get_history(pair=pair, ticker_interval=ticker_interval, since_ms=since_ms if since_ms else int(arrow.utcnow().shift(days=-30).float_timestamp) * 1000) @@ -229,7 +263,46 @@ def download_pair_history(datadir: Optional[Path], misc.file_dump_json(filename, data) return True - except BaseException: - logger.info('Failed to download the pair: "%s", Interval: %s', - pair, tick_interval) + + except Exception as e: + logger.error( + f'Failed to download history data for pair: "{pair}", interval: {ticker_interval}. ' + f'Error: {e}' + ) 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 diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index b4dfa5624..7085663d6 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -10,10 +10,8 @@ import utils_find_1st as utf1st from pandas import DataFrame from freqtrade import constants, OperationalException -from freqtrade.arguments import Arguments -from freqtrade.arguments import TimeRange +from freqtrade.configuration import Arguments, TimeRange from freqtrade.data import history -from freqtrade.optimize import get_timeframe from freqtrade.strategy.interface import SellType @@ -47,11 +45,6 @@ class Edge(): self.config = config self.exchange = exchange 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._cached_pairs: Dict[str, Any] = {} # Keeps a list of pairs @@ -102,7 +95,7 @@ class Edge(): data = history.load_data( datadir=Path(self.config['datadir']) if self.config.get('datadir') else None, pairs=pairs, - ticker_interval=self.ticker_interval, + ticker_interval=self.strategy.ticker_interval, refresh_pairs=self._refresh_pairs, exchange=self.exchange, timerange=self._timerange @@ -114,10 +107,10 @@ class Edge(): logger.critical("No data found. Edge is stopped ...") return False - preprocessed = self.tickerdata_to_dataframe(data) + preprocessed = self.strategy.tickerdata_to_dataframe(data) # Print timeframe - min_date, max_date = self.get_timeframe(preprocessed) + min_date, max_date = history.get_timeframe(preprocessed) logger.info( 'Measuring data from %s up to %s (%s days) ...', min_date.isoformat(), @@ -132,13 +125,14 @@ class Edge(): pair_data = pair_data.sort_values(by=['date']) pair_data = pair_data.reset_index(drop=True) - ticker_data = self.advise_sell( - self.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy() + ticker_data = self.strategy.advise_sell( + 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) # If no trade found then exit if len(trades) == 0: + logger.info("No trades found.") return False # Fill missing, calculable columns, profit, duration , abs etc. @@ -203,6 +197,22 @@ class Edge(): return self._final_pairs + def accepted_pairs(self) -> list: + """ + return a list of accepted pairs along with their winrate, expectancy and stoploss + """ + final = [] + for pair, info in self._cached_pairs.items(): + if info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2)) and \ + info.winrate > float(self.edge_config.get('minimum_winrate', 0.60)): + final.append({ + 'Pair': pair, + 'Winrate': info.winrate, + 'Expectancy': info.expectancy, + 'Stoploss': info.stoploss, + }) + return final + def _fill_calculable_fields(self, result: DataFrame) -> DataFrame: """ The result frame contains a number of columns that are calculable diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index f6db04da6..5c58320f6 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -1,3 +1,10 @@ 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.binance import Binance # noqa: F401 diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 127f4e916..18e754e3f 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -11,6 +11,7 @@ class Binance(Exchange): _ft_has: Dict = { "stoploss_on_exchange": True, + "order_time_in_force": ['gtc', 'fok', 'ioc'], } def get_order_book(self, pair: str, limit: int = 100) -> dict: diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ab9e29722..657f382d8 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1,33 +1,32 @@ # pragma pylint: disable=W0603 -""" Cryptocurrency Exchanges support """ -import logging +""" +Cryptocurrency Exchanges support +""" +import asyncio import inspect -from random import randint -from typing import List, Dict, Tuple, Any, Optional +import logging +from copy import deepcopy 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 asyncio import ccxt import ccxt.async_support as ccxt_async from pandas import DataFrame -from freqtrade import constants, OperationalException, DependencyException, TemporaryError +from freqtrade import (DependencyException, InvalidOrderException, + OperationalException, TemporaryError, constants) from freqtrade.data.converter import parse_ticker_dataframe +from freqtrade.misc import deep_merge_dicts logger = logging.getLogger(__name__) + API_RETRY_COUNT = 4 -# Urls to exchange markets, insert quote and base with .format() -_EXCHANGE_URLS = { - ccxt.bittrex.__name__: '/Market/Index?MarketName={quote}-{base}', - ccxt.binance.__name__: '/tradeDetail.html?symbol={base}_{quote}', -} - - def retrier_async(f): async def wrapper(*args, **kwargs): count = kwargs.pop('count', API_RETRY_COUNT) @@ -70,11 +69,15 @@ class Exchange(object): _params: Dict = {} # Dict to specify which options each exchange implements - # TODO: this should be merged with attributes from subclasses - # To avoid having to copy/paste this to all subclasses. - _ft_has = { + # This defines defaults, which can be selectively overridden by subclasses using _ft_has + # or by specifying them in the configuration. + _ft_has_default: Dict = { "stoploss_on_exchange": False, + "order_time_in_force": ["gtc"], + "ohlcv_candle_limit": 500, + "ohlcv_partial_candle": True, } + _ft_has: Dict = {} def __init__(self, config: dict) -> None: """ @@ -82,6 +85,9 @@ class Exchange(object): it does basic validation whether the specified exchange and pairs are valid. :return: None """ + self._api: ccxt.Exchange = None + self._api_async: ccxt_async.Exchange = None + self._config.update(config) self._cached_ticker: Dict[str, Any] = {} @@ -101,9 +107,22 @@ class Exchange(object): logger.info('Instance is running with dry_run enabled') exchange_config = config['exchange'] - self._api: ccxt.Exchange = self._init_ccxt( + + # 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 = self._init_ccxt( exchange_config, ccxt_kwargs=exchange_config.get('ccxt_config')) - self._api_async: ccxt_async.Exchange = self._init_ccxt( + self._api_async = self._init_ccxt( exchange_config, ccxt_async, ccxt_kwargs=exchange_config.get('ccxt_async_config')) logger.info('Using Exchange "%s"', self.name) @@ -140,15 +159,14 @@ class Exchange(object): # Find matching class for the given exchange name name = exchange_config['name'] - if name not in ccxt_module.exchanges: - raise OperationalException(f'Exchange {name} is not supported') + if not is_exchange_available(name, ccxt_module): + raise OperationalException(f'Exchange {name} is not supported by ccxt') ex_config = { 'apiKey': exchange_config.get('key'), 'secret': exchange_config.get('secret'), 'password': exchange_config.get('password'), 'uid': exchange_config.get('uid', ''), - 'enableRateLimit': exchange_config.get('ccxt_rate_limit', True) } if ccxt_kwargs: logger.info('Applying additional ccxt config: %s', ccxt_kwargs) @@ -156,8 +174,10 @@ class Exchange(object): try: api = getattr(ccxt_module, name.lower())(ex_config) - except (KeyError, AttributeError): - raise OperationalException(f'Exchange {name} is not supported') + except (KeyError, AttributeError) as e: + raise OperationalException(f'Exchange {name} is not supported') from e + except ccxt.BaseError as e: + raise OperationalException(f"Initialization of ccxt failed. Reason: {e}") from e self.set_sandbox(api, exchange_config, name) @@ -224,8 +244,11 @@ class Exchange(object): > arrow.utcnow().timestamp): return None logger.debug("Performing scheduled market reload..") - self._api.load_markets(reload=True) - self._last_markets_refresh = arrow.utcnow().timestamp + try: + 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: """ @@ -237,24 +260,44 @@ class Exchange(object): if not self.markets: logger.warning('Unable to validate pairs (assuming they are correct).') - # return + return - stake_cur = self._config['stake_currency'] for pair in pairs: # Note: ccxt has BaseCurrency/QuoteCurrency format for pairs # TODO: add a support for having coins in BTC/USDT format - if not pair.endswith(stake_cur): - raise OperationalException( - f'Pair {pair} not compatible with stake_currency: {stake_cur}') if self.markets and pair not in self.markets: raise OperationalException( f'Pair {pair} is not available on {self.name}. ' f'Please remove {pair} from your whitelist.') + elif self.markets[pair].get('info', {}).get('IsRestricted', False): + # Warn users about restricted pairs in whitelist. + # We cannot determine reliably if Users are affected. + logger.warning(f"Pair {pair} is restricted for some users on this exchange." + f"Please check if you are impacted by this restriction " + f"on the exchange and eventually remove {pair} from your whitelist.") + + def get_valid_pair_combination(self, curr_1, curr_2) -> str: + """ + Get valid pair combination of curr_1 and curr_2 by trying both combinations. + """ + for pair in [f"{curr_1}/{curr_2}", f"{curr_2}/{curr_1}"]: + if pair in self.markets and self.markets[pair].get('active'): + return pair + raise DependencyException(f"Could not combine {curr_1} and {curr_2} to get a valid pair.") def validate_timeframes(self, timeframe: List[str]) -> None: """ Checks if ticker interval from config is a supported timeframe on the exchange """ + if not hasattr(self._api, "timeframes") or self._api.timeframes is None: + # If timeframes attribute is missing (or is None), the exchange probably + # has no fetchOHLCV method. + # Therefore we also show that. + raise OperationalException( + f"The ccxt library does not provide the list of timeframes " + f"for the exchange \"{self.name}\" and this exchange " + f"is therefore not supported. ccxt fetchOHLCV: {self.exchange_has('fetchOHLCV')}") + timeframes = self._api.timeframes if timeframe not in timeframes: raise OperationalException( @@ -279,10 +322,10 @@ class Exchange(object): """ Checks if order time in force configured in strategy/config are supported """ - if any(v != 'gtc' for k, v in order_time_in_force.items()): - if self.name != 'Binance': - raise OperationalException( - f'Time in force policies are not supporetd for {self.name} yet.') + if any(v not in self._ft_has["order_time_in_force"] + for k, v in order_time_in_force.items()): + raise OperationalException( + f'Time in force policies are not supported for {self.name} yet.') def exchange_has(self, endpoint: str) -> bool: """ @@ -350,7 +393,9 @@ class Exchange(object): try: # Set the precision for amount and price(rate) as accepted by the exchange amount = self.symbol_amount_prec(pair, amount) - rate = self.symbol_price_prec(pair, rate) if ordertype != 'market' else None + needs_price = (ordertype != 'market' + or self._api.options.get("createMarketBuyOrderRequiresPrice", False)) + rate = self.symbol_price_prec(pair, rate) if needs_price else None return self._api.create_order(pair, ordertype, side, amount, rate, params) @@ -358,18 +403,18 @@ class Exchange(object): except ccxt.InsufficientFunds as e: raise DependencyException( f'Insufficient funds to create {ordertype} {side} order on market {pair}.' - f'Tried to {side} amount {amount} at rate {rate} (total {rate*amount}).' - f'Message: {e}') + f'Tried to {side} amount {amount} at rate {rate} (total {rate * amount}).' + f'Message: {e}') from e except ccxt.InvalidOrder as e: raise DependencyException( f'Could not create {ordertype} {side} order on market {pair}.' - f'Tried to {side} amount {amount} at rate {rate} (total {rate*amount}).' - f'Message: {e}') + f'Tried to {side} amount {amount} at rate {rate} (total {rate * amount}).' + f'Message: {e}') from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') + f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: - raise OperationalException(e) + raise OperationalException(e) from e def buy(self, pair: str, ordertype: str, amount: float, rate: float, time_in_force) -> Dict: @@ -454,9 +499,9 @@ class Exchange(object): return balances except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not get balance due to {e.__class__.__name__}. Message: {e}') + f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: - raise OperationalException(e) + raise OperationalException(e) from e @retrier def get_tickers(self) -> Dict: @@ -465,18 +510,18 @@ class Exchange(object): except ccxt.NotSupported as e: raise OperationalException( f'Exchange {self._api.name} does not support fetching tickers in batch.' - f'Message: {e}') + f'Message: {e}') from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not load tickers due to {e.__class__.__name__}. Message: {e}') + f'Could not load tickers due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: - raise OperationalException(e) + raise OperationalException(e) from e @retrier def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict: if refresh or pair not in self._cached_ticker.keys(): try: - if pair not in self._api.markets: + if pair not in self._api.markets or not self._api.markets[pair].get('active'): raise DependencyException(f"Pair {pair} not available") data = self._api.fetch_ticker(pair) try: @@ -489,33 +534,35 @@ class Exchange(object): return data except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not load ticker due to {e.__class__.__name__}. Message: {e}') + f'Could not load ticker due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: - raise OperationalException(e) + raise OperationalException(e) from e else: logger.info("returning cached ticker-data for %s", pair) return self._cached_ticker[pair] - def get_history(self, pair: str, tick_interval: str, + def get_history(self, pair: str, ticker_interval: str, since_ms: int) -> List: """ Gets candle history using asyncio and returns the list of candles. Handles all async doing. """ return asyncio.get_event_loop().run_until_complete( - self._async_get_history(pair=pair, tick_interval=tick_interval, + self._async_get_history(pair=pair, ticker_interval=ticker_interval, since_ms=since_ms)) async def _async_get_history(self, pair: str, - tick_interval: str, + ticker_interval: str, since_ms: int) -> List: - # Assume exchange returns 500 candles - _LIMIT = 500 - one_call = constants.TICKER_INTERVAL_MINUTES[tick_interval] * 60 * _LIMIT * 1000 - logger.debug("one_call: %s", one_call) + one_call = timeframe_to_msecs(ticker_interval) * self._ohlcv_candle_limit + 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( - pair, tick_interval, since) for since in + pair, ticker_interval, since) for since in range(since_ms, arrow.utcnow().timestamp * 1000, one_call)] tickers = await asyncio.gather(*input_coroutines, return_exceptions=True) @@ -544,7 +591,10 @@ class Exchange(object): or self._now_is_time_to_refresh(pair, ticker_interval)): input_coroutines.append(self._async_get_candle_history(pair, ticker_interval)) 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( asyncio.gather(*input_coroutines, return_exceptions=True)) @@ -555,35 +605,40 @@ class Exchange(object): logger.warning("Async code raised an exception: %s", res.__class__.__name__) continue pair = res[0] - tick_interval = res[1] + ticker_interval = res[1] ticks = res[2] # keeping last candle time as last refreshed time of the pair if ticks: - self._pairs_last_refresh_time[(pair, tick_interval)] = ticks[-1][0] // 1000 + self._pairs_last_refresh_time[(pair, ticker_interval)] = ticks[-1][0] // 1000 # keeping parsed dataframe in cache - self._klines[(pair, tick_interval)] = parse_ticker_dataframe( - ticks, tick_interval, fill_missing=True) + self._klines[(pair, ticker_interval)] = parse_ticker_dataframe( + ticks, ticker_interval, pair=pair, fill_missing=True, + drop_incomplete=self._ohlcv_partial_candle) return tickers def _now_is_time_to_refresh(self, pair: str, ticker_interval: str) -> bool: # Calculating ticker interval in seconds - interval_in_sec = constants.TICKER_INTERVAL_MINUTES[ticker_interval] * 60 + interval_in_sec = timeframe_to_seconds(ticker_interval) return not ((self._pairs_last_refresh_time.get((pair, ticker_interval), 0) + interval_in_sec) >= arrow.utcnow().timestamp) @retrier_async - async def _async_get_candle_history(self, pair: str, tick_interval: str, + async def _async_get_candle_history(self, pair: str, ticker_interval: str, since_ms: Optional[int] = None) -> Tuple[str, str, List]: """ Asyncronously gets candle histories using fetch_ohlcv - returns tuple: (pair, tick_interval, ohlcv_list) + returns tuple: (pair, ticker_interval, ohlcv_list) """ try: # fetch ohlcv asynchronously - logger.debug("fetching %s, %s since %s ...", pair, tick_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=tick_interval, + data = await self._api_async.fetch_ohlcv(pair, timeframe=ticker_interval, since=since_ms) # Because some exchange sort Tickers ASC and other DESC. @@ -595,19 +650,19 @@ class Exchange(object): data = sorted(data, key=lambda x: x[0]) except IndexError: logger.exception("Error loading %s. Result was %s.", pair, data) - return pair, tick_interval, [] - logger.debug("done fetching %s, %s ...", pair, tick_interval) - return pair, tick_interval, data + return pair, ticker_interval, [] + logger.debug("Done fetching pair %s, interval %s ...", pair, ticker_interval) + return pair, ticker_interval, data except ccxt.NotSupported as e: raise OperationalException( f'Exchange {self._api.name} does not support fetching historical candlestick data.' - f'Message: {e}') + f'Message: {e}') from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not load ticker history due to {e.__class__.__name__}. Message: {e}') + raise TemporaryError(f'Could not load ticker history due to {e.__class__.__name__}. ' + f'Message: {e}') from e except ccxt.BaseError as e: - raise OperationalException(f'Could not fetch ticker data. Msg: {e}') + raise OperationalException(f'Could not fetch ticker data. Msg: {e}') from e @retrier def cancel_order(self, order_id: str, pair: str) -> None: @@ -617,13 +672,13 @@ class Exchange(object): try: return self._api.cancel_order(order_id, pair) except ccxt.InvalidOrder as e: - raise DependencyException( - f'Could not cancel order. Message: {e}') + raise InvalidOrderException( + f'Could not cancel order. Message: {e}') from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') + f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: - raise OperationalException(e) + raise OperationalException(e) from e @retrier def get_order(self, order_id: str, pair: str) -> Dict: @@ -633,13 +688,13 @@ class Exchange(object): try: return self._api.fetch_order(order_id, pair) except ccxt.InvalidOrder as e: - raise DependencyException( - f'Could not get order. Message: {e}') + raise InvalidOrderException( + f'Tried to get an invalid order (id: {order_id}). Message: {e}') from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not get order due to {e.__class__.__name__}. Message: {e}') + f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: - raise OperationalException(e) + raise OperationalException(e) from e @retrier def get_order_book(self, pair: str, limit: int = 100) -> dict: @@ -655,12 +710,12 @@ class Exchange(object): except ccxt.NotSupported as e: raise OperationalException( f'Exchange {self._api.name} does not support fetching order book.' - f'Message: {e}') + f'Message: {e}') from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not get order book due to {e.__class__.__name__}. Message: {e}') + f'Could not get order book due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: - raise OperationalException(e) + raise OperationalException(e) from e @retrier def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List: @@ -670,16 +725,17 @@ class Exchange(object): return [] try: # Allow 5s offset to catch slight time offsets (discovered in #1185) - my_trades = self._api.fetch_my_trades(pair, since.timestamp() - 5) + # since needs to be int in milliseconds + my_trades = self._api.fetch_my_trades(pair, int((since.timestamp() - 5) * 1000)) matched_trades = [trade for trade in my_trades if trade['order'] == order_id] return matched_trades except ccxt.NetworkError as e: raise TemporaryError( - f'Could not get trades due to networking error. Message: {e}') + f'Could not get trades due to networking error. Message: {e}') from e except ccxt.BaseError as e: - raise OperationalException(e) + raise OperationalException(e) from e @retrier def get_fee(self, symbol='ETH/BTC', type='', side='', amount=1, @@ -693,6 +749,45 @@ class Exchange(object): price=price, takerOrMaker=taker_or_maker)['rate'] except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - 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}') from e except ccxt.BaseError as e: - raise OperationalException(e) + raise OperationalException(e) from e + + +def is_exchange_bad(exchange: str) -> bool: + return exchange in ['bitmex', 'bitstamp'] + + +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 diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 6f1fb2c99..d52165e0a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -4,20 +4,19 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade() import copy import logging -import time import traceback from datetime import datetime -from typing import Any, Callable, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple import arrow from requests.exceptions import RequestException -import sdnotify -from freqtrade import (DependencyException, OperationalException, - TemporaryError, __version__, constants, persistence) +from freqtrade import (DependencyException, OperationalException, InvalidOrderException, + __version__, constants, persistence) from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge +from freqtrade.exchange import timeframe_to_minutes from freqtrade.persistence import Trade from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.resolvers import ExchangeResolver, StrategyResolver, PairListResolver @@ -42,26 +41,19 @@ class FreqtradeBot(object): to get the config dict. """ - logger.info( - 'Starting freqtrade %s', - __version__, - ) + logger.info('Starting freqtrade %s', __version__) - # Init bot states + # Init bot state self.state = State.STOPPED # Init objects self.config = config - self._sd_notify = sdnotify.SystemdNotifier() if \ - self.config.get('internals', {}).get('sd_notify', False) else None - self.strategy: IStrategy = StrategyResolver(self.config).strategy self.rpc: RPCManager = RPCManager(self) - exchange_name = self.config.get('exchange', {}).get('name', 'bittrex').title() - self.exchange = ExchangeResolver(exchange_name, self.config).exchange + self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange self.wallets = Wallets(self.config, self.exchange) self.dataprovider = DataProvider(self.config, self.exchange) @@ -79,29 +71,13 @@ class FreqtradeBot(object): self.config.get('edge', {}).get('enabled', False) else None self.active_pair_whitelist: List[str] = self.config['exchange']['pair_whitelist'] - self._init_modules() - # Tell the systemd that we completed initialization phase - if self._sd_notify: - logger.debug("sd_notify: READY=1") - self._sd_notify.notify("READY=1") + persistence.init(self.config.get('db_url', None), + clean_open_orders=self.config.get('dry_run', False)) - def _init_modules(self) -> None: - """ - Initializes all modules and updates the config - :return: None - """ - # Initialize all modules - - persistence.init(self.config) - - # Set initial application state + # Set initial bot state from config initial_state = self.config.get('initial_state') - - if initial_state: - self.state = State[initial_state.upper()] - else: - self.state = State.STOPPED + self.state = State[initial_state.upper()] if initial_state else State.STOPPED def cleanup(self) -> None: """ @@ -113,130 +89,60 @@ class FreqtradeBot(object): self.rpc.cleanup() persistence.cleanup() - def stopping(self) -> None: - # Tell systemd that we are exiting now - if self._sd_notify: - logger.debug("sd_notify: STOPPING=1") - self._sd_notify.notify("STOPPING=1") - - def reconfigure(self) -> None: - # Tell systemd that we initiated reconfiguring - if self._sd_notify: - logger.debug("sd_notify: RELOADING=1") - self._sd_notify.notify("RELOADING=1") - - def worker(self, old_state: State = None) -> State: + def startup(self) -> None: """ - Trading routine that must be run at each loop - :param old_state: the previous service state from the previous call - :return: current service state + Called on startup and after reloading the bot - triggers notifications and + performs startup tasks """ - # Log state transition - state = self.state - if state != old_state: - self.rpc.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, - 'status': f'{state.name.lower()}' - }) - logger.info('Changing state to: %s', state.name) - if state == State.RUNNING: - self.rpc.startup_messages(self.config, self.pairlists) + self.rpc.startup_messages(self.config, self.pairlists) + if not self.edge: + # Adjust stoploss if it was changed + Trade.stoploss_reinitialization(self.strategy.stoploss) - throttle_secs = self.config.get('internals', {}).get( - 'process_throttle_secs', - constants.PROCESS_THROTTLE_SECS - ) - - if state == State.STOPPED: - # Ping systemd watchdog before sleeping in the stopped state - if self._sd_notify: - logger.debug("sd_notify: WATCHDOG=1\\nSTATUS=State: STOPPED.") - self._sd_notify.notify("WATCHDOG=1\nSTATUS=State: STOPPED.") - - time.sleep(throttle_secs) - - elif state == State.RUNNING: - # Ping systemd watchdog before throttling - if self._sd_notify: - logger.debug("sd_notify: WATCHDOG=1\\nSTATUS=State: RUNNING.") - self._sd_notify.notify("WATCHDOG=1\nSTATUS=State: RUNNING.") - - self._throttle(func=self._process, min_secs=throttle_secs) - - return state - - def _throttle(self, func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any: - """ - Throttles the given callable that it - takes at least `min_secs` to finish execution. - :param func: Any callable - :param min_secs: minimum execution time in seconds - :return: Any - """ - start = time.time() - result = func(*args, **kwargs) - end = time.time() - duration = max(min_secs - (end - start), 0.0) - logger.debug('Throttling %s for %.2f seconds', func.__name__, duration) - time.sleep(duration) - return result - - def _process(self) -> bool: + def process(self) -> bool: """ Queries the persistence layer for open trades and handles them, otherwise a new trade is created. :return: True if one or more trades has been created or closed, False otherwise """ state_changed = False - try: - # Check whether markets have to be reloaded - self.exchange._reload_markets() - # Refresh whitelist - self.pairlists.refresh_pairlist() - self.active_pair_whitelist = self.pairlists.whitelist + # Check whether markets have to be reloaded + self.exchange._reload_markets() - # Calculating Edge positioning - if self.edge: - self.edge.calculate() - self.active_pair_whitelist = self.edge.adjust(self.active_pair_whitelist) + # Refresh whitelist + self.pairlists.refresh_pairlist() + self.active_pair_whitelist = self.pairlists.whitelist - # Query trades from persistence layer - trades = Trade.get_open_trades() + # Calculating Edge positioning + if self.edge: + self.edge.calculate() + self.active_pair_whitelist = self.edge.adjust(self.active_pair_whitelist) - # Extend active-pair whitelist with pairs from open trades - # It ensures that tickers are downloaded for open trades - self._extend_whitelist_with_trades(self.active_pair_whitelist, trades) + # Query trades from persistence layer + trades = Trade.get_open_trades() - # Refreshing candles - self.dataprovider.refresh(self._create_pair_whitelist(self.active_pair_whitelist), - self.strategy.informative_pairs()) + # Extend active-pair whitelist with pairs from open trades + # It ensures that tickers are downloaded for open trades + self._extend_whitelist_with_trades(self.active_pair_whitelist, trades) - # First process current opened trades - for trade in trades: - state_changed |= self.process_maybe_execute_sell(trade) + # Refreshing candles + self.dataprovider.refresh(self._create_pair_whitelist(self.active_pair_whitelist), + self.strategy.informative_pairs()) - # Then looking for buy opportunities - if len(trades) < self.config['max_open_trades']: - state_changed = self.process_maybe_execute_buy() + # First process current opened trades + for trade in trades: + state_changed |= self.process_maybe_execute_sell(trade) - if 'unfilledtimeout' in self.config: - # Check and handle any timed out open orders - self.check_handle_timedout() - Trade.session.flush() + # Then looking for buy opportunities + if len(trades) < self.config['max_open_trades']: + state_changed = self.process_maybe_execute_buy() + + if 'unfilledtimeout' in self.config: + # Check and handle any timed out open orders + self.check_handle_timedout() + Trade.session.flush() - except TemporaryError as error: - logger.warning(f"Error: {error}, retrying in {constants.RETRY_TIMEOUT} seconds...") - time.sleep(constants.RETRY_TIMEOUT) - except OperationalException: - tb = traceback.format_exc() - hint = 'Issue `/start` if you think it is safe to restart.' - self.rpc.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, - 'status': f'OperationalException:\n```\n{tb}```{hint}' - }) - logger.exception('OperationalException. Stopping trader ...') - self.state = State.STOPPED return state_changed def _extend_whitelist_with_trades(self, whitelist: List[str], trades: List[Any]): @@ -298,19 +204,19 @@ class FreqtradeBot(object): else: 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: open_trades = len(Trade.get_open_trades()) if open_trades >= self.config['max_open_trades']: logger.warning('Can\'t open a new trade: max number of trades is reached') 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 - if avaliable_amount < stake_amount: + if available_amount < stake_amount: 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']})" ) @@ -356,6 +262,10 @@ class FreqtradeBot(object): interval = self.strategy.ticker_interval whitelist = copy.deepcopy(self.active_pair_whitelist) + if not whitelist: + logger.warning("Whitelist is empty.") + return False + # Remove currently opened and latest pairs from whitelist for trade in Trade.get_open_trades(): if trade.pair in whitelist: @@ -363,7 +273,8 @@ class FreqtradeBot(object): logger.debug('Ignoring %s in pair whitelist', trade.pair) if not whitelist: - raise DependencyException('No currency pairs in whitelist') + logger.info("No currency pair in whitelist, but checking to sell open trades.") + return False # running get_signal on historical data fetched for _pair in whitelist: @@ -433,8 +344,8 @@ class FreqtradeBot(object): return False amount = stake_amount / buy_limit_requested - - order = self.exchange.buy(pair=pair, ordertype=self.strategy.order_types['buy'], + order_type = self.strategy.order_types['buy'] + order = self.exchange.buy(pair=pair, ordertype=order_type, amount=amount, rate=buy_limit_requested, time_in_force=time_in_force) order_id = order['id'] @@ -444,7 +355,6 @@ class FreqtradeBot(object): buy_limit_filled_price = buy_limit_requested if order_status == 'expired' or order_status == 'rejected': - order_type = self.strategy.order_types['buy'] order_tif = self.strategy.order_time_in_force['buy'] # return false if the order is not filled @@ -472,13 +382,13 @@ class FreqtradeBot(object): stake_amount = order['cost'] amount = order['amount'] buy_limit_filled_price = order['price'] - order_id = None self.rpc.send_msg({ 'type': RPCMessageType.BUY_NOTIFICATION, 'exchange': self.exchange.name.capitalize(), 'pair': pair_s, 'limit': buy_limit_filled_price, + 'order_type': order_type, 'stake_amount': stake_amount, 'stake_currency': stake_currency, 'fiat_currency': fiat_currency @@ -498,9 +408,13 @@ class FreqtradeBot(object): exchange=self.exchange.id, open_order_id=order_id, strategy=self.strategy.get_strategy_name(), - ticker_interval=constants.TICKER_INTERVAL_MINUTES[self.config['ticker_interval']] + ticker_interval=timeframe_to_minutes(self.config['ticker_interval']) ) + # Update fees if order is closed + if order_status == 'closed': + self.update_trade_state(trade, order) + Trade.session.add(trade) Trade.session.flush() @@ -531,24 +445,7 @@ class FreqtradeBot(object): :return: True if executed """ try: - # Get order details for actual price per unit - if trade.open_order_id: - # Update trade with order values - logger.info('Found open order for %s', trade) - order = self.exchange.get_order(trade.open_order_id, trade.pair) - # Try update amount (binance-fix) - try: - new_amount = self.get_real_amount(trade, order) - if order['amount'] != new_amount: - order['amount'] = new_amount - # Fee was applied, so set to 0 - trade.fee_open = 0 - - except OperationalException as exception: - logger.warning("Could not update trade amount: %s", exception) - - # This handles both buy and sell orders! - trade.update(order) + self.update_trade_state(trade) if self.strategy.order_types.get('stoploss_on_exchange') and trade.is_open: result = self.handle_stoploss_on_exchange(trade) @@ -573,7 +470,7 @@ class FreqtradeBot(object): def get_real_amount(self, trade: Trade, order: Dict) -> float: """ 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'] # Only run for closed orders @@ -581,8 +478,11 @@ class FreqtradeBot(object): return order_amount # use fee from order-dict if possible - if 'fee' in order and order['fee'] and (order['fee'].keys() >= {'currency', 'cost'}): - if trade.pair.startswith(order['fee']['currency']): + if ('fee' in order and order['fee'] is not None and + (order['fee'].keys() >= {'currency', 'cost'})): + if (order['fee']['currency'] is not None and + order['fee']['cost'] is not None and + trade.pair.startswith(order['fee']['currency'])): new_amount = order_amount - order['fee']['cost'] logger.info("Applying fee on amount for %s (from %s to %s) from Order", trade, order['amount'], new_amount) @@ -599,9 +499,12 @@ class FreqtradeBot(object): fee_abs = 0 for exectrade in trades: amount += exectrade['amount'] - if "fee" in exectrade and (exectrade['fee'].keys() >= {'currency', 'cost'}): + if ("fee" in exectrade and exectrade['fee'] is not None and + (exectrade['fee'].keys() >= {'currency', 'cost'})): # only applies if fee is in quote currency! - if trade.pair.startswith(exectrade['fee']['currency']): + if (exectrade['fee']['currency'] is not None and + exectrade['fee']['cost'] is not None and + trade.pair.startswith(exectrade['fee']['currency'])): fee_abs += exectrade['fee']['cost'] if amount != order_amount: @@ -613,6 +516,36 @@ class FreqtradeBot(object): f"(from {order_amount} to {real_amount}) from Trades") return real_amount + def update_trade_state(self, trade, action_order: dict = None): + """ + Checks trades with open orders and updates the amount if necessary + """ + # Get order details for actual price per unit + if trade.open_order_id: + # Update trade with order values + logger.info('Found open order for %s', trade) + try: + order = action_order or self.exchange.get_order(trade.open_order_id, trade.pair) + except InvalidOrderException as exception: + logger.warning('Unable to fetch order %s: %s', trade.open_order_id, exception) + return + # Try update amount (binance-fix) + try: + new_amount = self.get_real_amount(trade, order) + if order['amount'] != new_amount: + order['amount'] = new_amount + # Fee was applied, so set to 0 + trade.fee_open = 0 + + except OperationalException as exception: + logger.warning("Could not update trade amount: %s", exception) + + 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: """ Get sell rate - either using get-ticker bid or first bid based on orderbook @@ -663,13 +596,13 @@ class FreqtradeBot(object): logger.info(' order book asks top %s: %0.8f', i, order_book_rate) sell_rate = order_book_rate - if self.check_sell(trade, sell_rate, buy, sell): + if self._check_and_execute_sell(trade, sell_rate, buy, sell): return True else: logger.debug('checking sell') sell_rate = self.get_sell_rate(trade.pair, True) - if self.check_sell(trade, sell_rate, buy, sell): + if self._check_and_execute_sell(trade, sell_rate, buy, sell): return True logger.debug('Found no sell signal for %s.', trade) @@ -682,11 +615,25 @@ class FreqtradeBot(object): is enabled. """ - result = False + logger.debug('Handling stoploss on exchange %s ...', trade) - # If trade is open and the buy order is fulfilled but there is no stoploss, - # then we add a stoploss on exchange - if not trade.open_order_id and not trade.stoploss_order_id: + stoploss_order = None + + try: + # First we check if there is already a stoploss on exchange + stoploss_order = self.exchange.get_order(trade.stoploss_order_id, trade.pair) \ + if trade.stoploss_order_id else None + except InvalidOrderException as exception: + logger.warning('Unable to fetch stoploss order: %s', exception) + + # If trade open order id does not exist: buy order is fulfilled + buy_order_fulfilled = not trade.open_order_id + + # Limit price threshold: As limit price should always be below price + limit_price_pct = 0.99 + + # If buy order is fulfilled but there is no stoploss, we add a stoploss on exchange + if (buy_order_fulfilled and not stoploss_order): if self.edge: stoploss = self.edge.stoploss(pair=trade.pair) else: @@ -695,32 +642,47 @@ class FreqtradeBot(object): stop_price = trade.open_rate * (1 + stoploss) # limit price should be less than stop price. - # 0.99 is arbitrary here. - limit_price = stop_price * 0.99 + limit_price = stop_price * limit_price_pct - stoploss_order_id = self.exchange.stoploss_limit( - pair=trade.pair, amount=trade.amount, stop_price=stop_price, rate=limit_price - )['id'] - trade.stoploss_order_id = str(stoploss_order_id) - trade.stoploss_last_update = datetime.now() + try: + stoploss_order_id = self.exchange.stoploss_limit( + pair=trade.pair, amount=trade.amount, stop_price=stop_price, rate=limit_price + )['id'] + trade.stoploss_order_id = str(stoploss_order_id) + trade.stoploss_last_update = datetime.now() + return False - # Or the trade open and there is already a stoploss on exchange. - # so we check if it is hit ... - elif trade.stoploss_order_id: - logger.debug('Handling stoploss on exchange %s ...', trade) - order = self.exchange.get_order(trade.stoploss_order_id, trade.pair) - if order['status'] == 'closed': - trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value - trade.update(order) - self.notify_sell(trade) - result = True - elif self.config.get('trailing_stop', False): - # if trailing stoploss is enabled we check if stoploss value has changed - # in which case we cancel stoploss order and put another one with new - # value immediately - self.handle_trailing_stoploss_on_exchange(trade, order) + except DependencyException as exception: + logger.warning('Unable to place a stoploss order on exchange: %s', exception) - return result + # If stoploss order is canceled for some reason we add it + if stoploss_order and stoploss_order['status'] == 'canceled': + try: + stoploss_order_id = self.exchange.stoploss_limit( + pair=trade.pair, amount=trade.amount, + stop_price=trade.stop_loss, rate=trade.stop_loss * limit_price_pct + )['id'] + trade.stoploss_order_id = str(stoploss_order_id) + return False + except DependencyException as exception: + logger.warning('Stoploss order was cancelled, ' + 'but unable to recreate one: %s', exception) + + # We check if stoploss order is fulfilled + if stoploss_order and stoploss_order['status'] == 'closed': + trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value + trade.update(stoploss_order) + self._notify_sell(trade) + return True + + # Finally we check if stoploss on exchange should be moved up because of trailing. + if stoploss_order and self.config.get('trailing_stop', False): + # if trailing stoploss is enabled we check if stoploss value has changed + # in which case we cancel stoploss order and put another one with new + # value immediately + self.handle_trailing_stoploss_on_exchange(trade, stoploss_order) + + return False def handle_trailing_stoploss_on_exchange(self, trade: Trade, order): """ @@ -736,23 +698,34 @@ class FreqtradeBot(object): update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60) if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() > update_beat: # cancelling the current stoploss on exchange first - logger.info('Trailing stoploss: cancelling current stoploss on exchange ' - 'in order to add another one ...') - if self.exchange.cancel_order(order['id'], trade.pair): + logger.info('Trailing stoploss: cancelling current stoploss on exchange (id:{%s})' + 'in order to add another one ...', order['id']) + 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 stoploss_order_id = self.exchange.stoploss_limit( pair=trade.pair, amount=trade.amount, stop_price=trade.stop_loss, rate=trade.stop_loss * 0.99 )['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: - if self.edge: - stoploss = self.edge.stoploss(trade.pair) - should_sell = self.strategy.should_sell( - trade, sell_rate, datetime.utcnow(), buy, sell, force_stoploss=stoploss) - else: - should_sell = self.strategy.should_sell(trade, sell_rate, datetime.utcnow(), buy, sell) + def _check_and_execute_sell(self, trade: Trade, sell_rate: float, + buy: bool, sell: bool) -> bool: + """ + Check and execute sell + """ + should_sell = self.strategy.should_sell( + trade, sell_rate, datetime.utcnow(), buy, sell, + force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 + ) if should_sell.sell_flag: self.execute_sell(trade, sell_rate, should_sell.sell_type) @@ -780,7 +753,7 @@ class FreqtradeBot(object): if not trade.open_order_id: continue order = self.exchange.get_order(trade.open_order_id, trade.pair) - except (RequestException, DependencyException): + except (RequestException, DependencyException, InvalidOrderException): logger.info( 'Cannot query order for %s due to %s', trade, @@ -890,7 +863,10 @@ class FreqtradeBot(object): # First cancelling stoploss on exchange ... 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 order_id = self.exchange.sell(pair=str(trade.pair), @@ -903,9 +879,9 @@ class FreqtradeBot(object): trade.close_rate_requested = limit trade.sell_reason = sell_reason.value Trade.session.flush() - self.notify_sell(trade) + self._notify_sell(trade) - def notify_sell(self, trade: Trade): + def _notify_sell(self, trade: Trade): """ Sends rpc notification when a sell occured. """ @@ -922,6 +898,7 @@ class FreqtradeBot(object): 'pair': trade.pair, 'gain': gain, 'limit': trade.close_rate_requested, + 'order_type': self.strategy.order_types['sell'], 'amount': trade.amount, 'open_rate': trade.open_rate, 'current_rate': current_rate, diff --git a/freqtrade/loggers.py b/freqtrade/loggers.py new file mode 100644 index 000000000..90b8905e5 --- /dev/null +++ b/freqtrade/loggers.py @@ -0,0 +1,50 @@ +import logging +import sys + +from logging.handlers import RotatingFileHandler +from typing import Any, Dict, List + + +logger = logging.getLogger(__name__) + + +def _set_loggers(verbosity: int = 0) -> None: + """ + Set the logging level for third party libraries + :return: None + """ + + logging.getLogger('requests').setLevel( + logging.INFO if verbosity <= 1 else logging.DEBUG + ) + logging.getLogger("urllib3").setLevel( + logging.INFO if verbosity <= 1 else logging.DEBUG + ) + logging.getLogger('ccxt.base.exchange').setLevel( + logging.INFO if verbosity <= 2 else logging.DEBUG + ) + logging.getLogger('telegram').setLevel(logging.INFO) + + +def setup_logging(config: Dict[str, Any]) -> None: + """ + Process -v/--verbose, --logfile options + """ + # Log level + verbosity = config['verbosity'] + + # Log to stdout, not stderr + log_handlers: List[logging.Handler] = [logging.StreamHandler(sys.stdout)] + + if config.get('logfile'): + log_handlers.append(RotatingFileHandler(config['logfile'], + maxBytes=1024 * 1024, # 1Mb + backupCount=10)) + + logging.basicConfig( + level=logging.INFO if verbosity < 1 else logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=log_handlers + ) + _set_loggers(verbosity) + logger.info('Verbosity set to %s', verbosity) diff --git a/freqtrade/main.py b/freqtrade/main.py index c41d54f0e..a96fd43c5 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -3,90 +3,66 @@ Main Freqtrade bot script. Read the documentation to know what cli arguments you need. """ -import logging + 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 typing import List +from typing import Any, List from freqtrade import OperationalException -from freqtrade.arguments import Arguments -from freqtrade.configuration import Configuration, set_loggers -from freqtrade.freqtradebot import FreqtradeBot -from freqtrade.state import State -from freqtrade.rpc import RPCMessageType +from freqtrade.configuration import Arguments +from freqtrade.worker import Worker + 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. :return: None """ - arguments = Arguments( - sysargv, - 'Free, open source crypto trading bot' - ) - args = 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 - - freqtrade = None - return_code = 1 + return_code: Any = 1 + worker = None try: - # Load and validate configuration - config = Configuration(args, None).get_config() + arguments = Arguments( + sysargv, + 'Free, open source crypto trading bot' + ) + args: Namespace = arguments.get_parsed_arg() - # Init the bot - freqtrade = FreqtradeBot(config) - - state = None - while True: - state = freqtrade.worker(old_state=state) - if state == State.RELOAD_CONF: - freqtrade = reconfigure(freqtrade, args) + # 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: logger.info('SIGINT received, aborting ...') return_code = 0 except OperationalException as e: logger.error(str(e)) return_code = 2 - except BaseException: + except Exception: logger.exception('Fatal exception!') finally: - if freqtrade: - freqtrade.stopping() - freqtrade.rpc.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, - 'status': 'process died' - }) - freqtrade.cleanup() + if worker: + worker.exit() sys.exit(return_code) -def reconfigure(freqtrade: FreqtradeBot, args: Namespace) -> FreqtradeBot: - """ - Cleans up current instance, reloads the configuration and returns the new instance - """ - freqtrade.reconfigure() - - # Clean up current modules - freqtrade.cleanup() - - # Create new instance - freqtrade = FreqtradeBot(Configuration(args, None).get_config()) - freqtrade.rpc.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, - 'status': 'config reloaded' - }) - return freqtrade - - if __name__ == '__main__': - set_loggers() - main(sys.argv[1:]) + main() diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 38f758669..05946e008 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -1,17 +1,15 @@ """ Various tool function for Freqtrade and scripts """ - import gzip import logging import re from datetime import datetime -from typing import Dict import numpy as np -from pandas import DataFrame import rapidjson + logger = logging.getLogger(__name__) @@ -41,24 +39,6 @@ def datesarray_to_datetimearray(dates: np.ndarray) -> np.ndarray: return dates.dt.to_pydatetime() -def common_datearray(dfs: Dict[str, DataFrame]) -> np.ndarray: - """ - Return dates from Dataframe - :param dfs: Dict with format pair: pair_data - :return: List of dates - """ - alldates = {} - for pair, pair_data in dfs.items(): - dates = datesarray_to_datetimearray(pair_data['date']) - for date in dates: - alldates[date] = 1 - lst = [] - for date, _ in alldates.items(): - lst.append(date) - arr = np.array(lst) - return np.sort(arr, axis=0) - - def file_dump_json(filename, data, is_zip=False) -> None: """ Dump JSON data into a file @@ -117,6 +97,8 @@ def format_ms_time(date: int) -> str: def deep_merge_dicts(source, destination): """ + Values from Source override destination, destination is returned (and modified!!) + Sample: >>> a = { 'first' : { 'rows' : { 'pass' : 'dog', 'number' : '1' } } } >>> b = { 'first' : { 'rows' : { 'fail' : 'cat', 'number' : '5' } } } >>> merge(b, a) == { 'first' : { 'rows' : { 'pass' : 'dog', 'fail' : 'cat', 'number' : '5' } } } diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index 19b8dd90a..8b548eefe 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -1,49 +1,111 @@ -# pragma pylint: disable=missing-docstring - import logging -from datetime import datetime -from typing import Dict, Tuple -import operator +from argparse import Namespace +from typing import Any, Dict -import arrow -from pandas import DataFrame +from filelock import FileLock, Timeout + +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__) -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 - :param data: dictionary with preprocessed backtesting data - :return: tuple containing min_date, max_date + Prepare the configuration for the Hyperopt module + :param args: Cli args from Arguments() + :return: Configuration """ - 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] + config = setup_utils_configuration(args, method) + + if method == RunMode.BACKTEST: + if config['stake_amount'] == constants.UNLIMITED_STAKE_AMOUNT: + raise DependencyException('stake amount could not be "%s" for backtesting' % + 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, - max_date: datetime, ticker_interval_mins: int) -> bool: +def start_backtesting(args: Namespace) -> None: """ - 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 - :param min_date: start-date of the data - :param max_date: end-date of the data - :param ticker_interval_mins: ticker interval in minutes + # Initialize configuration + config = setup_configuration(args, RunMode.BACKTEST) + + 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 - expected_frames = int((max_date - min_date).total_seconds() // 60 // ticker_interval_mins) - found_missing = False - for pair, df in data.items(): - dflen = len(df) - 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 + Start hyperopt script + :param args: Cli args from Arguments() + :return: None + """ + # Import here to avoid loading hyperopt module when it's not used + from freqtrade.optimize.hyperopt import Hyperopt, HYPEROPT_LOCKFILE + + # Initialize configuration + config = setup_configuration(args, RunMode.HYPEROPT) + + 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() diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 031b490c8..252175269 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -4,25 +4,24 @@ This module contains the backtesting logic """ import logging -from argparse import Namespace from copy import deepcopy from datetime import datetime, timedelta from pathlib import Path from typing import Any, Dict, List, NamedTuple, Optional from pandas import DataFrame -from tabulate import tabulate -from freqtrade import optimize -from freqtrade import DependencyException, constants -from freqtrade.arguments import Arguments -from freqtrade.configuration import Configuration +from freqtrade import OperationalException +from freqtrade.configuration import Arguments from freqtrade.data import history +from freqtrade.data.dataprovider import DataProvider +from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import file_dump_json from freqtrade.persistence import Trade from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.state import RunMode -from freqtrade.strategy.interface import SellType, IStrategy +from freqtrade.strategy.interface import IStrategy, SellType +from tabulate import tabulate logger = logging.getLogger(__name__) @@ -64,32 +63,38 @@ class Backtesting(object): self.config['exchange']['uid'] = '' self.config['dry_run'] = True self.strategylist: List[IStrategy] = [] + + self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange + self.fee = self.exchange.get_fee() + + if self.config.get('runmode') != RunMode.HYPEROPT: + self.dataprovider = DataProvider(self.config, self.exchange) + IStrategy.dp = self.dataprovider + if self.config.get('strategy_list', None): - # Force one interval - self.ticker_interval = str(self.config.get('ticker_interval')) - self.ticker_interval_mins = constants.TICKER_INTERVAL_MINUTES[self.ticker_interval] for strat in list(self.config['strategy_list']): stratconf = deepcopy(self.config) stratconf['strategy'] = strat self.strategylist.append(StrategyResolver(stratconf).strategy) else: - # only one strategy + # No strategy list specified, only one strategy self.strategylist.append(StrategyResolver(self.config).strategy) - # Load one strategy + + # Load one (first) strategy self._set_strategy(self.strategylist[0]) - exchange_name = self.config.get('exchange', {}).get('name', 'bittrex').title() - self.exchange = ExchangeResolver(exchange_name, self.config).exchange - self.fee = self.exchange.get_fee() def _set_strategy(self, strategy): """ Load strategy into backtesting """ self.strategy = strategy + if "ticker_interval" not in self.config: + raise OperationalException("Ticker-interval needs to be set in either configuration " + "or as cli argument `--ticker-interval 5m`") + self.ticker_interval = self.config.get('ticker_interval') - self.ticker_interval_mins = constants.TICKER_INTERVAL_MINUTES[self.ticker_interval] - self.tickerdata_to_dataframe = strategy.tickerdata_to_dataframe + self.ticker_interval_mins = timeframe_to_minutes(self.ticker_interval) self.advise_buy = strategy.advise_buy self.advise_sell = strategy.advise_sell # Set stoploss_on_exchange to false for backtesting, @@ -202,12 +207,37 @@ class Backtesting(object): logger.info('Dumping backtest results to %s', recordfilename) file_dump_json(recordfilename, records) + def _get_ticker_list(self, processed) -> Dict[str, DataFrame]: + """ + Helper function to convert a processed tickerlist into a list for performance reasons. + + Used by backtest() - so keep this optimized for performance. + """ + headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high'] + ticker: Dict = {} + # Create ticker dict + for pair, pair_data in processed.items(): + pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run + + ticker_data = self.advise_sell( + self.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy() + + # to avoid using data from future, we buy/sell with signal from previous candle + ticker_data.loc[:, 'buy'] = ticker_data['buy'].shift(1) + ticker_data.loc[:, 'sell'] = ticker_data['sell'].shift(1) + + ticker_data.drop(ticker_data.head(1).index, inplace=True) + + # Convert from Pandas to list for performance reasons + # (Looping Pandas is slow.) + ticker[pair] = [x for x in ticker_data.itertuples()] + return ticker + def _get_sell_trade_entry( 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( open_rate=buy_row.open, open_date=buy_row.date, @@ -223,26 +253,23 @@ class Backtesting(object): # Increase trade_count_lock for every iteration 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, buy_signal, + sell = self.strategy.should_sell(trade, sell_row.open, sell_row.date, sell_row.buy, sell_row.sell, low=sell_row.low, high=sell_row.high) if sell.sell_flag: - trade_dur = int((sell_row.date - buy_row.date).total_seconds() // 60) # Special handling if high or low hit STOP_LOSS or ROI if sell.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): # Set close_rate to stoploss closerate = trade.stop_loss elif sell.sell_type == (SellType.ROI): - # get next entry in min_roi > to trade duration - # Interface.py skips on trade_duration <= duration - roi_entry = max(list(filter(lambda x: trade_dur >= x, - self.strategy.minimal_roi.keys()))) - roi = self.strategy.minimal_roi[roi_entry] - - # - (Expected abs profit + open_rate + open_fee) / (fee_close -1) - closerate = - (trade.open_rate * roi + trade.open_rate * - (1 + trade.fee_open)) / (trade.fee_close - 1) + roi = self.strategy.min_roi_reached_entry(trade_dur) + if roi is not None: + # - (Expected abs profit + open_rate + open_fee) / (fee_close -1) + closerate = - (trade.open_rate * roi + trade.open_rate * + (1 + trade.fee_open)) / (trade.fee_close - 1) + else: + # This should not be reached... + closerate = sell_row.open else: closerate = sell_row.open @@ -296,74 +323,75 @@ class Backtesting(object): position_stacking: do we allow position stacking? (default: False) :return: DataFrame """ - headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high'] + # Arguments are long and noisy, so this is commented out. + # Uncomment if you need to debug the backtest() method. +# logger.debug(f"Start backtest, args: {args}") processed = args['processed'] + stake_amount = args['stake_amount'] max_open_trades = args.get('max_open_trades', 0) position_stacking = args.get('position_stacking', False) start_date = args['start_date'] end_date = args['end_date'] trades = [] trade_count_lock: Dict = {} - ticker: Dict = {} - pairs = [] - # Create ticker dict - for pair, pair_data in processed.items(): - pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run - ticker_data = self.advise_sell( - self.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy() - - # to avoid using data from future, we buy/sell with signal from previous candle - ticker_data.loc[:, 'buy'] = ticker_data['buy'].shift(1) - ticker_data.loc[:, 'sell'] = ticker_data['sell'].shift(1) - - ticker_data.drop(ticker_data.head(1).index, inplace=True) - - # Convert from Pandas to list for performance reasons - # (Looping Pandas is slow.) - ticker[pair] = [x for x in ticker_data.itertuples()] - pairs.append(pair) + # Dict of ticker-lists for performance (looping lists is a lot faster than dataframes) + ticker: Dict = self._get_ticker_list(processed) lock_pair_until: Dict = {} + # Indexes per pair, so some pairs are allowed to have a missing start. + indexes: Dict = {} tmp = start_date + timedelta(minutes=self.ticker_interval_mins) - index = 0 - # Loop timerange and test per pair + + # Loop timerange and get candle for each pair at that point in time while tmp < end_date: - # print(f"time: {tmp}") + for i, pair in enumerate(ticker): + if pair not in indexes: + indexes[pair] = 0 + try: - row = ticker[pair][index] + row = ticker[pair][indexes[pair]] except IndexError: - # missing Data for one pair ... - # Warnings for this are shown by `validate_backtest_data` + # missing Data for one pair at the end. + # Warnings for this are shown during data loading continue + # Waits until the time-counter reaches the start of the data for this pair. + if row.date > tmp.datetime: + continue + + indexes[pair] += 1 + if row.buy == 0 or row.sell == 1: continue # skip rows where no buy signal or that would immediately sell off - if not position_stacking: - if pair in lock_pair_until and row.date <= lock_pair_until[pair]: - continue + if (not position_stacking and pair in lock_pair_until + and row.date <= lock_pair_until[pair]): + # without positionstacking, we can only have one open trade per pair. + continue + if max_open_trades > 0: # Check if max_open_trades has already been reached for the given date if not trade_count_lock.get(row.date, 0) < max_open_trades: continue - trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1 - trade_entry = self._get_sell_trade_entry(pair, row, ticker[pair][index + 1:], - trade_count_lock, args) + # since indexes has been incremented before, we need to go one step back to + # also check the buying candle for sell conditions. + trade_entry = self._get_sell_trade_entry(pair, row, ticker[pair][indexes[pair]-1:], + trade_count_lock, stake_amount, + max_open_trades) if trade_entry: lock_pair_until[pair] = trade_entry.close_time trades.append(trade_entry) else: # Set lock_pair_until to end of testing period if trade could not be closed - # This happens only if the buy-signal was with the last candle - lock_pair_until[pair] = end_date + lock_pair_until[pair] = end_date.datetime + # Move time one configured time_interval ahead. tmp += timedelta(minutes=self.ticker_interval_mins) - index += 1 return DataFrame.from_records(trades, columns=BacktestResult._fields) def start(self) -> None: @@ -376,24 +404,17 @@ class Backtesting(object): logger.info('Using stake_currency: %s ...', self.config['stake_currency']) logger.info('Using stake_amount: %s ...', self.config['stake_amount']) - if self.config.get('live'): - logger.info('Downloading data for all pairs in whitelist ...') - self.exchange.refresh_latest_ohlcv([(pair, self.ticker_interval) for pair in pairs]) - data = {key[0]: value for key, value in self.exchange._klines.items()} - - else: - logger.info('Using local backtesting data (using whitelist in given config) ...') - - timerange = Arguments.parse_timerange(None if self.config.get( - 'timerange') is None else str(self.config.get('timerange'))) - 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 - ) + timerange = Arguments.parse_timerange(None if self.config.get( + 'timerange') is None else str(self.config.get('timerange'))) + 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, + live=self.config.get('live', False) + ) if not data: logger.critical("No data found. Terminating.") @@ -406,20 +427,19 @@ class Backtesting(object): max_open_trades = 0 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: logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) 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, - constants.TICKER_INTERVAL_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 preprocessed = self.strategy.tickerdata_to_dataframe(data) @@ -456,38 +476,3 @@ class Backtesting(object): print(' Strategy Summary '.center(133, '=')) print(self._generate_text_table_strategy(all_results)) 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() diff --git a/freqtrade/optimize/default_hyperopt.py b/freqtrade/optimize/default_hyperopt.py index 721848d2e..e05dfc95c 100644 --- a/freqtrade/optimize/default_hyperopt.py +++ b/freqtrade/optimize/default_hyperopt.py @@ -1,24 +1,21 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +from functools import reduce +from typing import Any, Callable, Dict, List + import talib.abstract as ta from pandas import DataFrame -from typing import Dict, Any, Callable, List -from functools import reduce - -from skopt.space import Categorical, Dimension, Integer, Real +from skopt.space import Categorical, Dimension, Integer import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.optimize.hyperopt_interface import IHyperOpt -class_name = 'DefaultHyperOpts' - class DefaultHyperOpts(IHyperOpt): """ - Default hyperopt provided by freqtrade bot. + Default hyperopt provided by the Freqtrade bot. You can override it with your own hyperopt """ - @staticmethod def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe['adx'] = ta.ADX(dataframe) @@ -70,9 +67,10 @@ class DefaultHyperOpts(IHyperOpt): dataframe['close'], dataframe['sar'] )) - dataframe.loc[ - reduce(lambda x, y: x & y, conditions), - 'buy'] = 1 + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'buy'] = 1 return dataframe @@ -129,9 +127,10 @@ class DefaultHyperOpts(IHyperOpt): dataframe['sar'], dataframe['close'] )) - dataframe.loc[ - reduce(lambda x, y: x & y, conditions), - 'sell'] = 1 + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'sell'] = 1 return dataframe @@ -156,42 +155,6 @@ class DefaultHyperOpts(IHyperOpt): 'sell-sar_reversal'], name='sell-trigger') ] - @staticmethod - def generate_roi_table(params: Dict) -> Dict[int, float]: - """ - Generate the ROI table that will be used by Hyperopt - """ - roi_table = {} - roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3'] - roi_table[params['roi_t3']] = params['roi_p1'] + params['roi_p2'] - roi_table[params['roi_t3'] + params['roi_t2']] = params['roi_p1'] - roi_table[params['roi_t3'] + params['roi_t2'] + params['roi_t1']] = 0 - - return roi_table - - @staticmethod - def stoploss_space() -> List[Dimension]: - """ - Stoploss Value to search - """ - return [ - Real(-0.5, -0.02, name='stoploss'), - ] - - @staticmethod - def roi_space() -> List[Dimension]: - """ - Values to search for each ROI steps - """ - return [ - Integer(10, 120, name='roi_t1'), - Integer(10, 60, name='roi_t2'), - Integer(10, 40, name='roi_t3'), - Real(0.01, 0.04, name='roi_p1'), - Real(0.01, 0.07, name='roi_p2'), - Real(0.01, 0.20, name='roi_p3'), - ] - def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators. Should be a copy of from strategy diff --git a/freqtrade/optimize/default_hyperopt_loss.py b/freqtrade/optimize/default_hyperopt_loss.py new file mode 100644 index 000000000..4ab9fbe44 --- /dev/null +++ b/freqtrade/optimize/default_hyperopt_loss.py @@ -0,0 +1,52 @@ +""" +DefaultHyperOptLoss +This module defines the default HyperoptLoss class which is being used for +Hyperoptimization. +""" +from math import exp + +from pandas import DataFrame + +from freqtrade.optimize.hyperopt import IHyperOptLoss + + +# Set TARGET_TRADES to suit your number concurrent trades so its realistic +# to the number of days +TARGET_TRADES = 600 + +# This is assumed to be expected avg profit * expected trade count. +# For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades, +# expected max profit = 3.85 +# Check that the reported Σ% values do not exceed this! +# Note, this is ratio. 3.85 stated above means 385Σ%. +EXPECTED_MAX_PROFIT = 3.0 + +# Max average trade duration in minutes. +# If eval ends with higher value, we consider it a failed eval. +MAX_ACCEPTED_TRADE_DURATION = 300 + + +class DefaultHyperOptLoss(IHyperOptLoss): + """ + Defines the default loss function for hyperopt + """ + + @staticmethod + def hyperopt_loss_function(results: DataFrame, trade_count: int, + *args, **kwargs) -> float: + """ + Objective function, returns smaller number for better results + This is the Default algorithm + Weights are distributed as follows: + * 0.4 to trade duration + * 0.25: Avoiding trade loss + * 1.0 to total profit, compared to the expected value (`EXPECTED_MAX_PROFIT`) defined above + """ + total_profit = results.profit_percent.sum() + trade_duration = results.trade_duration.mean() + + trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8) + profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT) + duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1) + result = trade_loss + profit_loss + duration_loss + return result diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index 9b628cf2e..8d1fa381b 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -4,16 +4,14 @@ This module contains the edge backtesting interface """ import logging -from argparse import Namespace from typing import Dict, Any from tabulate import tabulate +from freqtrade import constants from freqtrade.edge import Edge -from freqtrade.arguments import Arguments -from freqtrade.configuration import Configuration +from freqtrade.configuration import Arguments from freqtrade.exchange import Exchange from freqtrade.resolvers import StrategyResolver -from freqtrade.state import RunMode logger = logging.getLogger(__name__) @@ -35,6 +33,7 @@ class EdgeCli(object): self.config['exchange']['secret'] = '' self.config['exchange']['password'] = '' self.config['exchange']['uid'] = '' + self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT self.config['dry_run'] = True self.exchange = Exchange(self.config) self.strategy = StrategyResolver(self.config).strategy @@ -73,37 +72,7 @@ class EdgeCli(object): floatfmt=floatfmt, tablefmt="pipe") def start(self) -> None: - self.edge.calculate() - print('') # blank like 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() + result = self.edge.calculate() + if result: + print('') # blank line for readability + print(self._generate_edge_table(self.edge._cached_pairs)) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index f6d39f11c..427b17cb8 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -5,33 +5,35 @@ This module contains the hyperopt logic """ import logging -import multiprocessing import os import sys -from argparse import Namespace -from math import exp + from operator import itemgetter from pathlib import Path from pprint import pprint -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional -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 skopt import Optimizer from skopt.space import Dimension -from freqtrade.arguments import Arguments -from freqtrade.configuration import Configuration -from freqtrade.data.history import load_data -from freqtrade.optimize import get_timeframe +from freqtrade.configuration import Arguments +from freqtrade.data.history import load_data, get_timeframe from freqtrade.optimize.backtesting import Backtesting -from freqtrade.state import RunMode -from freqtrade.resolvers import HyperOptResolver +# Import IHyperOptLoss to allow users import from this file +from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F4 +from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver, HyperOptLossResolver + logger = logging.getLogger(__name__) + +INITIAL_POINTS = 30 MAX_LOSS = 100000 # just a big enough number to be bad result in loss optimization TICKERDATA_PICKLE = os.path.join('user_data', 'hyperopt_tickerdata.pkl') +TRIALSDATA_PICKLE = os.path.join('user_data', 'hyperopt_results.pickle') +HYPEROPT_LOCKFILE = os.path.join('user_data', 'hyperopt.lock') class Hyperopt(Backtesting): @@ -44,28 +46,54 @@ class Hyperopt(Backtesting): """ def __init__(self, config: Dict[str, Any]) -> None: super().__init__(config) - self.config = config self.custom_hyperopt = HyperOptResolver(self.config).hyperopt - # set TARGET_TRADES to suit your number concurrent trades so its realistic - # to the number of days - self.target_trades = 600 - self.total_tries = config.get('epochs', 0) + self.custom_hyperoptloss = HyperOptLossResolver(self.config).hyperoptloss + self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function + + self.total_epochs = config.get('epochs', 0) self.current_best_loss = 100 - # max average trade duration in minutes - # if eval ends with higher value, we consider it a failed eval - self.max_accepted_trade_duration = 300 - - # this is expexted avg profit * expected trade count - # for example 3.5%, 1100 trades, self.expected_max_profit = 3.85 - # check that the reported Σ% values do not exceed this! - self.expected_max_profit = 3.0 + if not self.config.get('hyperopt_continue'): + self.clean_hyperopt() + else: + logger.info("Continuing on previous hyperopt results.") # Previous evaluations - self.trials_file = os.path.join('user_data', 'hyperopt_results.pickle') + self.trials_file = TRIALSDATA_PICKLE self.trials: List = [] + # Populate functions here (hasattr is slow so should not be run during "regular" operations) + if hasattr(self.custom_hyperopt, 'populate_buy_trend'): + self.advise_buy = self.custom_hyperopt.populate_buy_trend # type: ignore + + if hasattr(self.custom_hyperopt, 'populate_sell_trend'): + self.advise_sell = self.custom_hyperopt.populate_sell_trend # type: ignore + + # Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set + if self.config.get('use_max_market_positions', True): + self.max_open_trades = self.config['max_open_trades'] + else: + logger.debug('Ignoring max_open_trades (--disable-max-market-positions was used) ...') + self.max_open_trades = 0 + self.position_stacking = self.config.get('position_stacking', False), + + if self.has_space('sell'): + # Make sure experimental is enabled + if 'experimental' not in self.config: + self.config['experimental'] = {} + self.config['experimental']['use_sell_signal'] = True + + def clean_hyperopt(self): + """ + Remove hyperopt pickle files to restart hyperopt. + """ + for f in [TICKERDATA_PICKLE, TRIALSDATA_PICKLE]: + p = Path(f) + if p.is_file(): + logger.info(f"Removing `{p}`.") + p.unlink() + def get_args(self, params): dimensions = self.hyperopt_space() # Ensure the number of dimensions match @@ -102,140 +130,157 @@ class Hyperopt(Backtesting): """ results = sorted(self.trials, key=itemgetter('loss')) best_result = results[0] - logger.info( - 'Best result:\n%s\nwith values:\n', - best_result['result'] - ) - pprint(best_result['params'], indent=4) - if 'roi_t1' in best_result['params']: - logger.info('ROI table:') - pprint(self.custom_hyperopt.generate_roi_table(best_result['params']), indent=4) + params = best_result['params'] + + log_str = self.format_results_logstring(best_result) + print(f"\nBest result:\n\n{log_str}\n") + if self.has_space('buy'): + print('Buy hyperspace params:') + pprint({p.name: params.get(p.name) for p in self.hyperopt_space('buy')}, + indent=4) + if self.has_space('sell'): + print('Sell hyperspace params:') + pprint({p.name: params.get(p.name) for p in self.hyperopt_space('sell')}, + indent=4) + if self.has_space('roi'): + print("ROI table:") + pprint(self.custom_hyperopt.generate_roi_table(params), indent=4) + if self.has_space('stoploss'): + print(f"Stoploss: {params.get('stoploss')}") def log_results(self, results) -> None: """ Log results if it is better than any previous evaluation """ - if results['loss'] < self.current_best_loss: - current = results['current_tries'] - total = results['total_tries'] - res = results['result'] - loss = results['loss'] - self.current_best_loss = results['loss'] - log_msg = f'\n{current:5d}/{total}: {res}. Loss {loss:.5f}' - print(log_msg) + print_all = self.config.get('print_all', False) + if print_all or results['loss'] < self.current_best_loss: + log_str = self.format_results_logstring(results) + if print_all: + print(log_str) + else: + print('\n' + log_str) else: print('.', end='') sys.stdout.flush() - def calculate_loss(self, total_profit: float, trade_count: int, trade_duration: float) -> float: - """ - Objective function, returns smaller number for more optimal results - """ - trade_loss = 1 - 0.25 * exp(-(trade_count - self.target_trades) ** 2 / 10 ** 5.8) - profit_loss = max(0, 1 - total_profit / self.expected_max_profit) - duration_loss = 0.4 * min(trade_duration / self.max_accepted_trade_duration, 1) - result = trade_loss + profit_loss + duration_loss - return result + def format_results_logstring(self, results) -> str: + # Output human-friendly index here (starting from 1) + current = results['current_epoch'] + 1 + total = self.total_epochs + res = results['results_explanation'] + loss = results['loss'] + self.current_best_loss = results['loss'] + log_str = f'{current:5d}/{total}: {res} Objective: {loss:.5f}' + log_str = f'*{log_str}' if results['is_initial_point'] else f' {log_str}' + return log_str def has_space(self, space: str) -> bool: """ Tell if a space value is contained in the configuration """ - if space in self.config['spaces'] or 'all' in self.config['spaces']: - return True - return False + return any(s in self.config['spaces'] for s in [space, 'all']) - def hyperopt_space(self) -> List[Dimension]: + def hyperopt_space(self, space: Optional[str] = None) -> List[Dimension]: """ - Return the space to use during Hyperopt + Return the dimensions in the hyperoptimization space. + :param space: Defines hyperspace to return dimensions for. + If None, then the self.has_space() will be used to return dimensions + for all hyperspaces used. """ spaces: List[Dimension] = [] - if self.has_space('buy'): + if space == 'buy' or (space is None and self.has_space('buy')): + logger.debug("Hyperopt has 'buy' space") spaces += self.custom_hyperopt.indicator_space() - if self.has_space('sell'): + if space == 'sell' or (space is None and self.has_space('sell')): + logger.debug("Hyperopt has 'sell' space") spaces += self.custom_hyperopt.sell_indicator_space() - # Make sure experimental is enabled - if 'experimental' not in self.config: - self.config['experimental'] = {} - self.config['experimental']['use_sell_signal'] = True - if self.has_space('roi'): + if space == 'roi' or (space is None and self.has_space('roi')): + logger.debug("Hyperopt has 'roi' space") spaces += self.custom_hyperopt.roi_space() - if self.has_space('stoploss'): + if space == 'stoploss' or (space is None and self.has_space('stoploss')): + logger.debug("Hyperopt has 'stoploss' space") spaces += self.custom_hyperopt.stoploss_space() return spaces def generate_optimizer(self, _params: Dict) -> Dict: + """ + Used Optimize function. Called once per epoch to optimize whatever is configured. + Keep this function as optimized as possible! + """ params = self.get_args(_params) if self.has_space('roi'): self.strategy.minimal_roi = self.custom_hyperopt.generate_roi_table(params) if self.has_space('buy'): self.advise_buy = self.custom_hyperopt.buy_strategy_generator(params) - elif hasattr(self.custom_hyperopt, 'populate_buy_trend'): - self.advise_buy = self.custom_hyperopt.populate_buy_trend # type: ignore if self.has_space('sell'): self.advise_sell = self.custom_hyperopt.sell_strategy_generator(params) - elif hasattr(self.custom_hyperopt, 'populate_sell_trend'): - self.advise_sell = self.custom_hyperopt.populate_sell_trend # type: ignore if self.has_space('stoploss'): self.strategy.stoploss = params['stoploss'] processed = load(TICKERDATA_PICKLE) + min_date, max_date = get_timeframe(processed) + results = self.backtest( { 'stake_amount': self.config['stake_amount'], 'processed': processed, - 'position_stacking': self.config.get('position_stacking', True), + 'max_open_trades': self.max_open_trades, + 'position_stacking': self.position_stacking, 'start_date': min_date, 'end_date': max_date, } ) - result_explanation = self.format_results(results) + results_explanation = self.format_results(results) - total_profit = results.profit_percent.sum() trade_count = len(results.index) - 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 { 'loss': MAX_LOSS, 'params': params, - 'result': result_explanation, + 'results_explanation': results_explanation, } - loss = self.calculate_loss(total_profit, trade_count, trade_duration) + loss = self.calculate_loss(results=results, trade_count=trade_count, + min_date=min_date.datetime, max_date=max_date.datetime) return { 'loss': loss, 'params': params, - 'result': result_explanation, + 'results_explanation': results_explanation, } def format_results(self, results: DataFrame) -> str: """ - Return the format result in a string + Return the formatted results explanation in a string """ trades = len(results.index) avg_profit = results.profit_percent.mean() * 100.0 total_profit = results.profit_abs.sum() stake_cur = self.config['stake_currency'] - profit = results.profit_percent.sum() + profit = results.profit_percent.sum() * 100.0 duration = results.trade_duration.mean() return (f'{trades:6d} trades. Avg profit {avg_profit: 5.2f}%. ' 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: return Optimizer( self.hyperopt_space(), base_estimator="ET", acq_optimizer="auto", - n_initial_points=30, - acq_optimizer_kwargs={'n_jobs': cpu_count} + n_initial_points=INITIAL_POINTS, + acq_optimizer_kwargs={'n_jobs': cpu_count}, + random_state=self.config.get('hyperopt_random_state', None) ) def run_optimizer_parallel(self, parallel, asked) -> List: @@ -258,69 +303,61 @@ class Hyperopt(Backtesting): datadir=Path(self.config['datadir']) if self.config.get('datadir') else None, pairs=self.config['exchange']['pair_whitelist'], ticker_interval=self.ticker_interval, + refresh_pairs=self.config.get('refresh_pairs', False), + exchange=self.exchange, timerange=timerange ) - if self.has_space('buy') or self.has_space('sell'): - self.strategy.advise_indicators = \ - self.custom_hyperopt.populate_indicators # type: ignore - dump(self.strategy.tickerdata_to_dataframe(data), TICKERDATA_PICKLE) + 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 + ) + + self.strategy.advise_indicators = \ + self.custom_hyperopt.populate_indicators # type: ignore + + 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.load_previous_results() - cpus = multiprocessing.cpu_count() + cpus = cpu_count() 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) - EVALS = max(self.total_tries // cpus, 1) + opt = self.get_optimizer(config_jobs) 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_epochs // jobs, 1) 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) - opt.tell(asked, [i['loss'] for i in f_val]) - - self.trials += f_val - for j in range(cpus): - self.log_results({ - 'loss': f_val[j]['loss'], - 'current_tries': i * cpus + j, - 'total_tries': self.total_tries, - 'result': f_val[j]['result'], - }) + opt.tell(asked, [v['loss'] for v in f_val]) + for j in range(jobs): + current = i * jobs + j + val = f_val[j] + val['current_epoch'] = current + val['is_initial_point'] = current < INITIAL_POINTS + self.log_results(val) + self.trials.append(val) + logger.debug(f"Optimizer epoch evaluated: {val}") except KeyboardInterrupt: print('User interrupted..') self.save_trials() 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() diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 622de3015..f1f123653 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -7,7 +7,7 @@ from abc import ABC, abstractmethod from typing import Dict, Any, Callable, List from pandas import DataFrame -from skopt.space import Dimension +from skopt.space import Dimension, Integer, Real class IHyperOpt(ABC): @@ -20,61 +20,86 @@ class IHyperOpt(ABC): stoploss -> float: optimal stoploss designed for the strategy ticker_interval -> int: value of the ticker interval to use for the strategy """ + ticker_interval: str @staticmethod @abstractmethod def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: """ - Populate indicators that will be used in the Buy and Sell strategy - :param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe() - :return: a Dataframe with all mandatory indicators for the strategies + Populate indicators that will be used in the Buy and Sell strategy. + :param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe(). + :return: A Dataframe with all mandatory indicators for the strategies. """ @staticmethod @abstractmethod def buy_strategy_generator(params: Dict[str, Any]) -> Callable: """ - Create a buy strategy generator + Create a buy strategy generator. """ @staticmethod @abstractmethod def sell_strategy_generator(params: Dict[str, Any]) -> Callable: """ - Create a sell strategy generator + Create a sell strategy generator. """ @staticmethod @abstractmethod def indicator_space() -> List[Dimension]: """ - Create an indicator space + Create an indicator space. """ @staticmethod @abstractmethod def sell_indicator_space() -> List[Dimension]: """ - Create a sell indicator space + Create a sell indicator space. """ @staticmethod - @abstractmethod def generate_roi_table(params: Dict) -> Dict[int, float]: """ - Create an roi table + Create a ROI table. + + Generates the ROI table that will be used by Hyperopt. + You may override it in your custom Hyperopt class. """ + roi_table = {} + roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3'] + roi_table[params['roi_t3']] = params['roi_p1'] + params['roi_p2'] + roi_table[params['roi_t3'] + params['roi_t2']] = params['roi_p1'] + roi_table[params['roi_t3'] + params['roi_t2'] + params['roi_t1']] = 0 + + return roi_table @staticmethod - @abstractmethod def stoploss_space() -> List[Dimension]: """ - Create a stoploss space + Create a stoploss space. + + Defines range of stoploss values to search. + You may override it in your custom Hyperopt class. """ + return [ + Real(-0.5, -0.02, name='stoploss'), + ] @staticmethod - @abstractmethod def roi_space() -> List[Dimension]: """ - Create a roi space + Create a ROI space. + + Defines values to search for each ROI steps. + You may override it in your custom Hyperopt class. """ + return [ + Integer(10, 120, name='roi_t1'), + Integer(10, 60, name='roi_t2'), + Integer(10, 40, name='roi_t3'), + Real(0.01, 0.04, name='roi_p1'), + Real(0.01, 0.07, name='roi_p2'), + Real(0.01, 0.20, name='roi_p3'), + ] diff --git a/freqtrade/optimize/hyperopt_loss_interface.py b/freqtrade/optimize/hyperopt_loss_interface.py new file mode 100644 index 000000000..b11b6e661 --- /dev/null +++ b/freqtrade/optimize/hyperopt_loss_interface.py @@ -0,0 +1,25 @@ +""" +IHyperOptLoss interface +This module defines the interface for the loss-function for hyperopts +""" + +from abc import ABC, abstractmethod +from datetime import datetime + +from pandas import DataFrame + + +class IHyperOptLoss(ABC): + """ + Interface for freqtrade hyperopts Loss functions. + Defines the custom loss function (`hyperopt_loss_function()` which is evaluated every epoch.) + """ + ticker_interval: str + + @staticmethod + @abstractmethod + def hyperopt_loss_function(results: DataFrame, trade_count: int, + min_date: datetime, max_date: datetime, *args, **kwargs) -> float: + """ + Objective function, returns smaller number for better results + """ diff --git a/freqtrade/optimize/hyperopt_loss_onlyprofit.py b/freqtrade/optimize/hyperopt_loss_onlyprofit.py new file mode 100644 index 000000000..a1c50e727 --- /dev/null +++ b/freqtrade/optimize/hyperopt_loss_onlyprofit.py @@ -0,0 +1,38 @@ +""" +OnlyProfitHyperOptLoss + +This module defines the alternative HyperOptLoss class which can be used for +Hyperoptimization. +""" +from pandas import DataFrame + +from freqtrade.optimize.hyperopt import IHyperOptLoss + + +# This is assumed to be expected avg profit * expected trade count. +# For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades, +# expected max profit = 3.85 +# +# Note, this is ratio. 3.85 stated above means 385Σ%, 3.0 means 300Σ%. +# +# In this implementation it's only used in calculation of the resulting value +# of the objective function as a normalization coefficient and does not +# represent any limit for profits as in the Freqtrade legacy default loss function. +EXPECTED_MAX_PROFIT = 3.0 + + +class OnlyProfitHyperOptLoss(IHyperOptLoss): + """ + Defines the loss function for hyperopt. + + This implementation takes only profit into account. + """ + + @staticmethod + def hyperopt_loss_function(results: DataFrame, trade_count: int, + *args, **kwargs) -> float: + """ + Objective function, returns smaller number for better results. + """ + total_profit = results.profit_percent.sum() + return 1 - total_profit / EXPECTED_MAX_PROFIT diff --git a/freqtrade/optimize/hyperopt_loss_sharpe.py b/freqtrade/optimize/hyperopt_loss_sharpe.py new file mode 100644 index 000000000..5631a75de --- /dev/null +++ b/freqtrade/optimize/hyperopt_loss_sharpe.py @@ -0,0 +1,45 @@ +""" +SharpeHyperOptLoss + +This module defines the alternative HyperOptLoss class which can be used for +Hyperoptimization. +""" +from datetime import datetime + +from pandas import DataFrame +import numpy as np + +from freqtrade.optimize.hyperopt import IHyperOptLoss + + +class SharpeHyperOptLoss(IHyperOptLoss): + """ + Defines the loss function for hyperopt. + + This implementation uses the Sharpe Ratio calculation. + """ + + @staticmethod + def hyperopt_loss_function(results: DataFrame, trade_count: int, + min_date: datetime, max_date: datetime, + *args, **kwargs) -> float: + """ + Objective function, returns smaller number for more optimal results. + + Uses Sharpe Ratio calculation. + """ + total_profit = results.profit_percent + days_period = (max_date - min_date).days + + # adding slippage of 0.1% per trade + total_profit = total_profit - 0.0005 + expected_yearly_return = total_profit.sum() / days_period + + if (np.std(total_profit) != 0.): + sharp_ratio = expected_yearly_return / np.std(total_profit) * np.sqrt(365) + else: + # Define high (negative) sharpe ratio to be clear that this is NOT optimal. + sharp_ratio = -20. + + # print(expected_yearly_return, np.std(total_profit), sharp_ratio) + return -sharp_ratio diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index 5559c582f..a112c63b4 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -60,34 +60,27 @@ class IPairList(ABC): def _validate_whitelist(self, whitelist: List[str]) -> List[str]: """ Check available markets and remove pair from whitelist if necessary - :param whitelist: the sorted list (based on BaseVolume) of pairs the user might want to - trade - :return: the list of pairs the user wants to trade without the one unavailable or + :param whitelist: the sorted list of pairs the user might want to trade + :return: the list of pairs the user wants to trade without those unavailable or black_listed """ - sanitized_whitelist = whitelist markets = self._freqtrade.exchange.markets - # Filter to markets in stake currency - markets = [markets[pair] for pair in markets if - markets[pair]['quote'] == self._config['stake_currency']] - known_pairs = set() - - # TODO: we should loop over whitelist instead of all markets - for market in markets: - pair = market['symbol'] + sanitized_whitelist = set() + for pair in whitelist: # pair is not in the generated dynamic market, or in the blacklist ... ignore it - if pair not in whitelist or pair in self.blacklist: + if (pair in self.blacklist or pair not in markets + or not pair.endswith(self._config['stake_currency'])): + logger.warning(f"Pair {pair} is not compatible with exchange " + f"{self._freqtrade.exchange.name} or contained in " + f"your blacklist. Removing it from whitelist..") continue - # else the pair is valid - known_pairs.add(pair) - # Market is not active + # Check if market is active + market = markets[pair] if not market['active']: - sanitized_whitelist.remove(pair) - logger.info( - 'Ignoring %s from whitelist. Market is not active.', - pair - ) + logger.info(f"Ignoring {pair} from whitelist. Market is not active.") + continue + sanitized_whitelist.add(pair) # We need to remove pairs that are unknown - return [x for x in sanitized_whitelist if x in known_pairs] + return list(sanitized_whitelist) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 10aff72ec..c844bbc4c 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -25,15 +25,16 @@ _DECL_BASE: Any = declarative_base() _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, registers all known command handlers 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 """ - db_url = config.get('db_url', None) kwargs = {} # Take care of thread ownership if in-memory db @@ -57,7 +58,7 @@ def init(config: Dict) -> None: check_migrate(engine) # 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() @@ -83,7 +84,7 @@ def check_migrate(engine) -> None: logger.debug(f'trying {table_back_name}') # Check for latest column - if not has_column(cols, 'stoploss_last_update'): + if not has_column(cols, 'stop_loss_pct'): logger.info(f'Running database migration - backup available as {table_back_name}') fee_open = get_column_def(cols, 'fee_open', 'fee') @@ -91,10 +92,13 @@ def check_migrate(engine) -> None: open_rate_requested = get_column_def(cols, 'open_rate_requested', 'null') close_rate_requested = get_column_def(cols, 'close_rate_requested', 'null') stop_loss = get_column_def(cols, 'stop_loss', '0.0') + stop_loss_pct = get_column_def(cols, 'stop_loss_pct', 'null') initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0') + initial_stop_loss_pct = get_column_def(cols, 'initial_stop_loss_pct', 'null') stoploss_order_id = get_column_def(cols, 'stoploss_order_id', 'null') stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null') max_rate = get_column_def(cols, 'max_rate', '0.0') + min_rate = get_column_def(cols, 'min_rate', 'null') sell_reason = get_column_def(cols, 'sell_reason', 'null') strategy = get_column_def(cols, 'strategy', 'null') ticker_interval = get_column_def(cols, 'ticker_interval', 'null') @@ -112,8 +116,9 @@ def check_migrate(engine) -> None: (id, exchange, pair, is_open, fee_open, fee_close, open_rate, open_rate_requested, close_rate, close_rate_requested, close_profit, stake_amount, amount, open_date, close_date, open_order_id, - stop_loss, initial_stop_loss, stoploss_order_id, stoploss_last_update, - max_rate, sell_reason, strategy, + stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, + stoploss_order_id, stoploss_last_update, + max_rate, min_rate, sell_reason, strategy, ticker_interval ) select id, lower(exchange), @@ -128,9 +133,11 @@ def check_migrate(engine) -> None: open_rate, {open_rate_requested} open_rate_requested, close_rate, {close_rate_requested} close_rate_requested, close_profit, stake_amount, amount, open_date, close_date, open_order_id, - {stop_loss} stop_loss, {initial_stop_loss} initial_stop_loss, + {stop_loss} stop_loss, {stop_loss_pct} stop_loss_pct, + {initial_stop_loss} initial_stop_loss, + {initial_stop_loss_pct} initial_stop_loss_pct, {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update, - {max_rate} max_rate, {sell_reason} sell_reason, + {max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason, {strategy} strategy, {ticker_interval} ticker_interval from {table_back_name} """) @@ -183,14 +190,20 @@ class Trade(_DECL_BASE): open_order_id = Column(String) # absolute value of the stop loss stop_loss = Column(Float, nullable=True, default=0.0) + # percentage value of the stop loss + stop_loss_pct = Column(Float, nullable=True) # absolute value of the initial stop loss initial_stop_loss = Column(Float, nullable=True, default=0.0) + # percentage value of the initial stop loss + initial_stop_loss_pct = Column(Float, nullable=True) # stoploss order id which is on exchange stoploss_order_id = Column(String, nullable=True, index=True) # last update time of the stoploss order on exchange stoploss_last_update = Column(DateTime, nullable=True) # absolute value of the highest reached price max_rate = Column(Float, nullable=True, default=0.0) + # Lowest price reached + min_rate = Column(Float, nullable=True) sell_reason = Column(String, nullable=True) strategy = Column(String, nullable=True) ticker_interval = Column(Integer, nullable=True) @@ -201,8 +214,42 @@ class Trade(_DECL_BASE): return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, ' 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): + """ + Adjust the max_rate and min_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) + def adjust_stop_loss(self, current_price: float, stoploss: float, initial: bool = False): - """this adjusts the stop loss to it's most recently observed setting""" + """ + This adjusts the stop loss to it's most recently observed setting + :param current_price: Current rate the asset is traded + :param stoploss: Stoploss as factor (sample -0.05 -> -5% below current price). + :param initial: Called to initiate stop_loss. + Skips everything if self.stop_loss is already set. + """ if initial and not (self.stop_loss is None or self.stop_loss == 0): # Don't modify if called with initial and nothing to do @@ -210,24 +257,20 @@ class Trade(_DECL_BASE): new_loss = float(current_price * (1 - abs(stoploss))) - # keeping track of the highest observed rate for this trade - if self.max_rate is None: - self.max_rate = current_price - else: - if current_price > self.max_rate: - self.max_rate = current_price - # no stop loss assigned yet if not self.stop_loss: logger.debug("assigning new stop loss") self.stop_loss = new_loss + self.stop_loss_pct = -1 * abs(stoploss) self.initial_stop_loss = new_loss + self.initial_stop_loss_pct = -1 * abs(stoploss) self.stoploss_last_update = datetime.utcnow() # evaluate if the stop loss needs to be updated else: if new_loss > self.stop_loss: # stop losses only walk up, never down! self.stop_loss = new_loss + self.stop_loss_pct = -1 * abs(stoploss) self.stoploss_last_update = datetime.utcnow() logger.debug("adjusted stop loss") else: @@ -379,3 +422,22 @@ class Trade(_DECL_BASE): Query trades from persistence layer """ 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}, ") diff --git a/freqtrade/plot/__init__.py b/freqtrade/plot/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py new file mode 100644 index 000000000..d03d3ae53 --- /dev/null +++ b/freqtrade/plot/plotting.py @@ -0,0 +1,324 @@ +import logging +from pathlib import Path +from typing import Dict, List, Optional + +import pandas as pd + +from freqtrade.configuration import Arguments +from freqtrade.data import history +from freqtrade.data.btanalysis import (combine_tickers_with_mean, + create_cum_profit, load_trades) +from freqtrade.exchange import Exchange +from freqtrade.resolvers import ExchangeResolver, StrategyResolver + +logger = logging.getLogger(__name__) + + +try: + from plotly.subplots import make_subplots + from plotly.offline import plot + import plotly.graph_objects as go +except ImportError: + logger.exception("Module plotly not found \n Please install using `pip install plotly`") + exit(1) + + +def init_plotscript(config): + """ + Initialize objects needed for plotting + :return: Dict with tickers, trades, pairs and strategy + """ + exchange: Optional[Exchange] = None + + # Exchange is only needed when downloading data! + if config.get("live", False) or config.get("refresh_pairs", False): + 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 + timerange = Arguments.parse_timerange(config.get("timerange")) + + 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), + ) + + trades = load_trades(config) + return {"tickers": tickers, + "trades": trades, + "pairs": pairs, + "strategy": strategy, + } + + +def add_indicators(fig, row, indicators: List[str], data: pd.DataFrame) -> 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.add_trace(scattergl, row, 1) + else: + logger.info( + 'Indicator "%s" ignored. Reason: This indicator is not found ' + 'in your strategy.', + indicator + ) + + return fig + + +def add_profit(fig, row, data: pd.DataFrame, column: str, name: str) -> make_subplots: + """ + Add profit-plot + :param fig: Plot figure to append to + :param row: row number for this plot + :param data: candlestick DataFrame + :param column: Column to use for plot + :param name: Name to use + :return: fig with added profit plot + """ + profit = go.Scattergl( + x=data.index, + y=data[column], + name=name, + ) + fig.add_trace(profit, row, 1) + + return fig + + +def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: + """ + Add 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.add_trace(trade_buys, 1, 1) + fig.add_trace(trade_sells, 1, 1) + else: + logger.warning("No trades found.") + return fig + + +def generate_candlestick_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 = 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.add_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.add_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.add_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.add_trace(bb_lower, 1, 1) + fig.add_trace(bb_upper, 1, 1) + + # Add indicators to main plot + fig = add_indicators(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.add_trace(volume, 2, 1) + + # Add indicators to seperate row + fig = add_indicators(fig=fig, row=3, indicators=indicators2, data=data) + + return fig + + +def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame], + trades: pd.DataFrame) -> go.Figure: + # Combine close-values for all pairs, rename columns to "pair" + df_comb = combine_tickers_with_mean(tickers, "close") + + # Add combined cumulative profit + df_comb = create_cum_profit(df_comb, trades, 'cum_profit') + + # Plot the pairs average close prices, and total profit growth + avgclose = go.Scattergl( + x=df_comb.index, + y=df_comb['mean'], + name='Avg close price', + ) + + fig = make_subplots(rows=3, cols=1, shared_xaxes=True, row_width=[1, 1, 1]) + fig['layout'].update(title="Profit plot") + + fig.add_trace(avgclose, 1, 1) + fig = add_profit(fig, 2, df_comb, 'cum_profit', 'Profit') + + for pair in pairs: + profit_col = f'cum_profit_{pair}' + df_comb = create_cum_profit(df_comb, trades[trades['pair'] == pair], profit_col) + + fig = add_profit(fig, 3, df_comb, profit_col, f"Profit {pair}") + + return fig + + +def generate_plot_filename(pair, ticker_interval) -> str: + """ + Generate filenames per pair/ticker_interval to be used for storing plots + """ + pair_name = pair.replace("/", "_") + file_name = 'freqtrade-plot-' + pair_name + '-' + ticker_interval + '.html' + + logger.info('Generate plot file for %s', pair) + + return file_name + + +def store_plot_file(fig, filename: str, auto_open: bool = False) -> 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 + """ + + Path("user_data/plots").mkdir(parents=True, exist_ok=True) + _filename = Path('user_data/plots').joinpath(filename) + plot(fig, filename=str(_filename), + auto_open=auto_open) + logger.info(f"Stored plot as {_filename}") diff --git a/freqtrade/resolvers/__init__.py b/freqtrade/resolvers/__init__.py index 5cf6c616a..8f79349fe 100644 --- a/freqtrade/resolvers/__init__.py +++ b/freqtrade/resolvers/__init__.py @@ -1,5 +1,6 @@ from freqtrade.resolvers.iresolver import IResolver # 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.strategy_resolver import StrategyResolver # noqa: F401 diff --git a/freqtrade/resolvers/exchange_resolver.py b/freqtrade/resolvers/exchange_resolver.py index 8d1845c71..6fb12a65f 100644 --- a/freqtrade/resolvers/exchange_resolver.py +++ b/freqtrade/resolvers/exchange_resolver.py @@ -22,11 +22,13 @@ class ExchangeResolver(IResolver): Load the custom class from config parameter :param config: configuration dictionary """ + exchange_name = exchange_name.title() try: self.exchange = self._load_exchange(exchange_name, kwargs={'config': config}) except ImportError: logger.info( f"No {exchange_name} specific subclass found. Using the generic class instead.") + if not hasattr(self, "exchange"): self.exchange = Exchange(config) def _load_exchange( @@ -43,13 +45,13 @@ class ExchangeResolver(IResolver): exchange = ex_class(kwargs['config']) if exchange: - logger.info("Using resolved exchange %s", exchange_name) + logger.info(f"Using resolved exchange '{exchange_name}'...") return exchange except AttributeError: # Pass and raise ImportError instead pass raise ImportError( - "Impossible to load Exchange '{}'. This class does not exist" - " or contains Python code errors".format(exchange_name) + f"Impossible to load Exchange '{exchange_name}'. This class does not exist " + "or contains Python code errors." ) diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index e7683bc78..5027d7ddf 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -7,8 +7,10 @@ import logging from pathlib import Path from typing import Optional, Dict -from freqtrade.constants import DEFAULT_HYPEROPT +from freqtrade import OperationalException +from freqtrade.constants import DEFAULT_HYPEROPT, DEFAULT_HYPEROPT_LOSS from freqtrade.optimize.hyperopt_interface import IHyperOpt +from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss from freqtrade.resolvers import IResolver logger = logging.getLogger(__name__) @@ -21,17 +23,19 @@ class HyperOptResolver(IResolver): __slots__ = ['hyperopt'] - def __init__(self, config: Optional[Dict] = None) -> None: + def __init__(self, config: Dict) -> None: """ Load the custom class from config parameter - :param config: configuration dictionary or None + :param config: configuration dictionary """ - config = config or {} # Verify the hyperopt is in the configuration, otherwise fallback to the default hyperopt hyperopt_name = config.get('hyperopt') or DEFAULT_HYPEROPT 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'): logger.warning("Custom Hyperopt does not provide populate_buy_trend. " "Using populate_buy_trend from DefaultStrategy.") @@ -50,25 +54,75 @@ class HyperOptResolver(IResolver): current_path = Path(__file__).parent.parent.joinpath('optimize').resolve() abs_paths = [ - current_path.parent.parent.joinpath('user_data/hyperopts'), + Path.cwd().joinpath('user_data/hyperopts'), current_path, ] if extra_dir: # Add extra hyperopt directory on top of search paths - abs_paths.insert(0, Path(extra_dir)) + abs_paths.insert(0, Path(extra_dir).resolve()) - for _path in abs_paths: - try: - hyperopt = self._search_object(directory=_path, object_type=IHyperOpt, - object_name=hyperopt_name) - if hyperopt: - logger.info("Using resolved hyperopt %s from '%s'", hyperopt_name, _path) - return hyperopt - except FileNotFoundError: - logger.warning('Path "%s" does not exist', _path.relative_to(Path.cwd())) - - raise ImportError( - "Impossible to load Hyperopt '{}'. This class does not exist" - " or contains Python code errors".format(hyperopt_name) + hyperopt = self._load_object(paths=abs_paths, object_type=IHyperOpt, + object_name=hyperopt_name) + if hyperopt: + return hyperopt + raise OperationalException( + f"Impossible to load Hyperopt '{hyperopt_name}'. This class does not exist " + "or contains Python code errors." + ) + + +class HyperOptLossResolver(IResolver): + """ + This class contains all the logic to load custom hyperopt loss class + """ + + __slots__ = ['hyperoptloss'] + + def __init__(self, config: Optional[Dict] = None) -> None: + """ + Load the custom class from config parameter + :param config: configuration dictionary or None + """ + config = config or {} + + # Verify the hyperopt is in the configuration, otherwise fallback to the default hyperopt + hyperopt_name = config.get('hyperopt_loss') or DEFAULT_HYPEROPT_LOSS + self.hyperoptloss = self._load_hyperoptloss( + hyperopt_name, extra_dir=config.get('hyperopt_path')) + + # Assign ticker_interval to be used in hyperopt + self.hyperoptloss.__class__.ticker_interval = str(config['ticker_interval']) + + if not hasattr(self.hyperoptloss, 'hyperopt_loss_function'): + raise OperationalException( + f"Found hyperopt {hyperopt_name} does not implement `hyperopt_loss_function`.") + + def _load_hyperoptloss( + self, hyper_loss_name: str, extra_dir: Optional[str] = None) -> IHyperOptLoss: + """ + Search and loads the specified hyperopt loss class. + :param hyper_loss_name: name of the module to import + :param extra_dir: additional directory to search for the given hyperopt + :return: HyperOptLoss instance or None + """ + current_path = Path(__file__).parent.parent.joinpath('optimize').resolve() + + abs_paths = [ + Path.cwd().joinpath('user_data/hyperopts'), + current_path, + ] + + if extra_dir: + # Add extra hyperopt directory on top of search paths + abs_paths.insert(0, Path(extra_dir).resolve()) + + hyperoptloss = self._load_object(paths=abs_paths, object_type=IHyperOptLoss, + object_name=hyper_loss_name) + if hyperoptloss: + return hyperoptloss + + raise OperationalException( + f"Impossible to load HyperoptLoss '{hyper_loss_name}'. This class does not exist " + "or contains Python code errors." ) diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 86b9a799b..841c3cf43 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -7,7 +7,7 @@ import importlib.util import inspect import logging from pathlib import Path -from typing import Optional, Type, Any +from typing import Any, List, Optional, Tuple, Type, Union logger = logging.getLogger(__name__) @@ -45,7 +45,7 @@ class IResolver(object): @staticmethod def _search_object(directory: Path, object_type, object_name: str, - kwargs: dict = {}) -> Optional[Any]: + kwargs: dict = {}) -> Union[Tuple[Any, Path], Tuple[None, None]]: """ Search for the objectname in the given directory :param directory: relative or absolute directory path @@ -57,9 +57,33 @@ class IResolver(object): if not str(entry).endswith('.py'): logger.debug('Ignoring %s', entry) continue + module_path = Path.resolve(directory.joinpath(entry)) obj = IResolver._get_valid_object( - object_type, Path.resolve(directory.joinpath(entry)), object_name + object_type, module_path, object_name ) if obj: - return obj(**kwargs) + return (obj(**kwargs), module_path) + return (None, None) + + @staticmethod + def _load_object(paths: List[Path], object_type, object_name: str, + kwargs: dict = {}) -> Optional[Any]: + """ + Try to load object from path list. + """ + + for _path in paths: + try: + (module, module_path) = IResolver._search_object(directory=_path, + object_type=object_type, + object_name=object_name, + kwargs=kwargs) + if module: + logger.info( + f"Using resolved {object_type.__name__.lower()[1:]} {object_name} " + f"from '{module_path}'...") + return module + except FileNotFoundError: + logger.warning('Path "%s" does not exist.', _path.resolve()) + return None diff --git a/freqtrade/resolvers/pairlist_resolver.py b/freqtrade/resolvers/pairlist_resolver.py index 4306a9669..3d95c0295 100644 --- a/freqtrade/resolvers/pairlist_resolver.py +++ b/freqtrade/resolvers/pairlist_resolver.py @@ -6,6 +6,7 @@ This module load custom hyperopts import logging from pathlib import Path +from freqtrade import OperationalException from freqtrade.pairlist.IPairList import IPairList from freqtrade.resolvers import IResolver @@ -38,22 +39,15 @@ class PairListResolver(IResolver): current_path = Path(__file__).parent.parent.joinpath('pairlist').resolve() abs_paths = [ - current_path.parent.parent.joinpath('user_data/pairlist'), + Path.cwd().joinpath('user_data/pairlist'), current_path, ] - for _path in abs_paths: - try: - pairlist = self._search_object(directory=_path, object_type=IPairList, - object_name=pairlist_name, - kwargs=kwargs) - if pairlist: - logger.info("Using resolved pairlist %s from '%s'", pairlist_name, _path) - return pairlist - except FileNotFoundError: - logger.warning('Path "%s" does not exist', _path.relative_to(Path.cwd())) - - raise ImportError( - "Impossible to load Pairlist '{}'. This class does not exist" - " or contains Python code errors".format(pairlist_name) + pairlist = self._load_object(paths=abs_paths, object_type=IPairList, + object_name=pairlist_name, kwargs=kwargs) + if pairlist: + return pairlist + raise OperationalException( + f"Impossible to load Pairlist '{pairlist_name}'. This class does not exist " + "or contains Python code errors." ) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index cece5a5d1..aa73327ff 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -11,7 +11,7 @@ from inspect import getfullargspec from pathlib import Path from typing import Dict, Optional -from freqtrade import constants +from freqtrade import constants, OperationalException from freqtrade.resolvers import IResolver from freqtrade.strategy import import_strategy from freqtrade.strategy.interface import IStrategy @@ -56,6 +56,8 @@ class StrategyResolver(IResolver): ("process_only_new_candles", None, False), ("order_types", None, False), ("order_time_in_force", None, False), + ("stake_currency", None, False), + ("stake_amount", None, False), ("use_sell_signal", False, True), ("sell_profit_only", False, True), ("ignore_roi_if_buy_signal", False, True), @@ -130,7 +132,7 @@ class StrategyResolver(IResolver): abs_paths.insert(0, Path(extra_dir).resolve()) if ":" in strategy_name: - logger.info("loading base64 endocded strategy") + logger.info("loading base64 encoded strategy") strat = strategy_name.split(":") if len(strat) == 2: @@ -145,22 +147,21 @@ class StrategyResolver(IResolver): # register temp path with the bot abs_paths.insert(0, temp.resolve()) - for _path in abs_paths: + strategy = self._load_object(paths=abs_paths, object_type=IStrategy, + object_name=strategy_name, kwargs={'config': config}) + if strategy: + strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args) + strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) + strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args) + try: - strategy = self._search_object(directory=_path, object_type=IStrategy, - object_name=strategy_name, kwargs={'config': config}) - if strategy: - logger.info("Using resolved strategy %s from '%s'", strategy_name, _path) - strategy._populate_fun_len = len( - getfullargspec(strategy.populate_indicators).args) - strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) - strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args) + return import_strategy(strategy, config=config) + except TypeError as e: + logger.warning( + f"Impossible to load strategy '{strategy_name}'. " + f"Error: {e}") - return import_strategy(strategy, config=config) - except FileNotFoundError: - logger.warning('Path "%s" does not exist', _path.relative_to(Path.cwd())) - - raise ImportError( - "Impossible to load Strategy '{}'. This class does not exist" - " or contains Python code errors".format(strategy_name) + raise OperationalException( + f"Impossible to load Strategy '{strategy_name}'. This class does not exist " + "or contains Python code errors." ) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py new file mode 100644 index 000000000..711202b27 --- /dev/null +++ b/freqtrade/rpc/api_server.py @@ -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) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index a0ffff107..f77e0eddb 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -48,6 +48,11 @@ class RPCException(Exception): def __str__(self): return self.message + def __json__(self): + return { + 'msg': self.message + } + class RPC(object): """ @@ -100,20 +105,17 @@ class RPC(object): current_profit = trade.calc_profit_percent(current_rate) fmt_close_profit = (f'{round(trade.close_profit * 100, 2):.2f}%' if trade.close_profit else None) - results.append(dict( - trade_id=trade.id, - pair=trade.pair, - 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), + trade_dict = trade.to_json() + trade_dict.update(dict( + base_currency=self._freqtrade.config['stake_currency'], close_profit=fmt_close_profit, + current_rate=current_rate, current_profit=round(current_profit * 100, 2), open_order='({} {} rem={:.8f})'.format( - order['type'], order['side'], order['remaining'] + order['type'], order['side'], order['remaining'] ) if order else None, )) + results.append(trade_dict) return results def _rpc_status_table(self) -> DataFrame: @@ -279,11 +281,13 @@ class RPC(object): rate = 1.0 else: try: - if coin == 'USDT': - rate = 1.0 / self._freqtrade.get_sell_rate('BTC/USDT', False) + pair = self._freqtrade.exchange.get_valid_pair_combination(coin, "BTC") + if pair.startswith("BTC"): + rate = 1.0 / self._freqtrade.get_sell_rate(pair, False) else: - rate = self._freqtrade.get_sell_rate(coin + '/BTC', False) + rate = self._freqtrade.get_sell_rate(pair, False) except (TemporaryError, DependencyException): + logger.warning(f" Could not get rate for pair {coin}.") continue est_btc: float = rate * balance['total'] total = total + est_btc @@ -295,7 +299,10 @@ class RPC(object): 'est_btc': est_btc, }) if total == 0.0: - raise RPCException('all balances are zero') + if self._freqtrade.config.get('dry_run', False): + raise RPCException('Running in Dry Run, balances are not available.') + else: + raise RPCException('All balances are zero.') symbol = fiat_display_currency value = self._fiat_converter.convert_amount(total, 'BTC', @@ -338,7 +345,7 @@ class RPC(object): 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 . Sells the given trade at current price @@ -378,7 +385,7 @@ class RPC(object): for trade in Trade.get_open_trades(): _exec_forcesell(trade) Trade.session.flush() - return + return {'result': 'Created sell orders for all open trades.'} # Query for trade trade = Trade.query.filter( @@ -393,6 +400,7 @@ class RPC(object): _exec_forcesell(trade) Trade.session.flush() + return {'result': f'Created sell order for trade {trade_id}.'} def _rpc_forcebuy(self, pair: str, price: Optional[float]) -> Optional[Trade]: """ @@ -446,17 +454,43 @@ class RPC(object): for pair, rate, count in pair_rates ] - def _rpc_count(self) -> List[Trade]: + def _rpc_count(self) -> Dict[str, float]: """ Returns the number of trades running """ if self._freqtrade.state != State.RUNNING: raise RPCException('trader is not running') - return Trade.get_open_trades() + trades = Trade.get_open_trades() + return { + 'current': len(trades), + 'max': float(self._freqtrade.config['max_open_trades']), + 'total_stake': sum((trade.open_rate * trade.amount) for trade in trades) + } def _rpc_whitelist(self) -> Dict: """ Returns the currently active whitelist""" res = {'method': self._freqtrade.pairlists.name, - 'length': len(self._freqtrade.pairlists.whitelist), + 'length': len(self._freqtrade.active_pair_whitelist), 'whitelist': self._freqtrade.active_pair_whitelist } return res + + def _rpc_blacklist(self, add: List[str] = None) -> Dict: + """ Returns the currently active blacklist""" + if add: + stake_currency = self._freqtrade.config.get('stake_currency') + for pair in add: + if (pair.endswith(stake_currency) + and pair not in self._freqtrade.pairlists.blacklist): + self._freqtrade.pairlists.blacklist.append(pair) + + res = {'method': self._freqtrade.pairlists.name, + 'length': len(self._freqtrade.pairlists.blacklist), + 'blacklist': self._freqtrade.pairlists.blacklist, + } + return res + + def _rpc_edge(self) -> List[Dict[str, Any]]: + """ Returns information related to Edge """ + if not self._freqtrade.edge: + raise RPCException(f'Edge is not enabled.') + return self._freqtrade.edge.accepted_pairs() diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index 7f0d0a5d4..fad532aa0 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -29,6 +29,12 @@ class RPCManager(object): from freqtrade.rpc.webhook import Webhook 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: """ Stops all enabled rpc modules """ logger.info('Cleaning up rpc modules ...') diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 6771ec803..fe4929780 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -4,7 +4,7 @@ This module manage Telegram communication """ import logging -from typing import Any, Callable, Dict +from typing import Any, Callable, Dict, List from tabulate import tabulate from telegram import Bot, ParseMode, ReplyKeyboardMarkup, Update @@ -20,7 +20,10 @@ logger = logging.getLogger(__name__) logger.debug('Included module rpc.telegram ...') -def authorized_only(command_handler: Callable[[Any, Bot, Update], None]) -> Callable[..., Any]: +MAX_TELEGRAM_MESSAGE_LENGTH = 4096 + + +def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]: """ Decorator to check if the message comes from the correct chat_id :param command_handler: Telegram CommandHandler @@ -93,6 +96,8 @@ class Telegram(RPC): CommandHandler('reload_conf', self._reload_conf), CommandHandler('stopbuy', self._stopbuy), CommandHandler('whitelist', self._whitelist), + CommandHandler('blacklist', self._blacklist, pass_args=True), + CommandHandler('edge', self._edge), CommandHandler('help', self._help), CommandHandler('version', self._version), ] @@ -127,7 +132,7 @@ class Telegram(RPC): msg['stake_amount_fiat'] = 0 message = ("*{exchange}:* Buying {pair}\n" - "with limit `{limit:.8f}\n" + "at rate `{limit:.8f}\n" "({stake_amount:.6f} {stake_currency}").format(**msg) if msg.get('fiat_currency', None): @@ -139,7 +144,7 @@ class Telegram(RPC): msg['profit_percent'] = round(msg['profit_percent'] * 100, 2) message = ("*{exchange}:* Selling {pair}\n" - "*Limit:* `{limit:.8f}`\n" + "*Rate:* `{limit:.8f}`\n" "*Amount:* `{amount:.8f}`\n" "*Open Rate:* `{open_rate:.8f}`\n" "*Current Rate:* `{current_rate:.8f}`\n" @@ -188,25 +193,36 @@ class Telegram(RPC): try: results = self._rpc_trade_status() - # pre format data - for result in results: - result['date'] = result['date'].humanize() - messages = [ - "*Trade ID:* `{trade_id}`\n" - "*Current Pair:* {pair}\n" - "*Open Since:* `{date}`\n" - "*Amount:* `{amount}`\n" - "*Open Rate:* `{open_rate:.8f}`\n" - "*Close Rate:* `{close_rate}`\n" - "*Current Rate:* `{current_rate:.8f}`\n" - "*Close Profit:* `{close_profit}`\n" - "*Current Profit:* `{current_profit:.2f}%`\n" - "*Open Order:* `{open_order}`".format(**result) - for result in results - ] + messages = [] + for r in results: + lines = [ + "*Trade ID:* `{trade_id}` `(since {open_date_hum})`", + "*Current Pair:* {pair}", + "*Amount:* `{amount} ({stake_amount} {base_currency})`", + "*Open Rate:* `{open_rate:.8f}`", + "*Close Rate:* `{close_rate}`" if r['close_rate'] else "", + "*Current Rate:* `{current_rate:.8f}`", + "*Close Profit:* `{close_profit}`" if r['close_profit'] else "", + "*Current Profit:* `{current_profit:.2f}%`", + + # Adding initial stoploss only if it is different from stoploss + "*Initial Stoploss:* `{initial_stop_loss:.8f}` " + + ("`({initial_stop_loss_pct:.2f}%)`" if r['initial_stop_loss_pct'] else "") + if r['stop_loss'] != r['initial_stop_loss'] else "", + + # Adding stoploss and stoploss percentage only if it is not None + "*Stoploss:* `{stop_loss:.8f}` " + + ("`({stop_loss_pct:.2f}%)`" if r['stop_loss_pct'] else ""), + + "*Open Order:* `{open_order}`" if r['open_order'] else "" + ] + # Filter empty lines using list-comprehension + messages.append("\n".join([l for l in lines if l]).format(**r)) + for msg in messages: self._send_msg(msg, bot=bot) + except RPCException as e: self._send_msg(str(e), bot=bot) @@ -251,7 +267,8 @@ class Telegram(RPC): headers=[ 'Day', f'Profit {stake_cur}', - f'Profit {fiat_disp_cur}' + f'Profit {fiat_disp_cur}', + f'Trades' ], tablefmt='simple') message = f'Daily Profit over the last {timescale} days:\n
{stats_tab}
' @@ -312,13 +329,20 @@ class Telegram(RPC): output = '' for currency in result['currencies']: if currency['est_btc'] > 0.0001: - output += "*{currency}:*\n" \ + curr_output = "*{currency}:*\n" \ "\t`Available: {available: .8f}`\n" \ "\t`Balance: {balance: .8f}`\n" \ "\t`Pending: {pending: .8f}`\n" \ "\t`Est. BTC: {est_btc: .8f}`\n".format(**currency) else: - output += "*{currency}:* not showing <1$ amount \n".format(**currency) + curr_output = "*{currency}:* not showing <1$ amount \n".format(**currency) + + # Handle overflowing messsage length + if len(output + curr_output) >= MAX_TELEGRAM_MESSAGE_LENGTH: + self._send_msg(output, bot=bot) + output = curr_output + else: + output += curr_output output += "\n*Estimated Value*:\n" \ "\t`BTC: {total: .8f}`\n" \ @@ -387,7 +411,9 @@ class Telegram(RPC): trade_id = update.message.text.replace('/forcesell', '').strip() 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: self._send_msg(str(e), bot=bot) @@ -441,12 +467,10 @@ class Telegram(RPC): :return: None """ try: - trades = self._rpc_count() - message = tabulate({ - 'current': [len(trades)], - 'max': [self._config['max_open_trades']], - 'total stake': [sum((trade.open_rate * trade.amount) for trade in trades)] - }, headers=['current', 'max', 'total stake'], tablefmt='simple') + counts = self._rpc_count() + message = tabulate({k: [v] for k, v in counts.items()}, + headers=['current', 'max', 'total stake'], + tablefmt='simple') message = "
{}
".format(message) logger.debug(message) self._send_msg(message, parse_mode=ParseMode.HTML) @@ -470,6 +494,38 @@ class Telegram(RPC): except RPCException as e: self._send_msg(str(e), bot=bot) + @authorized_only + def _blacklist(self, bot: Bot, update: Update, args: List[str]) -> None: + """ + Handler for /blacklist + Shows the currently active blacklist + """ + try: + + blacklist = self._rpc_blacklist(args) + + message = f"Blacklist contains {blacklist['length']} pairs\n" + message += f"`{', '.join(blacklist['blacklist'])}`" + + logger.debug(message) + self._send_msg(message) + except RPCException as e: + self._send_msg(str(e), bot=bot) + + @authorized_only + def _edge(self, bot: Bot, update: Update) -> None: + """ + Handler for /edge + Shows information related to Edge + """ + try: + edge_pairs = self._rpc_edge() + edge_pairs_tab = tabulate(edge_pairs, headers='keys', tablefmt='simple') + message = f'Edge only validated following pairs:\n
{edge_pairs_tab}
' + self._send_msg(message, bot=bot, parse_mode=ParseMode.HTML) + except RPCException as e: + self._send_msg(str(e), bot=bot) + @authorized_only def _help(self, bot: Bot, update: Update) -> None: """ @@ -497,6 +553,9 @@ class Telegram(RPC): "*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" \ "*/reload_conf:* `Reload configuration file` \n" \ "*/whitelist:* `Show current whitelist` \n" \ + "*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs " \ + "to the blacklist.` \n" \ + "*/edge:* `Shows validated pairs by Edge if it is enabeld` \n" \ "*/help:* `This help message`\n" \ "*/version:* `Show version`" diff --git a/freqtrade/state.py b/freqtrade/state.py index b69c26cb5..ce2683a77 100644 --- a/freqtrade/state.py +++ b/freqtrade/state.py @@ -18,11 +18,11 @@ class State(Enum): class RunMode(Enum): """ Bot running mode (backtest, hyperopt, ...) - can be "live", "dry-run", "backtest", "edgecli", "hyperopt". + can be "live", "dry-run", "backtest", "edge", "hyperopt". """ LIVE = "live" DRY_RUN = "dry_run" BACKTEST = "backtest" - EDGECLI = "edgecli" + EDGE = "edge" HYPEROPT = "hyperopt" OTHER = "other" # Used for plotting scripts and test diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index b29e26ef9..c62bfe5dc 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -6,6 +6,7 @@ from freqtrade.strategy.interface import IStrategy # Import Default-Strategy to have hyperopt correctly resolve from freqtrade.strategy.default_strategy import DefaultStrategy # noqa: F401 + logger = logging.getLogger(__name__) @@ -16,7 +17,6 @@ def import_strategy(strategy: IStrategy, config: dict) -> IStrategy: """ # Copy all attributes from base class and class - comb = {**strategy.__class__.__dict__, **strategy.__dict__} # 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'] attr = deepcopy(comb) + # Adjust module name attr['__module__'] = 'freqtrade.strategy' diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 41dcb8c57..37aa97bb1 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -6,17 +6,18 @@ import logging from abc import ABC, abstractmethod from datetime import datetime from enum import Enum -from typing import Dict, List, NamedTuple, Tuple +from typing import Dict, List, NamedTuple, Optional, Tuple import warnings import arrow from pandas import DataFrame -from freqtrade import constants from freqtrade.data.dataprovider import DataProvider +from freqtrade.exchange import timeframe_to_minutes from freqtrade.persistence import Trade from freqtrade.wallets import Wallets + logger = logging.getLogger(__name__) @@ -157,7 +158,24 @@ class IStrategy(ABC): """ Parses the given ticker history and returns a populated DataFrame add several TA indicators and buy signal to it - :return DataFrame with ticker data and indicator data + :param dataframe: Dataframe containing ticker data + :param metadata: Metadata dictionary with additional data (e.g. 'pair') + :return: DataFrame with ticker data and indicator data + """ + logger.debug("TA Analysis Launched") + dataframe = self.advise_indicators(dataframe, metadata) + dataframe = self.advise_buy(dataframe, metadata) + dataframe = self.advise_sell(dataframe, metadata) + return dataframe + + def _analyze_ticker_internal(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Parses the given ticker history and returns a populated DataFrame + add several TA indicators and buy signal to it + WARNING: Used internally only, may skip analysis if `process_only_new_candles` is set. + :param dataframe: Dataframe containing ticker data + :param metadata: Metadata dictionary with additional data (e.g. 'pair') + :return: DataFrame with ticker data and indicator data """ pair = str(metadata.get('pair')) @@ -167,10 +185,7 @@ class IStrategy(ABC): if (not self.process_only_new_candles or self._last_candle_seen_per_pair.get(pair, None) != dataframe.iloc[-1]['date']): # Defs that only make change on new candle data. - logger.debug("TA Analysis Launched") - dataframe = self.advise_indicators(dataframe, metadata) - dataframe = self.advise_buy(dataframe, metadata) - dataframe = self.advise_sell(dataframe, metadata) + dataframe = self.analyze_ticker(dataframe, metadata) self._last_candle_seen_per_pair[pair] = dataframe.iloc[-1]['date'] else: logger.debug("Skipping TA Analysis for already analyzed candle") @@ -197,7 +212,7 @@ class IStrategy(ABC): return False, False try: - dataframe = self.analyze_ticker(dataframe, {'pair': pair}) + dataframe = self._analyze_ticker_internal(dataframe, {'pair': pair}) except ValueError as error: logger.warning( 'Unable to analyze ticker for pair %s: %s', @@ -221,7 +236,7 @@ class IStrategy(ABC): # Check if dataframe is out of date signal_date = arrow.get(latest['date']) - interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval] + interval_minutes = timeframe_to_minutes(interval) offset = self.config.get('exchange', {}).get('outdated_offset', 5) if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + offset))): logger.warning( @@ -247,6 +262,9 @@ class IStrategy(ABC): """ This function evaluate if on the condition required to trigger a sell has been reached if the threshold is reached and updates the trade record. + :param low: Only used during backtesting to simulate stoploss + :param high: Only used during backtesting, to simulate ROI + :param force_stoploss: Externally provided stoploss :return: True if trade should be sold, False otherwise """ @@ -254,14 +272,16 @@ class IStrategy(ABC): current_rate = low or rate current_profit = trade.calc_profit_percent(current_rate) + trade.adjust_min_max_rates(high or current_rate) + stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, current_time=date, current_profit=current_profit, - force_stoploss=force_stoploss) + force_stoploss=force_stoploss, high=high) if stoplossflag.sell_flag: return stoplossflag - # Set current rate to low for backtesting sell + # Set current rate to high for backtesting sell current_rate = high or rate current_profit = trade.calc_profit_percent(current_rate) experimental = self.config.get('experimental', {}) @@ -285,8 +305,9 @@ class IStrategy(ABC): return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) - def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: datetime, - current_profit: float, force_stoploss: float) -> SellCheckTuple: + def stop_loss_reached(self, current_rate: float, trade: Trade, + current_time: datetime, current_profit: float, + force_stoploss: float, high: float = None) -> SellCheckTuple: """ Based on current profit of the trade and configured (trailing) stoploss, decides to sell or not @@ -294,16 +315,39 @@ class IStrategy(ABC): """ trailing_stop = self.config.get('trailing_stop', False) - trade.adjust_stop_loss(trade.open_rate, force_stoploss if force_stoploss - else self.stoploss, initial=True) + stop_loss_value = force_stoploss if force_stoploss else self.stoploss + + # Initiate stoploss with open_rate. Does nothing if stoploss is already set. + trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True) + + if trailing_stop: + # trailing stoploss handling + sl_offset = self.config.get('trailing_stop_positive_offset') or 0.0 + 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. + if not (tsl_only_offset and high_profit < sl_offset): + # Specific handling for trailing_stop_positive + if 'trailing_stop_positive' in self.config and high_profit > sl_offset: + # Ignore mypy error check in configuration that this is a float + stop_loss_value = self.config.get('trailing_stop_positive') # type: ignore + logger.debug(f"using positive stop loss: {stop_loss_value} " + f"offset: {sl_offset:.4g} profit: {current_profit:.4f}%") + + trade.adjust_stop_loss(high or current_rate, stop_loss_value) # evaluate if the stoploss was hit if stoploss is not on exchange if ((self.stoploss is not None) and - (trade.stop_loss >= current_rate) and - (not self.order_types.get('stoploss_on_exchange'))): + (trade.stop_loss >= current_rate) and + (not self.order_types.get('stoploss_on_exchange'))): + 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 logger.debug( f"HIT STOP: current price at {current_rate:.6f}, " @@ -315,48 +359,34 @@ class IStrategy(ABC): logger.debug('Stop loss hit.') return SellCheckTuple(sell_flag=True, sell_type=selltype) - # update the stop loss afterwards, after all by definition it's supposed to be hanging - if trailing_stop: - - # check if we have a special stop loss for positive condition - # and if profit is positive - stop_loss_value = force_stoploss if force_stoploss else self.stoploss - - sl_offset = self.config.get('trailing_stop_positive_offset') or 0.0 - - if 'trailing_stop_positive' in self.config and current_profit > sl_offset: - - # Ignore mypy error check in configuration that this is a float - stop_loss_value = self.config.get('trailing_stop_positive') # type: ignore - logger.debug(f"using positive stop loss mode: {stop_loss_value} " - f"with offset {sl_offset:.4g} " - f"since we have profit {current_profit:.4f}%") - - # if trailing_only_offset_is_reached is true, - # we update trailing stoploss only if offset is reached. - tsl_only_offset = self.config.get('trailing_only_offset_is_reached', False) - if not (tsl_only_offset and current_profit < sl_offset): - trade.adjust_stop_loss(current_rate, stop_loss_value) - return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) + def min_roi_reached_entry(self, trade_dur: int) -> Optional[float]: + """ + Based on trade duration defines the ROI entry that may have been reached. + :param trade_dur: trade duration in minutes + :return: minimal ROI entry value or None if none proper ROI entry was found. + """ + # Get highest entry in ROI dict where key <= trade-duration + roi_list = list(filter(lambda x: x <= trade_dur, self.minimal_roi.keys())) + if not roi_list: + return None + roi_entry = max(roi_list) + return self.minimal_roi[roi_entry] + def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool: """ - Based an earlier trade and current price and ROI configuration, decides whether bot should + Based on trade duration, current price and ROI configuration, decides whether bot should 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 - trade_dur = (current_time.timestamp() - trade.open_date.timestamp()) / 60 - - # Get highest entry in ROI dict where key >= trade-duration - roi_entry = max(list(filter(lambda x: trade_dur >= x, self.minimal_roi.keys()))) - threshold = self.minimal_roi[roi_entry] - if current_profit > threshold: - return True - - return False + trade_dur = int((current_time.timestamp() - trade.open_date.timestamp()) // 60) + roi = self.min_roi_reached_entry(trade_dur) + if roi is None: + return False + else: + return current_profit > roi def tickerdata_to_dataframe(self, tickerdata: Dict[str, List]) -> Dict[str, DataFrame]: """ @@ -373,6 +403,7 @@ class IStrategy(ABC): :param metadata: Additional information, like the currently traded pair :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: warnings.warn("deprecated - check out the Sample strategy to see " "the current function headers!", DeprecationWarning) @@ -388,6 +419,7 @@ class IStrategy(ABC): :param pair: Additional information, like the currently traded pair :return: DataFrame with buy column """ + logger.debug(f"Populating buy signals for pair {metadata.get('pair')}.") if self._buy_fun_len == 2: warnings.warn("deprecated - check out the Sample strategy to see " "the current function headers!", DeprecationWarning) @@ -403,6 +435,7 @@ class IStrategy(ABC): :param pair: Additional information, like the currently traded pair :return: DataFrame with sell column """ + logger.debug(f"Populating sell signals for pair {metadata.get('pair')}.") if self._sell_fun_len == 2: warnings.warn("deprecated - check out the Sample strategy to see " "the current function headers!", DeprecationWarning) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 26262cb4b..4b9bf6cd8 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -2,21 +2,25 @@ import json import logging import re +from copy import deepcopy from datetime import datetime from functools import reduce -from typing import Dict, Optional +from pathlib import Path from unittest.mock import MagicMock, PropertyMock import arrow import pytest from telegram import Chat, Message, Update -from freqtrade import constants +from freqtrade import constants, persistence +from freqtrade.configuration import Arguments from freqtrade.data.converter import parse_ticker_dataframe -from freqtrade.exchange import Exchange from freqtrade.edge import Edge, PairInfo +from freqtrade.exchange import Exchange from freqtrade.freqtradebot import FreqtradeBot from freqtrade.resolvers import ExchangeResolver +from freqtrade.worker import Worker + logging.getLogger('').setLevel(logging.INFO) @@ -35,6 +39,17 @@ def log_has_re(line, logs): False) +def get_args(args): + return Arguments(args, '').get_parsed_arg() + + +def patched_configuration_load_config_file(mocker, config) -> None: + mocker.patch( + 'freqtrade.configuration.configuration.load_config_file', + lambda *args, **kwargs: config + ) + + 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.validate_pairs', MagicMock()) @@ -53,7 +68,7 @@ def get_patched_exchange(mocker, config, api_mock=None, id='bittrex') -> Exchang patch_exchange(mocker, api_mock, id) config["exchange"]["name"] = id try: - exchange = ExchangeResolver(id.title(), config).exchange + exchange = ExchangeResolver(id, config).exchange except ImportError: exchange = Exchange(config) return exchange @@ -88,24 +103,54 @@ def get_patched_edge(mocker, config) -> Edge: # Functions for recurrent object patching -def get_patched_freqtradebot(mocker, config) -> FreqtradeBot: +def patch_freqtradebot(mocker, config) -> None: """ This function patch _init_modules() to not call dependencies :param mocker: a Mocker object to apply patches :param config: Config to pass to the bot :return: None """ - patch_coinmarketcap(mocker, {'price_usd': 12345.0}) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) + persistence.init(config['db_url']) patch_exchange(mocker, None) mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock()) + +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 patch_coinmarketcap(mocker, value: Optional[Dict[str, float]] = None) -> None: +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 + + +@pytest.fixture(autouse=True) +def patch_coinmarketcap(mocker) -> None: """ Mocker to coinmarketcap to speed up tests :param mocker: mocker to patch coinmarketcap class @@ -126,6 +171,11 @@ def patch_coinmarketcap(mocker, value: Optional[Dict[str, float]] = None) -> Non ) +@pytest.fixture(scope='function') +def init_persistence(default_conf): + persistence.init(default_conf['db_url'], default_conf['dry_run']) + + @pytest.fixture(scope="function") def default_conf(): """ Returns validated configuration suitable for most tests """ @@ -171,6 +221,10 @@ def default_conf(): "LTC/BTC", "XRP/BTC", "NEO/BTC" + ], + "pair_blacklist": [ + "DOGE/BTC", + "HOT/BTC", ] }, "telegram": { @@ -180,7 +234,7 @@ def default_conf(): }, "initial_state": "running", "db_url": "sqlite://", - "loglevel": logging.DEBUG, + "verbosity": 3, } return configuration @@ -250,7 +304,7 @@ def markets(): 'max': 500000, }, }, - 'info': '', + 'info': {}, }, 'TKN/BTC': { 'id': 'tknbtc', @@ -275,7 +329,7 @@ def markets(): 'max': 500000, }, }, - 'info': '', + 'info': {}, }, 'BLK/BTC': { 'id': 'blkbtc', @@ -300,7 +354,7 @@ def markets(): 'max': 500000, }, }, - 'info': '', + 'info': {}, }, 'LTC/BTC': { 'id': 'ltcbtc', @@ -325,7 +379,7 @@ def markets(): 'max': 500000, }, }, - 'info': '', + 'info': {}, }, 'XRP/BTC': { 'id': 'xrpbtc', @@ -350,7 +404,7 @@ def markets(): 'max': 500000, }, }, - 'info': '', + 'info': {}, }, 'NEO/BTC': { 'id': 'neobtc', @@ -375,7 +429,7 @@ def markets(): 'max': 500000, }, }, - 'info': '', + 'info': {}, }, 'BTT/BTC': { 'id': 'BTTBTC', @@ -403,7 +457,7 @@ def markets(): 'max': None } }, - 'info': "", + 'info': {}, }, 'ETH/USDT': { 'id': 'USDT-ETH', @@ -425,7 +479,7 @@ def markets(): } }, 'active': True, - 'info': "" + 'info': {}, }, 'LTC/USDT': { 'id': 'USDT-LTC', @@ -447,7 +501,7 @@ def markets(): 'max': None } }, - 'info': "" + 'info': {}, } } @@ -627,7 +681,7 @@ def ticker_history_list(): @pytest.fixture 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 @@ -831,8 +885,9 @@ def tickers(): @pytest.fixture def result(): - with open('freqtrade/tests/testdata/UNITTEST_BTC-1m.json') as data_file: - return parse_ticker_dataframe(json.load(data_file), '1m', True) + with Path('freqtrade/tests/testdata/UNITTEST_BTC-1m.json').open('r') as data_file: + return parse_ticker_dataframe(json.load(data_file), '1m', pair="UNITTEST/BTC", + fill_missing=True) # FIX: # Create an fixture/function @@ -930,9 +985,10 @@ def buy_order_fee(): @pytest.fixture(scope="function") def edge_conf(default_conf): - default_conf['max_open_trades'] = -1 - default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT - default_conf['edge'] = { + conf = deepcopy(default_conf) + conf['max_open_trades'] = -1 + conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT + conf['edge'] = { "enabled": True, "process_throttle_secs": 1800, "calculate_since_number_of_days": 14, @@ -948,4 +1004,40 @@ def edge_conf(default_conf): "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 + }, + } diff --git a/freqtrade/tests/data/test_btanalysis.py b/freqtrade/tests/data/test_btanalysis.py index dd7cbe0d9..bad8db66f 100644 --- a/freqtrade/tests/data/test_btanalysis.py +++ b/freqtrade/tests/data/test_btanalysis.py @@ -1,8 +1,19 @@ -import pytest -from pandas import DataFrame +from unittest.mock import MagicMock -from freqtrade.data.btanalysis import BT_DATA_COLUMNS, load_backtest_data -from freqtrade.data.history import make_testdata_path +import pytest +from arrow import Arrow +from pandas import DataFrame, to_datetime + +from freqtrade.configuration import Arguments, TimeRange +from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, + combine_tickers_with_mean, + create_cum_profit, + extract_trades_of_period, + load_backtest_data, load_trades, + load_trades_from_db) +from freqtrade.data.history import (load_data, load_pair_history, + make_testdata_path) +from freqtrade.tests.test_persistence import create_mock_trades def test_load_backtest_data(): @@ -19,3 +30,105 @@ def test_load_backtest_data(): with pytest.raises(ValueError, match=r"File .* does not exist\."): 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 + assert "profitperc" in trades.columns + + for col in BT_DATA_COLUMNS: + if col not in ['index', 'open_at_end']: + assert col in trades.columns + + +def test_extract_trades_of_period(): + 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 + + +def test_load_trades(default_conf, mocker): + db_mock = mocker.patch("freqtrade.data.btanalysis.load_trades_from_db", MagicMock()) + bt_mock = mocker.patch("freqtrade.data.btanalysis.load_backtest_data", MagicMock()) + + default_conf['trade_source'] = "DB" + load_trades(default_conf) + + assert db_mock.call_count == 1 + assert bt_mock.call_count == 0 + + db_mock.reset_mock() + bt_mock.reset_mock() + default_conf['trade_source'] = "file" + default_conf['exportfilename'] = "testfile.json" + load_trades(default_conf) + + assert db_mock.call_count == 0 + assert bt_mock.call_count == 1 + + +def test_combine_tickers_with_mean(): + pairs = ["ETH/BTC", "XLM/BTC"] + tickers = load_data(datadir=None, + pairs=pairs, + ticker_interval='5m' + ) + df = combine_tickers_with_mean(tickers) + assert isinstance(df, DataFrame) + assert "ETH/BTC" in df.columns + assert "XLM/BTC" in df.columns + assert "mean" in df.columns + + +def test_create_cum_profit(): + filename = make_testdata_path(None) / "backtest-result_test.json" + bt_data = load_backtest_data(filename) + timerange = Arguments.parse_timerange("20180110-20180112") + + df = load_pair_history(pair="POWR/BTC", ticker_interval='5m', + datadir=None, timerange=timerange) + + cum_profits = create_cum_profit(df.set_index('date'), + bt_data[bt_data["pair"] == 'POWR/BTC'], + "cum_profits") + assert "cum_profits" in cum_profits.columns + assert cum_profits.iloc[0]['cum_profits'] == 0 + assert cum_profits.iloc[-1]['cum_profits'] == 0.0798005 diff --git a/freqtrade/tests/data/test_converter.py b/freqtrade/tests/data/test_converter.py index 46d564003..f68224e0e 100644 --- a/freqtrade/tests/data/test_converter.py +++ b/freqtrade/tests/data/test_converter.py @@ -2,8 +2,7 @@ import logging from freqtrade.data.converter import parse_ticker_dataframe, ohlcv_fill_up_missing_data -from freqtrade.data.history import load_pair_history -from freqtrade.optimize import validate_backtest_data, get_timeframe +from freqtrade.data.history import load_pair_history, validate_backtest_data, get_timeframe 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) # 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 log_has('Parsing tickerlist to dataframe', caplog.record_tuples) @@ -28,18 +28,19 @@ def test_ohlcv_fill_up_missing_data(caplog): pair='UNITTEST/BTC', fill_up_missing=False) 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) # Column names should not change 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) # Test fillup actually fixes invalid backtest data min_date, max_date = get_timeframe({'UNITTEST/BTC': data}) - assert validate_backtest_data({'UNITTEST/BTC': data}, min_date, max_date, 1) - assert not validate_backtest_data({'UNITTEST/BTC': data2}, min_date, max_date, 1) + assert validate_backtest_data(data, 'UNITTEST/BTC', 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): @@ -79,10 +80,10 @@ def test_ohlcv_fill_up_missing_data2(caplog): ] # 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 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 # 3rd candle has been filled row = data2.loc[2, :] @@ -95,5 +96,55 @@ def test_ohlcv_fill_up_missing_data2(caplog): # Column names should not change 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) + + +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) diff --git a/freqtrade/tests/data/test_dataprovider.py b/freqtrade/tests/data/test_dataprovider.py index b17bba273..993f0b59b 100644 --- a/freqtrade/tests/data/test_dataprovider.py +++ b/freqtrade/tests/data/test_dataprovider.py @@ -9,31 +9,31 @@ from freqtrade.tests.conftest import get_patched_exchange def test_ohlcv(mocker, default_conf, ticker_history): default_conf["runmode"] = RunMode.DRY_RUN - tick_interval = default_conf["ticker_interval"] + ticker_interval = default_conf["ticker_interval"] exchange = get_patched_exchange(mocker, default_conf) - exchange._klines[("XRP/BTC", tick_interval)] = ticker_history - exchange._klines[("UNITTEST/BTC", tick_interval)] = ticker_history + exchange._klines[("XRP/BTC", ticker_interval)] = ticker_history + exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history dp = DataProvider(default_conf, exchange) assert dp.runmode == RunMode.DRY_RUN - assert ticker_history.equals(dp.ohlcv("UNITTEST/BTC", tick_interval)) - assert isinstance(dp.ohlcv("UNITTEST/BTC", tick_interval), DataFrame) - assert dp.ohlcv("UNITTEST/BTC", tick_interval) is not ticker_history - assert dp.ohlcv("UNITTEST/BTC", tick_interval, copy=False) is ticker_history - assert not dp.ohlcv("UNITTEST/BTC", tick_interval).empty - assert dp.ohlcv("NONESENSE/AAA", tick_interval).empty + assert ticker_history.equals(dp.ohlcv("UNITTEST/BTC", ticker_interval)) + assert isinstance(dp.ohlcv("UNITTEST/BTC", ticker_interval), DataFrame) + assert dp.ohlcv("UNITTEST/BTC", ticker_interval) is not ticker_history + assert dp.ohlcv("UNITTEST/BTC", ticker_interval, copy=False) is ticker_history + assert not dp.ohlcv("UNITTEST/BTC", ticker_interval).empty + assert dp.ohlcv("NONESENSE/AAA", ticker_interval).empty # Test with and without parameter - assert dp.ohlcv("UNITTEST/BTC", tick_interval).equals(dp.ohlcv("UNITTEST/BTC")) + assert dp.ohlcv("UNITTEST/BTC", ticker_interval).equals(dp.ohlcv("UNITTEST/BTC")) default_conf["runmode"] = RunMode.LIVE dp = DataProvider(default_conf, exchange) assert dp.runmode == RunMode.LIVE - assert isinstance(dp.ohlcv("UNITTEST/BTC", tick_interval), DataFrame) + assert isinstance(dp.ohlcv("UNITTEST/BTC", ticker_interval), DataFrame) default_conf["runmode"] = RunMode.BACKTEST dp = DataProvider(default_conf, exchange) assert dp.runmode == RunMode.BACKTEST - assert dp.ohlcv("UNITTEST/BTC", tick_interval).empty + assert dp.ohlcv("UNITTEST/BTC", ticker_interval).empty def test_historic_ohlcv(mocker, default_conf, ticker_history): @@ -54,15 +54,15 @@ def test_historic_ohlcv(mocker, default_conf, ticker_history): def test_available_pairs(mocker, default_conf, ticker_history): exchange = get_patched_exchange(mocker, default_conf) - tick_interval = default_conf["ticker_interval"] - exchange._klines[("XRP/BTC", tick_interval)] = ticker_history - exchange._klines[("UNITTEST/BTC", tick_interval)] = ticker_history + ticker_interval = default_conf["ticker_interval"] + exchange._klines[("XRP/BTC", ticker_interval)] = ticker_history + exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history dp = DataProvider(default_conf, exchange) assert len(dp.available_pairs) == 2 assert dp.available_pairs == [ - ("XRP/BTC", tick_interval), - ("UNITTEST/BTC", tick_interval), + ("XRP/BTC", ticker_interval), + ("UNITTEST/BTC", ticker_interval), ] @@ -71,10 +71,10 @@ def test_refresh(mocker, default_conf, ticker_history): mocker.patch("freqtrade.exchange.Exchange.refresh_latest_ohlcv", refresh_mock) exchange = get_patched_exchange(mocker, default_conf, id="binance") - tick_interval = default_conf["ticker_interval"] - pairs = [("XRP/BTC", tick_interval), ("UNITTEST/BTC", tick_interval)] + ticker_interval = default_conf["ticker_interval"] + pairs = [("XRP/BTC", ticker_interval), ("UNITTEST/BTC", ticker_interval)] - pairs_non_trad = [("ETH/USDT", tick_interval), ("BTC/TUSD", "1h")] + pairs_non_trad = [("ETH/USDT", ticker_interval), ("BTC/TUSD", "1h")] dp = DataProvider(default_conf, exchange) dp.refresh(pairs) diff --git a/freqtrade/tests/data/test_history.py b/freqtrade/tests/data/test_history.py index bc859b325..424333e99 100644 --- a/freqtrade/tests/data/test_history.py +++ b/freqtrade/tests/data/test_history.py @@ -2,24 +2,27 @@ import json import os -from pathlib import Path import uuid +from pathlib import Path from shutil import copyfile +from unittest.mock import MagicMock import arrow -from pandas import DataFrame import pytest +from pandas import DataFrame from freqtrade import OperationalException -from freqtrade.arguments import TimeRange +from freqtrade.configuration import TimeRange from freqtrade.data import history from freqtrade.data.history import (download_pair_history, load_cached_data_for_updating, - load_tickerdata_file, - make_testdata_path, + load_tickerdata_file, make_testdata_path, trim_tickerlist) +from freqtrade.exchange import timeframe_to_minutes 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 _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: ld = history.load_pair_history(pair='UNITTEST/BTC', ticker_interval='30m', datadir=None) 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: @@ -67,8 +74,11 @@ def test_load_data_7min_ticker(mocker, caplog, default_conf) -> None: assert not isinstance(ld, DataFrame) assert ld is None assert log_has( - 'No data for pair: "UNITTEST/BTC", Interval: 7m. ' - 'Use --refresh-pairs-cached to download the data', caplog.record_tuples) + 'No history data for pair: "UNITTEST/BTC", interval: 7m. ' + '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: @@ -77,7 +87,11 @@ def test_load_data_1min_ticker(ticker_history, mocker, caplog) -> None: _backup_file(file, copy_file=True) history.load_data(datadir=None, ticker_interval='1m', pairs=['UNITTEST/BTC']) 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) @@ -96,9 +110,12 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, defau refresh_pairs=False, pair='MEME/BTC') assert os.path.isfile(file) is False - assert log_has('No data for pair: "MEME/BTC", Interval: 1m. ' - 'Use --refresh-pairs-cached to download the data', - caplog.record_tuples) + assert log_has( + 'No history data for pair: "MEME/BTC", interval: 1m. ' + '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 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, pair='MEME/BTC') 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.*'): history.load_pair_history(datadir=None, 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) +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: assert str(Path('freqtrade') / 'tests' / 'testdata') in str(make_testdata_path(None)) @@ -242,10 +288,10 @@ def test_download_pair_history(ticker_history_list, mocker, default_conf) -> Non assert download_pair_history(datadir=None, exchange=exchange, pair='MEME/BTC', - tick_interval='1m') + ticker_interval='1m') assert download_pair_history(datadir=None, exchange=exchange, pair='CFI/BTC', - tick_interval='1m') + ticker_interval='1m') assert not exchange._pairs_last_refresh_time assert os.path.isfile(file1_1) is True assert os.path.isfile(file2_1) is True @@ -259,10 +305,10 @@ def test_download_pair_history(ticker_history_list, mocker, default_conf) -> Non assert download_pair_history(datadir=None, exchange=exchange, pair='MEME/BTC', - tick_interval='5m') + ticker_interval='5m') assert download_pair_history(datadir=None, exchange=exchange, pair='CFI/BTC', - tick_interval='5m') + ticker_interval='5m') assert not exchange._pairs_last_refresh_time assert os.path.isfile(file1_5) is True assert os.path.isfile(file2_5) is True @@ -280,14 +326,14 @@ def test_download_pair_history2(mocker, default_conf) -> None: json_dump_mock = mocker.patch('freqtrade.misc.file_dump_json', return_value=None) mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=tick) exchange = get_patched_exchange(mocker, default_conf) - download_pair_history(None, exchange, pair="UNITTEST/BTC", tick_interval='1m') - download_pair_history(None, exchange, pair="UNITTEST/BTC", tick_interval='3m') + download_pair_history(None, exchange, pair="UNITTEST/BTC", ticker_interval='1m') + download_pair_history(None, exchange, pair="UNITTEST/BTC", ticker_interval='3m') assert json_dump_mock.call_count == 2 def test_download_backtesting_data_exception(ticker_history, mocker, caplog, default_conf) -> None: mocker.patch('freqtrade.exchange.Exchange.get_history', - side_effect=BaseException('File Error')) + side_effect=Exception('File Error')) exchange = get_patched_exchange(mocker, default_conf) @@ -298,11 +344,15 @@ def test_download_backtesting_data_exception(ticker_history, mocker, caplog, def assert not download_pair_history(datadir=None, exchange=exchange, pair='MEME/BTC', - tick_interval='1m') + ticker_interval='1m') # clean files freshly downloaded _clean_test_file(file1_1) _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: @@ -473,3 +523,62 @@ def test_file_dump_json_tofile() -> None: # Remove the 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 diff --git a/freqtrade/tests/edge/test_edge.py b/freqtrade/tests/edge/test_edge.py index c1c1b49cd..45b8e609e 100644 --- a/freqtrade/tests/edge/test_edge.py +++ b/freqtrade/tests/edge/test_edge.py @@ -10,10 +10,11 @@ import numpy as np import pytest from pandas import DataFrame, to_datetime +from freqtrade import OperationalException from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.edge import Edge, PairInfo 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, _build_backtest_dataframe, _get_frame_time_from_offset) @@ -30,7 +31,50 @@ ticker_start_time = arrow.get(2018, 10, 3) ticker_interval_in_minute = 60 _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 tc0 = BTContainer(data=[ # D O H L C V B S @@ -122,8 +166,8 @@ def test_edge_results(edge_conf, mocker, caplog, data) -> None: for c, trade in enumerate(data.trades): res = results.iloc[c] assert res.exit_type == trade.sell_reason - assert res.open_time == _get_frame_time_from_offset(trade.open_tick) - assert res.close_time == _get_frame_time_from_offset(trade.close_tick) + assert arrow.get(res.open_time) == _get_frame_time_from_offset(trade.open_tick) + assert arrow.get(res.close_time) == _get_frame_time_from_offset(trade.close_tick) def test_adjust(mocker, edge_conf): @@ -203,46 +247,6 @@ def test_nonexisting_stake_amount(mocker, edge_conf): 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): freqtrade = get_patched_freqtradebot(mocker, edge_conf) 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 base = 0.001 - ETHBTC = [ + NEOBTC = [ [ ticker_start_time.shift(minutes=(x * ticker_interval_in_minute)).timestamp * 1000, math.sin(x * hz) / 1000 + base, @@ -281,8 +285,8 @@ def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=Fals 123.45 ] for x in range(0, 500)] - pairdata = {'NEO/BTC': parse_ticker_dataframe(ETHBTC, '1h', fill_missing=True), - 'LTC/BTC': parse_ticker_dataframe(LTCBTC, '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', pair="LTC/BTC", fill_missing=True)} return pairdata @@ -298,6 +302,40 @@ def test_edge_process_downloaded_data(mocker, edge_conf): 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): edge_conf['edge']['min_trade_number'] = 2 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'].required_risk_reward, 10) == 2.0 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) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 7c757df09..ebe5ad9df 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -2,7 +2,7 @@ # pragma pylint: disable=protected-access import copy import logging -from datetime import datetime +from datetime import datetime, timezone from random import randint from unittest.mock import MagicMock, Mock, PropertyMock @@ -11,7 +11,8 @@ import ccxt import pytest from pandas import DataFrame -from freqtrade import DependencyException, OperationalException, TemporaryError +from freqtrade import (DependencyException, InvalidOrderException, + OperationalException, TemporaryError) from freqtrade.exchange import Binance, Exchange, Kraken from freqtrade.exchange.exchange import API_RETRY_COUNT from freqtrade.resolvers.exchange_resolver import ExchangeResolver @@ -32,13 +33,13 @@ def get_mock_coro(return_value): def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, fun, mock_ccxt_fun, **kwargs): with pytest.raises(TemporaryError): - api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError) + api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeaDBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) getattr(exchange, fun)(**kwargs) assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1 with pytest.raises(OperationalException): - api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError) + api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) getattr(exchange, fun)(**kwargs) assert api_mock.__dict__[mock_ccxt_fun].call_count == 1 @@ -46,13 +47,13 @@ def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fun, **kwargs): with pytest.raises(TemporaryError): - api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError) + api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock) await getattr(exchange, fun)(**kwargs) assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1 with pytest.raises(OperationalException): - api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError) + api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock) await getattr(exchange, fun)(**kwargs) assert api_mock.__dict__[mock_ccxt_fun].call_count == 1 @@ -123,14 +124,14 @@ def test_exchange_resolver(default_conf, mocker, caplog): caplog.record_tuples) caplog.clear() - exchange = ExchangeResolver('Kraken', default_conf).exchange + exchange = ExchangeResolver('kraken', default_conf).exchange assert isinstance(exchange, Exchange) assert isinstance(exchange, Kraken) assert not isinstance(exchange, Binance) assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog.record_tuples) - exchange = ExchangeResolver('Binance', default_conf).exchange + exchange = ExchangeResolver('binance', default_conf).exchange assert isinstance(exchange, Exchange) assert isinstance(exchange, Binance) assert not isinstance(exchange, Kraken) @@ -139,6 +140,28 @@ def test_exchange_resolver(default_conf, mocker, caplog): caplog.record_tuples) +def test_validate_order_time_in_force(default_conf, mocker, caplog): + caplog.set_level(logging.INFO) + # explicitly test bittrex, exchanges implementing other policies need seperate tests + ex = get_patched_exchange(mocker, default_conf, id="bittrex") + tif = { + "buy": "gtc", + "sell": "gtc", + } + + ex.validate_order_time_in_force(tif) + tif2 = { + "buy": "fok", + "sell": "ioc", + } + with pytest.raises(OperationalException, match=r"Time in force.*not supported for .*"): + ex.validate_order_time_in_force(tif2) + + # Patch to see if this will pass if the values are in the ft dict + ex._ft_has.update({"order_time_in_force": ["gtc", "fok", "ioc"]}) + ex.validate_order_time_in_force(tif2) + + def test_symbol_amount_prec(default_conf, mocker): ''' Test rounds down to 4 Decimal places @@ -233,13 +256,13 @@ def test__load_async_markets(default_conf, mocker, caplog): def test__load_markets(default_conf, mocker, caplog): caplog.set_level(logging.INFO) api_mock = MagicMock() - api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError()) + api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError("SomeError")) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock()) Exchange(default_conf) - assert log_has('Unable to initialize markets. Reason: ', caplog.record_tuples) + assert log_has('Unable to initialize markets. Reason: SomeError', caplog.record_tuples) expected_return = {'ETH/BTC': 'available'} api_mock = MagicMock() @@ -278,10 +301,24 @@ def test__reload_markets(default_conf, mocker, caplog): 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("LoadError")) + 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 api_mock = MagicMock() type(api_mock).markets = PropertyMock(return_value={ - 'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': '' + 'ETH/BTC': {}, 'LTC/BTC': {}, 'XRP/BTC': {}, 'NEO/BTC': {} }) id_mock = PropertyMock(return_value='test_exchange') type(api_mock).id = id_mock @@ -295,7 +332,7 @@ def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs d def test_validate_pairs_not_available(default_conf, mocker): api_mock = MagicMock() type(api_mock).markets = PropertyMock(return_value={ - 'XRP/BTC': 'inactive' + 'XRP/BTC': {'inactive': True} }) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) @@ -305,19 +342,6 @@ def test_validate_pairs_not_available(default_conf, mocker): Exchange(default_conf) -def test_validate_pairs_not_compatible(default_conf, mocker): - api_mock = MagicMock() - type(api_mock).markets = PropertyMock(return_value={ - 'ETH/BTC': '', 'TKN/BTC': '', 'TRST/BTC': '', 'SWT/BTC': '', 'BCC/BTC': '' - }) - default_conf['stake_currency'] = 'ETH' - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock()) - with pytest.raises(OperationalException, match=r'not compatible'): - Exchange(default_conf) - - def test_validate_pairs_exception(default_conf, mocker, caplog): caplog.set_level(logging.INFO) api_mock = MagicMock() @@ -337,20 +361,21 @@ def test_validate_pairs_exception(default_conf, mocker, caplog): caplog.record_tuples) -def test_validate_pairs_stake_exception(default_conf, mocker, caplog): - caplog.set_level(logging.INFO) - default_conf['stake_currency'] = 'ETH' +def test_validate_pairs_restricted(default_conf, mocker, caplog): api_mock = MagicMock() - api_mock.name = MagicMock(return_value='binance') - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock) + type(api_mock).markets = PropertyMock(return_value={ + 'ETH/BTC': {}, 'LTC/BTC': {}, 'NEO/BTC': {}, + 'XRP/BTC': {'info': {'IsRestricted': True}} + }) + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock()) - with pytest.raises( - OperationalException, - match=r'Pair ETH/BTC not compatible with stake_currency: ETH' - ): - Exchange(default_conf) + Exchange(default_conf) + assert log_has(f"Pair XRP/BTC is restricted for some users on this exchange." + f"Please check if you are impacted by this restriction " + f"on the exchange and eventually remove XRP/BTC from your whitelist.", + caplog.record_tuples) def test_validate_timeframes(default_conf, mocker): @@ -388,6 +413,45 @@ def test_validate_timeframes_failed(default_conf, mocker): Exchange(default_conf) +def test_validate_timeframes_emulated_ohlcv_1(default_conf, mocker): + default_conf["ticker_interval"] = "3m" + api_mock = MagicMock() + id_mock = PropertyMock(return_value='test_exchange') + type(api_mock).id = id_mock + + # delete timeframes so magicmock does not autocreate it + del api_mock.timeframes + + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) + with pytest.raises(OperationalException, + match=r'The ccxt library does not provide the list of timeframes ' + r'for the exchange ".*" and this exchange ' + r'is therefore not supported. *'): + Exchange(default_conf) + + +def test_validate_timeframes_emulated_ohlcvi_2(default_conf, mocker): + default_conf["ticker_interval"] = "3m" + api_mock = MagicMock() + id_mock = PropertyMock(return_value='test_exchange') + type(api_mock).id = id_mock + + # delete timeframes so magicmock does not autocreate it + del api_mock.timeframes + + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch('freqtrade.exchange.Exchange._load_markets', + MagicMock(return_value={'timeframes': None})) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) + with pytest.raises(OperationalException, + match=r'The ccxt library does not provide the list of timeframes ' + r'for the exchange ".*" and this exchange ' + r'is therefore not supported. *'): + Exchange(default_conf) + + def test_validate_timeframes_not_in_config(default_conf, mocker): del default_conf["ticker_interval"] api_mock = MagicMock() @@ -496,15 +560,17 @@ def test_dry_run_order(default_conf, mocker, side, exchange_name): ("buy"), ("sell") ]) -@pytest.mark.parametrize("ordertype,rate", [ - ("market", None), - ("limit", 200), - ("stop_loss_limit", 200) +@pytest.mark.parametrize("ordertype,rate,marketprice", [ + ("market", None, None), + ("market", 200, True), + ("limit", 200, None), + ("stop_loss_limit", 200, None) ]) @pytest.mark.parametrize("exchange_name", EXCHANGES) -def test_create_order(default_conf, mocker, side, ordertype, rate, exchange_name): +def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, exchange_name): api_mock = MagicMock() order_id = 'test_prod_{}_{}'.format(side, randint(0, 10 ** 6)) + api_mock.options = {} if not marketprice else {"createMarketBuyOrderRequiresPrice": True} api_mock.create_order = MagicMock(return_value={ 'id': order_id, 'info': { @@ -545,6 +611,7 @@ def test_buy_prod(default_conf, mocker, exchange_name): order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) order_type = 'market' time_in_force = 'gtc' + api_mock.options = {} api_mock.create_order = MagicMock(return_value={ 'id': order_id, 'info': { @@ -584,25 +651,25 @@ def test_buy_prod(default_conf, mocker, exchange_name): # test exception handling with pytest.raises(DependencyException): - api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds) + api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("Not enough funds")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.buy(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200, time_in_force=time_in_force) with pytest.raises(DependencyException): - api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder) + api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.buy(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200, time_in_force=time_in_force) with pytest.raises(TemporaryError): - api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError) + api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("Network disconnect")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.buy(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200, time_in_force=time_in_force) with pytest.raises(OperationalException): - api_mock.create_order = MagicMock(side_effect=ccxt.BaseError) + api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("Unknown error")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.buy(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200, time_in_force=time_in_force) @@ -612,6 +679,7 @@ def test_buy_prod(default_conf, mocker, exchange_name): def test_buy_considers_time_in_force(default_conf, mocker, exchange_name): api_mock = MagicMock() order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) + api_mock.options = {} api_mock.create_order = MagicMock(return_value={ 'id': order_id, 'info': { @@ -672,6 +740,7 @@ def test_sell_prod(default_conf, mocker, exchange_name): api_mock = MagicMock() order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6)) order_type = 'market' + api_mock.options = {} api_mock.create_order = MagicMock(return_value={ 'id': order_id, 'info': { @@ -706,22 +775,22 @@ def test_sell_prod(default_conf, mocker, exchange_name): # test exception handling with pytest.raises(DependencyException): - api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds) + api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) with pytest.raises(DependencyException): - api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder) + api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) with pytest.raises(TemporaryError): - api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError) + api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No Connection")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) with pytest.raises(OperationalException): - api_mock.create_order = MagicMock(side_effect=ccxt.BaseError) + api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) @@ -736,6 +805,7 @@ def test_sell_considers_time_in_force(default_conf, mocker, exchange_name): 'foo': 'bar' } }) + api_mock.options = {} default_conf['dry_run'] = False mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) @@ -793,7 +863,7 @@ def test_get_balance_prod(default_conf, mocker, exchange_name): assert exchange.get_balance(currency='BTC') == 123.4 with pytest.raises(OperationalException): - api_mock.fetch_balance = MagicMock(side_effect=ccxt.BaseError) + api_mock.fetch_balance = MagicMock(side_effect=ccxt.BaseError("Unknown error")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.get_balance(currency='BTC') @@ -866,7 +936,7 @@ def test_get_tickers(default_conf, mocker, exchange_name): "get_tickers", "fetch_tickers") with pytest.raises(OperationalException): - api_mock.fetch_tickers = MagicMock(side_effect=ccxt.NotSupported) + api_mock.fetch_tickers = MagicMock(side_effect=ccxt.NotSupported("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.get_tickers() @@ -885,7 +955,7 @@ def test_get_ticker(default_conf, mocker, exchange_name): 'last': 0.0001, } api_mock.fetch_ticker = MagicMock(return_value=tick) - api_mock.markets = {'ETH/BTC': {}} + api_mock.markets = {'ETH/BTC': {'active': True}} exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) # retrieve original ticker ticker = exchange.get_ticker(pair='ETH/BTC') @@ -947,8 +1017,8 @@ def test_get_history(default_conf, mocker, caplog, exchange_name): ] pair = 'ETH/BTC' - async def mock_candle_hist(pair, tick_interval, since_ms): - return pair, tick_interval, tick + async def mock_candle_hist(pair, ticker_interval, since_ms): + return pair, ticker_interval, tick exchange._async_get_candle_history = Mock(wraps=mock_candle_hist) # one_call calculation * 1.8 should do 2 calls @@ -1008,7 +1078,7 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m')]) 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) @@ -1044,11 +1114,11 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_ # exchange = Exchange(default_conf) await async_ccxt_exception(mocker, default_conf, MagicMock(), "_async_get_candle_history", "fetch_ohlcv", - pair='ABCD/BTC', tick_interval=default_conf['ticker_interval']) + pair='ABCD/BTC', ticker_interval=default_conf['ticker_interval']) api_mock = MagicMock() with pytest.raises(OperationalException, match=r'Could not fetch ticker data*'): - api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError) + api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError("Unknown error")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) await exchange._async_get_candle_history(pair, "5m", (arrow.utcnow().timestamp - 2000) * 1000) @@ -1120,15 +1190,15 @@ def test_get_order_book(default_conf, mocker, order_book_l2, exchange_name): def test_get_order_book_exception(default_conf, mocker, exchange_name): api_mock = MagicMock() with pytest.raises(OperationalException): - api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NotSupported) + api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NotSupported("Not supported")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.get_order_book(pair='ETH/BTC', limit=50) with pytest.raises(TemporaryError): - api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NetworkError) + api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NetworkError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.get_order_book(pair='ETH/BTC', limit=50) with pytest.raises(OperationalException): - api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.BaseError) + api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.get_order_book(pair='ETH/BTC', limit=50) @@ -1240,11 +1310,11 @@ def test_cancel_order(default_conf, mocker, exchange_name): exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) assert exchange.cancel_order(order_id='_', pair='TKN/BTC') == 123 - with pytest.raises(DependencyException): - api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder) + with pytest.raises(InvalidOrderException): + api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.cancel_order(order_id='_', pair='TKN/BTC') - assert api_mock.cancel_order.call_count == API_RETRY_COUNT + 1 + assert api_mock.cancel_order.call_count == 1 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, "cancel_order", "cancel_order", @@ -1267,11 +1337,11 @@ def test_get_order(default_conf, mocker, exchange_name): exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) assert exchange.get_order('X', 'TKN/BTC') == 456 - with pytest.raises(DependencyException): - api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder) + with pytest.raises(InvalidOrderException): + api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.get_order(order_id='_', pair='TKN/BTC') - assert api_mock.fetch_order.call_count == API_RETRY_COUNT + 1 + assert api_mock.fetch_order.call_count == 1 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, 'get_order', 'fetch_order', @@ -1291,7 +1361,7 @@ def test_name(default_conf, mocker, exchange_name): @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_get_trades_for_order(default_conf, mocker, exchange_name): order_id = 'ABCD-ABCD' - since = datetime(2018, 5, 5) + since = datetime(2018, 5, 5, tzinfo=timezone.utc) default_conf["dry_run"] = False mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) api_mock = MagicMock() @@ -1321,6 +1391,13 @@ def test_get_trades_for_order(default_conf, mocker, exchange_name): orders = exchange.get_trades_for_order(order_id, 'LTC/BTC', since) assert len(orders) == 1 assert orders[0]['price'] == 165 + assert api_mock.fetch_my_trades.call_count == 1 + # since argument should be + assert isinstance(api_mock.fetch_my_trades.call_args[0][1], int) + assert api_mock.fetch_my_trades.call_args[0][0] == 'LTC/BTC' + # Same test twice, hardcoded number and doing the same calculation + assert api_mock.fetch_my_trades.call_args[0][1] == 1525478395000 + assert api_mock.fetch_my_trades.call_args[0][1] == int(since.timestamp() - 5) * 1000 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, 'get_trades_for_order', 'fetch_my_trades', @@ -1384,22 +1461,22 @@ def test_stoploss_limit_order(default_conf, mocker): # test exception handling with pytest.raises(DependencyException): - api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds) + api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) with pytest.raises(DependencyException): - api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder) + api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) with pytest.raises(TemporaryError): - api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError) + api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection")) exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) with pytest.raises(OperationalException): - api_mock.create_order = MagicMock(side_effect=ccxt.BaseError) + api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) @@ -1427,3 +1504,46 @@ def test_stoploss_limit_order_dry_run(default_conf, mocker): assert order['type'] == order_type assert order['price'] == 220 assert order['amount'] == 1 + + +def test_merge_ft_has_dict(default_conf, mocker): + mocker.patch.multiple('freqtrade.exchange.Exchange', + _init_ccxt=MagicMock(return_value=MagicMock()), + _load_async_markets=MagicMock(), + validate_pairs=MagicMock(), + 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 + + +def test_get_valid_pair_combination(default_conf, mocker, markets): + mocker.patch.multiple('freqtrade.exchange.Exchange', + _init_ccxt=MagicMock(return_value=MagicMock()), + _load_async_markets=MagicMock(), + validate_pairs=MagicMock(), + validate_timeframes=MagicMock(), + markets=PropertyMock(return_value=markets)) + ex = Exchange(default_conf) + + assert ex.get_valid_pair_combination("ETH", "BTC") == "ETH/BTC" + assert ex.get_valid_pair_combination("BTC", "ETH") == "ETH/BTC" + with pytest.raises(DependencyException, match=r"Could not combine.* to get a valid pair."): + ex.get_valid_pair_combination("NOPAIR", "ETH") diff --git a/freqtrade/tests/exchange/test_kraken.py b/freqtrade/tests/exchange/test_kraken.py index 8b81a08a9..8f476affb 100644 --- a/freqtrade/tests/exchange/test_kraken.py +++ b/freqtrade/tests/exchange/test_kraken.py @@ -11,6 +11,7 @@ def test_buy_kraken_trading_agreement(default_conf, mocker): order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) order_type = 'limit' time_in_force = 'ioc' + api_mock.options = {} api_mock.create_order = MagicMock(return_value={ 'id': order_id, 'info': { @@ -42,6 +43,7 @@ def test_sell_kraken_trading_agreement(default_conf, mocker): api_mock = MagicMock() order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6)) order_type = 'market' + api_mock.options = {} api_mock.create_order = MagicMock(return_value={ 'id': order_id, 'info': { diff --git a/freqtrade/tests/optimize/__init__.py b/freqtrade/tests/optimize/__init__.py index 129a09f40..41500051f 100644 --- a/freqtrade/tests/optimize/__init__.py +++ b/freqtrade/tests/optimize/__init__.py @@ -3,11 +3,11 @@ from typing import NamedTuple, List import arrow from pandas import DataFrame +from freqtrade.exchange import timeframe_to_minutes from freqtrade.strategy.interface import SellType -from freqtrade.constants import TICKER_INTERVAL_MINUTES ticker_start_time = arrow.get(2018, 10, 3) -tests_ticker_interval = "1h" +tests_ticker_interval = '1h' class BTrade(NamedTuple): @@ -28,11 +28,16 @@ class BTContainer(NamedTuple): roi: float trades: List[BTrade] profit_perc: float + 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): - return ticker_start_time.shift(minutes=(offset * TICKER_INTERVAL_MINUTES[tests_ticker_interval]) - ).datetime.replace(tzinfo=None) + return ticker_start_time.shift(minutes=(offset * timeframe_to_minutes(tests_ticker_interval)) + ).datetime def _build_backtest_dataframe(ticker_with_signals): diff --git a/freqtrade/tests/optimize/test_backtest_detail.py b/freqtrade/tests/optimize/test_backtest_detail.py index e8514e76f..87f567b4f 100644 --- a/freqtrade/tests/optimize/test_backtest_detail.py +++ b/freqtrade/tests/optimize/test_backtest_detail.py @@ -2,22 +2,35 @@ import logging from unittest.mock import MagicMock -from pandas import DataFrame import pytest +from pandas import DataFrame - -from freqtrade.optimize import get_timeframe +from freqtrade.data.history import get_timeframe from freqtrade.optimize.backtesting import Backtesting 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.optimize import (BTContainer, BTrade, + _build_backtest_dataframe, + _get_frame_time_from_offset, + tests_ticker_interval) - -# Test 0 Minus 8% Close +# Test 0: Sell with signal sell in candle 3 # Test with Stop-loss at 1% -# TC1: Stop-Loss Triggered 1% loss 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: Stop-Loss Triggered 1% loss +# Test with Stop-loss at 1% +tc1 = 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) @@ -30,10 +43,9 @@ tc0 = BTContainer(data=[ ) -# Test 1 Minus 4% Low, minus 1% close +# Test 2: Minus 4% Low, minus 1% close # Test with Stop-Loss at 3% -# TC2: Stop-Loss Triggered 3% Loss -tc1 = BTContainer(data=[ +tc2 = 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) @@ -46,14 +58,13 @@ tc1 = BTContainer(data=[ ) -# Test 3 Candle drops 4%, Recovers 1%. -# Entry Criteria Met -# Candle drops 20% -# Candle Data for test 3 -# Test with Stop-Loss at 2% -# TC3: Trade-A: Stop-Loss Triggered 2% Loss -# Trade-B: Stop-Loss Triggered 2% Loss -tc2 = BTContainer(data=[ +# Test 3: Multiple trades. +# Candle drops 4%, Recovers 1%. +# Entry Criteria Met +# Candle drops 20% +# Trade-A: Stop-Loss Triggered 2% Loss +# Trade-B: Stop-Loss Triggered 2% Loss +tc3 = 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) @@ -67,11 +78,11 @@ tc2 = BTContainer(data=[ BTrade(sell_reason=SellType.STOP_LOSS, open_tick=4, close_tick=5)] ) -# Test 4 Minus 3% / recovery +15% +# Test 4: Minus 3% / recovery +15% # Candle Data for test 3 – Candle drops 3% Closed 15% up # Test with Stop-loss at 2% ROI 6% -# TC4: Stop-Loss Triggered 2% Loss -tc3 = BTContainer(data=[ +# Stop-Loss Triggered 2% Loss +tc4 = 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) @@ -83,10 +94,9 @@ tc3 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2)] ) -# Test 4 / Drops 0.5% Closes +20% -# Set stop-loss at 1% ROI 3% -# TC5: ROI triggers 3% Gain -tc4 = BTContainer(data=[ +# Test 5: Drops 0.5% Closes +20%, ROI triggers 3% Gain +# stop-loss: 1%, ROI: 3% +tc5 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4980, 4987, 6172, 1, 0], [1, 5000, 5025, 4980, 4987, 6172, 0, 0], # enter trade (signal on last candle) @@ -98,11 +108,9 @@ tc4 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] ) -# Test 6 / Drops 3% / Recovers 6% Positive / Closes 1% positve -# Candle Data for test 6 -# Set stop-loss at 2% ROI at 5% -# TC6: Stop-Loss triggers 2% Loss -tc5 = BTContainer(data=[ +# Test 6: Drops 3% / Recovers 6% Positive / Closes 1% positve, Stop-Loss triggers 2% Loss +# stop-loss: 2% ROI: 5% +tc6 = 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) @@ -114,11 +122,9 @@ tc5 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2)] ) -# Test 7 - 6% Positive / 1% Negative / Close 1% Positve -# Candle Data for test 7 -# Set stop-loss at 2% ROI at 3% -# TC7: ROI Triggers 3% Gain -tc6 = BTContainer(data=[ +# Test 7: 6% Positive / 1% Negative / Close 1% Positve, ROI Triggers 3% Gain +# stop-loss: 2% ROI: 3% +tc7 = 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], @@ -130,6 +136,123 @@ tc6 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=2)] ) + +# Test 8: trailing_stop should raise so candle 3 causes a stoploss. +# stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted in candle 2 +tc8 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5050, 4950, 5000, 6172, 0, 0], + [2, 5000, 5250, 4750, 4850, 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.055, trailing_stop=True, + trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] +) + + +# Test 9: trailing_stop should raise - high and low in same candle. +# stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted in candle 3 +tc9 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5050, 4950, 5000, 6172, 0, 0], + [2, 5000, 5050, 4950, 5000, 6172, 0, 0], + [3, 5000, 5200, 4550, 4850, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.10, roi=0.10, profit_perc=-0.064, trailing_stop=True, + 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%. +# stop-loss: 10%, ROI: 10% (should not apply), stoploss 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. +# stop-loss: 10%, ROI: 10% (should not apply), stoploss 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. +# stop-loss: 10%, ROI: 10% (should not apply), stoploss 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)] +) + +# Test 13: Buy and sell ROI on same candle +# stop-loss: 10% (should not apply), ROI: 1% +tc13 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5100, 4950, 5100, 6172, 0, 0], + [2, 5100, 5251, 4850, 5100, 6172, 0, 0], + [3, 4850, 5050, 4850, 4750, 6172, 0, 0], + [4, 4750, 4950, 4850, 4750, 6172, 0, 0]], + stop_loss=-0.10, roi=0.01, profit_perc=0.01, + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=1)] +) + +# Test 14 - Buy and Stoploss on same candle +# stop-loss: 5%, ROI: 10% (should not apply) +tc14 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5100, 4600, 5100, 6172, 0, 0], + [2, 5100, 5251, 4850, 5100, 6172, 0, 0], + [3, 4850, 5050, 4850, 4750, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.05, roi=0.10, profit_perc=-0.05, + trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=1)] +) + + +# Test 15 - Buy and ROI on same candle, followed by buy and Stoploss on next candle +# stop-loss: 5%, ROI: 10% (should not apply) +tc15 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5100, 4900, 5100, 6172, 1, 0], + [2, 5100, 5251, 4650, 5100, 6172, 0, 0], + [3, 4850, 5050, 4850, 4750, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.05, roi=0.01, profit_perc=-0.04, + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=1), + BTrade(sell_reason=SellType.STOP_LOSS, open_tick=2, close_tick=2)] +) + TESTS = [ tc0, tc1, @@ -138,6 +261,15 @@ TESTS = [ tc4, tc5, tc6, + tc7, + tc8, + tc9, + tc10, + tc11, + tc12, + tc13, + tc14, + tc15, ] @@ -148,8 +280,16 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: """ default_conf["stoploss"] = data.stop_loss default_conf["minimal_roi"] = {"0": data.roi} - default_conf['ticker_interval'] = tests_ticker_interval - mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.0)) + default_conf["ticker_interval"] = tests_ticker_interval + 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)) patch_exchange(mocker) frame = _build_backtest_dataframe(data.data) backtesting = Backtesting(default_conf) @@ -157,7 +297,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: backtesting.advise_sell = lambda a, m: frame caplog.set_level(logging.DEBUG) - pair = 'UNITTEST/BTC' + pair = "UNITTEST/BTC" # Dummy data as we mock the analyze functions data_processed = {pair: DataFrame()} min_date, max_date = get_timeframe({pair: frame}) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 40754cfbc..71d460621 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -1,9 +1,7 @@ # pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, unused-argument -import json import math import random -from typing import List from unittest.mock import MagicMock import numpy as np @@ -11,28 +9,27 @@ import pandas as pd import pytest from arrow import Arrow -from freqtrade import DependencyException, constants -from freqtrade.arguments import Arguments, TimeRange +from freqtrade import DependencyException, OperationalException, constants +from freqtrade.configuration import TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import evaluate_result_multi from freqtrade.data.converter import parse_ticker_dataframe -from freqtrade.optimize import get_timeframe -from freqtrade.optimize.backtesting import (Backtesting, setup_configuration, - start) +from freqtrade.data.dataprovider import DataProvider +from freqtrade.data.history import get_timeframe +from freqtrade.optimize import setup_configuration, start_backtesting +from freqtrade.optimize.backtesting import Backtesting from freqtrade.state import RunMode from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.strategy.interface import SellType -from freqtrade.tests.conftest import log_has, patch_exchange - - -def get_args(args) -> List[str]: - return Arguments(args, '').get_parsed_arg() +from freqtrade.tests.conftest import (get_args, log_has, log_has_re, + patch_exchange, + patched_configuration_load_config_file) def trim_dictlist(dict_list, num): new = {} for pair, pair_data in dict_list.items(): - new[pair] = pair_data[num:] + new[pair] = pair_data[num:].reset_index() return new @@ -77,7 +74,8 @@ def load_data_test(what): pair[x][5] # Keep old volume ] 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: @@ -104,9 +102,10 @@ def simple_backtest(config, contour, num_results, mocker) -> None: 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) - 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 @@ -167,9 +166,7 @@ def _trend_alternate(dataframe=None, metadata=None): # Unit tests def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None: - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_load_config_file(mocker, default_conf) args = [ '--config', 'config.json', @@ -177,7 +174,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> 'backtesting' ] - config = setup_configuration(get_args(args)) + config = setup_configuration(get_args(args), RunMode.BACKTEST) assert 'max_open_trades' in config assert 'stake_currency' in config assert 'stake_amount' in config @@ -185,11 +182,11 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> assert 'pair_whitelist' in config['exchange'] assert 'datadir' in config assert log_has( - 'Using data folder: {} ...'.format(config['datadir']), + 'Using data directory: {} ...'.format(config['datadir']), caplog.record_tuples ) 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 not log_has('Parameter -l/--live detected ...', caplog.record_tuples) @@ -206,11 +203,13 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> assert config['runmode'] == RunMode.BACKTEST +@pytest.mark.filterwarnings("ignore:DEPRECATED") def test_setup_bt_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) + patched_configuration_load_config_file(mocker, default_conf) + mocker.patch( + 'freqtrade.configuration.configuration.create_datadir', + lambda c, x: x + ) args = [ '--config', 'config.json', @@ -227,7 +226,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> '--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 'stake_currency' in config assert 'stake_amount' in config @@ -237,15 +236,12 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> assert config['runmode'] == RunMode.BACKTEST assert log_has( - 'Using data folder: {} ...'.format(config['datadir']), + 'Using data directory: {} ...'.format(config['datadir']), caplog.record_tuples ) assert 'ticker_interval' in config - assert log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples) - assert log_has( - 'Using ticker_interval: 1m ...', - caplog.record_tuples - ) + assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', + caplog.record_tuples) assert 'live' in config assert log_has('Parameter -l/--live detected ...', caplog.record_tuples) @@ -259,6 +255,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> 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']), @@ -280,9 +277,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog) -> None: default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_load_config_file(mocker, default_conf) args = [ '--config', 'config.json', @@ -291,7 +286,7 @@ def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog ] 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: @@ -299,16 +294,15 @@ def test_start(mocker, fee, default_conf, caplog) -> None: mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) patch_exchange(mocker) mocker.patch('freqtrade.optimize.backtesting.Backtesting.start', start_mock) - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_load_config_file(mocker, default_conf) + args = [ '--config', 'config.json', '--strategy', 'DefaultStrategy', 'backtesting' ] args = get_args(args) - start(args) + start_backtesting(args) assert log_has( 'Starting freqtrade in Backtesting mode', caplog.record_tuples @@ -346,16 +340,35 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None: assert callable(backtesting.strategy.tickerdata_to_dataframe) assert callable(backtesting.advise_buy) assert callable(backtesting.advise_sell) + assert isinstance(backtesting.strategy.dp, DataProvider) get_fee.assert_called() assert backtesting.fee == 0.5 assert not backtesting.strategy.order_types["stoploss_on_exchange"] +def test_backtesting_init_no_ticker_interval(mocker, default_conf, caplog) -> None: + """ + Check that stoploss_on_exchange is set to False while backtesting + since backtesting assumes a perfect stoploss anyway. + """ + patch_exchange(mocker) + del default_conf['ticker_interval'] + default_conf['strategy_list'] = ['DefaultStrategy', + 'TestStrategy'] + + mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5)) + with pytest.raises(OperationalException): + Backtesting(default_conf) + log_has("Ticker-interval needs to be set in either configuration " + "or as cli argument `--ticker-interval 5m`", caplog.record_tuples) + + def test_tickerdata_to_dataframe_bt(default_conf, mocker) -> None: patch_exchange(mocker) timerange = TimeRange(None, 'line', 0, -100) 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) data = backtesting.strategy.tickerdata_to_dataframe(tickerlist) @@ -472,7 +485,7 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None: 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.optimize.get_timeframe', get_timeframe) + mocker.patch('freqtrade.data.history.get_timeframe', get_timeframe) mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock()) patch_exchange(mocker) mocker.patch.multiple( @@ -492,10 +505,9 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None: backtesting.start() # check the logs, that will contain the backtest result exists = [ - 'Using local backtesting data (using whitelist in given config) ...', 'Using stake_currency: BTC ...', '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)..' ] for line in exists: @@ -507,7 +519,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) 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()) patch_exchange(mocker) mocker.patch.multiple( @@ -624,8 +636,9 @@ def test_processed(default_conf, mocker) -> None: def test_backtest_pricecontours(default_conf, fee, mocker) -> None: + # TODO: Evaluate usefullness of this, the patterns and buy-signls are unrealistic mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - tests = [['raise', 19], ['lower', 0], ['sine', 18]] + tests = [['raise', 19], ['lower', 0], ['sine', 35]] # We need to enable sell-signal - otherwise it sells on ROI!! default_conf['experimental'] = {"use_sell_signal": True} @@ -683,25 +696,32 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker): assert len(results.loc[results.open_at_end]) == 0 -def test_backtest_multi_pair(default_conf, fee, mocker): +@pytest.mark.parametrize("pair", ['ADA/BTC', 'LTC/BTC']) +@pytest.mark.parametrize("tres", [0, 20, 30]) +def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair): def _trend_alternate_hold(dataframe=None, metadata=None): """ - Buy every 8th candle - sell every other 8th -2 (hold on to pairs a bit) + Buy every xth candle - sell every other xth -2 (hold on to pairs a bit) """ - multi = 8 + if metadata['pair'] in('ETH/BTC', 'LTC/BTC'): + multi = 20 + else: + multi = 18 dataframe['buy'] = np.where(dataframe.index % multi == 0, 1, 0) dataframe['sell'] = np.where((dataframe.index + multi - 2) % multi == 0, 1, 0) - if metadata['pair'] in('ETH/BTC', 'LTC/BTC'): - dataframe['buy'] = dataframe['buy'].shift(-4) - dataframe['sell'] = dataframe['sell'].shift(-4) return dataframe mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) patch_exchange(mocker) + pairs = ['ADA/BTC', 'DASH/BTC', 'ETH/BTC', 'LTC/BTC', 'NXT/BTC'] data = history.load_data(datadir=None, ticker_interval='5m', pairs=pairs) + # Only use 500 lines to increase performance data = trim_dictlist(data, -500) + + # Remove data for one pair from the beginning of the data + data[pair] = data[pair][tres:].reset_index() # We need to enable sell-signal - otherwise it sells on ROI!! default_conf['experimental'] = {"use_sell_signal": True} default_conf['ticker_interval'] = '5m' @@ -812,6 +832,7 @@ def test_backtest_record(default_conf, fee, mocker): assert dur > 0 +@pytest.mark.filterwarnings("ignore:DEPRECATED") def test_backtest_start_live(default_conf, mocker, caplog): default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] @@ -824,9 +845,7 @@ def test_backtest_start_live(default_conf, mocker, caplog): patch_exchange(mocker, api_mock) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock()) mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', MagicMock()) - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_load_config_file(mocker, default_conf) args = [ '--config', 'config.json', @@ -840,19 +859,19 @@ def test_backtest_start_live(default_conf, mocker, caplog): '--disable-max-market-positions' ] args = get_args(args) - start(args) + start_backtesting(args) # check the logs, that will contain the backtest result exists = [ - 'Parameter -i/--ticker-interval detected ...', - 'Using ticker_interval: 1m ...', + 'Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', 'Parameter -l/--live detected ...', 'Ignoring max_open_trades (--disable-max-market-positions was used) ...', 'Parameter --timerange detected: -100 ...', - 'Using data folder: freqtrade/tests/testdata ...', + 'Using data directory: freqtrade/tests/testdata ...', 'Using stake_currency: BTC ...', 'Using stake_amount: 0.001 ...', - 'Downloading data for all pairs in whitelist ...', - 'Measuring data from 2017-11-14T19:31:00+00:00 up to 2017-11-14T22:58:00+00:00 (0 days)..', + 'Live: Downloading data for all defined pairs ...', + '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 ...' ] @@ -860,6 +879,7 @@ def test_backtest_start_live(default_conf, mocker, caplog): assert log_has(line, caplog.record_tuples) +@pytest.mark.filterwarnings("ignore:DEPRECATED") def test_backtest_start_multi_strat(default_conf, mocker, caplog): default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] @@ -876,9 +896,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog): gen_strattable_mock = MagicMock() mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table_strategy', gen_strattable_mock) - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_load_config_file(mocker, default_conf) args = [ '--config', 'config.json', @@ -894,7 +912,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog): 'TestStrategy', ] args = get_args(args) - start(args) + start_backtesting(args) # 2 backtests, 4 tables assert backtestmock.call_count == 2 assert gen_table_mock.call_count == 4 @@ -902,16 +920,16 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog): # check the logs, that will contain the backtest result exists = [ - 'Parameter -i/--ticker-interval detected ...', - 'Using ticker_interval: 1m ...', + 'Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', 'Parameter -l/--live detected ...', 'Ignoring max_open_trades (--disable-max-market-positions was used) ...', 'Parameter --timerange detected: -100 ...', - 'Using data folder: freqtrade/tests/testdata ...', + 'Using data directory: freqtrade/tests/testdata ...', 'Using stake_currency: BTC ...', 'Using stake_amount: 0.001 ...', - 'Downloading data for all pairs in whitelist ...', - 'Measuring data from 2017-11-14T19:31:00+00:00 up to 2017-11-14T22:58:00+00:00 (0 days)..', + 'Live: Downloading data for all defined pairs ...', + '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 ...', 'Running backtesting for Strategy DefaultStrategy', 'Running backtesting for Strategy TestStrategy', diff --git a/freqtrade/tests/optimize/test_edge_cli.py b/freqtrade/tests/optimize/test_edge_cli.py index a58620139..badaa5c45 100644 --- a/freqtrade/tests/optimize/test_edge_cli.py +++ b/freqtrade/tests/optimize/test_edge_cli.py @@ -2,23 +2,17 @@ # pragma pylint: disable=protected-access, too-many-lines, invalid-name, too-many-arguments from unittest.mock import MagicMock -import json -from typing import List + from freqtrade.edge import PairInfo -from freqtrade.arguments import Arguments -from freqtrade.optimize.edge_cli import (EdgeCli, setup_configuration, start) +from freqtrade.optimize import setup_configuration, start_edge +from freqtrade.optimize.edge_cli import EdgeCli from freqtrade.state import RunMode -from freqtrade.tests.conftest import log_has, patch_exchange - - -def get_args(args) -> List[str]: - return Arguments(args, '').get_parsed_arg() +from freqtrade.tests.conftest import (get_args, log_has, log_has_re, patch_exchange, + patched_configuration_load_config_file) def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None: - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(default_conf) - )) + patched_configuration_load_config_file(mocker, default_conf) args = [ '--config', 'config.json', @@ -26,8 +20,8 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> 'edge' ] - config = setup_configuration(get_args(args)) - assert config['runmode'] == RunMode.EDGECLI + config = setup_configuration(get_args(args), RunMode.EDGE) + assert config['runmode'] == RunMode.EDGE assert 'max_open_trades' in config assert 'stake_currency' in config @@ -36,11 +30,11 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> assert 'pair_whitelist' in config['exchange'] assert 'datadir' in config assert log_has( - 'Using data folder: {} ...'.format(config['datadir']), + 'Using data directory: {} ...'.format(config['datadir']), caplog.record_tuples ) 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 not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples) @@ -50,10 +44,11 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> None: - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(edge_conf) - )) - mocker.patch('freqtrade.configuration.Configuration._create_datadir', lambda s, c, x: x) + patched_configuration_load_config_file(mocker, edge_conf) + mocker.patch( + 'freqtrade.configuration.configuration.create_datadir', + lambda c, x: x + ) args = [ '--config', 'config.json', @@ -66,24 +61,21 @@ def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> N '--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 '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.EDGECLI + assert config['runmode'] == RunMode.EDGE assert log_has( - 'Using data folder: {} ...'.format(config['datadir']), + 'Using data directory: {} ...'.format(config['datadir']), caplog.record_tuples ) assert 'ticker_interval' in config - assert log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples) - assert log_has( - 'Using ticker_interval: 1m ...', - caplog.record_tuples - ) + assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', + caplog.record_tuples) assert 'refresh_pairs' in config assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples) @@ -99,16 +91,15 @@ def test_start(mocker, fee, edge_conf, caplog) -> None: mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) patch_exchange(mocker) mocker.patch('freqtrade.optimize.edge_cli.EdgeCli.start', start_mock) - mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(edge_conf) - )) + patched_configuration_load_config_file(mocker, edge_conf) + args = [ '--config', 'config.json', '--strategy', 'DefaultStrategy', 'edge' ] args = get_args(args) - start(args) + start_edge(args) assert log_has( 'Starting freqtrade in Edge mode', caplog.record_tuples @@ -118,8 +109,10 @@ def test_start(mocker, fee, edge_conf, caplog) -> None: def test_edge_init(mocker, edge_conf) -> None: patch_exchange(mocker) + edge_conf['stake_amount'] = 20 edge_cli = EdgeCli(edge_conf) assert edge_cli.config == edge_conf + assert edge_cli.config['stake_amount'] == 'unlimited' assert callable(edge_cli.edge.calculate) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 20baee99e..e3b049c06 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -1,26 +1,51 @@ # pragma pylint: disable=missing-docstring,W0212,C0103 -from datetime import datetime import os +from datetime import datetime from unittest.mock import MagicMock import pandas as pd import pytest +from arrow import Arrow +from filelock import Timeout +from freqtrade import DependencyException, OperationalException from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.history import load_tickerdata_file -from freqtrade.optimize.hyperopt import Hyperopt, start +from freqtrade.optimize import setup_configuration, start_hyperopt from freqtrade.optimize.default_hyperopt import DefaultHyperOpts -from freqtrade.resolvers import StrategyResolver, HyperOptResolver -from freqtrade.tests.conftest import log_has, patch_exchange -from freqtrade.tests.optimize.test_backtesting import get_args +from freqtrade.optimize.default_hyperopt_loss import DefaultHyperOptLoss +from freqtrade.optimize.hyperopt import (HYPEROPT_LOCKFILE, TICKERDATA_PICKLE, + Hyperopt) +from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver, HyperOptLossResolver +from freqtrade.state import RunMode +from freqtrade.strategy.interface import SellType +from freqtrade.tests.conftest import (get_args, log_has, log_has_re, + patch_exchange, + patched_configuration_load_config_file) @pytest.fixture(scope='function') def hyperopt(default_conf, mocker): + default_conf.update({'spaces': ['all']}) patch_exchange(mocker) return Hyperopt(default_conf) +@pytest.fixture(scope='function') +def hyperopt_results(): + return pd.DataFrame( + { + 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], + 'profit_percent': [0.1, 0.2, 0.3], + 'profit_abs': [0.2, 0.4, 0.5], + 'trade_duration': [10, 30, 10], + 'profit': [2, 0, 0], + 'loss': [0, 0, 1], + 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] + } + ) + + # Functions for recurrent object patching def create_trials(mocker, hyperopt) -> None: """ @@ -39,12 +64,112 @@ def create_trials(mocker, hyperopt) -> None: return [{'loss': 1, 'result': 'foo', 'params': {}}] -def test_hyperoptresolver(mocker, default_conf, caplog) -> None: +def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, caplog) -> None: + patched_configuration_load_config_file(mocker, default_conf) - mocker.patch( - 'freqtrade.configuration.Configuration._load_config_file', - lambda *args, **kwargs: 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 directory: {} ...'.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: + patched_configuration_load_config_file(mocker, default_conf) + mocker.patch( + 'freqtrade.configuration.configuration.create_datadir', + lambda 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 directory: {} ...'.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: + patched_configuration_load_config_file(mocker, default_conf) + hyperopts = DefaultHyperOpts delattr(hyperopts, 'populate_buy_trend') delattr(hyperopts, 'populate_sell_trend') @@ -59,26 +184,47 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None: "Using populate_sell_trend from DefaultStrategy.", caplog.record_tuples) assert log_has("Custom Hyperopt does not provide populate_buy_trend. " "Using populate_buy_trend from DefaultStrategy.", caplog.record_tuples) + assert hasattr(x, "ticker_interval") + + +def test_hyperoptresolver_wrongname(mocker, default_conf, caplog) -> None: + default_conf.update({'hyperopt': "NonExistingHyperoptClass"}) + + with pytest.raises(OperationalException, match=r'Impossible to load Hyperopt.*'): + HyperOptResolver(default_conf, ).hyperopt + + +def test_hyperoptlossresolver(mocker, default_conf, caplog) -> None: + + hl = DefaultHyperOptLoss + mocker.patch( + 'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver._load_hyperoptloss', + MagicMock(return_value=hl) + ) + x = HyperOptLossResolver(default_conf, ).hyperoptloss + assert hasattr(x, "hyperopt_loss_function") + + +def test_hyperoptlossresolver_wrongname(mocker, default_conf, caplog) -> None: + default_conf.update({'hyperopt_loss': "NonExistingLossClass"}) + + with pytest.raises(OperationalException, match=r'Impossible to load HyperoptLoss.*'): + HyperOptLossResolver(default_conf, ).hyperopt def test_start(mocker, default_conf, caplog) -> None: start_mock = MagicMock() - mocker.patch( - 'freqtrade.configuration.Configuration._load_config_file', - lambda *args, **kwargs: default_conf - ) + patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock) patch_exchange(mocker) args = [ '--config', 'config.json', - '--strategy', 'DefaultStrategy', 'hyperopt', '--epochs', '5' ] args = get_args(args) - StrategyResolver({'strategy': 'DefaultStrategy'}) - start(args) + start_hyperopt(args) import pprint pprint.pprint(caplog.record_tuples) @@ -90,12 +236,33 @@ def test_start(mocker, default_conf, caplog) -> None: assert start_mock.call_count == 1 +def test_start_no_data(mocker, default_conf, caplog) -> None: + patched_configuration_load_config_file(mocker, 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: start_mock = MagicMock() - mocker.patch( - 'freqtrade.configuration.Configuration._load_config_file', - lambda *args, **kwargs: default_conf - ) + patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock) patch_exchange(mocker) @@ -106,51 +273,115 @@ def test_start_failure(mocker, default_conf, caplog) -> None: '--epochs', '5' ] args = get_args(args) - StrategyResolver({'strategy': 'DefaultStrategy'}) - with pytest.raises(ValueError): - start(args) + with pytest.raises(DependencyException): + start_hyperopt(args) assert log_has( "Please don't use --strategy for hyperopt.", caplog.record_tuples ) -def test_loss_calculation_prefer_correct_trade_count(hyperopt) -> None: - StrategyResolver({'strategy': 'DefaultStrategy'}) +def test_start_filelock(mocker, default_conf, caplog) -> None: + start_mock = MagicMock(side_effect=Timeout(HYPEROPT_LOCKFILE)) + patched_configuration_load_config_file(mocker, default_conf) + mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock) + patch_exchange(mocker) - correct = hyperopt.calculate_loss(1, hyperopt.target_trades, 20) - over = hyperopt.calculate_loss(1, hyperopt.target_trades + 100, 20) - under = hyperopt.calculate_loss(1, hyperopt.target_trades - 100, 20) + 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(default_conf, hyperopt_results) -> None: + hl = HyperOptLossResolver(default_conf).hyperoptloss + correct = hl.hyperopt_loss_function(hyperopt_results, 600) + over = hl.hyperopt_loss_function(hyperopt_results, 600 + 100) + under = hl.hyperopt_loss_function(hyperopt_results, 600 - 100) assert over > correct assert under > correct -def test_loss_calculation_prefer_shorter_trades(hyperopt) -> None: - shorter = hyperopt.calculate_loss(1, 100, 20) - longer = hyperopt.calculate_loss(1, 100, 30) +def test_loss_calculation_prefer_shorter_trades(default_conf, hyperopt_results) -> None: + resultsb = hyperopt_results.copy() + resultsb.loc[1, 'trade_duration'] = 20 + + hl = HyperOptLossResolver(default_conf).hyperoptloss + longer = hl.hyperopt_loss_function(hyperopt_results, 100) + shorter = hl.hyperopt_loss_function(resultsb, 100) assert shorter < longer -def test_loss_calculation_has_limited_profit(hyperopt) -> None: - correct = hyperopt.calculate_loss(hyperopt.expected_max_profit, hyperopt.target_trades, 20) - over = hyperopt.calculate_loss(hyperopt.expected_max_profit * 2, hyperopt.target_trades, 20) - under = hyperopt.calculate_loss(hyperopt.expected_max_profit / 2, hyperopt.target_trades, 20) - assert over == correct +def test_loss_calculation_has_limited_profit(default_conf, hyperopt_results) -> None: + results_over = hyperopt_results.copy() + results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 + results_under = hyperopt_results.copy() + results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 + + hl = HyperOptLossResolver(default_conf).hyperoptloss + correct = hl.hyperopt_loss_function(hyperopt_results, 600) + over = hl.hyperopt_loss_function(results_over, 600) + under = hl.hyperopt_loss_function(results_under, 600) + assert over < correct + assert under > correct + + +def test_sharpe_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None: + results_over = hyperopt_results.copy() + results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 + results_under = hyperopt_results.copy() + results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 + + default_conf.update({'hyperopt_loss': 'SharpeHyperOptLoss'}) + hl = HyperOptLossResolver(default_conf).hyperoptloss + correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + assert over < correct + assert under > correct + + +def test_onlyprofit_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None: + results_over = hyperopt_results.copy() + results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 + results_under = hyperopt_results.copy() + results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 + + default_conf.update({'hyperopt_loss': 'OnlyProfitHyperOptLoss'}) + hl = HyperOptLossResolver(default_conf).hyperoptloss + correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + assert over < correct assert under > correct def test_log_results_if_loss_improves(hyperopt, capsys) -> None: hyperopt.current_best_loss = 2 + hyperopt.total_epochs = 2 hyperopt.log_results( { 'loss': 1, - 'current_tries': 1, - 'total_tries': 2, - 'result': 'foo' + 'current_epoch': 1, + 'results_explanation': 'foo.', + 'is_initial_point': False } ) 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: @@ -203,29 +434,44 @@ def test_roi_table_generation(hyperopt) -> None: assert hyperopt.custom_hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0} -def test_start_calls_optimizer(mocker, default_conf, caplog) -> None: +def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) 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( 'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel', - MagicMock(return_value=[{'loss': 1, 'result': 'foo result', 'params': {}}]) + MagicMock(return_value=[{'loss': 1, 'results_explanation': 'foo result', 'params': {}}]) ) patch_exchange(mocker) - default_conf.update({'config': 'config.json.example'}) - default_conf.update({'epochs': 1}) - default_conf.update({'timerange': None}) - default_conf.update({'spaces': 'all'}) + default_conf.update({'config': 'config.json.example', + 'epochs': 1, + 'timerange': None, + 'spaces': 'all', + 'hyperopt_jobs': 1, }) hyperopt = Hyperopt(default_conf) hyperopt.strategy.tickerdata_to_dataframe = MagicMock() + hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.start() + parallel.assert_called_once() - assert 'Best result:\nfoo result\nwith values:\n\n' in caplog.text + out, err = capsys.readouterr() + assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out assert dumper.called + # Should be called twice, once for tickerdata, once to save evaluations + assert dumper.call_count == 2 + assert hasattr(hyperopt, "advise_sell") + assert hasattr(hyperopt, "advise_buy") + assert hasattr(hyperopt, "max_open_trades") + assert hyperopt.max_open_trades == default_conf['max_open_trades'] + assert hasattr(hyperopt, "position_stacking") def test_format_results(hyperopt): @@ -266,7 +512,8 @@ def test_has_space(hyperopt): def test_populate_indicators(hyperopt) -> None: 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) dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], {'pair': 'UNITTEST/BTC'}) @@ -279,7 +526,8 @@ def test_populate_indicators(hyperopt) -> None: def test_buy_strategy_generator(hyperopt) -> None: 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) dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], {'pair': 'UNITTEST/BTC'}) @@ -307,6 +555,7 @@ def test_generate_optimizer(mocker, default_conf) -> None: default_conf.update({'config': 'config.json.example'}) default_conf.update({'timerange': None}) default_conf.update({'spaces': 'all'}) + default_conf.update({'hyperopt_min_trades': 1}) trades = [ ('POWR/BTC', 0.023117, 0.000233, 100) @@ -320,7 +569,7 @@ def test_generate_optimizer(mocker, default_conf) -> None: ) mocker.patch( 'freqtrade.optimize.hyperopt.get_timeframe', - MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) + MagicMock(return_value=(Arrow(2017, 12, 10), Arrow(2017, 12, 13))) ) patch_exchange(mocker) mocker.patch('freqtrade.optimize.hyperopt.load', MagicMock()) @@ -354,11 +603,44 @@ def test_generate_optimizer(mocker, default_conf) -> None: } response_expected = { 'loss': 1.9840569076926293, - 'result': ' 1 trades. Avg profit 2.31%. Total profit 0.00023300 BTC ' - '(0.0231Σ%). Avg duration 100.0 mins.', + 'results_explanation': ' 1 trades. Avg profit 2.31%. Total profit 0.00023300 BTC ' + '( 2.31Σ%). Avg duration 100.0 mins.', 'params': optimizer_param } hyperopt = Hyperopt(default_conf) generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values())) assert generate_optimizer_value == response_expected + + +def test_clean_hyperopt(mocker, default_conf, caplog): + patch_exchange(mocker) + default_conf.update({'config': 'config.json.example', + 'epochs': 1, + 'timerange': None, + 'spaces': 'all', + 'hyperopt_jobs': 1, + }) + mocker.patch("freqtrade.optimize.hyperopt.Path.is_file", MagicMock(return_value=True)) + unlinkmock = mocker.patch("freqtrade.optimize.hyperopt.Path.unlink", MagicMock()) + Hyperopt(default_conf) + + assert unlinkmock.call_count == 2 + assert log_has(f"Removing `{TICKERDATA_PICKLE}`.", caplog.record_tuples) + + +def test_continue_hyperopt(mocker, default_conf, caplog): + patch_exchange(mocker) + default_conf.update({'config': 'config.json.example', + 'epochs': 1, + 'timerange': None, + 'spaces': 'all', + 'hyperopt_jobs': 1, + 'hyperopt_continue': True + }) + mocker.patch("freqtrade.optimize.hyperopt.Path.is_file", MagicMock(return_value=True)) + unlinkmock = mocker.patch("freqtrade.optimize.hyperopt.Path.unlink", MagicMock()) + Hyperopt(default_conf) + + assert unlinkmock.call_count == 0 + assert log_has(f"Continuing on previous hyperopt results.", caplog.record_tuples) diff --git a/freqtrade/tests/optimize/test_optimize.py b/freqtrade/tests/optimize/test_optimize.py deleted file mode 100644 index 99cd24c26..000000000 --- a/freqtrade/tests/optimize/test_optimize.py +++ /dev/null @@ -1,65 +0,0 @@ -# pragma pylint: disable=missing-docstring, protected-access, C0103 -from freqtrade import optimize, constants -from freqtrade.arguments import TimeRange -from freqtrade.data import history -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, - constants.TICKER_INTERVAL_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, - constants.TICKER_INTERVAL_MINUTES["5m"]) - assert len(caplog.record_tuples) == 0 diff --git a/freqtrade/tests/pairlist/test_pairlist.py b/freqtrade/tests/pairlist/test_pairlist.py index 52f44c41b..e7439bb51 100644 --- a/freqtrade/tests/pairlist/test_pairlist.py +++ b/freqtrade/tests/pairlist/test_pairlist.py @@ -34,9 +34,9 @@ def whitelist_conf(default_conf): def test_load_pairlist_noexist(mocker, markets, default_conf): freqtradebot = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) - with pytest.raises(ImportError, - match=r"Impossible to load Pairlist 'NonexistingPairList'." - r" This class does not exist or contains Python code errors"): + with pytest.raises(OperationalException, + match=r"Impossible to load Pairlist 'NonexistingPairList'. " + r"This class does not exist or contains Python code errors."): PairListResolver('NonexistingPairList', freqtradebot, default_conf).pairlist @@ -107,7 +107,16 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): assert set(whitelist) == set(pairslist) -def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, markets, tickers) -> None: +@pytest.mark.parametrize("precision_filter,base_currency,key,whitelist_result", [ + (False, "BTC", "quoteVolume", ['ETH/BTC', 'TKN/BTC', 'BTT/BTC']), + (False, "BTC", "bidVolume", ['BTT/BTC', 'TKN/BTC', 'ETH/BTC']), + (False, "USDT", "quoteVolume", ['ETH/USDT', 'LTC/USDT']), + (False, "ETH", "quoteVolume", []), # this replaces tests that were removed from test_exchange + (True, "BTC", "quoteVolume", ["ETH/BTC", "TKN/BTC"]), + (True, "BTC", "bidVolume", ["TKN/BTC", "ETH/BTC"]) +]) +def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, markets, tickers, base_currency, key, + whitelist_result, precision_filter) -> None: whitelist_conf['pairlist']['method'] = 'VolumePairList' mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) @@ -115,32 +124,10 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, markets, tickers) mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers) mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, p, r: round(r, 8)) - # Test to retrieved BTC sorted on quoteVolume (default) - whitelist = freqtrade.pairlists._gen_pair_whitelist(base_currency='BTC', key='quoteVolume') - assert whitelist == ['ETH/BTC', 'TKN/BTC', 'BTT/BTC'] - - # Test to retrieve BTC sorted on bidVolume - whitelist = freqtrade.pairlists._gen_pair_whitelist(base_currency='BTC', key='bidVolume') - assert whitelist == ['BTT/BTC', 'TKN/BTC', 'ETH/BTC'] - - # Test with USDT sorted on quoteVolume (default) - freqtrade.config['stake_currency'] = 'USDT' # this has to be set, otherwise markets are removed - whitelist = freqtrade.pairlists._gen_pair_whitelist(base_currency='USDT', key='quoteVolume') - assert whitelist == ['ETH/USDT', 'LTC/USDT'] - - # Test with ETH (our fixture does not have ETH, so result should be empty) - freqtrade.config['stake_currency'] = 'ETH' - whitelist = freqtrade.pairlists._gen_pair_whitelist(base_currency='ETH', key='quoteVolume') - assert whitelist == [] - - freqtrade.pairlists._precision_filter = True - freqtrade.config['stake_currency'] = 'BTC' - # Retest First 2 test-cases to make sure BTT is not in it (too low priced) - whitelist = freqtrade.pairlists._gen_pair_whitelist(base_currency='BTC', key='quoteVolume') - assert whitelist == ['ETH/BTC', 'TKN/BTC'] - - whitelist = freqtrade.pairlists._gen_pair_whitelist(base_currency='BTC', key='bidVolume') - assert whitelist == ['TKN/BTC', 'ETH/BTC'] + freqtrade.pairlists._precision_filter = precision_filter + freqtrade.config['stake_currency'] = base_currency + whitelist = freqtrade.pairlists._gen_pair_whitelist(base_currency=base_currency, key=key) + assert whitelist == whitelist_result def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None: @@ -166,17 +153,24 @@ def test_pairlist_class(mocker, whitelist_conf, markets, pairlist): assert isinstance(freqtrade.pairlists.whitelist, list) assert isinstance(freqtrade.pairlists.blacklist, list) - whitelist = ['ETH/BTC', 'TKN/BTC'] + +@pytest.mark.parametrize("pairlist", AVAILABLE_PAIRLISTS) +@pytest.mark.parametrize("whitelist,log_message", [ + (['ETH/BTC', 'TKN/BTC'], ""), + (['ETH/BTC', 'TKN/BTC', 'TRX/ETH'], "is not compatible with exchange"), # TRX/ETH wrong stake + (['ETH/BTC', 'TKN/BTC', 'BCH/BTC'], "is not compatible with exchange"), # BCH/BTC not available + (['ETH/BTC', 'TKN/BTC', 'BLK/BTC'], "is not compatible with exchange"), # BLK/BTC in blacklist + (['ETH/BTC', 'TKN/BTC', 'LTC/BTC'], "Market is not active") # LTC/BTC is inactive +]) +def test_validate_whitelist(mocker, whitelist_conf, markets, pairlist, whitelist, caplog, + log_message): + whitelist_conf['pairlist']['method'] = pairlist + mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) + caplog.clear() + new_whitelist = freqtrade.pairlists._validate_whitelist(whitelist) - assert set(whitelist) == set(new_whitelist) - - whitelist = ['ETH/BTC', 'TKN/BTC', 'TRX/ETH'] - new_whitelist = freqtrade.pairlists._validate_whitelist(whitelist) - # TRX/ETH was removed - assert set(['ETH/BTC', 'TKN/BTC']) == set(new_whitelist) - - whitelist = ['ETH/BTC', 'TKN/BTC', 'BLK/BTC'] - new_whitelist = freqtrade.pairlists._validate_whitelist(whitelist) - # BLK/BTC is in blacklist ... - assert set(['ETH/BTC', 'TKN/BTC']) == set(new_whitelist) + assert set(new_whitelist) == set(['ETH/BTC', 'TKN/BTC']) + assert log_message in caplog.text diff --git a/freqtrade/tests/rpc/test_fiat_convert.py b/freqtrade/tests/rpc/test_fiat_convert.py index fbc942432..66870efcc 100644 --- a/freqtrade/tests/rpc/test_fiat_convert.py +++ b/freqtrade/tests/rpc/test_fiat_convert.py @@ -8,7 +8,7 @@ import pytest from requests.exceptions import RequestException from freqtrade.rpc.fiat_convert import CryptoFiat, CryptoToFiatConverter -from freqtrade.tests.conftest import log_has, patch_coinmarketcap +from freqtrade.tests.conftest import log_has def test_pair_convertion_object(): @@ -40,7 +40,6 @@ def test_pair_convertion_object(): def test_fiat_convert_is_supported(mocker): - patch_coinmarketcap(mocker) fiat_convert = CryptoToFiatConverter() assert fiat_convert._is_supported_fiat(fiat='USD') is True assert fiat_convert._is_supported_fiat(fiat='usd') is True @@ -49,7 +48,6 @@ def test_fiat_convert_is_supported(mocker): def test_fiat_convert_add_pair(mocker): - patch_coinmarketcap(mocker) fiat_convert = CryptoToFiatConverter() @@ -72,8 +70,6 @@ def test_fiat_convert_add_pair(mocker): def test_fiat_convert_find_price(mocker): - patch_coinmarketcap(mocker) - fiat_convert = CryptoToFiatConverter() with pytest.raises(ValueError, match=r'The fiat ABC is not supported.'): @@ -93,15 +89,12 @@ def test_fiat_convert_find_price(mocker): def test_fiat_convert_unsupported_crypto(mocker, caplog): mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._cryptomap', return_value=[]) - patch_coinmarketcap(mocker) fiat_convert = CryptoToFiatConverter() assert fiat_convert._find_price(crypto_symbol='CRYPTO_123', fiat_symbol='EUR') == 0.0 assert log_has('unsupported crypto-symbol CRYPTO_123 - returning 0.0', caplog.record_tuples) def test_fiat_convert_get_price(mocker): - patch_coinmarketcap(mocker) - mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', return_value=28000.0) @@ -134,21 +127,18 @@ def test_fiat_convert_get_price(mocker): def test_fiat_convert_same_currencies(mocker): - patch_coinmarketcap(mocker) fiat_convert = CryptoToFiatConverter() assert fiat_convert.get_price(crypto_symbol='USD', fiat_symbol='USD') == 1.0 def test_fiat_convert_two_FIAT(mocker): - patch_coinmarketcap(mocker) fiat_convert = CryptoToFiatConverter() assert fiat_convert.get_price(crypto_symbol='USD', fiat_symbol='EUR') == 0.0 def test_loadcryptomap(mocker): - patch_coinmarketcap(mocker) fiat_convert = CryptoToFiatConverter() assert len(fiat_convert._cryptomap) == 2 @@ -174,7 +164,6 @@ def test_fiat_init_network_exception(mocker): def test_fiat_convert_without_network(mocker): # Because CryptoToFiatConverter is a Singleton we reset the value of _coinmarketcap - patch_coinmarketcap(mocker) fiat_convert = CryptoToFiatConverter() @@ -205,7 +194,6 @@ def test_fiat_invalid_response(mocker, caplog): def test_convert_amount(mocker): - patch_coinmarketcap(mocker) mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter.get_price', return_value=12345.0) fiat_convert = CryptoToFiatConverter() diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index baddc0685..d273244b0 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -2,19 +2,19 @@ # pragma pylint: disable=invalid-sequence-index, invalid-name, too-many-arguments from datetime import datetime -from unittest.mock import MagicMock, ANY, PropertyMock +from unittest.mock import ANY, MagicMock, PropertyMock import pytest from numpy import isnan -from freqtrade import TemporaryError, DependencyException +from freqtrade import DependencyException, TemporaryError +from freqtrade.edge import PairInfo from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade from freqtrade.rpc import RPC, RPCException from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.state import State -from freqtrade.tests.test_freqtradebot import patch_get_signal -from freqtrade.tests.conftest import patch_coinmarketcap, patch_exchange +from freqtrade.tests.conftest import patch_exchange, patch_get_signal # Functions for recurrent object patching @@ -27,7 +27,6 @@ def prec_satoshi(a, b) -> float: # Unit tests def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None: - patch_coinmarketcap(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -47,17 +46,25 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None: freqtradebot.create_trade() results = rpc._rpc_trade_status() - assert { 'trade_id': 1, 'pair': 'ETH/BTC', - 'date': ANY, + 'base_currency': 'BTC', + 'open_date': ANY, + 'open_date_hum': ANY, + 'close_date': None, + 'close_date_hum': None, 'open_rate': 1.099e-05, 'close_rate': None, 'current_rate': 1.098e-05, 'amount': 90.99181074, + 'stake_amount': 0.001, 'close_profit': None, 'current_profit': -0.59, + 'stop_loss': 0.0, + 'initial_stop_loss': 0.0, + 'initial_stop_loss_pct': None, + 'stop_loss_pct': None, 'open_order': '(limit buy rem=0.00000000)' } == results[0] @@ -71,19 +78,27 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None: assert { 'trade_id': 1, 'pair': 'ETH/BTC', - 'date': ANY, + 'base_currency': 'BTC', + 'open_date': ANY, + 'open_date_hum': ANY, + 'close_date': None, + 'close_date_hum': None, 'open_rate': 1.099e-05, 'close_rate': None, 'current_rate': ANY, 'amount': 90.99181074, + 'stake_amount': 0.001, 'close_profit': None, 'current_profit': ANY, + 'stop_loss': 0.0, + 'initial_stop_loss': 0.0, + 'initial_stop_loss_pct': None, + 'stop_loss_pct': None, 'open_order': '(limit buy rem=0.00000000)' } == results[0] def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( @@ -103,7 +118,7 @@ def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None: freqtradebot.create_trade() 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 '-0.59%' in result['Profit'].all() @@ -112,14 +127,13 @@ def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None: # invalidate ticker cache rpc._freqtrade.exchange._cached_ticker = {} 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 'nan%' in result['Profit'].all() def test_rpc_daily_profit(default_conf, update, ticker, fee, limit_buy_order, limit_sell_order, markets, mocker) -> None: - patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( @@ -172,7 +186,6 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, 'freqtrade.rpc.fiat_convert.Market', ticker=MagicMock(return_value={'price_usd': 15000.0}), ) - patch_coinmarketcap(mocker) patch_exchange(mocker) mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) @@ -311,7 +324,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets, assert prec_satoshi(stats['best_rate'], 6.2) -def test_rpc_balance_handle(default_conf, mocker): +def test_rpc_balance_handle_error(default_conf, mocker): mock_balance = { 'BTC': { 'free': 10.0, @@ -330,7 +343,6 @@ def test_rpc_balance_handle(default_conf, mocker): 'freqtrade.rpc.fiat_convert.Market', ticker=MagicMock(return_value={'price_usd': 15000.0}), ) - patch_coinmarketcap(mocker) patch_exchange(mocker) mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) @@ -359,8 +371,73 @@ def test_rpc_balance_handle(default_conf, mocker): assert result['total'] == 12.0 +def test_rpc_balance_handle(default_conf, mocker): + mock_balance = { + 'BTC': { + 'free': 10.0, + 'total': 12.0, + 'used': 2.0, + }, + 'ETH': { + 'free': 1.0, + 'total': 5.0, + 'used': 4.0, + }, + 'PAX': { + 'free': 5.0, + 'total': 10.0, + 'used': 5.0, + } + } + + mocker.patch.multiple( + 'freqtrade.rpc.fiat_convert.Market', + ticker=MagicMock(return_value={'price_usd': 15000.0}), + ) + patch_exchange(mocker) + mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) + mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_balances=MagicMock(return_value=mock_balance), + get_ticker=MagicMock( + side_effect=lambda p, r: {'bid': 100} if p == "BTC/PAX" else {'bid': 0.01}), + get_valid_pair_combination=MagicMock( + side_effect=lambda a, b: f"{b}/{a}" if a == "PAX" else f"{a}/{b}") + ) + + freqtradebot = FreqtradeBot(default_conf) + patch_get_signal(freqtradebot, (True, False)) + rpc = RPC(freqtradebot) + rpc._fiat_converter = CryptoToFiatConverter() + + result = rpc._rpc_balance(default_conf['fiat_display_currency']) + assert prec_satoshi(result['total'], 12.15) + assert prec_satoshi(result['value'], 182250) + assert 'USD' == result['symbol'] + assert result['currencies'] == [ + {'currency': 'BTC', + 'available': 10.0, + 'balance': 12.0, + 'pending': 2.0, + 'est_btc': 12.0, + }, + {'available': 1.0, + 'balance': 5.0, + 'currency': 'ETH', + 'est_btc': 0.05, + 'pending': 4.0 + }, + {'available': 5.0, + 'balance': 10.0, + 'currency': 'PAX', + 'est_btc': 0.1, + 'pending': 5.0} + ] + assert result['total'] == 12.15 + + def test_rpc_start(mocker, default_conf) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( @@ -383,7 +460,6 @@ def test_rpc_start(mocker, default_conf) -> None: def test_rpc_stop(mocker, default_conf) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( @@ -407,7 +483,6 @@ def test_rpc_stop(mocker, default_conf) -> None: def test_rpc_stopbuy(mocker, default_conf) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( @@ -427,7 +502,6 @@ def test_rpc_stopbuy(mocker, default_conf) -> None: def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) @@ -459,12 +533,15 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: with pytest.raises(RPCException, match=r'.*invalid argument*'): 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() - 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 with pytest.raises(RPCException, match=r'.*trader is not running*'): @@ -507,7 +584,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 - 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 trade.amount == amount @@ -521,14 +599,14 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: '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 assert cancel_order_mock.call_count == 2 def test_performance_handle(default_conf, ticker, limit_buy_order, fee, limit_sell_order, markets, mocker) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( @@ -564,7 +642,6 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee, def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( @@ -579,20 +656,17 @@ def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None: patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) - trades = rpc._rpc_count() - nb_trades = len(trades) - assert nb_trades == 0 + counts = rpc._rpc_count() + assert counts["current"] == 0 # Create some test data freqtradebot.create_trade() - trades = rpc._rpc_count() - nb_trades = len(trades) - assert nb_trades == 1 + counts = rpc._rpc_count() + assert counts["current"] == 1 def test_rpcforcebuy(mocker, default_conf, ticker, fee, markets, limit_buy_order) -> None: default_conf['forcebuy_enable'] = True - patch_coinmarketcap(mocker) patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) buy_mm = MagicMock(return_value={'id': limit_buy_order['id']}) @@ -641,7 +715,6 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, markets, limit_buy_order def test_rpcforcebuy_stopped(mocker, default_conf) -> None: default_conf['forcebuy_enable'] = True default_conf['initial_state'] = 'stopped' - patch_coinmarketcap(mocker) patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) @@ -654,7 +727,6 @@ def test_rpcforcebuy_stopped(mocker, default_conf) -> None: def test_rpcforcebuy_disabled(mocker, default_conf) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) @@ -667,7 +739,6 @@ def test_rpcforcebuy_disabled(mocker, default_conf) -> None: def test_rpc_whitelist(mocker, default_conf) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) @@ -679,7 +750,6 @@ def test_rpc_whitelist(mocker, default_conf) -> None: def test_rpc_whitelist_dynamic(mocker, default_conf) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker) default_conf['pairlist'] = {'method': 'VolumePairList', 'config': {'number_assets': 4} @@ -693,3 +763,51 @@ def test_rpc_whitelist_dynamic(mocker, default_conf) -> None: assert ret['method'] == 'VolumePairList' assert ret['length'] == 4 assert ret['whitelist'] == default_conf['exchange']['pair_whitelist'] + + +def test_rpc_blacklist(mocker, default_conf) -> None: + patch_exchange(mocker) + mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) + + freqtradebot = FreqtradeBot(default_conf) + rpc = RPC(freqtradebot) + ret = rpc._rpc_blacklist(None) + assert ret['method'] == 'StaticPairList' + assert len(ret['blacklist']) == 2 + assert ret['blacklist'] == default_conf['exchange']['pair_blacklist'] + assert ret['blacklist'] == ['DOGE/BTC', 'HOT/BTC'] + + ret = rpc._rpc_blacklist(["ETH/BTC"]) + assert ret['method'] == 'StaticPairList' + assert len(ret['blacklist']) == 3 + assert ret['blacklist'] == default_conf['exchange']['pair_blacklist'] + assert ret['blacklist'] == ['DOGE/BTC', 'HOT/BTC', 'ETH/BTC'] + + +def test_rpc_edge_disabled(mocker, default_conf) -> None: + patch_exchange(mocker) + mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) + freqtradebot = FreqtradeBot(default_conf) + rpc = RPC(freqtradebot) + with pytest.raises(RPCException, match=r'Edge is not enabled.'): + rpc._rpc_edge() + + +def test_rpc_edge_enabled(mocker, edge_conf) -> None: + patch_exchange(mocker) + mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) + mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock( + return_value={ + 'E/F': PairInfo(-0.02, 0.66, 3.71, 0.50, 1.71, 10, 60), + } + )) + freqtradebot = FreqtradeBot(edge_conf) + + rpc = RPC(freqtradebot) + ret = rpc._rpc_edge() + + assert len(ret) == 1 + assert ret[0]['Pair'] == 'E/F' + assert ret[0]['Winrate'] == 0.66 + assert ret[0]['Expectancy'] == 1.71 + assert ret[0]['Stoploss'] == -0.02 diff --git a/freqtrade/tests/rpc/test_rpc_apiserver.py b/freqtrade/tests/rpc/test_rpc_apiserver.py new file mode 100644 index 000000000..bd420ada6 --- /dev/null +++ b/freqtrade/tests/rpc/test_rpc_apiserver.py @@ -0,0 +1,558 @@ +""" +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) + mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination', + side_effect=lambda a, b: f"{a}/{b}") + + 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.'} diff --git a/freqtrade/tests/rpc/test_rpc_manager.py b/freqtrade/tests/rpc/test_rpc_manager.py index 15d9c20c6..91fd2297f 100644 --- a/freqtrade/tests/rpc/test_rpc_manager.py +++ b/freqtrade/tests/rpc/test_rpc_manager.py @@ -135,3 +135,32 @@ def test_startupmessages_telegram_enabled(mocker, default_conf, caplog) -> None: rpc_manager.startup_messages(default_conf, freqtradebot.pairlists) assert telegram_mock.call_count == 3 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 diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 8e8d1f1bb..1bee5bff3 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -4,7 +4,8 @@ import re from datetime import datetime -from random import randint +from random import choice, randint +from string import ascii_uppercase from unittest.mock import MagicMock, PropertyMock import arrow @@ -13,16 +14,15 @@ from telegram import Chat, Message, Update from telegram.error import NetworkError from freqtrade import __version__ +from freqtrade.edge import PairInfo from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade from freqtrade.rpc import RPCMessageType from freqtrade.rpc.telegram import Telegram, authorized_only -from freqtrade.strategy.interface import SellType from freqtrade.state import State +from freqtrade.strategy.interface import SellType from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has, - patch_exchange) -from freqtrade.tests.test_freqtradebot import patch_get_signal -from freqtrade.tests.conftest import patch_coinmarketcap + patch_exchange, patch_get_signal) class DummyCls(Telegram): @@ -74,7 +74,7 @@ def test_init(default_conf, mocker, caplog) -> None: message_str = "rpc.telegram is listening for following commands: [['status'], ['profit'], " \ "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], " \ "['performance'], ['daily'], ['count'], ['reload_conf'], " \ - "['stopbuy'], ['whitelist'], ['help'], ['version']]" + "['stopbuy'], ['whitelist'], ['blacklist'], ['edge'], ['help'], ['version']]" assert log_has(message_str, caplog.record_tuples) @@ -90,7 +90,6 @@ def test_cleanup(default_conf, mocker) -> None: def test_authorized_only(default_conf, mocker, caplog) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker, None) chat = Chat(0, 0) @@ -118,7 +117,6 @@ def test_authorized_only(default_conf, mocker, caplog) -> None: def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker, None) chat = Chat(0xdeadbeef, 0) update = Update(randint(1, 100)) @@ -145,7 +143,6 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None: def test_authorized_only_exception(default_conf, mocker, caplog) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker) update = Update(randint(1, 100)) @@ -178,7 +175,6 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None: default_conf['telegram']['enabled'] = False default_conf['telegram']['chat_id'] = 123 - patch_coinmarketcap(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -194,13 +190,22 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None: _rpc_trade_status=MagicMock(return_value=[{ 'trade_id': 1, 'pair': 'ETH/BTC', - 'date': arrow.utcnow(), + 'base_currency': 'BTC', + 'open_date': arrow.utcnow(), + 'open_date_hum': arrow.utcnow().humanize, + 'close_date': None, + 'close_date_hum': None, 'open_rate': 1.099e-05, 'close_rate': None, 'current_rate': 1.098e-05, 'amount': 90.99181074, + 'stake_amount': 90.99181074, 'close_profit': None, 'current_profit': -0.59, + 'initial_stop_loss': 1.098e-05, + 'stop_loss': 1.099e-05, + 'initial_stop_loss_pct': -0.05, + 'stop_loss_pct': -0.01, 'open_order': '(limit buy rem=0.00000000)' }]), _status_table=status_table, @@ -226,7 +231,6 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None: def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -267,12 +271,18 @@ def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> No # Trigger status while we have a fulfilled order for the open trade telegram._status(bot=MagicMock(), update=update) + # close_rate should not be included in the message as the trade is not closed + # and no line should be empty + lines = msg_mock.call_args_list[0][0][0].split('\n') + assert '' not in lines + assert 'Close Rate' not in ''.join(lines) + assert 'Close Profit' not in ''.join(lines) + assert msg_mock.call_count == 1 assert 'ETH/BTC' in msg_mock.call_args_list[0][0][0] def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -324,7 +334,6 @@ def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker) def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, limit_sell_order, markets, mocker) -> None: - patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) patch_exchange(mocker) mocker.patch( 'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', @@ -395,7 +404,6 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: - patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -431,7 +439,6 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, limit_buy_order, limit_sell_order, markets, mocker) -> None: - patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) patch_exchange(mocker) mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch.multiple( @@ -488,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] -def test_telegram_balance_handle(default_conf, update, mocker) -> 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 test_telegram_balance_handle(default_conf, update, mocker, rpc_balance) -> None: def mock_ticker(symbol, refresh): if symbol == 'BTC/USDT': @@ -536,9 +516,10 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None: 'last': 0.1, } - patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) - 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_valid_pair_combination', + side_effect=lambda a, b: f"{a}/{b}") msg_mock = MagicMock() mocker.patch.multiple( @@ -558,6 +539,7 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None: assert '*BTC:*' in result assert '*ETH:*' not in result assert '*USDT:*' in result + assert '*EUR:*' in result assert 'Balance:' in result assert 'Est. BTC:' in result assert 'BTC: 12.00000000' in result @@ -579,10 +561,71 @@ def test_balance_handle_empty_response(default_conf, update, mocker) -> None: telegram = Telegram(freqtradebot) + freqtradebot.config['dry_run'] = False telegram._balance(bot=MagicMock(), update=update) result = msg_mock.call_args_list[0][0][0] assert msg_mock.call_count == 1 - assert 'all balances are zero' in result + assert 'All balances are zero.' in result + + +def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None: + mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value={}) + + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) + + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + patch_get_signal(freqtradebot, (True, False)) + + telegram = Telegram(freqtradebot) + + telegram._balance(bot=MagicMock(), update=update) + result = msg_mock.call_args_list[0][0][0] + assert msg_mock.call_count == 1 + assert "Running in Dry Run, balances are not available." in result + + +def test_balance_handle_too_large_response(default_conf, update, mocker) -> None: + balances = [] + for i in range(100): + curr = choice(ascii_uppercase) + choice(ascii_uppercase) + choice(ascii_uppercase) + balances.append({ + 'currency': curr, + 'available': 1.0, + 'pending': 0.5, + 'balance': i, + 'est_btc': 1 + }) + mocker.patch('freqtrade.rpc.rpc.RPC._rpc_balance', return_value={ + 'currencies': balances, + 'total': 100.0, + 'symbol': 100.0, + 'value': 1000.0, + }) + + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) + + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + patch_get_signal(freqtradebot, (True, False)) + + telegram = Telegram(freqtradebot) + + telegram._balance(bot=MagicMock(), update=update) + assert msg_mock.call_count > 1 + # Test if wrap happens around 4000 - + # and each single currency-output is around 120 characters long so we need + # an offset to avoid random test failures + assert len(msg_mock.call_args_list[0][0][0]) < 4096 + assert len(msg_mock.call_args_list[0][0][0]) > (4096 - 120) def test_start_handle(default_conf, update, mocker) -> None: @@ -623,7 +666,6 @@ def test_start_handle_already_running(default_conf, update, mocker) -> None: def test_stop_handle(default_conf, update, mocker) -> None: - patch_coinmarketcap(mocker) msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', @@ -643,7 +685,6 @@ def test_stop_handle(default_conf, update, mocker) -> None: def test_stop_handle_already_stopped(default_conf, update, mocker) -> None: - patch_coinmarketcap(mocker) msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', @@ -663,7 +704,6 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None: def test_stopbuy_handle(default_conf, update, mocker) -> None: - patch_coinmarketcap(mocker) msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', @@ -683,7 +723,6 @@ def test_stopbuy_handle(default_conf, update, mocker) -> None: def test_reload_conf_handle(default_conf, update, mocker) -> None: - patch_coinmarketcap(mocker) msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', @@ -704,7 +743,6 @@ def test_reload_conf_handle(default_conf, update, mocker) -> None: def test_forcesell_handle(default_conf, update, ticker, fee, ticker_sell_up, markets, mocker) -> None: - patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) @@ -742,6 +780,7 @@ def test_forcesell_handle(default_conf, update, ticker, fee, 'gain': 'profit', 'limit': 1.172e-05, 'amount': 90.99181073703367, + 'order_type': 'limit', 'open_rate': 1.099e-05, 'current_rate': 1.172e-05, 'profit_amount': 6.126e-05, @@ -754,7 +793,6 @@ def test_forcesell_handle(default_conf, update, ticker, fee, def test_forcesell_down_handle(default_conf, update, ticker, fee, ticker_sell_down, markets, mocker) -> None: - patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) @@ -797,6 +835,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, 'gain': 'loss', 'limit': 1.044e-05, 'amount': 90.99181073703367, + 'order_type': 'limit', 'open_rate': 1.099e-05, 'current_rate': 1.044e-05, 'profit_amount': -5.492e-05, @@ -808,7 +847,6 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker) -> None: - patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) patch_exchange(mocker) mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) @@ -843,6 +881,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker 'gain': 'loss', 'limit': 1.098e-05, 'amount': 90.99181073703367, + 'order_type': 'limit', 'open_rate': 1.099e-05, 'current_rate': 1.098e-05, 'profit_amount': -5.91e-06, @@ -854,7 +893,6 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: - patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) msg_mock = MagicMock() @@ -894,7 +932,6 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: def test_forcebuy_handle(default_conf, update, markets, mocker) -> None: - patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.rpc.telegram.Telegram._send_msg', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) @@ -931,7 +968,6 @@ def test_forcebuy_handle(default_conf, update, markets, mocker) -> None: def test_forcebuy_handle_exception(default_conf, update, markets, mocker) -> None: - patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram._send_msg', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) @@ -954,7 +990,6 @@ def test_forcebuy_handle_exception(default_conf, update, markets, mocker) -> Non def test_performance_handle(default_conf, update, ticker, fee, limit_buy_order, limit_sell_order, markets, mocker) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker) msg_mock = MagicMock() mocker.patch.multiple( @@ -994,7 +1029,6 @@ def test_performance_handle(default_conf, update, ticker, fee, def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker) msg_mock = MagicMock() mocker.patch.multiple( @@ -1035,7 +1069,6 @@ def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> Non def test_whitelist_static(default_conf, update, mocker) -> None: - patch_coinmarketcap(mocker) msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', @@ -1053,7 +1086,6 @@ def test_whitelist_static(default_conf, update, mocker) -> None: def test_whitelist_dynamic(default_conf, update, mocker) -> None: - patch_coinmarketcap(mocker) msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', @@ -1074,8 +1106,71 @@ def test_whitelist_dynamic(default_conf, update, mocker) -> None: in msg_mock.call_args_list[0][0][0]) +def test_blacklist_static(default_conf, update, mocker) -> None: + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + + telegram = Telegram(freqtradebot) + + telegram._blacklist(bot=MagicMock(), update=update, args=[]) + assert msg_mock.call_count == 1 + assert ("Blacklist contains 2 pairs\n`DOGE/BTC, HOT/BTC`" + in msg_mock.call_args_list[0][0][0]) + + msg_mock.reset_mock() + telegram._blacklist(bot=MagicMock(), update=update, args=["ETH/BTC"]) + assert msg_mock.call_count == 1 + assert ("Blacklist contains 3 pairs\n`DOGE/BTC, HOT/BTC, ETH/BTC`" + in msg_mock.call_args_list[0][0][0]) + assert freqtradebot.pairlists.blacklist == ["DOGE/BTC", "HOT/BTC", "ETH/BTC"] + + +def test_edge_disabled(default_conf, update, mocker) -> None: + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) + + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + + telegram = Telegram(freqtradebot) + + telegram._edge(bot=MagicMock(), update=update) + assert msg_mock.call_count == 1 + assert "Edge is not enabled." in msg_mock.call_args_list[0][0][0] + + +def test_edge_enabled(edge_conf, update, mocker) -> None: + msg_mock = MagicMock() + mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock( + return_value={ + 'E/F': PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60), + } + )) + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) + + freqtradebot = get_patched_freqtradebot(mocker, edge_conf) + + telegram = Telegram(freqtradebot) + + telegram._edge(bot=MagicMock(), update=update) + assert msg_mock.call_count == 1 + assert 'Edge only validated following pairs:\n
' in msg_mock.call_args_list[0][0][0]
+    assert 'Pair      Winrate    Expectancy    Stoploss' in msg_mock.call_args_list[0][0][0]
+
+
 def test_help_handle(default_conf, update, mocker) -> None:
-    patch_coinmarketcap(mocker)
     msg_mock = MagicMock()
     mocker.patch.multiple(
         'freqtrade.rpc.telegram.Telegram',
@@ -1092,7 +1187,6 @@ def test_help_handle(default_conf, update, mocker) -> None:
 
 
 def test_version_handle(default_conf, update, mocker) -> None:
-    patch_coinmarketcap(mocker)
     msg_mock = MagicMock()
     mocker.patch.multiple(
         'freqtrade.rpc.telegram.Telegram',
@@ -1121,6 +1215,7 @@ def test_send_msg_buy_notification(default_conf, mocker) -> None:
         'exchange': 'Bittrex',
         'pair': 'ETH/BTC',
         'limit': 1.099e-05,
+        'order_type': 'limit',
         'stake_amount': 0.001,
         'stake_amount_fiat': 0.0,
         'stake_currency': 'BTC',
@@ -1128,7 +1223,7 @@ def test_send_msg_buy_notification(default_conf, mocker) -> None:
     })
     assert msg_mock.call_args[0][0] \
         == '*Bittrex:* Buying ETH/BTC\n' \
-           'with limit `0.00001099\n' \
+           'at rate `0.00001099\n' \
            '(0.001000 BTC,0.000 USD)`'
 
 
@@ -1150,6 +1245,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
         'gain': 'loss',
         'limit': 3.201e-05,
         'amount': 1333.3333333333335,
+        'order_type': 'market',
         'open_rate': 7.5e-05,
         'current_rate': 3.201e-05,
         'profit_amount': -0.05746268,
@@ -1160,7 +1256,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
     })
     assert msg_mock.call_args[0][0] \
         == ('*Binance:* Selling KEY/ETH\n'
-            '*Limit:* `0.00003201`\n'
+            '*Rate:* `0.00003201`\n'
             '*Amount:* `1333.33333333`\n'
             '*Open Rate:* `0.00007500`\n'
             '*Current Rate:* `0.00003201`\n'
@@ -1175,6 +1271,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
         'gain': 'loss',
         'limit': 3.201e-05,
         'amount': 1333.3333333333335,
+        'order_type': 'market',
         'open_rate': 7.5e-05,
         'current_rate': 3.201e-05,
         'profit_amount': -0.05746268,
@@ -1184,7 +1281,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
     })
     assert msg_mock.call_args[0][0] \
         == ('*Binance:* Selling KEY/ETH\n'
-            '*Limit:* `0.00003201`\n'
+            '*Rate:* `0.00003201`\n'
             '*Amount:* `1333.33333333`\n'
             '*Open Rate:* `0.00007500`\n'
             '*Current Rate:* `0.00003201`\n'
@@ -1272,6 +1369,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None:
         'exchange': 'Bittrex',
         'pair': 'ETH/BTC',
         'limit': 1.099e-05,
+        'order_type': 'limit',
         'stake_amount': 0.001,
         'stake_amount_fiat': 0.0,
         'stake_currency': 'BTC',
@@ -1279,7 +1377,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None:
     })
     assert msg_mock.call_args[0][0] \
         == '*Bittrex:* Buying ETH/BTC\n' \
-           'with limit `0.00001099\n' \
+           'at rate `0.00001099\n' \
            '(0.001000 BTC)`'
 
 
@@ -1300,6 +1398,7 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
         'gain': 'loss',
         'limit': 3.201e-05,
         'amount': 1333.3333333333335,
+        'order_type': 'limit',
         'open_rate': 7.5e-05,
         'current_rate': 3.201e-05,
         'profit_amount': -0.05746268,
@@ -1310,7 +1409,7 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
     })
     assert msg_mock.call_args[0][0] \
         == '*Binance:* Selling KEY/ETH\n' \
-           '*Limit:* `0.00003201`\n' \
+           '*Rate:* `0.00003201`\n' \
            '*Amount:* `1333.33333333`\n' \
            '*Open Rate:* `0.00007500`\n' \
            '*Current Rate:* `0.00003201`\n' \
@@ -1319,7 +1418,6 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
 
 
 def test__send_msg(default_conf, mocker) -> None:
-    patch_coinmarketcap(mocker)
     mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
     bot = MagicMock()
     freqtradebot = get_patched_freqtradebot(mocker, default_conf)
@@ -1331,7 +1429,6 @@ def test__send_msg(default_conf, mocker) -> None:
 
 
 def test__send_msg_network_error(default_conf, mocker, caplog) -> None:
-    patch_coinmarketcap(mocker)
     mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
     bot = MagicMock()
     bot.send_message = MagicMock(side_effect=NetworkError('Oh snap'))
diff --git a/freqtrade/tests/rpc/test_rpc_webhook.py b/freqtrade/tests/rpc/test_rpc_webhook.py
index da7aec0a6..a2dcd9b31 100644
--- a/freqtrade/tests/rpc/test_rpc_webhook.py
+++ b/freqtrade/tests/rpc/test_rpc_webhook.py
@@ -74,6 +74,7 @@ def test_send_msg(default_conf, mocker):
         'gain': "profit",
         'limit': 0.005,
         'amount': 0.8,
+        'order_type': 'limit',
         'open_rate': 0.004,
         'current_rate': 0.005,
         'profit_amount': 0.001,
@@ -126,6 +127,7 @@ def test_exception_send_msg(default_conf, mocker, caplog):
         'exchange': 'Bittrex',
         'pair': 'ETH/BTC',
         'limit': 0.005,
+        'order_type': 'limit',
         'stake_amount': 0.8,
         'stake_amount_fiat': 500,
         'stake_currency': 'BTC',
diff --git a/freqtrade/tests/strategy/test_default_strategy.py b/freqtrade/tests/strategy/test_default_strategy.py
index be514f2d1..74c81882a 100644
--- a/freqtrade/tests/strategy/test_default_strategy.py
+++ b/freqtrade/tests/strategy/test_default_strategy.py
@@ -10,7 +10,8 @@ from freqtrade.strategy.default_strategy import DefaultStrategy
 @pytest.fixture
 def result():
     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():
diff --git a/freqtrade/tests/strategy/test_interface.py b/freqtrade/tests/strategy/test_interface.py
index d6ef0c8e7..9f5ab70e3 100644
--- a/freqtrade/tests/strategy/test_interface.py
+++ b/freqtrade/tests/strategy/test_interface.py
@@ -6,7 +6,7 @@ from unittest.mock import MagicMock
 import arrow
 from pandas import DataFrame
 
-from freqtrade.arguments import TimeRange
+from freqtrade.configuration import TimeRange
 from freqtrade.data.converter import parse_ticker_dataframe
 from freqtrade.data.history import load_tickerdata_file
 from freqtrade.persistence import Trade
@@ -19,13 +19,13 @@ _STRATEGY = DefaultStrategy(config={})
 
 def test_returns_latest_buy_signal(mocker, default_conf, ticker_history):
     mocker.patch.object(
-        _STRATEGY, 'analyze_ticker',
+        _STRATEGY, '_analyze_ticker_internal',
         return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}])
     )
     assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (True, False)
 
     mocker.patch.object(
-        _STRATEGY, 'analyze_ticker',
+        _STRATEGY, '_analyze_ticker_internal',
         return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}])
     )
     assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (False, True)
@@ -33,14 +33,14 @@ def test_returns_latest_buy_signal(mocker, default_conf, ticker_history):
 
 def test_returns_latest_sell_signal(mocker, default_conf, ticker_history):
     mocker.patch.object(
-        _STRATEGY, 'analyze_ticker',
+        _STRATEGY, '_analyze_ticker_internal',
         return_value=DataFrame([{'sell': 1, 'buy': 0, 'date': arrow.utcnow()}])
     )
 
     assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (False, True)
 
     mocker.patch.object(
-        _STRATEGY, 'analyze_ticker',
+        _STRATEGY, '_analyze_ticker_internal',
         return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}])
     )
     assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (True, False)
@@ -60,7 +60,7 @@ def test_get_signal_empty(default_conf, mocker, caplog):
 def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ticker_history):
     caplog.set_level(logging.INFO)
     mocker.patch.object(
-        _STRATEGY, 'analyze_ticker',
+        _STRATEGY, '_analyze_ticker_internal',
         side_effect=ValueError('xyz')
     )
     assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'],
@@ -71,7 +71,7 @@ def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ticker_hi
 def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ticker_history):
     caplog.set_level(logging.INFO)
     mocker.patch.object(
-        _STRATEGY, 'analyze_ticker',
+        _STRATEGY, '_analyze_ticker_internal',
         return_value=DataFrame([])
     )
     assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'],
@@ -86,7 +86,7 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ticker_history):
     oldtime = arrow.utcnow().shift(minutes=-16)
     ticks = DataFrame([{'buy': 1, 'date': oldtime}])
     mocker.patch.object(
-        _STRATEGY, 'analyze_ticker',
+        _STRATEGY, '_analyze_ticker_internal',
         return_value=DataFrame(ticks)
     )
     assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'],
@@ -111,7 +111,8 @@ def test_tickerdata_to_dataframe(default_conf) -> None:
 
     timerange = TimeRange(None, 'line', 0, -100)
     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)
     assert len(data['UNITTEST/BTC']) == 102       # partial candle was removed
 
@@ -185,6 +186,39 @@ def test_min_roi_reached2(default_conf, fee) -> None:
         assert strategy.min_roi_reached(trade, 0.31, arrow.utcnow().shift(minutes=-2).datetime)
 
 
+def test_min_roi_reached3(default_conf, fee) -> None:
+
+    # test for issue #1948
+    min_roi = {20: 0.07,
+               30: 0.05,
+               55: 0.30,
+               }
+    strategy = DefaultStrategy(default_conf)
+    strategy.minimal_roi = min_roi
+    trade = Trade(
+            pair='ETH/BTC',
+            stake_amount=0.001,
+            open_date=arrow.utcnow().shift(hours=-1).datetime,
+            fee_open=fee.return_value,
+            fee_close=fee.return_value,
+            exchange='bittrex',
+            open_rate=1,
+    )
+
+    assert not strategy.min_roi_reached(trade, 0.02, arrow.utcnow().shift(minutes=-56).datetime)
+    assert not strategy.min_roi_reached(trade, 0.12, arrow.utcnow().shift(minutes=-56).datetime)
+
+    assert not strategy.min_roi_reached(trade, 0.04, arrow.utcnow().shift(minutes=-39).datetime)
+    assert strategy.min_roi_reached(trade, 0.071, arrow.utcnow().shift(minutes=-39).datetime)
+
+    assert not strategy.min_roi_reached(trade, 0.04, arrow.utcnow().shift(minutes=-26).datetime)
+    assert strategy.min_roi_reached(trade, 0.06, arrow.utcnow().shift(minutes=-26).datetime)
+
+    # Should not trigger with 20% profit since after 55 minutes only 30% is active.
+    assert not strategy.min_roi_reached(trade, 0.20, arrow.utcnow().shift(minutes=-2).datetime)
+    assert strategy.min_roi_reached(trade, 0.31, arrow.utcnow().shift(minutes=-2).datetime)
+
+
 def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None:
     caplog.set_level(logging.DEBUG)
     ind_mock = MagicMock(side_effect=lambda x, meta: x)
@@ -218,7 +252,7 @@ def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None:
                        caplog.record_tuples)
 
 
-def test_analyze_ticker_skip_analyze(ticker_history, mocker, caplog) -> None:
+def test__analyze_ticker_internal_skip_analyze(ticker_history, mocker, caplog) -> None:
     caplog.set_level(logging.DEBUG)
     ind_mock = MagicMock(side_effect=lambda x, meta: x)
     buy_mock = MagicMock(side_effect=lambda x, meta: x)
@@ -233,7 +267,7 @@ def test_analyze_ticker_skip_analyze(ticker_history, mocker, caplog) -> None:
     strategy = DefaultStrategy({})
     strategy.process_only_new_candles = True
 
-    ret = strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'})
+    ret = strategy._analyze_ticker_internal(ticker_history, {'pair': 'ETH/BTC'})
     assert 'high' in ret.columns
     assert 'low' in ret.columns
     assert 'close' in ret.columns
@@ -246,7 +280,7 @@ def test_analyze_ticker_skip_analyze(ticker_history, mocker, caplog) -> None:
                        caplog.record_tuples)
     caplog.clear()
 
-    ret = strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'})
+    ret = strategy._analyze_ticker_internal(ticker_history, {'pair': 'ETH/BTC'})
     # No analysis happens as process_only_new_candles is true
     assert ind_mock.call_count == 1
     assert buy_mock.call_count == 1
diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py
index 602ea5dbe..df8c0f126 100644
--- a/freqtrade/tests/strategy/test_strategy.py
+++ b/freqtrade/tests/strategy/test_strategy.py
@@ -1,17 +1,21 @@
 # pragma pylint: disable=missing-docstring, protected-access, C0103
 import logging
+import tempfile
+import warnings
 from base64 import urlsafe_b64encode
 from os import path
 from pathlib import Path
-import warnings
+from unittest.mock import Mock
 
 import pytest
 from pandas import DataFrame
 
+from freqtrade import OperationalException
+from freqtrade.resolvers import StrategyResolver
 from freqtrade.strategy import import_strategy
 from freqtrade.strategy.default_strategy import DefaultStrategy
 from freqtrade.strategy.interface import IStrategy
-from freqtrade.resolvers import StrategyResolver
+from freqtrade.tests.conftest import log_has_re
 
 
 def test_import_strategy(caplog):
@@ -41,59 +45,69 @@ def test_import_strategy(caplog):
 
 def test_search_strategy():
     default_config = {}
-    default_location = Path(__file__).parent.parent.joinpath('strategy').resolve()
-    assert isinstance(
-        StrategyResolver._search_object(
-            directory=default_location,
-            object_type=IStrategy,
-            kwargs={'config': default_config},
-            object_name='DefaultStrategy'
-        ),
-        IStrategy
+    default_location = Path(__file__).parent.parent.parent.joinpath('strategy').resolve()
+
+    s, _ = StrategyResolver._search_object(
+        directory=default_location,
+        object_type=IStrategy,
+        kwargs={'config': default_config},
+        object_name='DefaultStrategy'
     )
-    assert StrategyResolver._search_object(
+    assert isinstance(s, IStrategy)
+
+    s, _ = StrategyResolver._search_object(
         directory=default_location,
         object_type=IStrategy,
         kwargs={'config': default_config},
         object_name='NotFoundStrategy'
-    ) is None
+    )
+    assert s is None
 
 
 def test_load_strategy(result):
     resolver = StrategyResolver({'strategy': 'TestStrategy'})
-    metadata = {'pair': 'ETH/BTC'}
-    assert 'adx' in resolver.strategy.advise_indicators(result, metadata=metadata)
+    assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
 
 
-def test_load_strategy_byte64(result):
-    with open("freqtrade/tests/strategy/test_strategy.py", "r") as file:
-        encoded_string = urlsafe_b64encode(file.read().encode("utf-8")).decode("utf-8")
+def test_load_strategy_base64(result, caplog):
+    with open("user_data/strategies/test_strategy.py", "rb") as file:
+        encoded_string = urlsafe_b64encode(file.read()).decode("utf-8")
     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'})
+    # Make sure strategy was loaded from base64 (using temp directory)!!
+    assert log_has_re(r"Using resolved strategy TestStrategy from '"
+                      + tempfile.gettempdir() + r"/.*/TestStrategy\.py'\.\.\.",
+                      caplog.record_tuples)
 
 
 def test_load_strategy_invalid_directory(result, caplog):
     resolver = StrategyResolver()
-    extra_dir = path.join('some', 'path')
+    extra_dir = Path.cwd() / 'some/path'
     resolver._load_strategy('TestStrategy', config={}, extra_dir=extra_dir)
 
-    assert (
-        'freqtrade.resolvers.strategy_resolver',
-        logging.WARNING,
-        'Path "{}" does not exist'.format(extra_dir),
-    ) in caplog.record_tuples
+    assert log_has_re(r'Path .*' + r'some.*path.*' + r'.* does not exist', caplog.record_tuples)
 
     assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
 
 
 def test_load_not_found_strategy():
     strategy = StrategyResolver()
-    with pytest.raises(ImportError,
-                       match=r"Impossible to load Strategy 'NotFoundStrategy'."
-                             r" This class does not exist or contains Python code errors"):
+    with pytest.raises(OperationalException,
+                       match=r"Impossible to load Strategy 'NotFoundStrategy'. "
+                             r"This class does not exist or contains Python code errors."):
         strategy._load_strategy(strategy_name='NotFoundStrategy', config={})
 
 
+def test_load_staticmethod_importerror(mocker, caplog):
+    mocker.patch("freqtrade.resolvers.strategy_resolver.import_strategy", Mock(
+        side_effect=TypeError("can't pickle staticmethod objects")))
+    with pytest.raises(OperationalException,
+                       match=r"Impossible to load Strategy 'DefaultStrategy'. "
+                             r"This class does not exist or contains Python code errors."):
+        StrategyResolver()
+    assert log_has_re(r".*Error: can't pickle staticmethod objects", caplog.record_tuples)
+
+
 def test_strategy(result):
     config = {'strategy': 'DefaultStrategy'}
 
@@ -194,11 +208,13 @@ def test_strategy_override_ticker_interval(caplog):
 
     config = {
         'strategy': 'DefaultStrategy',
-        'ticker_interval': 60
+        'ticker_interval': 60,
+        'stake_currency': 'ETH'
     }
     resolver = StrategyResolver(config)
 
     assert resolver.strategy.ticker_interval == 60
+    assert resolver.strategy.stake_currency == 'ETH'
     assert ('freqtrade.resolvers.strategy_resolver',
             logging.INFO,
             "Override strategy 'ticker_interval' with value in config file: 60."
@@ -350,6 +366,7 @@ def test_strategy_override_use_sell_profit_only(caplog):
             ) in caplog.record_tuples
 
 
+@pytest.mark.filterwarnings("ignore:deprecated")
 def test_deprecate_populate_indicators(result):
     default_location = path.join(path.dirname(path.realpath(__file__)))
     resolver = StrategyResolver({'strategy': 'TestStrategyLegacy',
@@ -357,7 +374,7 @@ def test_deprecate_populate_indicators(result):
     with warnings.catch_warnings(record=True) as w:
         # Cause all warnings to always be triggered.
         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 issubclass(w[-1].category, DeprecationWarning)
         assert "deprecated - check out the Sample strategy to see the current function headers!" \
@@ -366,7 +383,7 @@ def test_deprecate_populate_indicators(result):
     with warnings.catch_warnings(record=True) as w:
         # Cause all warnings to always be triggered.
         warnings.simplefilter("always")
-        resolver.strategy.advise_buy(indicators, 'ETH/BTC')
+        resolver.strategy.advise_buy(indicators, {'pair': 'ETH/BTC'})
         assert len(w) == 1
         assert issubclass(w[-1].category, DeprecationWarning)
         assert "deprecated - check out the Sample strategy to see the current function headers!" \
@@ -375,13 +392,14 @@ def test_deprecate_populate_indicators(result):
     with warnings.catch_warnings(record=True) as w:
         # Cause all warnings to always be triggered.
         warnings.simplefilter("always")
-        resolver.strategy.advise_sell(indicators, 'ETH_BTC')
+        resolver.strategy.advise_sell(indicators, {'pair': 'ETH_BTC'})
         assert len(w) == 1
         assert issubclass(w[-1].category, DeprecationWarning)
         assert "deprecated - check out the Sample strategy to see the current function headers!" \
             in str(w[-1].message)
 
 
+@pytest.mark.filterwarnings("ignore:deprecated")
 def test_call_deprecated_function(result, monkeypatch):
     default_location = path.join(path.dirname(path.realpath(__file__)))
     resolver = StrategyResolver({'strategy': 'TestStrategyLegacy',
diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py
index 0952d1c5d..bf744f72b 100644
--- a/freqtrade/tests/test_arguments.py
+++ b/freqtrade/tests/test_arguments.py
@@ -1,10 +1,11 @@
 # pragma pylint: disable=missing-docstring, C0103
-
 import argparse
 
 import pytest
 
-from freqtrade.arguments import Arguments, TimeRange
+from freqtrade.configuration import Arguments, TimeRange
+from freqtrade.configuration.arguments import ARGS_DOWNLOADER, ARGS_PLOT_DATAFRAME
+from freqtrade.configuration.cli_options import check_int_positive
 
 
 # Parse common command-line-arguments. Used for all tools
@@ -19,7 +20,7 @@ def test_parse_args_defaults() -> None:
     assert args.config == ['config.json']
     assert args.strategy_path is None
     assert args.datadir is None
-    assert args.loglevel == 0
+    assert args.verbosity == 0
 
 
 def test_parse_args_config() -> None:
@@ -42,16 +43,16 @@ def test_parse_args_db_url() -> None:
 
 def test_parse_args_verbose() -> None:
     args = Arguments(['-v'], '').get_parsed_arg()
-    assert args.loglevel == 1
+    assert args.verbosity == 1
 
     args = Arguments(['--verbose'], '').get_parsed_arg()
-    assert args.loglevel == 1
+    assert args.verbosity == 1
 
 
-def test_scripts_options() -> None:
+def test_common_scripts_options() -> None:
     arguments = Arguments(['-p', 'ETH/BTC'], '')
-    arguments.scripts_options()
-    args = arguments.get_parsed_arg()
+    arguments._build_args(ARGS_DOWNLOADER)
+    args = arguments._parse_args()
     assert args.pairs == 'ETH/BTC'
 
 
@@ -85,21 +86,6 @@ def test_parse_args_strategy_path_invalid() -> None:
         Arguments(['--strategy-path'], '').get_parsed_arg()
 
 
-def test_parse_args_dynamic_whitelist() -> None:
-    args = Arguments(['--dynamic-whitelist'], '').get_parsed_arg()
-    assert args.dynamic_whitelist == 20
-
-
-def test_parse_args_dynamic_whitelist_10() -> None:
-    args = Arguments(['--dynamic-whitelist', '10'], '').get_parsed_arg()
-    assert args.dynamic_whitelist == 10
-
-
-def test_parse_args_dynamic_whitelist_invalid_values() -> None:
-    with pytest.raises(SystemExit, match=r'2'):
-        Arguments(['--dynamic-whitelist', 'abc'], '').get_parsed_arg()
-
-
 def test_parse_timerange_incorrect() -> None:
     assert TimeRange(None, 'line', 0, -200) == Arguments.parse_timerange('-200')
     assert TimeRange('line', None, 200, 0) == Arguments.parse_timerange('200-')
@@ -146,7 +132,7 @@ def test_parse_args_backtesting_custom() -> None:
     call_args = Arguments(args, '').get_parsed_arg()
     assert call_args.config == ['test_conf.json']
     assert call_args.live is True
-    assert call_args.loglevel == 0
+    assert call_args.verbosity == 0
     assert call_args.subparser == 'backtesting'
     assert call_args.func is not None
     assert call_args.ticker_interval == '1m'
@@ -165,23 +151,57 @@ def test_parse_args_hyperopt_custom() -> None:
     call_args = Arguments(args, '').get_parsed_arg()
     assert call_args.config == ['test_conf.json']
     assert call_args.epochs == 20
-    assert call_args.loglevel == 0
+    assert call_args.verbosity == 0
     assert call_args.subparser == 'hyperopt'
     assert call_args.spaces == ['buy']
     assert call_args.func is not None
 
 
-def test_testdata_dl_options() -> None:
+def test_download_data_options() -> None:
     args = [
         '--pairs-file', 'file_with_pairs',
-        '--export', 'export/folder',
+        '--datadir', 'datadir/directory',
         '--days', '30',
         '--exchange', 'binance'
     ]
     arguments = Arguments(args, '')
-    arguments.testdata_dl_options()
-    args = arguments.parse_args()
+    arguments._build_args(ARGS_DOWNLOADER)
+    args = arguments._parse_args()
     assert args.pairs_file == 'file_with_pairs'
-    assert args.export == 'export/folder'
+    assert args.datadir == 'datadir/directory'
     assert args.days == 30
     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._build_args(ARGS_PLOT_DATAFRAME)
+    pargs = arguments._parse_args()
+    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 check_int_positive("3") == 3
+    assert check_int_positive("1") == 1
+    assert check_int_positive("100") == 100
+
+    with pytest.raises(argparse.ArgumentTypeError):
+        check_int_positive("-2")
+
+    with pytest.raises(argparse.ArgumentTypeError):
+        check_int_positive("0")
+
+    with pytest.raises(argparse.ArgumentTypeError):
+        check_int_positive("3.5")
+
+    with pytest.raises(argparse.ArgumentTypeError):
+        check_int_positive("DeadBeef")
diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py
index 21547d205..e325a0de2 100644
--- a/freqtrade/tests/test_configuration.py
+++ b/freqtrade/tests/test_configuration.py
@@ -1,62 +1,96 @@
 # pragma pylint: disable=missing-docstring, protected-access, invalid-name
-
 import json
 import logging
+import warnings
 from argparse import Namespace
 from copy import deepcopy
+from pathlib import Path
 from unittest.mock import MagicMock
 
 import pytest
 from jsonschema import Draft4Validator, ValidationError, validate
 
 from freqtrade import OperationalException, constants
-from freqtrade.arguments import Arguments
-from freqtrade.configuration import Configuration, set_loggers
+from freqtrade.configuration import Arguments, Configuration
+from freqtrade.configuration.check_exchange import check_exchange
+from freqtrade.configuration.create_datadir import create_datadir
+from freqtrade.configuration.json_schema import validate_config_schema
+from freqtrade.configuration.load_config import load_config_file
 from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL
+from freqtrade.loggers import _set_loggers
 from freqtrade.state import RunMode
-from freqtrade.tests.conftest import log_has
+from freqtrade.tests.conftest import (log_has, log_has_re,
+                                      patched_configuration_load_config_file)
+
+
+@pytest.fixture(scope="function")
+def all_conf():
+    config_file = Path(__file__).parents[2] / "config_full.json.example"
+    print(config_file)
+    conf = load_config_file(str(config_file))
+    return conf
 
 
 def test_load_config_invalid_pair(default_conf) -> None:
     default_conf['exchange']['pair_whitelist'].append('ETH-BTC')
 
     with pytest.raises(ValidationError, match=r'.*does not match.*'):
-        configuration = Configuration(Namespace())
-        configuration._validate_config_schema(default_conf)
+        validate_config_schema(default_conf)
 
 
 def test_load_config_missing_attributes(default_conf) -> None:
     default_conf.pop('exchange')
 
     with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'):
-        configuration = Configuration(Namespace())
-        configuration._validate_config_schema(default_conf)
+        validate_config_schema(default_conf)
 
 
 def test_load_config_incorrect_stake_amount(default_conf) -> None:
     default_conf['stake_amount'] = 'fake'
 
     with pytest.raises(ValidationError, match=r'.*\'fake\' does not match \'unlimited\'.*'):
-        configuration = Configuration(Namespace())
-        configuration._validate_config_schema(default_conf)
+        validate_config_schema(default_conf)
 
 
 def test_load_config_file(default_conf, mocker, caplog) -> None:
-    file_mock = mocker.patch('freqtrade.configuration.open', mocker.mock_open(
+    file_mock = mocker.patch('freqtrade.configuration.load_config.open', mocker.mock_open(
         read_data=json.dumps(default_conf)
     ))
 
-    configuration = Configuration(Namespace())
-    validated_conf = configuration._load_config_file('somefile')
+    validated_conf = load_config_file('somefile')
     assert file_mock.call_count == 1
     assert validated_conf.items() >= default_conf.items()
 
 
+def test__args_to_config(caplog):
+
+    arg_list = ['--strategy-path', 'TestTest']
+    args = Arguments(arg_list, '').get_parsed_arg()
+    configuration = Configuration(args)
+    config = {}
+    with warnings.catch_warnings(record=True) as w:
+        # No warnings ...
+        configuration._args_to_config(config, argname="strategy_path", logstring="DeadBeef")
+        assert len(w) == 0
+        assert log_has("DeadBeef", caplog.record_tuples)
+        assert config['strategy_path'] == "TestTest"
+
+    configuration = Configuration(args)
+    config = {}
+    with warnings.catch_warnings(record=True) as w:
+        # Deprecation warnings!
+        configuration._args_to_config(config, argname="strategy_path", logstring="DeadBeef",
+                                      deprecated_msg="Going away soon!")
+        assert len(w) == 1
+        assert issubclass(w[-1].category, DeprecationWarning)
+        assert "DEPRECATED: Going away soon!" in str(w[-1].message)
+        assert log_has("DeadBeef", caplog.record_tuples)
+        assert config['strategy_path'] == "TestTest"
+
+
 def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None:
     default_conf['max_open_trades'] = 0
-    mocker.patch('freqtrade.configuration.open', mocker.mock_open(
-        read_data=json.dumps(default_conf)
-    ))
+    patched_configuration_load_config_file(mocker, default_conf)
 
     args = Arguments([], '').get_parsed_arg()
     configuration = Configuration(args)
@@ -78,7 +112,10 @@ def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None:
     config_files = [conf1, conf2]
 
     configsmock = MagicMock(side_effect=config_files)
-    mocker.patch('freqtrade.configuration.Configuration._load_config_file', configsmock)
+    mocker.patch(
+        'freqtrade.configuration.configuration.load_config_file',
+        configsmock
+    )
 
     arg_list = ['-c', 'test_conf.json', '--config', 'test2_conf.json', ]
     args = Arguments(arg_list, '').get_parsed_arg()
@@ -98,9 +135,7 @@ def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None:
 
 def test_load_config_max_open_trades_minus_one(default_conf, mocker, caplog) -> None:
     default_conf['max_open_trades'] = -1
-    mocker.patch('freqtrade.configuration.open', mocker.mock_open(
-        read_data=json.dumps(default_conf)
-    ))
+    patched_configuration_load_config_file(mocker, default_conf)
 
     args = Arguments([], '').get_parsed_arg()
     configuration = Configuration(args)
@@ -115,19 +150,16 @@ def test_load_config_max_open_trades_minus_one(default_conf, mocker, caplog) ->
 
 def test_load_config_file_exception(mocker) -> None:
     mocker.patch(
-        'freqtrade.configuration.open',
+        'freqtrade.configuration.configuration.open',
         MagicMock(side_effect=FileNotFoundError('File not found'))
     )
-    configuration = Configuration(Namespace())
 
     with pytest.raises(OperationalException, match=r'.*Config file "somefile" not found!*'):
-        configuration._load_config_file('somefile')
+        load_config_file('somefile')
 
 
 def test_load_config(default_conf, mocker) -> None:
-    mocker.patch('freqtrade.configuration.open', mocker.mock_open(
-        read_data=json.dumps(default_conf)
-    ))
+    patched_configuration_load_config_file(mocker, default_conf)
 
     args = Arguments([], '').get_parsed_arg()
     configuration = Configuration(args)
@@ -139,11 +171,9 @@ def test_load_config(default_conf, mocker) -> None:
 
 
 def test_load_config_with_params(default_conf, mocker) -> None:
-    mocker.patch('freqtrade.configuration.open', mocker.mock_open(
-        read_data=json.dumps(default_conf)
-    ))
+    patched_configuration_load_config_file(mocker, default_conf)
+
     arglist = [
-        '--dynamic-whitelist', '10',
         '--strategy', 'TestStrategy',
         '--strategy-path', '/some/path',
         '--db-url', 'sqlite:///someurl',
@@ -152,8 +182,6 @@ def test_load_config_with_params(default_conf, mocker) -> None:
     configuration = Configuration(args)
     validated_conf = configuration.load_config()
 
-    assert validated_conf.get('pairlist', {}).get('method') == 'VolumePairList'
-    assert validated_conf.get('pairlist', {}).get('config').get('number_assets') == 10
     assert validated_conf.get('strategy') == 'TestStrategy'
     assert validated_conf.get('strategy_path') == '/some/path'
     assert validated_conf.get('db_url') == 'sqlite:///someurl'
@@ -162,9 +190,7 @@ def test_load_config_with_params(default_conf, mocker) -> None:
     conf = default_conf.copy()
     conf["dry_run"] = False
     conf["db_url"] = "sqlite:///path/to/db.sqlite"
-    mocker.patch('freqtrade.configuration.open', mocker.mock_open(
-        read_data=json.dumps(conf)
-    ))
+    patched_configuration_load_config_file(mocker, conf)
 
     arglist = [
         '--strategy', 'TestStrategy',
@@ -180,9 +206,7 @@ def test_load_config_with_params(default_conf, mocker) -> None:
     conf = default_conf.copy()
     conf["dry_run"] = True
     conf["db_url"] = "sqlite:///path/to/db.sqlite"
-    mocker.patch('freqtrade.configuration.open', mocker.mock_open(
-        read_data=json.dumps(conf)
-    ))
+    patched_configuration_load_config_file(mocker, conf)
 
     arglist = [
         '--strategy', 'TestStrategy',
@@ -198,9 +222,7 @@ def test_load_config_with_params(default_conf, mocker) -> None:
     conf = default_conf.copy()
     conf["dry_run"] = False
     del conf["db_url"]
-    mocker.patch('freqtrade.configuration.open', mocker.mock_open(
-        read_data=json.dumps(conf)
-    ))
+    patched_configuration_load_config_file(mocker, conf)
 
     arglist = [
         '--strategy', 'TestStrategy',
@@ -218,9 +240,7 @@ def test_load_config_with_params(default_conf, mocker) -> None:
     conf = default_conf.copy()
     conf["dry_run"] = True
     conf["db_url"] = DEFAULT_DB_PROD_URL
-    mocker.patch('freqtrade.configuration.open', mocker.mock_open(
-        read_data=json.dumps(conf)
-    ))
+    patched_configuration_load_config_file(mocker, conf)
 
     arglist = [
         '--strategy', 'TestStrategy',
@@ -238,9 +258,7 @@ def test_load_custom_strategy(default_conf, mocker) -> None:
         'strategy': 'CustomStrategy',
         'strategy_path': '/tmp/strategies',
     })
-    mocker.patch('freqtrade.configuration.open', mocker.mock_open(
-        read_data=json.dumps(default_conf)
-    ))
+    patched_configuration_load_config_file(mocker, default_conf)
 
     args = Arguments([], '').get_parsed_arg()
     configuration = Configuration(args)
@@ -251,11 +269,9 @@ def test_load_custom_strategy(default_conf, mocker) -> None:
 
 
 def test_show_info(default_conf, mocker, caplog) -> None:
-    mocker.patch('freqtrade.configuration.open', mocker.mock_open(
-        read_data=json.dumps(default_conf)
-    ))
+    patched_configuration_load_config_file(mocker, default_conf)
+
     arglist = [
-        '--dynamic-whitelist', '10',
         '--strategy', 'TestStrategy',
         '--db-url', 'sqlite:///tmp/testdb',
     ]
@@ -264,21 +280,13 @@ def test_show_info(default_conf, mocker, caplog) -> None:
     configuration = Configuration(args)
     configuration.get_config()
 
-    assert log_has(
-        'Parameter --dynamic-whitelist has been deprecated, '
-        'and will be completely replaced by the whitelist dict in the future. '
-        'For now: using dynamically generated whitelist based on VolumePairList. '
-        '(not applicable with Backtesting and Hyperopt)',
-        caplog.record_tuples
-    )
     assert log_has('Using DB: "sqlite:///tmp/testdb"', caplog.record_tuples)
     assert log_has('Dry run is enabled', caplog.record_tuples)
 
 
 def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
-    mocker.patch('freqtrade.configuration.open', mocker.mock_open(
-        read_data=json.dumps(default_conf)
-    ))
+    patched_configuration_load_config_file(mocker, default_conf)
+
     arglist = [
         '--config', 'config.json',
         '--strategy', 'DefaultStrategy',
@@ -296,7 +304,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
     assert 'pair_whitelist' in config['exchange']
     assert 'datadir' in config
     assert log_has(
-        'Using data folder: {} ...'.format(config['datadir']),
+        'Using data directory: {} ...'.format(config['datadir']),
         caplog.record_tuples
     )
     assert 'ticker_interval' in config
@@ -315,11 +323,13 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
     assert 'export' not in config
 
 
+@pytest.mark.filterwarnings("ignore:DEPRECATED")
 def test_setup_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)
+    patched_configuration_load_config_file(mocker, default_conf)
+    mocker.patch(
+        'freqtrade.configuration.configuration.create_datadir',
+        lambda c, x: x
+    )
 
     arglist = [
         '--config', 'config.json',
@@ -346,15 +356,12 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
     assert 'pair_whitelist' in config['exchange']
     assert 'datadir' in config
     assert log_has(
-        'Using data folder: {} ...'.format(config['datadir']),
+        'Using data directory: {} ...'.format(config['datadir']),
         caplog.record_tuples
     )
     assert 'ticker_interval' in config
-    assert log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
-    assert log_has(
-        'Using ticker_interval: 1m ...',
-        caplog.record_tuples
-    )
+    assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
+                   caplog.record_tuples)
 
     assert 'live' in config
     assert log_has('Parameter -l/--live detected ...', caplog.record_tuples)
@@ -385,9 +392,7 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non
     """
     Test setup_configuration() function
     """
-    mocker.patch('freqtrade.configuration.open', mocker.mock_open(
-        read_data=json.dumps(default_conf)
-    ))
+    patched_configuration_load_config_file(mocker, default_conf)
 
     arglist = [
         '--config', 'config.json',
@@ -411,15 +416,12 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non
     assert 'pair_whitelist' in config['exchange']
     assert 'datadir' in config
     assert log_has(
-        'Using data folder: {} ...'.format(config['datadir']),
+        'Using data directory: {} ...'.format(config['datadir']),
         caplog.record_tuples
     )
     assert 'ticker_interval' in config
-    assert log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
-    assert log_has(
-        'Using ticker_interval: 1m ...',
-        caplog.record_tuples
-    )
+    assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
+                   caplog.record_tuples)
 
     assert 'strategy_list' in config
     assert log_has('Using strategy list of 2 Strategies', caplog.record_tuples)
@@ -438,9 +440,8 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non
 
 
 def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
-    mocker.patch('freqtrade.configuration.open', mocker.mock_open(
-        read_data=json.dumps(default_conf)
-    ))
+    patched_configuration_load_config_file(mocker, default_conf)
+
     arglist = [
         'hyperopt',
         '--epochs', '10',
@@ -453,8 +454,8 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
 
     assert 'epochs' in config
     assert int(config['epochs']) == 10
-    assert log_has('Parameter --epochs detected ...', caplog.record_tuples)
-    assert log_has('Will run Hyperopt with for 10 epochs ...', caplog.record_tuples)
+    assert log_has('Parameter --epochs detected ... Will run Hyperopt with for 10 epochs ...',
+                   caplog.record_tuples)
 
     assert 'spaces' in config
     assert config['spaces'] == ['all']
@@ -464,41 +465,60 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
 
 
 def test_check_exchange(default_conf, caplog) -> None:
-    configuration = Configuration(Namespace())
-
-    # Test a valid exchange
+    # Test an officially supported by Freqtrade team exchange
     default_conf.get('exchange').update({'name': 'BITTREX'})
-    assert configuration.check_exchange(default_conf)
+    assert 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'})
-    assert configuration.check_exchange(default_conf)
+    assert 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 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 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 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'})
-    configuration.config = default_conf
 
     with pytest.raises(
         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)
-
-    # Test ccxt_rate_limit depreciation
-    default_conf.get('exchange').update({'name': 'binance'})
-    default_conf['exchange']['ccxt_rate_limit'] = True
-    configuration.check_exchange(default_conf)
-    assert log_has("`ccxt_rate_limit` has been deprecated in favor of "
-                   "`ccxt_config` and `ccxt_async_config` and will be removed "
-                   "in a future version.",
-                   caplog.record_tuples)
+        check_exchange(default_conf)
 
 
 def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None:
-    mocker.patch('freqtrade.configuration.open', mocker.mock_open(
-        read_data=json.dumps(default_conf)))
+    patched_configuration_load_config_file(mocker, default_conf)
+
     # Prevent setting loggers
-    mocker.patch('freqtrade.configuration.set_loggers', MagicMock)
+    mocker.patch('freqtrade.loggers._set_loggers', MagicMock)
     arglist = ['-vvv']
     args = Arguments(arglist, '').get_parsed_arg()
 
@@ -520,7 +540,7 @@ def test_set_loggers() -> None:
     previous_value2 = logging.getLogger('ccxt.base.exchange').level
     previous_value3 = logging.getLogger('telegram').level
 
-    set_loggers()
+    _set_loggers()
 
     value1 = logging.getLogger('requests').level
     assert previous_value1 is not value1
@@ -534,24 +554,38 @@ def test_set_loggers() -> None:
     assert previous_value3 is not value3
     assert value3 is logging.INFO
 
-    set_loggers(log_level=2)
+    _set_loggers(verbosity=2)
 
     assert logging.getLogger('requests').level is logging.DEBUG
     assert logging.getLogger('ccxt.base.exchange').level is logging.INFO
     assert logging.getLogger('telegram').level is logging.INFO
 
-    set_loggers(log_level=3)
+    _set_loggers(verbosity=3)
 
     assert logging.getLogger('requests').level is logging.DEBUG
     assert logging.getLogger('ccxt.base.exchange').level is logging.DEBUG
     assert logging.getLogger('telegram').level is logging.INFO
 
 
+def test_set_logfile(default_conf, mocker):
+    patched_configuration_load_config_file(mocker, default_conf)
+
+    arglist = [
+        '--logfile', 'test_file.log',
+    ]
+    args = Arguments(arglist, '').get_parsed_arg()
+    configuration = Configuration(args)
+    validated_conf = configuration.load_config()
+
+    assert validated_conf['logfile'] == "test_file.log"
+    f = Path("test_file.log")
+    assert f.is_file()
+    f.unlink()
+
+
 def test_load_config_warn_forcebuy(default_conf, mocker, caplog) -> None:
     default_conf['forcebuy_enable'] = True
-    mocker.patch('freqtrade.configuration.open', mocker.mock_open(
-        read_data=json.dumps(default_conf)
-    ))
+    patched_configuration_load_config_file(mocker, default_conf)
 
     args = Arguments([], '').get_parsed_arg()
     configuration = Configuration(args)
@@ -565,13 +599,12 @@ def test_validate_default_conf(default_conf) -> None:
     validate(default_conf, constants.CONF_SCHEMA, Draft4Validator)
 
 
-def test__create_datadir(mocker, default_conf, caplog) -> None:
-    mocker.patch('os.path.isdir', MagicMock(return_value=False))
-    md = MagicMock()
-    mocker.patch('os.makedirs', md)
-    cfg = Configuration(Namespace())
-    cfg._create_datadir(default_conf, '/foo/bar')
-    assert md.call_args[0][0] == "/foo/bar"
+def test_create_datadir(mocker, default_conf, caplog) -> None:
+    mocker.patch.object(Path, "is_dir", MagicMock(return_value=False))
+    md = mocker.patch.object(Path, 'mkdir', MagicMock())
+
+    create_datadir(default_conf, '/foo/bar')
+    assert md.call_args[1]['parents'] is True
     assert log_has('Created data directory: /foo/bar', caplog.record_tuples)
 
 
@@ -599,3 +632,56 @@ def test_validate_tsl(default_conf):
     default_conf['trailing_stop_positive_offset'] = 0.015
     Configuration(Namespace())
     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'):
+        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'):
+        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]
+
+    validate_config_schema(all_conf)
+    assert subkey in all_conf[key]
+    assert all_conf[key][subkey] == keys[2]
diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py
index e4f0415f7..1a4c5159c 100644
--- a/freqtrade/tests/test_freqtradebot.py
+++ b/freqtrade/tests/test_freqtradebot.py
@@ -2,7 +2,6 @@
 # pragma pylint: disable=protected-access, too-many-lines, invalid-name, too-many-arguments
 
 import logging
-import re
 import time
 from copy import deepcopy
 from unittest.mock import MagicMock, PropertyMock
@@ -11,39 +10,19 @@ import arrow
 import pytest
 import requests
 
-from freqtrade import (DependencyException, OperationalException,
-                       TemporaryError, constants)
+from freqtrade import (DependencyException, InvalidOrderException,
+                       OperationalException, TemporaryError, constants)
+from freqtrade.data.dataprovider import DataProvider
 from freqtrade.freqtradebot import FreqtradeBot
 from freqtrade.persistence import Trade
 from freqtrade.rpc import RPCMessageType
 from freqtrade.state import State
-from freqtrade.strategy.interface import SellType, SellCheckTuple
-from freqtrade.tests.conftest import log_has, log_has_re, patch_exchange, patch_edge, patch_wallet
-
-
-# Functions for recurrent object patching
-def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
-    """
-    This function patch _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)
-
-    return FreqtradeBot(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
+from freqtrade.strategy.interface import SellCheckTuple, SellType
+from freqtrade.tests.conftest import (get_patched_freqtradebot,
+                                      get_patched_worker, log_has, log_has_re,
+                                      patch_edge, patch_exchange,
+                                      patch_get_signal, patch_wallet)
+from freqtrade.worker import Worker
 
 
 def patch_RPCManager(mocker) -> MagicMock:
@@ -59,7 +38,7 @@ def patch_RPCManager(mocker) -> MagicMock:
 
 # Unit tests
 
-def test_freqtradebot(mocker, default_conf, markets) -> None:
+def test_freqtradebot_state(mocker, default_conf, markets) -> None:
     mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
     freqtrade = get_patched_freqtradebot(mocker, default_conf)
     assert freqtrade.state is State.RUNNING
@@ -69,6 +48,16 @@ def test_freqtradebot(mocker, default_conf, markets) -> None:
     assert freqtrade.state is State.STOPPED
 
 
+def test_worker_state(mocker, default_conf, markets) -> None:
+    mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
+    worker = get_patched_worker(mocker, default_conf)
+    assert worker.state is State.RUNNING
+
+    default_conf.pop('initial_state')
+    worker = Worker(args=None, config=default_conf)
+    assert worker.state is State.STOPPED
+
+
 def test_cleanup(mocker, default_conf, caplog) -> None:
     mock_cleanup = MagicMock()
     mocker.patch('freqtrade.persistence.cleanup', mock_cleanup)
@@ -80,24 +69,29 @@ def test_cleanup(mocker, default_conf, caplog) -> None:
 
 def test_worker_running(mocker, default_conf, caplog) -> None:
     mock_throttle = MagicMock()
-    mocker.patch('freqtrade.freqtradebot.FreqtradeBot._throttle', mock_throttle)
+    mocker.patch('freqtrade.worker.Worker._throttle', mock_throttle)
+    mocker.patch('freqtrade.persistence.Trade.stoploss_reinitialization', MagicMock())
 
-    freqtrade = get_patched_freqtradebot(mocker, default_conf)
+    worker = get_patched_worker(mocker, default_conf)
 
-    state = freqtrade.worker(old_state=None)
+    state = worker._worker(old_state=None)
     assert state is State.RUNNING
     assert log_has('Changing state to: RUNNING', caplog.record_tuples)
     assert mock_throttle.call_count == 1
+    # Check strategy is loaded, and received a dataprovider object
+    assert worker.freqtrade.strategy
+    assert worker.freqtrade.strategy.dp
+    assert isinstance(worker.freqtrade.strategy.dp, DataProvider)
 
 
 def test_worker_stopped(mocker, default_conf, caplog) -> None:
     mock_throttle = MagicMock()
-    mocker.patch('freqtrade.freqtradebot.FreqtradeBot._throttle', mock_throttle)
+    mocker.patch('freqtrade.worker.Worker._throttle', mock_throttle)
     mock_sleep = mocker.patch('time.sleep', return_value=None)
 
-    freqtrade = get_patched_freqtradebot(mocker, default_conf)
-    freqtrade.state = State.STOPPED
-    state = freqtrade.worker(old_state=State.RUNNING)
+    worker = get_patched_worker(mocker, default_conf)
+    worker.state = State.STOPPED
+    state = worker._worker(old_state=State.RUNNING)
     assert state is State.STOPPED
     assert log_has('Changing state to: STOPPED', caplog.record_tuples)
     assert mock_throttle.call_count == 0
@@ -109,17 +103,17 @@ def test_throttle(mocker, default_conf, caplog) -> None:
         return 42
 
     caplog.set_level(logging.DEBUG)
-    freqtrade = get_patched_freqtradebot(mocker, default_conf)
+    worker = get_patched_worker(mocker, default_conf)
 
     start = time.time()
-    result = freqtrade._throttle(throttled_func, min_secs=0.1)
+    result = worker._throttle(throttled_func, min_secs=0.1)
     end = time.time()
 
     assert result == 42
     assert end - start > 0.1
     assert log_has('Throttling throttled_func for 0.10 seconds', caplog.record_tuples)
 
-    result = freqtrade._throttle(throttled_func, min_secs=-1)
+    result = worker._throttle(throttled_func, min_secs=-1)
     assert result == 42
 
 
@@ -127,12 +121,12 @@ def test_throttle_with_assets(mocker, default_conf) -> None:
     def throttled_func(nb_assets=-1):
         return nb_assets
 
-    freqtrade = get_patched_freqtradebot(mocker, default_conf)
+    worker = get_patched_worker(mocker, default_conf)
 
-    result = freqtrade._throttle(throttled_func, min_secs=0.1, nb_assets=666)
+    result = worker._throttle(throttled_func, min_secs=0.1, nb_assets=666)
     assert result == 666
 
-    result = freqtrade._throttle(throttled_func, min_secs=0.1)
+    result = worker._throttle(throttled_func, min_secs=0.1)
     assert result == -1
 
 
@@ -218,7 +212,7 @@ def test_edge_called_in_process(mocker, edge_conf) -> None:
     freqtrade = FreqtradeBot(edge_conf)
     freqtrade.pairlists._validate_whitelist = _refresh_whitelist
     patch_get_signal(freqtrade)
-    freqtrade._process()
+    freqtrade.process()
     assert freqtrade.active_pair_whitelist == ['NEO/BTC', 'LTC/BTC']
 
 
@@ -545,8 +539,7 @@ def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_ord
     freqtrade = FreqtradeBot(default_conf)
     patch_get_signal(freqtrade)
 
-    result = freqtrade.create_trade()
-    assert result is False
+    assert not freqtrade.create_trade()
 
 
 def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order,
@@ -567,11 +560,12 @@ def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order,
     freqtrade = FreqtradeBot(default_conf)
     patch_get_signal(freqtrade)
 
-    assert freqtrade.create_trade() is False
+    assert not freqtrade.create_trade()
     assert freqtrade._get_trade_stake_amount('ETH/BTC') is None
 
 
-def test_create_trade_no_pairs(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None:
+def test_create_trade_no_pairs_let(default_conf, ticker, limit_buy_order, fee,
+                                   markets, mocker, caplog) -> None:
     patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
@@ -583,18 +577,17 @@ def test_create_trade_no_pairs(default_conf, ticker, limit_buy_order, fee, marke
     )
 
     default_conf['exchange']['pair_whitelist'] = ["ETH/BTC"]
-    default_conf['exchange']['pair_blacklist'] = ["ETH/BTC"]
     freqtrade = FreqtradeBot(default_conf)
     patch_get_signal(freqtrade)
 
-    freqtrade.create_trade()
-
-    with pytest.raises(DependencyException, match=r'.*No currency pairs in whitelist.*'):
-        freqtrade.create_trade()
+    assert freqtrade.create_trade()
+    assert not freqtrade.create_trade()
+    assert log_has("No currency pair in whitelist, but checking to sell open trades.",
+                   caplog.record_tuples)
 
 
-def test_create_trade_no_pairs_after_blacklist(default_conf, ticker,
-                                               limit_buy_order, fee, markets, mocker) -> None:
+def test_create_trade_no_pairs_in_whitelist(default_conf, ticker, limit_buy_order, fee,
+                                            markets, mocker, caplog) -> None:
     patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
@@ -604,15 +597,12 @@ def test_create_trade_no_pairs_after_blacklist(default_conf, ticker,
         get_fee=fee,
         markets=PropertyMock(return_value=markets)
     )
-    default_conf['exchange']['pair_whitelist'] = ["ETH/BTC"]
-    default_conf['exchange']['pair_blacklist'] = ["ETH/BTC"]
+    default_conf['exchange']['pair_whitelist'] = []
     freqtrade = FreqtradeBot(default_conf)
     patch_get_signal(freqtrade)
 
-    freqtrade.create_trade()
-
-    with pytest.raises(DependencyException, match=r'.*No currency pairs in whitelist.*'):
-        freqtrade.create_trade()
+    assert not freqtrade.create_trade()
+    assert log_has("Whitelist is empty.", caplog.record_tuples)
 
 
 def test_create_trade_no_signal(default_conf, fee, mocker) -> None:
@@ -652,7 +642,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order,
     trades = Trade.query.filter(Trade.is_open.is_(True)).all()
     assert not trades
 
-    result = freqtrade._process()
+    result = freqtrade.process()
     assert result is True
 
     trades = Trade.query.filter(Trade.is_open.is_(True)).all()
@@ -683,10 +673,10 @@ def test_process_exchange_failures(default_conf, ticker, markets, mocker) -> Non
     )
     sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None)
 
-    freqtrade = FreqtradeBot(default_conf)
-    patch_get_signal(freqtrade)
+    worker = Worker(args=None, config=default_conf)
+    patch_get_signal(worker.freqtrade)
 
-    result = freqtrade._process()
+    result = worker._process()
     assert result is False
     assert sleep_mock.has_calls()
 
@@ -700,14 +690,14 @@ def test_process_operational_exception(default_conf, ticker, markets, mocker) ->
         markets=PropertyMock(return_value=markets),
         buy=MagicMock(side_effect=OperationalException)
     )
-    freqtrade = FreqtradeBot(default_conf)
-    patch_get_signal(freqtrade)
+    worker = Worker(args=None, config=default_conf)
+    patch_get_signal(worker.freqtrade)
 
-    assert freqtrade.state == State.RUNNING
+    assert worker.state == State.RUNNING
 
-    result = freqtrade._process()
+    result = worker._process()
     assert result is False
-    assert freqtrade.state == State.STOPPED
+    assert worker.state == State.STOPPED
     assert 'OperationalException' in msg_mock.call_args_list[-1][0][0]['status']
 
 
@@ -728,18 +718,18 @@ def test_process_trade_handling(
 
     trades = Trade.query.filter(Trade.is_open.is_(True)).all()
     assert not trades
-    result = freqtrade._process()
+    result = freqtrade.process()
     assert result is True
     trades = Trade.query.filter(Trade.is_open.is_(True)).all()
     assert len(trades) == 1
 
-    result = freqtrade._process()
+    result = freqtrade.process()
     assert result is False
 
 
 def test_process_trade_no_whitelist_pair(
         default_conf, ticker, limit_buy_order, markets, fee, mocker) -> None:
-    """ Test _process with trade not in pair list """
+    """ Test process with trade not in pair list """
     patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
@@ -776,7 +766,7 @@ def test_process_trade_no_whitelist_pair(
     ))
 
     assert pair not in freqtrade.active_pair_whitelist
-    result = freqtrade._process()
+    result = freqtrade.process()
     assert pair in freqtrade.active_pair_whitelist
     # Make sure each pair is only in the list once
     assert len(freqtrade.active_pair_whitelist) == len(set(freqtrade.active_pair_whitelist))
@@ -806,7 +796,7 @@ def test_process_informative_pairs_added(default_conf, ticker, markets, mocker)
     freqtrade.strategy.informative_pairs = inf_pairs
     # patch_get_signal(freqtrade)
 
-    freqtrade._process()
+    freqtrade.process()
     assert inf_pairs.call_count == 1
     assert refresh_mock.call_count == 1
     assert ("BTC/ETH", "1m") in refresh_mock.call_args[0][0]
@@ -1003,7 +993,21 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
     assert freqtrade.handle_stoploss_on_exchange(trade) is False
     assert trade.stoploss_order_id == 100
 
-    # Third case: when stoploss is set and it is hit
+    # Third case: when stoploss was set but it was canceled for some reason
+    # should set a stoploss immediately and return False
+    trade.is_open = True
+    trade.open_order_id = None
+    trade.stoploss_order_id = 100
+
+    canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'})
+    mocker.patch('freqtrade.exchange.Exchange.get_order', canceled_stoploss_order)
+    stoploss_limit.reset_mock()
+
+    assert freqtrade.handle_stoploss_on_exchange(trade) is False
+    assert stoploss_limit.call_count == 1
+    assert trade.stoploss_order_id == "13434334"
+
+    # Fourth case: when stoploss is set and it is hit
     # should unset stoploss_order_id and return true
     # as a trade actually happened
     freqtrade.create_trade()
@@ -1025,6 +1029,22 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
     assert trade.stoploss_order_id is None
     assert trade.is_open is False
 
+    mocker.patch(
+        'freqtrade.exchange.Exchange.stoploss_limit',
+        side_effect=DependencyException()
+    )
+    freqtrade.handle_stoploss_on_exchange(trade)
+    assert log_has('Unable to place a stoploss order on exchange: ', caplog.record_tuples)
+
+    # Fifth case: get_order returns InvalidOrder
+    # It should try to add stoploss order
+    trade.stoploss_order_id = 100
+    stoploss_limit.reset_mock()
+    mocker.patch('freqtrade.exchange.Exchange.get_order', side_effect=InvalidOrderException())
+    mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_limit)
+    freqtrade.handle_stoploss_on_exchange(trade)
+    assert stoploss_limit.call_count == 1
+
 
 def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
                                               markets, limit_buy_order, limit_sell_order) -> None:
@@ -1121,6 +1141,77 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
                                                 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,
                                               markets, limit_buy_order, limit_sell_order) -> None:
 
@@ -1275,14 +1366,6 @@ def test_process_maybe_execute_sell(mocker, default_conf, limit_buy_order, caplo
     # test amount modified by fee-logic
     assert not freqtrade.process_maybe_execute_sell(trade)
 
-    trade.is_open = True
-    trade.open_order_id = None
-    # Assert we call handle_trade() if trade is feasible for execution
-    assert freqtrade.process_maybe_execute_sell(trade)
-
-    regexp = re.compile('Found open order for.*')
-    assert filter(regexp.match, caplog.record_tuples)
-
 
 def test_process_maybe_execute_sell_exception(mocker, default_conf,
                                               limit_buy_order, caplog) -> None:
@@ -1293,21 +1376,133 @@ def test_process_maybe_execute_sell_exception(mocker, default_conf,
     trade.open_order_id = '123'
     trade.open_fee = 0.001
 
+    # Test raise of DependencyException exception
+    mocker.patch(
+        'freqtrade.freqtradebot.FreqtradeBot.update_trade_state',
+        side_effect=DependencyException()
+    )
+    freqtrade.process_maybe_execute_sell(trade)
+    assert log_has('Unable to sell trade: ', caplog.record_tuples)
+
+
+def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> None:
+    freqtrade = get_patched_freqtradebot(mocker, default_conf)
+
+    mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True))
+    mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order)
+    mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[])
+    mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount',
+                 return_value=limit_buy_order['amount'])
+
+    trade = Trade()
+    # Mock session away
+    Trade.session = MagicMock()
+    trade.open_order_id = '123'
+    trade.open_fee = 0.001
+    freqtrade.update_trade_state(trade)
+    # Test amount not modified by fee-logic
+    assert not log_has_re(r'Applying fee to .*', caplog.record_tuples)
+    assert trade.open_order_id is None
+    assert trade.amount == limit_buy_order['amount']
+
+    trade.open_order_id = '123'
+    mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=90.81)
+    assert trade.amount != 90.81
+    # test amount modified by fee-logic
+    freqtrade.update_trade_state(trade)
+    assert trade.amount == 90.81
+    assert trade.open_order_id is None
+
+    trade.is_open = True
+    trade.open_order_id = None
+    # Assert we call handle_trade() if trade is feasible for execution
+    freqtrade.update_trade_state(trade)
+
+    assert log_has_re('Found open order for.*', caplog.record_tuples)
+
+
+def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_buy_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))
+    patch_exchange(mocker)
+    Trade.session = MagicMock()
+    amount = sum(x['amount'] for x in trades_for_order)
+    freqtrade = get_patched_freqtradebot(mocker, default_conf)
+    trade = Trade(
+        pair='LTC/ETH',
+        amount=amount,
+        exchange='binance',
+        open_rate=0.245441,
+        open_order_id="123456",
+        is_open=True,
+    )
+    freqtrade.update_trade_state(trade, limit_buy_order)
+    assert trade.amount != amount
+    assert trade.amount == limit_buy_order['amount']
+
+
+def test_update_trade_state_exception(mocker, default_conf,
+                                      limit_buy_order, caplog) -> None:
+    freqtrade = get_patched_freqtradebot(mocker, default_conf)
+    mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order)
+
+    trade = MagicMock()
+    trade.open_order_id = '123'
+    trade.open_fee = 0.001
+
     # Test raise of OperationalException exception
     mocker.patch(
         'freqtrade.freqtradebot.FreqtradeBot.get_real_amount',
         side_effect=OperationalException()
     )
-    freqtrade.process_maybe_execute_sell(trade)
+    freqtrade.update_trade_state(trade)
     assert log_has('Could not update trade amount: ', caplog.record_tuples)
 
-    # Test raise of DependencyException exception
-    mocker.patch(
-        'freqtrade.freqtradebot.FreqtradeBot.get_real_amount',
-        side_effect=DependencyException()
+
+def test_update_trade_state_orderexception(mocker, default_conf, caplog) -> None:
+    freqtrade = get_patched_freqtradebot(mocker, default_conf)
+    mocker.patch('freqtrade.exchange.Exchange.get_order',
+                 MagicMock(side_effect=InvalidOrderException))
+
+    trade = MagicMock()
+    trade.open_order_id = '123'
+    trade.open_fee = 0.001
+
+    # Test raise of OperationalException exception
+    grm_mock = mocker.patch("freqtrade.freqtradebot.FreqtradeBot.get_real_amount", MagicMock())
+    freqtrade.update_trade_state(trade)
+    assert grm_mock.call_count == 0
+    assert log_has(f'Unable to fetch order {trade.open_order_id}: ', 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.process_maybe_execute_sell(trade)
-    assert log_has('Unable to sell trade: ', caplog.record_tuples)
+    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,
@@ -1760,14 +1955,11 @@ def test_check_handle_timedout_exception(default_conf, ticker, mocker, caplog) -
     )
 
     Trade.session.add(trade_buy)
-    regexp = re.compile(
-        'Cannot query order for Trade(id=1, pair=ETH/BTC, amount=90.99181073, '
-        'open_rate=0.00001099, open_since=10 hours ago) due to Traceback (most '
-        'recent call last):\n.*'
-    )
 
     freqtrade.check_handle_timedout()
-    assert filter(regexp.match, caplog.record_tuples)
+    assert log_has_re(r'Cannot query order for Trade\(id=1, pair=ETH/BTC, amount=90.99181073, '
+                      r'open_rate=0.00001099, open_since=10 hours ago\) due to Traceback \(most '
+                      r'recent call last\):\n.*', caplog.record_tuples)
 
 
 def test_handle_timedout_limit_buy(mocker, default_conf) -> None:
@@ -1850,6 +2042,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, moc
         'gain': 'profit',
         'limit': 1.172e-05,
         'amount': 90.99181073703367,
+        'order_type': 'limit',
         'open_rate': 1.099e-05,
         'current_rate': 1.172e-05,
         'profit_amount': 6.126e-05,
@@ -1896,6 +2089,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets,
         'gain': 'loss',
         'limit': 1.044e-05,
         'amount': 90.99181073703367,
+        'order_type': 'limit',
         'open_rate': 1.099e-05,
         'current_rate': 1.044e-05,
         'profit_amount': -5.492e-05,
@@ -1950,6 +2144,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe
         'gain': 'loss',
         'limit': 1.08801e-05,
         'amount': 90.99181073703367,
+        'order_type': 'limit',
         'open_rate': 1.099e-05,
         'current_rate': 1.044e-05,
         'profit_amount': -1.498e-05,
@@ -1961,6 +2156,36 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe
     } == 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,
                                                 ticker, fee, ticker_sell_up,
                                                 markets, mocker) -> None:
@@ -2121,6 +2346,7 @@ def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee,
         'gain': 'profit',
         'limit': 1.172e-05,
         'amount': 90.99181073703367,
+        'order_type': 'limit',
         'open_rate': 1.099e-05,
         'current_rate': 1.172e-05,
         'profit_amount': 6.126e-05,
@@ -2168,6 +2394,7 @@ def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee,
         'gain': 'loss',
         'limit': 1.044e-05,
         'amount': 90.99181073703367,
+        'order_type': 'limit',
         'open_rate': 1.099e-05,
         'current_rate': 1.044e-05,
         'profit_amount': -5.492e-05,
@@ -2259,9 +2486,8 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, market
     }
     freqtrade = FreqtradeBot(default_conf)
     patch_get_signal(freqtrade)
-    freqtrade.strategy.stop_loss_reached = \
-        lambda current_rate, trade, current_time, force_stoploss, current_profit: SellCheckTuple(
-            sell_flag=False, sell_type=SellType.NONE)
+    freqtrade.strategy.stop_loss_reached = MagicMock(return_value=SellCheckTuple(
+            sell_flag=False, sell_type=SellType.NONE))
     freqtrade.create_trade()
 
     trade = Trade.query.first()
@@ -2342,9 +2568,9 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, markets, caplog,
     mocker.patch.multiple(
         'freqtrade.exchange.Exchange',
         get_ticker=MagicMock(return_value={
-            'bid': 0.00000102,
-            'ask': 0.00000103,
-            'last': 0.00000102
+            'bid': 0.00001099,
+            'ask': 0.00001099,
+            'last': 0.00001099
         }),
         buy=MagicMock(return_value={'id': limit_buy_order['id']}),
         get_fee=fee,
@@ -2356,15 +2582,33 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, markets, caplog,
     freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
 
     freqtrade.create_trade()
-
     trade = Trade.query.first()
-    trade.update(limit_buy_order)
-    trade.max_rate = trade.open_rate * 1.003
+    assert freqtrade.handle_trade(trade) is False
+
+    # 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)
     # Sell as trailing-stop is reached
     assert freqtrade.handle_trade(trade) is True
     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)
     assert trade.sell_reason == SellType.TRAILING_STOP_LOSS.value
 
@@ -2407,8 +2651,7 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, markets
                  }))
     # stop-loss not reached, adjusted stoploss
     assert freqtrade.handle_trade(trade) is False
-    assert log_has(f'using positive stop loss mode: 0.01 with offset 0 '
-                   f'since we have profit 0.2666%',
+    assert log_has(f'using positive stop loss: 0.01 offset: 0 profit: 0.2666%',
                    caplog.record_tuples)
     assert log_has(f'adjusted stop loss', caplog.record_tuples)
     assert trade.stop_loss == 0.0000138501
@@ -2467,8 +2710,7 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee,
                  }))
     # stop-loss not reached, adjusted stoploss
     assert freqtrade.handle_trade(trade) is False
-    assert log_has(f'using positive stop loss mode: 0.01 with offset 0.011 '
-                   f'since we have profit 0.2666%',
+    assert log_has(f'using positive stop loss: 0.01 offset: 0.011 profit: 0.2666%',
                    caplog.record_tuples)
     assert log_has(f'adjusted stop loss', caplog.record_tuples)
     assert trade.stop_loss == 0.0000138501
@@ -2547,8 +2789,7 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, fee,
                  }))
 
     assert freqtrade.handle_trade(trade) is False
-    assert log_has(f'using positive stop loss mode: 0.05 with offset 0.055 '
-                   f'since we have profit 0.1218%',
+    assert log_has(f'using positive stop loss: 0.05 offset: 0.055 profit: 0.1218%',
                    caplog.record_tuples)
     assert log_has(f'adjusted stop loss', caplog.record_tuples)
     assert trade.stop_loss == 0.0000117705
@@ -2656,6 +2897,30 @@ def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, mo
     assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
 
 
+def test_get_real_amount_no_currency_in_fee(default_conf, trades_for_order, buy_order_fee, mocker):
+
+    limit_buy_order = deepcopy(buy_order_fee)
+    limit_buy_order['fee'] = {'cost': 0.004, 'currency': None}
+    trades_for_order[0]['fee']['currency'] = None
+
+    patch_RPCManager(mocker)
+    patch_exchange(mocker)
+    mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
+    amount = sum(x['amount'] for x in trades_for_order)
+    trade = Trade(
+        pair='LTC/ETH',
+        amount=amount,
+        exchange='binance',
+        open_rate=0.245441,
+        open_order_id="123456"
+    )
+    freqtrade = FreqtradeBot(default_conf)
+    patch_get_signal(freqtrade)
+
+    # Amount does not change
+    assert freqtrade.get_real_amount(trade, limit_buy_order) == amount
+
+
 def test_get_real_amount_BNB(default_conf, trades_for_order, buy_order_fee, mocker):
     trades_for_order[0]['fee']['currency'] = 'BNB'
     trades_for_order[0]['fee']['cost'] = 0.00094518
@@ -2987,10 +3252,27 @@ def test_get_sell_rate(default_conf, mocker, ticker, order_book_l2) -> None:
     assert rate == 0.043936
 
 
-def test_startup_messages(default_conf, mocker):
+def test_startup_state(default_conf, mocker):
     default_conf['pairlist'] = {'method': 'VolumePairList',
                                 'config': {'number_assets': 20}
                                 }
     mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
-    freqtrade = get_patched_freqtradebot(mocker, default_conf)
-    assert freqtrade.state is State.RUNNING
+    worker = get_patched_worker(mocker, default_conf)
+    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
diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py
index 51c95a4a9..bcaad4d7d 100644
--- a/freqtrade/tests/test_main.py
+++ b/freqtrade/tests/test_main.py
@@ -6,11 +6,13 @@ from unittest.mock import MagicMock
 import pytest
 
 from freqtrade import OperationalException
-from freqtrade.arguments import Arguments
+from freqtrade.configuration import Arguments
 from freqtrade.freqtradebot import FreqtradeBot
-from freqtrade.main import main, reconfigure
+from freqtrade.main import main
 from freqtrade.state import State
-from freqtrade.tests.conftest import log_has, patch_exchange
+from freqtrade.tests.conftest import (log_has, patch_exchange,
+                                      patched_configuration_load_config_file)
+from freqtrade.worker import Worker
 
 
 def test_parse_args_backtesting(mocker) -> None:
@@ -18,42 +20,40 @@ def test_parse_args_backtesting(mocker) -> None:
     Test that main() can start backtesting and also ensure we can pass some specific arguments
     further argument parsing is done in test_arguments.py
     """
-    backtesting_mock = mocker.patch('freqtrade.optimize.backtesting.start', MagicMock())
-    main(['backtesting'])
+    backtesting_mock = mocker.patch('freqtrade.optimize.start_backtesting', MagicMock())
+    # it's sys.exit(0) at the end of backtesting
+    with pytest.raises(SystemExit):
+        main(['backtesting'])
     assert backtesting_mock.call_count == 1
     call_args = backtesting_mock.call_args[0][0]
     assert call_args.config == ['config.json']
     assert call_args.live is False
-    assert call_args.loglevel == 0
+    assert call_args.verbosity == 0
     assert call_args.subparser == 'backtesting'
     assert call_args.func is not None
     assert call_args.ticker_interval is None
 
 
 def test_main_start_hyperopt(mocker) -> None:
-    hyperopt_mock = mocker.patch('freqtrade.optimize.hyperopt.start', MagicMock())
-    main(['hyperopt'])
+    hyperopt_mock = mocker.patch('freqtrade.optimize.start_hyperopt', MagicMock())
+    # it's sys.exit(0) at the end of hyperopt
+    with pytest.raises(SystemExit):
+        main(['hyperopt'])
     assert hyperopt_mock.call_count == 1
     call_args = hyperopt_mock.call_args[0][0]
     assert call_args.config == ['config.json']
-    assert call_args.loglevel == 0
+    assert call_args.verbosity == 0
     assert call_args.subparser == 'hyperopt'
     assert call_args.func is not None
 
 
 def test_main_fatal_exception(mocker, default_conf, caplog) -> None:
     patch_exchange(mocker)
-    mocker.patch.multiple(
-        'freqtrade.freqtradebot.FreqtradeBot',
-        _init_modules=MagicMock(),
-        worker=MagicMock(side_effect=Exception),
-        cleanup=MagicMock(),
-    )
-    mocker.patch(
-        'freqtrade.configuration.Configuration._load_config_file',
-        lambda *args, **kwargs: default_conf
-    )
+    mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock())
+    mocker.patch('freqtrade.worker.Worker._worker', MagicMock(side_effect=Exception))
+    patched_configuration_load_config_file(mocker, default_conf)
     mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
+    mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
 
     args = ['-c', 'config.json.example']
 
@@ -66,17 +66,11 @@ def test_main_fatal_exception(mocker, default_conf, caplog) -> None:
 
 def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None:
     patch_exchange(mocker)
-    mocker.patch.multiple(
-        'freqtrade.freqtradebot.FreqtradeBot',
-        _init_modules=MagicMock(),
-        worker=MagicMock(side_effect=KeyboardInterrupt),
-        cleanup=MagicMock(),
-    )
-    mocker.patch(
-        'freqtrade.configuration.Configuration._load_config_file',
-        lambda *args, **kwargs: default_conf
-    )
+    mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock())
+    mocker.patch('freqtrade.worker.Worker._worker', MagicMock(side_effect=KeyboardInterrupt))
+    patched_configuration_load_config_file(mocker, default_conf)
     mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
+    mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
 
     args = ['-c', 'config.json.example']
 
@@ -89,17 +83,14 @@ def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None:
 
 def test_main_operational_exception(mocker, default_conf, caplog) -> None:
     patch_exchange(mocker)
-    mocker.patch.multiple(
-        'freqtrade.freqtradebot.FreqtradeBot',
-        _init_modules=MagicMock(),
-        worker=MagicMock(side_effect=OperationalException('Oh snap!')),
-        cleanup=MagicMock(),
-    )
+    mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock())
     mocker.patch(
-        'freqtrade.configuration.Configuration._load_config_file',
-        lambda *args, **kwargs: default_conf
+        'freqtrade.worker.Worker._worker',
+        MagicMock(side_effect=OperationalException('Oh snap!'))
     )
+    patched_configuration_load_config_file(mocker, default_conf)
     mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
+    mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
 
     args = ['-c', 'config.json.example']
 
@@ -112,59 +103,54 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None:
 
 def test_main_reload_conf(mocker, default_conf, caplog) -> None:
     patch_exchange(mocker)
-    mocker.patch.multiple(
-        'freqtrade.freqtradebot.FreqtradeBot',
-        _init_modules=MagicMock(),
-        worker=MagicMock(return_value=State.RELOAD_CONF),
-        cleanup=MagicMock(),
-    )
-    mocker.patch(
-        'freqtrade.configuration.Configuration._load_config_file',
-        lambda *args, **kwargs: default_conf
-    )
+    mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock())
+    # 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)
+    patched_configuration_load_config_file(mocker, default_conf)
+    reconfigure_mock = mocker.patch('freqtrade.main.Worker._reconfigure', MagicMock())
+
     mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
+    mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
 
-    # Raise exception as side effect to avoid endless loop
-    reconfigure_mock = mocker.patch(
-        'freqtrade.main.reconfigure', MagicMock(side_effect=Exception)
-    )
-
+    args = Arguments(['-c', 'config.json.example'], '').get_parsed_arg()
+    worker = Worker(args=args, config=default_conf)
     with pytest.raises(SystemExit):
         main(['-c', 'config.json.example'])
 
-    assert reconfigure_mock.call_count == 1
     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:
     patch_exchange(mocker)
-    mocker.patch.multiple(
-        'freqtrade.freqtradebot.FreqtradeBot',
-        _init_modules=MagicMock(),
-        worker=MagicMock(side_effect=OperationalException('Oh snap!')),
-        cleanup=MagicMock(),
-    )
+    mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock())
     mocker.patch(
-        'freqtrade.configuration.Configuration._load_config_file',
-        lambda *args, **kwargs: default_conf
+        'freqtrade.worker.Worker._worker',
+        MagicMock(side_effect=OperationalException('Oh snap!'))
     )
+    patched_configuration_load_config_file(mocker, default_conf)
     mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
+    mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
 
-    freqtrade = FreqtradeBot(default_conf)
+    args = Arguments(['-c', 'config.json.example'], '').get_parsed_arg()
+    worker = Worker(args=args, config=default_conf)
+    freqtrade = worker.freqtrade
 
     # Renew mock to return modified data
     conf = deepcopy(default_conf)
     conf['stake_amount'] += 1
-    mocker.patch(
-        'freqtrade.configuration.Configuration._load_config_file',
-        lambda *args, **kwargs: conf
-    )
+    patched_configuration_load_config_file(mocker, conf)
 
+    worker._config = conf
     # reconfigure should return a new instance
-    freqtrade2 = reconfigure(
-        freqtrade,
-        Arguments(['-c', 'config.json.example'], '').get_parsed_arg()
-    )
+    worker._reconfigure()
+    freqtrade2 = worker.freqtrade
 
     # Verify we have a new instance with the new config
     assert freqtrade is not freqtrade2
diff --git a/freqtrade/tests/test_misc.py b/freqtrade/tests/test_misc.py
index 2da6b8718..1a6b2a92d 100644
--- a/freqtrade/tests/test_misc.py
+++ b/freqtrade/tests/test_misc.py
@@ -4,10 +4,9 @@ import datetime
 from unittest.mock import MagicMock
 
 from freqtrade.data.converter import parse_ticker_dataframe
-from freqtrade.misc import (common_datearray, datesarray_to_datetimearray,
-                            file_dump_json, file_load_json, format_ms_time, shorten_date)
-from freqtrade.data.history import load_tickerdata_file, make_testdata_path
-from freqtrade.strategy.default_strategy import DefaultStrategy
+from freqtrade.data.history import pair_data_filename
+from freqtrade.misc import (datesarray_to_datetimearray, file_dump_json,
+                            file_load_json, format_ms_time, shorten_date)
 
 
 def test_shorten_date() -> None:
@@ -17,7 +16,8 @@ def test_shorten_date() -> None:
 
 
 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'])
 
     assert isinstance(dates[0], datetime.datetime)
@@ -31,19 +31,6 @@ def test_datesarray_to_datetimearray(ticker_history_list):
     assert date_len == 2
 
 
-def test_common_datearray(default_conf) -> None:
-    strategy = DefaultStrategy(default_conf)
-    tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
-    tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, "1m", fill_missing=True)}
-    dataframes = strategy.tickerdata_to_dataframe(tickerlist)
-
-    dates = common_datearray(dataframes)
-
-    assert dates.size == dataframes['UNITTEST/BTC']['date'].size
-    assert dates[0] == dataframes['UNITTEST/BTC']['date'][0]
-    assert dates[-1] == dataframes['UNITTEST/BTC']['date'].iloc[-1]
-
-
 def test_file_dump_json(mocker) -> None:
     file_open = mocker.patch('freqtrade.misc.open', MagicMock())
     json_dump = mocker.patch('rapidjson.dump', MagicMock())
@@ -60,13 +47,13 @@ def test_file_dump_json(mocker) -> None:
 def test_file_load_json(mocker) -> None:
 
     # 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
     # 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
     # 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
 
 
diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py
index a9519e693..32425ef7b 100644
--- a/freqtrade/tests/test_persistence.py
+++ b/freqtrade/tests/test_persistence.py
@@ -1,7 +1,8 @@
 # pragma pylint: disable=missing-docstring, C0103
-from unittest.mock import MagicMock
 import logging
+from unittest.mock import MagicMock
 
+import arrow
 import pytest
 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
 
 
-@pytest.fixture(scope='function')
-def init_persistence(default_conf):
-    init(default_conf)
+def create_mock_trades(fee):
+    """
+    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):
     # Check if init create a session
-    init(default_conf)
+    init(default_conf['db_url'], default_conf['dry_run'])
     assert hasattr(Trade, 'session')
     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'})
     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.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
     default_conf.update({'db_url': 'unknown:///some.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):
@@ -45,7 +85,7 @@ def test_init_prod_db(default_conf, mocker):
 
     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.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())
 
-    init(default_conf)
+    init(default_conf['db_url'], default_conf['dry_run'])
     assert create_engine_mock.call_count == 1
     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
 
 
+@pytest.mark.usefixtures("init_persistence")
 def test_clean_dry_run_db(default_conf, fee):
-    init(default_conf)
 
     # Simulate dry_run entries
     trade = Trade(
@@ -423,7 +463,7 @@ def test_migrate_old(mocker, default_conf, fee):
     engine.execute(create_table_old)
     engine.execute(insert_table_old)
     # 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
     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")
     # 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
     trade = Trade.query.filter(Trade.id == 1).first()
@@ -510,6 +550,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
     assert trade.pair == "ETC/BTC"
     assert trade.exchange == "binance"
     assert trade.max_rate == 0.0
+    assert trade.min_rate is None
     assert trade.stop_loss == 0.0
     assert trade.initial_stop_loss == 0.0
     assert trade.sell_reason is None
@@ -564,7 +605,7 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog):
     engine.execute(insert_table_old)
 
     # 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
     trade = Trade.query.filter(Trade.id == 1).first()
@@ -585,7 +626,58 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog):
                    caplog.record_tuples)
 
 
-def test_adjust_stop_loss(limit_buy_order, limit_sell_order, fee):
+def test_adjust_stop_loss(fee):
+    trade = Trade(
+        pair='ETH/BTC',
+        stake_amount=0.001,
+        fee_open=fee.return_value,
+        fee_close=fee.return_value,
+        exchange='bittrex',
+        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
+
+    # Get percent of profit with a lower rate
+    trade.adjust_stop_loss(0.96, 0.05)
+    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
+
+    # Get percent of profit with a custom rate (Higher than open rate)
+    trade.adjust_stop_loss(1.3, -0.1)
+    assert round(trade.stop_loss, 8) == 1.17
+    assert trade.stop_loss_pct == -0.1
+    assert trade.initial_stop_loss == 0.95
+    assert trade.initial_stop_loss_pct == -0.05
+
+    # current rate lower again ... should not change
+    trade.adjust_stop_loss(1.2, 0.1)
+    assert round(trade.stop_loss, 8) == 1.17
+    assert trade.initial_stop_loss == 0.95
+    assert trade.initial_stop_loss_pct == -0.05
+
+    # current rate higher... should raise stoploss
+    trade.adjust_stop_loss(1.4, 0.1)
+    assert round(trade.stop_loss, 8) == 1.26
+    assert trade.initial_stop_loss == 0.95
+    assert trade.initial_stop_loss_pct == -0.05
+
+    #  Initial is true but stop_loss set - so doesn't do anything
+    trade.adjust_stop_loss(1.7, 0.1, True)
+    assert round(trade.stop_loss, 8) == 1.26
+    assert trade.initial_stop_loss == 0.95
+    assert trade.initial_stop_loss_pct == -0.05
+    assert trade.stop_loss_pct == -0.1
+
+
+def test_adjust_min_max_rates(fee):
     trade = Trade(
         pair='ETH/BTC',
         stake_amount=0.001,
@@ -595,44 +687,35 @@ def test_adjust_stop_loss(limit_buy_order, limit_sell_order, fee):
         open_rate=1,
     )
 
-    trade.adjust_stop_loss(trade.open_rate, 0.05, True)
-    assert trade.stop_loss == 0.95
+    trade.adjust_min_max_rates(trade.open_rate)
     assert trade.max_rate == 1
-    assert trade.initial_stop_loss == 0.95
+    assert trade.min_rate == 1
 
-    # Get percent of profit with a lowre rate
-    trade.adjust_stop_loss(0.96, 0.05)
-    assert trade.stop_loss == 0.95
+    # check min adjusted, max remained
+    trade.adjust_min_max_rates(0.96)
     assert trade.max_rate == 1
-    assert trade.initial_stop_loss == 0.95
+    assert trade.min_rate == 0.96
 
-    # Get percent of profit with a custom rate (Higher than open rate)
-    trade.adjust_stop_loss(1.3, -0.1)
-    assert round(trade.stop_loss, 8) == 1.17
-    assert trade.max_rate == 1.3
-    assert trade.initial_stop_loss == 0.95
+    # check max adjusted, min remains
+    trade.adjust_min_max_rates(1.05)
+    assert trade.max_rate == 1.05
+    assert trade.min_rate == 0.96
 
-    # current rate lower again ... should not change
-    trade.adjust_stop_loss(1.2, 0.1)
-    assert round(trade.stop_loss, 8) == 1.17
-    assert trade.max_rate == 1.3
-    assert trade.initial_stop_loss == 0.95
-
-    # current rate higher... should raise stoploss
-    trade.adjust_stop_loss(1.4, 0.1)
-    assert round(trade.stop_loss, 8) == 1.26
-    assert trade.max_rate == 1.4
-    assert trade.initial_stop_loss == 0.95
-
-    #  Initial is true but stop_loss set - so doesn't do anything
-    trade.adjust_stop_loss(1.7, 0.1, True)
-    assert round(trade.stop_loss, 8) == 1.26
-    assert trade.max_rate == 1.4
-    assert trade.initial_stop_loss == 0.95
+    # current rate "in the middle" - no adjustment
+    trade.adjust_min_max_rates(1.03)
+    assert trade.max_rate == 1.05
+    assert trade.min_rate == 0.96
 
 
+@pytest.mark.usefixtures("init_persistence")
 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
     trade = Trade(
@@ -641,36 +724,117 @@ def test_get_open(default_conf, fee):
         amount=123.0,
         fee_open=fee.return_value,
         fee_close=fee.return_value,
+        open_date=arrow.utcnow().shift(hours=-2).datetime,
         open_rate=0.123,
         exchange='bittrex',
         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(
-        pair='ETC/BTC',
+        pair='XRP/BTC',
         stake_amount=0.001,
-        amount=123.0,
+        amount=100.0,
         fee_open=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,
+        close_rate=0.125,
         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(
-        pair='ETC/BTC',
+        pair='ETH/BTC',
         stake_amount=0.001,
-        amount=123.0,
         fee_open=fee.return_value,
+        open_date=arrow.utcnow().shift(hours=-2).datetime,
+        amount=10,
         fee_close=fee.return_value,
-        open_rate=0.123,
         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)
 
-    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
diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py
new file mode 100644
index 000000000..f9da773fe
--- /dev/null
+++ b/freqtrade/tests/test_plotting.py
@@ -0,0 +1,270 @@
+
+from copy import deepcopy
+from unittest.mock import MagicMock
+
+import plotly.graph_objects as go
+from plotly.subplots import make_subplots
+
+from freqtrade.configuration import Arguments, TimeRange
+from freqtrade.data import history
+from freqtrade.data.btanalysis import create_cum_profit, load_backtest_data
+from freqtrade.plot.plotting import (add_indicators, add_profit,
+                                     generate_candlestick_graph,
+                                     generate_plot_filename,
+                                     generate_profit_graph, init_plotscript,
+                                     plot_trades, store_plot_file)
+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 add_indicators and plot_trades"""
+    return fig
+
+
+def find_trace_in_fig_data(data, search_string: str):
+    matches = (d for d in data if d.name == search_string)
+    return next(matches)
+
+
+def generage_empty_figure():
+    return make_subplots(
+        rows=3,
+        cols=1,
+        shared_xaxes=True,
+        row_width=[1, 1, 4],
+        vertical_spacing=0.0001,
+    )
+
+
+def test_init_plotscript(default_conf, mocker):
+    default_conf['timerange'] = "20180110-20180112"
+    default_conf['trade_source'] = "file"
+    default_conf['ticker_interval'] = "5m"
+    default_conf["datadir"] = history.make_testdata_path(None)
+    default_conf['exportfilename'] = str(
+        history.make_testdata_path(None) / "backtest-result_test.json")
+    ret = init_plotscript(default_conf)
+    assert "tickers" in ret
+    assert "trades" in ret
+    assert "pairs" in ret
+    assert "strategy" in ret
+
+    default_conf['pairs'] = "POWR/BTC,XLM/BTC"
+    ret = init_plotscript(default_conf)
+    assert "tickers" in ret
+    assert "POWR/BTC" in ret["tickers"]
+    assert "XLM/BTC" in ret["tickers"]
+
+
+def test_add_indicators(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 = add_indicators(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 = add_indicators(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 = add_indicators(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_candlestick_graph_no_signals_no_trades(default_conf, mocker, caplog):
+    row_mock = mocker.patch('freqtrade.plot.plotting.add_indicators',
+                            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_candlestick_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_candlestick_graph_no_trades(default_conf, mocker):
+    row_mock = mocker.patch('freqtrade.plot.plotting.add_indicators',
+                            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_candlestick_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_filename():
+    fn = generate_plot_filename("UNITTEST/BTC", "5m")
+    assert fn == "freqtrade-plot-UNITTEST_BTC-5m.html"
+
+
+def test_generate_plot_file(mocker, caplog):
+    fig = generage_empty_figure()
+    plot_mock = mocker.patch("freqtrade.plot.plotting.plot", MagicMock())
+    store_plot_file(fig, filename="freqtrade-plot-UNITTEST_BTC-5m.html")
+
+    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")
+    assert log_has("Stored plot as user_data/plots/freqtrade-plot-UNITTEST_BTC-5m.html",
+                   caplog.record_tuples)
+
+
+def test_add_profit():
+    filename = history.make_testdata_path(None) / "backtest-result_test.json"
+    bt_data = load_backtest_data(filename)
+    timerange = Arguments.parse_timerange("20180110-20180112")
+
+    df = history.load_pair_history(pair="POWR/BTC", ticker_interval='5m',
+                                   datadir=None, timerange=timerange)
+    fig = generage_empty_figure()
+
+    cum_profits = create_cum_profit(df.set_index('date'),
+                                    bt_data[bt_data["pair"] == 'POWR/BTC'],
+                                    "cum_profits")
+
+    fig1 = add_profit(fig, row=2, data=cum_profits, column='cum_profits', name='Profits')
+    figure = fig1.layout.figure
+    profits = find_trace_in_fig_data(figure.data, "Profits")
+    assert isinstance(profits, go.Scattergl)
+    assert profits.yaxis == "y2"
+
+
+def test_generate_profit_graph():
+    filename = history.make_testdata_path(None) / "backtest-result_test.json"
+    trades = load_backtest_data(filename)
+    timerange = Arguments.parse_timerange("20180110-20180112")
+    pairs = ["POWR/BTC", "XLM/BTC"]
+
+    tickers = history.load_data(datadir=None,
+                                pairs=pairs,
+                                ticker_interval='5m',
+                                timerange=timerange
+                                )
+    trades = trades[trades['pair'].isin(pairs)]
+
+    fig = generate_profit_graph(pairs, tickers, trades)
+    assert isinstance(fig, go.Figure)
+
+    assert fig.layout.title.text == "Profit plot"
+    figure = fig.layout.figure
+    assert len(figure.data) == 4
+
+    avgclose = find_trace_in_fig_data(figure.data, "Avg close price")
+    assert isinstance(avgclose, go.Scattergl)
+
+    profit = find_trace_in_fig_data(figure.data, "Profit")
+    assert isinstance(profit, go.Scattergl)
+
+    for pair in pairs:
+        profit_pair = find_trace_in_fig_data(figure.data, f"Profit {pair}")
+        assert isinstance(profit_pair, go.Scattergl)
diff --git a/freqtrade/tests/test_talib.py b/freqtrade/tests/test_talib.py
index 093c3023c..2c7f73eb1 100644
--- a/freqtrade/tests/test_talib.py
+++ b/freqtrade/tests/test_talib.py
@@ -13,4 +13,4 @@ def test_talib_bollingerbands_near_zero_values():
         {'close': 0.00000014}
     ])
     bollinger = ta.BBANDS(inputs, matype=0, timeperiod=2)
-    assert (bollinger['upperband'][3] != bollinger['middleband'][3])
+    assert bollinger['upperband'][3] != bollinger['middleband'][3]
diff --git a/freqtrade/tests/test_utils.py b/freqtrade/tests/test_utils.py
new file mode 100644
index 000000000..a12b709d7
--- /dev/null
+++ b/freqtrade/tests/test_utils.py
@@ -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)
diff --git a/freqtrade/utils.py b/freqtrade/utils.py
new file mode 100644
index 000000000..d550ef43c
--- /dev/null
+++ b/freqtrade/utils.py
@@ -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())}")
diff --git a/freqtrade/vendor/qtpylib/indicators.py b/freqtrade/vendor/qtpylib/indicators.py
index 3866d36c1..b3b2ac533 100644
--- a/freqtrade/vendor/qtpylib/indicators.py
+++ b/freqtrade/vendor/qtpylib/indicators.py
@@ -4,13 +4,13 @@
 # QTPyLib: Quantitative Trading Python Library
 # 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 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
 # distributed under the License is distributed on an "AS IS" BASIS,
@@ -19,8 +19,8 @@
 # limitations under the License.
 #
 
-import sys
 import warnings
+import sys
 from datetime import datetime, timedelta
 
 import numpy as np
@@ -62,19 +62,20 @@ def numpy_rolling_series(func):
 
 @numpy_rolling_series
 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
 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'):
     """ remove previous globex day from df """
-    if len(df) == 0:
+    if df.empty:
         return df
 
     # get start/end/now as decimals
@@ -103,47 +104,47 @@ def session(df, start='17:00', end='16:00'):
 
     return df.copy()
 
-
 # ---------------------------------------------
 
+
 def heikinashi(bars):
     bars = bars.copy()
     bars['ha_close'] = (bars['open'] + bars['high'] +
                         bars['low'] + bars['close']) / 4
 
-    bars['ha_open'] = (bars['open'].shift(1) + bars['close'].shift(1)) / 2
-    bars.loc[:1, 'ha_open'] = bars['open'].values[0]
-    for x in range(2):
-        bars.loc[1:, 'ha_open'] = (
-            (bars['ha_open'].shift(1) + bars['ha_close'].shift(1)) / 2)[1:]
+    # ha open
+    bars.at[0, 'ha_open'] = (bars.at[0, 'open'] + bars.at[0, 'close']) / 2
+    for i in range(1, len(bars)):
+        bars.at[i, 'ha_open'] = (bars.at[i - 1, 'ha_open'] + bars.at[i - 1, 'ha_close']) / 2
 
     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)
 
-    return pd.DataFrame(
-        index=bars.index,
-        data={
-            'open': bars['ha_open'],
-            'high': bars['ha_high'],
-            'low': bars['ha_low'],
-            'close': bars['ha_close']})
-
+    return pd.DataFrame(index=bars.index,
+                        data={'open': bars['ha_open'],
+                              'high': bars['ha_high'],
+                              '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):
-    rsi_series = rsi(series, rsi_len)
-    bb_series = bollinger_bands(rsi_series, bollinger_len, bollinger_std)
-    signal = sma(rsi_series, rsi_signal_len)
-    rsi_series = sma(rsi_series, rsi_smoothing)
+
+def tdi(series, rsi_lookback=13, rsi_smooth_len=2,
+        rsi_signal_len=7, bb_lookback=34, bb_std=1.6185):
+
+    rsi_data = rsi(series, rsi_lookback)
+    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={
-        "rsi": rsi_series,
-        "signal": signal,
-        "bbupper": bb_series['upper'],
-        "bblower": bb_series['lower'],
-        "bbmid": bb_series['mid']
+        "rsi": rsi_data,
+        "rsi_signal": rsi_signal,
+        "rsi_smooth": rsi_smooth,
+        "rsi_bb_upper": bb_series['upper'],
+        "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):
-    mtx = np.empty(len)
+def nans(length=1):
+    mtx = np.empty(length)
     mtx[:] = np.nan
     return mtx
 
@@ -212,8 +213,7 @@ def atr(bars, window=14, exp=False):
     else:
         res = rolling_mean(tr, window)
 
-    res = pd.Series(res)
-    return (res.shift(1) * (window - 1) + res) / window
+    return pd.Series(res)
 
 
 # ---------------------------------------------
@@ -222,7 +222,7 @@ def crossed(series1, series2, direction=None):
     if isinstance(series1, np.ndarray):
         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)
 
     if direction is None or direction == "above":
@@ -256,7 +256,7 @@ def rolling_std(series, window=200, min_periods=None):
     else:
         try:
             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()
 
 # ---------------------------------------------
@@ -269,7 +269,7 @@ def rolling_mean(series, window=200, min_periods=None):
     else:
         try:
             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()
 
 # ---------------------------------------------
@@ -279,7 +279,7 @@ def rolling_min(series, window=14, min_periods=None):
     min_periods = window if min_periods is None else min_periods
     try:
         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()
 
 
@@ -289,7 +289,7 @@ def rolling_max(series, window=14, min_periods=None):
     min_periods = window if min_periods is None else min_periods
     try:
         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()
 
 
@@ -299,16 +299,17 @@ def rolling_weighted_mean(series, window=200, min_periods=None):
     min_periods = window if min_periods is None else min_periods
     try:
         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)
 
 
 # ---------------------------------------------
 
-def hull_moving_average(series, window=200):
-    wma = (2 * rolling_weighted_mean(series, window=window / 2)) - \
-        rolling_weighted_mean(series, window=window)
-    return rolling_weighted_mean(wma, window=np.sqrt(window))
+def hull_moving_average(series, window=200, min_periods=None):
+    min_periods = window if min_periods is None else min_periods
+    ma = (2 * rolling_weighted_mean(series, window / 2, min_periods)) - \
+        rolling_weighted_mean(series, window, min_periods)
+    return rolling_weighted_mean(ma, np.sqrt(window), min_periods)
 
 
 # ---------------------------------------------
@@ -325,8 +326,8 @@ def wma(series, window=200, min_periods=None):
 
 # ---------------------------------------------
 
-def hma(series, window=200):
-    return hull_moving_average(series, window=window)
+def hma(series, window=200, min_periods=None):
+    return hull_moving_average(series, window=window, min_periods=min_periods)
 
 
 # ---------------------------------------------
@@ -361,7 +362,8 @@ def rolling_vwap(bars, window=200, min_periods=None):
                                       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 +372,7 @@ def rsi(series, window=14):
     """
     compute the n period relative strength indicator
     """
+
     # 100-(100/relative_strength)
     deltas = np.diff(series)
     seed = deltas[:window + 1]
@@ -406,13 +409,13 @@ def macd(series, fast=3, slow=10, smooth=16):
     using a fast and slow exponential moving avg'
     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)
-    signal = rolling_weighted_mean(macd, window=smooth)
-    histogram = macd - signal
-    # return macd, signal, histogram
+    signal = rolling_weighted_mean(macd_line, window=smooth)
+    histogram = macd_line - signal
+    # return macd_line, signal, histogram
     return pd.DataFrame(index=series.index, data={
-        'macd': macd.values,
+        'macd': macd_line.values,
         'signal': signal.values,
         'histogram': histogram.values
     })
@@ -421,14 +424,14 @@ def macd(series, fast=3, slow=10, smooth=16):
 # ---------------------------------------------
 
 def bollinger_bands(series, window=20, stds=2):
-    sma = rolling_mean(series, window=window)
-    std = rolling_std(series, window=window)
-    upper = sma + std * stds
-    lower = sma - std * stds
+    ma = rolling_mean(series, window=window, min_periods=1)
+    std = rolling_std(series, window=window, min_periods=1)
+    upper = ma + std * stds
+    lower = ma - std * stds
 
     return pd.DataFrame(index=series.index, data={
         'upper': upper,
-        'mid': sma,
+        'mid': ma,
         'lower': lower
     })
 
@@ -454,7 +457,7 @@ def returns(series):
     try:
         res = (series / series.shift(1) -
                1).replace([np.inf, -np.inf], float('NaN'))
-    except BaseException:
+    except Exception as e:  # noqa: F841
         res = nans(len(series))
 
     return pd.Series(index=series.index, data=res)
@@ -466,7 +469,7 @@ def log_returns(series):
     try:
         res = np.log(series / series.shift(1)
                      ).replace([np.inf, -np.inf], float('NaN'))
-    except BaseException:
+    except Exception as e:  # noqa: F841
         res = nans(len(series))
 
     return pd.Series(index=series.index, data=res)
@@ -479,7 +482,7 @@ def implied_volatility(series, window=252):
         logret = np.log(series / series.shift(1)
                         ).replace([np.inf, -np.inf], float('NaN'))
         res = numpy_rolling_std(logret, window) * np.sqrt(window)
-    except BaseException:
+    except Exception as e:  # noqa: F841
         res = nans(len(series))
 
     return pd.Series(index=series.index, data=res)
@@ -530,32 +533,55 @@ def stoch(df, window=14, d=3, k=3, fast=False):
     compute the n period relative strength indicator
     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)
-                         for i in np.arange(window)], 1).apply(list, 1)
-    lows_ma = lows_ma.T.min().T
+    my_df = pd.DataFrame(index=df.index)
 
-    fast_k = ((df['close'] - lows_ma) / (highs_ma - lows_ma)) * 100
-    fast_d = numpy_rolling_mean(fast_k, d)
+    my_df['rolling_max'] = df['high'].rolling(window).max()
+    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:
-        data = {
-            'k': fast_k,
-            'd': fast_d
-        }
+        return my_df.loc[:, ['fast_k', 'fast_d']]
 
-    else:
-        slow_k = numpy_rolling_mean(fast_k, k)
-        slow_d = numpy_rolling_mean(slow_k, d)
-        data = {
-            'k': slow_k,
-            'd': slow_d
-        }
+    my_df['slow_k'] = my_df['fast_k'].rolling(k).mean()
+    my_df['slow_d'] = my_df['slow_k'].rolling(d).mean()
 
-    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 +597,21 @@ def zscore(bars, window=20, stds=1, col='close'):
 
 def pvt(bars):
     """ Price Volume Trend """
-    pvt = ((bars['close'] - bars['close'].shift(1)) /
-           bars['close'].shift(1)) * bars['volume']
-    return pvt.cumsum()
+    trend = ((bars['close'] - bars['close'].shift(1)) /
+             bars['close'].shift(1)) * bars['volume']
+    return trend.cumsum()
+
+
+def chopiness(bars, window=14):
+    atrsum = true_range(bars).rolling(window).sum()
+    highs = bars['high'].rolling(window).max()
+    lows = bars['low'].rolling(window).min()
+    return 100 * np.log10(atrsum / (highs - lows)) / np.log10(window)
 
 
 # =============================================
 
+
 PandasObject.session = session
 PandasObject.atr = atr
 PandasObject.bollinger_bands = bollinger_bands
@@ -602,6 +636,7 @@ PandasObject.rsi = rsi
 PandasObject.stoch = stoch
 PandasObject.zscore = zscore
 PandasObject.pvt = pvt
+PandasObject.chopiness = chopiness
 PandasObject.tdi = tdi
 PandasObject.true_range = true_range
 PandasObject.mid_price = mid_price
@@ -613,4 +648,11 @@ PandasObject.rolling_weighted_mean = rolling_weighted_mean
 
 PandasObject.sma = sma
 PandasObject.wma = wma
+PandasObject.ema = wma
 PandasObject.hma = hma
+
+PandasObject.zlsma = zlsma
+PandasObject.zlwma = zlema
+PandasObject.zlema = zlema
+PandasObject.zlhma = zlhma
+PandasObject.zlma = zlma
diff --git a/freqtrade/worker.py b/freqtrade/worker.py
new file mode 100755
index 000000000..db0dba0e8
--- /dev/null
+++ b/freqtrade/worker.py
@@ -0,0 +1,189 @@
+"""
+Main Freqtrade worker class.
+"""
+import logging
+import time
+import traceback
+from argparse import Namespace
+from typing import Any, Callable, Optional
+import sdnotify
+
+from freqtrade import (constants, OperationalException, TemporaryError,
+                       __version__)
+from freqtrade.configuration import Configuration
+from freqtrade.freqtradebot import FreqtradeBot
+from freqtrade.state import State
+from freqtrade.rpc import RPCMessageType
+
+
+logger = logging.getLogger(__name__)
+
+
+class Worker(object):
+    """
+    Freqtradebot worker class
+    """
+
+    def __init__(self, args: Namespace, config=None) -> None:
+        """
+        Init all variables and objects the bot needs to work
+        """
+        logger.info('Starting worker %s', __version__)
+
+        self._args = args
+        self._config = config
+        self._init(False)
+
+        # Tell systemd that we completed initialization phase
+        if self._sd_notify:
+            logger.debug("sd_notify: READY=1")
+            self._sd_notify.notify("READY=1")
+
+    def _init(self, reconfig: bool) -> None:
+        """
+        Also called from the _reconfigure() method (with reconfig=True).
+        """
+        if reconfig or self._config is None:
+            # Load configuration
+            self._config = Configuration(self._args, None).get_config()
+
+        # Init the instance of the bot
+        self.freqtrade = FreqtradeBot(self._config)
+
+        self._throttle_secs = self._config.get('internals', {}).get(
+            'process_throttle_secs',
+            constants.PROCESS_THROTTLE_SECS
+        )
+
+        self._sd_notify = sdnotify.SystemdNotifier() if \
+            self._config.get('internals', {}).get('sd_notify', False) else None
+
+    @property
+    def state(self) -> State:
+        return self.freqtrade.state
+
+    @state.setter
+    def state(self, value: State) -> None:
+        self.freqtrade.state = value
+
+    def run(self) -> None:
+        state = None
+        while True:
+            state = self._worker(old_state=state)
+            if state == State.RELOAD_CONF:
+                self._reconfigure()
+
+    def _worker(self, old_state: Optional[State], throttle_secs: Optional[float] = None) -> State:
+        """
+        Trading routine that must be run at each loop
+        :param old_state: the previous service state from the previous call
+        :return: current service state
+        """
+        state = self.freqtrade.state
+        if throttle_secs is None:
+            throttle_secs = self._throttle_secs
+
+        # Log state transition
+        if state != old_state:
+            self.freqtrade.rpc.send_msg({
+                'type': RPCMessageType.STATUS_NOTIFICATION,
+                'status': f'{state.name.lower()}'
+            })
+            logger.info('Changing state to: %s', state.name)
+            if state == State.RUNNING:
+                self.freqtrade.startup()
+
+        if state == State.STOPPED:
+            # Ping systemd watchdog before sleeping in the stopped state
+            if self._sd_notify:
+                logger.debug("sd_notify: WATCHDOG=1\\nSTATUS=State: STOPPED.")
+                self._sd_notify.notify("WATCHDOG=1\nSTATUS=State: STOPPED.")
+
+            time.sleep(throttle_secs)
+
+        elif state == State.RUNNING:
+            # Ping systemd watchdog before throttling
+            if self._sd_notify:
+                logger.debug("sd_notify: WATCHDOG=1\\nSTATUS=State: RUNNING.")
+                self._sd_notify.notify("WATCHDOG=1\nSTATUS=State: RUNNING.")
+
+            self._throttle(func=self._process, min_secs=throttle_secs)
+
+        return state
+
+    def _throttle(self, func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any:
+        """
+        Throttles the given callable that it
+        takes at least `min_secs` to finish execution.
+        :param func: Any callable
+        :param min_secs: minimum execution time in seconds
+        :return: Any
+        """
+        start = time.time()
+        result = func(*args, **kwargs)
+        end = time.time()
+        duration = max(min_secs - (end - start), 0.0)
+        logger.debug('Throttling %s for %.2f seconds', func.__name__, duration)
+        time.sleep(duration)
+        return result
+
+    def _process(self) -> bool:
+        logger.debug("========================================")
+        state_changed = False
+        try:
+            state_changed = self.freqtrade.process()
+        except TemporaryError as error:
+            logger.warning(f"Error: {error}, retrying in {constants.RETRY_TIMEOUT} seconds...")
+            time.sleep(constants.RETRY_TIMEOUT)
+        except OperationalException:
+            tb = traceback.format_exc()
+            hint = 'Issue `/start` if you think it is safe to restart.'
+            self.freqtrade.rpc.send_msg({
+                'type': RPCMessageType.STATUS_NOTIFICATION,
+                'status': f'OperationalException:\n```\n{tb}```{hint}'
+            })
+            logger.exception('OperationalException. Stopping trader ...')
+            self.freqtrade.state = State.STOPPED
+            # TODO: The return value of _process() is not used apart tests
+            # and should (could) be eliminated later. See PR #1689.
+#            state_changed = True
+        return state_changed
+
+    def _reconfigure(self) -> None:
+        """
+        Cleans up current freqtradebot instance, reloads the configuration and
+        replaces it with the new instance
+        """
+        # Tell systemd that we initiated reconfiguration
+        if self._sd_notify:
+            logger.debug("sd_notify: RELOADING=1")
+            self._sd_notify.notify("RELOADING=1")
+
+        # Clean up current freqtrade modules
+        self.freqtrade.cleanup()
+
+        # Load and validate config and create new instance of the bot
+        self._init(True)
+
+        self.freqtrade.rpc.send_msg({
+            'type': RPCMessageType.STATUS_NOTIFICATION,
+            'status': 'config reloaded'
+        })
+
+        # Tell systemd that we completed reconfiguration
+        if self._sd_notify:
+            logger.debug("sd_notify: READY=1")
+            self._sd_notify.notify("READY=1")
+
+    def exit(self) -> None:
+        # Tell systemd that we are exiting now
+        if self._sd_notify:
+            logger.debug("sd_notify: STOPPING=1")
+            self._sd_notify.notify("STOPPING=1")
+
+        if self.freqtrade:
+            self.freqtrade.rpc.send_msg({
+                'type': RPCMessageType.STATUS_NOTIFICATION,
+                'status': 'process died'
+            })
+            self.freqtrade.cleanup()
diff --git a/mkdocs.yml b/mkdocs.yml
index 9a6fec851..b5e759432 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -2,18 +2,22 @@ site_name: Freqtrade
 nav:
     - About: index.md
     - Installation: installation.md
+    - Installation Docker: docker.md
     - Configuration: configuration.md
-    - Start the bot: bot-usage.md
+    - Strategy Customization: strategy-customization.md
     - Stoploss: stoploss.md
-    - Custom Strategy: bot-optimization.md
-    - Telegram: telegram-usage.md
-    - Web Hook: webhook-config.md
+    - Start the bot: bot-usage.md
+    - Control the bot:
+        - Telegram: telegram-usage.md
+        - Web Hook: webhook-config.md
+        - REST API: rest-api.md
     - Backtesting: backtesting.md
     - Hyperopt: hyperopt.md
     - Edge positioning: edge.md
     - Plotting: plotting.md
     - Deprecated features: deprecated.md
     - FAQ: faq.md
+    - Data Analysis: data-analysis.md
     - SQL Cheatsheet: sql_cheatsheet.md
     - Sandbox testing: sandbox-testing.md
     - Contributors guide: developer.md
diff --git a/requirements-common.txt b/requirements-common.txt
new file mode 100644
index 000000000..1413539c3
--- /dev/null
+++ b/requirements-common.txt
@@ -0,0 +1,32 @@
+# requirements without requirements installable via conda
+# mainly used for Raspberry pi installs
+ccxt==1.18.1021
+SQLAlchemy==1.3.6
+python-telegram-bot==11.1.0
+arrow==0.14.4
+cachetools==3.1.1
+requests==2.22.0
+urllib3==1.25.3
+wrapt==1.11.2
+scikit-learn==0.21.3
+joblib==0.13.2
+jsonschema==3.0.2
+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.4
+
+#Load ticker files 30% faster
+python-rapidjson==0.7.2
+
+# Notify systemd
+sdnotify==0.3.2
+
+# Api server
+flask==1.1.1
diff --git a/requirements-dev.txt b/requirements-dev.txt
index e0aaf9461..03b37417e 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,12 +1,14 @@
 # Include all requirements to run the bot.
 -r requirements.txt
+-r requirements-plot.txt
 
-flake8==3.7.7
+coveralls==1.8.2
+flake8==3.7.8
 flake8-type-annotations==0.1.0
 flake8-tidy-imports==2.0.0
-pytest==4.3.1
-pytest-mock==1.10.1
+mypy==0.720
+pytest==5.0.1
 pytest-asyncio==0.10.0
-pytest-cov==2.6.1
-coveralls==1.7.0
-mypy==0.670
+pytest-cov==2.7.1
+pytest-mock==1.10.4
+pytest-random-order==1.0.4
diff --git a/requirements-plot.txt b/requirements-plot.txt
index e582fddf6..f10bfac3f 100644
--- a/requirements-plot.txt
+++ b/requirements-plot.txt
@@ -1,5 +1,5 @@
 # Include all requirements to run the bot.
 -r requirements.txt
 
-plotly==3.7.1
+plotly==4.1.0
 
diff --git a/requirements.txt b/requirements.txt
index 3a25bb8bc..6420c7879 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,29 +1,6 @@
-ccxt==1.18.385
-SQLAlchemy==1.3.1
-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
-numpy==1.16.2
-pandas==0.24.2
-scikit-learn==0.20.3
-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
+# Load common requirements
+-r requirements-common.txt
 
-# 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
+numpy==1.17.0
+pandas==0.25.0
+scipy==1.3.0
diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py
index 5dee41bdd..580592294 100755
--- a/scripts/download_backtest_data.py
+++ b/scripts/download_backtest_data.py
@@ -1,111 +1,144 @@
 #!/usr/bin/env python3
-
-"""This script generate json data"""
+"""
+This script generates json files with pairs history data
+"""
+import arrow
 import json
 import sys
 from pathlib import Path
-import arrow
-from typing import Any, Dict
+from typing import Any, Dict, List
 
-from freqtrade.arguments import Arguments
-from freqtrade.arguments import TimeRange
-from freqtrade.exchange import Exchange
+from freqtrade.configuration import Arguments, TimeRange
+from freqtrade.configuration import Configuration
+from freqtrade.configuration.arguments import ARGS_DOWNLOADER
+from freqtrade.configuration.check_exchange import check_exchange
+from freqtrade.configuration.load_config import load_config_file
 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
 
 import logging
-logging.basicConfig(
-    level=logging.INFO,
-    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
-)
-set_loggers(0)
+
+logger = logging.getLogger('download_backtest_data')
 
 DEFAULT_DL_PATH = 'user_data/data'
 
-arguments = Arguments(sys.argv[1:], 'download utility')
-arguments.testdata_dl_options()
-args = arguments.parse_args()
+# Do not read the default config if config is not specified
+# in the command line options explicitely
+arguments = Arguments(sys.argv[1:], 'Download backtest data',
+                      no_default_config=True)
+arguments._build_args(optionlist=ARGS_DOWNLOADER)
+args = arguments._parse_args()
 
-timeframes = args.timeframes
+# Use bittrex as default exchange
+exchange_name = args.exchange or 'bittrex'
+
+pairs: List = []
+
+configuration = Configuration(args)
+config: Dict[str, Any] = {}
 
 if args.config:
-    configuration = Configuration(args)
-
-    config: Dict[str, Any] = {}
     # Now expecting a list of config filenames here, not a string
     for path in args.config:
-        print('Using config: %s ...', path)
+        logger.info(f"Using config: {path}...")
         # Merge config options, overwriting old values
-        config = deep_merge_dicts(configuration._load_config_file(path), config)
+        config = deep_merge_dicts(load_config_file(path), config)
 
     config['stake_currency'] = ''
     # Ensure we do not use Exchange credentials
+    config['exchange']['dry_run'] = True
     config['exchange']['key'] = ''
     config['exchange']['secret'] = ''
+
+    pairs = config['exchange']['pair_whitelist']
+
+    if config.get('ticker_interval'):
+        timeframes = args.timeframes or [config.get('ticker_interval')]
+    else:
+        timeframes = args.timeframes or ['1m', '5m']
+
 else:
-    config = {'stake_currency': '',
-              'dry_run': True,
-              'exchange': {
-                  'name': args.exchange,
-                  'key': '',
-                  'secret': '',
-                  'pair_whitelist': [],
-                  'ccxt_async_config': {
-                      "enableRateLimit": False
-                  }
-              }
-              }
+    config = {
+        'stake_currency': '',
+        'dry_run': True,
+        'exchange': {
+            'name': exchange_name,
+            'key': '',
+            'secret': '',
+            'pair_whitelist': [],
+            'ccxt_async_config': {
+                'enableRateLimit': True,
+                'rateLimit': 200
+            }
+        }
+    }
+    timeframes = args.timeframes or ['1m', '5m']
 
+configuration._process_logging_options(config)
 
-dl_path = Path(DEFAULT_DL_PATH).joinpath(config['exchange']['name'])
-if args.export:
-    dl_path = Path(args.export)
+if args.config and args.exchange:
+    logger.warning("The --exchange option is ignored, "
+                   "using exchange settings from the configuration file.")
 
-if not dl_path.is_dir():
-    sys.exit(f'Directory {dl_path} does not exist.')
+# Check if the exchange set by the user is supported
+check_exchange(config)
+
+configuration._process_datadir_options(config)
+
+dl_path = Path(config['datadir'])
 
 pairs_file = Path(args.pairs_file) if args.pairs_file else dl_path.joinpath('pairs.json')
-if not pairs_file.exists():
-    sys.exit(f'No pairs file found with path {pairs_file}.')
 
-with pairs_file.open() as file:
-    PAIRS = list(set(json.load(file)))
+if not pairs or args.pairs_file:
+    logger.info(f'Reading pairs file "{pairs_file}".')
+    # Download pairs from the pairs file if no config is specified
+    # or if pairs file is specified explicitely
+    if not pairs_file.exists():
+        sys.exit(f'No pairs file found with path "{pairs_file}".')
 
-PAIRS.sort()
+    with pairs_file.open() as file:
+        pairs = list(set(json.load(file)))
 
+    pairs.sort()
 
 timerange = TimeRange()
 if args.days:
     time_since = arrow.utcnow().shift(days=-args.days).strftime("%Y%m%d")
     timerange = arguments.parse_timerange(f'{time_since}-')
 
+logger.info(f'About to download pairs: {pairs}, intervals: {timeframes} to {dl_path}')
 
-print(f'About to download pairs: {PAIRS} to {dl_path}')
-
-# Init exchange
-exchange = Exchange(config)
 pairs_not_available = []
 
-for pair in PAIRS:
-    if pair not in exchange._api.markets:
-        pairs_not_available.append(pair)
-        print(f"skipping pair {pair}")
-        continue
-    for tick_interval in timeframes:
-        pair_print = pair.replace('/', '_')
-        filename = f'{pair_print}-{tick_interval}.json'
-        dl_file = dl_path.joinpath(filename)
-        if args.erase and dl_file.exists():
-            print(f'Deleting existing data for pair {pair}, interval {tick_interval}')
-            dl_file.unlink()
+try:
+    # Init exchange
+    exchange = Exchange(config)
 
-        print(f'downloading pair {pair}, interval {tick_interval}')
-        download_pair_history(datadir=dl_path, exchange=exchange,
-                              pair=pair,
-                              tick_interval=tick_interval,
-                              timerange=timerange)
+    for pair in pairs:
+        if pair not in exchange._api.markets:
+            pairs_not_available.append(pair)
+            logger.info(f"Skipping pair {pair}...")
+            continue
+        for ticker_interval in timeframes:
+            pair_print = pair.replace('/', '_')
+            filename = f'{pair_print}-{ticker_interval}.json'
+            dl_file = dl_path.joinpath(filename)
+            if args.erase and dl_file.exists():
+                logger.info(
+                    f'Deleting existing data for pair {pair}, interval {ticker_interval}.')
+                dl_file.unlink()
 
+            logger.info(f'Downloading pair {pair}, interval {ticker_interval}.')
+            download_pair_history(datadir=dl_path, exchange=exchange,
+                                  pair=pair, ticker_interval=str(ticker_interval),
+                                  timerange=timerange)
 
-if pairs_not_available:
-    print(f"Pairs [{','.join(pairs_not_available)}] not availble.")
+except KeyboardInterrupt:
+    sys.exit("SIGINT received, aborting ...")
+
+finally:
+    if pairs_not_available:
+        logger.info(
+            f"Pairs [{','.join(pairs_not_available)}] not available "
+            f"on exchange {config['exchange']['name']}.")
diff --git a/scripts/get_market_pairs.py b/scripts/get_market_pairs.py
index 6ee6464d3..cd38bf2fa 100644
--- a/scripts/get_market_pairs.py
+++ b/scripts/get_market_pairs.py
@@ -1,5 +1,10 @@
+"""
+This script was adapted from ccxt here:
+https://github.com/ccxt/ccxt/blob/master/examples/py/arbitrage-pairs.py
+"""
 import os
 import sys
+import traceback
 
 root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
 sys.path.append(root + '/python')
@@ -49,8 +54,12 @@ def print_supported_exchanges():
 
 try:
 
-    id = sys.argv[1]  # get exchange id from command line arguments
+    if len(sys.argv) < 2:
+        dump("Usage: python " + sys.argv[0], green('id'))
+        print_supported_exchanges()
+        sys.exit(1)
 
+    id = sys.argv[1]  # get exchange id from command line arguments
 
     # check if the exchange is supported by ccxt
     exchange_found = id in ccxt.exchanges
@@ -88,6 +97,7 @@ try:
 
 except Exception as e:
     dump('[' + type(e).__name__ + ']', str(e))
+    dump(traceback.format_exc())
     dump("Usage: python " + sys.argv[0], green('id'))
     print_supported_exchanges()
-
+    sys.exit(1)
diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py
index 581518b12..6412c45a4 100755
--- a/scripts/plot_dataframe.py
+++ b/scripts/plot_dataframe.py
@@ -2,19 +2,7 @@
 """
 Script to display when the bot will buy on specific pair(s)
 
-Mandatory Cli parameters:
--p / --pairs: pair(s) to examine
-
-Option but recommended
--s / --strategy: strategy to use
-
-
-Optional Cli parameters
--d / --datadir: path to pair(s) backtest data
---timerange: specify what timerange of data to use.
--l / --live: Live, to download the latest ticker for the pair(s)
--db / --db-url: Show trades stored in database
-
+Use `python plot_dataframe.py --help` to display the command line arguments
 
 Indicators recommended
 Row 1: sma, ema3, ema5, ema10, ema50
@@ -24,370 +12,23 @@ Example of usage:
 > python3 scripts/plot_dataframe.py --pairs BTC/EUR,XRP/BTC -d user_data/data/
   --indicators1 sma,ema3 --indicators2 fastk,fastd
 """
-import json
 import logging
 import sys
-from argparse import Namespace
-from pathlib import Path
-from typing import Dict, List, Any
+from typing import Any, Dict, List
 
-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, TimeRange
-from freqtrade.data import history
-from freqtrade.data.btanalysis import load_backtest_data, BT_DATA_COLUMNS
-from freqtrade.exchange import Exchange
-from freqtrade.optimize.backtesting import setup_configuration
-from freqtrade.persistence import Trade
-from freqtrade.resolvers import StrategyResolver
+from freqtrade.configuration import Arguments
+from freqtrade.configuration.arguments import ARGS_PLOT_DATAFRAME
+from freqtrade.data.btanalysis import extract_trades_of_period
+from freqtrade.optimize import setup_configuration
+from freqtrade.plot.plotting import (init_plotscript, generate_candlestick_graph,
+                                     store_plot_file,
+                                     generate_plot_filename)
+from freqtrade.state import RunMode
 
 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, tick_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 + '-' + tick_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}
-    """
-
-    tick_interval = strategy.ticker_interval
-    timerange = Arguments.parse_timerange(args.timerange)
-
-    tickers = {}
-    if args.live:
-        logger.info('Downloading pairs.')
-        exchange.refresh_latest_ohlcv([(pair, tick_interval) for pair in pairs])
-        for pair in pairs:
-            tickers[pair] = exchange.klines((pair, tick_interval))
-    else:
-        tickers = history.load_data(
-            datadir=Path(_CONF.get("datadir")),
-            pairs=pairs,
-            ticker_interval=tick_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:
-    """
-    Get tickers then Populate strategy indicators and signals, then return the full dataframe
-    :return: the DataFrame of a pair
-    """
-
-    dataframes = strategy.tickerdata_to_dataframe(tickers)
-    dataframe = dataframes[pair]
-    dataframe = strategy.advise_buy(dataframe, {'pair': pair})
-    dataframe = strategy.advise_sell(dataframe, {'pair': pair})
-
-    return dataframe
-
-
-def extract_trades_of_period(dataframe, trades) -> 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']]
-    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):
+def analyse_and_plot_pairs(config: Dict[str, Any]):
     """
     From arguments provided in cli:
     -Initialise backtest env
@@ -398,37 +39,50 @@ def analyse_and_plot_pairs(args: Namespace):
     -Generate plot files
     :return: None
     """
-    strategy, exchange, pairs = get_trading_env(args)
-    # Set timerange to use
-    timerange = Arguments.parse_timerange(args.timerange)
-    tick_interval = strategy.ticker_interval
+    plot_elements = init_plotscript(config)
+    trades = plot_elements['trades']
+    strategy = plot_elements["strategy"]
 
-    tickers = get_tickers_data(strategy, exchange, pairs, args)
     pair_counter = 0
-    for pair, data in tickers.items():
+    for pair, data in plot_elements["tickers"].items():
         pair_counter += 1
         logger.info("analyse pair %s", pair)
         tickers = {}
         tickers[pair] = data
-        dataframe = generate_dataframe(strategy, tickers, pair)
 
-        trades = load_trades(args, pair, timerange)
-        trades = extract_trades_of_period(dataframe, trades)
+        dataframe = strategy.analyze_ticker(tickers[pair], {'pair': pair})
 
-        fig = generate_graph(
+        trades_pair = trades.loc[trades['pair'] == pair]
+        trades_pair = extract_trades_of_period(dataframe, trades_pair)
+
+        fig = generate_candlestick_graph(
             pair=pair,
-            trades=trades,
             data=dataframe,
-            indicators1=args.indicators1,
-            indicators2=args.indicators2
+            trades=trades_pair,
+            indicators1=config["indicators1"].split(","),
+            indicators2=config["indicators2"].split(",")
         )
 
-        is_last = (False, True)[pair_counter == len(tickers)]
-        generate_plot_file(fig, pair, tick_interval, is_last)
+        store_plot_file(fig, generate_plot_filename(pair, config['ticker_interval']))
 
     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._build_args(optionlist=ARGS_PLOT_DATAFRAME)
+    parsed_args = arguments._parse_args()
+
+    # Load the configuration
+    config = setup_configuration(parsed_args, RunMode.OTHER)
+    return config
+
+
 def main(sysargv: List[str]) -> None:
     """
     This function will initiate the bot and start the trading loop.
diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py
index e2f85932f..4290bca45 100755
--- a/scripts/plot_profit.py
+++ b/scripts/plot_profit.py
@@ -2,216 +2,52 @@
 """
 Script to display profits
 
-Mandatory Cli parameters:
--p / --pair: pair to examine
-
-Optional Cli parameters
--c / --config: specify configuration file
--s / --strategy: strategy to use
--d / --datadir: path to pair backtest data
---timerange: specify what timerange of data to use
---export-filename: Specify where the backtest export is located.
+Use `python plot_profit.py --help` to display the command line arguments
 """
 import logging
 import sys
-import json
-from argparse import Namespace
-from pathlib import Path
-from typing import List, Optional
-import numpy as np
+from typing import Any, Dict, List
 
-from plotly import tools
-from plotly.offline import plot
-import plotly.graph_objs as go
-
-from freqtrade.arguments import Arguments
-from freqtrade.configuration import Configuration
-from freqtrade import constants
-from freqtrade.data import history
-from freqtrade.resolvers import StrategyResolver
+from freqtrade.configuration import Arguments
+from freqtrade.configuration.arguments import ARGS_PLOT_PROFIT
+from freqtrade.optimize import setup_configuration
+from freqtrade.plot.plotting import init_plotscript, generate_profit_graph, store_plot_file
 from freqtrade.state import RunMode
-import freqtrade.misc as misc
-
 
 logger = logging.getLogger(__name__)
 
 
-# data:: [ pair,      profit-%,  enter,         exit,        time, duration]
-# data:: ["ETH/BTC", 0.0023975, "1515598200", "1515602100", "2018-01-10 07:30:00+00:00", 65]
-def make_profit_array(data: List, px: int, min_date: int,
-                      interval: int,
-                      filter_pairs: Optional[List] = None) -> np.ndarray:
-    pg = np.zeros(px)
-    filter_pairs = filter_pairs or []
-    # Go through the trades
-    # and make an total profit
-    # array
-    for trade in data:
-        pair = trade[0]
-        if filter_pairs and pair not in filter_pairs:
-            continue
-        profit = trade[1]
-        trade_sell_time = int(trade[3])
-
-        ix = define_index(min_date, trade_sell_time, interval)
-        if ix < px:
-            logger.debug('[%s]: Add profit %s on %s', pair, profit, trade[4])
-            pg[ix] += profit
-
-    # rewrite the pg array to go from
-    # total profits at each timeframe
-    # to accumulated profits
-    pa = 0
-    for x in range(0, len(pg)):
-        p = pg[x]  # Get current total percent
-        pa += p  # Add to the accumulated percent
-        pg[x] = pa  # write back to save memory
-
-    return pg
-
-
-def plot_profit(args: Namespace) -> None:
+def plot_profit(config: Dict[str, Any]) -> None:
     """
     Plots the total profit for all pairs.
     Note, the profit calculation isn't realistic.
     But should be somewhat proportional, and therefor useful
     in helping out to find a good algorithm.
     """
+    plot_elements = init_plotscript(config)
+    trades = plot_elements['trades']
+    # Filter trades to relevant pairs
+    trades = trades[trades['pair'].isin(plot_elements["pairs"])]
 
-    # We need to use the same pairs, same tick_interval
-    # and same timeperiod as used in backtesting
-    # to match the tickerdata against the profits-results
-    timerange = Arguments.parse_timerange(args.timerange)
-
-    config = Configuration(args, RunMode.OTHER).get_config()
-
-    # Init strategy
-    try:
-        strategy = StrategyResolver({'strategy': config.get('strategy')}).strategy
-
-    except AttributeError:
-        logger.critical(
-            'Impossible to load the strategy. Please check the file "user_data/strategies/%s.py"',
-            config.get('strategy')
-        )
-        exit(1)
-
-    # Load the profits results
-    try:
-        filename = args.exportfilename
-        with open(filename) as file:
-            data = json.load(file)
-    except FileNotFoundError:
-        logger.critical(
-            'File "backtest-result.json" not found. This script require backtesting '
-            'results to run.\nPlease run a backtesting with the parameter --export.')
-        exit(1)
-
-    # Take pairs from the cli otherwise switch to the pair in the config file
-    if args.pairs:
-        filter_pairs = args.pairs
-        filter_pairs = filter_pairs.split(',')
-    else:
-        filter_pairs = config['exchange']['pair_whitelist']
-
-    tick_interval = strategy.ticker_interval
-    pairs = config['exchange']['pair_whitelist']
-
-    if filter_pairs:
-        pairs = list(set(pairs) & set(filter_pairs))
-        logger.info('Filter, keep pairs %s' % pairs)
-
-    tickers = history.load_data(
-        datadir=Path(config.get('datadir')),
-        pairs=pairs,
-        ticker_interval=tick_interval,
-        refresh_pairs=False,
-        timerange=timerange
-    )
-    dataframes = strategy.tickerdata_to_dataframe(tickers)
-
-    # NOTE: the dataframes are of unequal length,
-    # 'dates' is an merged date array of them all.
-
-    dates = misc.common_datearray(dataframes)
-    min_date = int(min(dates).timestamp())
-    max_date = int(max(dates).timestamp())
-    num_iterations = define_index(min_date, max_date, tick_interval) + 1
-
-    # Make an average close price of all the pairs that was involved.
+    # Create an average close price of all the pairs that were involved.
     # this could be useful to gauge the overall market trend
-    # We are essentially saying:
-    #  array <- sum dataframes[*]['close'] / num_items dataframes
-    #  FIX: there should be some onliner numpy/panda for this
-    avgclose = np.zeros(num_iterations)
-    num = 0
-    for pair, pair_data in dataframes.items():
-        close = pair_data['close']
-        maxprice = max(close)  # Normalize price to [0,1]
-        logger.info('Pair %s has length %s' % (pair, len(close)))
-        for x in range(0, len(close)):
-            avgclose[x] += close[x] / maxprice
-        # avgclose += close
-        num += 1
-    avgclose /= num
-
-    # make an profits-growth array
-    pg = make_profit_array(data, num_iterations, min_date, tick_interval, filter_pairs)
-
-    #
-    # Plot the pairs average close prices, and total profit growth
-    #
-
-    avgclose = go.Scattergl(
-        x=dates,
-        y=avgclose,
-        name='Avg close price',
-    )
-
-    profit = go.Scattergl(
-        x=dates,
-        y=pg,
-        name='Profit',
-    )
-
-    fig = tools.make_subplots(rows=3, cols=1, shared_xaxes=True, row_width=[1, 1, 1])
-
-    fig.append_trace(avgclose, 1, 1)
-    fig.append_trace(profit, 2, 1)
-
-    for pair in pairs:
-        pg = make_profit_array(data, num_iterations, min_date, tick_interval, pair)
-        pair_profit = go.Scattergl(
-            x=dates,
-            y=pg,
-            name=pair,
-        )
-        fig.append_trace(pair_profit, 3, 1)
-
-    plot(fig, filename=str(Path('user_data').joinpath('freqtrade-profit-plot.html')))
+    fig = generate_profit_graph(plot_elements["pairs"], plot_elements["tickers"], trades)
+    store_plot_file(fig, filename='freqtrade-profit-plot.html', auto_open=True)
 
 
-def define_index(min_date: int, max_date: int, interval: str) -> int:
-    """
-    Return the index of a specific date
-    """
-    interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval]
-    return int((max_date - min_date) / (interval_minutes * 60))
-
-
-def plot_parse_args(args: List[str]) -> Namespace:
+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 profits')
-    arguments.scripts_options()
-    arguments.common_args_parser()
-    arguments.optimizer_shared_options(arguments.parser)
-    arguments.backtesting_options(arguments.parser)
+    arguments._build_args(optionlist=ARGS_PLOT_PROFIT)
+    parsed_args = arguments._parse_args()
 
-    return arguments.parse_args()
+    # Load the configuration
+    config = setup_configuration(parsed_args, RunMode.OTHER)
+    return config
 
 
 def main(sysargv: List[str]) -> None:
diff --git a/scripts/rest_client.py b/scripts/rest_client.py
new file mode 100755
index 000000000..a46b3ebfb
--- /dev/null
+++ b/scripts/rest_client.py
@@ -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)
diff --git a/setup.py b/setup.py
index 35fdb2938..41e1b8f45 100644
--- a/setup.py
+++ b/setup.py
@@ -8,6 +8,30 @@ if version_info.major == 3 and version_info.minor < 6 or \
 
 from freqtrade import __version__
 
+# Requirements used for submodules
+api = ['flask']
+plot = ['plotly>=4.0']
+
+develop = [
+    'coveralls',
+    'flake8',
+    'flake8-type-annotations',
+    'flake8-tidy-imports',
+    'mypy',
+    'pytest',
+    'pytest-asyncio',
+    'pytest-cov',
+    'pytest-mock',
+    'pytest-random-order',
+]
+
+jupyter = [
+    'jupyter',
+    'nbstripout',
+    'ipykernel',
+    ]
+
+all_extra = api + plot + develop + jupyter
 
 setup(name='freqtrade',
       version=__version__,
@@ -17,32 +41,49 @@ setup(name='freqtrade',
       author_email='michael.egger@tsn.at',
       license='GPLv3',
       packages=['freqtrade'],
-      scripts=['bin/freqtrade'],
       setup_requires=['pytest-runner', 'numpy'],
       tests_require=['pytest', 'pytest-mock', 'pytest-cov'],
       install_requires=[
-          'ccxt',
+          # from requirements-common.txt
+          'ccxt>=1.18',
           'SQLAlchemy',
           'python-telegram-bot',
           'arrow',
+          'cachetools',
           'requests',
           'urllib3',
           'wrapt',
-          'pandas',
           'scikit-learn',
-          'scipy',
           'joblib',
           'jsonschema',
           'TA-Lib',
           'tabulate',
-          'cachetools',
           'coinmarketcap',
           'scikit-optimize',
+          'filelock',
+          'py_find_1st',
           'python-rapidjson',
-          'py_find_1st'
+          'sdnotify',
+          # from requirements.txt
+          'numpy',
+          'pandas',
+          'scipy',
       ],
+      extras_require={
+          'api': api,
+          'dev': all_extra,
+          'plot': plot,
+          'all': all_extra,
+          'jupyter': jupyter,
+          
+      },
       include_package_data=True,
       zip_safe=False,
+      entry_points={
+          'console_scripts': [
+              'freqtrade = freqtrade.main:main',
+          ],
+      },
       classifiers=[
           'Programming Language :: Python :: 3.6',
           'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
diff --git a/setup.sh b/setup.sh
index 66d449037..c4b6e074a 100755
--- a/setup.sh
+++ b/setup.sh
@@ -1,12 +1,27 @@
 #!/usr/bin/env bash
 #encoding=utf8
 
+function check_installed_pip() {
+   ${PYTHON} -m pip > /dev/null
+   if [ $? -ne 0 ]; then
+        echo "pip not found (called as '${PYTHON} -m pip'). Please make sure that pip is available for ${PYTHON}."
+        exit 1
+   fi
+}
+
 # Check which python version is installed
 function check_installed_python() {
+    if [ -n "${VIRTUAL_ENV}" ]; then
+        echo "Please deactivate your virtual environment before running setup.sh."
+        echo "You can do this by running 'deactivate'."
+        exit 2
+    fi
+
     which python3.7
     if [ $? -eq 0 ]; then
         echo "using Python 3.7"
         PYTHON=python3.7
+        check_installed_pip
         return
     fi
 
@@ -14,6 +29,7 @@ function check_installed_python() {
     if [ $? -eq 0 ]; then
         echo "using Python 3.6"
         PYTHON=python3.6
+        check_installed_pip
         return
    fi
 
@@ -21,29 +37,30 @@ function check_installed_python() {
         echo "No usable python found. Please make sure to have python3.6 or python3.7 installed"
         exit 1
    fi
-
 }
 
 function updateenv() {
     echo "-------------------------"
     echo "Updating your virtual env"
     echo "-------------------------"
+    if [ ! -f .env/bin/activate ]; then
+        echo "Something went wrong, no virtual environment found."
+        exit 1
+    fi
     source .env/bin/activate
-    echo "pip3 install in-progress. Please wait..."
-    # Install numpy first to have py_find_1st install clean
-    pip3 install --upgrade pip numpy
-    pip3 install --upgrade -r requirements.txt
-
+    echo "pip install in-progress. Please wait..."
+    ${PYTHON} -m pip install --upgrade pip
     read -p "Do you want to install dependencies for dev [y/N]? "
     if [[ $REPLY =~ ^[Yy]$ ]]
     then
-        pip3 install --upgrade -r requirements-dev.txt
+        ${PYTHON} -m pip install --upgrade -r requirements-dev.txt
     else
+        ${PYTHON} -m pip install --upgrade -r requirements.txt
         echo "Dev dependencies ignored."
     fi
 
-    pip3 install --quiet -e .
-    echo "pip3 install completed"
+    ${PYTHON} -m pip install -e .
+    echo "pip install completed"
     echo
 }
 
@@ -61,6 +78,10 @@ function install_talib() {
     ./configure --prefix=/usr/local
     make
     sudo make install
+    if [ -x "$(command -v apt-get)" ]; then
+        echo "Updating library path using ldconfig"
+        sudo ldconfig
+    fi
     cd .. && rm -rf ./ta-lib/
     cd ..
 }
@@ -74,16 +95,14 @@ function install_macos() {
         echo "-------------------------"
         /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
     fi
-    brew install python3 wget
     install_talib
     test_and_fix_python_on_mac
 }
 
 # Install bot Debian_ubuntu
 function install_debian() {
-    sudo add-apt-repository ppa:jonathonf/python-3.6
     sudo apt-get update
-    sudo apt-get install python3.6 python3.6-venv python3.6-dev build-essential autoconf libtool pkg-config make wget git
+    sudo apt-get install -y build-essential autoconf libtool pkg-config make wget git
     install_talib
 }
 
@@ -98,30 +117,39 @@ function reset() {
     echo "----------------------------"
     echo "Reseting branch and virtual env"
     echo "----------------------------"
+
     if [ "1" == $(git branch -vv |grep -cE "\* develop|\* master") ]
     then
-        if [ -d ".env" ]; then
-          echo "- Delete your previous virtual env"
-          rm -rf .env
-        fi
 
-        git fetch -a
+        read -p "Reset git branch? (This will remove all changes you made!) [y/N]? "
+        if [[ $REPLY =~ ^[Yy]$ ]]; then
 
-        if [ "1" == $(git branch -vv |grep -c "* develop") ]
-        then
-          echo "- Hard resetting of 'develop' branch."
-          git reset --hard origin/develop
-        elif [ "1" == $(git branch -vv |grep -c "* master") ]
-        then
-          echo "- Hard resetting of 'master' branch."
-          git reset --hard origin/master
+            git fetch -a
+
+            if [ "1" == $(git branch -vv |grep -c "* develop") ]
+            then
+                echo "- Hard resetting of 'develop' branch."
+                git reset --hard origin/develop
+            elif [ "1" == $(git branch -vv |grep -c "* master") ]
+            then
+                echo "- Hard resetting of 'master' branch."
+                git reset --hard origin/master
+            fi
         fi
     else
         echo "Reset ignored because you are not on 'master' or 'develop'."
     fi
 
+    if [ -d ".env" ]; then
+        echo "- Delete your previous virtual env"
+        rm -rf .env
+    fi
     echo
     ${PYTHON} -m venv .env
+    if [ $? -ne 0 ]; then
+        echo "Could not create virtual environment. Leaving now"
+        exit 1
+    fi
     updateenv
 }
 
@@ -235,7 +263,7 @@ function install() {
     echo "-------------------------"
     echo "Run the bot !"
     echo "-------------------------"
-    echo "You can now use the bot by executing 'source .env/bin/activate; python freqtrade/main.py'."
+    echo "You can now use the bot by executing 'source .env/bin/activate; freqtrade'."
 }
 
 function plot() {
@@ -244,7 +272,7 @@ echo "
 Installing dependencies for Plotting scripts
 -----------------------------------------
 "
-pip install plotly --upgrade
+${PYTHON} -m pip install plotly --upgrade
 }
 
 function help() {
diff --git a/user_data/hyperopts/sample_hyperopt.py b/user_data/hyperopts/sample_hyperopt.py
index 54f65a7e6..1a3823afa 100644
--- a/user_data/hyperopts/sample_hyperopt.py
+++ b/user_data/hyperopts/sample_hyperopt.py
@@ -1,33 +1,40 @@
 # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
 
+from functools import reduce
+from math import exp
+from typing import Any, Callable, Dict, List
+from datetime import datetime
+
+import numpy as np# noqa F401
 import talib.abstract as ta
 from pandas import DataFrame
-from typing import Dict, Any, Callable, List
-from functools import reduce
-
-import numpy
 from skopt.space import Categorical, Dimension, Integer, Real
 
 import freqtrade.vendor.qtpylib.indicators as qtpylib
 from freqtrade.optimize.hyperopt_interface import IHyperOpt
 
-class_name = 'SampleHyperOpts'
 
-
-# This class is a sample. Feel free to customize it.
 class SampleHyperOpts(IHyperOpt):
     """
-    This is a test hyperopt to inspire you.
-    More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md
-     You can:
-    - Rename the class name (Do not forget to update class_name)
-    - Add any methods you want to build your hyperopt
-    - Add any lib you need to build your hyperopt
-     You must keep:
-    - the prototype for the methods: populate_indicators, indicator_space, buy_strategy_generator,
-    roi_space, generate_roi_table, stoploss_space
-    """
+    This is a sample hyperopt to inspire you.
+    Feel free to customize it.
 
+    More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md
+
+    You should:
+    - Rename the class name to some unique name.
+    - Add any methods you want to build your hyperopt.
+    - Add any lib you need to build your hyperopt.
+
+    You must keep:
+    - The prototypes for the methods: populate_indicators, indicator_space, buy_strategy_generator.
+
+    The roi_space, generate_roi_table, stoploss_space methods are no longer required to be
+    copied in every custom hyperopt. However, you may override them if you need the
+    'roi' and the 'stoploss' spaces that differ from the defaults offered by Freqtrade.
+    Sample implementation of these methods can be found in
+    https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_advanced.py
+    """
     @staticmethod
     def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
         dataframe['adx'] = ta.ADX(dataframe)
@@ -79,9 +86,10 @@ class SampleHyperOpts(IHyperOpt):
                         dataframe['close'], dataframe['sar']
                     ))
 
-            dataframe.loc[
-                reduce(lambda x, y: x & y, conditions),
-                'buy'] = 1
+            if conditions:
+                dataframe.loc[
+                    reduce(lambda x, y: x & y, conditions),
+                    'buy'] = 1
 
             return dataframe
 
@@ -138,9 +146,10 @@ class SampleHyperOpts(IHyperOpt):
                         dataframe['sar'], dataframe['close']
                     ))
 
-            dataframe.loc[
-                reduce(lambda x, y: x & y, conditions),
-                'sell'] = 1
+            if conditions:
+                dataframe.loc[
+                    reduce(lambda x, y: x & y, conditions),
+                    'sell'] = 1
 
             return dataframe
 
@@ -165,42 +174,6 @@ class SampleHyperOpts(IHyperOpt):
                          'sell-sar_reversal'], name='sell-trigger')
         ]
 
-    @staticmethod
-    def generate_roi_table(params: Dict) -> Dict[int, float]:
-        """
-        Generate the ROI table that will be used by Hyperopt
-        """
-        roi_table = {}
-        roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3']
-        roi_table[params['roi_t3']] = params['roi_p1'] + params['roi_p2']
-        roi_table[params['roi_t3'] + params['roi_t2']] = params['roi_p1']
-        roi_table[params['roi_t3'] + params['roi_t2'] + params['roi_t1']] = 0
-
-        return roi_table
-
-    @staticmethod
-    def stoploss_space() -> List[Dimension]:
-        """
-        Stoploss Value to search
-        """
-        return [
-            Real(-0.5, -0.02, name='stoploss'),
-        ]
-
-    @staticmethod
-    def roi_space() -> List[Dimension]:
-        """
-        Values to search for each ROI steps
-        """
-        return [
-            Integer(10, 120, name='roi_t1'),
-            Integer(10, 60, name='roi_t2'),
-            Integer(10, 40, name='roi_t3'),
-            Real(0.01, 0.04, name='roi_p1'),
-            Real(0.01, 0.07, name='roi_p2'),
-            Real(0.01, 0.20, name='roi_p3'),
-        ]
-
     def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
         """
         Based on TA indicators. Should be a copy of from strategy
diff --git a/user_data/hyperopts/sample_hyperopt_advanced.py b/user_data/hyperopts/sample_hyperopt_advanced.py
new file mode 100644
index 000000000..00062a58d
--- /dev/null
+++ b/user_data/hyperopts/sample_hyperopt_advanced.py
@@ -0,0 +1,261 @@
+# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
+
+from functools import reduce
+from math import exp
+from typing import Any, Callable, Dict, List
+from datetime import datetime
+
+import numpy as np# noqa F401
+import talib.abstract as ta
+from pandas import DataFrame
+from skopt.space import Categorical, Dimension, Integer, Real
+
+import freqtrade.vendor.qtpylib.indicators as qtpylib
+from freqtrade.optimize.hyperopt_interface import IHyperOpt
+
+
+class AdvancedSampleHyperOpts(IHyperOpt):
+    """
+    This is a sample hyperopt to inspire you.
+    Feel free to customize it.
+
+    More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md
+
+    You should:
+    - Rename the class name to some unique name.
+    - Add any methods you want to build your hyperopt.
+    - Add any lib you need to build your hyperopt.
+
+    You must keep:
+    - The prototypes for the methods: populate_indicators, indicator_space, buy_strategy_generator.
+
+    The roi_space, generate_roi_table, stoploss_space methods are no longer required to be
+    copied in every custom hyperopt. However, you may override them if you need the
+    'roi' and the 'stoploss' spaces that differ from the defaults offered by Freqtrade.
+
+    This sample illustrates how to override these methods.
+    """
+    @staticmethod
+    def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
+        dataframe['adx'] = ta.ADX(dataframe)
+        macd = ta.MACD(dataframe)
+        dataframe['macd'] = macd['macd']
+        dataframe['macdsignal'] = macd['macdsignal']
+        dataframe['mfi'] = ta.MFI(dataframe)
+        dataframe['rsi'] = ta.RSI(dataframe)
+        stoch_fast = ta.STOCHF(dataframe)
+        dataframe['fastd'] = stoch_fast['fastd']
+        dataframe['minus_di'] = ta.MINUS_DI(dataframe)
+        # Bollinger bands
+        bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
+        dataframe['bb_lowerband'] = bollinger['lower']
+        dataframe['bb_upperband'] = bollinger['upper']
+        dataframe['sar'] = ta.SAR(dataframe)
+        return dataframe
+
+    @staticmethod
+    def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
+        """
+        Define the buy strategy parameters to be used by hyperopt
+        """
+        def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
+            """
+            Buy strategy Hyperopt will build and use
+            """
+            conditions = []
+            # GUARDS AND TRENDS
+            if 'mfi-enabled' in params and params['mfi-enabled']:
+                conditions.append(dataframe['mfi'] < params['mfi-value'])
+            if 'fastd-enabled' in params and params['fastd-enabled']:
+                conditions.append(dataframe['fastd'] < params['fastd-value'])
+            if 'adx-enabled' in params and params['adx-enabled']:
+                conditions.append(dataframe['adx'] > params['adx-value'])
+            if 'rsi-enabled' in params and params['rsi-enabled']:
+                conditions.append(dataframe['rsi'] < params['rsi-value'])
+
+            # TRIGGERS
+            if 'trigger' in params:
+                if params['trigger'] == 'bb_lower':
+                    conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
+                if params['trigger'] == 'macd_cross_signal':
+                    conditions.append(qtpylib.crossed_above(
+                        dataframe['macd'], dataframe['macdsignal']
+                    ))
+                if params['trigger'] == 'sar_reversal':
+                    conditions.append(qtpylib.crossed_above(
+                        dataframe['close'], dataframe['sar']
+                    ))
+
+            if conditions:
+                dataframe.loc[
+                    reduce(lambda x, y: x & y, conditions),
+                    'buy'] = 1
+
+            return dataframe
+
+        return populate_buy_trend
+
+    @staticmethod
+    def indicator_space() -> List[Dimension]:
+        """
+        Define your Hyperopt space for searching strategy parameters
+        """
+        return [
+            Integer(10, 25, name='mfi-value'),
+            Integer(15, 45, name='fastd-value'),
+            Integer(20, 50, name='adx-value'),
+            Integer(20, 40, name='rsi-value'),
+            Categorical([True, False], name='mfi-enabled'),
+            Categorical([True, False], name='fastd-enabled'),
+            Categorical([True, False], name='adx-enabled'),
+            Categorical([True, False], name='rsi-enabled'),
+            Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger')
+        ]
+
+    @staticmethod
+    def sell_strategy_generator(params: Dict[str, Any]) -> Callable:
+        """
+        Define the sell strategy parameters to be used by hyperopt
+        """
+        def populate_sell_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
+            """
+            Sell strategy Hyperopt will build and use
+            """
+            # print(params)
+            conditions = []
+            # GUARDS AND TRENDS
+            if 'sell-mfi-enabled' in params and params['sell-mfi-enabled']:
+                conditions.append(dataframe['mfi'] > params['sell-mfi-value'])
+            if 'sell-fastd-enabled' in params and params['sell-fastd-enabled']:
+                conditions.append(dataframe['fastd'] > params['sell-fastd-value'])
+            if 'sell-adx-enabled' in params and params['sell-adx-enabled']:
+                conditions.append(dataframe['adx'] < params['sell-adx-value'])
+            if 'sell-rsi-enabled' in params and params['sell-rsi-enabled']:
+                conditions.append(dataframe['rsi'] > params['sell-rsi-value'])
+
+            # TRIGGERS
+            if 'sell-trigger' in params:
+                if params['sell-trigger'] == 'sell-bb_upper':
+                    conditions.append(dataframe['close'] > dataframe['bb_upperband'])
+                if params['sell-trigger'] == 'sell-macd_cross_signal':
+                    conditions.append(qtpylib.crossed_above(
+                        dataframe['macdsignal'], dataframe['macd']
+                    ))
+                if params['sell-trigger'] == 'sell-sar_reversal':
+                    conditions.append(qtpylib.crossed_above(
+                        dataframe['sar'], dataframe['close']
+                    ))
+
+            if conditions:
+                dataframe.loc[
+                    reduce(lambda x, y: x & y, conditions),
+                    'sell'] = 1
+
+            return dataframe
+
+        return populate_sell_trend
+
+    @staticmethod
+    def sell_indicator_space() -> List[Dimension]:
+        """
+        Define your Hyperopt space for searching sell strategy parameters
+        """
+        return [
+            Integer(75, 100, name='sell-mfi-value'),
+            Integer(50, 100, name='sell-fastd-value'),
+            Integer(50, 100, name='sell-adx-value'),
+            Integer(60, 100, name='sell-rsi-value'),
+            Categorical([True, False], name='sell-mfi-enabled'),
+            Categorical([True, False], name='sell-fastd-enabled'),
+            Categorical([True, False], name='sell-adx-enabled'),
+            Categorical([True, False], name='sell-rsi-enabled'),
+            Categorical(['sell-bb_upper',
+                         'sell-macd_cross_signal',
+                         'sell-sar_reversal'], name='sell-trigger')
+        ]
+
+    @staticmethod
+    def generate_roi_table(params: Dict) -> Dict[int, float]:
+        """
+        Generate the ROI table that will be used by Hyperopt
+
+        This implementation generates the default legacy Freqtrade ROI tables.
+
+        Change it if you need different number of steps in the generated
+        ROI tables or other structure of the ROI tables.
+
+        Please keep it aligned with parameters in the 'roi' optimization
+        hyperspace defined by the roi_space method.
+        """
+        roi_table = {}
+        roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3']
+        roi_table[params['roi_t3']] = params['roi_p1'] + params['roi_p2']
+        roi_table[params['roi_t3'] + params['roi_t2']] = params['roi_p1']
+        roi_table[params['roi_t3'] + params['roi_t2'] + params['roi_t1']] = 0
+
+        return roi_table
+
+    @staticmethod
+    def roi_space() -> List[Dimension]:
+        """
+        Values to search for each ROI steps
+
+        Override it if you need some different ranges for the parameters in the
+        'roi' optimization hyperspace.
+
+        Please keep it aligned with the implementation of the
+        generate_roi_table method.
+        """
+        return [
+            Integer(10, 120, name='roi_t1'),
+            Integer(10, 60, name='roi_t2'),
+            Integer(10, 40, name='roi_t3'),
+            Real(0.01, 0.04, name='roi_p1'),
+            Real(0.01, 0.07, name='roi_p2'),
+            Real(0.01, 0.20, name='roi_p3'),
+        ]
+
+    @staticmethod
+    def stoploss_space() -> List[Dimension]:
+        """
+        Stoploss Value to search
+
+        Override it if you need some different range for the parameter in the
+        'stoploss' optimization hyperspace.
+        """
+        return [
+            Real(-0.5, -0.02, name='stoploss'),
+        ]
+
+    def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
+        """
+        Based on TA indicators. Should be a copy of from strategy
+        must align to populate_indicators in this file
+        Only used when --spaces does not include buy
+        """
+        dataframe.loc[
+            (
+                (dataframe['close'] < dataframe['bb_lowerband']) &
+                (dataframe['mfi'] < 16) &
+                (dataframe['adx'] > 25) &
+                (dataframe['rsi'] < 21)
+            ),
+            'buy'] = 1
+
+        return dataframe
+
+    def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
+        """
+        Based on TA indicators. Should be a copy of from strategy
+        must align to populate_indicators in this file
+        Only used when --spaces does not include sell
+        """
+        dataframe.loc[
+            (
+                (qtpylib.crossed_above(
+                    dataframe['macdsignal'], dataframe['macd']
+                )) &
+                (dataframe['fastd'] > 54)
+            ),
+            'sell'] = 1
+        return dataframe
diff --git a/user_data/hyperopts/sample_hyperopt_loss.py b/user_data/hyperopts/sample_hyperopt_loss.py
new file mode 100644
index 000000000..5a2fb72b6
--- /dev/null
+++ b/user_data/hyperopts/sample_hyperopt_loss.py
@@ -0,0 +1,47 @@
+from math import exp
+from datetime import datetime
+
+from pandas import DataFrame
+
+from freqtrade.optimize.hyperopt import IHyperOptLoss
+
+# Define some constants:
+
+# set TARGET_TRADES to suit your number concurrent trades so its realistic
+# to the number of days
+TARGET_TRADES = 600
+# This is assumed to be expected avg profit * expected trade count.
+# For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades,
+# 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Σ%.
+EXPECTED_MAX_PROFIT = 3.0
+
+# max average trade duration in minutes
+# if eval ends with higher value, we consider it a failed eval
+MAX_ACCEPTED_TRADE_DURATION = 300
+
+
+class SampleHyperOptLoss(IHyperOptLoss):
+    """
+    Defines the default loss function for hyperopt
+    This is intended to give you some inspiration for your own loss function.
+
+    The Function needs to return a number (float) - which becomes for better backtest results.
+    """
+
+    @staticmethod
+    def hyperopt_loss_function(results: DataFrame, trade_count: int,
+                               min_date: datetime, max_date: datetime,
+                               *args, **kwargs) -> float:
+        """
+        Objective function, returns smaller number for better results
+        """
+        total_profit = results.profit_percent.sum()
+        trade_duration = results.trade_duration.mean()
+
+        trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8)
+        profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT)
+        duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1)
+        result = trade_loss + profit_loss + duration_loss
+        return result
diff --git a/user_data/notebooks/.gitkeep b/user_data/notebooks/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/user_data/notebooks/analysis_example.ipynb b/user_data/notebooks/analysis_example.ipynb
new file mode 100644
index 000000000..f5e2c12d7
--- /dev/null
+++ b/user_data/notebooks/analysis_example.ipynb
@@ -0,0 +1,243 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Analyzing bot data\n",
+    "\n",
+    "You can analyze the results of backtests and trading history easily using Jupyter notebooks.  \n",
+    "**Copy this file so your changes don't get clobbered with the next freqtrade update!**  \n",
+    "For usage instructions, see [jupyter.org](https://jupyter.org/documentation).  \n",
+    "*Pro tip - Don't forget to start a jupyter notbook server from within your conda or venv environment or use [nb_conda_kernels](https://github.com/Anaconda-Platform/nb_conda_kernels)*\n",
+    "\n",
+    "\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Imports\n",
+    "from pathlib import Path\n",
+    "import os\n",
+    "from freqtrade.data.history import load_pair_history\n",
+    "from freqtrade.resolvers import StrategyResolver\n",
+    "from freqtrade.data.btanalysis import load_backtest_data\n",
+    "from freqtrade.data.btanalysis import load_trades_from_db"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Change directory\n",
+    "# Define all paths relative to the project root shown in the cell output\n",
+    "try:\n",
+    "    os.chdir(Path(Path.cwd(), '../..'))\n",
+    "    print(Path.cwd())\n",
+    "except:\n",
+    "    pass"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Example snippets"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Load backtest results into a pandas dataframe"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Load backtest results\n",
+    "df = load_backtest_data(\"user_data/backtest_data/backtest-result.json\")\n",
+    "\n",
+    "# Show value-counts per pair\n",
+    "df.groupby(\"pair\")[\"sell_reason\"].value_counts()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Load live trading results into a pandas dataframe"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Fetch trades from database\n",
+    "df = load_trades_from_db(\"sqlite:///tradesv3.sqlite\")\n",
+    "\n",
+    "# Display results\n",
+    "df.groupby(\"pair\")[\"sell_reason\"].value_counts()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Strategy debugging example\n",
+    "\n",
+    "Debugging a strategy can be time-consuming. FreqTrade offers helper functions to visualize raw data."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Import requirements and define variables used in analyses"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Define some constants\n",
+    "ticker_interval = \"5m\"\n",
+    "# Name of the strategy class\n",
+    "strategy_name = 'AwesomeStrategy'\n",
+    "# Path to user data\n",
+    "user_data_dir = 'user_data'\n",
+    "# Location of the strategy\n",
+    "strategy_location = Path(user_data_dir, 'strategies')\n",
+    "# Location of the data\n",
+    "data_location = Path(user_data_dir, 'data', 'binance')\n",
+    "# Pair to analyze \n",
+    "# Only use one pair here\n",
+    "pair = \"BTC_USDT\""
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Load exchange data"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Load data using values set above\n",
+    "bt_data = load_pair_history(datadir=Path(data_location),\n",
+    "                            ticker_interval=ticker_interval,\n",
+    "                            pair=pair)\n",
+    "\n",
+    "# Confirm success\n",
+    "print(\"Loaded \" + str(len(bt_data)) + f\" rows of data for {pair} from {data_location}\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Load and run strategy\n",
+    "* Rerun each time the strategy file is changed"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Load strategy using values set above\n",
+    "strategy = StrategyResolver({'strategy': strategy_name,\n",
+    "                            'user_data_dir': user_data_dir,\n",
+    "                            'strategy_path': strategy_location}).strategy\n",
+    "\n",
+    "# Generate buy/sell signals using strategy\n",
+    "df = strategy.analyze_ticker(bt_data, {'pair': pair})"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Display the trade details\n",
+    "* Note that using `data.head()` would also work, however most indicators have some \"startup\" data at the top of the dataframe.\n",
+    "\n",
+    "#### Some possible problems\n",
+    "\n",
+    "* Columns with NaN values at the end of the dataframe\n",
+    "* Columns used in `crossed*()` functions with completely different units\n",
+    "\n",
+    "#### Comparison with full backtest\n",
+    "\n",
+    "having 200 buy signals as output for one pair from `analyze_ticker()` does not necessarily mean that 200 trades will be made during backtesting.\n",
+    "\n",
+    "Assuming you use only one condition such as, `df['rsi'] < 30` as buy condition, this will generate multiple \"buy\" signals for each pair in sequence (until rsi returns > 29).\n",
+    "The bot will only buy on the first of these signals (and also only if a trade-slot (\"max_open_trades\") is still available), or on one of the middle signals, as soon as a \"slot\" becomes available.\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Report results\n",
+    "print(f\"Generated {df['buy'].sum()} buy signals\")\n",
+    "data = df.set_index('date', drop=True)\n",
+    "data.tail()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "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."
+   ]
+  }
+ ],
+ "metadata": {
+  "file_extension": ".py",
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.7.3"
+  },
+  "mimetype": "text/x-python",
+  "name": "python",
+  "npconvert_exporter": "python",
+  "pygments_lexer": "ipython3",
+  "version": 3
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/user_data/strategies/test_strategy.py b/user_data/strategies/test_strategy.py
index 3cb78842f..d8ff790b2 100644
--- a/user_data/strategies/test_strategy.py
+++ b/user_data/strategies/test_strategy.py
@@ -44,14 +44,14 @@ class TestStrategy(IStrategy):
 
     # trailing stoploss
     trailing_stop = False
-    trailing_stop_positive = 0.01
-    trailing_stop_positive_offset = 0.0  # Disabled / not configured
+    # trailing_stop_positive = 0.01
+    # trailing_stop_positive_offset = 0.0  # Disabled / not configured
 
     # Optimal ticker interval for the strategy
     ticker_interval = '5m'
 
     # run "populate_indicators" only for new candle
-    ta_on_candle = False
+    process_only_new_candles = False
 
     # Experimental settings (configuration will overide these if set)
     use_sell_signal = False
@@ -253,6 +253,17 @@ class TestStrategy(IStrategy):
         dataframe['ha_low'] = heikinashi['low']
         """
 
+        # Retrieve best bid and best ask
+        # ------------------------------------
+        """
+        # first check if dataprovider is available 
+        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]
+        """
+        
         return dataframe
 
     def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: