Merge pull request #2070 from freqtrade/new_release

New release 2019.7
This commit is contained in:
Matthias 2019-07-30 06:19:43 +02:00 committed by GitHub
commit 0d9d23a888
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
84 changed files with 2647 additions and 1871 deletions

View File

@ -19,7 +19,6 @@ addons:
install: install:
- cd build_helpers && ./install_ta-lib.sh; cd .. - cd build_helpers && ./install_ta-lib.sh; cd ..
- export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH - export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
- pip install --upgrade pytest-random-order
- pip install -r requirements-dev.txt - pip install -r requirements-dev.txt
- pip install -e . - pip install -e .
jobs: jobs:
@ -27,17 +26,17 @@ jobs:
include: include:
- stage: tests - stage: tests
script: script:
- pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/ - pytest --random-order --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/
# Allow failure for coveralls # Allow failure for coveralls
- coveralls || true - coveralls || true
name: pytest name: pytest
- script: - script:
- cp config.json.example config.json - cp config.json.example config.json
- python freqtrade --datadir freqtrade/tests/testdata backtesting - freqtrade --datadir freqtrade/tests/testdata backtesting
name: backtest name: backtest
- script: - script:
- cp config.json.example config.json - cp config.json.example config.json
- python freqtrade --datadir freqtrade/tests/testdata hyperopt -e 5 - freqtrade --datadir freqtrade/tests/testdata hyperopt -e 5
name: hyperopt name: hyperopt
- script: flake8 freqtrade scripts - script: flake8 freqtrade scripts
name: flake8 name: flake8
@ -56,4 +55,4 @@ notifications:
cache: cache:
pip: True pip: True
directories: directories:
- /usr/local/lib - /usr/local/lib/

View File

@ -3,9 +3,7 @@
import sys import sys
import warnings import warnings
from freqtrade.main import main, set_loggers from freqtrade.main import main
set_loggers()
warnings.warn( warnings.warn(
"Deprecated - To continue to run the bot like this, please run `pip install -e .` again.", "Deprecated - To continue to run the bot like this, please run `pip install -e .` again.",

View File

@ -123,5 +123,5 @@
"process_throttle_secs": 5 "process_throttle_secs": 5
}, },
"strategy": "DefaultStrategy", "strategy": "DefaultStrategy",
"strategy_path": "/some/folder/" "strategy_path": "user_data/strategies/"
} }

View File

@ -13,7 +13,7 @@ Backtesting will use the crypto-currencies (pair) from your config file
and load static tickers located in and load static tickers located in
[/freqtrade/tests/testdata](https://github.com/freqtrade/freqtrade/tree/develop/freqtrade/tests/testdata). [/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 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. 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. The result of backtesting will confirm you if your bot has better odds of making a profit than a loss.
@ -24,37 +24,37 @@ The backtesting is very easy with freqtrade.
#### With 5 min tickers (Per default) #### With 5 min tickers (Per default)
```bash ```bash
python3 freqtrade backtesting freqtrade backtesting
``` ```
#### With 1 min tickers #### With 1 min tickers
```bash ```bash
python3 freqtrade backtesting --ticker-interval 1m freqtrade backtesting --ticker-interval 1m
``` ```
#### Update cached pairs with the latest data #### Update cached pairs with the latest data
```bash ```bash
python3 freqtrade backtesting --refresh-pairs-cached freqtrade backtesting --refresh-pairs-cached
``` ```
#### With live data (do not alter your testdata files) #### With live data (do not alter your testdata files)
```bash ```bash
python3 freqtrade backtesting --live freqtrade backtesting --live
``` ```
#### Using a different on-disk ticker-data source #### Using a different on-disk ticker-data source
```bash ```bash
python3 freqtrade backtesting --datadir freqtrade/tests/testdata-20180101 freqtrade backtesting --datadir freqtrade/tests/testdata-20180101
``` ```
#### With a (custom) strategy file #### With a (custom) strategy file
```bash ```bash
python3 freqtrade -s TestStrategy backtesting freqtrade -s TestStrategy backtesting
``` ```
Where `-s TestStrategy` refers to the class name within the strategy file `test_strategy.py` found in the `freqtrade/user_data/strategies` directory Where `-s TestStrategy` refers to the class name within the strategy file `test_strategy.py` found in the `freqtrade/user_data/strategies` directory
@ -62,15 +62,15 @@ Where `-s TestStrategy` refers to the class name within the strategy file `test_
#### Exporting trades to file #### Exporting trades to file
```bash ```bash
python3 freqtrade 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 #### Exporting trades to file specifying a custom filename
```bash ```bash
python3 freqtrade backtesting --export trades --export-filename=backtest_teststrategy.json freqtrade backtesting --export trades --export-filename=backtest_teststrategy.json
``` ```
#### Running backtest with smaller testset #### Running backtest with smaller testset
@ -81,7 +81,7 @@ you want to use. The last N ticks/timeframes will be used.
Example: Example:
```bash ```bash
python3 freqtrade backtesting --timerange=-200 freqtrade backtesting --timerange=-200
``` ```
#### Advanced use of timerange #### Advanced use of timerange
@ -107,7 +107,7 @@ To download new set of backtesting ticker data, you can use a download script.
If you are using Binance for example: 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. - update the `pairs.json` to contain the currency pairs you are interested in.
```bash ```bash
@ -123,9 +123,9 @@ python scripts/download_backtest_data.py --exchange binance
This will download ticker data for all the currency pairs you defined in `pairs.json`. This will download ticker data for all the currency pairs you defined in `pairs.json`.
- To use a different folder than the exchange specific default, use `--datadir 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 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`. - To download ticker data for only 10 days, use `--days 10`.
- Use `--timeframes` to specify which tickers to download. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute tickers. - Use `--timeframes` to specify which tickers to download. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute tickers.
- To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with other options. - 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.
@ -231,7 +231,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 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. 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 ``` bash
freqtrade backtesting --timerange 20180401-20180410 --ticker-interval 5m --strategy-list Strategy001 Strategy002 --export trades freqtrade backtesting --timerange 20180401-20180410 --ticker-interval 5m --strategy-list Strategy001 Strategy002 --export trades

View File

@ -2,13 +2,16 @@
This page explains the different parameters of the bot and how to run it. This page explains the different parameters of the bot and how to run it.
!Note:
If you've used `setup.sh`, don't forget to activate your virtual environment (`source .env/bin/activate`) before running freqtrade commands.
## Bot commands ## Bot commands
``` ```
usage: freqtrade [-h] [-v] [--logfile FILE] [--version] [-c PATH] [-d PATH] usage: freqtrade [-h] [-v] [--logfile FILE] [--version] [-c PATH] [-d PATH]
[-s NAME] [--strategy-path PATH] [--dynamic-whitelist [INT]] [-s NAME] [--strategy-path PATH] [--db-url PATH]
[--db-url PATH] [--sd-notify] [--sd-notify]
{backtesting,edge,hyperopt} ... {backtesting,edge,hyperopt} ...
Free, open source crypto trading bot Free, open source crypto trading bot
@ -23,7 +26,7 @@ optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages). -v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
--logfile FILE Log to the file specified --logfile FILE Log to the file specified
--version show program's version number and exit -V, --version show program's version number and exit
-c PATH, --config PATH -c PATH, --config PATH
Specify configuration file (default: None). Multiple Specify configuration file (default: None). Multiple
--config options may be used. Can be set to '-' to --config options may be used. Can be set to '-' to
@ -34,9 +37,6 @@ optional arguments:
Specify strategy class name (default: Specify strategy class name (default:
DefaultStrategy). DefaultStrategy).
--strategy-path PATH Specify additional strategy lookup path. --strategy-path PATH Specify additional strategy lookup path.
--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 --db-url PATH Override trades database URL, this is useful if
dry_run is enabled or in custom deployments (default: dry_run is enabled or in custom deployments (default:
None). None).
@ -49,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` default, the bot will load the file `./config.json`
```bash ```bash
python3 freqtrade -c path/far/far/away/config.json freqtrade -c path/far/far/away/config.json
``` ```
### How to use multiple configuration files? ### How to use multiple configuration files?
@ -65,13 +65,13 @@ empty key and secrete values while running in the Dry Mode (which does not actua
require them): require them):
```bash ```bash
python3 freqtrade -c ./config.json freqtrade -c ./config.json
``` ```
and specify both configuration files when running in the normal Live Trade Mode: and specify both configuration files when running in the normal Live Trade Mode:
```bash ```bash
python3 freqtrade -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 This could help you hide your private Exchange key and Exchange secrete on you local machine
@ -97,7 +97,7 @@ In `user_data/strategies` you have a file `my_awesome_strategy.py` which has
a strategy class called `AwesomeStrategy` to load it: a strategy class called `AwesomeStrategy` to load it:
```bash ```bash
python3 freqtrade --strategy AwesomeStrategy freqtrade --strategy AwesomeStrategy
``` ```
If the bot does not find your strategy file, it will display in an error If the bot does not find your strategy file, it will display in an error
@ -109,27 +109,16 @@ Learn more about strategy file in
### How to use **--strategy-path**? ### How to use **--strategy-path**?
This parameter allows you to add an additional strategy lookup path, which gets This parameter allows you to add an additional strategy lookup path, which gets
checked before the default locations (The passed path must be a folder!): checked before the default locations (The passed path must be a directory!):
```bash ```bash
python3 freqtrade --strategy AwesomeStrategy --strategy-path /some/folder freqtrade --strategy AwesomeStrategy --strategy-path /some/directory
``` ```
#### How to install a strategy? #### 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. `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**? ### How to use **--db-url**?
When you run the bot in Dry-run mode, per default no transactions are When you run the bot in Dry-run mode, per default no transactions are
@ -138,7 +127,7 @@ using `--db-url`. This can also be used to specify a custom database
in production mode. Example command: in production mode. Example command:
```bash ```bash
python3 freqtrade -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite freqtrade -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite
``` ```
## Backtesting commands ## Backtesting commands
@ -213,19 +202,23 @@ to find optimal parameter values for your stategy.
``` ```
usage: freqtrade hyperopt [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] usage: freqtrade hyperopt [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE]
[--max_open_trades MAX_OPEN_TRADES] [--max_open_trades INT]
[--stake_amount STAKE_AMOUNT] [-r] [--stake_amount STAKE_AMOUNT] [-r]
[--customhyperopt NAME] [--eps] [--dmmp] [-e INT] [--customhyperopt NAME] [--hyperopt-path PATH]
[-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]] [--eps] [-e INT]
[--print-all] [-j JOBS] [-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: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL -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 --timerange TIMERANGE
Specify what timerange of data to use. Specify what timerange of data to use.
--max_open_trades MAX_OPEN_TRADES --max_open_trades INT
Specify max_open_trades to use. Specify max_open_trades to use.
--stake_amount STAKE_AMOUNT --stake_amount STAKE_AMOUNT
Specify stake_amount. Specify stake_amount.
@ -235,18 +228,20 @@ optional arguments:
run your optimization commands with up-to-date data. run your optimization commands with up-to-date data.
--customhyperopt NAME --customhyperopt NAME
Specify hyperopt class name (default: Specify hyperopt class name (default:
DefaultHyperOpts). `DefaultHyperOpts`).
--hyperopt-path PATH Specify additional lookup path for Hyperopts and
Hyperopt Loss functions.
--eps, --enable-position-stacking --eps, --enable-position-stacking
Allow buying the same pair multiple times (position Allow buying the same pair multiple times (position
stacking). stacking).
-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 --dmmp, --disable-max-market-positions
Disable applying `max_open_trades` during backtest Disable applying `max_open_trades` during backtest
(same as setting `max_open_trades` to a very high (same as setting `max_open_trades` to a very high
number). number).
-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. --print-all Print all results, not only the best ones.
-j JOBS, --job-workers JOBS -j JOBS, --job-workers JOBS
The number of concurrently running jobs for The number of concurrently running jobs for
@ -254,6 +249,19 @@ optional arguments:
(default), all CPUs are used, for -2, all CPUs but one (default), all CPUs are used, for -2, all CPUs but one
are used, etc. If 1 is given, no parallel computing are used, etc. If 1 is given, no parallel computing
code is used at all. 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 ## Edge commands
@ -289,11 +297,6 @@ optional arguments:
To understand edge and how to read the results, please read the [edge documentation](edge.md). 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 ## Next step
The optimal strategy of the bot will change with time depending of the market trends. The next step is to The optimal strategy of the bot will change with time depending of the market trends. The next step is to

View File

@ -44,8 +44,8 @@ Mandatory Parameters are marked as **Required**.
| `exchange.sandbox` | false | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details. | `exchange.sandbox` | false | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details.
| `exchange.key` | '' | API key to use for the exchange. Only required when you are in production mode. | `exchange.key` | '' | API key to use for the exchange. Only required when you are in production mode.
| `exchange.secret` | '' | API secret to use for the exchange. Only required when you are in production mode. | `exchange.secret` | '' | API secret to use for the exchange. Only required when you are in production mode.
| `exchange.pair_whitelist` | [] | List of currency to use by the bot. Can be overrided with `--dynamic-whitelist` param. | `exchange.pair_whitelist` | [] | List of 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 currency the bot must avoid. Useful when using `--dynamic-whitelist` param. | `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_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.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. | `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.use_sell_signal` | false | Use your sell strategy in addition of the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy).
| `experimental.sell_profit_only` | false | Waits until you have made a positive profit before taking a sell decision. [Strategy Override](#parameters-in-the-strategy). | `experimental.sell_profit_only` | false | Waits until you have made a positive profit before taking a sell decision. [Strategy Override](#parameters-in-the-strategy).
| `experimental.ignore_roi_if_buy_signal` | false | Does not sell if the buy-signal is still active. Takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy). | `experimental.ignore_roi_if_buy_signal` | false | Does not sell if the buy-signal is still active. Takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy).
| `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). | `pairlist.config` | None | Additional configuration for dynamic pairlists. [More information below](#dynamic-pairlists).
| `telegram.enabled` | true | **Required.** Enable or not the usage of Telegram. | `telegram.enabled` | true | **Required.** Enable or not the usage of Telegram.
| `telegram.token` | token | Your Telegram bot token. Only required if `telegram.enabled` is `true`. | `telegram.token` | token | Your Telegram bot token. Only required if `telegram.enabled` is `true`.
@ -67,7 +67,7 @@ Mandatory Parameters are marked as **Required**.
| `initial_state` | running | Defines the initial application state. More information below. | `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. | `forcebuy_enable` | false | Enables the RPC Commands to force a buy. More information below.
| `strategy` | DefaultStrategy | Defines Strategy class to use. | `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.process_throttle_secs` | 5 | **Required.** Set the process throttle. Value in second.
| `internals.sd_notify` | false | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details. | `internals.sd_notify` | false | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details.
| `logfile` | | Specify Logfile. Uses a rolling strategy of 10 files, with 1Mb per file. | `logfile` | | Specify Logfile. Uses a rolling strategy of 10 files, with 1Mb per file.
@ -380,8 +380,6 @@ section of the configuration.
* `StaticPairList` * `StaticPairList`
* It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`. * It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`.
* `VolumePairList` * `VolumePairList`
* Formerly available as `--dynamic-whitelist [<number_assets>]`. 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 * It selects `number_assets` top pairs based on `sort_key`, which can be one of
`askVolume`, `bidVolume` and `quoteVolume`, defaults to `quoteVolume`. `askVolume`, `bidVolume` and `quoteVolume`, defaults to `quoteVolume`.
* There is a possibility to filter low-value coins that would not allow setting a stop loss * There is a possibility to filter low-value coins that would not allow setting a stop loss

View File

@ -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 the bot features that were declared as DEPRECATED by the bot development team
and are no longer supported. Please avoid their usage in your configuration. and are no longer supported. Please avoid their usage in your configuration.
### the `--live` command line option
`--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 ### The **--dynamic-whitelist** command line option
Per default `--dynamic-whitelist` will retrieve the 20 currencies based This command line option was deprecated in 2018 and removed freqtrade 2019.6-dev (develop branch)
on BaseVolume. This value can be changed when you run the script. and in freqtrade 2019.7 (master branch).
**By Default**
Get the 20 currencies based on BaseVolume.
```bash
python3 freqtrade --dynamic-whitelist
```
**Customize the number of currencies to retrieve**
Get the 30 currencies based on BaseVolume.
```bash
python3 freqtrade --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).

View File

@ -130,7 +130,7 @@ If the day shows the same day, then the last candle can be assumed as incomplete
This part of the documentation is aimed at maintainers, and shows how to create a release. This part of the documentation is aimed at maintainers, and shows how to create a release.
### create release branch ### Create release branch
``` bash ``` bash
# make sure you're in develop branch # make sure you're in develop branch
@ -140,11 +140,14 @@ git checkout develop
git checkout -b new_release git checkout -b new_release
``` ```
* Edit `freqtrade/__init__.py` and add the desired version (for example `0.18.0`) * Edit `freqtrade/__init__.py` and add the 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 * Commit this part
* push that branch to the remote and create a PR against the master branch * 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 ``` bash
# Needs to be done before merging / pulling that branch. # Needs to be done before merging / pulling that branch.
@ -160,5 +163,5 @@ git log --oneline --no-decorate --no-merges master..develop
### After-release ### After-release
* Update version in develop to next valid version and postfix that with `-dev` (`0.18.0 -> 0.18.1-dev`). * Update version in develop by postfixing that with `-dev` (`2019.6 -> 2019.6-dev`).
* Create a PR against develop to update that branch. * Create a PR against develop to update that branch.

View File

@ -140,7 +140,7 @@ To run a restartable instance in the background (feel free to place your configu
#### Move your config file and database #### Move your config file and database
The following will assume that you place your configuration / database files to `~/.freqtrade`, which is a hidden folder in your home directory. Feel free to use a different folder and replace the folder in the upcomming commands. 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 ```bash
mkdir ~/.freqtrade mkdir ~/.freqtrade

View File

@ -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. 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 !!! 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 !!! Note
Edge does not consider anything else than buy/sell/stoploss signals. So trailing stoploss, ROI, and everything else are ignored in its calculation. Edge does not consider anything else than buy/sell/stoploss signals. So trailing stoploss, ROI, and everything else are ignored in its calculation.
@ -209,7 +209,7 @@ Edge will remove sudden pumps in a given market while going through historical d
You can run Edge independently in order to see in details the result. Here is an example: You can run Edge independently in order to see in details the result. Here is an example:
```bash ```bash
python3 freqtrade edge freqtrade edge
``` ```
An example of its output: An example of its output:
@ -235,19 +235,19 @@ An example of its output:
### Update cached pairs with the latest data ### Update cached pairs with the latest data
```bash ```bash
python3 freqtrade edge --refresh-pairs-cached freqtrade edge --refresh-pairs-cached
``` ```
### Precising stoploss range ### Precising stoploss range
```bash ```bash
python3 freqtrade 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 ### Advanced use of timerange
```bash ```bash
python3 freqtrade 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. 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.

View File

@ -1,14 +1,25 @@
# Freqtrade FAQ # 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 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 situation of the market etc, it can take up to hours to find good entry
position for a trade. Be patient! 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 I understand your disappointment but unfortunately 12 trades is just
not enough to say anything. If you run backtesting, you can see that our not enough to say anything. If you run backtesting, you can see that our
@ -19,24 +30,24 @@ 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 gamble, which should leave you with modest wins on monthly basis but
you can't say much from few trades. you can't say much from few trades.
#### Id like to change the stake amount. Can I just stop the bot with /stop and then change the config.json and run it again? ### Id 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 Not quite. Trades are persisted to a database but the configuration is
currently only read when the bot is killed and restarted. `/stop` more 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. 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 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). 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. You can use the `/forcesell all` command from Telegram.
### Hyperopt module ## Hyperopt module
#### How many epoch do I need to get a good Hyperopt result? ### How many epoch do I need to get a good Hyperopt result?
Per default Hyperopts without `-e` or `--epochs` parameter will only 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
@ -47,16 +58,16 @@ compute.
We recommend you to run it at least 10.000 epochs: We recommend you to run it at least 10.000 epochs:
```bash ```bash
python3 freqtrade hyperopt -e 10000 freqtrade hyperopt -e 10000
``` ```
or if you want intermediate result to see or if you want intermediate result to see
```bash ```bash
for i in {1..100}; do python3 freqtrade 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. Finding a great Hyperopt results takes time.
@ -74,13 +85,14 @@ 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 Did you run 100 000 evals? Congrats, you've done roughly 1 / 100 000 th
of the search space. 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. 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: 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/ - https://www.tradeciety.com/ultimate-math-guide-for-traders/
- http://www.vantharp.com/tharp-concepts/expectancy.asp - http://www.vantharp.com/tharp-concepts/expectancy.asp
- https://samuraitradingacademy.com/trading-expectancy/ - https://samuraitradingacademy.com/trading-expectancy/

View File

@ -34,7 +34,7 @@ Depending on the space you want to optimize, only some of the below are required
### 1. Install a Custom Hyperopt File ### 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`: 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` Copy the file `user_data/hyperopts/sample_hyperopt.py` into `user_data/hyperopts/awesome_hyperopt.py`
@ -144,21 +144,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 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 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. 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 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`. 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 <Class-name>` 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 ## Execute Hyperopt
Once you have updated your hyperopt configuration you can run it. 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. We strongly recommend to use `screen` or `tmux` to prevent any connection loss.
```bash ```bash
python3 freqtrade -c config.json hyperopt --customhyperopt <hyperoptname> -e 5000 --spaces all freqtrade -c config.json hyperopt --customhyperopt <hyperoptname> -e 5000 --spaces all
``` ```
Use `<hyperoptname>` as the name of the custom hyperopt used. Use `<hyperoptname>` as the name of the custom hyperopt used.
@ -168,8 +237,11 @@ running at least several thousand evaluations.
The `--spaces all` flag determines that all possible parameters should be optimized. Possibilities are listed below. 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 !!! 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 ### Execute Hyperopt with Different Ticker-Data Source
@ -179,12 +251,11 @@ use data from directory `user_data/data`.
### Running Hyperopt with Smaller Testset ### Running Hyperopt with Smaller Testset
Use the `--timerange` argument to change how much of the testset Use the `--timerange` argument to change how much of the testset you want to use.
you want to use. The last N ticks/timeframes will be used. For example, to use one month of data, pass the following parameter to the hyperopt call:
Example:
```bash ```bash
python3 freqtrade hyperopt --timerange -200 freqtrade hyperopt --timerange 20180401-20180501
``` ```
### Running Hyperopt with Smaller Search Space ### Running Hyperopt with Smaller Search Space
@ -197,12 +268,33 @@ new buy strategy you have.
Legal values are: Legal values are:
- `all`: optimize everything * `all`: optimize everything
- `buy`: just search for a new buy strategy * `buy`: just search for a new buy strategy
- `sell`: just search for a new sell strategy * `sell`: just search for a new sell strategy
- `roi`: just optimize the minimal profit table for your strategy * `roi`: just optimize the minimal profit table for your strategy
- `stoploss`: search for the best stoploss value * `stoploss`: search for the best stoploss value
- space-separated list of any of the above values for example `--spaces roi stoploss` * 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 ## Understand the Hyperopt Result
@ -231,7 +323,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: 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) (dataframe['rsi'] < 29.0)
``` ```
@ -288,19 +380,11 @@ This would translate to the following ROI table:
} }
``` ```
### Validate backtest result ### Validate backtesting results
Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected. Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected.
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. 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.
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.
## Next Step ## Next Step

View File

@ -4,12 +4,22 @@ This page explains how to prepare your environment for running the bot.
## Prerequisite ## Prerequisite
### 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 Before running your bot in production you will need to setup few
external API. In production mode, the bot will require valid Exchange API 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). credentials. We also recommend a [Telegram bot](telegram-usage.md#setup-your-telegram-bot) (optional but recommended).
- [Setup your exchange account](#setup-your-exchange-account)
### Setup your exchange account ### Setup your exchange account
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. 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.
@ -18,6 +28,9 @@ You will need to create API Keys (Usually you get `key` and `secret`) from the E
Freqtrade provides a Linux/MacOS script to install all dependencies and help you to configure the bot. Freqtrade provides a Linux/MacOS script to install all dependencies and help you to configure the bot.
!!! 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 ```bash
git clone git@github.com:freqtrade/freqtrade.git git clone git@github.com:freqtrade/freqtrade.git
cd freqtrade cd freqtrade
@ -30,7 +43,7 @@ git checkout develop
## Easy Installation - Linux Script ## Easy Installation - Linux Script
If you are on Debian, Ubuntu or MacOS a freqtrade provides a script to Install, Update, Configure, and Reset your bot. If you are on Debian, Ubuntu or MacOS freqtrade provides a script to Install, Update, Configure, and Reset your bot.
```bash ```bash
$ ./setup.sh $ ./setup.sh
@ -45,7 +58,7 @@ usage:
This script will install everything you need to run the bot: 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 * Setup your virtualenv
* Configure your `config.json` file * Configure your `config.json` file
@ -70,24 +83,16 @@ Config parameter is a `config.json` configurator. This script will ask you quest
We've included/collected install instructions for Ubuntu 16.04, MacOS, and Windows. These are guidelines and your success may vary with other distros. We've included/collected install instructions for Ubuntu 16.04, MacOS, and Windows. These are guidelines and your success may vary with other distros.
OS Specific steps are listed first, the [Common](#common) section below is necessary for all systems. OS Specific steps are listed first, the [Common](#common) section below is necessary for all systems.
### Requirements !!! Note
Python3.6 or higher and the corresponding pip are assumed to be available.
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)
### Linux - Ubuntu 16.04 ### Linux - Ubuntu 16.04
#### Install Python 3.6, Git, and wget #### Install necessary dependencies
```bash ```bash
sudo add-apt-repository ppa:jonathonf/python-3.6
sudo apt-get update 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 #### Raspberry Pi / Raspbian
@ -111,14 +116,6 @@ python3 -m pip install -r requirements-common.txt
python3 -m pip install -e . python3 -m pip install -e .
``` ```
### MacOS
#### Install Python 3.6, git and wget
```bash
brew install python3 git wget
```
### Common ### Common
#### 1. Install TA-Lib #### 1. Install TA-Lib
@ -159,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 ```bash
git checkout master git checkout master
@ -177,9 +174,9 @@ cp config.json.example config.json
#### 5. Install python dependencies #### 5. Install python dependencies
``` bash ``` bash
pip3 install --upgrade pip python3 -m pip install --upgrade pip
pip3 install -r requirements.txt python3 -m pip install -r requirements.txt
pip3 install -e . python3 -m pip install -e .
``` ```
#### 6. Run the Bot #### 6. Run the Bot
@ -187,7 +184,7 @@ 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. 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 ```bash
python3.6 freqtrade -c config.json freqtrade -c config.json
``` ```
*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. *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.
@ -218,7 +215,7 @@ The `freqtrade.service.watchdog` file contains an example of the service unit co
as the watchdog. as the watchdog.
!!! Note !!! Note
The sd_notify communication between the bot and the systemd service manager will not work if the bot runs in a Docker container. The sd_notify communication between the bot and the systemd service manager will not work if the bot runs in a Docker container.
------ ------
@ -237,8 +234,6 @@ If that is not available on your system, feel free to try the instructions below
git clone https://github.com/freqtrade/freqtrade.git git clone https://github.com/freqtrade/freqtrade.git
``` ```
copy paste `config.json` to ``\path\freqtrade-develop\freqtrade`
#### Install ta-lib #### Install ta-lib
Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows). Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows).

View File

@ -16,10 +16,10 @@ Sample configuration:
}, },
``` ```
!!! Danger: Security warning !!! 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. 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 !!! Danger Password selection
Please make sure to select a very strong, unique password to protect your bot from unauthorized access. 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. 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.
@ -62,7 +62,7 @@ docker run -d \
``` ```
!!! Danger "Security warning" !!! 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. 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 ## Consuming the API

View File

@ -5,8 +5,7 @@ indicators.
## Install a custom strategy file ## Install a custom strategy file
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`.
`user_data/strategies`.
Let assume you have a class called `AwesomeStrategy` in the file `awesome-strategy.py`: 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) 2. Start the bot with the param `--strategy AwesomeStrategy` (the parameter is the class name)
```bash ```bash
python3 freqtrade --strategy AwesomeStrategy freqtrade --strategy AwesomeStrategy
``` ```
## Change your strategy ## Change your strategy
@ -22,7 +21,7 @@ python3 freqtrade --strategy AwesomeStrategy
The bot includes a default strategy file. However, we recommend you to 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 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 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. 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` `cp user_data/strategies/test_strategy.py user_data/strategies/awesome-strategy.py`
@ -41,7 +40,7 @@ The bot also include a sample strategy called `TestStrategy` you can update: `us
You can test it with the parameter: `--strategy TestStrategy` You can test it with the parameter: `--strategy TestStrategy`
```bash ```bash
python3 freqtrade --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) **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)
@ -398,10 +397,10 @@ The default buy strategy is located in the file
### Specify custom strategy location ### 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 ```bash
python3 freqtrade --strategy AwesomeStrategy --strategy-path /some/folder freqtrade --strategy AwesomeStrategy --strategy-path /some/directory
``` ```
### Further strategy ideas ### Further strategy ideas

