Merge pull request #5805 from freqtrade/new_release

New release 2021.10
This commit is contained in:
Matthias 2021-10-28 20:49:51 +02:00 committed by GitHub
commit dadf015c23
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
84 changed files with 2585 additions and 1545 deletions

View File

@ -53,7 +53,7 @@ Please find the complete documentation on our [website](https://www.freqtrade.io
- [x] **Dry-run**: Run the bot without paying money. - [x] **Dry-run**: Run the bot without paying money.
- [x] **Backtesting**: Run a simulation of your buy/sell strategy. - [x] **Backtesting**: Run a simulation of your buy/sell strategy.
- [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell strategy parameters with real exchange data. - [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell strategy parameters with real exchange data.
- [x] **Edge position sizing** Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. [Learn more](https://www.freqtrade.io/en/latest/edge/). - [x] **Edge position sizing** Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. [Learn more](https://www.freqtrade.io/en/stable/edge/).
- [x] **Whitelist crypto-currencies**: Select which crypto-currency you want to trade or use dynamic whitelists. - [x] **Whitelist crypto-currencies**: Select which crypto-currency you want to trade or use dynamic whitelists.
- [x] **Blacklist crypto-currencies**: Select which crypto-currency you want to avoid. - [x] **Blacklist crypto-currencies**: Select which crypto-currency you want to avoid.
- [x] **Manageable via Telegram**: Manage the bot with Telegram. - [x] **Manageable via Telegram**: Manage the bot with Telegram.
@ -71,7 +71,7 @@ cd freqtrade
./setup.sh --install ./setup.sh --install
``` ```
For any other type of installation please refer to [Installation doc](https://www.freqtrade.io/en/latest/installation/). For any other type of installation please refer to [Installation doc](https://www.freqtrade.io/en/stable/installation/).
## Basic Usage ## Basic Usage

View File

@ -11,8 +11,13 @@ if [ ! -f "${INSTALL_LOC}/lib/libta_lib.a" ]; then
&& curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD' -o config.guess \ && curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD' -o config.guess \
&& curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD' -o config.sub \ && curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD' -o config.sub \
&& ./configure --prefix=${INSTALL_LOC}/ \ && ./configure --prefix=${INSTALL_LOC}/ \
&& make -j$(nproc) \ && make
&& which sudo && sudo make install || make install if [ $? -ne 0 ]; then
echo "Failed building ta-lib."
cd .. && rm -rf ./ta-lib/
exit 1
fi
which sudo && sudo make install || make install
if [ -x "$(command -v apt-get)" ]; then if [ -x "$(command -v apt-get)" ]; then
echo "Updating library path using ldconfig" echo "Updating library path using ldconfig"
sudo ldconfig sudo ldconfig

View File

@ -28,10 +28,8 @@
"name": "binance", "name": "binance",
"key": "your_exchange_key", "key": "your_exchange_key",
"secret": "your_exchange_secret", "secret": "your_exchange_secret",
"ccxt_config": {"enableRateLimit": true}, "ccxt_config": {},
"ccxt_async_config": { "ccxt_async_config": {
"enableRateLimit": true,
"rateLimit": 200
}, },
"pair_whitelist": [ "pair_whitelist": [
"ALGO/BTC", "ALGO/BTC",

View File

@ -28,11 +28,8 @@
"name": "ftx", "name": "ftx",
"key": "your_exchange_key", "key": "your_exchange_key",
"secret": "your_exchange_secret", "secret": "your_exchange_secret",
"ccxt_config": {"enableRateLimit": true}, "ccxt_config": {},
"ccxt_async_config": { "ccxt_async_config": {},
"enableRateLimit": true,
"rateLimit": 50
},
"pair_whitelist": [ "pair_whitelist": [
"BTC/USD", "BTC/USD",
"ETH/USD", "ETH/USD",

View File

@ -84,12 +84,8 @@
"key": "your_exchange_key", "key": "your_exchange_key",
"secret": "your_exchange_secret", "secret": "your_exchange_secret",
"password": "", "password": "",
"ccxt_config": {"enableRateLimit": true}, "ccxt_config": {},
"ccxt_async_config": { "ccxt_async_config": {},
"enableRateLimit": true,
"rateLimit": 500,
"aiohttp_trust_env": false
},
"pair_whitelist": [ "pair_whitelist": [
"ALGO/BTC", "ALGO/BTC",
"ATOM/BTC", "ATOM/BTC",

View File

@ -28,10 +28,8 @@
"name": "kraken", "name": "kraken",
"key": "your_exchange_key", "key": "your_exchange_key",
"secret": "your_exchange_key", "secret": "your_exchange_key",
"ccxt_config": {"enableRateLimit": true}, "ccxt_config": {},
"ccxt_async_config": { "ccxt_async_config": {
"enableRateLimit": true,
"rateLimit": 1000
}, },
"pair_whitelist": [ "pair_whitelist": [
"ADA/EUR", "ADA/EUR",

View File

@ -15,10 +15,10 @@ services:
volumes: volumes:
- "./user_data:/freqtrade/user_data" - "./user_data:/freqtrade/user_data"
# Expose api on port 8080 (localhost only) # Expose api on port 8080 (localhost only)
# Please read the https://www.freqtrade.io/en/latest/rest-api/ documentation # Please read the https://www.freqtrade.io/en/stable/rest-api/ documentation
# before enabling this. # before enabling this.
# ports: ports:
# - "127.0.0.1:8080:8080" - "127.0.0.1:8080:8080"
# Default command used when running `docker compose up` # Default command used when running `docker compose up`
command: > command: >
trade trade

View File

@ -52,6 +52,71 @@ freqtrade trade -c MyConfigUSDT.json -s MyCustomStrategy --db-url sqlite:///user
For more information regarding usage of the sqlite databases, for example to manually enter or remove trades, please refer to the [SQL Cheatsheet](sql_cheatsheet.md). For more information regarding usage of the sqlite databases, for example to manually enter or remove trades, please refer to the [SQL Cheatsheet](sql_cheatsheet.md).
### Multiple instances using docker
To run multiple instances of freqtrade using docker you will need to edit the docker-compose.yml file and add all the instances you want as separate services. Remember, you can separate your configuration into multiple files, so it's a good idea to think about making them modular, then if you need to edit something common to all bots, you can do that in a single config file.
``` yml
---
version: '3'
services:
freqtrade1:
image: freqtradeorg/freqtrade:stable
# image: freqtradeorg/freqtrade:develop
# Use plotting image
# image: freqtradeorg/freqtrade:develop_plot
# Build step - only needed when additional dependencies are needed
# build:
# context: .
# dockerfile: "./docker/Dockerfile.custom"
restart: always
container_name: freqtrade1
volumes:
- "./user_data:/freqtrade/user_data"
# Expose api on port 8080 (localhost only)
# Please read the https://www.freqtrade.io/en/latest/rest-api/ documentation
# before enabling this.
ports:
- "127.0.0.1:8080:8080"
# Default command used when running `docker compose up`
command: >
trade
--logfile /freqtrade/user_data/logs/freqtrade1.log
--db-url sqlite:////freqtrade/user_data/tradesv3_freqtrade1.sqlite
--config /freqtrade/user_data/config.json
--config /freqtrade/user_data/config.freqtrade1.json
--strategy SampleStrategy
freqtrade2:
image: freqtradeorg/freqtrade:stable
# image: freqtradeorg/freqtrade:develop
# Use plotting image
# image: freqtradeorg/freqtrade:develop_plot
# Build step - only needed when additional dependencies are needed
# build:
# context: .
# dockerfile: "./docker/Dockerfile.custom"
restart: always
container_name: freqtrade2
volumes:
- "./user_data:/freqtrade/user_data"
# Expose api on port 8080 (localhost only)
# Please read the https://www.freqtrade.io/en/latest/rest-api/ documentation
# before enabling this.
ports:
- "127.0.0.1:8081:8080"
# Default command used when running `docker compose up`
command: >
trade
--logfile /freqtrade/user_data/logs/freqtrade2.log
--db-url sqlite:////freqtrade/user_data/tradesv3_freqtrade2.sqlite
--config /freqtrade/user_data/config.json
--config /freqtrade/user_data/config.freqtrade2.json
--strategy SampleStrategy
```
You can use whatever naming convention you want, freqtrade1 and 2 are arbitrary. Note, that you will need to use different database files, port mappings and telegram configurations for each instance, as mentioned above.
## Configure the bot running as a systemd service ## Configure the bot running as a systemd service
Copy the `freqtrade.service` file to your systemd user directory (usually `~/.config/systemd/user`) and update `WorkingDirectory` and `ExecStart` to match your setup. Copy the `freqtrade.service` file to your systemd user directory (usually `~/.config/systemd/user`) and update `WorkingDirectory` and `ExecStart` to match your setup.

View File

@ -21,6 +21,7 @@ usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[--timeframe-detail TIMEFRAME_DETAIL] [--timeframe-detail TIMEFRAME_DETAIL]
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
[--export {none,trades}] [--export-filename PATH] [--export {none,trades}] [--export-filename PATH]
[--breakdown {day,week,month} [{day,week,month} ...]]
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
@ -30,7 +31,7 @@ optional arguments:
Specify what timerange of data to use. Specify what timerange of data to use.
--data-format-ohlcv {json,jsongz,hdf5} --data-format-ohlcv {json,jsongz,hdf5}
Storage format for downloaded candle (OHLCV) data. Storage format for downloaded candle (OHLCV) data.
(default: `None`). (default: `json`).
--max-open-trades INT --max-open-trades INT
Override the value of the `max_open_trades` Override the value of the `max_open_trades`
configuration setting. configuration setting.
@ -65,8 +66,7 @@ optional arguments:
set either in config or via command line. When using set either in config or via command line. When using
this together with `--export trades`, the strategy- this together with `--export trades`, the strategy-
name is injected into the filename (so `backtest- name is injected into the filename (so `backtest-
data.json` becomes `backtest-data- data.json` becomes `backtest-data-SampleStrategy.json`
SampleStrategy.json`
--export {none,trades} --export {none,trades}
Export backtest results (default: trades). Export backtest results (default: trades).
--export-filename PATH --export-filename PATH
@ -74,6 +74,8 @@ optional arguments:
Requires `--export` to be set as well. Example: Requires `--export` to be set as well. Example:
`--export-filename=user_data/backtest_results/backtest `--export-filename=user_data/backtest_results/backtest
_today.json` _today.json`
--breakdown {day,week,month} [{day,week,month} ...]
Show backtesting breakdown per [day, week, month].
Common arguments: Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages). -v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
@ -429,6 +431,31 @@ It contains some useful key metrics about performance of your strategy on backte
- `Drawdown Start` / `Drawdown End`: Start and end datetime for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command). - `Drawdown Start` / `Drawdown End`: Start and end datetime for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command).
- `Market change`: Change of the market during the backtest period. Calculated as average of all pairs changes from the first to the last candle using the "close" column. - `Market change`: Change of the market during the backtest period. Calculated as average of all pairs changes from the first to the last candle using the "close" column.
### Daily / Weekly / Monthly breakdown
You can get an overview over daily / weekly or monthly results by using the `--breakdown <>` switch.
To visualize daily and weekly breakdowns, you can use the following:
``` bash
freqtrade backtesting --strategy MyAwesomeStrategy --breakdown day month
```
``` output
======================== DAY BREAKDOWN =========================
| Day | Tot Profit USDT | Wins | Draws | Losses |
|------------+-------------------+--------+---------+----------|
| 03/07/2021 | 200.0 | 2 | 0 | 0 |
| 04/07/2021 | -50.31 | 0 | 0 | 2 |
| 05/07/2021 | 220.611 | 3 | 2 | 0 |
| 06/07/2021 | 150.974 | 3 | 0 | 2 |
| 07/07/2021 | -70.193 | 1 | 0 | 2 |
| 08/07/2021 | 212.413 | 2 | 0 | 3 |
```
The output will show a table containing the realized absolute Profit (in stake currency) for the given timeperiod, as well as wins, draws and losses that materialized (closed) on this day.
### Further backtest-result analysis ### Further backtest-result analysis
To further analyze your backtest results, you can [export the trades](#exporting-trades-to-file). To further analyze your backtest results, you can [export the trades](#exporting-trades-to-file).

View File

@ -447,45 +447,6 @@ The possible values are: `gtc` (default), `fok` or `ioc`.
This is ongoing work. For now, it is supported only for binance and kucoin. This is ongoing work. For now, it is supported only for binance and kucoin.
Please don't change the default value unless you know what you are doing and have researched the impact of using different values for your particular exchange. Please don't change the default value unless you know what you are doing and have researched the impact of using different values for your particular exchange.
### Exchange configuration
Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports over 100 cryptocurrency
exchange markets and trading APIs. The complete up-to-date list can be found in the
[CCXT repo homepage](https://github.com/ccxt/ccxt/tree/master/python).
However, the bot was tested by the development team with only Bittrex, Binance and Kraken,
so these are the only officially supported exchanges:
- [Bittrex](https://bittrex.com/): "bittrex"
- [Binance](https://www.binance.com/): "binance"
- [Kraken](https://kraken.com/): "kraken"
Feel free to test other exchanges and submit your PR to improve the bot.
Some exchanges require special configuration, which can be found on the [Exchange-specific Notes](exchanges.md) documentation page.
#### Sample exchange configuration
A exchange configuration for "binance" would look as follows:
```json
"exchange": {
"name": "binance",
"key": "your_exchange_key",
"secret": "your_exchange_secret",
"ccxt_config": {"enableRateLimit": true},
"ccxt_async_config": {
"enableRateLimit": true,
"rateLimit": 200
},
```
This configuration enables binance, as well as rate-limiting to avoid bans from the exchange.
`"rateLimit": 200` defines a wait-event of 0.2s between each call. This can also be completely disabled by setting `"enableRateLimit"` to false.
!!! Note
Optimal settings for rate-limiting depend on the exchange and the size of the whitelist, so an ideal parameter will vary on many other settings.
We try to provide sensible defaults per exchange where possible, if you encounter bans please make sure that `"enableRateLimit"` is enabled and increase the `"rateLimit"` parameter step by step.
### What values can be used for fiat_display_currency? ### What values can be used for fiat_display_currency?
The `fiat_display_currency` configuration parameter sets the base currency to use for the The `fiat_display_currency` configuration parameter sets the base currency to use for the

View File

@ -11,7 +11,7 @@ Otherwise `--exchange` becomes mandatory.
You can use a relative timerange (`--days 20`) or an absolute starting point (`--timerange 20200101-`). For incremental downloads, the relative approach should be used. You can use a relative timerange (`--days 20`) or an absolute starting point (`--timerange 20200101-`). For incremental downloads, the relative approach should be used.
!!! Tip "Tip: Updating existing data" !!! Tip "Tip: Updating existing data"
If you already have backtesting data available in your data-directory and would like to refresh this data up to today, do not use `--days` or `--timerange` parameters. Freqtrade will keep the available data and only download the missing data. If you already have backtesting data available in your data-directory and would like to refresh this data up to today, freqtrade will automatically calculate the data missing for the existing pairs and the download will occur from the latest available point until "now", neither --days or --timerange parameters are required. Freqtrade will keep the available data and only download the missing data.
If you are updating existing data after inserting new pairs that you have no data for, use `--new-pairs-days xx` parameter. Specified number of days will be downloaded for new pairs while old pairs will be updated with missing data only. If you are updating existing data after inserting new pairs that you have no data for, use `--new-pairs-days xx` parameter. Specified number of days will be downloaded for new pairs while old pairs will be updated with missing data only.
If you use `--days xx` parameter alone - data for specified number of days will be downloaded for _all_ pairs. Be careful, if specified number of days is smaller than gap between now and last downloaded candle - freqtrade will delete all existing data to avoid gaps in candle data. If you use `--days xx` parameter alone - data for specified number of days will be downloaded for _all_ pairs. Be careful, if specified number of days is smaller than gap between now and last downloaded candle - freqtrade will delete all existing data to avoid gaps in candle data.
@ -22,6 +22,7 @@ usage: freqtrade download-data [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[-d PATH] [--userdir PATH] [-d PATH] [--userdir PATH]
[-p PAIRS [PAIRS ...]] [--pairs-file FILE] [-p PAIRS [PAIRS ...]] [--pairs-file FILE]
[--days INT] [--new-pairs-days INT] [--days INT] [--new-pairs-days INT]
[--include-inactive-pairs]
[--timerange TIMERANGE] [--dl-trades] [--timerange TIMERANGE] [--dl-trades]
[--exchange EXCHANGE] [--exchange EXCHANGE]
[-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...]] [-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...]]
@ -38,6 +39,8 @@ optional arguments:
--days INT Download data for given number of days. --days INT Download data for given number of days.
--new-pairs-days INT Download data of new pairs for given number of days. --new-pairs-days INT Download data of new pairs for given number of days.
Default: `None`. Default: `None`.
--include-inactive-pairs
Also download data from inactive pairs.
--timerange TIMERANGE --timerange TIMERANGE
Specify what timerange of data to use. Specify what timerange of data to use.
--dl-trades Download trades instead of OHLCV data. The bot will --dl-trades Download trades instead of OHLCV data. The bot will
@ -52,10 +55,10 @@ optional arguments:
exchange/pairs/timeframes. exchange/pairs/timeframes.
--data-format-ohlcv {json,jsongz,hdf5} --data-format-ohlcv {json,jsongz,hdf5}
Storage format for downloaded candle (OHLCV) data. Storage format for downloaded candle (OHLCV) data.
(default: `None`). (default: `json`).
--data-format-trades {json,jsongz,hdf5} --data-format-trades {json,jsongz,hdf5}
Storage format for downloaded trades data. (default: Storage format for downloaded trades data. (default:
`None`). `jsongz`).
Common arguments: Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages). -v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
@ -80,6 +83,82 @@ Common arguments:
For that reason, `download-data` does not care about the "startup-period" defined in a strategy. It's up to the user to download additional days if the backtest should start at a specific point in time (while respecting startup period). For that reason, `download-data` does not care about the "startup-period" defined in a strategy. It's up to the user to download additional days if the backtest should start at a specific point in time (while respecting startup period).
### Pairs file
In alternative to the whitelist from `config.json`, a `pairs.json` file can be used.
If you are using Binance for example:
- create a directory `user_data/data/binance` and copy or create the `pairs.json` file in that directory.
- update the `pairs.json` file to contain the currency pairs you are interested in.
```bash
mkdir -p user_data/data/binance
touch user_data/data/binance/pairs.json
```
The format of the `pairs.json` file is a simple json list.
Mixing different stake-currencies is allowed for this file, since it's only used for downloading.
``` json
[
"ETH/BTC",
"ETH/USDT",
"BTC/USDT",
"XRP/ETH"
]
```
!!! Tip "Downloading all data for one quote currency"
Often, you'll want to download data for all pairs of a specific quote-currency. In such cases, you can use the following shorthand:
`freqtrade download-data --exchange binance --pairs .*/USDT <...>`. The provided "pairs" string will be expanded to contain all active pairs on the exchange.
To also download data for inactive (delisted) pairs, add `--include-inactive-pairs` to the command.
??? Note "Permission denied errors"
If your configuration directory `user_data` was made by docker, you may get the following error:
```
cp: cannot create regular file 'user_data/data/binance/pairs.json': Permission denied
```
You can fix the permissions of your user-data directory as follows:
```
sudo chown -R $UID:$GID user_data
```
### Start download
Then run:
```bash
freqtrade download-data --exchange binance
```
This will download historical candle (OHLCV) data for all the currency pairs you defined in `pairs.json`.
Alternatively, specify the pairs directly
```bash
freqtrade download-data --exchange binance --pairs ETH/USDT XRP/USDT BTC/USDT
```
or as regex (to download all active USDT pairs)
```bash
freqtrade download-data --exchange binance --pairs .*/USDT
```
### Other Notes
- 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 historical data from, please use a different configuration file (you'll probably need to adjust rate limits etc.)
- To use `pairs.json` from some other directory, use `--pairs-file some_other_dir/pairs.json`.
- To download historical candle (OHLCV) data for only 10 days, use `--days 10` (defaults to 30 days).
- To download historical candle (OHLCV) data from a fixed starting point, use `--timerange 20200101-` - which will download all data from January 1st, 2020. Eventually set end dates are ignored.
- Use `--timeframes` to specify what timeframe download the historical candle (OHLCV) data for. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute data.
- To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with most other options.
### Data format ### Data format
Freqtrade currently supports 3 data-formats for both OHLCV and trades data: Freqtrade currently supports 3 data-formats for both OHLCV and trades data:
@ -204,6 +283,61 @@ It'll also remove original jsongz data files (`--erase` parameter).
freqtrade convert-trade-data --format-from jsongz --format-to json --datadir ~/.freqtrade/data/kraken --erase freqtrade convert-trade-data --format-from jsongz --format-to json --datadir ~/.freqtrade/data/kraken --erase
``` ```
### Sub-command trades to ohlcv
When you need to use `--dl-trades` (kraken only) to download data, conversion of trades data to ohlcv data is the last step.
This command will allow you to repeat this last step for additional timeframes without re-downloading the data.
```
usage: freqtrade trades-to-ohlcv [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[-d PATH] [--userdir PATH]
[-p PAIRS [PAIRS ...]]
[-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...]]
[--exchange EXCHANGE]
[--data-format-ohlcv {json,jsongz,hdf5}]
[--data-format-trades {json,jsongz,hdf5}]
optional arguments:
-h, --help show this help message and exit
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
Limit command to these pairs. Pairs are space-
separated.
-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...], --timeframes {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...]
Specify which tickers to download. Space-separated
list. Default: `1m 5m`.
--exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no
config is provided.
--data-format-ohlcv {json,jsongz,hdf5}
Storage format for downloaded candle (OHLCV) data.
(default: `json`).
--data-format-trades {json,jsongz,hdf5}
Storage format for downloaded trades data. (default:
`jsongz`).
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
--logfile FILE Log to the file specified. Special values are:
'syslog', 'journald'. See the documentation for more
details.
-V, --version show program's version number and exit
-c PATH, --config PATH
Specify configuration file (default:
`userdir/config.json` or `config.json` whichever
exists). Multiple --config options may be used. Can be
set to `-` to read config from stdin.
-d PATH, --datadir PATH
Path to directory with historical backtesting data.
--userdir PATH, --user-data-dir PATH
Path to userdata directory.
```
#### Example trade-to-ohlcv conversion
``` bash
freqtrade trades-to-ohlcv --exchange kraken -t 5m 1h 1d --pairs BTC/EUR ETH/EUR
```
### Sub-command list-data ### Sub-command list-data
You can get a list of downloaded data using the `list-data` sub-command. You can get a list of downloaded data using the `list-data` sub-command.
@ -257,64 +391,6 @@ ETH/BTC 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d
ETH/USDT 5m, 15m, 30m, 1h, 2h, 4h ETH/USDT 5m, 15m, 30m, 1h, 2h, 4h
``` ```
### Pairs file
In alternative to the whitelist from `config.json`, a `pairs.json` file can be used.
If you are using Binance for example:
- create a directory `user_data/data/binance` and copy or create the `pairs.json` file in that directory.
- update the `pairs.json` file to contain the currency pairs you are interested in.
```bash
mkdir -p user_data/data/binance
cp tests/testdata/pairs.json user_data/data/binance
```
If your configuration directory `user_data` was made by docker, you may get the following error:
```
cp: cannot create regular file 'user_data/data/binance/pairs.json': Permission denied
```
You can fix the permissions of your user-data directory as follows:
```
sudo chown -R $UID:$GID user_data
```
The format of the `pairs.json` file is a simple json list.
Mixing different stake-currencies is allowed for this file, since it's only used for downloading.
``` json
[
"ETH/BTC",
"ETH/USDT",
"BTC/USDT",
"XRP/ETH"
]
```
### Start download
Then run:
```bash
freqtrade download-data --exchange binance
```
This will download historical candle (OHLCV) data for all the currency pairs you defined in `pairs.json`.
### Other Notes
- 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 historical data from, please use a different configuration file (you'll probably need to adjust rate limits etc.)
- To use `pairs.json` from some other directory, use `--pairs-file some_other_dir/pairs.json`.
- To download historical candle (OHLCV) data for only 10 days, use `--days 10` (defaults to 30 days).
- To download historical candle (OHLCV) data from a fixed starting point, use `--timerange 20200101-` - which will download all data from January 1st, 2020. Eventually set end dates are ignored.
- Use `--timeframes` to specify what timeframe download the historical candle (OHLCV) data for. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute data.
- To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with most other options.
### Trades (tick) data ### Trades (tick) data
By default, `download-data` sub-command downloads Candles (OHLCV) data. Some exchanges also provide historic trade-data via their API. By default, `download-data` sub-command downloads Candles (OHLCV) data. Some exchanges also provide historic trade-data via their API.

View File

@ -8,7 +8,7 @@ All contributions, bug reports, bug fixes, documentation improvements, enhanceme
Documentation is available at [https://freqtrade.io](https://www.freqtrade.io/) and needs to be provided with every new feature PR. Documentation is available at [https://freqtrade.io](https://www.freqtrade.io/) and needs to be provided with every new feature PR.
Special fields for the documentation (like Note boxes, ...) can be found [here](https://squidfunk.github.io/mkdocs-material/extensions/admonition/). Special fields for the documentation (like Note boxes, ...) can be found [here](https://squidfunk.github.io/mkdocs-material/reference/admonitions/).
To test the documentation locally use the following commands. To test the documentation locally use the following commands.

View File

@ -70,6 +70,18 @@ docker-compose up -d
!!! Warning "Default configuration" !!! Warning "Default configuration"
While the configuration generated will be mostly functional, you will still need to verify that all options correspond to what you want (like Pricing, pairlist, ...) before starting the bot. While the configuration generated will be mostly functional, you will still need to verify that all options correspond to what you want (like Pricing, pairlist, ...) before starting the bot.
#### Accessing the UI
If you've selected to enable FreqUI in the `new-config` step, you will have freqUI available at port `localhost:8080`.
You can now access the UI by typing localhost:8080 in your browser.
??? Note "UI Access on a remote servers"
If you're running on a VPS, you should consider using either a ssh tunnel, or setup a VPN (openVPN, wireguard) to connect to your bot.
This will ensure that freqUI is not directly exposed to the internet, which is not recommended for security reasons (freqUI does not support https out of the box).
Setup of these tools is not part of this tutorial, however many good tutorials can be found on the internet.
Please also read the [API configuration with docker](rest-api.md#configuration-with-docker) section to learn more about this configuration.
#### Monitoring the bot #### Monitoring the bot
You can check for running instances with `docker-compose ps`. You can check for running instances with `docker-compose ps`.
@ -109,6 +121,7 @@ All freqtrade arguments will be available by running `docker-compose run --rm fr
!!! Warning "`docker-compose` for trade commands" !!! Warning "`docker-compose` for trade commands"
Trade commands (`freqtrade trade <...>`) should not be ran via `docker-compose run` - but should use `docker-compose up -d` instead. Trade commands (`freqtrade trade <...>`) should not be ran via `docker-compose run` - but should use `docker-compose up -d` instead.
This makes sure that the container is properly started (including port forwardings) and will make sure that the container will restart after a system reboot. This makes sure that the container is properly started (including port forwardings) and will make sure that the container will restart after a system reboot.
If you intend to use freqUI, please also ensure to adjust the [configuration accordingly](rest-api.md#configuration-with-docker), otherwise the UI will not be available.
!!! Note "`docker-compose run --rm`" !!! Note "`docker-compose run --rm`"
Including `--rm` will remove the container after completion, and is highly recommended for all modes except trading mode (running with `freqtrade trade` command). Including `--rm` will remove the container after completion, and is highly recommended for all modes except trading mode (running with `freqtrade trade` command).
@ -147,27 +160,9 @@ You'll then also need to modify the `docker-compose.yml` file and uncomment the
dockerfile: "./Dockerfile.<yourextension>" dockerfile: "./Dockerfile.<yourextension>"
``` ```
You can then run `docker-compose build` to build the docker image, and run it using the commands described above. You can then run `docker-compose build --pull` to build the docker image, and run it using the commands described above.
### Troubleshooting ### Plotting with docker-compose
#### Docker on Windows
* Error: `"Timestamp for this request is outside of the recvWindow."`
* The market api requests require a synchronized clock but the time in the docker container shifts a bit over time into the past.
To fix this issue temporarily you need to run `wsl --shutdown` and restart docker again (a popup on windows 10 will ask you to do so).
A permanent solution is either to host the docker container on a linux host or restart the wsl from time to time with the scheduler.
```
taskkill /IM "Docker Desktop.exe" /F
wsl --shutdown
start "" "C:\Program Files\Docker\Docker\Docker Desktop.exe"
```
!!! Warning
Due to the above, we do not recommend the usage of docker on windows for production setups, but only for experimentation, datadownload and backtesting.
Best use a linux-VPS for running freqtrade reliably.
## Plotting with docker-compose
Commands `freqtrade plot-profit` and `freqtrade plot-dataframe` ([Documentation](plotting.md)) are available by changing the image to `*_plot` in your docker-compose.yml file. Commands `freqtrade plot-profit` and `freqtrade plot-dataframe` ([Documentation](plotting.md)) are available by changing the image to `*_plot` in your docker-compose.yml file.
You can then use these commands as follows: You can then use these commands as follows:
@ -178,7 +173,7 @@ docker-compose run --rm freqtrade plot-dataframe --strategy AwesomeStrategy -p B
The output will be stored in the `user_data/plot` directory, and can be opened with any modern browser. The output will be stored in the `user_data/plot` directory, and can be opened with any modern browser.
## Data analysis using docker compose ### Data analysis using docker compose
Freqtrade provides a docker-compose file which starts up a jupyter lab server. Freqtrade provides a docker-compose file which starts up a jupyter lab server.
You can run this server using the following command: You can run this server using the following command:
@ -195,3 +190,22 @@ Since part of this image is built on your machine, it is recommended to rebuild
``` bash ``` bash
docker-compose -f docker/docker-compose-jupyter.yml build --no-cache docker-compose -f docker/docker-compose-jupyter.yml build --no-cache
``` ```
## Troubleshooting
### Docker on Windows
* Error: `"Timestamp for this request is outside of the recvWindow."`
* The market api requests require a synchronized clock but the time in the docker container shifts a bit over time into the past.
To fix this issue temporarily you need to run `wsl --shutdown` and restart docker again (a popup on windows 10 will ask you to do so).
A permanent solution is either to host the docker container on a linux host or restart the wsl from time to time with the scheduler.
``` bash
taskkill /IM "Docker Desktop.exe" /F
wsl --shutdown
start "" "C:\Program Files\Docker\Docker\Docker Desktop.exe"
```
!!! Warning
Due to the above, we do not recommend the usage of docker on windows for production setups, but only for experimentation, datadownload and backtesting.
Best use a linux-VPS for running freqtrade reliably.

View File

@ -2,6 +2,56 @@
This page combines common gotchas and informations which are exchange-specific and most likely don't apply to other exchanges. This page combines common gotchas and informations which are exchange-specific and most likely don't apply to other exchanges.
## Exchange configuration
Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports over 100 cryptocurrency
exchange markets and trading APIs. The complete up-to-date list can be found in the
[CCXT repo homepage](https://github.com/ccxt/ccxt/tree/master/python).
However, the bot was tested by the development team with only a few exchanges.
A current list of these can be found in the "Home" section of this documentation.
Feel free to test other exchanges and submit your feedback or PR to improve the bot or confirm exchanges that work flawlessly..
Some exchanges require special configuration, which can be found below.
### Sample exchange configuration
A exchange configuration for "binance" would look as follows:
```json
"exchange": {
"name": "binance",
"key": "your_exchange_key",
"secret": "your_exchange_secret",
"ccxt_config": {},
"ccxt_async_config": {},
// ...
```
### Setting rate limits
Usually, rate limits set by CCXT are reliable and work well.
In case of problems related to rate-limits (usually DDOS Exceptions in your logs), it's easy to change rateLimit settings to other values.
```json
"exchange": {
"name": "kraken",
"key": "your_exchange_key",
"secret": "your_exchange_secret",
"ccxt_config": {"enableRateLimit": true},
"ccxt_async_config": {
"enableRateLimit": true,
"rateLimit": 3100
},
```
This configuration enables kraken, as well as rate-limiting to avoid bans from the exchange.
`"rateLimit": 3100` defines a wait-event of 0.2s between each call. This can also be completely disabled by setting `"enableRateLimit"` to false.
!!! Note
Optimal settings for rate-limiting depend on the exchange and the size of the whitelist, so an ideal parameter will vary on many other settings.
We try to provide sensible defaults per exchange where possible, if you encounter bans please make sure that `"enableRateLimit"` is enabled and increase the `"rateLimit"` parameter step by step.
## Binance ## Binance
Binance supports [time_in_force](configuration.md#understand-order_time_in_force). Binance supports [time_in_force](configuration.md#understand-order_time_in_force).

View File

@ -54,9 +54,11 @@ you can't say much from few trades.
Yes. You can edit your config and use the `/reload_config` command to reload the configuration. The bot will stop, reload the configuration and strategy and will restart with the new configuration and strategy. Yes. You can edit your config and use the `/reload_config` command to reload the configuration. The bot will stop, reload the configuration and strategy and will restart with the new configuration and strategy.
### I want to improve the bot with a new strategy ### I want to use incomplete candles
That's great. We have a nice backtesting and hyperoptimization setup. See the tutorial [here|Testing-new-strategies-with-Hyperopt](bot-usage.md#hyperopt-commands). Freqtrade will not provide incomplete candles to strategies. Using incomplete candles will lead to repainting and consequently to strategies with "ghost" buys, which are impossible to both backtest, and verify after they happened.
You can use "current" market data by using the [dataprovider](strategy-customization.md#orderbookpair-maximum)'s orderbook or ticker methods - which however cannot be used during backtesting.
### 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?
@ -82,11 +84,11 @@ Currently known to happen for US Bittrex users.
Read [the Bittrex section about restricted markets](exchanges.md#restricted-markets) for more information. Read [the Bittrex section about restricted markets](exchanges.md#restricted-markets) for more information.
### I'm getting the "Exchange Bittrex does not support market orders." message and cannot run my strategy ### I'm getting the "Exchange XXX does not support market orders." message and cannot run my strategy
As the message says, Bittrex does not support market orders and you have one of the [order types](configuration.md/#understand-order_types) set to "market". Your strategy was probably written with other exchanges in mind and sets "market" orders for "stoploss" orders, which is correct and preferable for most of the exchanges supporting market orders (but not for Bittrex). As the message says, your exchange does not support market orders and you have one of the [order types](configuration.md/#understand-order_types) set to "market". Your strategy was probably written with other exchanges in mind and sets "market" orders for "stoploss" orders, which is correct and preferable for most of the exchanges supporting market orders (but not for Bittrex and Gate.io).
To fix it for Bittrex, redefine order types in the strategy to use "limit" instead of "market": To fix this, redefine order types in the strategy to use "limit" instead of "market":
``` ```
order_types = { order_types = {
@ -136,6 +138,8 @@ On Windows, the `--logfile` option is also supported by Freqtrade and you can us
> type \path\to\mylogfile.log | findstr "something" > type \path\to\mylogfile.log | findstr "something"
``` ```
## Hyperopt module
### Why does freqtrade not have GPU support? ### Why does freqtrade not have GPU support?
First of all, most indicator libraries don't have GPU support - as such, there would be little benefit for indicator calculations. First of all, most indicator libraries don't have GPU support - as such, there would be little benefit for indicator calculations.
@ -152,8 +156,6 @@ The benefit of using GPU would therefore be pretty slim - and will not justify t
There is however nothing preventing you from using GPU-enabled indicators within your strategy if you think you must have this - you will however probably be disappointed by the slim gain that will give you (compared to the complexity). There is however nothing preventing you from using GPU-enabled indicators within your strategy if you think you must have this - you will however probably be disappointed by the slim gain that will give you (compared to the complexity).
## Hyperopt module
### How many epochs do I need to get a good Hyperopt result? ### How many epochs do I need to get a good Hyperopt result?
Per default Hyperopt called without the `-e`/`--epochs` command line option will only Per default Hyperopt called without the `-e`/`--epochs` command line option will only

View File

@ -51,6 +51,7 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
[--print-all] [--no-color] [--print-json] [-j JOBS] [--print-all] [--no-color] [--print-json] [-j JOBS]
[--random-state INT] [--min-trades INT] [--random-state INT] [--min-trades INT]
[--hyperopt-loss NAME] [--disable-param-export] [--hyperopt-loss NAME] [--disable-param-export]
[--ignore-missing-spaces]
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
@ -60,7 +61,7 @@ optional arguments:
Specify what timerange of data to use. Specify what timerange of data to use.
--data-format-ohlcv {json,jsongz,hdf5} --data-format-ohlcv {json,jsongz,hdf5}
Storage format for downloaded candle (OHLCV) data. Storage format for downloaded candle (OHLCV) data.
(default: `None`). (default: `json`).
--max-open-trades INT --max-open-trades INT
Override the value of the `max_open_trades` Override the value of the `max_open_trades`
configuration setting. configuration setting.
@ -114,9 +115,13 @@ optional arguments:
Hyperopt-loss-functions are: Hyperopt-loss-functions are:
ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss, ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss,
SharpeHyperOptLoss, SharpeHyperOptLossDaily, SharpeHyperOptLoss, SharpeHyperOptLossDaily,
SortinoHyperOptLoss, SortinoHyperOptLossDaily SortinoHyperOptLoss, SortinoHyperOptLossDaily,
MaxDrawDownHyperOptLoss
--disable-param-export --disable-param-export
Disable automatic hyperopt parameter export. Disable automatic hyperopt parameter export.
--ignore-missing-spaces, --ignore-unparameterized-spaces
Suppress errors for any requested Hyperopt spaces that
do not contain any parameters.
Common arguments: Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages). -v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
@ -512,12 +517,13 @@ This class should be in its own file within the `user_data/hyperopts/` directory
Currently, the following loss functions are builtin: Currently, the following loss functions are builtin:
* `ShortTradeDurHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function) - Mostly for short trade duration and avoiding losses. * `ShortTradeDurHyperOptLoss` - (default legacy Freqtrade hyperoptimization loss function) - Mostly for short trade duration and avoiding losses.
* `OnlyProfitHyperOptLoss` (which takes only amount of profit into consideration) * `OnlyProfitHyperOptLoss` - takes only amount of profit into consideration.
* `SharpeHyperOptLoss` (optimizes Sharpe Ratio calculated on trade returns relative to standard deviation) * `SharpeHyperOptLoss` - optimizes Sharpe Ratio calculated on trade returns relative to standard deviation.
* `SharpeHyperOptLossDaily` (optimizes Sharpe Ratio calculated on **daily** trade returns relative to standard deviation) * `SharpeHyperOptLossDaily` - optimizes Sharpe Ratio calculated on **daily** trade returns relative to standard deviation.
* `SortinoHyperOptLoss` (optimizes Sortino Ratio calculated on trade returns relative to **downside** standard deviation) * `SortinoHyperOptLoss` - optimizes Sortino Ratio calculated on trade returns relative to **downside** standard deviation.
* `SortinoHyperOptLossDaily` (optimizes Sortino Ratio calculated on **daily** trade returns relative to **downside** standard deviation) * `SortinoHyperOptLossDaily` - optimizes Sortino Ratio calculated on **daily** trade returns relative to **downside** standard deviation.
* `MaxDrawDownHyperOptLoss` - Optimizes Maximum drawdown.
Creation of a custom loss function is covered in the [Advanced Hyperopt](advanced-hyperopt.md) part of the documentation. Creation of a custom loss function is covered in the [Advanced Hyperopt](advanced-hyperopt.md) part of the documentation.

View File

@ -52,6 +52,8 @@ To skip pair validation against active markets, set `"allow_inactive": true` wit
This can be useful for backtesting expired pairs (like quarterly spot-markets). This can be useful for backtesting expired pairs (like quarterly spot-markets).
This option must be configured along with `exchange.skip_pair_validation` in the exchange configuration. This option must be configured along with `exchange.skip_pair_validation` in the exchange configuration.
When used in a "follow-up" position (e.g. after VolumePairlist), all pairs in `'pair_whitelist'` will be added to the end of the pairlist.
#### Volume Pair List #### Volume Pair List
`VolumePairList` employs sorting/filtering of pairs by their trading volume. It selects `number_assets` top pairs with sorting based on the `sort_key` (which can only be `quoteVolume`). `VolumePairList` employs sorting/filtering of pairs by their trading volume. It selects `number_assets` top pairs with sorting based on the `sort_key` (which can only be `quoteVolume`).
@ -194,17 +196,22 @@ Trade count is used as a tie breaker.
You can use the `minutes` parameter to only consider performance of the past X minutes (rolling window). You can use the `minutes` parameter to only consider performance of the past X minutes (rolling window).
Not defining this parameter (or setting it to 0) will use all-time performance. Not defining this parameter (or setting it to 0) will use all-time performance.
The optional `min_profit` parameter defines the minimum profit a pair must have to be considered.
Pairs below this level will be filtered out.
Using this parameter without `minutes` is highly discouraged, as it can lead to an empty pairlist without without a way to recover.
```json ```json
"pairlists": [ "pairlists": [
// ... // ...
{ {
"method": "PerformanceFilter", "method": "PerformanceFilter",
"minutes": 1440 // rolling 24h "minutes": 1440, // rolling 24h
"min_profit": 0.01
} }
], ],
``` ```
!!! Note !!! Warning "Backtesting"
`PerformanceFilter` does not support backtesting mode. `PerformanceFilter` does not support backtesting mode.
#### PrecisionFilter #### PrecisionFilter

View File

@ -113,6 +113,13 @@ git checkout develop
You may later switch between branches at any time with the `git checkout stable`/`git checkout develop` commands. You may later switch between branches at any time with the `git checkout stable`/`git checkout develop` commands.
??? Note "Install from pypi"
An alternative way to install Freqtrade is from [pypi](https://pypi.org/project/freqtrade/). The downside is that this method requires ta-lib to be correctly installed beforehand, and is therefore currently not the recommended way to install Freqtrade.
``` bash
pip install freqtrade
```
------ ------
## Script Installation ## Script Installation

View File

@ -1,4 +1,4 @@
mkdocs==1.2.2 mkdocs==1.2.3
mkdocs-material==7.3.0 mkdocs-material==7.3.4
mdx_truly_sane_lists==1.2 mdx_truly_sane_lists==1.2
pymdown-extensions==8.2 pymdown-extensions==9.0

View File

@ -78,7 +78,7 @@ If you run your bot using docker, you'll need to have the bot listen to incoming
}, },
``` ```
Uncomment the following from your docker-compose file: Make sure that the following 2 lines are available in your docker-compose file:
```yml ```yml
ports: ports:

View File

@ -122,6 +122,16 @@ def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame
Look into the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_strategy.py). Look into the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_strategy.py).
Then uncomment indicators you need. Then uncomment indicators you need.
#### Indicator libraries
Out of the box, freqtrade installs the following technical libraries:
* [ta-lib](http://mrjbq7.github.io/ta-lib/)
* [pandas-ta](https://twopirllc.github.io/pandas-ta/)
* [technical](https://github.com/freqtrade/technical/)
Additional technical libraries can be installed as necessary, or custom indicators may be written / invented by the strategy author.
### Strategy startup period ### Strategy startup period
Most indicators have an instable startup period, in which they are either not available, or the calculation is incorrect. This can lead to inconsistencies, since Freqtrade does not know how long this instable period should be. Most indicators have an instable startup period, in which they are either not available, or the calculation is incorrect. This can lead to inconsistencies, since Freqtrade does not know how long this instable period should be.
@ -302,7 +312,7 @@ Currently this is `pair`, which can be accessed using `metadata['pair']` - and w
The Metadata-dict should not be modified and does not persist information across multiple calls. The Metadata-dict should not be modified and does not persist information across multiple calls.
Instead, have a look at the section [Storing information](strategy-advanced.md#Storing-information) Instead, have a look at the section [Storing information](strategy-advanced.md#Storing-information)
## Additional data (informative_pairs) ## Informative Pairs
### Get data for non-tradeable pairs ### Get data for non-tradeable pairs
@ -331,6 +341,133 @@ A full sample can be found [in the DataProvider section](#complete-data-provider
*** ***
### Informative pairs decorator (`@informative()`)
In most common case it is possible to easily define informative pairs by using a decorator. All decorated `populate_indicators_*` methods run in isolation,
not having access to data from other informative pairs, in the end all informative dataframes are merged and passed to main `populate_indicators()` method.
When hyperopting, use of hyperoptable parameter `.value` attribute is not supported. Please use `.range` attribute. See [optimizing an indicator parameter](hyperopt.md#optimizing-an-indicator-parameter)
for more information.
??? info "Full documentation"
``` python
def informative(timeframe: str, asset: str = '',
fmt: Optional[Union[str, Callable[[KwArg(str)], str]]] = None,
ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]:
"""
A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to
define informative indicators.
Example usage:
@informative('1h')
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
return dataframe
:param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe.
:param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use
current pair.
:param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not
specified, defaults to:
* {base}_{quote}_{column}_{timeframe} if asset is specified.
* {column}_{timeframe} if asset is not specified.
Format string supports these format variables:
* {asset} - full name of the asset, for example 'BTC/USDT'.
* {base} - base currency in lower case, for example 'eth'.
* {BASE} - same as {base}, except in upper case.
* {quote} - quote currency in lower case, for example 'usdt'.
* {QUOTE} - same as {quote}, except in upper case.
* {column} - name of dataframe column.
* {timeframe} - timeframe of informative dataframe.
:param ffill: ffill dataframe after merging informative pair.
"""
```
??? Example "Fast and easy way to define informative pairs"
Most of the time we do not need power and flexibility offered by `merge_informative_pair()`, therefore we can use a decorator to quickly define informative pairs.
``` python
from datetime import datetime
from freqtrade.persistence import Trade
from freqtrade.strategy import IStrategy, informative
class AwesomeStrategy(IStrategy):
# This method is not required.
# def informative_pairs(self): ...
# Define informative upper timeframe for each pair. Decorators can be stacked on same
# method. Available in populate_indicators as 'rsi_30m' and 'rsi_1h'.
@informative('30m')
@informative('1h')
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
return dataframe
# Define BTC/STAKE informative pair. Available in populate_indicators and other methods as
# 'btc_rsi_1h'. Current stake currency should be specified as {stake} format variable
# instead of hardcoding actual stake currency. Available in populate_indicators and other
# methods as 'btc_usdt_rsi_1h' (when stake currency is USDT).
@informative('1h', 'BTC/{stake}')
def populate_indicators_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
return dataframe
# Define BTC/ETH informative pair. You must specify quote currency if it is different from
# stake currency. Available in populate_indicators and other methods as 'eth_btc_rsi_1h'.
@informative('1h', 'ETH/BTC')
def populate_indicators_eth_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
return dataframe
# Define BTC/STAKE informative pair. A custom formatter may be specified for formatting
# column names. A callable `fmt(**kwargs) -> str` may be specified, to implement custom
# formatting. Available in populate_indicators and other methods as 'rsi_upper'.
@informative('1h', 'BTC/{stake}', '{column}')
def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['rsi_upper'] = ta.RSI(dataframe, timeperiod=14)
return dataframe
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Strategy timeframe indicators for current pair.
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
# Informative pairs are available in this method.
dataframe['rsi_less'] = dataframe['rsi'] < dataframe['rsi_1h']
return dataframe
```
!!! Note
Do not use `@informative` decorator if you need to use data of one informative pair when generating another informative pair. Instead, define informative pairs
manually as described [in the DataProvider section](#complete-data-provider-sample).
!!! Note
Use string formatting when accessing informative dataframes of other pairs. This will allow easily changing stake currency in config without having to adjust strategy code.
``` python
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
stake = self.config['stake_currency']
dataframe.loc[
(
(dataframe[f'btc_{stake}_rsi_1h'] < 35)
&
(dataframe['volume'] > 0)
),
['buy', 'buy_tag']] = (1, 'buy_signal_rsi')
return dataframe
```
Alternatively column renaming may be used to remove stake currency from column names: `@informative('1h', 'BTC/{stake}', fmt='{base}_{column}_{timeframe}')`.
!!! Warning "Duplicate method names"
Methods tagged with `@informative()` decorator must always have unique names! Re-using same name (for example when copy-pasting already defined informative method)
will overwrite previously defined method and not produce any errors due to limitations of Python programming language. In such cases you will find that indicators
created in earlier-defined methods are not available in the dataframe. Carefully review method names and make sure they are unique!
## Additional data (DataProvider) ## Additional data (DataProvider)
The strategy provides access to the `DataProvider`. This allows you to get additional data to use in your strategy. The strategy provides access to the `DataProvider`. This allows you to get additional data to use in your strategy.
@ -676,131 +813,6 @@ In some situations it may be confusing to deal with stops relative to current ra
``` ```
### *@informative()*
``` python
def informative(timeframe: str, asset: str = '',
fmt: Optional[Union[str, Callable[[KwArg(str)], str]]] = None,
ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]:
"""
A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to
define informative indicators.
Example usage:
@informative('1h')
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
return dataframe
:param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe.
:param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use
current pair.
:param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not
specified, defaults to:
* {base}_{quote}_{column}_{timeframe} if asset is specified.
* {column}_{timeframe} if asset is not specified.
Format string supports these format variables:
* {asset} - full name of the asset, for example 'BTC/USDT'.
* {base} - base currency in lower case, for example 'eth'.
* {BASE} - same as {base}, except in upper case.
* {quote} - quote currency in lower case, for example 'usdt'.
* {QUOTE} - same as {quote}, except in upper case.
* {column} - name of dataframe column.
* {timeframe} - timeframe of informative dataframe.
:param ffill: ffill dataframe after merging informative pair.
"""
```
In most common case it is possible to easily define informative pairs by using a decorator. All decorated `populate_indicators_*` methods run in isolation,
not having access to data from other informative pairs, in the end all informative dataframes are merged and passed to main `populate_indicators()` method.
When hyperopting, use of hyperoptable parameter `.value` attribute is not supported. Please use `.range` attribute. See [optimizing an indicator parameter](hyperopt.md#optimizing-an-indicator-parameter)
for more information.
??? Example "Fast and easy way to define informative pairs"
Most of the time we do not need power and flexibility offered by `merge_informative_pair()`, therefore we can use a decorator to quickly define informative pairs.
``` python
from datetime import datetime
from freqtrade.persistence import Trade
from freqtrade.strategy import IStrategy, informative
class AwesomeStrategy(IStrategy):
# This method is not required.
# def informative_pairs(self): ...
# Define informative upper timeframe for each pair. Decorators can be stacked on same
# method. Available in populate_indicators as 'rsi_30m' and 'rsi_1h'.
@informative('30m')
@informative('1h')
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
return dataframe
# Define BTC/STAKE informative pair. Available in populate_indicators and other methods as
# 'btc_rsi_1h'. Current stake currency should be specified as {stake} format variable
# instead of hardcoding actual stake currency. Available in populate_indicators and other
# methods as 'btc_usdt_rsi_1h' (when stake currency is USDT).
@informative('1h', 'BTC/{stake}')
def populate_indicators_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
return dataframe
# Define BTC/ETH informative pair. You must specify quote currency if it is different from
# stake currency. Available in populate_indicators and other methods as 'eth_btc_rsi_1h'.
@informative('1h', 'ETH/BTC')
def populate_indicators_eth_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
return dataframe
# Define BTC/STAKE informative pair. A custom formatter may be specified for formatting
# column names. A callable `fmt(**kwargs) -> str` may be specified, to implement custom
# formatting. Available in populate_indicators and other methods as 'rsi_upper'.
@informative('1h', 'BTC/{stake}', '{column}')
def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['rsi_upper'] = ta.RSI(dataframe, timeperiod=14)
return dataframe
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Strategy timeframe indicators for current pair.
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
# Informative pairs are available in this method.
dataframe['rsi_less'] = dataframe['rsi'] < dataframe['rsi_1h']
return dataframe
```
!!! Note
Do not use `@informative` decorator if you need to use data of one informative pair when generating another informative pair. Instead, define informative pairs
manually as described [in the DataProvider section](#complete-data-provider-sample).
!!! Note
Use string formatting when accessing informative dataframes of other pairs. This will allow easily changing stake currency in config without having to adjust strategy code.
``` python
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
stake = self.config['stake_currency']
dataframe.loc[
(
(dataframe[f'btc_{stake}_rsi_1h'] < 35)
&
(dataframe['volume'] > 0)
),
['buy', 'buy_tag']] = (1, 'buy_signal_rsi')
return dataframe
```
Alternatively column renaming may be used to remove stake currency from column names: `@informative('1h', 'BTC/{stake}', fmt='{base}_{column}_{timeframe}')`.
!!! Warning "Duplicate method names"
Methods tagged with `@informative()` decorator must always have unique names! Re-using same name (for example when copy-pasting already defined informative method)
will overwrite previously defined method and not produce any errors due to limitations of Python programming language. In such cases you will find that indicators
created in earlier-defined methods are not available in the dataframe. Carefully review method names and make sure they are unique!
## Additional data (Wallets) ## Additional data (Wallets)
The strategy provides access to the `Wallets` object. This contains the current balances on the exchange. The strategy provides access to the `Wallets` object. This contains the current balances on the exchange.

View File

@ -171,7 +171,7 @@ official commands. You can ask at any moment for help with `/help`.
| `/profit [<n>]` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default) | `/profit [<n>]` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default)
| `/forcesell <trade_id>` | Instantly sells the given trade (Ignoring `minimum_roi`). | `/forcesell <trade_id>` | Instantly sells the given trade (Ignoring `minimum_roi`).
| `/forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`). | `/forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`).
| `/forcebuy <pair> [rate]` | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True) | `/forcebuy <pair> [rate]` | Instantly buys the given pair. Rate is optional and only applies to limit orders. (`forcebuy_enable` must be set to True)
| `/performance` | Show performance of each finished trade grouped by pair | `/performance` | Show performance of each finished trade grouped by pair
| `/balance` | Show account balance per currency | `/balance` | Show account balance per currency
| `/daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7) | `/daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7)

View File

@ -281,7 +281,7 @@ bitmax True missing opt: fetchMyTrades
bitmex False Various reasons. bitmex False Various reasons.
bitpanda True bitpanda True
bitso False missing: fetchOHLCV bitso False missing: fetchOHLCV
bitstamp False Does not provide history. Details in https://github.com/freqtrade/freqtrade/issues/1983 bitstamp True missing opt: fetchTickers
bitstamp1 False missing: fetchOrder, fetchOHLCV bitstamp1 False missing: fetchOrder, fetchOHLCV
bittrex True bittrex True
bitvavo True bitvavo True
@ -667,6 +667,7 @@ usage: freqtrade hyperopt-show [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[--profitable] [-n INT] [--print-json] [--profitable] [-n INT] [--print-json]
[--hyperopt-filename FILENAME] [--no-header] [--hyperopt-filename FILENAME] [--no-header]
[--disable-param-export] [--disable-param-export]
[--breakdown {day,week,month} [{day,week,month} ...]]
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
@ -680,6 +681,8 @@ optional arguments:
--no-header Do not print epoch details header. --no-header Do not print epoch details header.
--disable-param-export --disable-param-export
Disable automatic hyperopt parameter export. Disable automatic hyperopt parameter export.
--breakdown {day,week,month} [{day,week,month} ...]
Show backtesting breakdown per [day, week, month].
Common arguments: Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages). -v, --verbose Verbose mode (-vv for more, -vvv to get all messages).

View File

@ -16,7 +16,6 @@ dependencies:
- cachetools - cachetools
- requests - requests
- urllib3 - urllib3
- wrapt
- jsonschema - jsonschema
- TA-Lib - TA-Lib
- tabulate - tabulate
@ -64,7 +63,6 @@ dependencies:
- py_find_1st - py_find_1st
- tables - tables
- pytest-random-order - pytest-random-order
- flake8-type-annotations
- ccxt - ccxt
- flake8-tidy-imports - flake8-tidy-imports
- -e . - -e .

View File

@ -1,5 +1,5 @@
""" Freqtrade bot """ """ Freqtrade bot """
__version__ = '2021.9' __version__ = '2021.10'
if __version__ == 'develop': if __version__ == 'develop':

View File

@ -8,8 +8,8 @@ Note: Be careful with file-scoped imports in these subfiles.
""" """
from freqtrade.commands.arguments import Arguments from freqtrade.commands.arguments import Arguments
from freqtrade.commands.build_config_commands import start_new_config from freqtrade.commands.build_config_commands import start_new_config
from freqtrade.commands.data_commands import (start_convert_data, start_download_data, from freqtrade.commands.data_commands import (start_convert_data, start_convert_trades,
start_list_data) start_download_data, start_list_data)
from freqtrade.commands.deploy_commands import (start_create_userdir, start_install_ui, from freqtrade.commands.deploy_commands import (start_create_userdir, start_install_ui,
start_new_strategy) start_new_strategy)
from freqtrade.commands.hyperopt_commands import start_hyperopt_list, start_hyperopt_show from freqtrade.commands.hyperopt_commands import start_hyperopt_list, start_hyperopt_show

View File

@ -23,7 +23,8 @@ ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv",
ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions", ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions",
"enable_protections", "dry_run_wallet", "timeframe_detail", "enable_protections", "dry_run_wallet", "timeframe_detail",
"strategy_list", "export", "exportfilename"] "strategy_list", "export", "exportfilename",
"backtest_breakdown"]
ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path", ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
"position_stacking", "use_max_market_positions", "position_stacking", "use_max_market_positions",
@ -31,7 +32,8 @@ ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
"epochs", "spaces", "print_all", "epochs", "spaces", "print_all",
"print_colorized", "print_json", "hyperopt_jobs", "print_colorized", "print_json", "hyperopt_jobs",
"hyperopt_random_state", "hyperopt_min_trades", "hyperopt_random_state", "hyperopt_min_trades",
"hyperopt_loss", "disableparamexport"] "hyperopt_loss", "disableparamexport",
"hyperopt_ignore_missing_space"]
ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"] ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"]
@ -58,11 +60,13 @@ ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "template"]
ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase"] ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase"]
ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes"] ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes"]
ARGS_CONVERT_TRADES = ["pairs", "timeframes", "exchange", "dataformat_ohlcv", "dataformat_trades"]
ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs"] ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs"]
ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "new_pairs_days", "timerange", ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "new_pairs_days", "include_inactive",
"download_trades", "exchange", "timeframes", "erase", "dataformat_ohlcv", "timerange", "download_trades", "exchange", "timeframes",
"dataformat_trades"] "erase", "dataformat_ohlcv", "dataformat_trades"]
ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit", ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit",
"db_url", "trade_source", "export", "exportfilename", "db_url", "trade_source", "export", "exportfilename",
@ -71,7 +75,7 @@ ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit",
ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url", ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url",
"trade_source", "timeframe", "plot_auto_open"] "trade_source", "timeframe", "plot_auto_open"]
ARGS_INSTALL_UI = ["erase_ui_only"] ARGS_INSTALL_UI = ["erase_ui_only", 'ui_version']
ARGS_SHOW_TRADES = ["db_url", "trade_ids", "print_json"] ARGS_SHOW_TRADES = ["db_url", "trade_ids", "print_json"]
@ -86,12 +90,12 @@ ARGS_HYPEROPT_LIST = ["hyperopt_list_best", "hyperopt_list_profitable",
ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperopt_show_index", ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperopt_show_index",
"print_json", "hyperoptexportfilename", "hyperopt_show_no_header", "print_json", "hyperoptexportfilename", "hyperopt_show_no_header",
"disableparamexport"] "disableparamexport", "backtest_breakdown"]
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes", NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
"list-markets", "list-pairs", "list-strategies", "list-data", "list-markets", "list-pairs", "list-strategies", "list-data",
"hyperopt-list", "hyperopt-show", "hyperopt-list", "hyperopt-show",
"plot-dataframe", "plot-profit", "show-trades"] "plot-dataframe", "plot-profit", "show-trades", "trades-to-ohlcv"]
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"] NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
@ -169,14 +173,14 @@ class Arguments:
self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot') self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot')
self._build_args(optionlist=['version'], parser=self.parser) self._build_args(optionlist=['version'], parser=self.parser)
from freqtrade.commands import (start_backtesting, start_convert_data, start_create_userdir, from freqtrade.commands import (start_backtesting, start_convert_data, start_convert_trades,
start_download_data, start_edge, start_hyperopt, start_create_userdir, start_download_data, start_edge,
start_hyperopt_list, start_hyperopt_show, start_install_ui, start_hyperopt, start_hyperopt_list, start_hyperopt_show,
start_list_data, start_list_exchanges, start_list_markets, start_install_ui, start_list_data, start_list_exchanges,
start_list_strategies, start_list_timeframes, start_list_markets, start_list_strategies,
start_new_config, start_new_strategy, start_plot_dataframe, start_list_timeframes, start_new_config, start_new_strategy,
start_plot_profit, start_show_trades, start_test_pairlist, start_plot_dataframe, start_plot_profit, start_show_trades,
start_trading, start_webserver) start_test_pairlist, start_trading, start_webserver)
subparsers = self.parser.add_subparsers(dest='command', subparsers = self.parser.add_subparsers(dest='command',
# Use custom message when no subhandler is added # Use custom message when no subhandler is added
@ -236,6 +240,15 @@ class Arguments:
convert_trade_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=False)) convert_trade_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=False))
self._build_args(optionlist=ARGS_CONVERT_DATA, parser=convert_trade_data_cmd) self._build_args(optionlist=ARGS_CONVERT_DATA, parser=convert_trade_data_cmd)
# Add trades-to-ohlcv subcommand
convert_trade_data_cmd = subparsers.add_parser(
'trades-to-ohlcv',
help='Convert trade data to OHLCV data.',
parents=[_common_parser],
)
convert_trade_data_cmd.set_defaults(func=start_convert_trades)
self._build_args(optionlist=ARGS_CONVERT_TRADES, parser=convert_trade_data_cmd)
# Add list-data subcommand # Add list-data subcommand
list_data_cmd = subparsers.add_parser( list_data_cmd = subparsers.add_parser(
'list-data', 'list-data',

View File

@ -163,7 +163,8 @@ def ask_user_config() -> Dict[str, Any]:
{ {
"type": "text", "type": "text",
"name": "api_server_listen_addr", "name": "api_server_listen_addr",
"message": "Insert Api server Listen Address (best left untouched default!)", "message": ("Insert Api server Listen Address (0.0.0.0 for docker, "
"otherwise best left untouched)"),
"default": "127.0.0.1", "default": "127.0.0.1",
"when": lambda x: x['api_server'] "when": lambda x: x['api_server']
}, },

View File

@ -193,6 +193,12 @@ AVAILABLE_CLI_OPTIONS = {
type=float, type=float,
metavar='FLOAT', metavar='FLOAT',
), ),
"backtest_breakdown": Arg(
'--breakdown',
help='Show backtesting breakdown per [day, week, month].',
nargs='+',
choices=constants.BACKTEST_BREAKDOWNS
),
# Edge # Edge
"stoploss_range": Arg( "stoploss_range": Arg(
'--stoplosses', '--stoplosses',
@ -355,6 +361,11 @@ AVAILABLE_CLI_OPTIONS = {
type=check_int_positive, type=check_int_positive,
metavar='INT', metavar='INT',
), ),
"include_inactive": Arg(
'--include-inactive-pairs',
help='Also download data from inactive pairs.',
action='store_true',
),
"new_pairs_days": Arg( "new_pairs_days": Arg(
'--new-pairs-days', '--new-pairs-days',
help='Download data of new pairs for given number of days. Default: `%(default)s`.', help='Download data of new pairs for given number of days. Default: `%(default)s`.',
@ -381,12 +392,12 @@ AVAILABLE_CLI_OPTIONS = {
), ),
"dataformat_ohlcv": Arg( "dataformat_ohlcv": Arg(
'--data-format-ohlcv', '--data-format-ohlcv',
help='Storage format for downloaded candle (OHLCV) data. (default: `%(default)s`).', help='Storage format for downloaded candle (OHLCV) data. (default: `json`).',
choices=constants.AVAILABLE_DATAHANDLERS, choices=constants.AVAILABLE_DATAHANDLERS,
), ),
"dataformat_trades": Arg( "dataformat_trades": Arg(
'--data-format-trades', '--data-format-trades',
help='Storage format for downloaded trades data. (default: `%(default)s`).', help='Storage format for downloaded trades data. (default: `jsongz`).',
choices=constants.AVAILABLE_DATAHANDLERS, choices=constants.AVAILABLE_DATAHANDLERS,
), ),
"exchange": Arg( "exchange": Arg(
@ -414,6 +425,12 @@ AVAILABLE_CLI_OPTIONS = {
action='store_true', action='store_true',
default=False, default=False,
), ),
"ui_version": Arg(
'--ui-version',
help=('Specify a specific version of FreqUI to install. '
'Not specifying this installs the latest version.'),
type=str,
),
# Templating options # Templating options
"template": Arg( "template": Arg(
'--template', '--template',
@ -552,4 +569,10 @@ AVAILABLE_CLI_OPTIONS = {
help='Do not print epoch details header.', help='Do not print epoch details header.',
action='store_true', action='store_true',
), ),
"hyperopt_ignore_missing_space": Arg(
"--ignore-missing-spaces", "--ignore-unparameterized-spaces",
help=("Suppress errors for any requested Hyperopt spaces "
"that do not contain any parameters."),
action="store_true",
),
} }

View File

@ -11,6 +11,7 @@ from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_oh
from freqtrade.enums import RunMode from freqtrade.enums import RunMode
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange import timeframe_to_minutes
from freqtrade.exchange.exchange import market_is_active
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.resolvers import ExchangeResolver from freqtrade.resolvers import ExchangeResolver
@ -47,11 +48,13 @@ def start_download_data(args: Dict[str, Any]) -> None:
# Init exchange # Init exchange
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False) exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
markets = [p for p, m in exchange.markets.items() if market_is_active(m)
or config.get('include_inactive')]
expanded_pairs = expand_pairlist(config['pairs'], markets)
# Manual validations of relevant settings # Manual validations of relevant settings
if not config['exchange'].get('skip_pair_validation', False): if not config['exchange'].get('skip_pair_validation', False):
exchange.validate_pairs(config['pairs']) exchange.validate_pairs(expanded_pairs)
expanded_pairs = expand_pairlist(config['pairs'], list(exchange.markets))
logger.info(f"About to download pairs: {expanded_pairs}, " logger.info(f"About to download pairs: {expanded_pairs}, "
f"intervals: {config['timeframes']} to {config['datadir']}") f"intervals: {config['timeframes']} to {config['datadir']}")
@ -89,6 +92,41 @@ def start_download_data(args: Dict[str, Any]) -> None:
f"on exchange {exchange.name}.") f"on exchange {exchange.name}.")
def start_convert_trades(args: Dict[str, Any]) -> None:
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
timerange = TimeRange()
# Remove stake-currency to skip checks which are not relevant for datadownload
config['stake_currency'] = ''
if 'pairs' not in config:
raise OperationalException(
"Downloading data requires a list of pairs. "
"Please check the documentation on how to configure this.")
# Init exchange
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
# Manual validations of relevant settings
if not config['exchange'].get('skip_pair_validation', False):
exchange.validate_pairs(config['pairs'])
expanded_pairs = expand_pairlist(config['pairs'], list(exchange.markets))
logger.info(f"About to Convert pairs: {expanded_pairs}, "
f"intervals: {config['timeframes']} to {config['datadir']}")
for timeframe in config['timeframes']:
exchange.validate_timeframes(timeframe)
# Convert downloaded trade data to different timeframes
convert_trades_to_ohlcv(
pairs=expanded_pairs, timeframes=config['timeframes'],
datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')),
data_format_ohlcv=config['dataformat_ohlcv'],
data_format_trades=config['dataformat_trades'],
)
def start_convert_data(args: Dict[str, Any], ohlcv: bool = True) -> None: def start_convert_data(args: Dict[str, Any], ohlcv: bool = True) -> None:
""" """
Convert data from one format to another Convert data from one format to another

View File

@ -128,7 +128,7 @@ def download_and_install_ui(dest_folder: Path, dl_url: str, version: str):
f.write(version) f.write(version)
def get_ui_download_url() -> Tuple[str, str]: def get_ui_download_url(version: Optional[str] = None) -> Tuple[str, str]:
base_url = 'https://api.github.com/repos/freqtrade/frequi/' base_url = 'https://api.github.com/repos/freqtrade/frequi/'
# Get base UI Repo path # Get base UI Repo path
@ -136,8 +136,16 @@ def get_ui_download_url() -> Tuple[str, str]:
resp.raise_for_status() resp.raise_for_status()
r = resp.json() r = resp.json()
latest_version = r[0]['name'] if version:
assets = r[0].get('assets', []) tmp = [x for x in r if x['name'] == version]
if tmp:
latest_version = tmp[0]['name']
assets = tmp[0].get('assets', [])
else:
raise ValueError("UI-Version not found.")
else:
latest_version = r[0]['name']
assets = r[0].get('assets', [])
dl_url = '' dl_url = ''
if assets and len(assets) > 0: if assets and len(assets) > 0:
dl_url = assets[0]['browser_download_url'] dl_url = assets[0]['browser_download_url']
@ -156,7 +164,7 @@ def start_install_ui(args: Dict[str, Any]) -> None:
dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui/installed/' dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui/installed/'
# First make sure the assets are removed. # First make sure the assets are removed.
dl_url, latest_version = get_ui_download_url() dl_url, latest_version = get_ui_download_url(args.get('ui_version'))
curr_version = read_ui_version(dest_folder) curr_version = read_ui_version(dest_folder)
if curr_version == latest_version and not args.get('erase_ui_only'): if curr_version == latest_version and not args.get('erase_ui_only'):

View File

@ -96,7 +96,7 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
if 'strategy_name' in metrics: if 'strategy_name' in metrics:
strategy_name = metrics['strategy_name'] strategy_name = metrics['strategy_name']
show_backtest_result(strategy_name, metrics, show_backtest_result(strategy_name, metrics,
metrics['stake_currency']) metrics['stake_currency'], config.get('backtest_breakdown', []))
HyperoptTools.try_export_params(config, strategy_name, val) HyperoptTools.try_export_params(config, strategy_name, val)

View File

@ -269,8 +269,12 @@ class Configuration:
self._args_to_config(config, argname='export', self._args_to_config(config, argname='export',
logstring='Parameter --export detected: {} ...') logstring='Parameter --export detected: {} ...')
self._args_to_config(config, argname='backtest_breakdown',
logstring='Parameter --breakdown detected ...')
self._args_to_config(config, argname='disableparamexport', self._args_to_config(config, argname='disableparamexport',
logstring='Parameter --disableparamexport detected: {} ...') logstring='Parameter --disableparamexport detected: {} ...')
# Edge section: # Edge section:
if 'stoploss_range' in self.args and self.args["stoploss_range"]: if 'stoploss_range' in self.args and self.args["stoploss_range"]:
txt_range = eval(self.args["stoploss_range"]) txt_range = eval(self.args["stoploss_range"])
@ -369,6 +373,9 @@ class Configuration:
self._args_to_config(config, argname='hyperopt_show_no_header', self._args_to_config(config, argname='hyperopt_show_no_header',
logstring='Parameter --no-header detected: {}') logstring='Parameter --no-header detected: {}')
self._args_to_config(config, argname="hyperopt_ignore_missing_space",
logstring="Paramter --ignore-missing-space detected: {}")
def _process_plot_options(self, config: Dict[str, Any]) -> None: def _process_plot_options(self, config: Dict[str, Any]) -> None:
self._args_to_config(config, argname='pairs', self._args_to_config(config, argname='pairs',
@ -404,6 +411,9 @@ class Configuration:
self._args_to_config(config, argname='days', self._args_to_config(config, argname='days',
logstring='Detected --days: {}') logstring='Detected --days: {}')
self._args_to_config(config, argname='include_inactive',
logstring='Detected --include-inactive-pairs: {}')
self._args_to_config(config, argname='download_trades', self._args_to_config(config, argname='download_trades',
logstring='Detected --dl-trades: {}') logstring='Detected --dl-trades: {}')

View File

@ -24,13 +24,15 @@ ORDERTYPE_POSSIBILITIES = ['limit', 'market']
ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss',
'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily', 'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily',
'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily'] 'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily',
'MaxDrawDownHyperOptLoss']
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList',
'AgeFilter', 'OffsetFilter', 'PerformanceFilter', 'AgeFilter', 'OffsetFilter', 'PerformanceFilter',
'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter',
'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter'] 'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter']
AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard'] AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard']
AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5']
BACKTEST_BREAKDOWNS = ['day', 'week', 'month']
DRY_RUN_WALLET = 1000 DRY_RUN_WALLET = 1000
DATETIME_PRINT_FORMAT = '%Y-%m-%d %H:%M:%S' DATETIME_PRINT_FORMAT = '%Y-%m-%d %H:%M:%S'
MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons
@ -145,6 +147,10 @@ CONF_SCHEMA = {
'sell_profit_offset': {'type': 'number'}, 'sell_profit_offset': {'type': 'number'},
'ignore_roi_if_buy_signal': {'type': 'boolean'}, 'ignore_roi_if_buy_signal': {'type': 'boolean'},
'ignore_buying_expired_candle_after': {'type': 'number'}, 'ignore_buying_expired_candle_after': {'type': 'number'},
'backtest_breakdown': {
'type': 'array',
'items': {'type': 'string', 'enum': BACKTEST_BREAKDOWNS}
},
'bot_name': {'type': 'string'}, 'bot_name': {'type': 'string'},
'unfilledtimeout': { 'unfilledtimeout': {
'type': 'object', 'type': 'object',

View File

@ -16,8 +16,6 @@ API_FETCH_ORDER_RETRY_COUNT = 5
BAD_EXCHANGES = { BAD_EXCHANGES = {
"bitmex": "Various reasons.", "bitmex": "Various reasons.",
"bitstamp": "Does not provide history. "
"Details in https://github.com/freqtrade/freqtrade/issues/1983",
"phemex": "Does not provide history. ", "phemex": "Does not provide history. ",
"poloniex": "Does not provide fetch_order endpoint to fetch both open and closed orders.", "poloniex": "Does not provide fetch_order endpoint to fetch both open and closed orders.",
} }

View File

@ -480,7 +480,7 @@ class Exchange:
if startup_candles + 5 > candle_limit: if startup_candles + 5 > candle_limit:
raise OperationalException( raise OperationalException(
f"This strategy requires {startup_candles} candles to start. " f"This strategy requires {startup_candles} candles to start. "
f"{self.name} only provides {candle_limit} for {timeframe}.") f"{self.name} only provides {candle_limit - 5} for {timeframe}.")
def exchange_has(self, endpoint: str) -> bool: def exchange_has(self, endpoint: str) -> bool:
""" """
@ -523,7 +523,7 @@ class Exchange:
precision = self.markets[pair]['precision']['price'] precision = self.markets[pair]['precision']['price']
missing = price % precision missing = price % precision
if missing != 0: if missing != 0:
price = price - missing + precision price = round(price - missing + precision, 10)
else: else:
symbol_prec = self.markets[pair]['precision']['price'] symbol_prec = self.markets[pair]['precision']['price']
big_price = price * pow(10, symbol_prec) big_price = price * pow(10, symbol_prec)
@ -1058,7 +1058,7 @@ class Exchange:
ticker_rate = ticker[conf_strategy['price_side']] ticker_rate = ticker[conf_strategy['price_side']]
if ticker['last'] and ticker_rate: if ticker['last'] and ticker_rate:
if side == 'buy' and ticker_rate > ticker['last']: if side == 'buy' and ticker_rate > ticker['last']:
balance = conf_strategy['ask_last_balance'] balance = conf_strategy.get('ask_last_balance', 0.0)
ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate) ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate)
elif side == 'sell' and ticker_rate < ticker['last']: elif side == 'sell' and ticker_rate < ticker['last']:
balance = conf_strategy.get('bid_last_balance', 0.0) balance = conf_strategy.get('bid_last_balance', 0.0)

View File

@ -2,6 +2,7 @@
import logging import logging
from typing import Dict from typing import Dict
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
@ -23,3 +24,10 @@ class Gateio(Exchange):
} }
_headers = {'X-Gate-Channel-Id': 'freqtrade'} _headers = {'X-Gate-Channel-Id': 'freqtrade'}
def validate_ordertypes(self, order_types: Dict) -> None:
super().validate_ordertypes(order_types)
if any(v == 'market' for k, v in order_types.items()):
raise OperationalException(
f'Exchange {self.name} does not support market orders.')

