Merge pull request #5805 from freqtrade/new_release
New release 2021.10
This commit is contained in:
commit
dadf015c23
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
|
@ -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).
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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).
|
||||||
|
16
docs/faq.md
16
docs/faq.md
@ -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
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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.
|
||||||
|
@ -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)
|
||||||
|
@ -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).
|
||||||
|
@ -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 .
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
""" Freqtrade bot """
|
""" Freqtrade bot """
|
||||||
__version__ = '2021.9'
|
__version__ = '2021.10'
|
||||||
|
|
||||||
if __version__ == 'develop':
|
if __version__ == 'develop':
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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',
|
||||||
|
@ -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']
|
||||||
},
|
},
|
||||||
|
@ -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",
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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,6 +136,14 @@ def get_ui_download_url() -> Tuple[str, str]:
|
|||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
r = resp.json()
|
r = resp.json()
|
||||||
|
|
||||||
|
if version:
|
||||||
|
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']
|
latest_version = r[0]['name']
|
||||||
assets = r[0].get('assets', [])
|
assets = r[0].get('assets', [])
|
||||||
dl_url = ''
|
dl_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'):
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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: {}')
|
||||||
|
|
||||||
|
@ -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',
|
||||||
|
@ -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.",
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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.')
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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,11 +16,18 @@ 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 "
|
|
||||||
|
def _format_exception_message(space: str, ignore_missing_space: bool) -> None:
|
||||||
|
msg = (f"The '{space}' space is included into the hyperoptimization "
|
||||||
f"but no parameter for this space was not found in your Strategy. "
|
f"but no parameter for this space was not found in your Strategy. "
|
||||||
f"Please make sure to have parameters for this space enabled for optimization "
|
)
|
||||||
|
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.")
|
f"or remove the '{space}' space from hyperoptimization.")
|
||||||
|
|
||||||
|
|
||||||
@ -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')
|
||||||
|
41
freqtrade/optimize/hyperopt_loss_max_drawdown.py
Normal file
41
freqtrade/optimize/hyperopt_loss_max_drawdown.py
Normal 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]
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import io
|
import io
|
||||||
import logging
|
import logging
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
@ -64,7 +63,8 @@ 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:
|
||||||
|
rapidjson.dump(final_params, f, indent=2,
|
||||||
default=hyperopt_serializer,
|
default=hyperopt_serializer,
|
||||||
number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN
|
number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
@ -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]
|
||||||
|
@ -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
|
||||||
|
@ -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_
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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')
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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": [
|
||||||
|
@ -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": [
|
||||||
|
|
||||||
],
|
],
|
||||||
|
@ -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": [
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
10
freqtrade/vendor/qtpylib/indicators.py
vendored
10
freqtrade/vendor/qtpylib/indicators.py
vendored
@ -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))
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------
|
# ---------------------------------------------
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
2
setup.py
2
setup.py
@ -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',
|
||||||
|
10
setup.sh
10
setup.sh
@ -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() {
|
||||||
|
@ -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.2',
|
||||||
|
'assets': [{'browser_download_url': 'http://download22.zip'}]
|
||||||
|
},
|
||||||
|
{
|
||||||
'assets_url': 'http://whatever.json',
|
'assets_url': 'http://whatever.json',
|
||||||
'name': '0.0.1',
|
'name': '0.0.1',
|
||||||
'assets': [{'browser_download_url': 'http://download11.zip'}]}]])
|
'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 = [
|
||||||
|
@ -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 [{
|
||||||
|
'info': {
|
||||||
|
'id': 34567,
|
||||||
'orderId': 123456,
|
'orderId': 123456,
|
||||||
'price': '0.24544100',
|
'price': '2.0',
|
||||||
'qty': '8.00000000',
|
'qty': '8.00000000',
|
||||||
'commission': '0.00800000',
|
'commission': '0.00800000',
|
||||||
'commissionAsset': 'LTC',
|
'commissionAsset': 'LTC',
|
||||||
'time': 1521663363189,
|
'time': 1521663363189,
|
||||||
'isBuyer': True,
|
'isBuyer': True,
|
||||||
'isMaker': False,
|
'isMaker': False,
|
||||||
'isBestMatch': True},
|
'isBestMatch': True
|
||||||
|
},
|
||||||
'timestamp': 1521663363189,
|
'timestamp': 1521663363189,
|
||||||
'datetime': '2018-03-21T20:16:03.189Z',
|
'datetime': '2018-03-21T20:16:03.189Z',
|
||||||
'symbol': 'LTC/ETH',
|
'symbol': 'LTC/USDT',
|
||||||
'id': '34567',
|
'id': '34567',
|
||||||
'order': '123456',
|
'order': '123456',
|
||||||
'type': None,
|
'type': None,
|
||||||
'side': 'buy',
|
'side': 'buy',
|
||||||
'price': 0.245441,
|
'price': 2.0,
|
||||||
'cost': 1.963528,
|
'cost': 16.0,
|
||||||
'amount': 8.0,
|
'amount': 8.0,
|
||||||
'fee': {'cost': 0.008, 'currency': 'LTC'}}]
|
'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',
|
||||||
|
305
tests/conftest_trades_usdt.py
Normal file
305
tests/conftest_trades_usdt.py
Normal 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
|
@ -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,6 +1853,9 @@ 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)
|
||||||
|
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']['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)
|
||||||
@ -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,12 +1892,14 @@ 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
|
||||||
|
if last_ab is not None:
|
||||||
default_conf['ask_strategy']['bid_last_balance'] = last_ab
|
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})
|
||||||
@ -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)
|
||||||
|
28
tests/exchange/test_gateio.py
Normal file
28
tests/exchange/test_gateio.py
Normal 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)
|
@ -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),
|
||||||
|
],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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"]
|
||||||
|
@ -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
|
||||||
|
@ -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')
|
||||||
|
@ -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")
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
|
@ -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
Loading…
Reference in New Issue
Block a user