View File

@ -1,5 +1,5 @@
""" FreqTrade bot """ """ FreqTrade bot """
__version__ = '2019.6' __version__ = '2019.7'
class DependencyException(Exception): class DependencyException(Exception):

View File

@ -1,526 +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: Optional[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_options()
self.main_options()
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, no_default_config: bool = False) -> 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 no_default_config and parsed_arg.config is None:
parsed_arg.config = [constants.DEFAULT_CONFIG]
return parsed_arg
def common_options(self) -> None:
"""
Parses arguments that are common for the main Freqtrade, all subcommands and scripts.
"""
parser = self.parser
parser.add_argument(
'-v', '--verbose',
help='Verbose mode (-vv for more, -vvv to get all messages).',
action='count',
dest='loglevel',
default=0,
)
parser.add_argument(
'--logfile',
help='Log to the file specified.',
dest='logfile',
metavar='FILE',
)
parser.add_argument(
'--version',
action='version',
version=f'%(prog)s {__version__}'
)
parser.add_argument(
'-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.',
dest='config',
action='append',
metavar='PATH',
)
parser.add_argument(
'-d', '--datadir',
help='Path to backtest data.',
dest='datadir',
metavar='PATH',
)
def main_options(self) -> None:
"""
Parses arguments for the main Freqtrade.
"""
parser = self.parser
parser.add_argument(
'-s', '--strategy',
help='Specify strategy class name (default: `%(default)s`).',
dest='strategy',
default='DefaultStrategy',
metavar='NAME',
)
parser.add_argument(
'--strategy-path',
help='Specify additional strategy lookup path.',
dest='strategy_path',
metavar='PATH',
)
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='?',
)
parser.add_argument(
'--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).',
dest='db_url',
metavar='PATH',
)
parser.add_argument(
'--sd-notify',
help='Notify systemd service manager.',
action='store_true',
dest='sd_notify',
)
def common_optimize_options(self, subparser: argparse.ArgumentParser = None) -> None:
"""
Parses arguments common for Backtesting, Edge and Hyperopt modules.
:param parser:
"""
parser = subparser or self.parser
parser.add_argument(
'-i', '--ticker-interval',
help='Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`).',
dest='ticker_interval',
)
parser.add_argument(
'--timerange',
help='Specify what timerange of data to use.',
dest='timerange',
)
parser.add_argument(
'--max_open_trades',
help='Specify max_open_trades to use.',
type=int,
dest='max_open_trades',
)
parser.add_argument(
'--stake_amount',
help='Specify stake_amount.',
type=float,
dest='stake_amount',
)
parser.add_argument(
'-r', '--refresh-pairs-cached',
help='Refresh the pairs files in tests/testdata with the latest data from the '
'exchange. Use it if you want to run your optimization commands with '
'up-to-date data.',
action='store_true',
dest='refresh_pairs',
)
def backtesting_options(self, subparser: argparse.ArgumentParser = None) -> None:
"""
Parses given arguments for Backtesting module.
"""
parser = subparser or self.parser
parser.add_argument(
'--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(
'--strategy-list',
help='Provide a comma-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='+',
dest='strategy_list',
)
parser.add_argument(
'--export',
help='Export backtest results, argument are: trades. '
'Example: `--export=trades`',
dest='export',
)
parser.add_argument(
'--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`',
default=os.path.join('user_data', 'backtest_data', 'backtest-result.json'),
dest='exportfilename',
metavar='PATH',
)
def edge_options(self, subparser: argparse.ArgumentParser = None) -> None:
"""
Parses given arguments for Edge module.
"""
parser = subparser or self.parser
parser.add_argument(
'--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`',
dest='stoploss_range',
)
def hyperopt_options(self, subparser: argparse.ArgumentParser = None) -> None:
"""
Parses given arguments for Hyperopt module.
"""
parser = subparser or self.parser
parser.add_argument(
'--customhyperopt',
help='Specify hyperopt class name (default: `%(default)s`).',
dest='hyperopt',
default=constants.DEFAULT_HYPEROPT,
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-separated list. '
'Default: `%(default)s`.',
choices=['all', 'buy', 'sell', 'roi', 'stoploss'],
default='all',
nargs='+',
dest='spaces',
)
parser.add_argument(
'--print-all',
help='Print all results, not only the best ones.',
action='store_true',
dest='print_all',
default=False
)
parser.add_argument(
'-j', '--job-workers',
help='The number of concurrently running jobs for hyperoptimization '
'(hyperopt worker processes). '
'If -1 (default), all CPUs are used, for -2, all CPUs but one are used, etc. '
'If 1 is given, no parallel computing code is used at all.',
dest='hyperopt_jobs',
default=-1,
type=int,
metavar='JOBS',
)
parser.add_argument(
'--random-state',
help='Set random state to some positive integer for reproducible hyperopt results.',
dest='hyperopt_random_state',
type=Arguments.check_int_positive,
metavar='INT',
)
parser.add_argument(
'--min-trades',
help="Set minimal desired number of trades for evaluations in the hyperopt "
"optimization path (default: 1).",
dest='hyperopt_min_trades',
default=1,
type=Arguments.check_int_positive,
metavar='INT',
)
def list_exchanges_options(self, subparser: argparse.ArgumentParser = None) -> None:
"""
Parses given arguments for the list-exchanges command.
"""
parser = subparser or self.parser
parser.add_argument(
'-1', '--one-column',
help='Print exchanges in one column.',
action='store_true',
dest='print_one_column',
)
def _build_subcommands(self) -> None:
"""
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.common_optimize_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=start_edge)
self.common_optimize_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=start_hyperopt)
self.common_optimize_options(hyperopt_cmd)
self.hyperopt_options(hyperopt_cmd)
# Add list-exchanges subcommand
list_exchanges_cmd = subparsers.add_parser(
'list-exchanges',
help='Print available exchanges.'
)
list_exchanges_cmd.set_defaults(func=start_list_exchanges)
self.list_exchanges_options(list_exchanges_cmd)
@staticmethod
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)
@staticmethod
def check_int_positive(value: str) -> int:
try:
uint = int(value)
if uint <= 0:
raise ValueError
except ValueError:
raise argparse.ArgumentTypeError(
f"{value} is invalid for this parameter, should be a positive integer value"
)
return uint
def common_scripts_options(self, subparser: argparse.ArgumentParser = None) -> None:
"""
Parses arguments common for scripts.
"""
parser = subparser or self.parser
parser.add_argument(
'-p', '--pairs',
help='Show profits for only these pairs. Pairs are comma-separated.',
dest='pairs',
)
def download_data_options(self) -> None:
"""
Parses given arguments for testdata download script
"""
parser = self.parser
parser.add_argument(
'--pairs-file',
help='File containing a list of pairs to download.',
dest='pairs_file',
metavar='FILE',
)
parser.add_argument(
'--days',
help='Download data for given number of days.',
dest='days',
type=Arguments.check_int_positive,
metavar='INT',
)
parser.add_argument(
'--exchange',
help=f'Exchange name (default: `{constants.DEFAULT_EXCHANGE}`). '
f'Only valid if no config is provided.',
dest='exchange',
)
parser.add_argument(
'-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='+',
dest='timeframes',
)
parser.add_argument(
'--erase',
help='Clean all existing data for the selected exchange/pairs/timeframes.',
dest='erase',
action='store_true'
)
def plot_dataframe_options(self) -> None:
"""
Parses given arguments for plot dataframe script
"""
parser = self.parser
parser.add_argument(
'--indicators1',
help='Set indicators from your strategy you want in the first row of the graph. '
'Comma-separated list. Example: `ema3,ema5`. Default: `%(default)s`.',
default='sma,ema3,ema5',
dest='indicators1',
)
parser.add_argument(
'--indicators2',
help='Set indicators from your strategy you want in the third row of the graph. '
'Comma-separated list. Example: `fastd,fastk`. Default: `%(default)s`.',
default='macd,macdsignal',
dest='indicators2',
)
parser.add_argument(
'--plot-limit',
help='Specify tick limit for plotting. Notice: too high values cause huge files. '
'Default: %(default)s.',
dest='plot_limit',
default=750,
type=int,
)
parser.add_argument(
'--trade-source',
help='Specify the source for trades (Can be DB or file (backtest file)) '
'Default: %(default)s',
dest='trade_source',
default="file",
choices=["DB", "file"]
)

View File

@ -0,0 +1,2 @@
from freqtrade.configuration.arguments import Arguments, TimeRange # noqa: F401
from freqtrade.configuration.configuration import Configuration # noqa: F401

View File

@ -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)

View File

@ -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

View File

@ -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 comma-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",
),
}

View File

@ -3,62 +3,22 @@ This module contains the configuration class
""" """
import json import json
import logging import logging
import os
import sys import sys
import warnings
from argparse import Namespace from argparse import Namespace
from logging.handlers import RotatingFileHandler from typing import Any, Callable, Dict, Optional
from typing import Any, Callable, Dict, List, Optional
from jsonschema import Draft4Validator, validators
from jsonschema.exceptions import ValidationError, best_match
from freqtrade import OperationalException, constants from freqtrade import OperationalException, constants
from freqtrade.exchange import (is_exchange_bad, is_exchange_available, from freqtrade.configuration.check_exchange import check_exchange
is_exchange_officially_supported, available_exchanges) from freqtrade.configuration.create_datadir import create_datadir
from freqtrade.configuration.json_schema import validate_config_schema
from freqtrade.loggers import setup_logging
from freqtrade.misc import deep_merge_dicts from freqtrade.misc import deep_merge_dicts
from freqtrade.state import RunMode from freqtrade.state import RunMode
logger = logging.getLogger(__name__) 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)
def _extend_validator(validator_class):
"""
Extended validator for the Freqtrade configuration JSON Schema.
Currently it only handles defaults for subschemas.
"""
validate_properties = validator_class.VALIDATORS['properties']
def set_defaults(validator, properties, instance, schema):
for prop, subschema in properties.items():
if 'default' in subschema:
instance.setdefault(prop, subschema['default'])
for error in validate_properties(
validator, properties, instance, schema,
):
yield error
return validators.extend(
validator_class, {'properties': set_defaults}
)
FreqtradeValidator = _extend_validator(Draft4Validator)
class Configuration(object): class Configuration(object):
""" """
Class to read and init the bot configuration Class to read and init the bot configuration
@ -70,49 +30,30 @@ class Configuration(object):
self.config: Optional[Dict[str, Any]] = None self.config: Optional[Dict[str, Any]] = None
self.runmode = runmode self.runmode = runmode
def load_config(self) -> Dict[str, Any]: def get_config(self) -> Dict[str, Any]:
""" """
Extract information for sys.argv and load the bot configuration Return the config. Use this method to get the bot config
:return: Configuration dictionary :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] = {} config: Dict[str, Any] = {}
# Now expecting a list of config filenames here, not a string
# We expect here a list of config filenames
for path in self.args.config: for path in self.args.config:
logger.info('Using config: %s ...', path) logger.info('Using config: %s ...', path)
# Merge config options, overwriting old values # Merge config options, overwriting old values
config = deep_merge_dicts(self._load_config_file(path), config) config = deep_merge_dicts(self._load_config_file(path), config)
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 Optimize configurations
config = self._load_optimize_config(config)
# Add plotting options if available
config = self._load_plot_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 return config
def _load_config_file(self, path: str) -> Dict[str, Any]: def _load_config_file(self, path: str) -> Dict[str, Any]:
@ -124,69 +65,80 @@ class Configuration(object):
try: try:
# Read config from stdin if requested in the options # Read config from stdin if requested in the options
with open(path) if path != '-' else sys.stdin as file: with open(path) if path != '-' else sys.stdin as file:
conf = json.load(file) config = json.load(file)
except FileNotFoundError: except FileNotFoundError:
raise OperationalException( raise OperationalException(
f'Config file "{path}" not found!' f'Config file "{path}" not found!'
' Please create a config file or check whether it exists.') ' Please create a config file or check whether it exists.')
return conf return config
def _load_logging_config(self, config: Dict[str, Any]) -> None: 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: Extract information for sys.argv and load logging configuration:
the --loglevel, --logfile options the -v/--verbose, --logfile options
""" """
# Log level # Log level
if 'loglevel' in self.args and self.args.loglevel: if 'verbosity' in self.args and self.args.verbosity:
config.update({'verbosity': self.args.loglevel}) config.update({'verbosity': self.args.verbosity})
else: else:
config.update({'verbosity': 0}) config.update({'verbosity': 0})
# Log to stdout, not stderr
log_handlers: List[logging.Handler] = [logging.StreamHandler(sys.stdout)]
if 'logfile' in self.args and self.args.logfile: if 'logfile' in self.args and self.args.logfile:
config.update({'logfile': self.args.logfile}) config.update({'logfile': self.args.logfile})
# Allow setting this as either configuration or argument setup_logging(config)
if 'logfile' in config:
log_handlers.append(RotatingFileHandler(config['logfile'],
maxBytes=1024 * 1024, # 1Mb
backupCount=10))
logging.basicConfig( def _process_strategy_options(self, config: Dict[str, Any]) -> None:
level=logging.INFO if config['verbosity'] < 1 else logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=log_handlers
)
set_loggers(config['verbosity'])
logger.info('Verbosity set to %s', config['verbosity'])
def _load_common_config(self, config: Dict[str, Any]) -> Dict[str, Any]: # 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'):
Extract information for sys.argv and load common configuration config.update({'strategy': self.args.strategy})
:return: configuration as dictionary
"""
self._load_logging_config(config)
# Support for sd_notify self._args_to_config(config, argname='strategy_path',
if self.args.sd_notify: logstring='Using additional Strategy lookup path: {}')
config['internals'].update({'sd_notify': True})
# Add dynamic_whitelist if found def _process_common_options(self, config: Dict[str, Any]) -> None:
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: 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}) config.update({'db_url': self.args.db_url})
logger.info('Parameter --db-url detected ...') logger.info('Parameter --db-url detected ...')
@ -200,6 +152,8 @@ class Configuration(object):
config['db_url'] = constants.DEFAULT_DB_PROD_URL config['db_url'] = constants.DEFAULT_DB_PROD_URL
logger.info('Dry run is disabled') logger.info('Dry run is disabled')
logger.info(f'Using DB: "{config["db_url"]}"')
if config.get('forcebuy_enable', False): if config.get('forcebuy_enable', False):
logger.warning('`forcebuy` RPC message enabled.') logger.warning('`forcebuy` RPC message enabled.')
@ -207,59 +161,25 @@ class Configuration(object):
if config.get('max_open_trades') == -1: if config.get('max_open_trades') == -1:
config['max_open_trades'] = float('inf') config['max_open_trades'] = float('inf')
logger.info(f'Using DB: "{config["db_url"]}"') # 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 if the exchange set by the user is supported
self.check_exchange(config) check_exchange(config)
return config def _process_datadir_options(self, config: Dict[str, Any]) -> None:
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 _args_to_config(self, config: Dict[str, Any], argname: str,
logstring: str, logfun: Optional[Callable] = 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]))
def _load_datadir_config(self, config: Dict[str, Any]) -> None:
""" """
Extract information for sys.argv and load datadir configuration: Extract information for sys.argv and load datadir configuration:
the --datadir option the --datadir option
""" """
if 'datadir' in self.args and self.args.datadir: if 'datadir' in self.args and self.args.datadir:
config.update({'datadir': self._create_datadir(config, self.args.datadir)}) config.update({'datadir': create_datadir(config, self.args.datadir)})
else: else:
config.update({'datadir': self._create_datadir(config, None)}) config.update({'datadir': create_datadir(config, None)})
logger.info('Using data folder: %s ...', config.get('datadir')) logger.info('Using data directory: %s ...', config.get('datadir'))
def _load_optimize_config(self, config: Dict[str, Any]) -> Dict[str, Any]: def _process_optimize_options(self, config: Dict[str, Any]) -> None:
"""
Extract information for sys.argv and load Optimize configuration
:return: configuration as dictionary
"""
# This will override the strategy configuration # This will override the strategy configuration
self._args_to_config(config, argname='ticker_interval', self._args_to_config(config, argname='ticker_interval',
@ -267,7 +187,8 @@ class Configuration(object):
'Using ticker_interval: {} ...') 'Using ticker_interval: {} ...')
self._args_to_config(config, argname='live', self._args_to_config(config, argname='live',
logstring='Parameter -l/--live detected ...') logstring='Parameter -l/--live detected ...',
deprecated_msg='--live will be removed soon.')
self._args_to_config(config, argname='position_stacking', self._args_to_config(config, argname='position_stacking',
logstring='Parameter --enable-position-stacking detected ...') logstring='Parameter --enable-position-stacking detected ...')
@ -290,7 +211,7 @@ class Configuration(object):
self._args_to_config(config, argname='timerange', self._args_to_config(config, argname='timerange',
logstring='Parameter --timerange detected: {} ...') logstring='Parameter --timerange detected: {} ...')
self._load_datadir_config(config) self._process_datadir_options(config)
self._args_to_config(config, argname='refresh_pairs', self._args_to_config(config, argname='refresh_pairs',
logstring='Parameter -r/--refresh-pairs-cached detected ...') logstring='Parameter -r/--refresh-pairs-cached detected ...')
@ -319,6 +240,9 @@ class Configuration(object):
self._args_to_config(config, argname='hyperopt', self._args_to_config(config, argname='hyperopt',
logstring='Using Hyperopt file {}') 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', self._args_to_config(config, argname='epochs',
logstring='Parameter --epochs detected ... ' logstring='Parameter --epochs detected ... '
'Will run Hyperopt with for {} epochs ...' 'Will run Hyperopt with for {} epochs ...'
@ -339,13 +263,13 @@ class Configuration(object):
self._args_to_config(config, argname='hyperopt_min_trades', self._args_to_config(config, argname='hyperopt_min_trades',
logstring='Parameter --min-trades detected: {}') logstring='Parameter --min-trades detected: {}')
return config self._args_to_config(config, argname='hyperopt_continue',
logstring='Hyperopt continue: {}')
def _load_plot_config(self, config: Dict[str, Any]) -> Dict[str, Any]: self._args_to_config(config, argname='hyperopt_loss',
""" logstring='Using loss function: {}')
Extract information for sys.argv Plotting configuration
:return: configuration as dictionary def _process_plot_options(self, config: Dict[str, Any]) -> None:
"""
self._args_to_config(config, argname='pairs', self._args_to_config(config, argname='pairs',
logstring='Using pairs {}') logstring='Using pairs {}')
@ -360,25 +284,15 @@ class Configuration(object):
logstring='Limiting plot to: {}') logstring='Limiting plot to: {}')
self._args_to_config(config, argname='trade_source', self._args_to_config(config, argname='trade_source',
logstring='Using trades from: {}') logstring='Using trades from: {}')
return config
def _validate_config_schema(self, conf: Dict[str, Any]) -> Dict[str, Any]: def _process_runmode(self, config: Dict[str, Any]) -> None:
"""
Validate the configuration follow the Config Schema if not self.runmode:
:param conf: Config in JSON format # Handle real mode, infer dry/live from config
:return: Returns the config if valid, otherwise throw an exception self.runmode = RunMode.DRY_RUN if config.get('dry_run', True) else RunMode.LIVE
""" logger.info("Runmode set to {self.runmode}.")
try:
FreqtradeValidator(constants.CONF_SCHEMA).validate(conf) config.update({'runmode': self.runmode})
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: def _validate_config_consistency(self, conf: Dict[str, Any]) -> None:
""" """
@ -386,11 +300,11 @@ class Configuration(object):
:param conf: Config in JSON format :param conf: Config in JSON format
:return: Returns None if everything is ok, otherwise throw an OperationalException :return: Returns None if everything is ok, otherwise throw an OperationalException
""" """
# validating trailing stoploss # validating trailing stoploss
self._validate_trailing_stoploss(conf) self._validate_trailing_stoploss(conf)
def _validate_trailing_stoploss(self, conf: Dict[str, Any]) -> None: def _validate_trailing_stoploss(self, conf: Dict[str, Any]) -> None:
# Skip if trailing stoploss is not activated # Skip if trailing stoploss is not activated
if not conf.get('trailing_stop', False): if not conf.get('trailing_stop', False):
return return
@ -409,50 +323,24 @@ class Configuration(object):
f'The config trailing_stop_positive_offset needs ' f'The config trailing_stop_positive_offset needs '
'to be greater than trailing_stop_positive_offset in your config.') 'to be greater than trailing_stop_positive_offset in your config.')
def get_config(self) -> Dict[str, Any]: def _args_to_config(self, config: Dict[str, Any], argname: str,
logstring: str, logfun: Optional[Callable] = None,
deprecated_msg: Optional[str] = None) -> None:
""" """
Return the config. Use this method to get the bot config :param config: Configuration dictionary
:return: Dict: Bot config :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 self.config is None: if argname in self.args and getattr(self.args, argname):
self.config = self.load_config()
return self.config config.update({argname: getattr(self.args, argname)})
if logfun:
def check_exchange(self, config: Dict[str, Any], check_for_bad: bool = True) -> bool: logger.info(logstring.format(logfun(config[argname])))
""" else:
Check if the exchange name in the config file is supported by Freqtrade logger.info(logstring.format(config[argname]))
:param check_for_bad: if True, check the exchange against the list of known 'bad' if deprecated_msg:
exchanges warnings.warn(f"DEPRECATED: {deprecated_msg}", DeprecationWarning)
: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

View File

@ -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)

View File

@ -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
)

View File

@ -12,6 +12,7 @@ HYPEROPT_EPOCH = 100 # epochs
RETRY_TIMEOUT = 30 # sec RETRY_TIMEOUT = 30 # sec
DEFAULT_STRATEGY = 'DefaultStrategy' DEFAULT_STRATEGY = 'DefaultStrategy'
DEFAULT_HYPEROPT = 'DefaultHyperOpts' DEFAULT_HYPEROPT = 'DefaultHyperOpts'
DEFAULT_HYPEROPT_LOSS = 'DefaultHyperOptLoss'
DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite' DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
DEFAULT_DB_DRYRUN_URL = 'sqlite://' DEFAULT_DB_DRYRUN_URL = 'sqlite://'
UNLIMITED_STAKE_AMOUNT = 'unlimited' UNLIMITED_STAKE_AMOUNT = 'unlimited'

View File

@ -3,6 +3,7 @@ Helpers when analyzing backtest data
""" """
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Dict
import numpy as np import numpy as np
import pandas as pd import pandas as pd
@ -66,7 +67,6 @@ def evaluate_result_multi(results: pd.DataFrame, freq: str, max_open_trades: int
dates = pd.Series(pd.concat(dates).values, name='date') dates = pd.Series(pd.concat(dates).values, name='date')
df2 = pd.DataFrame(np.repeat(results.values, deltas, axis=0), columns=results.columns) 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 = pd.concat([dates, df2], axis=1)
df2 = df2.set_index('date') df2 = df2.set_index('date')
df_final = df2.resample(freq)[['pair']].count() df_final = df2.resample(freq)[['pair']].count()
@ -101,6 +101,18 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame:
return trades 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: 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 Compare trades and backtested pair DataFrames to get trades performed on backtested period
@ -109,3 +121,34 @@ def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame) -> p
trades = trades.loc[(trades['open_time'] >= dataframe.iloc[0]['date']) & trades = trades.loc[(trades['open_time'] >= dataframe.iloc[0]['date']) &
(trades['close_time'] <= dataframe.iloc[-1]['date'])] (trades['close_time'] <= dataframe.iloc[-1]['date'])]
return trades 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

View File

@ -17,7 +17,7 @@ from freqtrade.state import RunMode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class DataProvider(object): class DataProvider():
def __init__(self, config: dict, exchange: Exchange) -> None: def __init__(self, config: dict, exchange: Exchange) -> None:
self._config = config self._config = config
@ -81,11 +81,14 @@ class DataProvider(object):
# TODO: Implement me # TODO: Implement me
pass pass
def orderbook(self, pair: str, max: int): def orderbook(self, pair: str, maximum: int):
""" """
return latest orderbook data 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.
""" """
return self._exchange.get_order_book(pair, max) return self._exchange.get_order_book(pair, maximum)
@property @property
def runmode(self) -> RunMode: def runmode(self) -> RunMode:

View File

@ -16,7 +16,7 @@ import arrow
from pandas import DataFrame from pandas import DataFrame
from freqtrade import OperationalException, misc from freqtrade import OperationalException, misc
from freqtrade.arguments import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.converter import parse_ticker_dataframe
from freqtrade.exchange import Exchange, timeframe_to_minutes from freqtrade.exchange import Exchange, timeframe_to_minutes

View File

@ -10,8 +10,7 @@ import utils_find_1st as utf1st
from pandas import DataFrame from pandas import DataFrame
from freqtrade import constants, OperationalException from freqtrade import constants, OperationalException
from freqtrade.arguments import Arguments from freqtrade.configuration import Arguments, TimeRange
from freqtrade.arguments import TimeRange
from freqtrade.data import history from freqtrade.data import history
from freqtrade.strategy.interface import SellType from freqtrade.strategy.interface import SellType

View File

@ -85,6 +85,9 @@ class Exchange(object):
it does basic validation whether the specified exchange and pairs are valid. it does basic validation whether the specified exchange and pairs are valid.
:return: None :return: None
""" """
self._api: ccxt.Exchange = None
self._api_async: ccxt_async.Exchange = None
self._config.update(config) self._config.update(config)
self._cached_ticker: Dict[str, Any] = {} self._cached_ticker: Dict[str, Any] = {}
@ -117,9 +120,9 @@ class Exchange(object):
self._ohlcv_partial_candle = self._ft_has['ohlcv_partial_candle'] self._ohlcv_partial_candle = self._ft_has['ohlcv_partial_candle']
# Initialize ccxt objects # Initialize ccxt objects
self._api: ccxt.Exchange = self._init_ccxt( self._api = self._init_ccxt(
exchange_config, ccxt_kwargs=exchange_config.get('ccxt_config')) exchange_config, ccxt_kwargs=exchange_config.get('ccxt_config'))
self._api_async: ccxt_async.Exchange = self._init_ccxt( self._api_async = self._init_ccxt(
exchange_config, ccxt_async, ccxt_kwargs=exchange_config.get('ccxt_async_config')) exchange_config, ccxt_async, ccxt_kwargs=exchange_config.get('ccxt_async_config'))
logger.info('Using Exchange "%s"', self.name) logger.info('Using Exchange "%s"', self.name)
@ -171,8 +174,10 @@ class Exchange(object):
try: try:
api = getattr(ccxt_module, name.lower())(ex_config) api = getattr(ccxt_module, name.lower())(ex_config)
except (KeyError, AttributeError): except (KeyError, AttributeError) as e:
raise OperationalException(f'Exchange {name} is not supported') 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) self.set_sandbox(api, exchange_config, name)
@ -265,10 +270,28 @@ class Exchange(object):
f'Pair {pair} is not available on {self.name}. ' f'Pair {pair} is not available on {self.name}. '
f'Please remove {pair} from your whitelist.') f'Please remove {pair} from your whitelist.')
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: def validate_timeframes(self, timeframe: List[str]) -> None:
""" """
Checks if ticker interval from config is a supported timeframe on the exchange 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 timeframes = self._api.timeframes
if timeframe not in timeframes: if timeframe not in timeframes:
raise OperationalException( raise OperationalException(
@ -364,7 +387,9 @@ class Exchange(object):
try: try:
# Set the precision for amount and price(rate) as accepted by the exchange # Set the precision for amount and price(rate) as accepted by the exchange
amount = self.symbol_amount_prec(pair, amount) 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, return self._api.create_order(pair, ordertype, side,
amount, rate, params) amount, rate, params)
@ -372,18 +397,18 @@ class Exchange(object):
except ccxt.InsufficientFunds as e: except ccxt.InsufficientFunds as e:
raise DependencyException( raise DependencyException(
f'Insufficient funds to create {ordertype} {side} order on market {pair}.' f'Insufficient funds to create {ordertype} {side} order on market {pair}.'
f'Tried to {side} amount {amount} at rate {rate} (total {rate*amount}).' f'Tried to {side} amount {amount} at rate {rate} (total {rate * amount}).'
f'Message: {e}') f'Message: {e}') from e
except ccxt.InvalidOrder as e: except ccxt.InvalidOrder as e:
raise DependencyException( raise DependencyException(
f'Could not create {ordertype} {side} order on market {pair}.' f'Could not create {ordertype} {side} order on market {pair}.'
f'Tried to {side} amount {amount} at rate {rate} (total {rate*amount}).' f'Tried to {side} amount {amount} at rate {rate} (total {rate * amount}).'
f'Message: {e}') f'Message: {e}') from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( 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: except ccxt.BaseError as e:
raise OperationalException(e) raise OperationalException(e) from e
def buy(self, pair: str, ordertype: str, amount: float, def buy(self, pair: str, ordertype: str, amount: float,
rate: float, time_in_force) -> Dict: rate: float, time_in_force) -> Dict:
@ -468,9 +493,9 @@ class Exchange(object):
return balances return balances
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( 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: except ccxt.BaseError as e:
raise OperationalException(e) raise OperationalException(e) from e
@retrier @retrier
def get_tickers(self) -> Dict: def get_tickers(self) -> Dict:
@ -479,18 +504,18 @@ class Exchange(object):
except ccxt.NotSupported as e: except ccxt.NotSupported as e:
raise OperationalException( raise OperationalException(
f'Exchange {self._api.name} does not support fetching tickers in batch.' 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: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( 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: except ccxt.BaseError as e:
raise OperationalException(e) raise OperationalException(e) from e
@retrier @retrier
def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict: def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict:
if refresh or pair not in self._cached_ticker.keys(): if refresh or pair not in self._cached_ticker.keys():
try: 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") raise DependencyException(f"Pair {pair} not available")
data = self._api.fetch_ticker(pair) data = self._api.fetch_ticker(pair)
try: try:
@ -503,9 +528,9 @@ class Exchange(object):
return data return data
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( 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: except ccxt.BaseError as e:
raise OperationalException(e) raise OperationalException(e) from e
else: else:
logger.info("returning cached ticker-data for %s", pair) logger.info("returning cached ticker-data for %s", pair)
return self._cached_ticker[pair] return self._cached_ticker[pair]
@ -626,12 +651,12 @@ class Exchange(object):
except ccxt.NotSupported as e: except ccxt.NotSupported as e:
raise OperationalException( raise OperationalException(
f'Exchange {self._api.name} does not support fetching historical candlestick data.' 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: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(f'Could not load ticker history due to {e.__class__.__name__}. '
f'Could not load ticker history due to {e.__class__.__name__}. Message: {e}') f'Message: {e}') from e
except ccxt.BaseError as 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 @retrier
def cancel_order(self, order_id: str, pair: str) -> None: def cancel_order(self, order_id: str, pair: str) -> None:
@ -642,12 +667,12 @@ class Exchange(object):
return self._api.cancel_order(order_id, pair) return self._api.cancel_order(order_id, pair)
except ccxt.InvalidOrder as e: except ccxt.InvalidOrder as e:
raise InvalidOrderException( raise InvalidOrderException(
f'Could not cancel order. Message: {e}') f'Could not cancel order. Message: {e}') from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( 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: except ccxt.BaseError as e:
raise OperationalException(e) raise OperationalException(e) from e
@retrier @retrier
def get_order(self, order_id: str, pair: str) -> Dict: def get_order(self, order_id: str, pair: str) -> Dict:
@ -658,12 +683,12 @@ class Exchange(object):
return self._api.fetch_order(order_id, pair) return self._api.fetch_order(order_id, pair)
except ccxt.InvalidOrder as e: except ccxt.InvalidOrder as e:
raise InvalidOrderException( raise InvalidOrderException(
f'Tried to get an invalid order (id: {order_id}). Message: {e}') f'Tried to get an invalid order (id: {order_id}). Message: {e}') from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( 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: except ccxt.BaseError as e:
raise OperationalException(e) raise OperationalException(e) from e
@retrier @retrier
def get_order_book(self, pair: str, limit: int = 100) -> dict: def get_order_book(self, pair: str, limit: int = 100) -> dict:
@ -679,12 +704,12 @@ class Exchange(object):
except ccxt.NotSupported as e: except ccxt.NotSupported as e:
raise OperationalException( raise OperationalException(
f'Exchange {self._api.name} does not support fetching order book.' 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: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( 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: except ccxt.BaseError as e:
raise OperationalException(e) raise OperationalException(e) from e
@retrier @retrier
def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List: def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List:
@ -701,9 +726,9 @@ class Exchange(object):
except ccxt.NetworkError as e: except ccxt.NetworkError as e:
raise TemporaryError( 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: except ccxt.BaseError as e:
raise OperationalException(e) raise OperationalException(e) from e
@retrier @retrier
def get_fee(self, symbol='ETH/BTC', type='', side='', amount=1, def get_fee(self, symbol='ETH/BTC', type='', side='', amount=1,
@ -717,13 +742,13 @@ class Exchange(object):
price=price, takerOrMaker=taker_or_maker)['rate'] price=price, takerOrMaker=taker_or_maker)['rate']
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( 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: except ccxt.BaseError as e:
raise OperationalException(e) raise OperationalException(e) from e
def is_exchange_bad(exchange: str) -> bool: def is_exchange_bad(exchange: str) -> bool:
return exchange in ['bitmex'] return exchange in ['bitmex', 'bitstamp']
def is_exchange_available(exchange: str, ccxt_module=None) -> bool: def is_exchange_available(exchange: str, ccxt_module=None) -> bool:

View File

@ -478,8 +478,11 @@ class FreqtradeBot(object):
return order_amount return order_amount
# use fee from order-dict if possible # use fee from order-dict if possible
if 'fee' in order and order['fee'] and (order['fee'].keys() >= {'currency', 'cost'}): if ('fee' in order and order['fee'] is not None and
if trade.pair.startswith(order['fee']['currency']): (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'] new_amount = order_amount - order['fee']['cost']
logger.info("Applying fee on amount for %s (from %s to %s) from Order", logger.info("Applying fee on amount for %s (from %s to %s) from Order",
trade, order['amount'], new_amount) trade, order['amount'], new_amount)
@ -496,9 +499,12 @@ class FreqtradeBot(object):
fee_abs = 0 fee_abs = 0
for exectrade in trades: for exectrade in trades:
amount += exectrade['amount'] 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! # 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'] fee_abs += exectrade['fee']['cost']
if amount != order_amount: if amount != order_amount:
@ -518,7 +524,11 @@ class FreqtradeBot(object):
if trade.open_order_id: if trade.open_order_id:
# Update trade with order values # Update trade with order values
logger.info('Found open order for %s', trade) logger.info('Found open order for %s', trade)
order = action_order or self.exchange.get_order(trade.open_order_id, trade.pair) 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 update amount (binance-fix)
try: try:
new_amount = self.get_real_amount(trade, order) new_amount = self.get_real_amount(trade, order)
@ -586,13 +596,13 @@ class FreqtradeBot(object):
logger.info(' order book asks top %s: %0.8f', i, order_book_rate) logger.info(' order book asks top %s: %0.8f', i, order_book_rate)
sell_rate = 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 return True
else: else:
logger.debug('checking sell') logger.debug('checking sell')
sell_rate = self.get_sell_rate(trade.pair, True) 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 return True
logger.debug('Found no sell signal for %s.', trade) logger.debug('Found no sell signal for %s.', trade)
@ -662,7 +672,7 @@ class FreqtradeBot(object):
if stoploss_order and stoploss_order['status'] == 'closed': if stoploss_order and stoploss_order['status'] == 'closed':
trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
trade.update(stoploss_order) trade.update(stoploss_order)
self.notify_sell(trade) self._notify_sell(trade)
return True return True
# Finally we check if stoploss on exchange should be moved up because of trailing. # Finally we check if stoploss on exchange should be moved up because of trailing.
@ -707,13 +717,15 @@ class FreqtradeBot(object):
logger.exception(f"Could create trailing stoploss order " logger.exception(f"Could create trailing stoploss order "
f"for pair {trade.pair}.") f"for pair {trade.pair}.")
def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool: def _check_and_execute_sell(self, trade: Trade, sell_rate: float,
if self.edge: buy: bool, sell: bool) -> bool:
stoploss = self.edge.stoploss(trade.pair) """
should_sell = self.strategy.should_sell( Check and execute sell
trade, sell_rate, datetime.utcnow(), buy, sell, force_stoploss=stoploss) """
else: should_sell = self.strategy.should_sell(
should_sell = self.strategy.should_sell(trade, sell_rate, datetime.utcnow(), buy, 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: if should_sell.sell_flag:
self.execute_sell(trade, sell_rate, should_sell.sell_type) self.execute_sell(trade, sell_rate, should_sell.sell_type)
@ -741,7 +753,7 @@ class FreqtradeBot(object):
if not trade.open_order_id: if not trade.open_order_id:
continue continue
order = self.exchange.get_order(trade.open_order_id, trade.pair) order = self.exchange.get_order(trade.open_order_id, trade.pair)
except (RequestException, DependencyException): except (RequestException, DependencyException, InvalidOrderException):
logger.info( logger.info(
'Cannot query order for %s due to %s', 'Cannot query order for %s due to %s',
trade, trade,
@ -867,9 +879,9 @@ class FreqtradeBot(object):
trade.close_rate_requested = limit trade.close_rate_requested = limit
trade.sell_reason = sell_reason.value trade.sell_reason = sell_reason.value
Trade.session.flush() 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. Sends rpc notification when a sell occured.
""" """

50
freqtrade/loggers.py Normal file
View File

@ -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)

View File

@ -15,8 +15,7 @@ from argparse import Namespace
from typing import Any, List from typing import Any, List
from freqtrade import OperationalException from freqtrade import OperationalException
from freqtrade.arguments import Arguments from freqtrade.configuration import Arguments
from freqtrade.configuration import set_loggers
from freqtrade.worker import Worker from freqtrade.worker import Worker
@ -32,8 +31,6 @@ def main(sysargv: List[str] = None) -> None:
return_code: Any = 1 return_code: Any = 1
worker = None worker = None
try: try:
set_loggers()
arguments = Arguments( arguments = Arguments(
sysargv, sysargv,
'Free, open source crypto trading bot' 'Free, open source crypto trading bot'

View File

@ -5,10 +5,8 @@ import gzip
import logging import logging
import re import re
from datetime import datetime from datetime import datetime
from typing import Dict
import numpy as np import numpy as np
from pandas import DataFrame
import rapidjson import rapidjson
@ -41,24 +39,6 @@ def datesarray_to_datetimearray(dates: np.ndarray) -> np.ndarray:
return dates.dt.to_pydatetime() 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: def file_dump_json(filename, data, is_zip=False) -> None:
""" """
Dump JSON data into a file Dump JSON data into a file

View File

@ -12,7 +12,7 @@ from typing import Any, Dict, List, NamedTuple, Optional
from pandas import DataFrame from pandas import DataFrame
from tabulate import tabulate from tabulate import tabulate
from freqtrade.arguments import Arguments from freqtrade.configuration import Arguments
from freqtrade.data import history from freqtrade.data import history
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange import timeframe_to_minutes
@ -252,22 +252,20 @@ class Backtesting(object):
sell = self.strategy.should_sell(trade, sell_row.open, sell_row.date, sell_row.buy, sell = self.strategy.should_sell(trade, sell_row.open, sell_row.date, sell_row.buy,
sell_row.sell, low=sell_row.low, high=sell_row.high) sell_row.sell, low=sell_row.low, high=sell_row.high)
if sell.sell_flag: if sell.sell_flag:
trade_dur = int((sell_row.date - buy_row.date).total_seconds() // 60) trade_dur = int((sell_row.date - buy_row.date).total_seconds() // 60)
# Special handling if high or low hit STOP_LOSS or ROI # Special handling if high or low hit STOP_LOSS or ROI
if sell.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): if sell.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
# Set close_rate to stoploss # Set close_rate to stoploss
closerate = trade.stop_loss closerate = trade.stop_loss
elif sell.sell_type == (SellType.ROI): elif sell.sell_type == (SellType.ROI):
# get next entry in min_roi > to trade duration roi = self.strategy.min_roi_reached_entry(trade_dur)
# Interface.py skips on trade_duration <= duration if roi is not None:
roi_entry = max(list(filter(lambda x: trade_dur >= x, # - (Expected abs profit + open_rate + open_fee) / (fee_close -1)
self.strategy.minimal_roi.keys()))) closerate = - (trade.open_rate * roi + trade.open_rate *
roi = self.strategy.minimal_roi[roi_entry] (1 + trade.fee_open)) / (trade.fee_close - 1)
else:
# - (Expected abs profit + open_rate + open_fee) / (fee_close -1) # This should not be reached...
closerate = - (trade.open_rate * roi + trade.open_rate * closerate = sell_row.open
(1 + trade.fee_open)) / (trade.fee_close - 1)
else: else:
closerate = sell_row.open closerate = sell_row.open
@ -321,6 +319,9 @@ class Backtesting(object):
position_stacking: do we allow position stacking? (default: False) position_stacking: do we allow position stacking? (default: False)
:return: DataFrame :return: DataFrame
""" """
# 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'] processed = args['processed']
stake_amount = args['stake_amount'] stake_amount = args['stake_amount']
max_open_trades = args.get('max_open_trades', 0) max_open_trades = args.get('max_open_trades', 0)

View File

@ -1,17 +1,15 @@
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
from functools import reduce
from typing import Any, Callable, Dict, List
import talib.abstract as ta import talib.abstract as ta
from pandas import DataFrame from pandas import DataFrame
from 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, Real
import freqtrade.vendor.qtpylib.indicators as qtpylib import freqtrade.vendor.qtpylib.indicators as qtpylib
from freqtrade.optimize.hyperopt_interface import IHyperOpt from freqtrade.optimize.hyperopt_interface import IHyperOpt
class_name = 'DefaultHyperOpts'
class DefaultHyperOpts(IHyperOpt): class DefaultHyperOpts(IHyperOpt):
""" """

View File

@ -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

View File

@ -9,7 +9,7 @@ from tabulate import tabulate
from freqtrade import constants from freqtrade import constants
from freqtrade.edge import Edge from freqtrade.edge import Edge
from freqtrade.arguments import Arguments from freqtrade.configuration import Arguments
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.resolvers import StrategyResolver from freqtrade.resolvers import StrategyResolver

View File

@ -7,7 +7,7 @@ This module contains the hyperopt logic
import logging import logging
import os import os
import sys import sys
from math import exp
from operator import itemgetter from operator import itemgetter
from pathlib import Path from pathlib import Path
from pprint import pprint from pprint import pprint
@ -18,10 +18,12 @@ from pandas import DataFrame
from skopt import Optimizer from skopt import Optimizer
from skopt.space import Dimension from skopt.space import Dimension
from freqtrade.arguments import Arguments from freqtrade.configuration import Arguments
from freqtrade.data.history import load_data, get_timeframe from freqtrade.data.history import load_data, get_timeframe
from freqtrade.optimize.backtesting import Backtesting from freqtrade.optimize.backtesting import Backtesting
from freqtrade.resolvers.hyperopt_resolver 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__) logger = logging.getLogger(__name__)
@ -46,27 +48,46 @@ class Hyperopt(Backtesting):
super().__init__(config) super().__init__(config)
self.custom_hyperopt = HyperOptResolver(self.config).hyperopt self.custom_hyperopt = HyperOptResolver(self.config).hyperopt
# set TARGET_TRADES to suit your number concurrent trades so its realistic self.custom_hyperoptloss = HyperOptLossResolver(self.config).hyperoptloss
# to the number of days self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function
self.target_trades = 600
self.total_tries = config.get('epochs', 0) self.total_tries = config.get('epochs', 0)
self.current_best_loss = 100 self.current_best_loss = 100
# max average trade duration in minutes if not self.config.get('hyperopt_continue'):
# if eval ends with higher value, we consider it a failed eval self.clean_hyperopt()
self.max_accepted_trade_duration = 300 else:
logger.info("Continuing on previous hyperopt results.")
# 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Σ%.
self.expected_max_profit = 3.0
# Previous evaluations # Previous evaluations
self.trials_file = TRIALSDATA_PICKLE self.trials_file = TRIALSDATA_PICKLE
self.trials: List = [] self.trials: List = []
# Populate functions here (hasattr is slow so should not be run during "regular" operations)
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),
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): def get_args(self, params):
dimensions = self.hyperopt_space() dimensions = self.hyperopt_space()
# Ensure the number of dimensions match # Ensure the number of dimensions match
@ -134,16 +155,6 @@ class Hyperopt(Backtesting):
print('.', end='') print('.', end='')
sys.stdout.flush() 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 has_space(self, space: str) -> bool: def has_space(self, space: str) -> bool:
""" """
Tell if a space value is contained in the configuration Tell if a space value is contained in the configuration
@ -172,39 +183,40 @@ class Hyperopt(Backtesting):
return spaces return spaces
def generate_optimizer(self, _params: Dict) -> Dict: 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) params = self.get_args(_params)
if self.has_space('roi'): if self.has_space('roi'):
self.strategy.minimal_roi = self.custom_hyperopt.generate_roi_table(params) self.strategy.minimal_roi = self.custom_hyperopt.generate_roi_table(params)
if self.has_space('buy'): if self.has_space('buy'):
self.advise_buy = self.custom_hyperopt.buy_strategy_generator(params) self.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'): if self.has_space('sell'):
self.advise_sell = self.custom_hyperopt.sell_strategy_generator(params) 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'): if self.has_space('stoploss'):
self.strategy.stoploss = params['stoploss'] self.strategy.stoploss = params['stoploss']
processed = load(TICKERDATA_PICKLE) processed = load(TICKERDATA_PICKLE)
min_date, max_date = get_timeframe(processed) min_date, max_date = get_timeframe(processed)
results = self.backtest( results = self.backtest(
{ {
'stake_amount': self.config['stake_amount'], 'stake_amount': self.config['stake_amount'],
'processed': processed, '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, 'start_date': min_date,
'end_date': max_date, 'end_date': max_date,
} }
) )
result_explanation = self.format_results(results) result_explanation = self.format_results(results)
total_profit = results.profit_percent.sum()
trade_count = len(results.index) trade_count = len(results.index)
trade_duration = results.trade_duration.mean()
# If this evaluation contains too short amount of trades to be # If this evaluation contains too short amount of trades to be
# interesting -- consider it as 'bad' (assigned max. loss value) # interesting -- consider it as 'bad' (assigned max. loss value)
@ -217,7 +229,8 @@ class Hyperopt(Backtesting):
'result': result_explanation, 'result': result_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 { return {
'loss': loss, 'loss': loss,
@ -288,9 +301,8 @@ class Hyperopt(Backtesting):
(max_date - min_date).days (max_date - min_date).days
) )
if self.has_space('buy') or self.has_space('sell'): self.strategy.advise_indicators = \
self.strategy.advise_indicators = \ self.custom_hyperopt.populate_indicators # type: ignore
self.custom_hyperopt.populate_indicators # type: ignore
preprocessed = self.strategy.tickerdata_to_dataframe(data) preprocessed = self.strategy.tickerdata_to_dataframe(data)

View File

@ -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
"""