View File

@ -139,7 +139,7 @@ class FreqtradeBot(LoggingMixin):
# Only update open orders on startup # Only update open orders on startup
# This will update the database after the initial migration # This will update the database after the initial migration
self.update_open_orders() self.startup_update_open_orders()
def process(self) -> None: def process(self) -> None:
""" """
@ -237,7 +237,7 @@ class FreqtradeBot(LoggingMixin):
open_trades = len(Trade.get_open_trades()) open_trades = len(Trade.get_open_trades())
return max(0, self.config['max_open_trades'] - open_trades) return max(0, self.config['max_open_trades'] - open_trades)
def update_open_orders(self): def startup_update_open_orders(self):
""" """
Updates open orders based on order list kept in the database. Updates open orders based on order list kept in the database.
Mainly updates the state of orders - but may also close trades Mainly updates the state of orders - but may also close trades

View File

@ -45,7 +45,7 @@ progressbar.streams.wrap_stdout()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
INITIAL_POINTS = 5 INITIAL_POINTS = 30
# Keep no more than SKOPT_MODEL_QUEUE_SIZE models # Keep no more than SKOPT_MODEL_QUEUE_SIZE models
# in the skopt model queue, to optimize memory consumption # in the skopt model queue, to optimize memory consumption
@ -258,6 +258,7 @@ class Hyperopt:
if HyperoptTools.has_space(self.config, 'trailing'): if HyperoptTools.has_space(self.config, 'trailing'):
logger.debug("Hyperopt has 'trailing' space") logger.debug("Hyperopt has 'trailing' space")
self.trailing_space = self.custom_hyperopt.trailing_space() self.trailing_space = self.custom_hyperopt.trailing_space()
self.dimensions = (self.buy_space + self.sell_space + self.protection_space self.dimensions = (self.buy_space + self.sell_space + self.protection_space
+ self.roi_space + self.stoploss_space + self.trailing_space) + self.roi_space + self.stoploss_space + self.trailing_space)