View File

@ -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

View File

@ -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

View File

@ -1,22 +1,68 @@
import logging import logging
from typing import List from pathlib import Path
from typing import Dict, List, Optional
import pandas as pd import pandas as pd
from pathlib import Path
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__) logger = logging.getLogger(__name__)
try: try:
from plotly import tools from plotly.subplots import make_subplots
from plotly.offline import plot from plotly.offline import plot
import plotly.graph_objs as go import plotly.graph_objects as go
except ImportError: except ImportError:
logger.exception("Module plotly not found \n Please install using `pip install plotly`") logger.exception("Module plotly not found \n Please install using `pip install plotly`")
exit(1) exit(1)
def generate_row(fig, row, indicators: List[str], data: pd.DataFrame) -> tools.make_subplots: 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 Generator all the indicator selected by the user for a specific row
:param fig: Plot figure to append to :param fig: Plot figure to append to
@ -33,7 +79,7 @@ def generate_row(fig, row, indicators: List[str], data: pd.DataFrame) -> tools.m
mode='lines', mode='lines',
name=indicator name=indicator
) )
fig.append_trace(scattergl, row, 1) fig.add_trace(scattergl, row, 1)
else: else:
logger.info( logger.info(
'Indicator "%s" ignored. Reason: This indicator is not found ' 'Indicator "%s" ignored. Reason: This indicator is not found '
@ -44,9 +90,29 @@ def generate_row(fig, row, indicators: List[str], data: pd.DataFrame) -> tools.m
return fig return fig
def plot_trades(fig, trades: pd.DataFrame): def add_profit(fig, row, data: pd.DataFrame, column: str, name: str) -> make_subplots:
""" """
Plot trades to "fig" 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 # Trades can be empty
if trades is not None and len(trades) > 0: if trades is not None and len(trades) > 0:
@ -79,20 +145,16 @@ def plot_trades(fig, trades: pd.DataFrame):
color='red' color='red'
) )
) )
fig.append_trace(trade_buys, 1, 1) fig.add_trace(trade_buys, 1, 1)
fig.append_trace(trade_sells, 1, 1) fig.add_trace(trade_sells, 1, 1)
else: else:
logger.warning("No trades found.") logger.warning("No trades found.")
return fig return fig
def generate_graph( def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFrame = None,
pair: str, indicators1: List[str] = [],
data: pd.DataFrame, indicators2: List[str] = [],) -> go.Figure:
trades: pd.DataFrame = None,
indicators1: List[str] = [],
indicators2: List[str] = [],
) -> go.Figure:
""" """
Generate the graph from the data generated by Backtesting or from DB 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 Volume will always be ploted in row2, so Row 1 and 3 are to our disposal for custom indicators
@ -105,7 +167,7 @@ def generate_graph(
""" """
# Define the graph # Define the graph
fig = tools.make_subplots( fig = make_subplots(
rows=3, rows=3,
cols=1, cols=1,
shared_xaxes=True, shared_xaxes=True,
@ -127,7 +189,7 @@ def generate_graph(
close=data.close, close=data.close,
name='Price' name='Price'
) )
fig.append_trace(candles, 1, 1) fig.add_trace(candles, 1, 1)
if 'buy' in data.columns: if 'buy' in data.columns:
df_buy = data[data['buy'] == 1] df_buy = data[data['buy'] == 1]
@ -144,7 +206,7 @@ def generate_graph(
color='green', color='green',
) )
) )
fig.append_trace(buys, 1, 1) fig.add_trace(buys, 1, 1)
else: else:
logger.warning("No buy-signals found.") logger.warning("No buy-signals found.")
@ -163,7 +225,7 @@ def generate_graph(
color='red', color='red',
) )
) )
fig.append_trace(sells, 1, 1) fig.add_trace(sells, 1, 1)
else: else:
logger.warning("No sell-signals found.") logger.warning("No sell-signals found.")
@ -182,11 +244,11 @@ def generate_graph(
fillcolor="rgba(0,176,246,0.2)", fillcolor="rgba(0,176,246,0.2)",
line={'color': 'rgba(255,255,255,0)'}, line={'color': 'rgba(255,255,255,0)'},
) )
fig.append_trace(bb_lower, 1, 1) fig.add_trace(bb_lower, 1, 1)
fig.append_trace(bb_upper, 1, 1) fig.add_trace(bb_upper, 1, 1)
# Add indicators to main plot # Add indicators to main plot
fig = generate_row(fig=fig, row=1, indicators=indicators1, data=data) fig = add_indicators(fig=fig, row=1, indicators=indicators1, data=data)
fig = plot_trades(fig, trades) fig = plot_trades(fig, trades)
@ -196,15 +258,57 @@ def generate_graph(
y=data['volume'], y=data['volume'],
name='Volume' name='Volume'
) )
fig.append_trace(volume, 2, 1) fig.add_trace(volume, 2, 1)
# Add indicators to seperate row # Add indicators to seperate row
fig = generate_row(fig=fig, row=3, indicators=indicators2, data=data) fig = add_indicators(fig=fig, row=3, indicators=indicators2, data=data)
return fig return fig
def generate_plot_file(fig, pair, ticker_interval) -> None: 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 Generate a plot html file from pre populated fig plotly object
:param fig: Plotly Figure to plot :param fig: Plotly Figure to plot
@ -212,12 +316,8 @@ def generate_plot_file(fig, pair, ticker_interval) -> None:
:param ticker_interval: Used as part of the filename :param ticker_interval: Used as part of the filename
:return: None :return: None
""" """
logger.info('Generate plot file for %s', pair)
pair_name = pair.replace("/", "_")
file_name = 'freqtrade-plot-' + pair_name + '-' + ticker_interval + '.html'
Path("user_data/plots").mkdir(parents=True, exist_ok=True) Path("user_data/plots").mkdir(parents=True, exist_ok=True)
plot(fig, filename=str(Path('user_data/plots').joinpath(file_name)), plot(fig, filename=str(Path('user_data/plots').joinpath(filename)),
auto_open=False) auto_open=auto_open)