View File

@ -3,6 +3,7 @@ HyperOptAuto class.
This module implements a convenience auto-hyperopt class, which can be used together with strategies This module implements a convenience auto-hyperopt class, which can be used together with strategies
that implement IHyperStrategy interface. that implement IHyperStrategy interface.
""" """
import logging
from contextlib import suppress from contextlib import suppress
from typing import Callable, Dict, List from typing import Callable, Dict, List
@ -15,12 +16,19 @@ with suppress(ImportError):
from freqtrade.optimize.hyperopt_interface import EstimatorType, IHyperOpt from freqtrade.optimize.hyperopt_interface import EstimatorType, IHyperOpt
def _format_exception_message(space: str) -> str: logger = logging.getLogger(__name__)
raise OperationalException(
f"The '{space}' space is included into the hyperoptimization "
f"but no parameter for this space was not found in your Strategy. " def _format_exception_message(space: str, ignore_missing_space: bool) -> None:
f"Please make sure to have parameters for this space enabled for optimization " msg = (f"The '{space}' space is included into the hyperoptimization "
f"or remove the '{space}' space from hyperoptimization.") f"but no parameter for this space was not found in your Strategy. "
)
if ignore_missing_space:
logger.warning(msg + "This space will be ignored.")
else:
raise OperationalException(
msg + f"Please make sure to have parameters for this space enabled for optimization "
f"or remove the '{space}' space from hyperoptimization.")
class HyperOptAuto(IHyperOpt): class HyperOptAuto(IHyperOpt):
@ -48,13 +56,16 @@ class HyperOptAuto(IHyperOpt):
if attr.optimize: if attr.optimize:
yield attr.get_space(attr_name) yield attr.get_space(attr_name)
def _get_indicator_space(self, category): def _get_indicator_space(self, category) -> List:
# TODO: is this necessary, or can we call "generate_space" directly? # TODO: is this necessary, or can we call "generate_space" directly?
indicator_space = list(self._generate_indicator_space(category)) indicator_space = list(self._generate_indicator_space(category))
if len(indicator_space) > 0: if len(indicator_space) > 0:
return indicator_space return indicator_space
else: else:
_format_exception_message(category) _format_exception_message(
category,
self.config.get("hyperopt_ignore_missing_space", False))
return []
def buy_indicator_space(self) -> List['Dimension']: def buy_indicator_space(self) -> List['Dimension']:
return self._get_indicator_space('buy') return self._get_indicator_space('buy')

View File

@ -0,0 +1,41 @@
"""
MaxDrawDownHyperOptLoss
This module defines the alternative HyperOptLoss class which can be used for
Hyperoptimization.
"""
from datetime import datetime
from pandas import DataFrame
from freqtrade.data.btanalysis import calculate_max_drawdown
from freqtrade.optimize.hyperopt import IHyperOptLoss
class MaxDrawDownHyperOptLoss(IHyperOptLoss):
"""
Defines the loss function for hyperopt.
This implementation optimizes for max draw down and profit
Less max drawdown more profit -> Lower return value
"""
@staticmethod
def hyperopt_loss_function(results: DataFrame, trade_count: int,
min_date: datetime, max_date: datetime,
*args, **kwargs) -> float:
"""
Objective function.
Uses profit ratio weighted max_drawdown when drawdown is available.
Otherwise directly optimizes profit ratio.
"""
total_profit = results['profit_abs'].sum()
try:
max_drawdown = calculate_max_drawdown(results, value_col='profit_abs')
except ValueError:
# No losing trade, therefore no drawdown.
return -total_profit
return -total_profit / max_drawdown[0]

View File

@ -1,4 +1,3 @@
import io import io
import logging import logging
from copy import deepcopy from copy import deepcopy
@ -64,10 +63,11 @@ class HyperoptTools():
'export_time': datetime.now(timezone.utc), 'export_time': datetime.now(timezone.utc),
} }
logger.info(f"Dumping parameters to {filename}") logger.info(f"Dumping parameters to {filename}")
rapidjson.dump(final_params, filename.open('w'), indent=2, with filename.open('w') as f:
default=hyperopt_serializer, rapidjson.dump(final_params, f, indent=2,
number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN default=hyperopt_serializer,
) number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN
)
@staticmethod @staticmethod
def try_export_params(config: Dict[str, Any], strategy_name: str, params: Dict): def try_export_params(config: Dict[str, Any], strategy_name: str, params: Dict):

View File