View File

@ -28,6 +28,7 @@ class ExchangeResolver(IResolver):
except ImportError: except ImportError:
logger.info( logger.info(
f"No {exchange_name} specific subclass found. Using the generic class instead.") f"No {exchange_name} specific subclass found. Using the generic class instead.")
if not hasattr(self, "exchange"):
self.exchange = Exchange(config) self.exchange = Exchange(config)
def _load_exchange( def _load_exchange(
@ -44,13 +45,13 @@ class ExchangeResolver(IResolver):
exchange = ex_class(kwargs['config']) exchange = ex_class(kwargs['config'])
if exchange: if exchange:
logger.info("Using resolved exchange %s", exchange_name) logger.info(f"Using resolved exchange '{exchange_name}'...")
return exchange return exchange
except AttributeError: except AttributeError:
# Pass and raise ImportError instead # Pass and raise ImportError instead
pass pass
raise ImportError( raise ImportError(
"Impossible to load Exchange '{}'. This class does not exist" f"Impossible to load Exchange '{exchange_name}'. This class does not exist "
" or contains Python code errors".format(exchange_name) "or contains Python code errors."
) )

View File

@ -7,8 +7,10 @@ import logging
from pathlib import Path from pathlib import Path
from typing import Optional, Dict 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_interface import IHyperOpt
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss
from freqtrade.resolvers import IResolver from freqtrade.resolvers import IResolver
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -21,12 +23,11 @@ class HyperOptResolver(IResolver):
__slots__ = ['hyperopt'] __slots__ = ['hyperopt']
def __init__(self, config: Optional[Dict] = None) -> None: def __init__(self, config: Dict) -> None:
""" """
Load the custom class from config parameter Load the custom class from config parameter
:param config: configuration dictionary or None :param config: configuration dictionary
""" """
config = config or {}
# Verify the hyperopt is in the configuration, otherwise fallback to the default hyperopt # Verify the hyperopt is in the configuration, otherwise fallback to the default hyperopt
hyperopt_name = config.get('hyperopt') or DEFAULT_HYPEROPT hyperopt_name = config.get('hyperopt') or DEFAULT_HYPEROPT
@ -53,25 +54,75 @@ class HyperOptResolver(IResolver):
current_path = Path(__file__).parent.parent.joinpath('optimize').resolve() current_path = Path(__file__).parent.parent.joinpath('optimize').resolve()
abs_paths = [ abs_paths = [
current_path.parent.parent.joinpath('user_data/hyperopts'), Path.cwd().joinpath('user_data/hyperopts'),
current_path, current_path,
] ]
if extra_dir: if extra_dir:
# Add extra hyperopt directory on top of search paths # 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: hyperopt = self._load_object(paths=abs_paths, object_type=IHyperOpt,
try: object_name=hyperopt_name)
hyperopt = self._search_object(directory=_path, object_type=IHyperOpt, if hyperopt:
object_name=hyperopt_name) return hyperopt
if hyperopt: raise OperationalException(
logger.info("Using resolved hyperopt %s from '%s'", hyperopt_name, _path) f"Impossible to load Hyperopt '{hyperopt_name}'. This class does not exist "
return hyperopt "or contains Python code errors."
except FileNotFoundError: )
logger.warning('Path "%s" does not exist', _path.relative_to(Path.cwd()))
raise ImportError( class HyperOptLossResolver(IResolver):
"Impossible to load Hyperopt '{}'. This class does not exist" """
" or contains Python code errors".format(hyperopt_name) 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."
) )

View File

@ -7,7 +7,7 @@ import importlib.util
import inspect import inspect
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Optional, Type, Any from typing import Any, List, Optional, Tuple, Type, Union
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -45,7 +45,7 @@ class IResolver(object):
@staticmethod @staticmethod
def _search_object(directory: Path, object_type, object_name: str, 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 Search for the objectname in the given directory
:param directory: relative or absolute directory path :param directory: relative or absolute directory path
@ -57,9 +57,33 @@ class IResolver(object):
if not str(entry).endswith('.py'): if not str(entry).endswith('.py'):
logger.debug('Ignoring %s', entry) logger.debug('Ignoring %s', entry)
continue continue
module_path = Path.resolve(directory.joinpath(entry))
obj = IResolver._get_valid_object( obj = IResolver._get_valid_object(
object_type, Path.resolve(directory.joinpath(entry)), object_name object_type, module_path, object_name
) )
if obj: 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 return None

View File

@ -6,6 +6,7 @@ This module load custom hyperopts
import logging import logging
from pathlib import Path from pathlib import Path
from freqtrade import OperationalException
from freqtrade.pairlist.IPairList import IPairList from freqtrade.pairlist.IPairList import IPairList
from freqtrade.resolvers import IResolver from freqtrade.resolvers import IResolver
@ -38,22 +39,15 @@ class PairListResolver(IResolver):
current_path = Path(__file__).parent.parent.joinpath('pairlist').resolve() current_path = Path(__file__).parent.parent.joinpath('pairlist').resolve()
abs_paths = [ abs_paths = [
current_path.parent.parent.joinpath('user_data/pairlist'), Path.cwd().joinpath('user_data/pairlist'),
current_path, current_path,
] ]
for _path in abs_paths: pairlist = self._load_object(paths=abs_paths, object_type=IPairList,
try: object_name=pairlist_name, kwargs=kwargs)
pairlist = self._search_object(directory=_path, object_type=IPairList, if pairlist:
object_name=pairlist_name, return pairlist
kwargs=kwargs) raise OperationalException(
if pairlist: f"Impossible to load Pairlist '{pairlist_name}'. This class does not exist "
logger.info("Using resolved pairlist %s from '%s'", pairlist_name, _path) "or contains Python code errors."
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)
) )

View File

@ -11,7 +11,7 @@ from inspect import getfullargspec
from pathlib import Path from pathlib import Path
from typing import Dict, Optional from typing import Dict, Optional
from freqtrade import constants from freqtrade import constants, OperationalException
from freqtrade.resolvers import IResolver from freqtrade.resolvers import IResolver
from freqtrade.strategy import import_strategy from freqtrade.strategy import import_strategy
from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.interface import IStrategy
@ -132,7 +132,7 @@ class StrategyResolver(IResolver):
abs_paths.insert(0, Path(extra_dir).resolve()) abs_paths.insert(0, Path(extra_dir).resolve())
if ":" in strategy_name: if ":" in strategy_name:
logger.info("loading base64 endocded strategy") logger.info("loading base64 encoded strategy")
strat = strategy_name.split(":") strat = strategy_name.split(":")
if len(strat) == 2: if len(strat) == 2:
@ -147,25 +147,21 @@ class StrategyResolver(IResolver):
# register temp path with the bot # register temp path with the bot
abs_paths.insert(0, temp.resolve()) abs_paths.insert(0, temp.resolve())
for _path in abs_paths: strategy = self._load_object(paths=abs_paths, object_type=IStrategy,
try: object_name=strategy_name, kwargs={'config': config})
strategy = self._search_object(directory=_path, object_type=IStrategy, if strategy:
object_name=strategy_name, kwargs={'config': config}) strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args)
if strategy: strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args)
logger.info("Using resolved strategy %s from '%s'", strategy_name, _path) strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args)
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:
return import_strategy(strategy, config=config)
except TypeError as e:
logger.warning(
f"Impossible to load strategy '{strategy}' from {_path}. Error: {e}")
except FileNotFoundError:
logger.warning('Path "%s" does not exist', _path.relative_to(Path.cwd()))
raise ImportError( try:
f"Impossible to load Strategy '{strategy_name}'. This class does not exist" return import_strategy(strategy, config=config)
" or contains Python code errors" except TypeError as e:
logger.warning(
f"Impossible to load strategy '{strategy_name}'. "
f"Error: {e}")
raise OperationalException(
f"Impossible to load Strategy '{strategy_name}'. This class does not exist "
"or contains Python code errors."
) )

View File

@ -281,10 +281,11 @@ class RPC(object):
rate = 1.0 rate = 1.0
else: else:
try: try:
if coin in('USDT', 'USD', 'EUR'): pair = self._freqtrade.exchange.get_valid_pair_combination(coin, "BTC")
rate = 1.0 / self._freqtrade.get_sell_rate('BTC/' + coin, False) if pair.startswith("BTC"):
rate = 1.0 / self._freqtrade.get_sell_rate(pair, False)
else: else:
rate = self._freqtrade.get_sell_rate(coin + '/BTC', False) rate = self._freqtrade.get_sell_rate(pair, False)
except (TemporaryError, DependencyException): except (TemporaryError, DependencyException):
logger.warning(f" Could not get rate for pair {coin}.") logger.warning(f" Could not get rate for pair {coin}.")
continue continue
@ -298,7 +299,10 @@ class RPC(object):
'est_btc': est_btc, 'est_btc': est_btc,
}) })
if total == 0.0: 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 symbol = fiat_display_currency
value = self._fiat_converter.convert_amount(total, 'BTC', value = self._fiat_converter.convert_amount(total, 'BTC',

View File

@ -217,7 +217,8 @@ class Telegram(RPC):
"*Open Order:* `{open_order}`" if r['open_order'] else "" "*Open Order:* `{open_order}`" if r['open_order'] else ""
] ]
messages.append("\n".join(filter(None, lines)).format(**r)) # Filter empty lines using list-comprehension
messages.append("\n".join([l for l in lines if l]).format(**r))
for msg in messages: for msg in messages:
self._send_msg(msg, bot=bot) self._send_msg(msg, bot=bot)

View File

@ -6,7 +6,7 @@ import logging
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from datetime import datetime from datetime import datetime
from enum import Enum from enum import Enum
from typing import Dict, List, NamedTuple, Tuple from typing import Dict, List, NamedTuple, Optional, Tuple
import warnings import warnings
import arrow import arrow
@ -347,23 +347,32 @@ class IStrategy(ABC):
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) 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: 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!! sell. Requires current_profit to be in percent!!
:return: True if bot should sell at current rate :return: True if bot should sell at current rate
""" """
# Check if time matches and current rate is above threshold # Check if time matches and current rate is above threshold
trade_dur = (current_time.timestamp() - trade.open_date.timestamp()) / 60 trade_dur = int((current_time.timestamp() - trade.open_date.timestamp()) // 60)
roi = self.min_roi_reached_entry(trade_dur)
# Get highest entry in ROI dict where key >= trade-duration if roi is None:
roi_entry = max(list(filter(lambda x: trade_dur >= x, self.minimal_roi.keys()))) return False
threshold = self.minimal_roi[roi_entry] else:
if current_profit > threshold: return current_profit > roi
return True
return False
def tickerdata_to_dataframe(self, tickerdata: Dict[str, List]) -> Dict[str, DataFrame]: def tickerdata_to_dataframe(self, tickerdata: Dict[str, List]) -> Dict[str, DataFrame]:
""" """

View File

@ -6,7 +6,6 @@ from copy import deepcopy
from datetime import datetime from datetime import datetime
from functools import reduce from functools import reduce
from pathlib import Path from pathlib import Path
from typing import List
from unittest.mock import MagicMock, PropertyMock from unittest.mock import MagicMock, PropertyMock
import arrow import arrow
@ -14,7 +13,7 @@ import pytest
from telegram import Chat, Message, Update from telegram import Chat, Message, Update
from freqtrade import constants, persistence from freqtrade import constants, persistence
from freqtrade.arguments import Arguments from freqtrade.configuration import Arguments
from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.converter import parse_ticker_dataframe
from freqtrade.edge import Edge, PairInfo from freqtrade.edge import Edge, PairInfo
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
@ -22,6 +21,7 @@ from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.resolvers import ExchangeResolver from freqtrade.resolvers import ExchangeResolver
from freqtrade.worker import Worker from freqtrade.worker import Worker
logging.getLogger('').setLevel(logging.INFO) logging.getLogger('').setLevel(logging.INFO)
@ -39,10 +39,17 @@ def log_has_re(line, logs):
False) False)
def get_args(args) -> List[str]: def get_args(args):
return Arguments(args, '').get_parsed_arg() return Arguments(args, '').get_parsed_arg()
def patched_configuration_load_config_file(mocker, config) -> None:
mocker.patch(
'freqtrade.configuration.configuration.Configuration._load_config_file',
lambda *args, **kwargs: config
)
def patch_exchange(mocker, api_mock=None, id='bittrex') -> None: def patch_exchange(mocker, api_mock=None, id='bittrex') -> None:
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
@ -227,7 +234,7 @@ def default_conf():
}, },
"initial_state": "running", "initial_state": "running",
"db_url": "sqlite://", "db_url": "sqlite://",
"loglevel": logging.DEBUG, "verbosity": 3,
} }
return configuration return configuration

View File

@ -1,14 +1,18 @@
from unittest.mock import MagicMock from unittest.mock import MagicMock
from arrow import Arrow
import pytest import pytest
from arrow import Arrow
from pandas import DataFrame, to_datetime from pandas import DataFrame, to_datetime
from freqtrade.arguments import TimeRange from freqtrade.configuration import Arguments, TimeRange
from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, from freqtrade.data.btanalysis import (BT_DATA_COLUMNS,
combine_tickers_with_mean,
create_cum_profit,
extract_trades_of_period, extract_trades_of_period,
load_backtest_data, load_trades_from_db) load_backtest_data, load_trades,
from freqtrade.data.history import load_pair_history, make_testdata_path 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 from freqtrade.tests.test_persistence import create_mock_trades
@ -74,3 +78,52 @@ def test_extract_trades_of_period():
assert trades1.iloc[0].close_time == Arrow(2017, 11, 14, 10, 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].open_time == Arrow(2017, 11, 14, 14, 20, 0).datetime
assert trades1.iloc[-1].close_time == Arrow(2017, 11, 14, 15, 25, 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

View File

@ -12,7 +12,7 @@ import pytest
from pandas import DataFrame from pandas import DataFrame
from freqtrade import OperationalException from freqtrade import OperationalException
from freqtrade.arguments import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.data import history from freqtrade.data import history
from freqtrade.data.history import (download_pair_history, from freqtrade.data.history import (download_pair_history,
load_cached_data_for_updating, load_cached_data_for_updating,

View File

@ -33,13 +33,13 @@ def get_mock_coro(return_value):
def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
fun, mock_ccxt_fun, **kwargs): fun, mock_ccxt_fun, **kwargs):
with pytest.raises(TemporaryError): 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) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
getattr(exchange, fun)(**kwargs) getattr(exchange, fun)(**kwargs)
assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1 assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1
with pytest.raises(OperationalException): 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) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
getattr(exchange, fun)(**kwargs) getattr(exchange, fun)(**kwargs)
assert api_mock.__dict__[mock_ccxt_fun].call_count == 1 assert api_mock.__dict__[mock_ccxt_fun].call_count == 1
@ -47,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): async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fun, **kwargs):
with pytest.raises(TemporaryError): 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) exchange = get_patched_exchange(mocker, default_conf, api_mock)
await getattr(exchange, fun)(**kwargs) await getattr(exchange, fun)(**kwargs)
assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1 assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1
with pytest.raises(OperationalException): 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) exchange = get_patched_exchange(mocker, default_conf, api_mock)
await getattr(exchange, fun)(**kwargs) await getattr(exchange, fun)(**kwargs)
assert api_mock.__dict__[mock_ccxt_fun].call_count == 1 assert api_mock.__dict__[mock_ccxt_fun].call_count == 1
@ -256,13 +256,13 @@ def test__load_async_markets(default_conf, mocker, caplog):
def test__load_markets(default_conf, mocker, caplog): def test__load_markets(default_conf, mocker, caplog):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
api_mock = MagicMock() 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._init_ccxt', MagicMock(return_value=api_mock))
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock()) mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
Exchange(default_conf) Exchange(default_conf)
assert log_has('Unable to initialize markets. Reason: ', caplog.record_tuples) assert log_has('Unable to initialize markets. Reason: SomeError', caplog.record_tuples)
expected_return = {'ETH/BTC': 'available'} expected_return = {'ETH/BTC': 'available'}
api_mock = MagicMock() api_mock = MagicMock()
@ -305,7 +305,7 @@ def test__reload_markets_exception(default_conf, mocker, caplog):
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
api_mock = MagicMock() api_mock = MagicMock()
api_mock.load_markets = MagicMock(side_effect=ccxt.NetworkError) api_mock.load_markets = MagicMock(side_effect=ccxt.NetworkError("LoadError"))
default_conf['exchange']['markets_refresh_interval'] = 10 default_conf['exchange']['markets_refresh_interval'] = 10
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance")
@ -396,6 +396,45 @@ def test_validate_timeframes_failed(default_conf, mocker):
Exchange(default_conf) 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): def test_validate_timeframes_not_in_config(default_conf, mocker):
del default_conf["ticker_interval"] del default_conf["ticker_interval"]
api_mock = MagicMock() api_mock = MagicMock()
@ -504,15 +543,17 @@ def test_dry_run_order(default_conf, mocker, side, exchange_name):
("buy"), ("buy"),
("sell") ("sell")
]) ])
@pytest.mark.parametrize("ordertype,rate", [ @pytest.mark.parametrize("ordertype,rate,marketprice", [
("market", None), ("market", None, None),
("limit", 200), ("market", 200, True),
("stop_loss_limit", 200) ("limit", 200, None),
("stop_loss_limit", 200, None)
]) ])
@pytest.mark.parametrize("exchange_name", EXCHANGES) @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() api_mock = MagicMock()
order_id = 'test_prod_{}_{}'.format(side, randint(0, 10 ** 6)) 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={ api_mock.create_order = MagicMock(return_value={
'id': order_id, 'id': order_id,
'info': { 'info': {
@ -553,6 +594,7 @@ def test_buy_prod(default_conf, mocker, exchange_name):
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
order_type = 'market' order_type = 'market'
time_in_force = 'gtc' time_in_force = 'gtc'
api_mock.options = {}
api_mock.create_order = MagicMock(return_value={ api_mock.create_order = MagicMock(return_value={
'id': order_id, 'id': order_id,
'info': { 'info': {
@ -592,25 +634,25 @@ def test_buy_prod(default_conf, mocker, exchange_name):
# test exception handling # test exception handling
with pytest.raises(DependencyException): 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 = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.buy(pair='ETH/BTC', ordertype=order_type, exchange.buy(pair='ETH/BTC', ordertype=order_type,
amount=1, rate=200, time_in_force=time_in_force) amount=1, rate=200, time_in_force=time_in_force)
with pytest.raises(DependencyException): 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 = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.buy(pair='ETH/BTC', ordertype=order_type, exchange.buy(pair='ETH/BTC', ordertype=order_type,
amount=1, rate=200, time_in_force=time_in_force) amount=1, rate=200, time_in_force=time_in_force)
with pytest.raises(TemporaryError): with pytest.raises(TemporaryError):
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 = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.buy(pair='ETH/BTC', ordertype=order_type, exchange.buy(pair='ETH/BTC', ordertype=order_type,
amount=1, rate=200, time_in_force=time_in_force) amount=1, rate=200, time_in_force=time_in_force)
with pytest.raises(OperationalException): 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 = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.buy(pair='ETH/BTC', ordertype=order_type, exchange.buy(pair='ETH/BTC', ordertype=order_type,
amount=1, rate=200, time_in_force=time_in_force) amount=1, rate=200, time_in_force=time_in_force)
@ -620,6 +662,7 @@ def test_buy_prod(default_conf, mocker, exchange_name):
def test_buy_considers_time_in_force(default_conf, mocker, exchange_name): def test_buy_considers_time_in_force(default_conf, mocker, exchange_name):
api_mock = MagicMock() api_mock = MagicMock()
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
api_mock.options = {}
api_mock.create_order = MagicMock(return_value={ api_mock.create_order = MagicMock(return_value={
'id': order_id, 'id': order_id,
'info': { 'info': {
@ -680,6 +723,7 @@ def test_sell_prod(default_conf, mocker, exchange_name):
api_mock = MagicMock() api_mock = MagicMock()
order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6)) order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6))
order_type = 'market' order_type = 'market'
api_mock.options = {}
api_mock.create_order = MagicMock(return_value={ api_mock.create_order = MagicMock(return_value={
'id': order_id, 'id': order_id,
'info': { 'info': {
@ -714,22 +758,22 @@ def test_sell_prod(default_conf, mocker, exchange_name):
# test exception handling # test exception handling
with pytest.raises(DependencyException): 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 = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)
with pytest.raises(DependencyException): 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 = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)
with pytest.raises(TemporaryError): 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 = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)
with pytest.raises(OperationalException): 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 = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)
@ -744,6 +788,7 @@ def test_sell_considers_time_in_force(default_conf, mocker, exchange_name):
'foo': 'bar' 'foo': 'bar'
} }
}) })
api_mock.options = {}
default_conf['dry_run'] = False default_conf['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) 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) mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
@ -801,7 +846,7 @@ def test_get_balance_prod(default_conf, mocker, exchange_name):
assert exchange.get_balance(currency='BTC') == 123.4 assert exchange.get_balance(currency='BTC') == 123.4
with pytest.raises(OperationalException): 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_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.get_balance(currency='BTC') exchange.get_balance(currency='BTC')
@ -874,7 +919,7 @@ def test_get_tickers(default_conf, mocker, exchange_name):
"get_tickers", "fetch_tickers") "get_tickers", "fetch_tickers")
with pytest.raises(OperationalException): 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_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.get_tickers() exchange.get_tickers()
@ -893,7 +938,7 @@ def test_get_ticker(default_conf, mocker, exchange_name):
'last': 0.0001, 'last': 0.0001,
} }
api_mock.fetch_ticker = MagicMock(return_value=tick) 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) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
# retrieve original ticker # retrieve original ticker
ticker = exchange.get_ticker(pair='ETH/BTC') ticker = exchange.get_ticker(pair='ETH/BTC')
@ -1056,7 +1101,7 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_
api_mock = MagicMock() api_mock = MagicMock()
with pytest.raises(OperationalException, match=r'Could not fetch ticker data*'): 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) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
await exchange._async_get_candle_history(pair, "5m", await exchange._async_get_candle_history(pair, "5m",
(arrow.utcnow().timestamp - 2000) * 1000) (arrow.utcnow().timestamp - 2000) * 1000)
@ -1128,15 +1173,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): def test_get_order_book_exception(default_conf, mocker, exchange_name):
api_mock = MagicMock() api_mock = MagicMock()
with pytest.raises(OperationalException): 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_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.get_order_book(pair='ETH/BTC', limit=50) exchange.get_order_book(pair='ETH/BTC', limit=50)
with pytest.raises(TemporaryError): 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_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.get_order_book(pair='ETH/BTC', limit=50) exchange.get_order_book(pair='ETH/BTC', limit=50)
with pytest.raises(OperationalException): 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_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.get_order_book(pair='ETH/BTC', limit=50) exchange.get_order_book(pair='ETH/BTC', limit=50)
@ -1249,7 +1294,7 @@ def test_cancel_order(default_conf, mocker, exchange_name):
assert exchange.cancel_order(order_id='_', pair='TKN/BTC') == 123 assert exchange.cancel_order(order_id='_', pair='TKN/BTC') == 123
with pytest.raises(InvalidOrderException): with pytest.raises(InvalidOrderException):
api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder) 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 = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.cancel_order(order_id='_', pair='TKN/BTC') exchange.cancel_order(order_id='_', pair='TKN/BTC')
assert api_mock.cancel_order.call_count == 1 assert api_mock.cancel_order.call_count == 1
@ -1276,7 +1321,7 @@ def test_get_order(default_conf, mocker, exchange_name):
assert exchange.get_order('X', 'TKN/BTC') == 456 assert exchange.get_order('X', 'TKN/BTC') == 456
with pytest.raises(InvalidOrderException): with pytest.raises(InvalidOrderException):
api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder) 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_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.get_order(order_id='_', pair='TKN/BTC') exchange.get_order(order_id='_', pair='TKN/BTC')
assert api_mock.fetch_order.call_count == 1 assert api_mock.fetch_order.call_count == 1
@ -1392,22 +1437,22 @@ def test_stoploss_limit_order(default_conf, mocker):
# test exception handling # test exception handling
with pytest.raises(DependencyException): 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 = get_patched_exchange(mocker, default_conf, api_mock)
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
with pytest.raises(DependencyException): 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 = get_patched_exchange(mocker, default_conf, api_mock)
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
with pytest.raises(TemporaryError): 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 = get_patched_exchange(mocker, default_conf, api_mock)
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
with pytest.raises(OperationalException): 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 = get_patched_exchange(mocker, default_conf, api_mock)
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
@ -1438,10 +1483,11 @@ def test_stoploss_limit_order_dry_run(default_conf, mocker):
def test_merge_ft_has_dict(default_conf, mocker): def test_merge_ft_has_dict(default_conf, mocker):
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=MagicMock())) mocker.patch.multiple('freqtrade.exchange.Exchange',
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock()) _init_ccxt=MagicMock(return_value=MagicMock()),
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) _load_async_markets=MagicMock(),
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) validate_pairs=MagicMock(),
validate_timeframes=MagicMock())
ex = Exchange(default_conf) ex = Exchange(default_conf)
assert ex._ft_has == Exchange._ft_has_default assert ex._ft_has == Exchange._ft_has_default
@ -1462,3 +1508,18 @@ def test_merge_ft_has_dict(default_conf, mocker):
assert ex._ft_has != Exchange._ft_has_default assert ex._ft_has != Exchange._ft_has_default
assert not ex._ft_has['stoploss_on_exchange'] assert not ex._ft_has['stoploss_on_exchange']
assert ex._ft_has['DeadBeef'] == 20 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")

View File

@ -11,6 +11,7 @@ def test_buy_kraken_trading_agreement(default_conf, mocker):
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
order_type = 'limit' order_type = 'limit'
time_in_force = 'ioc' time_in_force = 'ioc'
api_mock.options = {}
api_mock.create_order = MagicMock(return_value={ api_mock.create_order = MagicMock(return_value={
'id': order_id, 'id': order_id,
'info': { 'info': {
@ -42,6 +43,7 @@ def test_sell_kraken_trading_agreement(default_conf, mocker):
api_mock = MagicMock() api_mock = MagicMock()
order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6)) order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6))
order_type = 'market' order_type = 'market'
api_mock.options = {}
api_mock.create_order = MagicMock(return_value={ api_mock.create_order = MagicMock(return_value={
'id': order_id, 'id': order_id,
'info': { 'info': {

View File

@ -1,6 +1,5 @@
# pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, unused-argument # pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, unused-argument
import json
import math import math
import random import random
from unittest.mock import MagicMock from unittest.mock import MagicMock
@ -11,7 +10,7 @@ import pytest
from arrow import Arrow from arrow import Arrow
from freqtrade import DependencyException, constants from freqtrade import DependencyException, constants
from freqtrade.arguments import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.data import history from freqtrade.data import history
from freqtrade.data.btanalysis import evaluate_result_multi from freqtrade.data.btanalysis import evaluate_result_multi
from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.converter import parse_ticker_dataframe
@ -22,7 +21,8 @@ from freqtrade.optimize.backtesting import Backtesting
from freqtrade.state import RunMode from freqtrade.state import RunMode
from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.strategy.default_strategy import DefaultStrategy
from freqtrade.strategy.interface import SellType from freqtrade.strategy.interface import SellType
from freqtrade.tests.conftest import get_args, log_has, log_has_re, patch_exchange from freqtrade.tests.conftest import (get_args, log_has, log_has_re, patch_exchange,
patched_configuration_load_config_file)
def trim_dictlist(dict_list, num): def trim_dictlist(dict_list, num):
@ -165,9 +165,7 @@ def _trend_alternate(dataframe=None, metadata=None):
# Unit tests # Unit tests
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None: def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)
))
args = [ args = [
'--config', 'config.json', '--config', 'config.json',
@ -183,7 +181,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
assert 'pair_whitelist' in config['exchange'] assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config assert 'datadir' in config
assert log_has( assert log_has(
'Using data folder: {} ...'.format(config['datadir']), 'Using data directory: {} ...'.format(config['datadir']),
caplog.record_tuples caplog.record_tuples
) )
assert 'ticker_interval' in config assert 'ticker_interval' in config
@ -205,10 +203,11 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> None: def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf) mocker.patch(
)) 'freqtrade.configuration.configuration.create_datadir',
mocker.patch('freqtrade.configuration.Configuration._create_datadir', lambda s, c, x: x) lambda c, x: x
)
args = [ args = [
'--config', 'config.json', '--config', 'config.json',
@ -235,7 +234,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) ->
assert config['runmode'] == RunMode.BACKTEST assert config['runmode'] == RunMode.BACKTEST
assert log_has( assert log_has(
'Using data folder: {} ...'.format(config['datadir']), 'Using data directory: {} ...'.format(config['datadir']),
caplog.record_tuples caplog.record_tuples
) )
assert 'ticker_interval' in config assert 'ticker_interval' in config
@ -276,9 +275,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) ->
def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog) -> None: def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog) -> None:
default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)
))
args = [ args = [
'--config', 'config.json', '--config', 'config.json',
@ -295,9 +292,8 @@ def test_start(mocker, fee, default_conf, caplog) -> None:
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch('freqtrade.optimize.backtesting.Backtesting.start', start_mock) mocker.patch('freqtrade.optimize.backtesting.Backtesting.start', start_mock)
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)
))
args = [ args = [
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'DefaultStrategy', '--strategy', 'DefaultStrategy',
@ -828,9 +824,7 @@ def test_backtest_start_live(default_conf, mocker, caplog):
patch_exchange(mocker, api_mock) patch_exchange(mocker, api_mock)
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock()) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock())
mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', MagicMock()) mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', MagicMock())
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)
))
args = [ args = [
'--config', 'config.json', '--config', 'config.json',
@ -851,7 +845,7 @@ def test_backtest_start_live(default_conf, mocker, caplog):
'Parameter -l/--live detected ...', 'Parameter -l/--live detected ...',
'Ignoring max_open_trades (--disable-max-market-positions was used) ...', 'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
'Parameter --timerange detected: -100 ...', 'Parameter --timerange detected: -100 ...',
'Using data folder: freqtrade/tests/testdata ...', 'Using data directory: freqtrade/tests/testdata ...',
'Using stake_currency: BTC ...', 'Using stake_currency: BTC ...',
'Using stake_amount: 0.001 ...', 'Using stake_amount: 0.001 ...',
'Live: Downloading data for all defined pairs ...', 'Live: Downloading data for all defined pairs ...',
@ -880,9 +874,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog):
gen_strattable_mock = MagicMock() gen_strattable_mock = MagicMock()
mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table_strategy', mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table_strategy',
gen_strattable_mock) gen_strattable_mock)
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)
))
args = [ args = [
'--config', 'config.json', '--config', 'config.json',
@ -910,7 +902,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog):
'Parameter -l/--live detected ...', 'Parameter -l/--live detected ...',
'Ignoring max_open_trades (--disable-max-market-positions was used) ...', 'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
'Parameter --timerange detected: -100 ...', 'Parameter --timerange detected: -100 ...',
'Using data folder: freqtrade/tests/testdata ...', 'Using data directory: freqtrade/tests/testdata ...',
'Using stake_currency: BTC ...', 'Using stake_currency: BTC ...',
'Using stake_amount: 0.001 ...', 'Using stake_amount: 0.001 ...',
'Live: Downloading data for all defined pairs ...', 'Live: Downloading data for all defined pairs ...',

View File

@ -1,20 +1,18 @@
# pragma pylint: disable=missing-docstring, C0103, C0330 # pragma pylint: disable=missing-docstring, C0103, C0330
# pragma pylint: disable=protected-access, too-many-lines, invalid-name, too-many-arguments # pragma pylint: disable=protected-access, too-many-lines, invalid-name, too-many-arguments
import json
from unittest.mock import MagicMock from unittest.mock import MagicMock
from freqtrade.edge import PairInfo from freqtrade.edge import PairInfo
from freqtrade.optimize import setup_configuration, start_edge from freqtrade.optimize import setup_configuration, start_edge
from freqtrade.optimize.edge_cli import EdgeCli from freqtrade.optimize.edge_cli import EdgeCli
from freqtrade.state import RunMode from freqtrade.state import RunMode
from freqtrade.tests.conftest import get_args, log_has, log_has_re, patch_exchange from freqtrade.tests.conftest import (get_args, log_has, log_has_re, patch_exchange,
patched_configuration_load_config_file)
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None: def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)
))
args = [ args = [
'--config', 'config.json', '--config', 'config.json',
@ -32,7 +30,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
assert 'pair_whitelist' in config['exchange'] assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config assert 'datadir' in config
assert log_has( assert log_has(
'Using data folder: {} ...'.format(config['datadir']), 'Using data directory: {} ...'.format(config['datadir']),
caplog.record_tuples caplog.record_tuples
) )
assert 'ticker_interval' in config assert 'ticker_interval' in config
@ -46,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: def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> None:
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, edge_conf)
read_data=json.dumps(edge_conf) mocker.patch(
)) 'freqtrade.configuration.configuration.create_datadir',
mocker.patch('freqtrade.configuration.Configuration._create_datadir', lambda s, c, x: x) lambda c, x: x
)
args = [ args = [
'--config', 'config.json', '--config', 'config.json',
@ -71,7 +70,7 @@ def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> N
assert 'datadir' in config assert 'datadir' in config
assert config['runmode'] == RunMode.EDGE assert config['runmode'] == RunMode.EDGE
assert log_has( assert log_has(
'Using data folder: {} ...'.format(config['datadir']), 'Using data directory: {} ...'.format(config['datadir']),
caplog.record_tuples caplog.record_tuples
) )
assert 'ticker_interval' in config assert 'ticker_interval' in config
@ -92,9 +91,8 @@ def test_start(mocker, fee, edge_conf, caplog) -> None:
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch('freqtrade.optimize.edge_cli.EdgeCli.start', start_mock) mocker.patch('freqtrade.optimize.edge_cli.EdgeCli.start', start_mock)
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, edge_conf)
read_data=json.dumps(edge_conf)
))
args = [ args = [
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'DefaultStrategy', '--strategy', 'DefaultStrategy',

View File

@ -1,22 +1,27 @@
# pragma pylint: disable=missing-docstring,W0212,C0103 # pragma pylint: disable=missing-docstring,W0212,C0103
import json
import os import os
from datetime import datetime from datetime import datetime
from unittest.mock import MagicMock from unittest.mock import MagicMock
from filelock import Timeout
import pandas as pd import pandas as pd
import pytest import pytest
from arrow import Arrow
from filelock import Timeout
from freqtrade import DependencyException from freqtrade import DependencyException, OperationalException
from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.converter import parse_ticker_dataframe
from freqtrade.data.history import load_tickerdata_file from freqtrade.data.history import load_tickerdata_file
from freqtrade.optimize.default_hyperopt import DefaultHyperOpts
from freqtrade.optimize.hyperopt import Hyperopt, HYPEROPT_LOCKFILE
from freqtrade.optimize import setup_configuration, start_hyperopt from freqtrade.optimize import setup_configuration, start_hyperopt
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.optimize.default_hyperopt import DefaultHyperOpts
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.state import RunMode
from freqtrade.tests.conftest import get_args, log_has, log_has_re, patch_exchange 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') @pytest.fixture(scope='function')
@ -25,6 +30,21 @@ def hyperopt(default_conf, mocker):
return Hyperopt(default_conf) 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 # Functions for recurrent object patching
def create_trials(mocker, hyperopt) -> None: def create_trials(mocker, hyperopt) -> None:
""" """
@ -44,9 +64,7 @@ def create_trials(mocker, hyperopt) -> None:
def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, caplog) -> None: def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)
))
args = [ args = [
'--config', 'config.json', '--config', 'config.json',
@ -61,7 +79,7 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca
assert 'pair_whitelist' in config['exchange'] assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config assert 'datadir' in config
assert log_has( assert log_has(
'Using data folder: {} ...'.format(config['datadir']), 'Using data directory: {} ...'.format(config['datadir']),
caplog.record_tuples caplog.record_tuples
) )
assert 'ticker_interval' in config assert 'ticker_interval' in config
@ -82,10 +100,11 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca
def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplog) -> None: def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf) mocker.patch(
)) 'freqtrade.configuration.configuration.create_datadir',
mocker.patch('freqtrade.configuration.Configuration._create_datadir', lambda s, c, x: x) lambda c, x: x
)
args = [ args = [
'--config', 'config.json', '--config', 'config.json',
@ -111,7 +130,7 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo
assert config['runmode'] == RunMode.HYPEROPT assert config['runmode'] == RunMode.HYPEROPT
assert log_has( assert log_has(
'Using data folder: {} ...'.format(config['datadir']), 'Using data directory: {} ...'.format(config['datadir']),
caplog.record_tuples caplog.record_tuples
) )
assert 'ticker_interval' in config assert 'ticker_interval' in config
@ -148,11 +167,8 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo
def test_hyperoptresolver(mocker, default_conf, caplog) -> None: def test_hyperoptresolver(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
)
hyperopts = DefaultHyperOpts hyperopts = DefaultHyperOpts
delattr(hyperopts, 'populate_buy_trend') delattr(hyperopts, 'populate_buy_trend')
delattr(hyperopts, 'populate_sell_trend') delattr(hyperopts, 'populate_sell_trend')
@ -170,12 +186,34 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None:
assert hasattr(x, "ticker_interval") 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: def test_start(mocker, default_conf, caplog) -> None:
start_mock = MagicMock() start_mock = MagicMock()
mocker.patch( patched_configuration_load_config_file(mocker, default_conf)
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock) mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock)
patch_exchange(mocker) patch_exchange(mocker)
@ -198,10 +236,7 @@ def test_start(mocker, default_conf, caplog) -> None:
def test_start_no_data(mocker, default_conf, caplog) -> None: def test_start_no_data(mocker, default_conf, caplog) -> None:
mocker.patch( patched_configuration_load_config_file(mocker, default_conf)
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock(return_value={})) mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock(return_value={}))
mocker.patch( mocker.patch(
'freqtrade.optimize.hyperopt.get_timeframe', 'freqtrade.optimize.hyperopt.get_timeframe',
@ -226,10 +261,7 @@ def test_start_no_data(mocker, default_conf, caplog) -> None:
def test_start_failure(mocker, default_conf, caplog) -> None: def test_start_failure(mocker, default_conf, caplog) -> None:
start_mock = MagicMock() start_mock = MagicMock()
mocker.patch( patched_configuration_load_config_file(mocker, default_conf)
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock) mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock)
patch_exchange(mocker) patch_exchange(mocker)
@ -250,10 +282,7 @@ def test_start_failure(mocker, default_conf, caplog) -> None:
def test_start_filelock(mocker, default_conf, caplog) -> None: def test_start_filelock(mocker, default_conf, caplog) -> None:
start_mock = MagicMock(side_effect=Timeout(HYPEROPT_LOCKFILE)) start_mock = MagicMock(side_effect=Timeout(HYPEROPT_LOCKFILE))
mocker.patch( patched_configuration_load_config_file(mocker, default_conf)
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock) mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock)
patch_exchange(mocker) patch_exchange(mocker)
@ -270,26 +299,72 @@ def test_start_filelock(mocker, default_conf, caplog) -> None:
) )
def test_loss_calculation_prefer_correct_trade_count(hyperopt) -> None: def test_loss_calculation_prefer_correct_trade_count(default_conf, hyperopt_results) -> None:
hl = HyperOptLossResolver(default_conf).hyperoptloss
correct = hyperopt.calculate_loss(1, hyperopt.target_trades, 20) correct = hl.hyperopt_loss_function(hyperopt_results, 600)
over = hyperopt.calculate_loss(1, hyperopt.target_trades + 100, 20) over = hl.hyperopt_loss_function(hyperopt_results, 600 + 100)
under = hyperopt.calculate_loss(1, hyperopt.target_trades - 100, 20) under = hl.hyperopt_loss_function(hyperopt_results, 600 - 100)
assert over > correct assert over > correct
assert under > correct assert under > correct
def test_loss_calculation_prefer_shorter_trades(hyperopt) -> None: def test_loss_calculation_prefer_shorter_trades(default_conf, hyperopt_results) -> None:
shorter = hyperopt.calculate_loss(1, 100, 20) resultsb = hyperopt_results.copy()
longer = hyperopt.calculate_loss(1, 100, 30) 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 assert shorter < longer
def test_loss_calculation_has_limited_profit(hyperopt) -> None: def test_loss_calculation_has_limited_profit(default_conf, hyperopt_results) -> None:
correct = hyperopt.calculate_loss(hyperopt.expected_max_profit, hyperopt.target_trades, 20) results_over = hyperopt_results.copy()
over = hyperopt.calculate_loss(hyperopt.expected_max_profit * 2, hyperopt.target_trades, 20) results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2
under = hyperopt.calculate_loss(hyperopt.expected_max_profit / 2, hyperopt.target_trades, 20) results_under = hyperopt_results.copy()
assert over == correct 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 assert under > correct
@ -387,6 +462,11 @@ def test_start_calls_optimizer(mocker, default_conf, caplog) -> None:
assert dumper.called assert dumper.called
# Should be called twice, once for tickerdata, once to save evaluations # Should be called twice, once for tickerdata, once to save evaluations
assert dumper.call_count == 2 assert dumper.call_count == 2
assert hasattr(hyperopt, "advise_sell")
assert hasattr(hyperopt, "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): def test_format_results(hyperopt):
@ -484,7 +564,7 @@ def test_generate_optimizer(mocker, default_conf) -> None:
) )
mocker.patch( mocker.patch(
'freqtrade.optimize.hyperopt.get_timeframe', '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) patch_exchange(mocker)
mocker.patch('freqtrade.optimize.hyperopt.load', MagicMock()) mocker.patch('freqtrade.optimize.hyperopt.load', MagicMock())
@ -526,3 +606,36 @@ def test_generate_optimizer(mocker, default_conf) -> None:
hyperopt = Hyperopt(default_conf) hyperopt = Hyperopt(default_conf)
generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values())) generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values()))
assert generate_optimizer_value == response_expected 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)

View File

@ -34,9 +34,9 @@ def whitelist_conf(default_conf):
def test_load_pairlist_noexist(mocker, markets, default_conf): def test_load_pairlist_noexist(mocker, markets, default_conf):
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
with pytest.raises(ImportError, with pytest.raises(OperationalException,
match=r"Impossible to load Pairlist 'NonexistingPairList'." match=r"Impossible to load Pairlist 'NonexistingPairList'. "
r" This class does not exist or contains Python code errors"): r"This class does not exist or contains Python code errors."):
PairListResolver('NonexistingPairList', freqtradebot, default_conf).pairlist PairListResolver('NonexistingPairList', freqtradebot, default_conf).pairlist

View File

@ -324,7 +324,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets,
assert prec_satoshi(stats['best_rate'], 6.2) 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 = { mock_balance = {
'BTC': { 'BTC': {
'free': 10.0, 'free': 10.0,
@ -371,6 +371,72 @@ def test_rpc_balance_handle(default_conf, mocker):
assert result['total'] == 12.0 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: def test_rpc_start(mocker, default_conf) -> None:
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())

View File

@ -244,6 +244,8 @@ def test_api_balance(botclient, mocker, rpc_balance):
} }
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance)
mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker) mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker)
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") rc = client_get(client, f"{BASE_URI}/balance")
assert_response(rc) assert_response(rc)

View File

@ -518,6 +518,8 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance) -> N
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance)
mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker) mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker)
mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination',
side_effect=lambda a, b: f"{a}/{b}")
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple( mocker.patch.multiple(
@ -559,10 +561,32 @@ def test_balance_handle_empty_response(default_conf, update, mocker) -> None:
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
freqtradebot.config['dry_run'] = False
telegram._balance(bot=MagicMock(), update=update) telegram._balance(bot=MagicMock(), update=update)
result = msg_mock.call_args_list[0][0][0] result = msg_mock.call_args_list[0][0][0]
assert msg_mock.call_count == 1 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: def test_balance_handle_too_large_response(default_conf, update, mocker) -> None:

View File

@ -6,7 +6,7 @@ from unittest.mock import MagicMock
import arrow import arrow
from pandas import DataFrame 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.converter import parse_ticker_dataframe
from freqtrade.data.history import load_tickerdata_file from freqtrade.data.history import load_tickerdata_file
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
@ -186,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) 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: def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None:
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
ind_mock = MagicMock(side_effect=lambda x, meta: x) ind_mock = MagicMock(side_effect=lambda x, meta: x)

View File

@ -1,5 +1,6 @@
# pragma pylint: disable=missing-docstring, protected-access, C0103 # pragma pylint: disable=missing-docstring, protected-access, C0103
import logging import logging
import tempfile
import warnings import warnings
from base64 import urlsafe_b64encode from base64 import urlsafe_b64encode
from os import path from os import path
@ -9,6 +10,7 @@ from unittest.mock import Mock
import pytest import pytest
from pandas import DataFrame from pandas import DataFrame
from freqtrade import OperationalException
from freqtrade.resolvers import StrategyResolver from freqtrade.resolvers import StrategyResolver
from freqtrade.strategy import import_strategy from freqtrade.strategy import import_strategy
from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.strategy.default_strategy import DefaultStrategy
@ -43,22 +45,23 @@ def test_import_strategy(caplog):
def test_search_strategy(): def test_search_strategy():
default_config = {} default_config = {}
default_location = Path(__file__).parent.parent.joinpath('strategy').resolve() default_location = Path(__file__).parent.parent.parent.joinpath('strategy').resolve()
assert isinstance(
StrategyResolver._search_object( s, _ = StrategyResolver._search_object(
directory=default_location, directory=default_location,
object_type=IStrategy, object_type=IStrategy,
kwargs={'config': default_config}, kwargs={'config': default_config},
object_name='DefaultStrategy' object_name='DefaultStrategy'
),
IStrategy
) )
assert StrategyResolver._search_object( assert isinstance(s, IStrategy)
s, _ = StrategyResolver._search_object(
directory=default_location, directory=default_location,
object_type=IStrategy, object_type=IStrategy,
kwargs={'config': default_config}, kwargs={'config': default_config},
object_name='NotFoundStrategy' object_name='NotFoundStrategy'
) is None )
assert s is None
def test_load_strategy(result): def test_load_strategy(result):
@ -66,11 +69,15 @@ def test_load_strategy(result):
assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
def test_load_strategy_byte64(result): def test_load_strategy_base64(result, caplog):
with open("freqtrade/tests/strategy/test_strategy.py", "r") as file: with open("user_data/strategies/test_strategy.py", "rb") as file:
encoded_string = urlsafe_b64encode(file.read().encode("utf-8")).decode("utf-8") encoded_string = urlsafe_b64encode(file.read()).decode("utf-8")
resolver = StrategyResolver({'strategy': 'TestStrategy:{}'.format(encoded_string)}) resolver = StrategyResolver({'strategy': 'TestStrategy:{}'.format(encoded_string)})
assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': '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): def test_load_strategy_invalid_directory(result, caplog):
@ -85,18 +92,18 @@ def test_load_strategy_invalid_directory(result, caplog):
def test_load_not_found_strategy(): def test_load_not_found_strategy():
strategy = StrategyResolver() strategy = StrategyResolver()
with pytest.raises(ImportError, with pytest.raises(OperationalException,
match=r"Impossible to load Strategy 'NotFoundStrategy'." match=r"Impossible to load Strategy 'NotFoundStrategy'. "
r" This class does not exist or contains Python code errors"): r"This class does not exist or contains Python code errors."):
strategy._load_strategy(strategy_name='NotFoundStrategy', config={}) strategy._load_strategy(strategy_name='NotFoundStrategy', config={})
def test_load_staticmethod_importerror(mocker, caplog): def test_load_staticmethod_importerror(mocker, caplog):
mocker.patch("freqtrade.resolvers.strategy_resolver.import_strategy", Mock( mocker.patch("freqtrade.resolvers.strategy_resolver.import_strategy", Mock(
side_effect=TypeError("can't pickle staticmethod objects"))) side_effect=TypeError("can't pickle staticmethod objects")))
with pytest.raises(ImportError, with pytest.raises(OperationalException,
match=r"Impossible to load Strategy 'DefaultStrategy'." match=r"Impossible to load Strategy 'DefaultStrategy'. "
r" This class does not exist or contains Python code errors"): r"This class does not exist or contains Python code errors."):
StrategyResolver() StrategyResolver()
assert log_has_re(r".*Error: can't pickle staticmethod objects", caplog.record_tuples) assert log_has_re(r".*Error: can't pickle staticmethod objects", caplog.record_tuples)
@ -359,6 +366,7 @@ def test_strategy_override_use_sell_profit_only(caplog):
) in caplog.record_tuples ) in caplog.record_tuples
@pytest.mark.filterwarnings("ignore:deprecated")
def test_deprecate_populate_indicators(result): def test_deprecate_populate_indicators(result):
default_location = path.join(path.dirname(path.realpath(__file__))) default_location = path.join(path.dirname(path.realpath(__file__)))
resolver = StrategyResolver({'strategy': 'TestStrategyLegacy', resolver = StrategyResolver({'strategy': 'TestStrategyLegacy',
@ -391,6 +399,7 @@ def test_deprecate_populate_indicators(result):
in str(w[-1].message) in str(w[-1].message)
@pytest.mark.filterwarnings("ignore:deprecated")
def test_call_deprecated_function(result, monkeypatch): def test_call_deprecated_function(result, monkeypatch):
default_location = path.join(path.dirname(path.realpath(__file__))) default_location = path.join(path.dirname(path.realpath(__file__)))
resolver = StrategyResolver({'strategy': 'TestStrategyLegacy', resolver = StrategyResolver({'strategy': 'TestStrategyLegacy',

View File

@ -3,7 +3,9 @@ import argparse
import pytest 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 # Parse common command-line-arguments. Used for all tools
@ -18,7 +20,7 @@ def test_parse_args_defaults() -> None:
assert args.config == ['config.json'] assert args.config == ['config.json']
assert args.strategy_path is None assert args.strategy_path is None
assert args.datadir is None assert args.datadir is None
assert args.loglevel == 0 assert args.verbosity == 0
def test_parse_args_config() -> None: def test_parse_args_config() -> None:
@ -41,16 +43,16 @@ def test_parse_args_db_url() -> None:
def test_parse_args_verbose() -> None: def test_parse_args_verbose() -> None:
args = Arguments(['-v'], '').get_parsed_arg() args = Arguments(['-v'], '').get_parsed_arg()
assert args.loglevel == 1 assert args.verbosity == 1
args = Arguments(['--verbose'], '').get_parsed_arg() args = Arguments(['--verbose'], '').get_parsed_arg()
assert args.loglevel == 1 assert args.verbosity == 1
def test_common_scripts_options() -> None: def test_common_scripts_options() -> None:
arguments = Arguments(['-p', 'ETH/BTC'], '') arguments = Arguments(['-p', 'ETH/BTC'], '')
arguments.common_scripts_options() arguments._build_args(ARGS_DOWNLOADER)
args = arguments.get_parsed_arg() args = arguments._parse_args()
assert args.pairs == 'ETH/BTC' assert args.pairs == 'ETH/BTC'
@ -84,21 +86,6 @@ def test_parse_args_strategy_path_invalid() -> None:
Arguments(['--strategy-path'], '').get_parsed_arg() Arguments(['--strategy-path'], '').get_parsed_arg()
def test_parse_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: def test_parse_timerange_incorrect() -> None:
assert TimeRange(None, 'line', 0, -200) == Arguments.parse_timerange('-200') assert TimeRange(None, 'line', 0, -200) == Arguments.parse_timerange('-200')
assert TimeRange('line', None, 200, 0) == Arguments.parse_timerange('200-') assert TimeRange('line', None, 200, 0) == Arguments.parse_timerange('200-')
@ -145,7 +132,7 @@ def test_parse_args_backtesting_custom() -> None:
call_args = Arguments(args, '').get_parsed_arg() call_args = Arguments(args, '').get_parsed_arg()
assert call_args.config == ['test_conf.json'] assert call_args.config == ['test_conf.json']
assert call_args.live is True assert call_args.live is True
assert call_args.loglevel == 0 assert call_args.verbosity == 0
assert call_args.subparser == 'backtesting' assert call_args.subparser == 'backtesting'
assert call_args.func is not None assert call_args.func is not None
assert call_args.ticker_interval == '1m' assert call_args.ticker_interval == '1m'
@ -164,7 +151,7 @@ def test_parse_args_hyperopt_custom() -> None:
call_args = Arguments(args, '').get_parsed_arg() call_args = Arguments(args, '').get_parsed_arg()
assert call_args.config == ['test_conf.json'] assert call_args.config == ['test_conf.json']
assert call_args.epochs == 20 assert call_args.epochs == 20
assert call_args.loglevel == 0 assert call_args.verbosity == 0
assert call_args.subparser == 'hyperopt' assert call_args.subparser == 'hyperopt'
assert call_args.spaces == ['buy'] assert call_args.spaces == ['buy']
assert call_args.func is not None assert call_args.func is not None
@ -173,16 +160,15 @@ def test_parse_args_hyperopt_custom() -> None:
def test_download_data_options() -> None: def test_download_data_options() -> None:
args = [ args = [
'--pairs-file', 'file_with_pairs', '--pairs-file', 'file_with_pairs',
'--datadir', 'datadir/folder', '--datadir', 'datadir/directory',
'--days', '30', '--days', '30',
'--exchange', 'binance' '--exchange', 'binance'
] ]
arguments = Arguments(args, '') arguments = Arguments(args, '')
arguments.common_options() arguments._build_args(ARGS_DOWNLOADER)
arguments.download_data_options() args = arguments._parse_args()
args = arguments.parse_args()
assert args.pairs_file == 'file_with_pairs' assert args.pairs_file == 'file_with_pairs'
assert args.datadir == 'datadir/folder' assert args.datadir == 'datadir/directory'
assert args.days == 30 assert args.days == 30
assert args.exchange == 'binance' assert args.exchange == 'binance'
@ -195,9 +181,8 @@ def test_plot_dataframe_options() -> None:
'-p', 'UNITTEST/BTC', '-p', 'UNITTEST/BTC',
] ]
arguments = Arguments(args, '') arguments = Arguments(args, '')
arguments.common_scripts_options() arguments._build_args(ARGS_PLOT_DATAFRAME)
arguments.plot_dataframe_options() pargs = arguments._parse_args()
pargs = arguments.parse_args(True)
assert pargs.indicators1 == "sma10,sma100" assert pargs.indicators1 == "sma10,sma100"
assert pargs.indicators2 == "macd,fastd,fastk" assert pargs.indicators2 == "macd,fastd,fastk"
assert pargs.plot_limit == 30 assert pargs.plot_limit == 30
@ -205,19 +190,18 @@ def test_plot_dataframe_options() -> None:
def test_check_int_positive() -> None: def test_check_int_positive() -> None:
assert check_int_positive("3") == 3
assert Arguments.check_int_positive("3") == 3 assert check_int_positive("1") == 1
assert Arguments.check_int_positive("1") == 1 assert check_int_positive("100") == 100
assert Arguments.check_int_positive("100") == 100
with pytest.raises(argparse.ArgumentTypeError): with pytest.raises(argparse.ArgumentTypeError):
Arguments.check_int_positive("-2") check_int_positive("-2")
with pytest.raises(argparse.ArgumentTypeError): with pytest.raises(argparse.ArgumentTypeError):
Arguments.check_int_positive("0") check_int_positive("0")
with pytest.raises(argparse.ArgumentTypeError): with pytest.raises(argparse.ArgumentTypeError):
Arguments.check_int_positive("3.5") check_int_positive("3.5")
with pytest.raises(argparse.ArgumentTypeError): with pytest.raises(argparse.ArgumentTypeError):
Arguments.check_int_positive("DeadBeef") check_int_positive("DeadBeef")

View File

@ -1,21 +1,25 @@
# pragma pylint: disable=missing-docstring, protected-access, invalid-name # pragma pylint: disable=missing-docstring, protected-access, invalid-name
import json import json
import logging import logging
import warnings
from argparse import Namespace from argparse import Namespace
from copy import deepcopy from copy import deepcopy
from unittest.mock import MagicMock
from pathlib import Path from pathlib import Path
from unittest.mock import MagicMock
import pytest import pytest
from jsonschema import Draft4Validator, ValidationError, validate from jsonschema import Draft4Validator, ValidationError, validate
from freqtrade import OperationalException, constants from freqtrade import OperationalException, constants
from freqtrade.arguments import Arguments from freqtrade.configuration import Arguments, Configuration
from freqtrade.configuration import Configuration, set_loggers 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.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL
from freqtrade.loggers import _set_loggers
from freqtrade.state import RunMode from freqtrade.state import RunMode
from freqtrade.tests.conftest import log_has, log_has_re from freqtrade.tests.conftest import (log_has, log_has_re,
patched_configuration_load_config_file)
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
@ -31,28 +35,25 @@ def test_load_config_invalid_pair(default_conf) -> None:
default_conf['exchange']['pair_whitelist'].append('ETH-BTC') default_conf['exchange']['pair_whitelist'].append('ETH-BTC')
with pytest.raises(ValidationError, match=r'.*does not match.*'): with pytest.raises(ValidationError, match=r'.*does not match.*'):
configuration = Configuration(Namespace()) validate_config_schema(default_conf)
configuration._validate_config_schema(default_conf)
def test_load_config_missing_attributes(default_conf) -> None: def test_load_config_missing_attributes(default_conf) -> None:
default_conf.pop('exchange') default_conf.pop('exchange')
with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'): with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'):
configuration = Configuration(Namespace()) validate_config_schema(default_conf)
configuration._validate_config_schema(default_conf)
def test_load_config_incorrect_stake_amount(default_conf) -> None: def test_load_config_incorrect_stake_amount(default_conf) -> None:
default_conf['stake_amount'] = 'fake' default_conf['stake_amount'] = 'fake'
with pytest.raises(ValidationError, match=r'.*\'fake\' does not match \'unlimited\'.*'): with pytest.raises(ValidationError, match=r'.*\'fake\' does not match \'unlimited\'.*'):
configuration = Configuration(Namespace()) validate_config_schema(default_conf)
configuration._validate_config_schema(default_conf)
def test_load_config_file(default_conf, mocker, caplog) -> None: def test_load_config_file(default_conf, mocker, caplog) -> None:
file_mock = mocker.patch('freqtrade.configuration.open', mocker.mock_open( file_mock = mocker.patch('freqtrade.configuration.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf) read_data=json.dumps(default_conf)
)) ))
@ -62,11 +63,35 @@ def test_load_config_file(default_conf, mocker, caplog) -> None:
assert validated_conf.items() >= default_conf.items() 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: def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None:
default_conf['max_open_trades'] = 0 default_conf['max_open_trades'] = 0
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)
))
args = Arguments([], '').get_parsed_arg() args = Arguments([], '').get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
@ -88,7 +113,10 @@ def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None:
config_files = [conf1, conf2] config_files = [conf1, conf2]
configsmock = MagicMock(side_effect=config_files) configsmock = MagicMock(side_effect=config_files)
mocker.patch('freqtrade.configuration.Configuration._load_config_file', configsmock) mocker.patch(
'freqtrade.configuration.configuration.Configuration._load_config_file',
configsmock
)
arg_list = ['-c', 'test_conf.json', '--config', 'test2_conf.json', ] arg_list = ['-c', 'test_conf.json', '--config', 'test2_conf.json', ]
args = Arguments(arg_list, '').get_parsed_arg() args = Arguments(arg_list, '').get_parsed_arg()
@ -108,9 +136,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: def test_load_config_max_open_trades_minus_one(default_conf, mocker, caplog) -> None:
default_conf['max_open_trades'] = -1 default_conf['max_open_trades'] = -1
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)
))
args = Arguments([], '').get_parsed_arg() args = Arguments([], '').get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
@ -125,7 +151,7 @@ def test_load_config_max_open_trades_minus_one(default_conf, mocker, caplog) ->
def test_load_config_file_exception(mocker) -> None: def test_load_config_file_exception(mocker) -> None:
mocker.patch( mocker.patch(
'freqtrade.configuration.open', 'freqtrade.configuration.configuration.open',
MagicMock(side_effect=FileNotFoundError('File not found')) MagicMock(side_effect=FileNotFoundError('File not found'))
) )
configuration = Configuration(Namespace()) configuration = Configuration(Namespace())
@ -135,9 +161,7 @@ def test_load_config_file_exception(mocker) -> None:
def test_load_config(default_conf, mocker) -> None: def test_load_config(default_conf, mocker) -> None:
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)
))
args = Arguments([], '').get_parsed_arg() args = Arguments([], '').get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
@ -149,11 +173,9 @@ def test_load_config(default_conf, mocker) -> None:
def test_load_config_with_params(default_conf, mocker) -> None: def test_load_config_with_params(default_conf, mocker) -> None:
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)
))
arglist = [ arglist = [
'--dynamic-whitelist', '10',
'--strategy', 'TestStrategy', '--strategy', 'TestStrategy',
'--strategy-path', '/some/path', '--strategy-path', '/some/path',
'--db-url', 'sqlite:///someurl', '--db-url', 'sqlite:///someurl',
@ -162,8 +184,6 @@ def test_load_config_with_params(default_conf, mocker) -> None:
configuration = Configuration(args) configuration = Configuration(args)
validated_conf = configuration.load_config() 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') == 'TestStrategy'
assert validated_conf.get('strategy_path') == '/some/path' assert validated_conf.get('strategy_path') == '/some/path'
assert validated_conf.get('db_url') == 'sqlite:///someurl' assert validated_conf.get('db_url') == 'sqlite:///someurl'
@ -172,9 +192,7 @@ def test_load_config_with_params(default_conf, mocker) -> None:
conf = default_conf.copy() conf = default_conf.copy()
conf["dry_run"] = False conf["dry_run"] = False
conf["db_url"] = "sqlite:///path/to/db.sqlite" conf["db_url"] = "sqlite:///path/to/db.sqlite"
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, conf)
read_data=json.dumps(conf)
))
arglist = [ arglist = [
'--strategy', 'TestStrategy', '--strategy', 'TestStrategy',
@ -190,9 +208,7 @@ def test_load_config_with_params(default_conf, mocker) -> None:
conf = default_conf.copy() conf = default_conf.copy()
conf["dry_run"] = True conf["dry_run"] = True
conf["db_url"] = "sqlite:///path/to/db.sqlite" conf["db_url"] = "sqlite:///path/to/db.sqlite"
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, conf)
read_data=json.dumps(conf)
))
arglist = [ arglist = [
'--strategy', 'TestStrategy', '--strategy', 'TestStrategy',
@ -208,9 +224,7 @@ def test_load_config_with_params(default_conf, mocker) -> None:
conf = default_conf.copy() conf = default_conf.copy()
conf["dry_run"] = False conf["dry_run"] = False
del conf["db_url"] del conf["db_url"]
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, conf)
read_data=json.dumps(conf)
))
arglist = [ arglist = [
'--strategy', 'TestStrategy', '--strategy', 'TestStrategy',
@ -228,9 +242,7 @@ def test_load_config_with_params(default_conf, mocker) -> None:
conf = default_conf.copy() conf = default_conf.copy()
conf["dry_run"] = True conf["dry_run"] = True
conf["db_url"] = DEFAULT_DB_PROD_URL conf["db_url"] = DEFAULT_DB_PROD_URL
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, conf)
read_data=json.dumps(conf)
))
arglist = [ arglist = [
'--strategy', 'TestStrategy', '--strategy', 'TestStrategy',
@ -248,9 +260,7 @@ def test_load_custom_strategy(default_conf, mocker) -> None:
'strategy': 'CustomStrategy', 'strategy': 'CustomStrategy',
'strategy_path': '/tmp/strategies', 'strategy_path': '/tmp/strategies',
}) })
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)
))
args = Arguments([], '').get_parsed_arg() args = Arguments([], '').get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
@ -261,11 +271,9 @@ def test_load_custom_strategy(default_conf, mocker) -> None:
def test_show_info(default_conf, mocker, caplog) -> None: def test_show_info(default_conf, mocker, caplog) -> None:
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)
))
arglist = [ arglist = [
'--dynamic-whitelist', '10',
'--strategy', 'TestStrategy', '--strategy', 'TestStrategy',
'--db-url', 'sqlite:///tmp/testdb', '--db-url', 'sqlite:///tmp/testdb',
] ]
@ -274,21 +282,13 @@ def test_show_info(default_conf, mocker, caplog) -> None:
configuration = Configuration(args) configuration = Configuration(args)
configuration.get_config() 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('Using DB: "sqlite:///tmp/testdb"', caplog.record_tuples)
assert log_has('Dry run is enabled', caplog.record_tuples) assert log_has('Dry run is enabled', caplog.record_tuples)
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None: def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)
))
arglist = [ arglist = [
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'DefaultStrategy', '--strategy', 'DefaultStrategy',
@ -306,7 +306,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
assert 'pair_whitelist' in config['exchange'] assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config assert 'datadir' in config
assert log_has( assert log_has(
'Using data folder: {} ...'.format(config['datadir']), 'Using data directory: {} ...'.format(config['datadir']),
caplog.record_tuples caplog.record_tuples
) )
assert 'ticker_interval' in config assert 'ticker_interval' in config
@ -326,10 +326,11 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None: def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf) mocker.patch(
)) 'freqtrade.configuration.configuration.create_datadir',
mocker.patch('freqtrade.configuration.Configuration._create_datadir', lambda s, c, x: x) lambda c, x: x
)
arglist = [ arglist = [
'--config', 'config.json', '--config', 'config.json',
@ -356,7 +357,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
assert 'pair_whitelist' in config['exchange'] assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config assert 'datadir' in config
assert log_has( assert log_has(
'Using data folder: {} ...'.format(config['datadir']), 'Using data directory: {} ...'.format(config['datadir']),
caplog.record_tuples caplog.record_tuples
) )
assert 'ticker_interval' in config assert 'ticker_interval' in config
@ -392,9 +393,7 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non
""" """
Test setup_configuration() function Test setup_configuration() function
""" """
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)
))
arglist = [ arglist = [
'--config', 'config.json', '--config', 'config.json',
@ -418,7 +417,7 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non
assert 'pair_whitelist' in config['exchange'] assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config assert 'datadir' in config
assert log_has( assert log_has(
'Using data folder: {} ...'.format(config['datadir']), 'Using data directory: {} ...'.format(config['datadir']),
caplog.record_tuples caplog.record_tuples
) )
assert 'ticker_interval' in config assert 'ticker_interval' in config
@ -442,9 +441,8 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non
def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None: def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)
))
arglist = [ arglist = [
'hyperopt', 'hyperopt',
'--epochs', '10', '--epochs', '10',
@ -468,25 +466,23 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
def test_check_exchange(default_conf, caplog) -> None: def test_check_exchange(default_conf, caplog) -> None:
configuration = Configuration(Namespace())
# Test an officially supported by Freqtrade team exchange # Test an officially supported by Freqtrade team exchange
default_conf.get('exchange').update({'name': 'BITTREX'}) default_conf.get('exchange').update({'name': 'BITTREX'})
assert configuration.check_exchange(default_conf) assert check_exchange(default_conf)
assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.", assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.",
caplog.record_tuples) caplog.record_tuples)
caplog.clear() caplog.clear()
# Test an officially supported by Freqtrade team exchange # Test an officially supported by Freqtrade team exchange
default_conf.get('exchange').update({'name': 'binance'}) default_conf.get('exchange').update({'name': 'binance'})
assert configuration.check_exchange(default_conf) assert check_exchange(default_conf)
assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.", assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.",
caplog.record_tuples) caplog.record_tuples)
caplog.clear() caplog.clear()
# Test an available exchange, supported by ccxt # Test an available exchange, supported by ccxt
default_conf.get('exchange').update({'name': 'kraken'}) default_conf.get('exchange').update({'name': 'kraken'})
assert configuration.check_exchange(default_conf) assert check_exchange(default_conf)
assert log_has_re(r"Exchange .* is supported by ccxt and .* not officially supported " assert log_has_re(r"Exchange .* is supported by ccxt and .* not officially supported "
r"by the Freqtrade development team\. .*", r"by the Freqtrade development team\. .*",
caplog.record_tuples) caplog.record_tuples)
@ -494,7 +490,7 @@ def test_check_exchange(default_conf, caplog) -> None:
# Test a 'bad' exchange, which known to have serious problems # Test a 'bad' exchange, which known to have serious problems
default_conf.get('exchange').update({'name': 'bitmex'}) default_conf.get('exchange').update({'name': 'bitmex'})
assert not configuration.check_exchange(default_conf) assert not check_exchange(default_conf)
assert log_has_re(r"Exchange .* is known to not work with the bot yet\. " assert log_has_re(r"Exchange .* is known to not work with the bot yet\. "
r"Use it only for development and testing purposes\.", r"Use it only for development and testing purposes\.",
caplog.record_tuples) caplog.record_tuples)
@ -502,7 +498,7 @@ def test_check_exchange(default_conf, caplog) -> None:
# Test a 'bad' exchange with check_for_bad=False # Test a 'bad' exchange with check_for_bad=False
default_conf.get('exchange').update({'name': 'bitmex'}) default_conf.get('exchange').update({'name': 'bitmex'})
assert configuration.check_exchange(default_conf, False) assert check_exchange(default_conf, False)
assert log_has_re(r"Exchange .* is supported by ccxt and .* not officially supported " assert log_has_re(r"Exchange .* is supported by ccxt and .* not officially supported "
r"by the Freqtrade development team\. .*", r"by the Freqtrade development team\. .*",
caplog.record_tuples) caplog.record_tuples)
@ -510,21 +506,20 @@ def test_check_exchange(default_conf, caplog) -> None:
# Test an invalid exchange # Test an invalid exchange
default_conf.get('exchange').update({'name': 'unknown_exchange'}) default_conf.get('exchange').update({'name': 'unknown_exchange'})
configuration.config = default_conf
with pytest.raises( with pytest.raises(
OperationalException, OperationalException,
match=r'.*Exchange "unknown_exchange" is not supported by ccxt ' match=r'.*Exchange "unknown_exchange" is not supported by ccxt '
r'and therefore not available for the bot.*' r'and therefore not available for the bot.*'
): ):
configuration.check_exchange(default_conf) check_exchange(default_conf)
def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None: def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None:
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)))
# Prevent setting loggers # Prevent setting loggers
mocker.patch('freqtrade.configuration.set_loggers', MagicMock) mocker.patch('freqtrade.loggers._set_loggers', MagicMock)
arglist = ['-vvv'] arglist = ['-vvv']
args = Arguments(arglist, '').get_parsed_arg() args = Arguments(arglist, '').get_parsed_arg()
@ -546,7 +541,7 @@ def test_set_loggers() -> None:
previous_value2 = logging.getLogger('ccxt.base.exchange').level previous_value2 = logging.getLogger('ccxt.base.exchange').level
previous_value3 = logging.getLogger('telegram').level previous_value3 = logging.getLogger('telegram').level
set_loggers() _set_loggers()
value1 = logging.getLogger('requests').level value1 = logging.getLogger('requests').level
assert previous_value1 is not value1 assert previous_value1 is not value1
@ -560,13 +555,13 @@ def test_set_loggers() -> None:
assert previous_value3 is not value3 assert previous_value3 is not value3
assert value3 is logging.INFO 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('requests').level is logging.DEBUG
assert logging.getLogger('ccxt.base.exchange').level is logging.INFO assert logging.getLogger('ccxt.base.exchange').level is logging.INFO
assert logging.getLogger('telegram').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('requests').level is logging.DEBUG
assert logging.getLogger('ccxt.base.exchange').level is logging.DEBUG assert logging.getLogger('ccxt.base.exchange').level is logging.DEBUG
@ -574,8 +569,7 @@ def test_set_loggers() -> None:
def test_set_logfile(default_conf, mocker): def test_set_logfile(default_conf, mocker):
mocker.patch('freqtrade.configuration.open', patched_configuration_load_config_file(mocker, default_conf)
mocker.mock_open(read_data=json.dumps(default_conf)))
arglist = [ arglist = [
'--logfile', 'test_file.log', '--logfile', 'test_file.log',
@ -592,9 +586,7 @@ def test_set_logfile(default_conf, mocker):
def test_load_config_warn_forcebuy(default_conf, mocker, caplog) -> None: def test_load_config_warn_forcebuy(default_conf, mocker, caplog) -> None:
default_conf['forcebuy_enable'] = True default_conf['forcebuy_enable'] = True
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)
))
args = Arguments([], '').get_parsed_arg() args = Arguments([], '').get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
@ -608,13 +600,12 @@ def test_validate_default_conf(default_conf) -> None:
validate(default_conf, constants.CONF_SCHEMA, Draft4Validator) validate(default_conf, constants.CONF_SCHEMA, Draft4Validator)
def test__create_datadir(mocker, default_conf, caplog) -> None: def test_create_datadir(mocker, default_conf, caplog) -> None:
mocker.patch('os.path.isdir', MagicMock(return_value=False)) mocker.patch.object(Path, "is_dir", MagicMock(return_value=False))
md = MagicMock() md = mocker.patch.object(Path, 'mkdir', MagicMock())
mocker.patch('os.makedirs', md)
cfg = Configuration(Namespace()) create_datadir(default_conf, '/foo/bar')
cfg._create_datadir(default_conf, '/foo/bar') assert md.call_args[1]['parents'] is True
assert md.call_args[0][0] == "/foo/bar"
assert log_has('Created data directory: /foo/bar', caplog.record_tuples) assert log_has('Created data directory: /foo/bar', caplog.record_tuples)
@ -655,8 +646,7 @@ def test_load_config_default_exchange(all_conf) -> None:
with pytest.raises(ValidationError, with pytest.raises(ValidationError,
match=r'\'exchange\' is a required property'): match=r'\'exchange\' is a required property'):
configuration = Configuration(Namespace()) validate_config_schema(all_conf)
configuration._validate_config_schema(all_conf)
def test_load_config_default_exchange_name(all_conf) -> None: def test_load_config_default_exchange_name(all_conf) -> None:
@ -670,8 +660,7 @@ def test_load_config_default_exchange_name(all_conf) -> None:
with pytest.raises(ValidationError, with pytest.raises(ValidationError,
match=r'\'name\' is a required property'): match=r'\'name\' is a required property'):
configuration = Configuration(Namespace()) validate_config_schema(all_conf)
configuration._validate_config_schema(all_conf)
@pytest.mark.parametrize("keys", [("exchange", "sandbox", False), @pytest.mark.parametrize("keys", [("exchange", "sandbox", False),
@ -694,7 +683,6 @@ def test_load_config_default_subkeys(all_conf, keys) -> None:
assert subkey not in all_conf[key] assert subkey not in all_conf[key]
configuration = Configuration(Namespace()) validate_config_schema(all_conf)
configuration._validate_config_schema(all_conf)
assert subkey in all_conf[key] assert subkey in all_conf[key]
assert all_conf[key][subkey] == keys[2] assert all_conf[key][subkey] == keys[2]

View File

@ -2,7 +2,6 @@
# pragma pylint: disable=protected-access, too-many-lines, invalid-name, too-many-arguments # pragma pylint: disable=protected-access, too-many-lines, invalid-name, too-many-arguments
import logging import logging
import re
import time import time
from copy import deepcopy from copy import deepcopy
from unittest.mock import MagicMock, PropertyMock from unittest.mock import MagicMock, PropertyMock
@ -1419,8 +1418,7 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No
# Assert we call handle_trade() if trade is feasible for execution # Assert we call handle_trade() if trade is feasible for execution
freqtrade.update_trade_state(trade) freqtrade.update_trade_state(trade)
regexp = re.compile('Found open order for.*') assert log_has_re('Found open order for.*', caplog.record_tuples)
assert filter(regexp.match, caplog.record_tuples)
def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_buy_order, mocker): def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_buy_order, mocker):
@ -1462,6 +1460,22 @@ def test_update_trade_state_exception(mocker, default_conf,
assert log_has('Could not update trade amount: ', caplog.record_tuples) assert log_has('Could not update trade amount: ', caplog.record_tuples)
def test_update_trade_state_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): 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) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
# get_order should not be called!! # get_order should not be called!!
@ -1941,14 +1955,11 @@ def test_check_handle_timedout_exception(default_conf, ticker, mocker, caplog) -
) )
Trade.session.add(trade_buy) 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() 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: def test_handle_timedout_limit_buy(mocker, default_conf) -> None:
@ -2886,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 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): 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']['currency'] = 'BNB'
trades_for_order[0]['fee']['cost'] = 0.00094518 trades_for_order[0]['fee']['cost'] = 0.00094518

View File

@ -6,11 +6,12 @@ from unittest.mock import MagicMock
import pytest import pytest
from freqtrade import OperationalException from freqtrade import OperationalException
from freqtrade.arguments import Arguments from freqtrade.configuration import Arguments
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.main import main from freqtrade.main import main
from freqtrade.state import State from freqtrade.state import State
from freqtrade.tests.conftest import log_has, patch_exchange from freqtrade.tests.conftest import (log_has, patch_exchange,
patched_configuration_load_config_file)
from freqtrade.worker import Worker from freqtrade.worker import Worker
@ -27,7 +28,7 @@ def test_parse_args_backtesting(mocker) -> None:
call_args = backtesting_mock.call_args[0][0] call_args = backtesting_mock.call_args[0][0]
assert call_args.config == ['config.json'] assert call_args.config == ['config.json']
assert call_args.live is False assert call_args.live is False
assert call_args.loglevel == 0 assert call_args.verbosity == 0
assert call_args.subparser == 'backtesting' assert call_args.subparser == 'backtesting'
assert call_args.func is not None assert call_args.func is not None
assert call_args.ticker_interval is None assert call_args.ticker_interval is None
@ -41,7 +42,7 @@ def test_main_start_hyperopt(mocker) -> None:
assert hyperopt_mock.call_count == 1 assert hyperopt_mock.call_count == 1
call_args = hyperopt_mock.call_args[0][0] call_args = hyperopt_mock.call_args[0][0]
assert call_args.config == ['config.json'] assert call_args.config == ['config.json']
assert call_args.loglevel == 0 assert call_args.verbosity == 0
assert call_args.subparser == 'hyperopt' assert call_args.subparser == 'hyperopt'
assert call_args.func is not None assert call_args.func is not None
@ -50,10 +51,7 @@ def test_main_fatal_exception(mocker, default_conf, caplog) -> None:
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock()) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock())
mocker.patch('freqtrade.worker.Worker._worker', MagicMock(side_effect=Exception)) mocker.patch('freqtrade.worker.Worker._worker', MagicMock(side_effect=Exception))
mocker.patch( patched_configuration_load_config_file(mocker, default_conf)
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
@ -70,10 +68,7 @@ def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None:
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock()) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock())
mocker.patch('freqtrade.worker.Worker._worker', MagicMock(side_effect=KeyboardInterrupt)) mocker.patch('freqtrade.worker.Worker._worker', MagicMock(side_effect=KeyboardInterrupt))
mocker.patch( patched_configuration_load_config_file(mocker, default_conf)
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
@ -93,10 +88,7 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None:
'freqtrade.worker.Worker._worker', 'freqtrade.worker.Worker._worker',
MagicMock(side_effect=OperationalException('Oh snap!')) MagicMock(side_effect=OperationalException('Oh snap!'))
) )
mocker.patch( patched_configuration_load_config_file(mocker, default_conf)
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
@ -118,10 +110,7 @@ def test_main_reload_conf(mocker, default_conf, caplog) -> None:
State.RUNNING, State.RUNNING,
OperationalException("Oh snap!")]) OperationalException("Oh snap!")])
mocker.patch('freqtrade.worker.Worker._worker', worker_mock) mocker.patch('freqtrade.worker.Worker._worker', worker_mock)
mocker.patch( patched_configuration_load_config_file(mocker, default_conf)
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
reconfigure_mock = mocker.patch('freqtrade.main.Worker._reconfigure', MagicMock()) reconfigure_mock = mocker.patch('freqtrade.main.Worker._reconfigure', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
@ -145,10 +134,7 @@ def test_reconfigure(mocker, default_conf) -> None:
'freqtrade.worker.Worker._worker', 'freqtrade.worker.Worker._worker',
MagicMock(side_effect=OperationalException('Oh snap!')) MagicMock(side_effect=OperationalException('Oh snap!'))
) )
mocker.patch( patched_configuration_load_config_file(mocker, default_conf)
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
@ -159,10 +145,7 @@ def test_reconfigure(mocker, default_conf) -> None:
# Renew mock to return modified data # Renew mock to return modified data
conf = deepcopy(default_conf) conf = deepcopy(default_conf)
conf['stake_amount'] += 1 conf['stake_amount'] += 1
mocker.patch( patched_configuration_load_config_file(mocker, conf)
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: conf
)
worker._config = conf worker._config = conf
# reconfigure should return a new instance # reconfigure should return a new instance

View File

@ -4,10 +4,9 @@ import datetime
from unittest.mock import MagicMock from unittest.mock import MagicMock
from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.converter import parse_ticker_dataframe
from freqtrade.misc import (common_datearray, datesarray_to_datetimearray, from freqtrade.data.history import pair_data_filename
file_dump_json, file_load_json, format_ms_time, shorten_date) from freqtrade.misc import (datesarray_to_datetimearray, file_dump_json,
from freqtrade.data.history import load_tickerdata_file, pair_data_filename file_load_json, format_ms_time, shorten_date)
from freqtrade.strategy.default_strategy import DefaultStrategy
def test_shorten_date() -> None: def test_shorten_date() -> None:
@ -32,20 +31,6 @@ def test_datesarray_to_datetimearray(ticker_history_list):
assert date_len == 2 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", pair="UNITTEST/BTC",
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: def test_file_dump_json(mocker) -> None:
file_open = mocker.patch('freqtrade.misc.open', MagicMock()) file_open = mocker.patch('freqtrade.misc.open', MagicMock())
json_dump = mocker.patch('rapidjson.dump', MagicMock()) json_dump = mocker.patch('rapidjson.dump', MagicMock())

View File

@ -1,31 +1,34 @@
from copy import deepcopy
from unittest.mock import MagicMock from unittest.mock import MagicMock
from plotly import tools import plotly.graph_objects as go
import plotly.graph_objs as go from plotly.subplots import make_subplots
from copy import deepcopy
from freqtrade.arguments import TimeRange from freqtrade.configuration import Arguments, TimeRange
from freqtrade.data import history from freqtrade.data import history
from freqtrade.data.btanalysis import load_backtest_data from freqtrade.data.btanalysis import create_cum_profit, load_backtest_data
from freqtrade.plot.plotting import (generate_graph, generate_plot_file, from freqtrade.plot.plotting import (add_indicators, add_profit,
generate_row, plot_trades) 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.strategy.default_strategy import DefaultStrategy
from freqtrade.tests.conftest import log_has, log_has_re from freqtrade.tests.conftest import log_has, log_has_re
def fig_generating_mock(fig, *args, **kwargs): def fig_generating_mock(fig, *args, **kwargs):
""" Return Fig - used to mock generate_row and plot_trades""" """ Return Fig - used to mock add_indicators and plot_trades"""
return fig return fig
def find_trace_in_fig_data(data, search_string: str): def find_trace_in_fig_data(data, search_string: str):
matches = filter(lambda x: x.name == search_string, data) matches = (d for d in data if d.name == search_string)
return next(matches) return next(matches)
def generage_empty_figure(): def generage_empty_figure():
return tools.make_subplots( return make_subplots(
rows=3, rows=3,
cols=1, cols=1,
shared_xaxes=True, shared_xaxes=True,
@ -34,7 +37,27 @@ def generage_empty_figure():
) )
def test_generate_row(default_conf, caplog): 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" pair = "UNITTEST/BTC"
timerange = TimeRange(None, 'line', 0, -1000) timerange = TimeRange(None, 'line', 0, -1000)
@ -49,20 +72,20 @@ def test_generate_row(default_conf, caplog):
fig = generage_empty_figure() fig = generage_empty_figure()
# Row 1 # Row 1
fig1 = generate_row(fig=deepcopy(fig), row=1, indicators=indicators1, data=data) fig1 = add_indicators(fig=deepcopy(fig), row=1, indicators=indicators1, data=data)
figure = fig1.layout.figure figure = fig1.layout.figure
ema10 = find_trace_in_fig_data(figure.data, "ema10") ema10 = find_trace_in_fig_data(figure.data, "ema10")
assert isinstance(ema10, go.Scatter) assert isinstance(ema10, go.Scatter)
assert ema10.yaxis == "y" assert ema10.yaxis == "y"
fig2 = generate_row(fig=deepcopy(fig), row=3, indicators=indicators2, data=data) fig2 = add_indicators(fig=deepcopy(fig), row=3, indicators=indicators2, data=data)
figure = fig2.layout.figure figure = fig2.layout.figure
macd = find_trace_in_fig_data(figure.data, "macd") macd = find_trace_in_fig_data(figure.data, "macd")
assert isinstance(macd, go.Scatter) assert isinstance(macd, go.Scatter)
assert macd.yaxis == "y3" assert macd.yaxis == "y3"
# No indicator found # No indicator found
fig3 = generate_row(fig=deepcopy(fig), row=3, indicators=['no_indicator'], data=data) fig3 = add_indicators(fig=deepcopy(fig), row=3, indicators=['no_indicator'], data=data)
assert fig == fig3 assert fig == fig3
assert log_has_re(r'Indicator "no_indicator" ignored\..*', caplog.record_tuples) assert log_has_re(r'Indicator "no_indicator" ignored\..*', caplog.record_tuples)
@ -95,8 +118,8 @@ def test_plot_trades(caplog):
assert trade_sell.marker.color == 'red' assert trade_sell.marker.color == 'red'
def test_generate_graph_no_signals_no_trades(default_conf, mocker, caplog): def test_generate_candlestick_graph_no_signals_no_trades(default_conf, mocker, caplog):
row_mock = mocker.patch('freqtrade.plot.plotting.generate_row', row_mock = mocker.patch('freqtrade.plot.plotting.add_indicators',
MagicMock(side_effect=fig_generating_mock)) MagicMock(side_effect=fig_generating_mock))
trades_mock = mocker.patch('freqtrade.plot.plotting.plot_trades', trades_mock = mocker.patch('freqtrade.plot.plotting.plot_trades',
MagicMock(side_effect=fig_generating_mock)) MagicMock(side_effect=fig_generating_mock))
@ -110,8 +133,8 @@ def test_generate_graph_no_signals_no_trades(default_conf, mocker, caplog):
indicators1 = [] indicators1 = []
indicators2 = [] indicators2 = []
fig = generate_graph(pair=pair, data=data, trades=None, fig = generate_candlestick_graph(pair=pair, data=data, trades=None,
indicators1=indicators1, indicators2=indicators2) indicators1=indicators1, indicators2=indicators2)
assert isinstance(fig, go.Figure) assert isinstance(fig, go.Figure)
assert fig.layout.title.text == pair assert fig.layout.title.text == pair
figure = fig.layout.figure figure = fig.layout.figure
@ -131,8 +154,8 @@ def test_generate_graph_no_signals_no_trades(default_conf, mocker, caplog):
assert log_has("No sell-signals found.", caplog.record_tuples) assert log_has("No sell-signals found.", caplog.record_tuples)
def test_generate_graph_no_trades(default_conf, mocker): def test_generate_candlestick_graph_no_trades(default_conf, mocker):
row_mock = mocker.patch('freqtrade.plot.plotting.generate_row', row_mock = mocker.patch('freqtrade.plot.plotting.add_indicators',
MagicMock(side_effect=fig_generating_mock)) MagicMock(side_effect=fig_generating_mock))
trades_mock = mocker.patch('freqtrade.plot.plotting.plot_trades', trades_mock = mocker.patch('freqtrade.plot.plotting.plot_trades',
MagicMock(side_effect=fig_generating_mock)) MagicMock(side_effect=fig_generating_mock))
@ -147,8 +170,8 @@ def test_generate_graph_no_trades(default_conf, mocker):
indicators1 = [] indicators1 = []
indicators2 = [] indicators2 = []
fig = generate_graph(pair=pair, data=data, trades=None, fig = generate_candlestick_graph(pair=pair, data=data, trades=None,
indicators1=indicators1, indicators2=indicators2) indicators1=indicators1, indicators2=indicators2)
assert isinstance(fig, go.Figure) assert isinstance(fig, go.Figure)
assert fig.layout.title.text == pair assert fig.layout.title.text == pair
figure = fig.layout.figure figure = fig.layout.figure
@ -178,12 +201,68 @@ def test_generate_graph_no_trades(default_conf, mocker):
assert trades_mock.call_count == 1 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): def test_generate_plot_file(mocker, caplog):
fig = generage_empty_figure() fig = generage_empty_figure()
plot_mock = mocker.patch("freqtrade.plot.plotting.plot", MagicMock()) plot_mock = mocker.patch("freqtrade.plot.plotting.plot", MagicMock())
generate_plot_file(fig, "UNITTEST/BTC", "5m") store_plot_file(fig, filename="freqtrade-plot-UNITTEST_BTC-5m.html")
assert plot_mock.call_count == 1 assert plot_mock.call_count == 1
assert plot_mock.call_args[0][0] == fig assert plot_mock.call_args[0][0] == fig
assert (plot_mock.call_args_list[0][1]['filename'] assert (plot_mock.call_args_list[0][1]['filename']
== "user_data/plots/freqtrade-plot-UNITTEST_BTC-5m.html") == "user_data/plots/freqtrade-plot-UNITTEST_BTC-5m.html")
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)

View File

@ -13,4 +13,4 @@ def test_talib_bollingerbands_near_zero_values():
{'close': 0.00000014} {'close': 0.00000014}
]) ])
bollinger = ta.BBANDS(inputs, matype=0, timeperiod=2) bollinger = ta.BBANDS(inputs, matype=0, timeperiod=2)
assert (bollinger['upperband'][3] != bollinger['middleband'][3]) assert bollinger['upperband'][3] != bollinger['middleband'][3]

View File

@ -213,8 +213,7 @@ def atr(bars, window=14, exp=False):
else: else:
res = rolling_mean(tr, window) res = rolling_mean(tr, window)
res = pd.Series(res) return pd.Series(res)
return (res.shift(1) * (window - 1) + res) / window
# --------------------------------------------- # ---------------------------------------------
@ -602,6 +601,14 @@ def pvt(bars):
bars['close'].shift(1)) * bars['volume'] bars['close'].shift(1)) * bars['volume']
return trend.cumsum() 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)
# ============================================= # =============================================
@ -629,6 +636,7 @@ PandasObject.rsi = rsi
PandasObject.stoch = stoch PandasObject.stoch = stoch
PandasObject.zscore = zscore PandasObject.zscore = zscore
PandasObject.pvt = pvt PandasObject.pvt = pvt
PandasObject.chopiness = chopiness
PandasObject.tdi = tdi PandasObject.tdi = tdi
PandasObject.true_range = true_range PandasObject.true_range = true_range
PandasObject.mid_price = mid_price PandasObject.mid_price = mid_price

View File

@ -128,6 +128,7 @@ class Worker(object):
return result return result
def _process(self) -> bool: def _process(self) -> bool:
logger.debug("========================================")
state_changed = False state_changed = False
try: try:
state_changed = self.freqtrade.process() state_changed = self.freqtrade.process()

View File

@ -1,9 +1,9 @@
# requirements without requirements installable via conda # requirements without requirements installable via conda
# mainly used for Raspberry pi installs # mainly used for Raspberry pi installs
ccxt==1.18.805 ccxt==1.18.992
SQLAlchemy==1.3.5 SQLAlchemy==1.3.6
python-telegram-bot==11.1.0 python-telegram-bot==11.1.0
arrow==0.14.2 arrow==0.14.3
cachetools==3.1.1 cachetools==3.1.1
requests==2.22.0 requests==2.22.0
urllib3==1.24.2 # pyup: ignore urllib3==1.24.2 # pyup: ignore
@ -29,4 +29,4 @@ python-rapidjson==0.7.2
sdnotify==0.3.2 sdnotify==0.3.2
# Api server # Api server
flask==1.0.3 flask==1.1.1

View File

@ -2,12 +2,13 @@
-r requirements.txt -r requirements.txt
-r requirements-plot.txt -r requirements-plot.txt
flake8==3.7.7 flake8==3.7.8
flake8-type-annotations==0.1.0 flake8-type-annotations==0.1.0
flake8-tidy-imports==2.0.0 flake8-tidy-imports==2.0.0
pytest==4.6.3 pytest==5.0.1
pytest-mock==1.10.4 pytest-mock==1.10.4
pytest-asyncio==0.10.0 pytest-asyncio==0.10.0
pytest-cov==2.7.1 pytest-cov==2.7.1
pytest-random-order==1.0.4
coveralls==1.8.1 coveralls==1.8.1
mypy==0.710 mypy==0.720

View File

@ -1,5 +1,5 @@
# Include all requirements to run the bot. # Include all requirements to run the bot.
-r requirements.txt -r requirements.txt
plotly==3.10.0 plotly==4.0.0

View File

@ -1,6 +1,6 @@
# Load common requirements # Load common requirements
-r requirements-common.txt -r requirements-common.txt
numpy==1.16.4 numpy==1.17.0
pandas==0.24.2 pandas==0.25.0
scipy==1.3.0 scipy==1.3.0

View File

@ -8,8 +8,10 @@ import sys
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List from typing import Any, Dict, List
from freqtrade.arguments import Arguments, TimeRange from freqtrade.configuration import Arguments, TimeRange
from freqtrade.configuration import Configuration from freqtrade.configuration import Configuration
from freqtrade.configuration.arguments import ARGS_DOWNLOADER
from freqtrade.configuration.check_exchange import check_exchange
from freqtrade.data.history import download_pair_history from freqtrade.data.history import download_pair_history
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.misc import deep_merge_dicts from freqtrade.misc import deep_merge_dicts
@ -20,13 +22,12 @@ logger = logging.getLogger('download_backtest_data')
DEFAULT_DL_PATH = 'user_data/data' DEFAULT_DL_PATH = 'user_data/data'
arguments = Arguments(sys.argv[1:], 'Download backtest data')
arguments.common_options()
arguments.download_data_options()
# Do not read the default config if config is not specified # Do not read the default config if config is not specified
# in the command line options explicitely # in the command line options explicitely
args = arguments.parse_args(no_default_config=True) arguments = Arguments(sys.argv[1:], 'Download backtest data',
no_default_config=True)
arguments._build_args(optionlist=ARGS_DOWNLOADER)
args = arguments._parse_args()
# Use bittrex as default exchange # Use bittrex as default exchange
exchange_name = args.exchange or 'bittrex' exchange_name = args.exchange or 'bittrex'
@ -73,16 +74,16 @@ else:
} }
timeframes = args.timeframes or ['1m', '5m'] timeframes = args.timeframes or ['1m', '5m']
configuration._load_logging_config(config) configuration._process_logging_options(config)
if args.config and args.exchange: if args.config and args.exchange:
logger.warning("The --exchange option is ignored, " logger.warning("The --exchange option is ignored, "
"using exchange settings from the configuration file.") "using exchange settings from the configuration file.")
# Check if the exchange set by the user is supported # Check if the exchange set by the user is supported
configuration.check_exchange(config) check_exchange(config)
configuration._load_datadir_config(config) configuration._process_datadir_options(config)
dl_path = Path(config['datadir']) dl_path = Path(config['datadir'])

View File

@ -2,19 +2,7 @@
""" """
Script to display when the bot will buy on specific pair(s) Script to display when the bot will buy on specific pair(s)
Mandatory Cli parameters: Use `python plot_dataframe.py --help` to display the command line arguments
-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
Indicators recommended Indicators recommended
Row 1: sma, ema3, ema5, ema10, ema50 Row 1: sma, ema3, ema5, ema10, ema50
@ -26,18 +14,17 @@ Example of usage:
""" """
import logging import logging
import sys import sys
from pathlib import Path
from typing import Any, Dict, List from typing import Any, Dict, List
import pandas as pd import pandas as pd
from freqtrade.arguments import Arguments from freqtrade.configuration import Arguments
from freqtrade.data import history from freqtrade.configuration.arguments import ARGS_PLOT_DATAFRAME
from freqtrade.data.btanalysis import (extract_trades_of_period, from freqtrade.data.btanalysis import extract_trades_of_period
load_backtest_data, load_trades_from_db)
from freqtrade.optimize import setup_configuration from freqtrade.optimize import setup_configuration
from freqtrade.plot.plotting import generate_graph, generate_plot_file from freqtrade.plot.plotting import (init_plotscript, generate_candlestick_graph,
from freqtrade.resolvers import ExchangeResolver, StrategyResolver store_plot_file,
generate_plot_filename)
from freqtrade.state import RunMode from freqtrade.state import RunMode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -68,52 +55,29 @@ def analyse_and_plot_pairs(config: Dict[str, Any]):
-Generate plot files -Generate plot files
:return: None :return: None
""" """
exchange = ExchangeResolver(config.get('exchange', {}).get('name'), config).exchange plot_elements = init_plotscript(config)
trades = plot_elements['trades']
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["timerange"])
ticker_interval = strategy.ticker_interval
tickers = history.load_data(
datadir=Path(str(config.get("datadir"))),
pairs=pairs,
ticker_interval=config['ticker_interval'],
refresh_pairs=config.get('refresh_pairs', False),
timerange=timerange,
exchange=exchange,
live=config.get("live", False),
)
pair_counter = 0 pair_counter = 0
for pair, data in tickers.items(): for pair, data in plot_elements["tickers"].items():
pair_counter += 1 pair_counter += 1
logger.info("analyse pair %s", pair) logger.info("analyse pair %s", pair)
tickers = {} tickers = {}
tickers[pair] = data tickers[pair] = data
dataframe = generate_dataframe(strategy, tickers, pair) dataframe = generate_dataframe(plot_elements["strategy"], tickers, pair)
if config["trade_source"] == "DB":
trades = load_trades_from_db(config["db_url"])
elif config["trade_source"] == "file":
trades = load_backtest_data(Path(config["exportfilename"]))
trades = trades.loc[trades['pair'] == pair] trades_pair = trades.loc[trades['pair'] == pair]
trades = extract_trades_of_period(dataframe, trades) trades_pair = extract_trades_of_period(dataframe, trades_pair)
fig = generate_graph( fig = generate_candlestick_graph(
pair=pair, pair=pair,
data=dataframe, data=dataframe,
trades=trades, trades=trades_pair,
indicators1=config["indicators1"].split(","), indicators1=config["indicators1"].split(","),
indicators2=config["indicators2"].split(",") indicators2=config["indicators2"].split(",")
) )
generate_plot_file(fig, pair, ticker_interval) store_plot_file(fig, generate_plot_filename(pair, config['ticker_interval']))
logger.info('End of ploting process %s plots generated', pair_counter) logger.info('End of ploting process %s plots generated', pair_counter)
@ -125,16 +89,11 @@ def plot_parse_args(args: List[str]) -> Dict[str, Any]:
:return: args: Array with all arguments :return: args: Array with all arguments
""" """
arguments = Arguments(args, 'Graph dataframe') arguments = Arguments(args, 'Graph dataframe')
arguments.common_options() arguments._build_args(optionlist=ARGS_PLOT_DATAFRAME)
arguments.main_options() parsed_args = arguments._parse_args()
arguments.common_optimize_options()
arguments.backtesting_options()
arguments.common_scripts_options()
arguments.plot_dataframe_options()
parsed_args = arguments.parse_args()
# Load the configuration # Load the configuration
config = setup_configuration(parsed_args, RunMode.BACKTEST) config = setup_configuration(parsed_args, RunMode.OTHER)
return config return config

View File

@ -2,217 +2,52 @@
""" """
Script to display profits Script to display profits
Mandatory Cli parameters: Use `python plot_profit.py --help` to display the command line arguments
-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.
""" """
import json
import logging import logging
import sys import sys
from argparse import Namespace from typing import Any, Dict, List
from pathlib import Path
from typing import List, Optional
import numpy as np from freqtrade.configuration import Arguments
import plotly.graph_objs as go from freqtrade.configuration.arguments import ARGS_PLOT_PROFIT
from plotly import tools from freqtrade.optimize import setup_configuration
from plotly.offline import plot from freqtrade.plot.plotting import init_plotscript, generate_profit_graph, store_plot_file
from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration
from freqtrade.data import history
from freqtrade.exchange import timeframe_to_seconds
from freqtrade.misc import common_datearray
from freqtrade.resolvers import StrategyResolver
from freqtrade.state import RunMode from freqtrade.state import RunMode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# data:: [ pair, profit-%, enter, exit, time, duration] def plot_profit(config: Dict[str, Any]) -> None:
# 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: str,
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:
""" """
Plots the total profit for all pairs. Plots the total profit for all pairs.
Note, the profit calculation isn't realistic. Note, the profit calculation isn't realistic.
But should be somewhat proportional, and therefor useful But should be somewhat proportional, and therefor useful
in helping out to find a good algorithm. 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 ticker_interval # Create an average close price of all the pairs that were involved.
# 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']
ticker_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(str(config.get('datadir'))),
pairs=pairs,
ticker_interval=ticker_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 = common_datearray(dataframes)
min_date = int(min(dates).timestamp())
max_date = int(max(dates).timestamp())
num_iterations = define_index(min_date, max_date, ticker_interval) + 1
# Make an average close price of all the pairs that was involved.
# this could be useful to gauge the overall market trend # this could be useful to gauge the overall market trend
# We are essentially saying: fig = generate_profit_graph(plot_elements["pairs"], plot_elements["tickers"], trades)
# array <- sum dataframes[*]['close'] / num_items dataframes store_plot_file(fig, filename='freqtrade-profit-plot.html', auto_open=True)
# 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, ticker_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, ticker_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')))
def define_index(min_date: int, max_date: int, ticker_interval: str) -> int: def plot_parse_args(args: List[str]) -> Dict[str, Any]:
"""
Return the index of a specific date
"""
interval_seconds = timeframe_to_seconds(ticker_interval)
return int((max_date - min_date) / interval_seconds)
def plot_parse_args(args: List[str]) -> Namespace:
""" """
Parse args passed to the script Parse args passed to the script
:param args: Cli arguments :param args: Cli arguments
:return: args: Array with all arguments :return: args: Array with all arguments
""" """
arguments = Arguments(args, 'Graph profits') arguments = Arguments(args, 'Graph profits')
arguments.common_options() arguments._build_args(optionlist=ARGS_PLOT_PROFIT)
arguments.main_options() parsed_args = arguments._parse_args()
arguments.common_optimize_options()
arguments.backtesting_options()
arguments.common_scripts_options()
return arguments.parse_args() # Load the configuration
config = setup_configuration(parsed_args, RunMode.OTHER)
return config
def main(sysargv: List[str]) -> None: def main(sysargv: List[str]) -> None:

View File

@ -1,12 +1,21 @@
#!/usr/bin/env bash #!/usr/bin/env bash
#encoding=utf8 #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 # Check which python version is installed
function check_installed_python() { function check_installed_python() {
which python3.7 which python3.7
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
echo "using Python 3.7" echo "using Python 3.7"
PYTHON=python3.7 PYTHON=python3.7
check_installed_pip
return return
fi fi
@ -14,6 +23,7 @@ function check_installed_python() {
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
echo "using Python 3.6" echo "using Python 3.6"
PYTHON=python3.6 PYTHON=python3.6
check_installed_pip
return return
fi fi
@ -21,7 +31,6 @@ function check_installed_python() {
echo "No usable python found. Please make sure to have python3.6 or python3.7 installed" echo "No usable python found. Please make sure to have python3.6 or python3.7 installed"
exit 1 exit 1
fi fi
} }
function updateenv() { function updateenv() {
@ -29,21 +38,21 @@ function updateenv() {
echo "Updating your virtual env" echo "Updating your virtual env"
echo "-------------------------" echo "-------------------------"
source .env/bin/activate source .env/bin/activate
echo "pip3 install in-progress. Please wait..." echo "pip install in-progress. Please wait..."
# Install numpy first to have py_find_1st install clean # Install numpy first to have py_find_1st install clean
pip3 install --upgrade pip numpy ${PYTHON} -m pip install --upgrade pip numpy
pip3 install --upgrade -r requirements.txt ${PYTHON} -m pip install --upgrade -r requirements.txt
read -p "Do you want to install dependencies for dev [y/N]? " read -p "Do you want to install dependencies for dev [y/N]? "
if [[ $REPLY =~ ^[Yy]$ ]] if [[ $REPLY =~ ^[Yy]$ ]]
then then
pip3 install --upgrade -r requirements-dev.txt ${PYTHON} -m pip install --upgrade -r requirements-dev.txt
else else
echo "Dev dependencies ignored." echo "Dev dependencies ignored."
fi fi
pip3 install --quiet -e . ${PYTHON} -m pip install -e .
echo "pip3 install completed" echo "pip install completed"
echo echo
} }
@ -74,16 +83,14 @@ function install_macos() {
echo "-------------------------" echo "-------------------------"
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
fi fi
brew install python3 wget
install_talib install_talib
test_and_fix_python_on_mac test_and_fix_python_on_mac
} }
# Install bot Debian_ubuntu # Install bot Debian_ubuntu
function install_debian() { function install_debian() {
sudo add-apt-repository ppa:jonathonf/python-3.6
sudo apt-get update 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 autoconf libtool pkg-config make wget git
install_talib install_talib
} }
@ -235,7 +242,7 @@ function install() {
echo "-------------------------" echo "-------------------------"
echo "Run the bot !" echo "Run the bot !"
echo "-------------------------" echo "-------------------------"
echo "You can now use the bot by executing 'source .env/bin/activate; python freqtrade'." echo "You can now use the bot by executing 'source .env/bin/activate; freqtrade'."
} }
function plot() { function plot() {
@ -244,7 +251,7 @@ echo "
Installing dependencies for Plotting scripts Installing dependencies for Plotting scripts
----------------------------------------- -----------------------------------------
" "
pip install plotly --upgrade ${PYTHON} -m pip install plotly --upgrade
} }
function help() { function help() {

View File

@ -1,18 +1,18 @@
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
from functools import reduce
from 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 import talib.abstract as ta
from pandas import DataFrame 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 from skopt.space import Categorical, Dimension, Integer, Real
import freqtrade.vendor.qtpylib.indicators as qtpylib import freqtrade.vendor.qtpylib.indicators as qtpylib
from freqtrade.optimize.hyperopt_interface import IHyperOpt from freqtrade.optimize.hyperopt_interface import IHyperOpt
class_name = 'SampleHyperOpts'
# This class is a sample. Feel free to customize it. # This class is a sample. Feel free to customize it.
class SampleHyperOpts(IHyperOpt): class SampleHyperOpts(IHyperOpt):

View File

@ -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