@ -4,7 +4,7 @@ from pathlib import Path
from typing import Any, Dict, List, Union from typing import Any, Dict, List, Union
from numpy import int64 from numpy import int64
from pandas import DataFrame from pandas import DataFrame, to_datetime
from tabulate import tabulate from tabulate import tabulate
from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT
@ -189,7 +189,6 @@ def generate_strategy_comparison(all_results: Dict) -> List[Dict]:
def generate_edge_table(results: dict) -> str: def generate_edge_table(results: dict) -> str:
floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', 'd', 'd') floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', 'd', 'd')
tabular_data = [] tabular_data = []
headers = ['Pair', 'Stoploss', 'Win Rate', 'Risk Reward Ratio', headers = ['Pair', 'Stoploss', 'Win Rate', 'Risk Reward Ratio',
@ -214,6 +213,41 @@ def generate_edge_table(results: dict) -> str:
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore
def _get_resample_from_period(period: str) -> str:
if period == 'day':
return '1d'
if period == 'week':
return '1w'
if period == 'month':
return '1M'
raise ValueError(f"Period {period} is not supported.")
def generate_periodic_breakdown_stats(trade_list: List, period: str) -> List[Dict[str, Any]]:
results = DataFrame.from_records(trade_list)
if len(results) == 0:
return []
results['close_date'] = to_datetime(results['close_date'], utc=True)
resample_period = _get_resample_from_period(period)
resampled = results.resample(resample_period, on='close_date')
stats = []
for name, day in resampled:
profit_abs = day['profit_abs'].sum().round(10)
wins = sum(day['profit_abs'] > 0)
draws = sum(day['profit_abs'] == 0)
loses = sum(day['profit_abs'] < 0)
stats.append(
{
'date': name.strftime('%d/%m/%Y'),
'profit_abs': profit_abs,
'wins': wins,
'draws': draws,
'loses': loses
}
)
return stats
def generate_trading_stats(results: DataFrame) -> Dict[str, Any]: def generate_trading_stats(results: DataFrame) -> Dict[str, Any]:
""" Generate overall trade statistics """ """ Generate overall trade statistics """
if len(results) == 0: if len(results) == 0:
@ -329,7 +363,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
results['open_timestamp'] = results['open_date'].view(int64) // 1e6 results['open_timestamp'] = results['open_date'].view(int64) // 1e6
results['close_timestamp'] = results['close_date'].view(int64) // 1e6 results['close_timestamp'] = results['close_date'].view(int64) // 1e6
backtest_days = (max_date - min_date).days backtest_days = (max_date - min_date).days or 1
strat_stats = { strat_stats = {
'trades': results.to_dict(orient='records'), 'trades': results.to_dict(orient='records'),
'locks': [lock.to_json() for lock in content['locks']], 'locks': [lock.to_json() for lock in content['locks']],
@ -338,6 +372,8 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
'results_per_pair': pair_results, 'results_per_pair': pair_results,
'sell_reason_summary': sell_reason_stats, 'sell_reason_summary': sell_reason_stats,
'left_open_trades': left_open_results, 'left_open_trades': left_open_results,
# 'days_breakdown_stats': days_breakdown_stats,
'total_trades': len(results), 'total_trades': len(results),
'total_volume': float(results['stake_amount'].sum()), 'total_volume': float(results['stake_amount'].sum()),
'avg_stake_amount': results['stake_amount'].mean() if len(results) > 0 else 0, 'avg_stake_amount': results['stake_amount'].mean() if len(results) > 0 else 0,
@ -354,7 +390,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
'backtest_run_start_ts': content['backtest_start_time'], 'backtest_run_start_ts': content['backtest_start_time'],
'backtest_run_end_ts': content['backtest_end_time'], 'backtest_run_end_ts': content['backtest_end_time'],
'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else 0, 'trades_per_day': round(len(results) / backtest_days, 2),
'market_change': market_change, 'market_change': market_change,
'pairlist': list(btdata.keys()), 'pairlist': list(btdata.keys()),
'stake_amount': config['stake_amount'], 'stake_amount': config['stake_amount'],
@ -506,6 +542,28 @@ def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_curren
return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right") return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right")
def text_table_periodic_breakdown(days_breakdown_stats: List[Dict[str, Any]],
stake_currency: str, period: str) -> str:
"""
Generate small table with Backtest results by days
:param days_breakdown_stats: Days breakdown metrics
:param stake_currency: Stakecurrency used
:return: pretty printed table with tabulate as string
"""
headers = [
period.capitalize(),
f'Tot Profit {stake_currency}',
'Wins',
'Draws',
'Losses',
]
output = [[
d['date'], round_coin_value(d['profit_abs'], stake_currency, False),
d['wins'], d['draws'], d['loses'],
] for d in days_breakdown_stats]
return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right")
def text_table_strategy(strategy_results, stake_currency: str) -> str: def text_table_strategy(strategy_results, stake_currency: str) -> str:
""" """
Generate summary table per strategy Generate summary table per strategy
@ -557,7 +615,10 @@ def text_table_add_metrics(strat_results: Dict) -> str:
strat_results['stake_currency'])), strat_results['stake_currency'])),
('Absolute profit ', round_coin_value(strat_results['profit_total_abs'], ('Absolute profit ', round_coin_value(strat_results['profit_total_abs'],
strat_results['stake_currency'])), strat_results['stake_currency'])),
('Total profit %', f"{round(strat_results['profit_total'] * 100, 2):}%"), ('Total profit %', f"{round(strat_results['profit_total'] * 100, 2)}%"),
('Trades per day', strat_results['trades_per_day']),
('Avg. daily profit %',
f"{round(strat_results['profit_total'] / strat_results['backtest_days'] * 100, 2)}%"),
('Avg. stake amount', round_coin_value(strat_results['avg_stake_amount'], ('Avg. stake amount', round_coin_value(strat_results['avg_stake_amount'],
strat_results['stake_currency'])), strat_results['stake_currency'])),
('Total trade volume', round_coin_value(strat_results['total_volume'], ('Total trade volume', round_coin_value(strat_results['total_volume'],
@ -614,7 +675,8 @@ def text_table_add_metrics(strat_results: Dict) -> str:
return message return message
def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: str): def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: str,
backtest_breakdown=[]):
""" """
Print results for one strategy Print results for one strategy
""" """
@ -636,6 +698,15 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency:
print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '=')) print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '='))
print(table) print(table)
for period in backtest_breakdown:
days_breakdown_stats = generate_periodic_breakdown_stats(
trade_list=results['trades'], period=period)
table = text_table_periodic_breakdown(days_breakdown_stats=days_breakdown_stats,
stake_currency=stake_currency, period=period)
if isinstance(table, str) and len(table) > 0:
print(f' {period.upper()} BREAKDOWN '.center(len(table.splitlines()[0]), '='))
print(table)
table = text_table_add_metrics(results) table = text_table_add_metrics(results)
if isinstance(table, str) and len(table) > 0: if isinstance(table, str) and len(table) > 0:
print(' SUMMARY METRICS '.center(len(table.splitlines()[0]), '=')) print(' SUMMARY METRICS '.center(len(table.splitlines()[0]), '='))
@ -650,7 +721,9 @@ def show_backtest_results(config: Dict, backtest_stats: Dict):
stake_currency = config['stake_currency'] stake_currency = config['stake_currency']
for strategy, results in backtest_stats['strategy'].items(): for strategy, results in backtest_stats['strategy'].items():
show_backtest_result(strategy, results, stake_currency) show_backtest_result(
strategy, results, stake_currency,
config.get('backtest_breakdown', []))
if len(backtest_stats['strategy']) > 1: if len(backtest_stats['strategy']) > 1:
# Print Strategy summary table # Print Strategy summary table

View File

@ -7,11 +7,15 @@ class SKDecimal(Integer):
def __init__(self, low, high, decimals=3, prior="uniform", base=10, transform=None, def __init__(self, low, high, decimals=3, prior="uniform", base=10, transform=None,
name=None, dtype=np.int64): name=None, dtype=np.int64):
self.decimals = decimals self.decimals = decimals
_low = int(low * pow(10, self.decimals))
_high = int(high * pow(10, self.decimals)) self.pow_dot_one = pow(0.1, self.decimals)
self.pow_ten = pow(10, self.decimals)
_low = int(low * self.pow_ten)
_high = int(high * self.pow_ten)
# trunc to precision to avoid points out of space # trunc to precision to avoid points out of space
self.low_orig = round(_low * pow(0.1, self.decimals), self.decimals) self.low_orig = round(_low * self.pow_dot_one, self.decimals)
self.high_orig = round(_high * pow(0.1, self.decimals), self.decimals) self.high_orig = round(_high * self.pow_dot_one, self.decimals)
super().__init__(_low, _high, prior, base, transform, name, dtype) super().__init__(_low, _high, prior, base, transform, name, dtype)
@ -25,9 +29,9 @@ class SKDecimal(Integer):
return self.low_orig <= point <= self.high_orig return self.low_orig <= point <= self.high_orig
def transform(self, Xt): def transform(self, Xt):
aa = [int(x * pow(10, self.decimals)) for x in Xt] return super().transform([int(v * self.pow_ten) for v in Xt])
return super().transform(aa)
def inverse_transform(self, Xt): def inverse_transform(self, Xt):
res = super().inverse_transform(Xt) res = super().inverse_transform(Xt)
return [round(x * pow(0.1, self.decimals), self.decimals) for x in res] # equivalent to [round(x * pow(0.1, self.decimals), self.decimals) for x in res]
return [int(v) / self.pow_ten for v in res]

View File

@ -21,6 +21,7 @@ class PerformanceFilter(IPairList):
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
self._minutes = pairlistconfig.get('minutes', 0) self._minutes = pairlistconfig.get('minutes', 0)
self._min_profit = pairlistconfig.get('min_profit', None)
@property @property
def needstickers(self) -> bool: def needstickers(self) -> bool:
@ -68,6 +69,14 @@ class PerformanceFilter(IPairList):
sorted_df = list_df.merge(performance, on='pair', how='left')\ sorted_df = list_df.merge(performance, on='pair', how='left')\
.fillna(0).sort_values(by=['count', 'pair'], ascending=True)\ .fillna(0).sort_values(by=['count', 'pair'], ascending=True)\
.sort_values(by=['profit'], ascending=False) .sort_values(by=['profit'], ascending=False)
if self._min_profit is not None:
removed = sorted_df[sorted_df['profit'] < self._min_profit]
for _, row in removed.iterrows():
self.log_once(
f"Removing pair {row['pair']} since {row['profit']} is "
f"below {self._min_profit}", logger.info)
sorted_df = sorted_df[sorted_df['profit'] >= self._min_profit]
pairlist = sorted_df['pair'].tolist() pairlist = sorted_df['pair'].tolist()
return pairlist return pairlist

View File

@ -4,9 +4,9 @@ Static Pair List provider
Provides pair white list as it configured in config Provides pair white list as it configured in config
""" """
import logging import logging
from copy import deepcopy
from typing import Any, Dict, List from typing import Any, Dict, List
from freqtrade.exceptions import OperationalException
from freqtrade.plugins.pairlist.IPairList import IPairList from freqtrade.plugins.pairlist.IPairList import IPairList
@ -20,10 +20,6 @@ class StaticPairList(IPairList):
pairlist_pos: int) -> None: pairlist_pos: int) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
if self._pairlist_pos != 0:
raise OperationalException(f"{self.name} can only be used in the first position "
"in the list of Pairlist Handlers.")
self._allow_inactive = self._pairlistconfig.get('allow_inactive', False) self._allow_inactive = self._pairlistconfig.get('allow_inactive', False)
@property @property
@ -64,4 +60,8 @@ class StaticPairList(IPairList):
:param tickers: Tickers (from exchange.get_tickers()). May be cached. :param tickers: Tickers (from exchange.get_tickers()). May be cached.
:return: new whitelist :return: new whitelist
""" """
return pairlist pairlist_ = deepcopy(pairlist)
for pair in self._config['exchange']['pair_whitelist']:
if pair not in pairlist_:
pairlist_.append(pair)
return pairlist_

View File

@ -91,7 +91,7 @@ class IResolver:
logger.debug(f"Searching for {cls.object_type.__name__} {object_name} in '{directory}'") logger.debug(f"Searching for {cls.object_type.__name__} {object_name} in '{directory}'")
for entry in directory.iterdir(): for entry in directory.iterdir():
# Only consider python files # Only consider python files
if not str(entry).endswith('.py'): if entry.suffix != '.py':
logger.debug('Ignoring %s', entry) logger.debug('Ignoring %s', entry)
continue continue
if entry.is_symlink() and not entry.is_file(): if entry.is_symlink() and not entry.is_file():
@ -169,7 +169,7 @@ class IResolver:
objects = [] objects = []
for entry in directory.iterdir(): for entry in directory.iterdir():
# Only consider python files # Only consider python files
if not str(entry).endswith('.py'): if entry.suffix != '.py':
logger.debug('Ignoring %s', entry) logger.debug('Ignoring %s', entry)
continue continue
module_path = entry.resolve() module_path = entry.resolve()

View File

@ -56,17 +56,21 @@ class StrategyResolver(IResolver):
if strategy._ft_params_from_file: if strategy._ft_params_from_file:
# Set parameters from Hyperopt results file # Set parameters from Hyperopt results file
params = strategy._ft_params_from_file params = strategy._ft_params_from_file
strategy.minimal_roi = params.get('roi', strategy.minimal_roi) strategy.minimal_roi = params.get('roi', getattr(strategy, 'minimal_roi', {}))
strategy.stoploss = params.get('stoploss', {}).get('stoploss', strategy.stoploss) strategy.stoploss = params.get('stoploss', {}).get(
'stoploss', getattr(strategy, 'stoploss', -0.1))
trailing = params.get('trailing', {}) trailing = params.get('trailing', {})
strategy.trailing_stop = trailing.get('trailing_stop', strategy.trailing_stop) strategy.trailing_stop = trailing.get(
strategy.trailing_stop_positive = trailing.get('trailing_stop_positive', 'trailing_stop', getattr(strategy, 'trailing_stop', False))
strategy.trailing_stop_positive) strategy.trailing_stop_positive = trailing.get(
'trailing_stop_positive', getattr(strategy, 'trailing_stop_positive', None))
strategy.trailing_stop_positive_offset = trailing.get( strategy.trailing_stop_positive_offset = trailing.get(
'trailing_stop_positive_offset', strategy.trailing_stop_positive_offset) 'trailing_stop_positive_offset',
getattr(strategy, 'trailing_stop_positive_offset', 0))
strategy.trailing_only_offset_is_reached = trailing.get( strategy.trailing_only_offset_is_reached = trailing.get(
'trailing_only_offset_is_reached', strategy.trailing_only_offset_is_reached) 'trailing_only_offset_is_reached',
getattr(strategy, 'trailing_only_offset_is_reached', 0.0))
# Set attributes # Set attributes
# Check if we need to override configuration # Check if we need to override configuration

View File

@ -347,3 +347,8 @@ class BacktestResponse(BaseModel):
trade_count: Optional[float] trade_count: Optional[float]
# TODO: Properly type backtestresult... # TODO: Properly type backtestresult...
backtest_result: Optional[Dict[str, Any]] backtest_result: Optional[Dict[str, Any]]
class SysInfo(BaseModel):
cpu_pct: List[float]
ram_pct: float

View File

@ -18,7 +18,8 @@ from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, Blac
OpenTradeSchema, PairHistory, PerformanceEntry, OpenTradeSchema, PairHistory, PerformanceEntry,
Ping, PlotConfig, Profit, ResultMsg, ShowConfig, Ping, PlotConfig, Profit, ResultMsg, ShowConfig,
Stats, StatusMsg, StrategyListResponse, Stats, StatusMsg, StrategyListResponse,
StrategyResponse, Version, WhitelistResponse) StrategyResponse, SysInfo, Version,
WhitelistResponse)
from freqtrade.rpc.api_server.deps import get_config, get_rpc, get_rpc_optional from freqtrade.rpc.api_server.deps import get_config, get_rpc, get_rpc_optional
from freqtrade.rpc.rpc import RPCException from freqtrade.rpc.rpc import RPCException
@ -259,3 +260,8 @@ def list_available_pairs(timeframe: Optional[str] = None, stake_currency: Option
'pair_interval': pair_interval, 'pair_interval': pair_interval,
} }
return result return result
@router.get('/sysinfo', response_model=SysInfo, tags=['info'])
def sysinfo():
return RPC._rpc_sysinfo()

View File

@ -1,5 +1,6 @@
from typing import Any, Dict, Optional from typing import Any, Dict, Iterator, Optional
from freqtrade.persistence import Trade
from freqtrade.rpc.rpc import RPC, RPCException from freqtrade.rpc.rpc import RPC, RPCException
from .webserver import ApiServer from .webserver import ApiServer
@ -11,10 +12,12 @@ def get_rpc_optional() -> Optional[RPC]:
return None return None
def get_rpc() -> Optional[RPC]: def get_rpc() -> Optional[Iterator[RPC]]:
_rpc = get_rpc_optional() _rpc = get_rpc_optional()
if _rpc: if _rpc:
return _rpc Trade.query.session.rollback()
yield _rpc
Trade.query.session.rollback()
else: else:
raise RPCException('Bot is not in the correct state') raise RPCException('Bot is not in the correct state')

View File

@ -8,6 +8,7 @@ from math import isnan
from typing import Any, Dict, List, Optional, Tuple, Union from typing import Any, Dict, List, Optional, Tuple, Union
import arrow import arrow
import psutil
from numpy import NAN, inf, int64, mean from numpy import NAN, inf, int64, mean
from pandas import DataFrame from pandas import DataFrame
@ -870,3 +871,10 @@ class RPC:
'subplots' not in self._freqtrade.strategy.plot_config): 'subplots' not in self._freqtrade.strategy.plot_config):
self._freqtrade.strategy.plot_config['subplots'] = {} self._freqtrade.strategy.plot_config['subplots'] = {}
return self._freqtrade.strategy.plot_config return self._freqtrade.strategy.plot_config
@staticmethod
def _rpc_sysinfo() -> Dict[str, Any]:
return {
"cpu_pct": psutil.cpu_percent(interval=1, percpu=True),
"ram_pct": psutil.virtual_memory().percent
}

View File

@ -25,6 +25,7 @@ from freqtrade.constants import DUST_PER_COIN
from freqtrade.enums import RPCMessageType from freqtrade.enums import RPCMessageType
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.misc import chunks, plural, round_coin_value from freqtrade.misc import chunks, plural, round_coin_value
from freqtrade.persistence import Trade
from freqtrade.rpc import RPC, RPCException, RPCHandler from freqtrade.rpc import RPC, RPCException, RPCHandler
@ -59,7 +60,8 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]:
update.message.chat_id update.message.chat_id
) )
return wrapper return wrapper
# Rollback session to avoid getting data stored in a transaction.
Trade.query.session.rollback()
logger.debug( logger.debug(
'Executing handler: %s for chat_id: %s', 'Executing handler: %s for chat_id: %s',
command_handler.__name__, command_handler.__name__,
@ -1031,7 +1033,8 @@ class Telegram(RPCHandler):
:return: None :return: None
""" """
forcebuy_text = ("*/forcebuy <pair> [<rate>]:* `Instantly buys the given pair. " forcebuy_text = ("*/forcebuy <pair> [<rate>]:* `Instantly buys the given pair. "
"Optionally takes a rate at which to buy.` \n") "Optionally takes a rate at which to buy "
"(only applies to limit orders).` \n")
message = ("*/start:* `Starts the trader`\n" message = ("*/start:* `Starts the trader`\n"
"*/stop:* `Stops the trader`\n" "*/stop:* `Stops the trader`\n"
"*/status <trade_id>|[table]:* `Lists all open trades`\n" "*/status <trade_id>|[table]:* `Lists all open trades`\n"

View File

@ -381,7 +381,8 @@ class HyperStrategyMixin(object):
if filename.is_file(): if filename.is_file():
logger.info(f"Loading parameters from file {filename}") logger.info(f"Loading parameters from file {filename}")
try: try:
params = json_load(filename.open('r')) with filename.open('r') as f:
params = json_load(f)
if params.get('strategy_name') != self.__class__.__name__: if params.get('strategy_name') != self.__class__.__name__:
raise OperationalException('Invalid parameter file provided.') raise OperationalException('Invalid parameter file provided.')
return params return params

View File

@ -65,9 +65,9 @@ class IStrategy(ABC, HyperStrategyMixin):
_populate_fun_len: int = 0 _populate_fun_len: int = 0
_buy_fun_len: int = 0 _buy_fun_len: int = 0
_sell_fun_len: int = 0 _sell_fun_len: int = 0
_ft_params_from_file: Dict = {} _ft_params_from_file: Dict
# associated minimal roi # associated minimal roi
minimal_roi: Dict minimal_roi: Dict = {}
# associated stoploss # associated stoploss
stoploss: float stoploss: float

View File

@ -2,11 +2,8 @@
"name": "{{ exchange_name | lower }}", "name": "{{ exchange_name | lower }}",
"key": "{{ exchange_key }}", "key": "{{ exchange_key }}",
"secret": "{{ exchange_secret }}", "secret": "{{ exchange_secret }}",
"ccxt_config": {"enableRateLimit": true}, "ccxt_config": {},
"ccxt_async_config": { "ccxt_async_config": {},
"enableRateLimit": true,
"rateLimit": 200
},
"pair_whitelist": [ "pair_whitelist": [
], ],
"pair_blacklist": [ "pair_blacklist": [

View File

@ -2,10 +2,8 @@
"name": "{{ exchange_name | lower }}", "name": "{{ exchange_name | lower }}",
"key": "{{ exchange_key }}", "key": "{{ exchange_key }}",
"secret": "{{ exchange_secret }}", "secret": "{{ exchange_secret }}",
"ccxt_config": {"enableRateLimit": true}, "ccxt_config": {},
"ccxt_async_config": { "ccxt_async_config": {},
"enableRateLimit": true
},
"pair_whitelist": [ "pair_whitelist": [
], ],

View File

@ -3,14 +3,8 @@
"key": "{{ exchange_key }}", "key": "{{ exchange_key }}",
"secret": "{{ exchange_secret }}", "secret": "{{ exchange_secret }}",
"password": "{{ exchange_key_password }}", "password": "{{ exchange_key_password }}",
"ccxt_config": { "ccxt_config": {},
"enableRateLimit": true "ccxt_async_config": {},
"rateLimit": 200
},
"ccxt_async_config": {
"enableRateLimit": true,
"rateLimit": 200
},
"pair_whitelist": [ "pair_whitelist": [
], ],
"pair_blacklist": [ "pair_blacklist": [

View File

@ -32,8 +32,7 @@ def custom_stake_amount(self, pair: str, current_time: 'datetime', current_rate:
use_custom_stoploss = True use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime',
current_rate: float, current_profit: float, dataframe: DataFrame, current_rate: float, current_profit: float, **kwargs) -> float:
**kwargs) -> float:
""" """
Custom stoploss logic, returning the new distance relative to current_rate (as ratio). Custom stoploss logic, returning the new distance relative to current_rate (as ratio).
e.g. returning -0.05 would create a stoploss 5% below current_rate. e.g. returning -0.05 would create a stoploss 5% below current_rate.
@ -44,14 +43,13 @@ def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime',
When not implemented by a strategy, returns the initial stoploss value When not implemented by a strategy, returns the initial stoploss value
Only called when use_custom_stoploss is set to True. Only called when use_custom_stoploss is set to True.
:param pair: Pair that's about to be sold. :param pair: Pair that's currently analyzed
:param trade: trade object. :param trade: trade object.
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
:param current_rate: Rate, calculated based on pricing settings in ask_strategy. :param current_rate: Rate, calculated based on pricing settings in ask_strategy.
:param current_profit: Current profit (as ratio), calculated based on current_rate. :param current_profit: Current profit (as ratio), calculated based on current_rate.
:param dataframe: Analyzed dataframe for this pair. Can contain future data in backtesting.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: New stoploss value, relative to the currentrate :return float: New stoploss value, relative to the current_rate
""" """
return self.stoploss return self.stoploss

View File

@ -339,11 +339,13 @@ def vwap(bars):
(input can be pandas series or numpy array) (input can be pandas series or numpy array)
bars are usually mid [ (h+l)/2 ] or typical [ (h+l+c)/3 ] bars are usually mid [ (h+l)/2 ] or typical [ (h+l+c)/3 ]
""" """
typical = ((bars['high'] + bars['low'] + bars['close']) / 3).values raise ValueError("using `qtpylib.vwap` facilitates lookahead bias. Please use "
volume = bars['volume'].values "`qtpylib.rolling_vwap` instead, which calculates vwap in a rolling manner.")
# typical = ((bars['high'] + bars['low'] + bars['close']) / 3).values
# volume = bars['volume'].values
return pd.Series(index=bars.index, # return pd.Series(index=bars.index,
data=np.cumsum(volume * typical) / np.cumsum(volume)) # data=np.cumsum(volume * typical) / np.cumsum(volume))
# --------------------------------------------- # ---------------------------------------------

View File

@ -54,8 +54,8 @@ theme:
primary: 'blue grey' primary: 'blue grey'
accent: 'tear' accent: 'tear'
toggle: toggle:
icon: material/toggle-switch-off-outline icon: material/toggle-switch
name: Switch to dark mode name: Switch to light mode
extra_css: extra_css:
- 'stylesheets/ft.extra.css' - 'stylesheets/ft.extra.css'
extra_javascript: extra_javascript:

View File

@ -4,13 +4,12 @@
-r requirements-hyperopt.txt -r requirements-hyperopt.txt
coveralls==3.2.0 coveralls==3.2.0
flake8==3.9.2 flake8==4.0.1
flake8-type-annotations==0.1.0 flake8-tidy-imports==4.5.0
flake8-tidy-imports==4.4.1
mypy==0.910 mypy==0.910
pytest==6.2.5 pytest==6.2.5
pytest-asyncio==0.15.1 pytest-asyncio==0.16.0
pytest-cov==2.12.1 pytest-cov==3.0.0
pytest-mock==3.6.1 pytest-mock==3.6.1
pytest-random-order==1.0.4 pytest-random-order==1.0.4
isort==5.9.3 isort==5.9.3
@ -21,7 +20,7 @@ time-machine==2.4.0
nbconvert==6.2.0 nbconvert==6.2.0
# mypy types # mypy types
types-cachetools==4.2.0 types-cachetools==4.2.4
types-filelock==0.1.5 types-filelock==3.2.1
types-requests==2.25.9 types-requests==2.25.11
types-tabulate==0.8.2 types-tabulate==0.8.3

View File

@ -3,9 +3,9 @@
# Required for hyperopt # Required for hyperopt
scipy==1.7.1 scipy==1.7.1
scikit-learn==0.24.2 scikit-learn==1.0
scikit-optimize==0.8.1 scikit-optimize==0.9.0
filelock==3.0.12 filelock==3.3.1
joblib==1.0.1 joblib==1.1.0
psutil==5.8.0 psutil==5.8.0
progressbar2==3.53.3 progressbar2==3.55.0

View File

@ -1,44 +1,44 @@
numpy==1.21.2 numpy==1.21.3
pandas==1.3.3 pandas==1.3.4
pandas-ta==0.3.14b pandas-ta==0.3.14b
ccxt==1.57.3 ccxt==1.59.2
# Pin cryptography for now due to rust build errors with piwheels # Pin cryptography for now due to rust build errors with piwheels
cryptography==3.4.8 cryptography==35.0.0
aiohttp==3.7.4.post0 aiohttp==3.7.4.post0
SQLAlchemy==1.4.25 SQLAlchemy==1.4.26
python-telegram-bot==13.7 python-telegram-bot==13.7
arrow==1.1.1 arrow==1.2.1
cachetools==4.2.2 cachetools==4.2.2
requests==2.26.0 requests==2.26.0
urllib3==1.26.7 urllib3==1.26.7
wrapt==1.12.1 jsonschema==4.1.2
jsonschema==3.2.0
TA-Lib==0.4.21 TA-Lib==0.4.21
technical==1.3.0 technical==1.3.0
tabulate==0.8.9 tabulate==0.8.9
pycoingecko==2.2.0 pycoingecko==2.2.0
jinja2==3.0.1 jinja2==3.0.2
tables==3.6.1 tables==3.6.1
blosc==1.10.4 blosc==1.10.6
# find first, C search in arrays # find first, C search in arrays
py_find_1st==1.1.5 py_find_1st==1.1.5
# Load ticker files 30% faster # Load ticker files 30% faster
python-rapidjson==1.4 python-rapidjson==1.5
# Notify systemd # Notify systemd
sdnotify==0.3.2 sdnotify==0.3.2
# API Server # API Server
fastapi==0.68.1 fastapi==0.70.0
uvicorn==0.15.0 uvicorn==0.15.0
pyjwt==2.1.0 pyjwt==2.3.0
aiofiles==0.7.0 aiofiles==0.7.0
psutil==5.8.0
# Support for colorized terminal output # Support for colorized terminal output
colorama==0.4.4 colorama==0.4.4
# Building config files interactively # Building config files interactively
questionary==1.10.0 questionary==1.10.0
prompt-toolkit==3.0.20 prompt-toolkit==3.0.21

View File

@ -312,7 +312,7 @@ class FtRestClient():
:param limit: Limit result to the last n candles. :param limit: Limit result to the last n candles.
:return: json object :return: json object
""" """
return self._get("available_pairs", params={ return self._get("pair_candles", params={
"pair": pair, "pair": pair,
"timeframe": timeframe, "timeframe": timeframe,
"limit": limit, "limit": limit,
@ -334,6 +334,13 @@ class FtRestClient():
"timerange": timerange if timerange else '', "timerange": timerange if timerange else '',
}) })
def sysinfo(self):
"""Provides system information (CPU, RAM usage)
:return: json object
"""
return self._get("sysinfo")
def add_arguments(): def add_arguments():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()

View File

@ -16,7 +16,6 @@ hyperopt = [
develop = [ develop = [
'coveralls', 'coveralls',
'flake8', 'flake8',
'flake8-type-annotations',
'flake8-tidy-imports', 'flake8-tidy-imports',
'mypy', 'mypy',
'pytest', 'pytest',
@ -51,7 +50,6 @@ setup(
'cachetools', 'cachetools',
'requests', 'requests',
'urllib3', 'urllib3',
'wrapt',
'jsonschema', 'jsonschema',
'TA-Lib', 'TA-Lib',
'pandas-ta', 'pandas-ta',

View File

@ -95,7 +95,15 @@ function install_talib() {
return return
fi fi
cd build_helpers && ./install_ta-lib.sh && cd .. cd build_helpers && ./install_ta-lib.sh
if [ $? -ne 0 ]; then
echo "Quitting. Please fix the above error before continuing."
cd ..
exit 1
fi;
cd ..
} }
function install_mac_newer_python_dependencies() { function install_mac_newer_python_dependencies() {

View File

@ -8,12 +8,12 @@ from zipfile import ZipFile
import arrow import arrow
import pytest import pytest
from freqtrade.commands import (start_convert_data, start_create_userdir, start_download_data, from freqtrade.commands import (start_convert_data, start_convert_trades, start_create_userdir,
start_hyperopt_list, start_hyperopt_show, start_install_ui, start_download_data, start_hyperopt_list, start_hyperopt_show,
start_list_data, start_list_exchanges, start_list_markets, start_install_ui, start_list_data, start_list_exchanges,
start_list_strategies, start_list_timeframes, start_new_strategy, start_list_markets, start_list_strategies, start_list_timeframes,
start_show_trades, start_test_pairlist, start_trading, start_new_strategy, start_show_trades, start_test_pairlist,
start_webserver) start_trading, start_webserver)
from freqtrade.commands.deploy_commands import (clean_ui_subdir, download_and_install_ui, from freqtrade.commands.deploy_commands import (clean_ui_subdir, download_and_install_ui,
get_ui_download_url, read_ui_version) get_ui_download_url, read_ui_version)
from freqtrade.configuration import setup_utils_configuration from freqtrade.configuration import setup_utils_configuration
@ -208,11 +208,10 @@ def test_list_timeframes(mocker, capsys):
assert re.search(r"^1d$", captured.out, re.MULTILINE) assert re.search(r"^1d$", captured.out, re.MULTILINE)
def test_list_markets(mocker, markets, capsys): def test_list_markets(mocker, markets_static, capsys):
api_mock = MagicMock() api_mock = MagicMock()
api_mock.markets = markets patch_exchange(mocker, api_mock=api_mock, id='bittrex', mock_markets=markets_static)
patch_exchange(mocker, api_mock=api_mock, id='bittrex')
# Test with no --config # Test with no --config
args = [ args = [
@ -237,7 +236,7 @@ def test_list_markets(mocker, markets, capsys):
"TKN/BTC, XLTCUSDT, XRP/BTC.\n" "TKN/BTC, XLTCUSDT, XRP/BTC.\n"
in captured.out) in captured.out)
patch_exchange(mocker, api_mock=api_mock, id="binance") patch_exchange(mocker, api_mock=api_mock, id="binance", mock_markets=markets_static)
# Test with --exchange # Test with --exchange
args = [ args = [
"list-markets", "list-markets",
@ -250,7 +249,7 @@ def test_list_markets(mocker, markets, capsys):
assert re.match("\nExchange Binance has 10 active markets:\n", assert re.match("\nExchange Binance has 10 active markets:\n",
captured.out) captured.out)
patch_exchange(mocker, api_mock=api_mock, id="bittrex") patch_exchange(mocker, api_mock=api_mock, id="bittrex", mock_markets=markets_static)
# Test with --all: all markets # Test with --all: all markets
args = [ args = [
"list-markets", "--all", "list-markets", "--all",
@ -606,16 +605,33 @@ def test_get_ui_download_url(mocker):
def test_get_ui_download_url_direct(mocker): def test_get_ui_download_url_direct(mocker):
response = MagicMock() response = MagicMock()
response.json = MagicMock( response.json = MagicMock(
side_effect=[[{ return_value=[
'assets_url': 'http://whatever.json', {
'name': '0.0.1', 'assets_url': 'http://whatever.json',
'assets': [{'browser_download_url': 'http://download11.zip'}]}]]) 'name': '0.0.2',
'assets': [{'browser_download_url': 'http://download22.zip'}]
},
{
'assets_url': 'http://whatever.json',
'name': '0.0.1',
'assets': [{'browser_download_url': 'http://download1.zip'}]
},
])
get_mock = mocker.patch("freqtrade.commands.deploy_commands.requests.get", get_mock = mocker.patch("freqtrade.commands.deploy_commands.requests.get",
return_value=response) return_value=response)
x, last_version = get_ui_download_url() x, last_version = get_ui_download_url()
assert get_mock.call_count == 1 assert get_mock.call_count == 1
assert last_version == '0.0.2'
assert x == 'http://download22.zip'
get_mock.reset_mock()
response.json.reset_mock()
x, last_version = get_ui_download_url('0.0.1')
assert last_version == '0.0.1' assert last_version == '0.0.1'
assert x == 'http://download11.zip' assert x == 'http://download1.zip'
with pytest.raises(ValueError, match="UI-Version not found."):
x, last_version = get_ui_download_url('0.0.3')
def test_download_data_keyboardInterrupt(mocker, caplog, markets): def test_download_data_keyboardInterrupt(mocker, caplog, markets):
@ -738,6 +754,46 @@ def test_download_data_no_pairs(mocker, caplog):
start_download_data(pargs) start_download_data(pargs)
def test_download_data_all_pairs(mocker, markets):
mocker.patch.object(Path, "exists", MagicMock(return_value=False))
dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data',
MagicMock(return_value=["ETH/BTC", "XRP/BTC"]))
patch_exchange(mocker)
mocker.patch(
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)
)
args = [
"download-data",
"--exchange",
"binance",
"--pairs",
".*/USDT"
]
pargs = get_args(args)
pargs['config'] = None
start_download_data(pargs)
expected = set(['ETH/USDT', 'XRP/USDT', 'NEO/USDT', 'TKN/USDT'])
assert set(dl_mock.call_args_list[0][1]['pairs']) == expected
assert dl_mock.call_count == 1
dl_mock.reset_mock()
args = [
"download-data",
"--exchange",
"binance",
"--pairs",
".*/USDT",
"--include-inactive-pairs",
]
pargs = get_args(args)
pargs['config'] = None
start_download_data(pargs)
expected = set(['ETH/USDT', 'LTC/USDT', 'XRP/USDT', 'NEO/USDT', 'TKN/USDT'])
assert set(dl_mock.call_args_list[0][1]['pairs']) == expected
def test_download_data_trades(mocker, caplog): def test_download_data_trades(mocker, caplog):
dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_trades_data', dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_trades_data',
MagicMock(return_value=[])) MagicMock(return_value=[]))
@ -760,6 +816,22 @@ def test_download_data_trades(mocker, caplog):
assert convert_mock.call_count == 1 assert convert_mock.call_count == 1
def test_start_convert_trades(mocker, caplog):
convert_mock = mocker.patch('freqtrade.commands.data_commands.convert_trades_to_ohlcv',
MagicMock(return_value=[]))
patch_exchange(mocker)
mocker.patch(
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})
)
args = [
"trades-to-ohlcv",
"--exchange", "kraken",
"--pairs", "ETH/BTC", "XRP/BTC",
]
start_convert_trades(get_args(args))
assert convert_mock.call_count == 1
def test_start_list_strategies(mocker, caplog, capsys): def test_start_list_strategies(mocker, caplog, capsys):
args = [ args = [

View File

@ -25,6 +25,8 @@ from freqtrade.resolvers import ExchangeResolver
from freqtrade.worker import Worker from freqtrade.worker import Worker
from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4, from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4,
mock_trade_5, mock_trade_6) mock_trade_5, mock_trade_6)
from tests.conftest_trades_usdt import (mock_trade_usdt_1, mock_trade_usdt_2, mock_trade_usdt_3,
mock_trade_usdt_4, mock_trade_usdt_5, mock_trade_usdt_6)
logging.getLogger('').setLevel(logging.INFO) logging.getLogger('').setLevel(logging.INFO)
@ -90,8 +92,10 @@ def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> No
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title())) mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title()))
mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2)) mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2))
if mock_markets: if mock_markets:
if isinstance(mock_markets, bool):
mock_markets = get_markets()
mocker.patch('freqtrade.exchange.Exchange.markets', mocker.patch('freqtrade.exchange.Exchange.markets',
PropertyMock(return_value=get_markets())) PropertyMock(return_value=mock_markets))
if api_mock: if api_mock:
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
@ -222,7 +226,40 @@ def create_mock_trades(fee, use_db: bool = True):
add_trade(trade) add_trade(trade)
if use_db: if use_db:
Trade.query.session.flush() Trade.commit()
def create_mock_trades_usdt(fee, use_db: bool = True):
"""
Create some fake trades ...
"""
def add_trade(trade):
if use_db:
Trade.query.session.add(trade)
else:
LocalTrade.add_bt_trade(trade)
# Simulate dry_run entries
trade = mock_trade_usdt_1(fee)
add_trade(trade)
trade = mock_trade_usdt_2(fee)
add_trade(trade)
trade = mock_trade_usdt_3(fee)
add_trade(trade)
trade = mock_trade_usdt_4(fee)
add_trade(trade)
trade = mock_trade_usdt_5(fee)
add_trade(trade)
trade = mock_trade_usdt_6(fee)
add_trade(trade)
if use_db:
Trade.commit()
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
@ -257,6 +294,11 @@ def default_conf(testdatadir):
return get_default_conf(testdatadir) return get_default_conf(testdatadir)
@pytest.fixture(scope="function")
def default_conf_usdt(testdatadir):
return get_default_conf_usdt(testdatadir)
def get_default_conf(testdatadir): def get_default_conf(testdatadir):
""" Returns validated configuration suitable for most tests """ """ Returns validated configuration suitable for most tests """
configuration = { configuration = {
@ -331,6 +373,32 @@ def get_default_conf(testdatadir):
return configuration return configuration
def get_default_conf_usdt(testdatadir):
configuration = get_default_conf(testdatadir)
configuration.update({
"stake_amount": 60.0,
"stake_currency": "USDT",
"exchange": {
"name": "binance",
"enabled": True,
"key": "key",
"secret": "secret",
"pair_whitelist": [
"ETH/USDT",
"LTC/USDT",
"XRP/USDT",
"NEO/USDT",
"TKN/USDT",
],
"pair_blacklist": [
"DOGE/USDT",
"HOT/USDT",
]
},
})
return configuration
@pytest.fixture @pytest.fixture
def update(): def update():
_update = Update(0) _update = Update(0)
@ -370,12 +438,41 @@ def ticker_sell_down():
}) })
@pytest.fixture
def ticker_usdt():
return MagicMock(return_value={
'bid': 2.0,
'ask': 2.02,
'last': 2.0,
})
@pytest.fixture
def ticker_usdt_sell_up():
return MagicMock(return_value={
'bid': 2.2,
'ask': 2.3,
'last': 2.2,
})
@pytest.fixture
def ticker_usdt_sell_down():
return MagicMock(return_value={
'bid': 2.01,
'ask': 2.0,
'last': 2.01,
})
@pytest.fixture @pytest.fixture
def markets(): def markets():
return get_markets() return get_markets()
def get_markets(): def get_markets():
# See get_markets_static() for immutable markets and do not modify them unless absolutely
# necessary!
return { return {
'ETH/BTC': { 'ETH/BTC': {
'id': 'ethbtc', 'id': 'ethbtc',
@ -600,6 +697,81 @@ def get_markets():
}, },
'info': {}, 'info': {},
}, },
'XRP/USDT': {
'id': 'xrpusdt',
'symbol': 'XRP/USDT',
'base': 'XRP',
'quote': 'USDT',
'active': True,
'precision': {
'price': 8,
'amount': 8,
'cost': 8,
},
'lot': 0.00000001,
'limits': {
'amount': {
'min': 0.01,
'max': 1000,
},
'price': 500000,
'cost': {
'min': 0.0001,
'max': 500000,
},
},
'info': {},
},
'NEO/USDT': {
'id': 'neousdt',
'symbol': 'NEO/USDT',
'base': 'NEO',
'quote': 'USDT',
'active': True,
'precision': {
'price': 8,
'amount': 8,
'cost': 8,
},
'lot': 0.00000001,
'limits': {
'amount': {
'min': 0.01,
'max': 1000,
},
'price': 500000,
'cost': {
'min': 0.0001,
'max': 500000,
},
},
'info': {},
},
'TKN/USDT': {
'id': 'tknusdt',
'symbol': 'TKN/USDT',
'base': 'TKN',
'quote': 'USDT',
'active': True,
'precision': {
'price': 8,
'amount': 8,
'cost': 8,
},
'lot': 0.00000001,
'limits': {
'amount': {
'min': 0.01,
'max': 1000,
},
'price': 500000,
'cost': {
'min': 0.0001,
'max': 500000,
},
},
'info': {},
},
'LTC/USD': { 'LTC/USD': {
'id': 'USD-LTC', 'id': 'USD-LTC',
'symbol': 'LTC/USD', 'symbol': 'LTC/USD',
@ -675,11 +847,22 @@ def get_markets():
@pytest.fixture @pytest.fixture
def shitcoinmarkets(markets): def markets_static():
# These markets are used in some tests that would need adaptation should anything change in
# market list. Do not modify this list without a good reason! Do not modify market parameters
# of listed pairs in get_markets() without a good reason either!
static_markets = ['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD',
'LTC/USDT', 'NEO/BTC', 'TKN/BTC', 'XLTCUSDT', 'XRP/BTC']
all_markets = get_markets()
return {m: all_markets[m] for m in static_markets}
@pytest.fixture
def shitcoinmarkets(markets_static):
""" """
Fixture with shitcoin markets - used to test filters in pairlists Fixture with shitcoin markets - used to test filters in pairlists
""" """
shitmarkets = deepcopy(markets) shitmarkets = deepcopy(markets_static)
shitmarkets.update({ shitmarkets.update({
'HOT/BTC': { 'HOT/BTC': {
'id': 'HOTBTC', 'id': 'HOTBTC',
@ -1521,27 +1704,34 @@ def result(testdatadir):
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def trades_for_order(): def trades_for_order():
return [{'info': {'id': 34567, return [{
'orderId': 123456, 'info': {
'price': '0.24544100', 'id': 34567,
'qty': '8.00000000', 'orderId': 123456,
'commission': '0.00800000', 'price': '2.0',
'commissionAsset': 'LTC', 'qty': '8.00000000',
'time': 1521663363189, 'commission': '0.00800000',
'isBuyer': True, 'commissionAsset': 'LTC',
'isMaker': False, 'time': 1521663363189,
'isBestMatch': True}, 'isBuyer': True,
'timestamp': 1521663363189, 'isMaker': False,
'datetime': '2018-03-21T20:16:03.189Z', 'isBestMatch': True
'symbol': 'LTC/ETH', },
'id': '34567', 'timestamp': 1521663363189,
'order': '123456', 'datetime': '2018-03-21T20:16:03.189Z',
'type': None, 'symbol': 'LTC/USDT',
'side': 'buy', 'id': '34567',
'price': 0.245441, 'order': '123456',
'cost': 1.963528, 'type': None,
'amount': 8.0, 'side': 'buy',
'fee': {'cost': 0.008, 'currency': 'LTC'}}] 'price': 2.0,
'cost': 16.0,
'amount': 8.0,
'fee': {
'cost': 0.008,
'currency': 'LTC'
}
}]
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
@ -1806,6 +1996,22 @@ def open_trade():
) )
@pytest.fixture(scope="function")
def open_trade_usdt():
return Trade(
pair='ADA/USDT',
open_rate=2.0,
exchange='binance',
open_order_id='123456789',
amount=30.0,
fee_open=0.0,
fee_close=0.0,
stake_amount=60.0,
open_date=arrow.utcnow().shift(minutes=-601).datetime,
is_open=True
)
@pytest.fixture @pytest.fixture
def saved_hyperopt_results(): def saved_hyperopt_results():
hyperopt_res = [ hyperopt_res = [
@ -1949,7 +2155,7 @@ def saved_hyperopt_results():
@pytest.fixture(scope='function') @pytest.fixture(scope='function')
def limit_buy_order_usdt_open(): def limit_buy_order_usdt_open():
return { return {
'id': 'mocked_limit_buy', 'id': 'mocked_limit_buy_usdt',
'type': 'limit', 'type': 'limit',
'side': 'buy', 'side': 'buy',
'symbol': 'mocked', 'symbol': 'mocked',
@ -1976,7 +2182,7 @@ def limit_buy_order_usdt(limit_buy_order_usdt_open):
@pytest.fixture @pytest.fixture
def limit_sell_order_usdt_open(): def limit_sell_order_usdt_open():
return { return {
'id': 'mocked_limit_sell', 'id': 'mocked_limit_sell_usdt',
'type': 'limit', 'type': 'limit',
'side': 'sell', 'side': 'sell',
'pair': 'mocked', 'pair': 'mocked',

View File

@ -0,0 +1,305 @@
from datetime import datetime, timedelta, timezone
from freqtrade.persistence.models import Order, Trade
MOCK_TRADE_COUNT = 6
def mock_order_usdt_1():
return {
'id': '1234',
'symbol': 'ADA/USDT',
'status': 'closed',
'side': 'buy',
'type': 'limit',
'price': 2.0,
'amount': 10.0,
'filled': 10.0,
'remaining': 0.0,
}
def mock_trade_usdt_1(fee):
trade = Trade(
pair='ADA/USDT',
stake_amount=20.0,
amount=10.0,
amount_requested=10.0,
fee_open=fee.return_value,
fee_close=fee.return_value,
is_open=True,
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17),
open_rate=2.0,
exchange='binance',
open_order_id='dry_run_buy_12345',
strategy='StrategyTestV2',
timeframe=5,
)
o = Order.parse_from_ccxt_object(mock_order_usdt_1(), 'ADA/USDT', 'buy')
trade.orders.append(o)
return trade
def mock_order_usdt_2():
return {
'id': '1235',
'symbol': 'ETC/USDT',
'status': 'closed',
'side': 'buy',
'type': 'limit',
'price': 2.0,
'amount': 100.0,
'filled': 100.0,
'remaining': 0.0,
}
def mock_order_usdt_2_sell():
return {
'id': '12366',
'symbol': 'ETC/USDT',
'status': 'closed',
'side': 'sell',
'type': 'limit',
'price': 2.05,
'amount': 100.0,
'filled': 100.0,
'remaining': 0.0,
}
def mock_trade_usdt_2(fee):
"""
Closed trade...
"""
trade = Trade(
pair='ETC/USDT',
stake_amount=200.0,
amount=100.0,
amount_requested=100.0,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_rate=2.0,
close_rate=2.05,
close_profit=5.0,
close_profit_abs=3.9875,
exchange='binance',
is_open=False,
open_order_id='dry_run_sell_12345',
strategy='StrategyTestV2',
timeframe=5,
sell_reason='sell_signal',
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2),
)
o = Order.parse_from_ccxt_object(mock_order_usdt_2(), 'ETC/USDT', 'buy')
trade.orders.append(o)
o = Order.parse_from_ccxt_object(mock_order_usdt_2_sell(), 'ETC/USDT', 'sell')
trade.orders.append(o)
return trade
def mock_order_usdt_3():
return {
'id': '41231a12a',
'symbol': 'XRP/USDT',
'status': 'closed',
'side': 'buy',
'type': 'limit',
'price': 1.0,
'amount': 30.0,
'filled': 30.0,
'remaining': 0.0,
}
def mock_order_usdt_3_sell():
return {
'id': '41231a666a',
'symbol': 'XRP/USDT',
'status': 'closed',
'side': 'sell',
'type': 'stop_loss_limit',
'price': 1.1,
'average': 1.1,
'amount': 30.0,
'filled': 30.0,
'remaining': 0.0,
}
def mock_trade_usdt_3(fee):
"""
Closed trade
"""
trade = Trade(
pair='XRP/USDT',
stake_amount=30.0,
amount=30.0,
amount_requested=30.0,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_rate=1.0,
close_rate=1.1,
close_profit=10.0,
close_profit_abs=9.8425,
exchange='binance',
is_open=False,
strategy='StrategyTestV2',
timeframe=5,
sell_reason='roi',
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
close_date=datetime.now(tz=timezone.utc),
)
o = Order.parse_from_ccxt_object(mock_order_usdt_3(), 'XRP/USDT', 'buy')
trade.orders.append(o)
o = Order.parse_from_ccxt_object(mock_order_usdt_3_sell(), 'XRP/USDT', 'sell')
trade.orders.append(o)
return trade
def mock_order_usdt_4():
return {
'id': 'prod_buy_12345',
'symbol': 'ETC/USDT',
'status': 'open',
'side': 'buy',
'type': 'limit',
'price': 2.0,
'amount': 10.0,
'filled': 0.0,
'remaining': 30.0,
}
def mock_trade_usdt_4(fee):
"""
Simulate prod entry
"""
trade = Trade(
pair='ETC/USDT',
stake_amount=20.0,
amount=10.0,
amount_requested=10.01,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=14),
is_open=True,
open_rate=2.0,
exchange='binance',
open_order_id='prod_buy_12345',
strategy='StrategyTestV2',
timeframe=5,
)
o = Order.parse_from_ccxt_object(mock_order_usdt_4(), 'ETC/USDT', 'buy')
trade.orders.append(o)
return trade
def mock_order_usdt_5():
return {
'id': 'prod_buy_3455',
'symbol': 'XRP/USDT',
'status': 'closed',
'side': 'buy',
'type': 'limit',
'price': 2.0,
'amount': 10.0,
'filled': 10.0,
'remaining': 0.0,
}
def mock_order_usdt_5_stoploss():
return {
'id': 'prod_stoploss_3455',
'symbol': 'XRP/USDT',
'status': 'open',
'side': 'sell',
'type': 'stop_loss_limit',
'price': 2.0,
'amount': 10.0,
'filled': 0.0,
'remaining': 30.0,
}
def mock_trade_usdt_5(fee):
"""
Simulate prod entry with stoploss
"""
trade = Trade(
pair='XRP/USDT',
stake_amount=20.0,
amount=10.0,
amount_requested=10.01,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=12),
is_open=True,
open_rate=2.0,
exchange='binance',
strategy='SampleStrategy',
stoploss_order_id='prod_stoploss_3455',
timeframe=5,
)
o = Order.parse_from_ccxt_object(mock_order_usdt_5(), 'XRP/USDT', 'buy')
trade.orders.append(o)
o = Order.parse_from_ccxt_object(mock_order_usdt_5_stoploss(), 'XRP/USDT', 'stoploss')
trade.orders.append(o)
return trade
def mock_order_usdt_6():
return {
'id': 'prod_buy_6',
'symbol': 'LTC/USDT',
'status': 'closed',
'side': 'buy',
'type': 'limit',
'price': 10.0,
'amount': 2.0,
'filled': 2.0,
'remaining': 0.0,
}
def mock_order_usdt_6_sell():
return {
'id': 'prod_sell_6',
'symbol': 'LTC/USDT',
'status': 'open',
'side': 'sell',
'type': 'limit',
'price': 12.0,
'amount': 2.0,
'filled': 0.0,
'remaining': 2.0,
}
def mock_trade_usdt_6(fee):
"""
Simulate prod entry with open sell order
"""
trade = Trade(
pair='LTC/USDT',
stake_amount=20.0,
amount=2.0,
amount_requested=2.0,
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=5),
fee_open=fee.return_value,
fee_close=fee.return_value,
is_open=True,
open_rate=10.0,
exchange='binance',
strategy='SampleStrategy',
open_order_id="prod_sell_6",
timeframe=5,
)
o = Order.parse_from_ccxt_object(mock_order_usdt_6(), 'LTC/USDT', 'buy')
trade.orders.append(o)
o = Order.parse_from_ccxt_object(mock_order_usdt_6_sell(), 'LTC/USDT', 'sell')
trade.orders.append(o)
return trade

View File

@ -275,6 +275,7 @@ def test_amount_to_precision(default_conf, mocker, amount, precision_mode, preci
(234.43, 4, 0.5, 234.5), (234.43, 4, 0.5, 234.5),
(234.53, 4, 0.5, 235.0), (234.53, 4, 0.5, 235.0),
(0.891534, 4, 0.0001, 0.8916), (0.891534, 4, 0.0001, 0.8916),
(64968.89, 4, 0.01, 64968.89),
]) ])
def test_price_to_precision(default_conf, mocker, price, precision_mode, precision, expected): def test_price_to_precision(default_conf, mocker, price, precision_mode, precision, expected):
@ -293,7 +294,7 @@ def test_price_to_precision(default_conf, mocker, price, precision_mode, precisi
PropertyMock(return_value=precision_mode)) PropertyMock(return_value=precision_mode))
pair = 'ETH/BTC' pair = 'ETH/BTC'
assert pytest.approx(exchange.price_to_precision(pair, price)) == expected assert exchange.price_to_precision(pair, price) == expected
@pytest.mark.parametrize("price,precision_mode,precision,expected", [ @pytest.mark.parametrize("price,precision_mode,precision,expected", [
@ -1831,6 +1832,7 @@ def test_fetch_l2_order_book_exception(default_conf, mocker, exchange_name):
('ask', 20, 19, 10, 0.3, 17), # Between ask and last ('ask', 20, 19, 10, 0.3, 17), # Between ask and last
('ask', 5, 6, 10, 1.0, 5), # last bigger than ask ('ask', 5, 6, 10, 1.0, 5), # last bigger than ask
('ask', 5, 6, 10, 0.5, 5), # last bigger than ask ('ask', 5, 6, 10, 0.5, 5), # last bigger than ask
('ask', 20, 19, 10, None, 20), # ask_last_balance missing
('ask', 10, 20, None, 0.5, 10), # last not available - uses ask ('ask', 10, 20, None, 0.5, 10), # last not available - uses ask
('ask', 4, 5, None, 0.5, 4), # last not available - uses ask ('ask', 4, 5, None, 0.5, 4), # last not available - uses ask
('ask', 4, 5, None, 1, 4), # last not available - uses ask ('ask', 4, 5, None, 1, 4), # last not available - uses ask
@ -1841,6 +1843,7 @@ def test_fetch_l2_order_book_exception(default_conf, mocker, exchange_name):
('bid', 21, 20, 10, 0.7, 13), # Between bid and last ('bid', 21, 20, 10, 0.7, 13), # Between bid and last
('bid', 21, 20, 10, 0.3, 17), # Between bid and last ('bid', 21, 20, 10, 0.3, 17), # Between bid and last
('bid', 6, 5, 10, 1.0, 5), # last bigger than bid ('bid', 6, 5, 10, 1.0, 5), # last bigger than bid
('bid', 21, 20, 10, None, 20), # ask_last_balance missing
('bid', 6, 5, 10, 0.5, 5), # last bigger than bid ('bid', 6, 5, 10, 0.5, 5), # last bigger than bid
('bid', 21, 20, None, 0.5, 20), # last not available - uses bid ('bid', 21, 20, None, 0.5, 20), # last not available - uses bid
('bid', 6, 5, None, 0.5, 5), # last not available - uses bid ('bid', 6, 5, None, 0.5, 5), # last not available - uses bid
@ -1850,7 +1853,10 @@ def test_fetch_l2_order_book_exception(default_conf, mocker, exchange_name):
def test_get_buy_rate(mocker, default_conf, caplog, side, ask, bid, def test_get_buy_rate(mocker, default_conf, caplog, side, ask, bid,
last, last_ab, expected) -> None: last, last_ab, expected) -> None:
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
default_conf['bid_strategy']['ask_last_balance'] = last_ab if last_ab is None:
del default_conf['bid_strategy']['ask_last_balance']
else:
default_conf['bid_strategy']['ask_last_balance'] = last_ab
default_conf['bid_strategy']['price_side'] = side default_conf['bid_strategy']['price_side'] = side
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
@ -1875,6 +1881,7 @@ def test_get_buy_rate(mocker, default_conf, caplog, side, ask, bid,
('bid', 12.0, 11.2, 10.5, 1.0, 11.2), # Last smaller than bid - uses bid ('bid', 12.0, 11.2, 10.5, 1.0, 11.2), # Last smaller than bid - uses bid
('bid', 12.0, 11.2, 10.5, 0.5, 11.2), # Last smaller than bid - uses bid ('bid', 12.0, 11.2, 10.5, 0.5, 11.2), # Last smaller than bid - uses bid
('bid', 0.003, 0.002, 0.005, 0.0, 0.002), ('bid', 0.003, 0.002, 0.005, 0.0, 0.002),
('bid', 0.003, 0.002, 0.005, None, 0.002),
('ask', 12.0, 11.0, 12.5, 0.0, 12.0), # full ask side ('ask', 12.0, 11.0, 12.5, 0.0, 12.0), # full ask side
('ask', 12.0, 11.0, 12.5, 1.0, 12.5), # full last side ('ask', 12.0, 11.0, 12.5, 1.0, 12.5), # full last side
('ask', 12.0, 11.0, 12.5, 0.5, 12.25), # between bid and lat ('ask', 12.0, 11.0, 12.5, 0.5, 12.25), # between bid and lat
@ -1885,13 +1892,15 @@ def test_get_buy_rate(mocker, default_conf, caplog, side, ask, bid,
('ask', 10.11, 11.2, 11.0, 0.0, 10.11), ('ask', 10.11, 11.2, 11.0, 0.0, 10.11),
('ask', 0.001, 0.002, 11.0, 0.0, 0.001), ('ask', 0.001, 0.002, 11.0, 0.0, 0.001),
('ask', 0.006, 1.0, 11.0, 0.0, 0.006), ('ask', 0.006, 1.0, 11.0, 0.0, 0.006),
('ask', 0.006, 1.0, 11.0, None, 0.006),
]) ])
def test_get_sell_rate(default_conf, mocker, caplog, side, bid, ask, def test_get_sell_rate(default_conf, mocker, caplog, side, bid, ask,
last, last_ab, expected) -> None: last, last_ab, expected) -> None:
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
default_conf['ask_strategy']['price_side'] = side default_conf['ask_strategy']['price_side'] = side
default_conf['ask_strategy']['bid_last_balance'] = last_ab if last_ab is not None:
default_conf['ask_strategy']['bid_last_balance'] = last_ab
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
return_value={'ask': ask, 'bid': bid, 'last': last}) return_value={'ask': ask, 'bid': bid, 'last': last})
pair = "ETH/BTC" pair = "ETH/BTC"
@ -2735,7 +2744,7 @@ def test_get_valid_pair_combination(default_conf, mocker, markets):
(['LTC'], ['NONEXISTENT'], False, False, (['LTC'], ['NONEXISTENT'], False, False,
[]), []),
]) ])
def test_get_markets(default_conf, mocker, markets, def test_get_markets(default_conf, mocker, markets_static,
base_currencies, quote_currencies, pairs_only, active_only, base_currencies, quote_currencies, pairs_only, active_only,
expected_keys): expected_keys):
mocker.patch.multiple('freqtrade.exchange.Exchange', mocker.patch.multiple('freqtrade.exchange.Exchange',
@ -2743,7 +2752,7 @@ def test_get_markets(default_conf, mocker, markets,
_load_async_markets=MagicMock(), _load_async_markets=MagicMock(),
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
validate_timeframes=MagicMock(), validate_timeframes=MagicMock(),
markets=PropertyMock(return_value=markets)) markets=PropertyMock(return_value=markets_static))
ex = Exchange(default_conf) ex = Exchange(default_conf)
pairs = ex.get_markets(base_currencies, quote_currencies, pairs_only, active_only) pairs = ex.get_markets(base_currencies, quote_currencies, pairs_only, active_only)
assert sorted(pairs.keys()) == sorted(expected_keys) assert sorted(pairs.keys()) == sorted(expected_keys)

View File

@ -0,0 +1,28 @@
import pytest
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import Gateio
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
def test_validate_order_types_gateio(default_conf, mocker):
default_conf['exchange']['name'] = 'gateio'
mocker.patch('freqtrade.exchange.Exchange._init_ccxt')
mocker.patch('freqtrade.exchange.Exchange._load_markets', return_value={})
mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
mocker.patch('freqtrade.exchange.Exchange.name', 'Bittrex')
exch = ExchangeResolver.load_exchange('gateio', default_conf, True)
assert isinstance(exch, Gateio)
default_conf['order_types'] = {
'buy': 'market',
'sell': 'limit',
'stoploss': 'market',
'stoploss_on_exchange': False
}
with pytest.raises(OperationalException,
match=r'Exchange .* does not support market orders.'):
ExchangeResolver.load_exchange('gateio', default_conf, True)

View File

@ -39,16 +39,25 @@ def hyperopt(hyperopt_conf, mocker):
def hyperopt_results(): def hyperopt_results():
return pd.DataFrame( return pd.DataFrame(
{ {
'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], 'pair': ['ETH/USDT', 'ETH/USDT', 'ETH/USDT', 'ETH/USDT'],
'profit_ratio': [-0.1, 0.2, 0.3], 'profit_ratio': [-0.1, 0.2, -0.1, 0.3],
'profit_abs': [-0.2, 0.4, 0.6], 'profit_abs': [-0.2, 0.4, -0.2, 0.6],
'trade_duration': [10, 30, 10], 'trade_duration': [10, 30, 10, 10],
'sell_reason': [SellType.STOP_LOSS, SellType.ROI, SellType.ROI], 'amount': [0.1, 0.1, 0.1, 0.1],
'sell_reason': [SellType.STOP_LOSS, SellType.ROI, SellType.STOP_LOSS, SellType.ROI],
'open_date':
[
datetime(2019, 1, 1, 9, 15, 0),
datetime(2019, 1, 2, 8, 55, 0),
datetime(2019, 1, 3, 9, 15, 0),
datetime(2019, 1, 4, 9, 15, 0),
],
'close_date': 'close_date':
[ [
datetime(2019, 1, 1, 9, 26, 3, 478039), datetime(2019, 1, 1, 9, 25, 0),
datetime(2019, 2, 1, 9, 26, 3, 478039), datetime(2019, 1, 2, 9, 25, 0),
datetime(2019, 3, 1, 9, 26, 3, 478039) datetime(2019, 1, 3, 9, 25, 0),
] datetime(2019, 1, 4, 9, 25, 0),
],
} }
) )

View File

@ -1102,6 +1102,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
'--timerange', '1510694220-1510700340', '--timerange', '1510694220-1510700340',
'--enable-position-stacking', '--enable-position-stacking',
'--disable-max-market-positions', '--disable-max-market-positions',
'--breakdown', 'day',
'--strategy-list', '--strategy-list',
'StrategyTestV2', 'StrategyTestV2',
'TestStrategyLegacyV1', 'TestStrategyLegacyV1',
@ -1130,6 +1131,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
captured = capsys.readouterr() captured = capsys.readouterr()
assert 'BACKTESTING REPORT' in captured.out assert 'BACKTESTING REPORT' in captured.out
assert 'SELL REASON STATS' in captured.out assert 'SELL REASON STATS' in captured.out
assert 'DAY BREAKDOWN' in captured.out
assert 'LEFT OPEN TRADES REPORT' in captured.out assert 'LEFT OPEN TRADES REPORT' in captured.out
assert '2017-11-14 21:17:00 -> 2017-11-14 22:58:00 | Max open trades : 1' in captured.out assert '2017-11-14 21:17:00 -> 2017-11-14 22:58:00 | Max open trades : 1' in captured.out
assert 'STRATEGY SUMMARY' in captured.out assert 'STRATEGY SUMMARY' in captured.out

View File

@ -702,7 +702,7 @@ def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> Non
assert hasattr(hyperopt, "position_stacking") assert hasattr(hyperopt, "position_stacking")
def test_simplified_interface_all_failed(mocker, hyperopt_conf) -> None: def test_simplified_interface_all_failed(mocker, hyperopt_conf, caplog) -> None:
mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
@ -724,7 +724,13 @@ def test_simplified_interface_all_failed(mocker, hyperopt_conf) -> None:
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
with pytest.raises(OperationalException, match=r"The 'protection' space is included into *"): with pytest.raises(OperationalException, match=r"The 'protection' space is included into *"):
hyperopt.start() hyperopt.init_spaces()
hyperopt.config['hyperopt_ignore_missing_space'] = True
caplog.clear()
hyperopt.init_spaces()
assert log_has_re(r"The 'protection' space is included into *", caplog)
assert hyperopt.protection_space == []
def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None: def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None:

View File

@ -209,7 +209,8 @@ def test_export_params(tmpdir):
assert filename.is_file() assert filename.is_file()
content = rapidjson.load(filename.open('r')) with filename.open('r') as f:
content = rapidjson.load(f)
assert content['strategy_name'] == 'StrategyTestV2' assert content['strategy_name'] == 'StrategyTestV2'
assert 'params' in content assert 'params' in content
assert "buy" in content["params"] assert "buy" in content["params"]

View File

@ -35,6 +35,7 @@ def test_hyperoptlossresolver_wrongname(default_conf) -> None:
def test_loss_calculation_prefer_correct_trade_count(hyperopt_conf, hyperopt_results) -> None: def test_loss_calculation_prefer_correct_trade_count(hyperopt_conf, hyperopt_results) -> None:
hyperopt_conf.update({'hyperopt_loss': "ShortTradeDurHyperOptLoss"})
hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf) hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf)
correct = hl.hyperopt_loss_function(hyperopt_results, 600, correct = hl.hyperopt_loss_function(hyperopt_results, 600,
datetime(2019, 1, 1), datetime(2019, 5, 1)) datetime(2019, 1, 1), datetime(2019, 5, 1))
@ -50,6 +51,7 @@ def test_loss_calculation_prefer_shorter_trades(hyperopt_conf, hyperopt_results)
resultsb = hyperopt_results.copy() resultsb = hyperopt_results.copy()
resultsb.loc[1, 'trade_duration'] = 20 resultsb.loc[1, 'trade_duration'] = 20
hyperopt_conf.update({'hyperopt_loss': "ShortTradeDurHyperOptLoss"})
hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf) hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf)
longer = hl.hyperopt_loss_function(hyperopt_results, 100, longer = hl.hyperopt_loss_function(hyperopt_results, 100,
datetime(2019, 1, 1), datetime(2019, 5, 1)) datetime(2019, 1, 1), datetime(2019, 5, 1))
@ -64,6 +66,7 @@ def test_loss_calculation_has_limited_profit(hyperopt_conf, hyperopt_results) ->
results_under = hyperopt_results.copy() results_under = hyperopt_results.copy()
results_under['profit_ratio'] = hyperopt_results['profit_ratio'] / 2 results_under['profit_ratio'] = hyperopt_results['profit_ratio'] / 2
hyperopt_conf.update({'hyperopt_loss': "ShortTradeDurHyperOptLoss"})
hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf) hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf)
correct = hl.hyperopt_loss_function(hyperopt_results, 600, correct = hl.hyperopt_loss_function(hyperopt_results, 600,
datetime(2019, 1, 1), datetime(2019, 5, 1)) datetime(2019, 1, 1), datetime(2019, 5, 1))
@ -75,91 +78,29 @@ def test_loss_calculation_has_limited_profit(hyperopt_conf, hyperopt_results) ->
assert under > correct assert under > correct
def test_sharpe_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None: @pytest.mark.parametrize('lossfunction', [
"OnlyProfitHyperOptLoss",
"SortinoHyperOptLoss",
"SortinoHyperOptLossDaily",
"SharpeHyperOptLoss",
"SharpeHyperOptLossDaily",
"MaxDrawDownHyperOptLoss",
])
def test_loss_functions_better_profits(default_conf, hyperopt_results, lossfunction) -> None:
results_over = hyperopt_results.copy() results_over = hyperopt_results.copy()
results_over['profit_abs'] = hyperopt_results['profit_abs'] * 2 + 0.2
results_over['profit_ratio'] = hyperopt_results['profit_ratio'] * 2 results_over['profit_ratio'] = hyperopt_results['profit_ratio'] * 2
results_under = hyperopt_results.copy() results_under = hyperopt_results.copy()
results_under['profit_abs'] = hyperopt_results['profit_abs'] / 2 - 0.2
results_under['profit_ratio'] = hyperopt_results['profit_ratio'] / 2 results_under['profit_ratio'] = hyperopt_results['profit_ratio'] / 2
default_conf.update({'hyperopt_loss': 'SharpeHyperOptLoss'}) default_conf.update({'hyperopt_loss': lossfunction})
hl = HyperOptLossResolver.load_hyperoptloss(default_conf) hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results),
datetime(2019, 1, 1), datetime(2019, 5, 1)) datetime(2019, 1, 1), datetime(2019, 5, 1))
over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), over = hl.hyperopt_loss_function(results_over, len(results_over),
datetime(2019, 1, 1), datetime(2019, 5, 1)) datetime(2019, 1, 1), datetime(2019, 5, 1))
under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), under = hl.hyperopt_loss_function(results_under, len(results_under),
datetime(2019, 1, 1), datetime(2019, 5, 1))
assert over < correct
assert under > correct
def test_sharpe_loss_daily_prefers_higher_profits(default_conf, hyperopt_results) -> None:
results_over = hyperopt_results.copy()
results_over['profit_ratio'] = hyperopt_results['profit_ratio'] * 2
results_under = hyperopt_results.copy()
results_under['profit_ratio'] = hyperopt_results['profit_ratio'] / 2
default_conf.update({'hyperopt_loss': 'SharpeHyperOptLossDaily'})
hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
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_sortino_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None:
results_over = hyperopt_results.copy()
results_over['profit_ratio'] = hyperopt_results['profit_ratio'] * 2
results_under = hyperopt_results.copy()
results_under['profit_ratio'] = hyperopt_results['profit_ratio'] / 2
default_conf.update({'hyperopt_loss': 'SortinoHyperOptLoss'})
hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
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_sortino_loss_daily_prefers_higher_profits(default_conf, hyperopt_results) -> None:
results_over = hyperopt_results.copy()
results_over['profit_ratio'] = hyperopt_results['profit_ratio'] * 2
results_under = hyperopt_results.copy()
results_under['profit_ratio'] = hyperopt_results['profit_ratio'] / 2
default_conf.update({'hyperopt_loss': 'SortinoHyperOptLossDaily'})
hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
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_abs'] = hyperopt_results['profit_abs'] * 2
results_under = hyperopt_results.copy()
results_under['profit_abs'] = hyperopt_results['profit_abs'] / 2
default_conf.update({'hyperopt_loss': 'OnlyProfitHyperOptLoss'})
hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
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)) datetime(2019, 1, 1), datetime(2019, 5, 1))
assert over < correct assert over < correct
assert under > correct assert under > correct

View File

@ -13,8 +13,10 @@ from freqtrade.data import history
from freqtrade.data.btanalysis import get_latest_backtest_filename, load_backtest_data from freqtrade.data.btanalysis import get_latest_backtest_filename, load_backtest_data
from freqtrade.edge import PairInfo from freqtrade.edge import PairInfo
from freqtrade.enums import SellType from freqtrade.enums import SellType
from freqtrade.optimize.optimize_reports import (generate_backtest_stats, generate_daily_stats, from freqtrade.optimize.optimize_reports import (_get_resample_from_period, generate_backtest_stats,
generate_edge_table, generate_pair_metrics, generate_daily_stats, generate_edge_table,
generate_pair_metrics,
generate_periodic_breakdown_stats,
generate_sell_reason_stats, generate_sell_reason_stats,
generate_strategy_comparison, generate_strategy_comparison,
generate_trading_stats, store_backtest_stats, generate_trading_stats, store_backtest_stats,
@ -377,3 +379,31 @@ def test_generate_edge_table():
assert generate_edge_table(results).count('| ETH/BTC |') == 1 assert generate_edge_table(results).count('| ETH/BTC |') == 1
assert generate_edge_table(results).count( assert generate_edge_table(results).count(
'| Risk Reward Ratio | Required Risk Reward | Expectancy |') == 1 '| Risk Reward Ratio | Required Risk Reward | Expectancy |') == 1
def test_generate_periodic_breakdown_stats(testdatadir):
filename = testdatadir / "backtest-result_new.json"
bt_data = load_backtest_data(filename).to_dict(orient='records')
res = generate_periodic_breakdown_stats(bt_data, 'day')
assert isinstance(res, list)
assert len(res) == 21
day = res[0]
assert 'date' in day
assert 'draws' in day
assert 'loses' in day
assert 'wins' in day
assert 'profit_abs' in day
# Select empty dataframe!
res = generate_periodic_breakdown_stats([], 'day')
assert res == []
def test__get_resample_from_period():
assert _get_resample_from_period('day') == '1d'
assert _get_resample_from_period('week') == '1w'
assert _get_resample_from_period('month') == '1M'
with pytest.raises(ValueError, match=r"Period noooo is not supported."):
_get_resample_from_period('noooo')

View File

@ -131,9 +131,9 @@ def test_load_pairlist_noexist(mocker, markets, default_conf):
default_conf, {}, 1) default_conf, {}, 1)
def test_load_pairlist_verify_multi(mocker, markets, default_conf): def test_load_pairlist_verify_multi(mocker, markets_static, default_conf):
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = 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_static))
plm = PairListManager(freqtrade.exchange, default_conf) plm = PairListManager(freqtrade.exchange, default_conf)
# Call different versions one after the other, should always consider what was passed in # Call different versions one after the other, should always consider what was passed in
# and have no side-effects (therefore the same check multiple times) # and have no side-effects (therefore the same check multiple times)
@ -415,10 +415,10 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
# SpreadFilter only # SpreadFilter only
([{"method": "SpreadFilter", "max_spread_ratio": 0.005}], ([{"method": "SpreadFilter", "max_spread_ratio": 0.005}],
"BTC", 'filter_at_the_beginning'), # OperationalException expected "BTC", 'filter_at_the_beginning'), # OperationalException expected
# Static Pairlist after VolumePairList, on a non-first position # Static Pairlist after VolumePairList, on a non-first position (appends pairs)
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, ([{"method": "VolumePairList", "number_assets": 2, "sort_key": "quoteVolume"},
{"method": "StaticPairList"}], {"method": "StaticPairList"}],
"BTC", 'static_in_the_middle'), "BTC", ['ETH/BTC', 'TKN/BTC', 'TRST/BTC', 'SWT/BTC', 'BCC/BTC', 'HOT/BTC']),
([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, ([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"},
{"method": "PriceFilter", "low_price_ratio": 0.02}], {"method": "PriceFilter", "low_price_ratio": 0.02}],
"USDT", ['ETH/USDT', 'NANO/USDT']), "USDT", ['ETH/USDT', 'NANO/USDT']),
@ -469,13 +469,6 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
if whitelist_result == 'static_in_the_middle':
with pytest.raises(OperationalException,
match=r"StaticPairList can only be used in the first position "
r"in the list of Pairlist Handlers."):
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
return
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
mocker.patch.multiple('freqtrade.exchange.Exchange', mocker.patch.multiple('freqtrade.exchange.Exchange',
get_tickers=tickers, get_tickers=tickers,
@ -665,11 +658,11 @@ def test_PerformanceFilter_error(mocker, whitelist_conf, caplog) -> None:
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
def test_PerformanceFilter_lookback(mocker, whitelist_conf, fee) -> None: def test_PerformanceFilter_lookback(mocker, whitelist_conf, fee, caplog) -> None:
whitelist_conf['exchange']['pair_whitelist'].append('XRP/BTC') whitelist_conf['exchange']['pair_whitelist'].append('XRP/BTC')
whitelist_conf['pairlists'] = [ whitelist_conf['pairlists'] = [
{"method": "StaticPairList"}, {"method": "StaticPairList"},
{"method": "PerformanceFilter", "minutes": 60} {"method": "PerformanceFilter", "minutes": 60, "min_profit": 0.01}
] ]
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
exchange = get_patched_exchange(mocker, whitelist_conf) exchange = get_patched_exchange(mocker, whitelist_conf)
@ -681,7 +674,8 @@ def test_PerformanceFilter_lookback(mocker, whitelist_conf, fee) -> None:
with time_machine.travel("2021-09-01 05:00:00 +00:00") as t: with time_machine.travel("2021-09-01 05:00:00 +00:00") as t:
create_mock_trades(fee) create_mock_trades(fee)
pm.refresh_pairlist() pm.refresh_pairlist()
assert pm.whitelist == ['XRP/BTC', 'ETH/BTC', 'TKN/BTC'] assert pm.whitelist == ['XRP/BTC']
assert log_has_re(r'Removing pair .* since .* is below .*', caplog)
# Move to "outside" of lookback window, so original sorting is restored. # Move to "outside" of lookback window, so original sorting is restored.
t.move_to("2021-09-01 07:00:00 +00:00") t.move_to("2021-09-01 07:00:00 +00:00")

View File

@ -1003,7 +1003,7 @@ def test_rpc_blacklist(mocker, default_conf) -> None:
assert len(ret['blacklist']) == 4 assert len(ret['blacklist']) == 4
assert ret['blacklist'] == default_conf['exchange']['pair_blacklist'] assert ret['blacklist'] == default_conf['exchange']['pair_blacklist']
assert ret['blacklist'] == ['DOGE/BTC', 'HOT/BTC', 'ETH/BTC', 'XRP/.*'] assert ret['blacklist'] == ['DOGE/BTC', 'HOT/BTC', 'ETH/BTC', 'XRP/.*']
assert ret['blacklist_expanded'] == ['ETH/BTC', 'XRP/BTC'] assert ret['blacklist_expanded'] == ['ETH/BTC', 'XRP/BTC', 'XRP/USDT']
assert 'errors' in ret assert 'errors' in ret
assert isinstance(ret['errors'], dict) assert isinstance(ret['errors'], dict)

View File

@ -95,7 +95,7 @@ def test_api_not_found(botclient):
assert rc.json() == {"detail": "Not Found"} assert rc.json() == {"detail": "Not Found"}
def test_api_ui_fallback(botclient): def test_api_ui_fallback(botclient, mocker):
ftbot, client = botclient ftbot, client = botclient
rc = client_get(client, "/favicon.ico") rc = client_get(client, "/favicon.ico")
@ -109,9 +109,16 @@ def test_api_ui_fallback(botclient):
rc = client_get(client, "/something") rc = client_get(client, "/something")
assert rc.status_code == 200 assert rc.status_code == 200
# Test directory traversal # Test directory traversal without mock
rc = client_get(client, '%2F%2F%2Fetc/passwd') rc = client_get(client, '%2F%2F%2Fetc/passwd')
assert rc.status_code == 200 assert rc.status_code == 200
# Allow both fallback or real UI
assert '`freqtrade install-ui`' in rc.text or '<!DOCTYPE html>' in rc.text
mocker.patch.object(Path, 'is_file', MagicMock(side_effect=[True, False]))
rc = client_get(client, '%2F%2F%2Fetc/passwd')
assert rc.status_code == 200
assert '`freqtrade install-ui`' in rc.text assert '`freqtrade install-ui`' in rc.text
@ -563,7 +570,6 @@ def test_api_trades(botclient, mocker, fee, markets):
assert rc.json()['total_trades'] == 0 assert rc.json()['total_trades'] == 0
create_mock_trades(fee) create_mock_trades(fee)
Trade.query.session.flush()
rc = client_get(client, f"{BASE_URI}/trades") rc = client_get(client, f"{BASE_URI}/trades")
assert_response(rc) assert_response(rc)
@ -590,7 +596,6 @@ def test_api_trade_single(botclient, mocker, fee, ticker, markets):
assert rc.json()['detail'] == 'Trade not found.' assert rc.json()['detail'] == 'Trade not found.'
create_mock_trades(fee) create_mock_trades(fee)
Trade.query.session.flush()
rc = client_get(client, f"{BASE_URI}/trade/3") rc = client_get(client, f"{BASE_URI}/trade/3")
assert_response(rc) assert_response(rc)
@ -613,10 +618,11 @@ def test_api_delete_trade(botclient, mocker, fee, markets):
assert_response(rc, 502) assert_response(rc, 502)
create_mock_trades(fee) create_mock_trades(fee)
Trade.query.session.flush()
ftbot.strategy.order_types['stoploss_on_exchange'] = True ftbot.strategy.order_types['stoploss_on_exchange'] = True
trades = Trade.query.all() trades = Trade.query.all()
trades[1].stoploss_order_id = '1234' trades[1].stoploss_order_id = '1234'
Trade.commit()
assert len(trades) > 2 assert len(trades) > 2
rc = client_delete(client, f"{BASE_URI}/trades/1") rc = client_delete(client, f"{BASE_URI}/trades/1")
@ -686,7 +692,6 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
assert rc.json() == {"error": "Error querying /api/v1/edge: Edge is not enabled."} assert rc.json() == {"error": "Error querying /api/v1/edge: Edge is not enabled."}
@pytest.mark.usefixtures("init_persistence")
def test_api_profit(botclient, mocker, ticker, fee, markets): def test_api_profit(botclient, mocker, ticker, fee, markets):
ftbot, client = botclient ftbot, client = botclient
patch_get_signal(ftbot) patch_get_signal(ftbot)
@ -737,7 +742,6 @@ def test_api_profit(botclient, mocker, ticker, fee, markets):
} }
@pytest.mark.usefixtures("init_persistence")
def test_api_stats(botclient, mocker, ticker, fee, markets,): def test_api_stats(botclient, mocker, ticker, fee, markets,):
ftbot, client = botclient ftbot, client = botclient
patch_get_signal(ftbot) patch_get_signal(ftbot)
@ -803,7 +807,7 @@ def test_api_performance(botclient, fee):
trade.close_profit_abs = trade.calc_profit() trade.close_profit_abs = trade.calc_profit()
Trade.query.session.add(trade) Trade.query.session.add(trade)
Trade.query.session.flush() Trade.commit()
rc = client_get(client, f"{BASE_URI}/performance") rc = client_get(client, f"{BASE_URI}/performance")
assert_response(rc) assert_response(rc)
@ -937,7 +941,7 @@ def test_api_blacklist(botclient, mocker):
data='{"blacklist": ["XRP/.*"]}') data='{"blacklist": ["XRP/.*"]}')
assert_response(rc) assert_response(rc)
assert rc.json() == {"blacklist": ["DOGE/BTC", "HOT/BTC", "ETH/BTC", "XRP/.*"], assert rc.json() == {"blacklist": ["DOGE/BTC", "HOT/BTC", "ETH/BTC", "XRP/.*"],
"blacklist_expanded": ["ETH/BTC", "XRP/BTC"], "blacklist_expanded": ["ETH/BTC", "XRP/BTC", "XRP/USDT"],
"length": 4, "length": 4,
"method": ["StaticPairList"], "method": ["StaticPairList"],
"errors": {}, "errors": {},
@ -1271,6 +1275,16 @@ def test_list_available_pairs(botclient):
assert len(rc.json()['pair_interval']) == 1 assert len(rc.json()['pair_interval']) == 1
def test_sysinfo(botclient):
ftbot, client = botclient
rc = client_get(client, f"{BASE_URI}/sysinfo")
assert_response(rc)
result = rc.json()
assert 'cpu_pct' in result
assert 'ram_pct' in result
def test_api_backtesting(botclient, mocker, fee, caplog): def test_api_backtesting(botclient, mocker, fee, caplog):
ftbot, client = botclient ftbot, client = botclient
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)

View File

@ -9,13 +9,13 @@ from freqtrade.strategy import (merge_informative_pair, stoploss_from_absolute,
timeframe_to_minutes) timeframe_to_minutes)
def generate_test_data(timeframe: str, size: int): def generate_test_data(timeframe: str, size: int, start: str = '2020-07-05'):
np.random.seed(42) np.random.seed(42)
tf_mins = timeframe_to_minutes(timeframe) tf_mins = timeframe_to_minutes(timeframe)
base = np.random.normal(20, 2, size=size) base = np.random.normal(20, 2, size=size)
date = pd.period_range('2020-07-05', periods=size, freq=f'{tf_mins}min').to_timestamp() date = pd.date_range(start, periods=size, freq=f'{tf_mins}min', tz='UTC')
df = pd.DataFrame({ df = pd.DataFrame({
'date': date, 'date': date,
'open': base, 'open': base,

View File

@ -62,8 +62,8 @@ def test_load_strategy(default_conf, result):
def test_load_strategy_base64(result, caplog, default_conf): def test_load_strategy_base64(result, caplog, default_conf):
with (Path(__file__).parents[2] / 'freqtrade/templates/sample_strategy.py').open("rb") as file: filepath = Path(__file__).parents[2] / 'freqtrade/templates/sample_strategy.py'
encoded_string = urlsafe_b64encode(file.read()).decode("utf-8") encoded_string = urlsafe_b64encode(filepath.read_bytes()).decode("utf-8")
default_conf.update({'strategy': 'SampleStrategy:{}'.format(encoded_string)}) default_conf.update({'strategy': 'SampleStrategy:{}'.format(encoded_string)})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)

File diff suppressed because it is too large Load Diff