commit
d0d634260f
@ -82,7 +82,8 @@ positional arguments:
|
|||||||
new-hyperopt Create new hyperopt
|
new-hyperopt Create new hyperopt
|
||||||
new-strategy Create new strategy
|
new-strategy Create new strategy
|
||||||
download-data Download backtesting data.
|
download-data Download backtesting data.
|
||||||
convert-data Convert candle (OHLCV) data from one format to another.
|
convert-data Convert candle (OHLCV) data from one format to
|
||||||
|
another.
|
||||||
convert-trade-data Convert trade data from one format to another.
|
convert-trade-data Convert trade data from one format to another.
|
||||||
backtesting Backtesting module.
|
backtesting Backtesting module.
|
||||||
edge Edge module.
|
edge Edge module.
|
||||||
@ -94,7 +95,7 @@ positional arguments:
|
|||||||
list-markets Print markets on exchange.
|
list-markets Print markets on exchange.
|
||||||
list-pairs Print pairs on exchange.
|
list-pairs Print pairs on exchange.
|
||||||
list-strategies Print available strategies.
|
list-strategies Print available strategies.
|
||||||
list-timeframes Print available ticker intervals (timeframes) for the exchange.
|
list-timeframes Print available timeframes for the exchange.
|
||||||
show-trades Show trades.
|
show-trades Show trades.
|
||||||
test-pairlist Test your pairlist configuration.
|
test-pairlist Test your pairlist configuration.
|
||||||
plot-dataframe Plot candles with indicators.
|
plot-dataframe Plot candles with indicators.
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
"stake_amount": 0.05,
|
"stake_amount": 0.05,
|
||||||
"tradable_balance_ratio": 0.99,
|
"tradable_balance_ratio": 0.99,
|
||||||
"fiat_display_currency": "USD",
|
"fiat_display_currency": "USD",
|
||||||
"ticker_interval": "5m",
|
"timeframe": "5m",
|
||||||
"dry_run": false,
|
"dry_run": false,
|
||||||
"cancel_open_orders_on_exit": false,
|
"cancel_open_orders_on_exit": false,
|
||||||
"trailing_stop": false,
|
"trailing_stop": false,
|
||||||
@ -76,6 +76,16 @@
|
|||||||
"token": "your_telegram_token",
|
"token": "your_telegram_token",
|
||||||
"chat_id": "your_telegram_chat_id"
|
"chat_id": "your_telegram_chat_id"
|
||||||
},
|
},
|
||||||
|
"api_server": {
|
||||||
|
"enabled": false,
|
||||||
|
"listen_ip_address": "127.0.0.1",
|
||||||
|
"listen_port": 8080,
|
||||||
|
"verbosity": "info",
|
||||||
|
"jwt_secret_key": "somethingrandom",
|
||||||
|
"CORS_origins": [],
|
||||||
|
"username": "",
|
||||||
|
"password": ""
|
||||||
|
},
|
||||||
"initial_state": "running",
|
"initial_state": "running",
|
||||||
"forcebuy_enable": false,
|
"forcebuy_enable": false,
|
||||||
"internals": {
|
"internals": {
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
"stake_amount": 0.05,
|
"stake_amount": 0.05,
|
||||||
"tradable_balance_ratio": 0.99,
|
"tradable_balance_ratio": 0.99,
|
||||||
"fiat_display_currency": "USD",
|
"fiat_display_currency": "USD",
|
||||||
"ticker_interval": "5m",
|
"timeframe": "5m",
|
||||||
"dry_run": true,
|
"dry_run": true,
|
||||||
"cancel_open_orders_on_exit": false,
|
"cancel_open_orders_on_exit": false,
|
||||||
"trailing_stop": false,
|
"trailing_stop": false,
|
||||||
@ -81,6 +81,16 @@
|
|||||||
"token": "your_telegram_token",
|
"token": "your_telegram_token",
|
||||||
"chat_id": "your_telegram_chat_id"
|
"chat_id": "your_telegram_chat_id"
|
||||||
},
|
},
|
||||||
|
"api_server": {
|
||||||
|
"enabled": false,
|
||||||
|
"listen_ip_address": "127.0.0.1",
|
||||||
|
"listen_port": 8080,
|
||||||
|
"verbosity": "info",
|
||||||
|
"jwt_secret_key": "somethingrandom",
|
||||||
|
"CORS_origins": [],
|
||||||
|
"username": "",
|
||||||
|
"password": ""
|
||||||
|
},
|
||||||
"initial_state": "running",
|
"initial_state": "running",
|
||||||
"forcebuy_enable": false,
|
"forcebuy_enable": false,
|
||||||
"internals": {
|
"internals": {
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
"last_stake_amount_min_ratio": 0.5,
|
"last_stake_amount_min_ratio": 0.5,
|
||||||
"dry_run": false,
|
"dry_run": false,
|
||||||
"cancel_open_orders_on_exit": false,
|
"cancel_open_orders_on_exit": false,
|
||||||
"ticker_interval": "5m",
|
"timeframe": "5m",
|
||||||
"trailing_stop": false,
|
"trailing_stop": false,
|
||||||
"trailing_stop_positive": 0.005,
|
"trailing_stop_positive": 0.005,
|
||||||
"trailing_stop_positive_offset": 0.0051,
|
"trailing_stop_positive_offset": 0.0051,
|
||||||
@ -64,6 +64,7 @@
|
|||||||
"sort_key": "quoteVolume",
|
"sort_key": "quoteVolume",
|
||||||
"refresh_period": 1800
|
"refresh_period": 1800
|
||||||
},
|
},
|
||||||
|
{"method": "AgeFilter", "min_days_listed": 10},
|
||||||
{"method": "PrecisionFilter"},
|
{"method": "PrecisionFilter"},
|
||||||
{"method": "PriceFilter", "low_price_ratio": 0.01},
|
{"method": "PriceFilter", "low_price_ratio": 0.01},
|
||||||
{"method": "SpreadFilter", "max_spread_ratio": 0.005}
|
{"method": "SpreadFilter", "max_spread_ratio": 0.005}
|
||||||
@ -121,7 +122,9 @@
|
|||||||
"enabled": false,
|
"enabled": false,
|
||||||
"listen_ip_address": "127.0.0.1",
|
"listen_ip_address": "127.0.0.1",
|
||||||
"listen_port": 8080,
|
"listen_port": 8080,
|
||||||
|
"verbosity": "info",
|
||||||
"jwt_secret_key": "somethingrandom",
|
"jwt_secret_key": "somethingrandom",
|
||||||
|
"CORS_origins": [],
|
||||||
"username": "freqtrader",
|
"username": "freqtrader",
|
||||||
"password": "SuperSecurePassword"
|
"password": "SuperSecurePassword"
|
||||||
},
|
},
|
||||||
@ -132,6 +135,7 @@
|
|||||||
"process_throttle_secs": 5,
|
"process_throttle_secs": 5,
|
||||||
"heartbeat_interval": 60
|
"heartbeat_interval": 60
|
||||||
},
|
},
|
||||||
|
"disable_dataframe_checks": false,
|
||||||
"strategy": "DefaultStrategy",
|
"strategy": "DefaultStrategy",
|
||||||
"strategy_path": "user_data/strategies/",
|
"strategy_path": "user_data/strategies/",
|
||||||
"dataformat_ohlcv": "json",
|
"dataformat_ohlcv": "json",
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
"stake_amount": 10,
|
"stake_amount": 10,
|
||||||
"tradable_balance_ratio": 0.99,
|
"tradable_balance_ratio": 0.99,
|
||||||
"fiat_display_currency": "EUR",
|
"fiat_display_currency": "EUR",
|
||||||
"ticker_interval": "5m",
|
"timeframe": "5m",
|
||||||
"dry_run": true,
|
"dry_run": true,
|
||||||
"cancel_open_orders_on_exit": false,
|
"cancel_open_orders_on_exit": false,
|
||||||
"trailing_stop": false,
|
"trailing_stop": false,
|
||||||
@ -87,6 +87,16 @@
|
|||||||
"token": "your_telegram_token",
|
"token": "your_telegram_token",
|
||||||
"chat_id": "your_telegram_chat_id"
|
"chat_id": "your_telegram_chat_id"
|
||||||
},
|
},
|
||||||
|
"api_server": {
|
||||||
|
"enabled": false,
|
||||||
|
"listen_ip_address": "127.0.0.1",
|
||||||
|
"listen_port": 8080,
|
||||||
|
"verbosity": "info",
|
||||||
|
"jwt_secret_key": "somethingrandom",
|
||||||
|
"CORS_origins": [],
|
||||||
|
"username": "",
|
||||||
|
"password": ""
|
||||||
|
},
|
||||||
"initial_state": "running",
|
"initial_state": "running",
|
||||||
"forcebuy_enable": false,
|
"forcebuy_enable": false,
|
||||||
"internals": {
|
"internals": {
|
||||||
|
@ -63,8 +63,8 @@ class SuperDuperHyperOptLoss(IHyperOptLoss):
|
|||||||
* 0.25: Avoiding trade loss
|
* 0.25: Avoiding trade loss
|
||||||
* 1.0 to total profit, compared to the expected value (`EXPECTED_MAX_PROFIT`) defined above
|
* 1.0 to total profit, compared to the expected value (`EXPECTED_MAX_PROFIT`) defined above
|
||||||
"""
|
"""
|
||||||
total_profit = results.profit_percent.sum()
|
total_profit = results['profit_percent'].sum()
|
||||||
trade_duration = results.trade_duration.mean()
|
trade_duration = results['trade_duration'].mean()
|
||||||
|
|
||||||
trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8)
|
trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8)
|
||||||
profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT)
|
profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT)
|
||||||
|
@ -4,6 +4,54 @@ This page explains some advanced tasks and configuration options that can be per
|
|||||||
|
|
||||||
If you do not know what things mentioned here mean, you probably do not need it.
|
If you do not know what things mentioned here mean, you probably do not need it.
|
||||||
|
|
||||||
|
## Running multiple instances of Freqtrade
|
||||||
|
|
||||||
|
This section will show you how to run multiple bots at the same time, on the same machine.
|
||||||
|
|
||||||
|
### Things to consider
|
||||||
|
|
||||||
|
* Use different database files.
|
||||||
|
* Use different Telegram bots (requires multiple different configuration files; applies only when Telegram is enabled).
|
||||||
|
* Use different ports (applies only when Freqtrade REST API webserver is enabled).
|
||||||
|
|
||||||
|
### Different database files
|
||||||
|
|
||||||
|
In order to keep track of your trades, profits, etc., freqtrade is using a SQLite database where it stores various types of information such as the trades you performed in the past and the current position(s) you are holding at any time. This allows you to keep track of your profits, but most importantly, keep track of ongoing activity if the bot process would be restarted or would be terminated unexpectedly.
|
||||||
|
|
||||||
|
Freqtrade will, by default, use separate database files for dry-run and live bots (this assumes no database-url is given in either configuration nor via command line argument).
|
||||||
|
For live trading mode, the default database will be `tradesv3.sqlite` and for dry-run it will be `tradesv3.dryrun.sqlite`.
|
||||||
|
|
||||||
|
The optional argument to the trade command used to specify the path of these files is `--db-url`, which requires a valid SQLAlchemy url.
|
||||||
|
So when you are starting a bot with only the config and strategy arguments in dry-run mode, the following 2 commands would have the same outcome.
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
freqtrade trade -c MyConfig.json -s MyStrategy
|
||||||
|
# is equivalent to
|
||||||
|
freqtrade trade -c MyConfig.json -s MyStrategy --db-url sqlite:///tradesv3.dryrun.sqlite
|
||||||
|
```
|
||||||
|
|
||||||
|
It means that if you are running the trade command in two different terminals, for example to test your strategy both for trades in USDT and in another instance for trades in BTC, you will have to run them with different databases.
|
||||||
|
|
||||||
|
If you specify the URL of a database which does not exist, freqtrade will create one with the name you specified. So to test your custom strategy with BTC and USDT stake currencies, you could use the following commands (in 2 separate terminals):
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
# Terminal 1:
|
||||||
|
freqtrade trade -c MyConfigBTC.json -s MyCustomStrategy --db-url sqlite:///user_data/tradesBTC.dryrun.sqlite
|
||||||
|
# Terminal 2:
|
||||||
|
freqtrade trade -c MyConfigUSDT.json -s MyCustomStrategy --db-url sqlite:///user_data/tradesUSDT.dryrun.sqlite
|
||||||
|
```
|
||||||
|
|
||||||
|
Conversely, if you wish to do the same thing in production mode, you will also have to create at least one new database (in addition to the default one) and specify the path to the "live" databases, for example:
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
# Terminal 1:
|
||||||
|
freqtrade trade -c MyConfigBTC.json -s MyCustomStrategy --db-url sqlite:///user_data/tradesBTC.live.sqlite
|
||||||
|
# Terminal 2:
|
||||||
|
freqtrade trade -c MyConfigUSDT.json -s MyCustomStrategy --db-url sqlite:///user_data/tradesUSDT.live.sqlite
|
||||||
|
```
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
## 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.
|
||||||
|
@ -12,7 +12,7 @@ real data. This is what we call
|
|||||||
[backtesting](https://en.wikipedia.org/wiki/Backtesting).
|
[backtesting](https://en.wikipedia.org/wiki/Backtesting).
|
||||||
|
|
||||||
Backtesting will use the crypto-currencies (pairs) from your config file and load historical candle (OHCLV) data from `user_data/data/<exchange>` by default.
|
Backtesting will use the crypto-currencies (pairs) from your config file and load historical candle (OHCLV) data from `user_data/data/<exchange>` by default.
|
||||||
If no data is available for the exchange / pair / timeframe (ticker interval) combination, backtesting will ask you to download them first using `freqtrade download-data`.
|
If no data is available for the exchange / pair / timeframe combination, backtesting will ask you to download them first using `freqtrade download-data`.
|
||||||
For details on downloading, please refer to the [Data Downloading](data-download.md) section in the documentation.
|
For details on downloading, please refer to the [Data Downloading](data-download.md) section in the documentation.
|
||||||
|
|
||||||
The result of backtesting will confirm if your bot has better odds of making a profit than a loss.
|
The result of backtesting will confirm if your bot has better odds of making a profit than a loss.
|
||||||
@ -35,7 +35,7 @@ freqtrade backtesting
|
|||||||
#### With 1 min candle (OHLCV) data
|
#### With 1 min candle (OHLCV) data
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
freqtrade backtesting --ticker-interval 1m
|
freqtrade backtesting --timeframe 1m
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Using a different on-disk historical candle (OHLCV) data source
|
#### Using a different on-disk historical candle (OHLCV) data source
|
||||||
@ -58,7 +58,7 @@ Where `-s SampleStrategy` refers to the class name within the strategy file `sam
|
|||||||
#### Comparing multiple Strategies
|
#### Comparing multiple Strategies
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
freqtrade backtesting --strategy-list SampleStrategy1 AwesomeStrategy --ticker-interval 5m
|
freqtrade backtesting --strategy-list SampleStrategy1 AwesomeStrategy --timeframe 5m
|
||||||
```
|
```
|
||||||
|
|
||||||
Where `SampleStrategy1` and `AwesomeStrategy` refer to class names of strategies.
|
Where `SampleStrategy1` and `AwesomeStrategy` refer to class names of strategies.
|
||||||
@ -228,13 +228,13 @@ You can then load the trades to perform further analysis as shown in our [data a
|
|||||||
|
|
||||||
To compare multiple strategies, a list of Strategies can be provided to backtesting.
|
To compare multiple strategies, a list of Strategies can be provided to backtesting.
|
||||||
|
|
||||||
This is limited to 1 timeframe (ticker interval) value per run. However, data is only loaded once from disk so if you have multiple
|
This is limited to 1 timeframe value per run. However, data is only loaded once from disk so if you have multiple
|
||||||
strategies you'd like to compare, this will give a nice runtime boost.
|
strategies you'd like to compare, this will give a nice runtime boost.
|
||||||
|
|
||||||
All listed Strategies need to be in the same directory.
|
All listed Strategies need to be in the same directory.
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
freqtrade backtesting --timerange 20180401-20180410 --ticker-interval 5m --strategy-list Strategy001 Strategy002 --export trades
|
freqtrade backtesting --timerange 20180401-20180410 --timeframe 5m --strategy-list Strategy001 Strategy002 --export trades
|
||||||
```
|
```
|
||||||
|
|
||||||
This will save the results to `user_data/backtest_results/backtest-result-<strategy>.json`, injecting the strategy-name into the target filename.
|
This will save the results to `user_data/backtest_results/backtest-result-<strategy>.json`, injecting the strategy-name into the target filename.
|
||||||
|
@ -9,22 +9,35 @@ This page explains the different parameters of the bot and how to run it.
|
|||||||
|
|
||||||
```
|
```
|
||||||
usage: freqtrade [-h] [-V]
|
usage: freqtrade [-h] [-V]
|
||||||
{trade,backtesting,edge,hyperopt,create-userdir,list-exchanges,list-timeframes,download-data,plot-dataframe,plot-profit}
|
{trade,create-userdir,new-config,new-hyperopt,new-strategy,download-data,convert-data,convert-trade-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,plot-dataframe,plot-profit}
|
||||||
...
|
...
|
||||||
|
|
||||||
Free, open source crypto trading bot
|
Free, open source crypto trading bot
|
||||||
|
|
||||||
positional arguments:
|
positional arguments:
|
||||||
{trade,backtesting,edge,hyperopt,create-userdir,list-exchanges,list-timeframes,download-data,plot-dataframe,plot-profit}
|
{trade,create-userdir,new-config,new-hyperopt,new-strategy,download-data,convert-data,convert-trade-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,plot-dataframe,plot-profit}
|
||||||
trade Trade module.
|
trade Trade module.
|
||||||
|
create-userdir Create user-data directory.
|
||||||
|
new-config Create new config
|
||||||
|
new-hyperopt Create new hyperopt
|
||||||
|
new-strategy Create new strategy
|
||||||
|
download-data Download backtesting data.
|
||||||
|
convert-data Convert candle (OHLCV) data from one format to
|
||||||
|
another.
|
||||||
|
convert-trade-data Convert trade data from one format to another.
|
||||||
backtesting Backtesting module.
|
backtesting Backtesting module.
|
||||||
edge Edge module.
|
edge Edge module.
|
||||||
hyperopt Hyperopt module.
|
hyperopt Hyperopt module.
|
||||||
create-userdir Create user-data directory.
|
hyperopt-list List Hyperopt results
|
||||||
|
hyperopt-show Show details of Hyperopt results
|
||||||
list-exchanges Print available exchanges.
|
list-exchanges Print available exchanges.
|
||||||
list-timeframes Print available ticker intervals (timeframes) for the
|
list-hyperopts Print available hyperopt classes.
|
||||||
exchange.
|
list-markets Print markets on exchange.
|
||||||
download-data Download backtesting data.
|
list-pairs Print pairs on exchange.
|
||||||
|
list-strategies Print available strategies.
|
||||||
|
list-timeframes Print available timeframes for the exchange.
|
||||||
|
show-trades Show trades.
|
||||||
|
test-pairlist Test your pairlist configuration.
|
||||||
plot-dataframe Plot candles with indicators.
|
plot-dataframe Plot candles with indicators.
|
||||||
plot-profit Generate plot showing profits.
|
plot-profit Generate plot showing profits.
|
||||||
|
|
||||||
@ -72,7 +85,6 @@ Strategy arguments:
|
|||||||
Specify strategy class name which will be used by the
|
Specify strategy class name which will be used by the
|
||||||
bot.
|
bot.
|
||||||
--strategy-path PATH Specify additional strategy lookup path.
|
--strategy-path PATH Specify additional strategy lookup path.
|
||||||
.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -197,7 +209,7 @@ Backtesting also uses the config specified via `-c/--config`.
|
|||||||
```
|
```
|
||||||
usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||||
[-d PATH] [--userdir PATH] [-s NAME]
|
[-d PATH] [--userdir PATH] [-s NAME]
|
||||||
[--strategy-path PATH] [-i TICKER_INTERVAL]
|
[--strategy-path PATH] [-i TIMEFRAME]
|
||||||
[--timerange TIMERANGE] [--max-open-trades INT]
|
[--timerange TIMERANGE] [--max-open-trades INT]
|
||||||
[--stake-amount STAKE_AMOUNT] [--fee FLOAT]
|
[--stake-amount STAKE_AMOUNT] [--fee FLOAT]
|
||||||
[--eps] [--dmmp]
|
[--eps] [--dmmp]
|
||||||
@ -206,7 +218,7 @@ usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
|||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL
|
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
|
||||||
Specify ticker interval (`1m`, `5m`, `30m`, `1h`,
|
Specify ticker interval (`1m`, `5m`, `30m`, `1h`,
|
||||||
`1d`).
|
`1d`).
|
||||||
--timerange TIMERANGE
|
--timerange TIMERANGE
|
||||||
@ -280,7 +292,7 @@ to find optimal parameter values for your strategy.
|
|||||||
```
|
```
|
||||||
usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
||||||
[--userdir PATH] [-s NAME] [--strategy-path PATH]
|
[--userdir PATH] [-s NAME] [--strategy-path PATH]
|
||||||
[-i TICKER_INTERVAL] [--timerange TIMERANGE]
|
[-i TIMEFRAME] [--timerange TIMERANGE]
|
||||||
[--max-open-trades INT]
|
[--max-open-trades INT]
|
||||||
[--stake-amount STAKE_AMOUNT] [--fee FLOAT]
|
[--stake-amount STAKE_AMOUNT] [--fee FLOAT]
|
||||||
[--hyperopt NAME] [--hyperopt-path PATH] [--eps]
|
[--hyperopt NAME] [--hyperopt-path PATH] [--eps]
|
||||||
@ -292,7 +304,7 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
|||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL
|
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
|
||||||
Specify ticker interval (`1m`, `5m`, `30m`, `1h`,
|
Specify ticker interval (`1m`, `5m`, `30m`, `1h`,
|
||||||
`1d`).
|
`1d`).
|
||||||
--timerange TIMERANGE
|
--timerange TIMERANGE
|
||||||
@ -323,7 +335,7 @@ optional arguments:
|
|||||||
--print-all Print all results, not only the best ones.
|
--print-all Print all results, not only the best ones.
|
||||||
--no-color Disable colorization of hyperopt results. May be
|
--no-color Disable colorization of hyperopt results. May be
|
||||||
useful if you are redirecting output to a file.
|
useful if you are redirecting output to a file.
|
||||||
--print-json Print best results in JSON format.
|
--print-json Print output in JSON format.
|
||||||
-j JOBS, --job-workers JOBS
|
-j JOBS, --job-workers JOBS
|
||||||
The number of concurrently running jobs for
|
The number of concurrently running jobs for
|
||||||
hyperoptimization (hyperopt worker processes). If -1
|
hyperoptimization (hyperopt worker processes). If -1
|
||||||
@ -341,11 +353,11 @@ optional arguments:
|
|||||||
class (IHyperOptLoss). Different functions can
|
class (IHyperOptLoss). Different functions can
|
||||||
generate completely different results, since the
|
generate completely different results, since the
|
||||||
target for optimization is different. Built-in
|
target for optimization is different. Built-in
|
||||||
Hyperopt-loss-functions are:
|
Hyperopt-loss-functions are: DefaultHyperOptLoss,
|
||||||
DefaultHyperOptLoss, OnlyProfitHyperOptLoss,
|
OnlyProfitHyperOptLoss, SharpeHyperOptLoss,
|
||||||
SharpeHyperOptLoss, SharpeHyperOptLossDaily,
|
SharpeHyperOptLossDaily, SortinoHyperOptLoss,
|
||||||
SortinoHyperOptLoss, SortinoHyperOptLossDaily.
|
SortinoHyperOptLossDaily.(default:
|
||||||
(default: `DefaultHyperOptLoss`).
|
`DefaultHyperOptLoss`).
|
||||||
|
|
||||||
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).
|
||||||
@ -378,13 +390,13 @@ To know your trade expectancy and winrate against historical data, you can use E
|
|||||||
```
|
```
|
||||||
usage: freqtrade edge [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
usage: freqtrade edge [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
||||||
[--userdir PATH] [-s NAME] [--strategy-path PATH]
|
[--userdir PATH] [-s NAME] [--strategy-path PATH]
|
||||||
[-i TICKER_INTERVAL] [--timerange TIMERANGE]
|
[-i TIMEFRAME] [--timerange TIMERANGE]
|
||||||
[--max-open-trades INT] [--stake-amount STAKE_AMOUNT]
|
[--max-open-trades INT] [--stake-amount STAKE_AMOUNT]
|
||||||
[--fee FLOAT] [--stoplosses STOPLOSS_RANGE]
|
[--fee FLOAT] [--stoplosses STOPLOSS_RANGE]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL
|
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
|
||||||
Specify ticker interval (`1m`, `5m`, `30m`, `1h`,
|
Specify ticker interval (`1m`, `5m`, `30m`, `1h`,
|
||||||
`1d`).
|
`1d`).
|
||||||
--timerange TIMERANGE
|
--timerange TIMERANGE
|
||||||
|
@ -47,14 +47,14 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
| `amend_last_stake_amount` | Use reduced last stake amount if necessary. [More information below](#configuring-amount-per-trade). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
| `amend_last_stake_amount` | Use reduced last stake amount if necessary. [More information below](#configuring-amount-per-trade). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||||
| `last_stake_amount_min_ratio` | Defines minimum stake amount that has to be left and executed. Applies only to the last stake amount when it's amended to a reduced value (i.e. if `amend_last_stake_amount` is set to `true`). [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.5`.* <br> **Datatype:** Float (as ratio)
|
| `last_stake_amount_min_ratio` | Defines minimum stake amount that has to be left and executed. Applies only to the last stake amount when it's amended to a reduced value (i.e. if `amend_last_stake_amount` is set to `true`). [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.5`.* <br> **Datatype:** Float (as ratio)
|
||||||
| `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stoploss value when calculating min pair stake amount in order to avoid possible trade refusals. <br>*Defaults to `0.05` (5%).* <br> **Datatype:** Positive Float as ratio.
|
| `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stoploss value when calculating min pair stake amount in order to avoid possible trade refusals. <br>*Defaults to `0.05` (5%).* <br> **Datatype:** Positive Float as ratio.
|
||||||
| `ticker_interval` | The timeframe (ticker interval) to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** String
|
| `timeframe` | The timeframe (former ticker interval) to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** String
|
||||||
| `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency). <br> **Datatype:** String
|
| `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency). <br> **Datatype:** String
|
||||||
| `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
| `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
||||||
| `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in the Dry Run mode.<br>*Defaults to `1000`.* <br> **Datatype:** Float
|
| `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in the Dry Run mode.<br>*Defaults to `1000`.* <br> **Datatype:** Float
|
||||||
| `cancel_open_orders_on_exit` | Cancel open orders when the `/stop` RPC command is issued, `Ctrl+C` is pressed or the bot dies unexpectedly. When set to `true`, this allows you to use `/stop` to cancel unfilled and partially filled orders in the event of a market crash. It does not impact open positions. <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
| `cancel_open_orders_on_exit` | Cancel open orders when the `/stop` RPC command is issued, `Ctrl+C` is pressed or the bot dies unexpectedly. When set to `true`, this allows you to use `/stop` to cancel unfilled and partially filled orders in the event of a market crash. It does not impact open positions. <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||||
| `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
| `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||||
| `minimal_roi` | **Required.** Set the threshold in percent the bot will use to sell a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Dict
|
| `minimal_roi` | **Required.** Set the threshold as ratio the bot will use to sell a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Dict
|
||||||
| `stoploss` | **Required.** Value of the stoploss in percent used by the bot. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Float (as ratio)
|
| `stoploss` | **Required.** Value as ratio of the stoploss used by the bot. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Float (as ratio)
|
||||||
| `trailing_stop` | Enables trailing stoploss (based on `stoploss` in either configuration or strategy file). More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Boolean
|
| `trailing_stop` | Enables trailing stoploss (based on `stoploss` in either configuration or strategy file). More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Boolean
|
||||||
| `trailing_stop_positive` | Changes stoploss once profit has been reached. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Float
|
| `trailing_stop_positive` | Changes stoploss once profit has been reached. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Float
|
||||||
| `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0` (no offset).* <br> **Datatype:** Float
|
| `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0` (no offset).* <br> **Datatype:** Float
|
||||||
@ -83,7 +83,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
| `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
| `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||||
| `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Not used by VolumePairList (see [below](#pairlists-and-pairlist-handlers)). <br> **Datatype:** List
|
| `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Not used by VolumePairList (see [below](#pairlists-and-pairlist-handlers)). <br> **Datatype:** List
|
||||||
| `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting (see [below](#pairlists-and-pairlist-handlers)). <br> **Datatype:** List
|
| `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting (see [below](#pairlists-and-pairlist-handlers)). <br> **Datatype:** List
|
||||||
| `exchange.ccxt_config` | Additional CCXT parameters passed to the regular ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict
|
| `exchange.ccxt_config` | Additional CCXT parameters passed to both ccxt instances (sync and async). This is usually the correct place for ccxt configurations. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict
|
||||||
|
| `exchange.ccxt_sync_config` | Additional CCXT parameters passed to the regular (sync) ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict
|
||||||
| `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict
|
| `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict
|
||||||
| `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded. <br>*Defaults to `60` minutes.* <br> **Datatype:** Positive Integer
|
| `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded. <br>*Defaults to `60` minutes.* <br> **Datatype:** Positive Integer
|
||||||
| `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation.
|
| `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation.
|
||||||
@ -102,11 +103,13 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
| `api_server.enabled` | Enable usage of API Server. See the [API Server documentation](rest-api.md) for more details. <br> **Datatype:** Boolean
|
| `api_server.enabled` | Enable usage of API Server. See the [API Server documentation](rest-api.md) for more details. <br> **Datatype:** Boolean
|
||||||
| `api_server.listen_ip_address` | Bind IP address. See the [API Server documentation](rest-api.md) for more details. <br> **Datatype:** IPv4
|
| `api_server.listen_ip_address` | Bind IP address. See the [API Server documentation](rest-api.md) for more details. <br> **Datatype:** IPv4
|
||||||
| `api_server.listen_port` | Bind Port. See the [API Server documentation](rest-api.md) for more details. <br>**Datatype:** Integer between 1024 and 65535
|
| `api_server.listen_port` | Bind Port. See the [API Server documentation](rest-api.md) for more details. <br>**Datatype:** Integer between 1024 and 65535
|
||||||
|
| `api_server.verbosity` | Logging verbosity. `info` will print all RPC Calls, while "error" will only display errors. <br>**Datatype:** Enum, either `info` or `error`. Defaults to `info`.
|
||||||
| `api_server.username` | Username for API server. See the [API Server documentation](rest-api.md) for more details. <br>**Keep it in secret, do not disclose publicly.**<br> **Datatype:** String
|
| `api_server.username` | Username for API server. See the [API Server documentation](rest-api.md) for more details. <br>**Keep it in secret, do not disclose publicly.**<br> **Datatype:** String
|
||||||
| `api_server.password` | Password for API server. See the [API Server documentation](rest-api.md) for more details. <br>**Keep it in secret, do not disclose publicly.**<br> **Datatype:** String
|
| `api_server.password` | Password for API server. See the [API Server documentation](rest-api.md) for more details. <br>**Keep it in secret, do not disclose publicly.**<br> **Datatype:** String
|
||||||
| `db_url` | Declares database URL to use. NOTE: This defaults to `sqlite:///tradesv3.dryrun.sqlite` if `dry_run` is `true`, and to `sqlite:///tradesv3.sqlite` for production instances. <br> **Datatype:** String, SQLAlchemy connect string
|
| `db_url` | Declares database URL to use. NOTE: This defaults to `sqlite:///tradesv3.dryrun.sqlite` if `dry_run` is `true`, and to `sqlite:///tradesv3.sqlite` for production instances. <br> **Datatype:** String, SQLAlchemy connect string
|
||||||
| `initial_state` | Defines the initial application state. More information below. <br>*Defaults to `stopped`.* <br> **Datatype:** Enum, either `stopped` or `running`
|
| `initial_state` | Defines the initial application state. More information below. <br>*Defaults to `stopped`.* <br> **Datatype:** Enum, either `stopped` or `running`
|
||||||
| `forcebuy_enable` | Enables the RPC Commands to force a buy. More information below. <br> **Datatype:** Boolean
|
| `forcebuy_enable` | Enables the RPC Commands to force a buy. More information below. <br> **Datatype:** Boolean
|
||||||
|
| `disable_dataframe_checks` | Disable checking the OHLCV dataframe returned from the strategy methods for correctness. Only use when intentionally changing the dataframe and understand what you are doing. [Strategy Override](#parameters-in-the-strategy).<br> *Defaults to `False`*. <br> **Datatype:** Boolean
|
||||||
| `strategy` | **Required** Defines Strategy class to use. Recommended to be set via `--strategy NAME`. <br> **Datatype:** ClassName
|
| `strategy` | **Required** Defines Strategy class to use. Recommended to be set via `--strategy NAME`. <br> **Datatype:** ClassName
|
||||||
| `strategy_path` | Adds an additional strategy lookup path (must be a directory). <br> **Datatype:** String
|
| `strategy_path` | Adds an additional strategy lookup path (must be a directory). <br> **Datatype:** String
|
||||||
| `internals.process_throttle_secs` | Set the process throttle. Value in second. <br>*Defaults to `5` seconds.* <br> **Datatype:** Positive Integer
|
| `internals.process_throttle_secs` | Set the process throttle. Value in second. <br>*Defaults to `5` seconds.* <br> **Datatype:** Positive Integer
|
||||||
@ -123,7 +126,7 @@ The following parameters can be set in either configuration file or strategy.
|
|||||||
Values set in the configuration file always overwrite values set in the strategy.
|
Values set in the configuration file always overwrite values set in the strategy.
|
||||||
|
|
||||||
* `minimal_roi`
|
* `minimal_roi`
|
||||||
* `ticker_interval`
|
* `timeframe`
|
||||||
* `stoploss`
|
* `stoploss`
|
||||||
* `trailing_stop`
|
* `trailing_stop`
|
||||||
* `trailing_stop_positive`
|
* `trailing_stop_positive`
|
||||||
@ -135,6 +138,7 @@ Values set in the configuration file always overwrite values set in the strategy
|
|||||||
* `stake_currency`
|
* `stake_currency`
|
||||||
* `stake_amount`
|
* `stake_amount`
|
||||||
* `unfilledtimeout`
|
* `unfilledtimeout`
|
||||||
|
* `disable_dataframe_checks`
|
||||||
* `use_sell_signal` (ask_strategy)
|
* `use_sell_signal` (ask_strategy)
|
||||||
* `sell_profit_only` (ask_strategy)
|
* `sell_profit_only` (ask_strategy)
|
||||||
* `ignore_roi_if_buy_signal` (ask_strategy)
|
* `ignore_roi_if_buy_signal` (ask_strategy)
|
||||||
@ -214,7 +218,7 @@ To allow the bot to trade all the available `stake_currency` in your account (mi
|
|||||||
### Understand minimal_roi
|
### Understand minimal_roi
|
||||||
|
|
||||||
The `minimal_roi` configuration parameter is a JSON object where the key is a duration
|
The `minimal_roi` configuration parameter is a JSON object where the key is a duration
|
||||||
in minutes and the value is the minimum ROI in percent.
|
in minutes and the value is the minimum ROI as ratio.
|
||||||
See the example below:
|
See the example below:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@ -268,7 +272,7 @@ the static list of pairs) if we should buy.
|
|||||||
|
|
||||||
### Understand order_types
|
### Understand order_types
|
||||||
|
|
||||||
The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds.
|
The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`, `emergencysell`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds.
|
||||||
|
|
||||||
This allows to buy using limit orders, sell using
|
This allows to buy using limit orders, sell using
|
||||||
limit-orders, and create stoplosses using using market orders. It also allows to set the
|
limit-orders, and create stoplosses using using market orders. It also allows to set the
|
||||||
@ -284,8 +288,12 @@ If this is configured, the following 4 values (`buy`, `sell`, `stoploss` and
|
|||||||
`emergencysell` is an optional value, which defaults to `market` and is used when creating stoploss on exchange orders fails.
|
`emergencysell` is an optional value, which defaults to `market` and is used when creating stoploss on exchange orders fails.
|
||||||
The below is the default which is used if this is not configured in either strategy or configuration file.
|
The below is the default which is used if this is not configured in either strategy or configuration file.
|
||||||
|
|
||||||
Since `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the stoploss_price and the Limit price.
|
Not all Exchanges support `stoploss_on_exchange`. If an exchange supports both limit and market stoploss orders, then the value of `stoploss` will be used to determine the stoploss type.
|
||||||
`stoploss` defines the stop-price - and limit should be slightly below this. This defaults to 0.99 / 1% (configurable via `stoploss_on_exchange_limit_ratio`).
|
|
||||||
|
If `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the stoploss_price and the Limit price.
|
||||||
|
`stoploss` defines the stop-price - and limit should be slightly below this.
|
||||||
|
|
||||||
|
This defaults to 0.99 / 1% (configurable via `stoploss_on_exchange_limit_ratio`).
|
||||||
Calculation example: we bought the asset at 100$.
|
Calculation example: we bought the asset at 100$.
|
||||||
Stop-price is 95$, then limit would be `95 * 0.99 = 94.05$` - so the stoploss will happen between 95$ and 94.05$.
|
Stop-price is 95$, then limit would be `95 * 0.99 = 94.05$` - so the stoploss will happen between 95$ and 94.05$.
|
||||||
|
|
||||||
@ -327,7 +335,10 @@ Configuration:
|
|||||||
refer to [the stoploss documentation](stoploss.md).
|
refer to [the stoploss documentation](stoploss.md).
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new order.
|
If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new stoploss order.
|
||||||
|
|
||||||
|
!!! Warning "Using market orders"
|
||||||
|
Please read the section [Market order pricing](#market-order-pricing) section when using market orders.
|
||||||
|
|
||||||
!!! Warning "Warning: stoploss_on_exchange failures"
|
!!! Warning "Warning: stoploss_on_exchange failures"
|
||||||
If stoploss on exchange creation fails for some reason, then an "emergency sell" is initiated. By default, this will sell the asset using a market order. The order-type for the emergency-sell can be changed by setting the `emergencysell` value in the `order_types` dictionary - however this is not advised.
|
If stoploss on exchange creation fails for some reason, then an "emergency sell" is initiated. By default, this will sell the asset using a market order. The order-type for the emergency-sell can be changed by setting the `emergencysell` value in the `order_types` dictionary - however this is not advised.
|
||||||
@ -455,6 +466,9 @@ Prices are always retrieved right before an order is placed, either by querying
|
|||||||
!!! Note
|
!!! Note
|
||||||
Orderbook data used by Freqtrade are the data retrieved from exchange by the ccxt's function `fetch_order_book()`, i.e. are usually data from the L2-aggregated orderbook, while the ticker data are the structures returned by the ccxt's `fetch_ticker()`/`fetch_tickers()` functions. Refer to the ccxt library [documentation](https://github.com/ccxt/ccxt/wiki/Manual#market-data) for more details.
|
Orderbook data used by Freqtrade are the data retrieved from exchange by the ccxt's function `fetch_order_book()`, i.e. are usually data from the L2-aggregated orderbook, while the ticker data are the structures returned by the ccxt's `fetch_ticker()`/`fetch_tickers()` functions. Refer to the ccxt library [documentation](https://github.com/ccxt/ccxt/wiki/Manual#market-data) for more details.
|
||||||
|
|
||||||
|
!!! Warning "Using market orders"
|
||||||
|
Please read the section [Market order pricing](#market-order-pricing) section when using market orders.
|
||||||
|
|
||||||
### Buy price
|
### Buy price
|
||||||
|
|
||||||
#### Check depth of market
|
#### Check depth of market
|
||||||
@ -549,13 +563,36 @@ A fixed slot (mirroring `bid_strategy.order_book_top`) can be defined by setting
|
|||||||
|
|
||||||
When not using orderbook (`ask_strategy.use_order_book=False`), the price at the `ask_strategy.price_side` side (defaults to `"ask"`) from the ticker will be used as the sell price.
|
When not using orderbook (`ask_strategy.use_order_book=False`), the price at the `ask_strategy.price_side` side (defaults to `"ask"`) from the ticker will be used as the sell price.
|
||||||
|
|
||||||
|
### Market order pricing
|
||||||
|
|
||||||
|
When using market orders, prices should be configured to use the "correct" side of the orderbook to allow realistic pricing detection.
|
||||||
|
Assuming both buy and sell are using market orders, a configuration similar to the following might be used
|
||||||
|
|
||||||
|
``` jsonc
|
||||||
|
"order_types": {
|
||||||
|
"buy": "market",
|
||||||
|
"sell": "market"
|
||||||
|
// ...
|
||||||
|
},
|
||||||
|
"bid_strategy": {
|
||||||
|
"price_side": "ask",
|
||||||
|
// ...
|
||||||
|
},
|
||||||
|
"ask_strategy":{
|
||||||
|
"price_side": "bid",
|
||||||
|
// ...
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
Obviously, if only one side is using limit orders, different pricing combinations can be used.
|
||||||
|
|
||||||
## Pairlists and Pairlist Handlers
|
## Pairlists and Pairlist Handlers
|
||||||
|
|
||||||
Pairlist Handlers define the list of pairs (pairlist) that the bot should trade. They are configured in the `pairlists` section of the configuration settings.
|
Pairlist Handlers define the list of pairs (pairlist) that the bot should trade. They are configured in the `pairlists` section of the configuration settings.
|
||||||
|
|
||||||
In your configuration, you can use Static Pairlist (defined by the [`StaticPairList`](#static-pair-list) Pairlist Handler) and Dynamic Pairlist (defined by the [`VolumePairList`](#volume-pair-list) Pairlist Handler).
|
In your configuration, you can use Static Pairlist (defined by the [`StaticPairList`](#static-pair-list) Pairlist Handler) and Dynamic Pairlist (defined by the [`VolumePairList`](#volume-pair-list) Pairlist Handler).
|
||||||
|
|
||||||
Additionaly, [`PrecisionFilter`](#precisionfilter), [`PriceFilter`](#pricefilter), [`ShuffleFilter`](#shufflefilter) and [`SpreadFilter`](#spreadfilter) act as Pairlist Filters, removing certain pairs and/or moving their positions in the pairlist.
|
Additionaly, [`AgeFilter`](#agefilter), [`PrecisionFilter`](#precisionfilter), [`PriceFilter`](#pricefilter), [`ShuffleFilter`](#shufflefilter) and [`SpreadFilter`](#spreadfilter) act as Pairlist Filters, removing certain pairs and/or moving their positions in the pairlist.
|
||||||
|
|
||||||
If multiple Pairlist Handlers are used, they are chained and a combination of all Pairlist Handlers forms the resulting pairlist the bot uses for trading and backtesting. Pairlist Handlers are executed in the sequence they are configured. You should always configure either `StaticPairList` or `VolumePairList` as the starting Pairlist Handler.
|
If multiple Pairlist Handlers are used, they are chained and a combination of all Pairlist Handlers forms the resulting pairlist the bot uses for trading and backtesting. Pairlist Handlers are executed in the sequence they are configured. You should always configure either `StaticPairList` or `VolumePairList` as the starting Pairlist Handler.
|
||||||
|
|
||||||
@ -565,6 +602,7 @@ Inactive markets are always removed from the resulting pairlist. Explicitly blac
|
|||||||
|
|
||||||
* [`StaticPairList`](#static-pair-list) (default, if not configured differently)
|
* [`StaticPairList`](#static-pair-list) (default, if not configured differently)
|
||||||
* [`VolumePairList`](#volume-pair-list)
|
* [`VolumePairList`](#volume-pair-list)
|
||||||
|
* [`AgeFilter`](#agefilter)
|
||||||
* [`PrecisionFilter`](#precisionfilter)
|
* [`PrecisionFilter`](#precisionfilter)
|
||||||
* [`PriceFilter`](#pricefilter)
|
* [`PriceFilter`](#pricefilter)
|
||||||
* [`ShuffleFilter`](#shufflefilter)
|
* [`ShuffleFilter`](#shufflefilter)
|
||||||
@ -587,7 +625,7 @@ It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklis
|
|||||||
|
|
||||||
#### Volume Pair List
|
#### Volume Pair List
|
||||||
|
|
||||||
`VolumePairList` employs sorting/filtering of pairs by their trading volume. I 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`).
|
||||||
|
|
||||||
When used in the chain of Pairlist Handlers in a non-leading position (after StaticPairList and other Pairlist Filters), `VolumePairList` considers outputs of previous Pairlist Handlers, adding its sorting/selection of the pairs by the trading volume.
|
When used in the chain of Pairlist Handlers in a non-leading position (after StaticPairList and other Pairlist Filters), `VolumePairList` considers outputs of previous Pairlist Handlers, adding its sorting/selection of the pairs by the trading volume.
|
||||||
|
|
||||||
@ -605,9 +643,19 @@ The `refresh_period` setting allows to define the period (in seconds), at which
|
|||||||
"number_assets": 20,
|
"number_assets": 20,
|
||||||
"sort_key": "quoteVolume",
|
"sort_key": "quoteVolume",
|
||||||
"refresh_period": 1800,
|
"refresh_period": 1800,
|
||||||
],
|
}],
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### AgeFilter
|
||||||
|
|
||||||
|
Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`).
|
||||||
|
|
||||||
|
When pairs are first listed on an exchange they can suffer huge price drops and volatility
|
||||||
|
in the first few days while the pair goes through its price-discovery period. Bots can often
|
||||||
|
be caught out buying before the pair has finished dropping in price.
|
||||||
|
|
||||||
|
This filter allows freqtrade to ignore pairs until they have been listed for at least `min_days_listed` days.
|
||||||
|
|
||||||
#### PrecisionFilter
|
#### PrecisionFilter
|
||||||
|
|
||||||
Filters low-value coins which would not allow setting stoplosses.
|
Filters low-value coins which would not allow setting stoplosses.
|
||||||
@ -655,6 +703,7 @@ The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets,
|
|||||||
"number_assets": 20,
|
"number_assets": 20,
|
||||||
"sort_key": "quoteVolume",
|
"sort_key": "quoteVolume",
|
||||||
},
|
},
|
||||||
|
{"method": "AgeFilter", "min_days_listed": 10},
|
||||||
{"method": "PrecisionFilter"},
|
{"method": "PrecisionFilter"},
|
||||||
{"method": "PriceFilter", "low_price_ratio": 0.01},
|
{"method": "PriceFilter", "low_price_ratio": 0.01},
|
||||||
{"method": "SpreadFilter", "max_spread_ratio": 0.005},
|
{"method": "SpreadFilter", "max_spread_ratio": 0.005},
|
||||||
|
@ -109,7 +109,7 @@ The following command will convert all candle (OHLCV) data available in `~/.freq
|
|||||||
It'll also remove original json data files (`--erase` parameter).
|
It'll also remove original json data files (`--erase` parameter).
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
freqtrade convert-data --format-from json --format-to jsongz --data-dir ~/.freqtrade/data/binance -t 5m 15m --erase
|
freqtrade convert-data --format-from json --format-to jsongz --datadir ~/.freqtrade/data/binance -t 5m 15m --erase
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Subcommand convert-trade data
|
#### Subcommand convert-trade data
|
||||||
@ -155,7 +155,7 @@ The following command will convert all available trade-data in `~/.freqtrade/dat
|
|||||||
It'll also remove original jsongz data files (`--erase` parameter).
|
It'll also remove original jsongz data files (`--erase` parameter).
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
freqtrade convert-trade-data --format-from jsongz --format-to json --data-dir ~/.freqtrade/data/kraken --erase
|
freqtrade convert-trade-data --format-from jsongz --format-to json --datadir ~/.freqtrade/data/kraken --erase
|
||||||
```
|
```
|
||||||
|
|
||||||
### Pairs file
|
### Pairs file
|
||||||
|
@ -92,13 +92,13 @@ docker-compose exec freqtrade_develop /bin/bash
|
|||||||
You have a great idea for a new pair selection algorithm you would like to try out? Great.
|
You have a great idea for a new pair selection algorithm you would like to try out? Great.
|
||||||
Hopefully you also want to contribute this back upstream.
|
Hopefully you also want to contribute this back upstream.
|
||||||
|
|
||||||
Whatever your motivations are - This should get you off the ground in trying to develop a new Pairlist provider.
|
Whatever your motivations are - This should get you off the ground in trying to develop a new Pairlist Handler.
|
||||||
|
|
||||||
First of all, have a look at the [VolumePairList](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/pairlist/VolumePairList.py) provider, and best copy this file with a name of your new Pairlist Provider.
|
First of all, have a look at the [VolumePairList](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/pairlist/VolumePairList.py) Handler, and best copy this file with a name of your new Pairlist Handler.
|
||||||
|
|
||||||
This is a simple provider, which however serves as a good example on how to start developing.
|
This is a simple Handler, which however serves as a good example on how to start developing.
|
||||||
|
|
||||||
Next, modify the classname of the provider (ideally align this with the Filename).
|
Next, modify the classname of the Handler (ideally align this with the module filename).
|
||||||
|
|
||||||
The base-class provides an instance of the exchange (`self._exchange`) the pairlist manager (`self._pairlistmanager`), as well as the main configuration (`self._config`), the pairlist dedicated configuration (`self._pairlistconfig`) and the absolute position within the list of pairlists.
|
The base-class provides an instance of the exchange (`self._exchange`) the pairlist manager (`self._pairlistmanager`), as well as the main configuration (`self._config`), the pairlist dedicated configuration (`self._pairlistconfig`) and the absolute position within the list of pairlists.
|
||||||
|
|
||||||
@ -114,28 +114,44 @@ Now, let's step through the methods which require actions:
|
|||||||
|
|
||||||
#### Pairlist configuration
|
#### Pairlist configuration
|
||||||
|
|
||||||
Configuration for PairListProvider is done in the bot configuration file in the element `"pairlist"`.
|
Configuration for the chain of Pairlist Handlers is done in the bot configuration file in the element `"pairlists"`, an array of configuration parameters for each Pairlist Handlers in the chain.
|
||||||
This Pairlist-object may contain configurations with additional configurations for the configured pairlist.
|
|
||||||
By convention, `"number_assets"` is used to specify the maximum number of pairs to keep in the whitelist. Please follow this to ensure a consistent user experience.
|
|
||||||
|
|
||||||
Additional elements can be configured as needed. `VolumePairList` uses `"sort_key"` to specify the sorting value - however feel free to specify whatever is necessary for your great algorithm to be successfull and dynamic.
|
By convention, `"number_assets"` is used to specify the maximum number of pairs to keep in the pairlist. Please follow this to ensure a consistent user experience.
|
||||||
|
|
||||||
|
Additional parameters can be configured as needed. For instance, `VolumePairList` uses `"sort_key"` to specify the sorting value - however feel free to specify whatever is necessary for your great algorithm to be successfull and dynamic.
|
||||||
|
|
||||||
#### short_desc
|
#### short_desc
|
||||||
|
|
||||||
Returns a description used for Telegram messages.
|
Returns a description used for Telegram messages.
|
||||||
This should contain the name of the Provider, as well as a short description containing the number of assets. Please follow the format `"PairlistName - top/bottom X pairs"`.
|
|
||||||
|
This should contain the name of the Pairlist Handler, as well as a short description containing the number of assets. Please follow the format `"PairlistName - top/bottom X pairs"`.
|
||||||
|
|
||||||
|
#### gen_pairlist
|
||||||
|
|
||||||
|
Override this method if the Pairlist Handler can be used as the leading Pairlist Handler in the chain, defining the initial pairlist which is then handled by all Pairlist Handlers in the chain. Examples are `StaticPairList` and `VolumePairList`.
|
||||||
|
|
||||||
|
This is called with each iteration of the bot (only if the Pairlist Handler is at the first location) - so consider implementing caching for compute/network heavy calculations.
|
||||||
|
|
||||||
|
It must return the resulting pairlist (which may then be passed into the chain of Pairlist Handlers).
|
||||||
|
|
||||||
|
Validations are optional, the parent class exposes a `_verify_blacklist(pairlist)` and `_whitelist_for_active_markets(pairlist)` to do default filtering. Use this if you limit your result to a certain number of pairs - so the endresult is not shorter than expected.
|
||||||
|
|
||||||
#### filter_pairlist
|
#### filter_pairlist
|
||||||
|
|
||||||
Override this method and run all calculations needed in this method.
|
This method is called for each Pairlist Handler in the chain by the pairlist manager.
|
||||||
|
|
||||||
This is called with each iteration of the bot - so consider implementing caching for compute/network heavy calculations.
|
This is called with each iteration of the bot - so consider implementing caching for compute/network heavy calculations.
|
||||||
|
|
||||||
It get's passed a pairlist (which can be the result of previous pairlists) as well as `tickers`, a pre-fetched version of `get_tickers()`.
|
It get's passed a pairlist (which can be the result of previous pairlists) as well as `tickers`, a pre-fetched version of `get_tickers()`.
|
||||||
|
|
||||||
It must return the resulting pairlist (which may then be passed into the next pairlist filter).
|
The default implementation in the base class simply calls the `_validate_pair()` method for each pair in the pairlist, but you may override it. So you should either implement the `_validate_pair()` in your Pairlist Handler or override `filter_pairlist()` to do something else.
|
||||||
|
|
||||||
|
If overridden, it must return the resulting pairlist (which may then be passed into the next Pairlist Handler in the chain).
|
||||||
|
|
||||||
Validations are optional, the parent class exposes a `_verify_blacklist(pairlist)` and `_whitelist_for_active_markets(pairlist)` to do default filters. Use this if you limit your result to a certain number of pairs - so the endresult is not shorter than expected.
|
Validations are optional, the parent class exposes a `_verify_blacklist(pairlist)` and `_whitelist_for_active_markets(pairlist)` to do default filters. Use this if you limit your result to a certain number of pairs - so the endresult is not shorter than expected.
|
||||||
|
|
||||||
|
In `VolumePairList`, this implements different methods of sorting, does early validation so only the expected number of pairs is returned.
|
||||||
|
|
||||||
##### sample
|
##### sample
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
@ -145,11 +161,6 @@ Validations are optional, the parent class exposes a `_verify_blacklist(pairlist
|
|||||||
return pairs
|
return pairs
|
||||||
```
|
```
|
||||||
|
|
||||||
#### _gen_pair_whitelist
|
|
||||||
|
|
||||||
This is a simple method used by `VolumePairList` - however serves as a good example.
|
|
||||||
In VolumePairList, this implements different methods of sorting, does early validation so only the expected number of pairs is returned.
|
|
||||||
|
|
||||||
## Implement a new Exchange (WIP)
|
## Implement a new Exchange (WIP)
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
|
@ -148,7 +148,6 @@ Edge module has following configuration options:
|
|||||||
| `enabled` | If true, then Edge will run periodically. <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
| `enabled` | If true, then Edge will run periodically. <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||||
| `process_throttle_secs` | How often should Edge run in seconds. <br>*Defaults to `3600` (once per hour).* <br> **Datatype:** Integer
|
| `process_throttle_secs` | How often should Edge run in seconds. <br>*Defaults to `3600` (once per hour).* <br> **Datatype:** Integer
|
||||||
| `calculate_since_number_of_days` | Number of days of data against which Edge calculates Win Rate, Risk Reward and Expectancy. <br> **Note** that it downloads historical data so increasing this number would lead to slowing down the bot. <br>*Defaults to `7`.* <br> **Datatype:** Integer
|
| `calculate_since_number_of_days` | Number of days of data against which Edge calculates Win Rate, Risk Reward and Expectancy. <br> **Note** that it downloads historical data so increasing this number would lead to slowing down the bot. <br>*Defaults to `7`.* <br> **Datatype:** Integer
|
||||||
| `capital_available_percentage` | **DEPRECATED - [replaced with `tradable_balance_ratio`](configuration.md#Available balance)** This is the percentage of the total capital on exchange in stake currency. <br>As an example if you have 10 ETH available in your wallet on the exchange and this value is 0.5 (which is 50%), then the bot will use a maximum amount of 5 ETH for trading and considers it as available capital. <br>*Defaults to `0.5`.* <br> **Datatype:** Float
|
|
||||||
| `allowed_risk` | Ratio of allowed risk per trade. <br>*Defaults to `0.01` (1%)).* <br> **Datatype:** Float
|
| `allowed_risk` | Ratio of allowed risk per trade. <br>*Defaults to `0.01` (1%)).* <br> **Datatype:** Float
|
||||||
| `stoploss_range_min` | Minimum stoploss. <br>*Defaults to `-0.01`.* <br> **Datatype:** Float
|
| `stoploss_range_min` | Minimum stoploss. <br>*Defaults to `-0.01`.* <br> **Datatype:** Float
|
||||||
| `stoploss_range_max` | Maximum stoploss. <br>*Defaults to `-0.10`.* <br> **Datatype:** Float
|
| `stoploss_range_max` | Maximum stoploss. <br>*Defaults to `-0.10`.* <br> **Datatype:** Float
|
||||||
@ -156,7 +155,7 @@ Edge module has following configuration options:
|
|||||||
| `minimum_winrate` | It filters out pairs which don't have at least minimum_winrate. <br>This comes handy if you want to be conservative and don't comprise win rate in favour of risk reward ratio. <br>*Defaults to `0.60`.* <br> **Datatype:** Float
|
| `minimum_winrate` | It filters out pairs which don't have at least minimum_winrate. <br>This comes handy if you want to be conservative and don't comprise win rate in favour of risk reward ratio. <br>*Defaults to `0.60`.* <br> **Datatype:** Float
|
||||||
| `minimum_expectancy` | It filters out pairs which have the expectancy lower than this number. <br>Having an expectancy of 0.20 means if you put 10$ on a trade you expect a 12$ return. <br>*Defaults to `0.20`.* <br> **Datatype:** Float
|
| `minimum_expectancy` | It filters out pairs which have the expectancy lower than this number. <br>Having an expectancy of 0.20 means if you put 10$ on a trade you expect a 12$ return. <br>*Defaults to `0.20`.* <br> **Datatype:** Float
|
||||||
| `min_trade_number` | When calculating *W*, *R* and *E* (expectancy) against historical data, you always want to have a minimum number of trades. The more this number is the more Edge is reliable. <br>Having a win rate of 100% on a single trade doesn't mean anything at all. But having a win rate of 70% over past 100 trades means clearly something. <br>*Defaults to `10` (it is highly recommended not to decrease this number).* <br> **Datatype:** Integer
|
| `min_trade_number` | When calculating *W*, *R* and *E* (expectancy) against historical data, you always want to have a minimum number of trades. The more this number is the more Edge is reliable. <br>Having a win rate of 100% on a single trade doesn't mean anything at all. But having a win rate of 70% over past 100 trades means clearly something. <br>*Defaults to `10` (it is highly recommended not to decrease this number).* <br> **Datatype:** Integer
|
||||||
| `max_trade_duration_minute` | Edge will filter out trades with long duration. If a trade is profitable after 1 month, it is hard to evaluate the strategy based on it. But if most of trades are profitable and they have maximum duration of 30 minutes, then it is clearly a good sign.<br>**NOTICE:** While configuring this value, you should take into consideration your timeframe (ticker interval). As an example filtering out trades having duration less than one day for a strategy which has 4h interval does not make sense. Default value is set assuming your strategy interval is relatively small (1m or 5m, etc.).<br>*Defaults to `1440` (one day).* <br> **Datatype:** Integer
|
| `max_trade_duration_minute` | Edge will filter out trades with long duration. If a trade is profitable after 1 month, it is hard to evaluate the strategy based on it. But if most of trades are profitable and they have maximum duration of 30 minutes, then it is clearly a good sign.<br>**NOTICE:** While configuring this value, you should take into consideration your timeframe. As an example filtering out trades having duration less than one day for a strategy which has 4h interval does not make sense. Default value is set assuming your strategy interval is relatively small (1m or 5m, etc.).<br>*Defaults to `1440` (one day).* <br> **Datatype:** Integer
|
||||||
| `remove_pumps` | Edge will remove sudden pumps in a given market while going through historical data. However, given that pumps happen very often in crypto markets, we recommend you keep this off.<br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
| `remove_pumps` | Edge will remove sudden pumps in a given market while going through historical data. However, given that pumps happen very often in crypto markets, we recommend you keep this off.<br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||||
|
|
||||||
## Running Edge independently
|
## Running Edge independently
|
||||||
|
@ -30,6 +30,15 @@ Binance has been split into 3, and users must use the correct ccxt exchange ID f
|
|||||||
The Kraken API does only provide 720 historic candles, which is sufficient for Freqtrade dry-run and live trade modes, but is a problem for backtesting.
|
The Kraken API does only provide 720 historic candles, which is sufficient for Freqtrade dry-run and live trade modes, but is a problem for backtesting.
|
||||||
To download data for the Kraken exchange, using `--dl-trades` is mandatory, otherwise the bot will download the same 720 candles over and over, and you'll not have enough backtest data.
|
To download data for the Kraken exchange, using `--dl-trades` is mandatory, otherwise the bot will download the same 720 candles over and over, and you'll not have enough backtest data.
|
||||||
|
|
||||||
|
Due to the heavy rate-limiting applied by Kraken, the following configuration section should be used to download data:
|
||||||
|
|
||||||
|
``` json
|
||||||
|
"ccxt_async_config": {
|
||||||
|
"enableRateLimit": true,
|
||||||
|
"rateLimit": 3100
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
## Bittrex
|
## Bittrex
|
||||||
|
|
||||||
### Order types
|
### Order types
|
||||||
@ -62,6 +71,30 @@ res = [ f"{x['MarketCurrency']}/{x['BaseCurrency']}" for x in ct.publicGetMarket
|
|||||||
print(res)
|
print(res)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## FTX
|
||||||
|
|
||||||
|
!!! Tip "Stoploss on Exchange"
|
||||||
|
FTX supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
|
||||||
|
You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide.
|
||||||
|
|
||||||
|
|
||||||
|
### Using subaccounts
|
||||||
|
|
||||||
|
To use subaccounts with FTX, you need to edit the configuration and add the following:
|
||||||
|
|
||||||
|
``` json
|
||||||
|
"exchange": {
|
||||||
|
"ccxt_config": {
|
||||||
|
"headers": {
|
||||||
|
"FTX-SUBACCOUNT": "name"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
Older versions of freqtrade may require this key to be added to `"ccxt_async_config"` as well.
|
||||||
|
|
||||||
## All exchanges
|
## All exchanges
|
||||||
|
|
||||||
Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys.
|
Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys.
|
||||||
|
14
docs/faq.md
14
docs/faq.md
@ -45,6 +45,20 @@ the tutorial [here|Testing-new-strategies-with-Hyperopt](bot-usage.md#hyperopt-c
|
|||||||
|
|
||||||
You can use the `/forcesell all` command from Telegram.
|
You can use the `/forcesell all` command from Telegram.
|
||||||
|
|
||||||
|
### I want to run multiple bots on the same machine
|
||||||
|
|
||||||
|
Please look at the [advanced setup documentation Page](advanced-setup.md#running-multiple-instances-of-freqtrade).
|
||||||
|
|
||||||
|
### I'm getting "Missing data fillup" messages in the log
|
||||||
|
|
||||||
|
This message is just a warning that the latest candles had missing candles in them.
|
||||||
|
Depending on the exchange, this can indicate that the pair didn't have a trade for the timeframe you are using - and the exchange does only return candles with volume.
|
||||||
|
On low volume pairs, this is a rather common occurance.
|
||||||
|
|
||||||
|
If this happens for all pairs in the pairlist, this might indicate a recent exchange downtime. Please check your exchange's public channels for details.
|
||||||
|
|
||||||
|
Irrespectively of the reason, Freqtrade will fill up these candles with "empty" candles, where open, high, low and close are set to the previous candle close - and volume is empty. In a chart, this will look like a `_` - and is aligned with how exchanges usually represent 0 volume candles.
|
||||||
|
|
||||||
### I'm getting the "RESTRICTED_MARKET" message in the log
|
### I'm getting the "RESTRICTED_MARKET" message in the log
|
||||||
|
|
||||||
Currently known to happen for US Bittrex users.
|
Currently known to happen for US Bittrex users.
|
||||||
|
@ -124,9 +124,9 @@ To avoid naming collisions in the search-space, please prefix all sell-spaces wi
|
|||||||
|
|
||||||
#### Using timeframe as a part of the Strategy
|
#### Using timeframe as a part of the Strategy
|
||||||
|
|
||||||
The Strategy class exposes the timeframe (ticker interval) value as the `self.ticker_interval` attribute.
|
The Strategy class exposes the timeframe value as the `self.timeframe` attribute.
|
||||||
The same value is available as class-attribute `HyperoptName.ticker_interval`.
|
The same value is available as class-attribute `HyperoptName.timeframe`.
|
||||||
In the case of the linked sample-value this would be `SampleHyperOpt.ticker_interval`.
|
In the case of the linked sample-value this would be `SampleHyperOpt.timeframe`.
|
||||||
|
|
||||||
## Solving a Mystery
|
## Solving a Mystery
|
||||||
|
|
||||||
@ -265,7 +265,7 @@ freqtrade hyperopt --timerange 20180401-20180501
|
|||||||
Hyperopt can reuse `populate_indicators`, `populate_buy_trend`, `populate_sell_trend` from your strategy, assuming these methods are **not** in your custom hyperopt file, and a strategy is provided.
|
Hyperopt can reuse `populate_indicators`, `populate_buy_trend`, `populate_sell_trend` from your strategy, assuming these methods are **not** in your custom hyperopt file, and a strategy is provided.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
freqtrade hyperopt --strategy SampleStrategy --customhyperopt SampleHyperopt
|
freqtrade hyperopt --strategy SampleStrategy --hyperopt SampleHyperopt
|
||||||
```
|
```
|
||||||
|
|
||||||
### Running Hyperopt with Smaller Search Space
|
### Running Hyperopt with Smaller Search Space
|
||||||
@ -403,7 +403,7 @@ As stated in the comment, you can also use it as the value of the `minimal_roi`
|
|||||||
|
|
||||||
#### Default ROI Search Space
|
#### Default ROI Search Space
|
||||||
|
|
||||||
If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps). Hyperopt implements adaptive ranges for ROI tables with ranges for values in the ROI steps that depend on the ticker_interval used. By default the values vary in the following ranges (for some of the most used timeframes, values are rounded to 5 digits after the decimal point):
|
If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps). Hyperopt implements adaptive ranges for ROI tables with ranges for values in the ROI steps that depend on the timeframe used. By default the values vary in the following ranges (for some of the most used timeframes, values are rounded to 5 digits after the decimal point):
|
||||||
|
|
||||||
| # step | 1m | | 5m | | 1h | | 1d | |
|
| # step | 1m | | 5m | | 1h | | 1d | |
|
||||||
| ------ | ------ | ----------------- | -------- | ----------- | ---------- | ----------------- | ------------ | ----------------- |
|
| ------ | ------ | ----------------- | -------- | ----------- | ---------- | ----------------- | ------------ | ----------------- |
|
||||||
@ -412,7 +412,7 @@ If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace f
|
|||||||
| 3 | 4...20 | 0.00387...0.01547 | 20...100 | 0.01...0.04 | 240...1200 | 0.02294...0.09177 | 5760...28800 | 0.04059...0.16237 |
|
| 3 | 4...20 | 0.00387...0.01547 | 20...100 | 0.01...0.04 | 240...1200 | 0.02294...0.09177 | 5760...28800 | 0.04059...0.16237 |
|
||||||
| 4 | 6...44 | 0.0 | 30...220 | 0.0 | 360...2640 | 0.0 | 8640...63360 | 0.0 |
|
| 4 | 6...44 | 0.0 | 30...220 | 0.0 | 360...2640 | 0.0 | 8640...63360 | 0.0 |
|
||||||
|
|
||||||
These ranges should be sufficient in most cases. The minutes in the steps (ROI dict keys) are scaled linearly depending on the timeframe (ticker interval) used. The ROI values in the steps (ROI dict values) are scaled logarithmically depending on the timeframe used.
|
These ranges should be sufficient in most cases. The minutes in the steps (ROI dict keys) are scaled linearly depending on the timeframe used. The ROI values in the steps (ROI dict values) are scaled logarithmically depending on the timeframe used.
|
||||||
|
|
||||||
If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default.
|
If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default.
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ Click each one for install guide:
|
|||||||
* [Python >= 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/)
|
* [Python >= 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/)
|
||||||
* [pip](https://pip.pypa.io/en/stable/installing/)
|
* [pip](https://pip.pypa.io/en/stable/installing/)
|
||||||
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||||
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended)
|
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended)
|
||||||
* [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html) (install instructions below)
|
* [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html) (install instructions below)
|
||||||
|
|
||||||
We also recommend a [Telegram bot](telegram-usage.md#setup-your-telegram-bot), which is optional but recommended.
|
We also recommend a [Telegram bot](telegram-usage.md#setup-your-telegram-bot), which is optional but recommended.
|
||||||
|
@ -31,7 +31,7 @@ usage: freqtrade plot-dataframe [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
|||||||
[--plot-limit INT] [--db-url PATH]
|
[--plot-limit INT] [--db-url PATH]
|
||||||
[--trade-source {DB,file}] [--export EXPORT]
|
[--trade-source {DB,file}] [--export EXPORT]
|
||||||
[--export-filename PATH]
|
[--export-filename PATH]
|
||||||
[--timerange TIMERANGE] [-i TICKER_INTERVAL]
|
[--timerange TIMERANGE] [-i TIMEFRAME]
|
||||||
[--no-trades]
|
[--no-trades]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
@ -65,7 +65,7 @@ optional arguments:
|
|||||||
_today.json`
|
_today.json`
|
||||||
--timerange TIMERANGE
|
--timerange TIMERANGE
|
||||||
Specify what timerange of data to use.
|
Specify what timerange of data to use.
|
||||||
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL
|
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
|
||||||
Specify ticker interval (`1m`, `5m`, `30m`, `1h`,
|
Specify ticker interval (`1m`, `5m`, `30m`, `1h`,
|
||||||
`1d`).
|
`1d`).
|
||||||
--no-trades Skip using trades from backtesting file and DB.
|
--no-trades Skip using trades from backtesting file and DB.
|
||||||
@ -227,7 +227,7 @@ usage: freqtrade plot-profit [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
|||||||
[-d PATH] [--userdir PATH] [-p PAIRS [PAIRS ...]]
|
[-d PATH] [--userdir PATH] [-p PAIRS [PAIRS ...]]
|
||||||
[--timerange TIMERANGE] [--export EXPORT]
|
[--timerange TIMERANGE] [--export EXPORT]
|
||||||
[--export-filename PATH] [--db-url PATH]
|
[--export-filename PATH] [--db-url PATH]
|
||||||
[--trade-source {DB,file}] [-i TICKER_INTERVAL]
|
[--trade-source {DB,file}] [-i TIMEFRAME]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
@ -250,7 +250,7 @@ optional arguments:
|
|||||||
--trade-source {DB,file}
|
--trade-source {DB,file}
|
||||||
Specify the source for trades (Can be DB or file
|
Specify the source for trades (Can be DB or file
|
||||||
(backtest file)) Default: file
|
(backtest file)) Default: file
|
||||||
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL
|
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
|
||||||
Specify ticker interval (`1m`, `5m`, `30m`, `1h`,
|
Specify ticker interval (`1m`, `5m`, `30m`, `1h`,
|
||||||
`1d`).
|
`1d`).
|
||||||
|
|
||||||
@ -261,9 +261,10 @@ Common arguments:
|
|||||||
details.
|
details.
|
||||||
-V, --version show program's version number and exit
|
-V, --version show program's version number and exit
|
||||||
-c PATH, --config PATH
|
-c PATH, --config PATH
|
||||||
Specify configuration file (default: `config.json`).
|
Specify configuration file (default:
|
||||||
Multiple --config options may be used. Can be set to
|
`userdir/config.json` or `config.json` whichever
|
||||||
`-` to read config from stdin.
|
exists). Multiple --config options may be used. Can be
|
||||||
|
set to `-` to read config from stdin.
|
||||||
-d PATH, --datadir PATH
|
-d PATH, --datadir PATH
|
||||||
Path to directory with historical backtesting data.
|
Path to directory with historical backtesting data.
|
||||||
--userdir PATH, --user-data-dir PATH
|
--userdir PATH, --user-data-dir PATH
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
mkdocs-material==5.2.1
|
mkdocs-material==5.3.3
|
||||||
mdx_truly_sane_lists==1.2
|
mdx_truly_sane_lists==1.2
|
||||||
|
@ -11,7 +11,9 @@ Sample configuration:
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"listen_ip_address": "127.0.0.1",
|
"listen_ip_address": "127.0.0.1",
|
||||||
"listen_port": 8080,
|
"listen_port": 8080,
|
||||||
|
"verbosity": "info",
|
||||||
"jwt_secret_key": "somethingrandom",
|
"jwt_secret_key": "somethingrandom",
|
||||||
|
"CORS_origins": [],
|
||||||
"username": "Freqtrader",
|
"username": "Freqtrader",
|
||||||
"password": "SuperSecret1!"
|
"password": "SuperSecret1!"
|
||||||
},
|
},
|
||||||
@ -109,7 +111,7 @@ python3 scripts/rest_client.py --config rest_config.json <command> [optional par
|
|||||||
| `start` | | Starts the trader
|
| `start` | | Starts the trader
|
||||||
| `stop` | | Stops the trader
|
| `stop` | | Stops the trader
|
||||||
| `stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
|
| `stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
|
||||||
| `reload_conf` | | Reloads the configuration file
|
| `reload_config` | | Reloads the configuration file
|
||||||
| `show_config` | | Shows part of the current configuration with relevant settings to operation
|
| `show_config` | | Shows part of the current configuration with relevant settings to operation
|
||||||
| `status` | | Lists all open trades
|
| `status` | | Lists all open trades
|
||||||
| `count` | | Displays number of trades used and available
|
| `count` | | Displays number of trades used and available
|
||||||
@ -173,7 +175,7 @@ profit
|
|||||||
Returns the profit summary
|
Returns the profit summary
|
||||||
:returns: json object
|
:returns: json object
|
||||||
|
|
||||||
reload_conf
|
reload_config
|
||||||
Reload configuration
|
Reload configuration
|
||||||
:returns: json object
|
:returns: json object
|
||||||
|
|
||||||
@ -195,7 +197,7 @@ stop
|
|||||||
|
|
||||||
stopbuy
|
stopbuy
|
||||||
Stop buying (but handle sells gracefully).
|
Stop buying (but handle sells gracefully).
|
||||||
use reload_conf to reset
|
use reload_config to reset
|
||||||
:returns: json object
|
:returns: json object
|
||||||
|
|
||||||
version
|
version
|
||||||
@ -231,3 +233,26 @@ Since the access token has a short timeout (15 min) - the `token/refresh` reques
|
|||||||
> curl -X POST --header "Authorization: Bearer ${refresh_token}"http://localhost:8080/api/v1/token/refresh
|
> curl -X POST --header "Authorization: Bearer ${refresh_token}"http://localhost:8080/api/v1/token/refresh
|
||||||
{"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1ODkxMTk5NzQsIm5iZiI6MTU4OTExOTk3NCwianRpIjoiMDBjNTlhMWUtMjBmYS00ZTk0LTliZjAtNWQwNTg2MTdiZDIyIiwiZXhwIjoxNTg5MTIwODc0LCJpZGVudGl0eSI6eyJ1IjoiRnJlcXRyYWRlciJ9LCJmcmVzaCI6ZmFsc2UsInR5cGUiOiJhY2Nlc3MifQ.1seHlII3WprjjclY6DpRhen0rqdF4j6jbvxIhUFaSbs"}
|
{"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1ODkxMTk5NzQsIm5iZiI6MTU4OTExOTk3NCwianRpIjoiMDBjNTlhMWUtMjBmYS00ZTk0LTliZjAtNWQwNTg2MTdiZDIyIiwiZXhwIjoxNTg5MTIwODc0LCJpZGVudGl0eSI6eyJ1IjoiRnJlcXRyYWRlciJ9LCJmcmVzaCI6ZmFsc2UsInR5cGUiOiJhY2Nlc3MifQ.1seHlII3WprjjclY6DpRhen0rqdF4j6jbvxIhUFaSbs"}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## CORS
|
||||||
|
|
||||||
|
All web-based frontends are subject to [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) - Cross-Origin Resource Sharing.
|
||||||
|
Since most of the requests to the Freqtrade API must be authenticated, a proper CORS policy is key to avoid security problems.
|
||||||
|
Also, the standard disallows `*` CORS policies for requests with credentials, so this setting must be set appropriately.
|
||||||
|
|
||||||
|
Users can configure this themselves via the `CORS_origins` configuration setting.
|
||||||
|
It consists of a list of allowed sites that are allowed to consume resources from the bot's API.
|
||||||
|
|
||||||
|
Assuming your application is deployed as `https://frequi.freqtrade.io/home/` - this would mean that the following configuration becomes necessary:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
//...
|
||||||
|
"jwt_secret_key": "somethingrandom",
|
||||||
|
"CORS_origins": ["https://frequi.freqtrade.io"],
|
||||||
|
//...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
We strongly recommend to also set `jwt_secret_key` to something random and known only to yourself to avoid unauthorized access to your bot.
|
||||||
|
@ -70,7 +70,7 @@ CREATE TABLE trades
|
|||||||
min_rate FLOAT,
|
min_rate FLOAT,
|
||||||
sell_reason VARCHAR,
|
sell_reason VARCHAR,
|
||||||
strategy VARCHAR,
|
strategy VARCHAR,
|
||||||
ticker_interval INTEGER,
|
timeframe INTEGER,
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
CHECK (is_open IN (0, 1))
|
CHECK (is_open IN (0, 1))
|
||||||
);
|
);
|
||||||
@ -101,7 +101,7 @@ SET is_open=0,
|
|||||||
close_date=<close_date>,
|
close_date=<close_date>,
|
||||||
close_rate=<close_rate>,
|
close_rate=<close_rate>,
|
||||||
close_profit=close_rate/open_rate-1,
|
close_profit=close_rate/open_rate-1,
|
||||||
close_profit_abs = (amount * <close_rate> * (1 - fee_close) - (amount * open_rate * 1 - fee_open),
|
close_profit_abs = (amount * <close_rate> * (1 - fee_close) - (amount * open_rate * 1 - fee_open)),
|
||||||
sell_reason=<sell_reason>
|
sell_reason=<sell_reason>
|
||||||
WHERE id=<trade_ID_to_update>;
|
WHERE id=<trade_ID_to_update>;
|
||||||
```
|
```
|
||||||
@ -114,7 +114,7 @@ SET is_open=0,
|
|||||||
close_date='2017-12-20 03:08:45.103418',
|
close_date='2017-12-20 03:08:45.103418',
|
||||||
close_rate=0.19638016,
|
close_rate=0.19638016,
|
||||||
close_profit=0.0496,
|
close_profit=0.0496,
|
||||||
close_profit_abs = (amount * 0.19638016 * (1 - fee_close) - (amount * open_rate * 1 - fee_open)
|
close_profit_abs = (amount * 0.19638016 * (1 - fee_close) - (amount * open_rate * 1 - fee_open))
|
||||||
sell_reason='force_sell'
|
sell_reason='force_sell'
|
||||||
WHERE id=31;
|
WHERE id=31;
|
||||||
```
|
```
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Stop Loss
|
# Stop Loss
|
||||||
|
|
||||||
The `stoploss` configuration parameter is loss in percentage that should trigger a sale.
|
The `stoploss` configuration parameter is loss as ratio that should trigger a sale.
|
||||||
For example, value `-0.10` will cause immediate sell if the profit dips below -10% for a given trade. This parameter is optional.
|
For example, value `-0.10` will cause immediate sell if the profit dips below -10% for a given trade. This parameter is optional.
|
||||||
|
|
||||||
Most of the strategy files already include the optimal `stoploss` value.
|
Most of the strategy files already include the optimal `stoploss` value.
|
||||||
@ -27,7 +27,7 @@ So this parameter will tell the bot how often it should update the stoploss orde
|
|||||||
This same logic will reapply a stoploss order on the exchange should you cancel it accidentally.
|
This same logic will reapply a stoploss order on the exchange should you cancel it accidentally.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Stoploss on exchange is only supported for Binance (stop-loss-limit) and Kraken (stop-loss-market) as of now.
|
Stoploss on exchange is only supported for Binance (stop-loss-limit), Kraken (stop-loss-market) and FTX (stop limit and stop-market) as of now.
|
||||||
|
|
||||||
## Static Stop Loss
|
## Static Stop Loss
|
||||||
|
|
||||||
@ -101,7 +101,7 @@ Simplified example:
|
|||||||
|
|
||||||
## Changing stoploss on open trades
|
## Changing stoploss on open trades
|
||||||
|
|
||||||
A stoploss on an open trade can be changed by changing the value in the configuration or strategy and use the `/reload_conf` command (alternatively, completely stopping and restarting the bot also works).
|
A stoploss on an open trade can be changed by changing the value in the configuration or strategy and use the `/reload_config` command (alternatively, completely stopping and restarting the bot also works).
|
||||||
|
|
||||||
The new stoploss value will be applied to open trades (and corresponding log-messages will be generated).
|
The new stoploss value will be applied to open trades (and corresponding log-messages will be generated).
|
||||||
|
|
||||||
|
@ -139,10 +139,10 @@ By letting the bot know how much history is needed, backtest trades can start at
|
|||||||
|
|
||||||
#### Example
|
#### Example
|
||||||
|
|
||||||
Let's try to backtest 1 month (January 2019) of 5m candles using the an example strategy with EMA100, as above.
|
Let's try to backtest 1 month (January 2019) of 5m candles using an example strategy with EMA100, as above.
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
freqtrade backtesting --timerange 20190101-20190201 --ticker-interval 5m
|
freqtrade backtesting --timerange 20190101-20190201 --timeframe 5m
|
||||||
```
|
```
|
||||||
|
|
||||||
Assuming `startup_candle_count` is set to 100, backtesting knows it needs 100 candles to generate valid buy signals. It will load data from `20190101 - (100 * 5m)` - which is ~2019-12-31 15:30:00.
|
Assuming `startup_candle_count` is set to 100, backtesting knows it needs 100 candles to generate valid buy signals. It will load data from `20190101 - (100 * 5m)` - which is ~2019-12-31 15:30:00.
|
||||||
@ -248,7 +248,7 @@ minimal_roi = {
|
|||||||
|
|
||||||
While technically not completely disabled, this would sell once the trade reaches 10000% Profit.
|
While technically not completely disabled, this would sell once the trade reaches 10000% Profit.
|
||||||
|
|
||||||
To use times based on candle duration (ticker_interval or timeframe), the following snippet can be handy.
|
To use times based on candle duration (timeframe), the following snippet can be handy.
|
||||||
This will allow you to change the ticket_interval for the strategy, and ROI times will still be set as candles (e.g. after 3 candles ...)
|
This will allow you to change the ticket_interval for the strategy, and ROI times will still be set as candles (e.g. after 3 candles ...)
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
@ -256,12 +256,12 @@ from freqtrade.exchange import timeframe_to_minutes
|
|||||||
|
|
||||||
class AwesomeStrategy(IStrategy):
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
ticker_interval = "1d"
|
timeframe = "1d"
|
||||||
ticker_interval_mins = timeframe_to_minutes(ticker_interval)
|
timeframe_mins = timeframe_to_minutes(timeframe)
|
||||||
minimal_roi = {
|
minimal_roi = {
|
||||||
"0": 0.05, # 5% for the first 3 candles
|
"0": 0.05, # 5% for the first 3 candles
|
||||||
str(ticker_interval_mins * 3)): 0.02, # 2% after 3 candles
|
str(timeframe_mins * 3)): 0.02, # 2% after 3 candles
|
||||||
str(ticker_interval_mins * 6)): 0.01, # 1% After 6 candles
|
str(timeframe_mins * 6)): 0.01, # 1% After 6 candles
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -290,7 +290,7 @@ Common values are `"1m"`, `"5m"`, `"15m"`, `"1h"`, however all values supported
|
|||||||
|
|
||||||
Please note that the same buy/sell signals may work well with one timeframe, but not with the others.
|
Please note that the same buy/sell signals may work well with one timeframe, but not with the others.
|
||||||
|
|
||||||
This setting is accessible within the strategy methods as the `self.ticker_interval` attribute.
|
This setting is accessible within the strategy methods as the `self.timeframe` attribute.
|
||||||
|
|
||||||
### Metadata dict
|
### Metadata dict
|
||||||
|
|
||||||
@ -400,7 +400,7 @@ This is where calling `self.dp.current_whitelist()` comes in handy.
|
|||||||
class SampleStrategy(IStrategy):
|
class SampleStrategy(IStrategy):
|
||||||
# strategy init stuff...
|
# strategy init stuff...
|
||||||
|
|
||||||
ticker_interval = '5m'
|
timeframe = '5m'
|
||||||
|
|
||||||
# more strategy init stuff..
|
# more strategy init stuff..
|
||||||
|
|
||||||
@ -557,7 +557,7 @@ Locks can also be lifted manually, by calling `self.unlock_pair(pair)`.
|
|||||||
To verify if a pair is currently locked, use `self.is_pair_locked(pair)`.
|
To verify if a pair is currently locked, use `self.is_pair_locked(pair)`.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Locked pairs are not persisted, so a restart of the bot, or calling `/reload_conf` will reset locked pairs.
|
Locked pairs are not persisted, so a restart of the bot, or calling `/reload_config` will reset locked pairs.
|
||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
Locking pairs is not functioning during backtesting.
|
Locking pairs is not functioning during backtesting.
|
||||||
|
@ -18,7 +18,7 @@ config = Configuration.from_files([])
|
|||||||
# config = Configuration.from_files(["config.json"])
|
# config = Configuration.from_files(["config.json"])
|
||||||
|
|
||||||
# Define some constants
|
# Define some constants
|
||||||
config["ticker_interval"] = "5m"
|
config["timeframe"] = "5m"
|
||||||
# Name of the strategy class
|
# Name of the strategy class
|
||||||
config["strategy"] = "SampleStrategy"
|
config["strategy"] = "SampleStrategy"
|
||||||
# Location of the data
|
# Location of the data
|
||||||
@ -33,7 +33,7 @@ pair = "BTC_USDT"
|
|||||||
from freqtrade.data.history import load_pair_history
|
from freqtrade.data.history import load_pair_history
|
||||||
|
|
||||||
candles = load_pair_history(datadir=data_location,
|
candles = load_pair_history(datadir=data_location,
|
||||||
timeframe=config["ticker_interval"],
|
timeframe=config["timeframe"],
|
||||||
pair=pair)
|
pair=pair)
|
||||||
|
|
||||||
# Confirm success
|
# Confirm success
|
||||||
|
@ -52,7 +52,7 @@ official commands. You can ask at any moment for help with `/help`.
|
|||||||
| `/start` | | Starts the trader
|
| `/start` | | Starts the trader
|
||||||
| `/stop` | | Stops the trader
|
| `/stop` | | Stops the trader
|
||||||
| `/stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
|
| `/stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
|
||||||
| `/reload_conf` | | Reloads the configuration file
|
| `/reload_config` | | Reloads the configuration file
|
||||||
| `/show_config` | | Shows part of the current configuration with relevant settings to operation
|
| `/show_config` | | Shows part of the current configuration with relevant settings to operation
|
||||||
| `/status` | | Lists all open trades
|
| `/status` | | Lists all open trades
|
||||||
| `/status table` | | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**)
|
| `/status table` | | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**)
|
||||||
@ -85,14 +85,14 @@ Below, example of Telegram message you will receive for each command.
|
|||||||
|
|
||||||
### /stopbuy
|
### /stopbuy
|
||||||
|
|
||||||
> **status:** `Setting max_open_trades to 0. Run /reload_conf to reset.`
|
> **status:** `Setting max_open_trades to 0. Run /reload_config to reset.`
|
||||||
|
|
||||||
Prevents the bot from opening new trades by temporarily setting "max_open_trades" to 0. Open trades will be handled via their regular rules (ROI / Sell-signal, stoploss, ...).
|
Prevents the bot from opening new trades by temporarily setting "max_open_trades" to 0. Open trades will be handled via their regular rules (ROI / Sell-signal, stoploss, ...).
|
||||||
|
|
||||||
After this, give the bot time to close off open trades (can be checked via `/status table`).
|
After this, give the bot time to close off open trades (can be checked via `/status table`).
|
||||||
Once all positions are sold, run `/stop` to completely stop the bot.
|
Once all positions are sold, run `/stop` to completely stop the bot.
|
||||||
|
|
||||||
`/reload_conf` resets "max_open_trades" to the value set in the configuration and resets this command.
|
`/reload_config` resets "max_open_trades" to the value set in the configuration and resets this command.
|
||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
The stop-buy signal is ONLY active while the bot is running, and is not persisted anyway, so restarting the bot will cause this to reset.
|
The stop-buy signal is ONLY active while the bot is running, and is not persisted anyway, so restarting the bot will cause this to reset.
|
||||||
@ -209,7 +209,7 @@ Shows the current whitelist
|
|||||||
Shows the current blacklist.
|
Shows the current blacklist.
|
||||||
If Pair is set, then this pair will be added to the pairlist.
|
If Pair is set, then this pair will be added to the pairlist.
|
||||||
Also supports multiple pairs, seperated by a space.
|
Also supports multiple pairs, seperated by a space.
|
||||||
Use `/reload_conf` to reset the blacklist.
|
Use `/reload_config` to reset the blacklist.
|
||||||
|
|
||||||
> Using blacklist `StaticPairList` with 2 pairs
|
> Using blacklist `StaticPairList` with 2 pairs
|
||||||
>`DODGE/BTC`, `HOT/BTC`.
|
>`DODGE/BTC`, `HOT/BTC`.
|
||||||
|
@ -62,7 +62,7 @@ $ freqtrade new-config --config config_binance.json
|
|||||||
? Please insert your stake currency: BTC
|
? Please insert your stake currency: BTC
|
||||||
? Please insert your stake amount: 0.05
|
? Please insert your stake amount: 0.05
|
||||||
? Please insert max_open_trades (Integer or 'unlimited'): 3
|
? Please insert max_open_trades (Integer or 'unlimited'): 3
|
||||||
? Please insert your timeframe (ticker interval): 5m
|
? Please insert your desired timeframe (e.g. 5m): 5m
|
||||||
? Please insert your display Currency (for reporting): USD
|
? Please insert your display Currency (for reporting): USD
|
||||||
? Select exchange binance
|
? Select exchange binance
|
||||||
? Do you want to enable Telegram? No
|
? Do you want to enable Telegram? No
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
""" Freqtrade bot """
|
""" Freqtrade bot """
|
||||||
__version__ = '2020.5'
|
__version__ = '2020.6'
|
||||||
|
|
||||||
if __version__ == 'develop':
|
if __version__ == 'develop':
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ ARGS_STRATEGY = ["strategy", "strategy_path"]
|
|||||||
|
|
||||||
ARGS_TRADE = ["db_url", "sd_notify", "dry_run"]
|
ARGS_TRADE = ["db_url", "sd_notify", "dry_run"]
|
||||||
|
|
||||||
ARGS_COMMON_OPTIMIZE = ["ticker_interval", "timerange",
|
ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange",
|
||||||
"max_open_trades", "stake_amount", "fee"]
|
"max_open_trades", "stake_amount", "fee"]
|
||||||
|
|
||||||
ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions",
|
ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions",
|
||||||
@ -59,10 +59,10 @@ ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "download_trades", "exchang
|
|||||||
|
|
||||||
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",
|
||||||
"timerange", "ticker_interval", "no_trades"]
|
"timerange", "timeframe", "no_trades"]
|
||||||
|
|
||||||
ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url",
|
ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url",
|
||||||
"trade_source", "ticker_interval"]
|
"trade_source", "timeframe"]
|
||||||
|
|
||||||
ARGS_SHOW_TRADES = ["db_url", "trade_ids", "print_json"]
|
ARGS_SHOW_TRADES = ["db_url", "trade_ids", "print_json"]
|
||||||
|
|
||||||
@ -318,7 +318,7 @@ class Arguments:
|
|||||||
# Add list-timeframes subcommand
|
# Add list-timeframes subcommand
|
||||||
list_timeframes_cmd = subparsers.add_parser(
|
list_timeframes_cmd = subparsers.add_parser(
|
||||||
'list-timeframes',
|
'list-timeframes',
|
||||||
help='Print available ticker intervals (timeframes) for the exchange.',
|
help='Print available timeframes for the exchange.',
|
||||||
parents=[_common_parser],
|
parents=[_common_parser],
|
||||||
)
|
)
|
||||||
list_timeframes_cmd.set_defaults(func=start_list_timeframes)
|
list_timeframes_cmd.set_defaults(func=start_list_timeframes)
|
||||||
|
@ -75,8 +75,8 @@ def ask_user_config() -> Dict[str, Any]:
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"name": "ticker_interval",
|
"name": "timeframe",
|
||||||
"message": "Please insert your timeframe (ticker interval):",
|
"message": "Please insert your desired timeframe (e.g. 5m):",
|
||||||
"default": "5m",
|
"default": "5m",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -110,8 +110,8 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
action='store_true',
|
action='store_true',
|
||||||
),
|
),
|
||||||
# Optimize common
|
# Optimize common
|
||||||
"ticker_interval": Arg(
|
"timeframe": Arg(
|
||||||
'-i', '--ticker-interval',
|
'-i', '--timeframe', '--ticker-interval',
|
||||||
help='Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`).',
|
help='Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`).',
|
||||||
),
|
),
|
||||||
"timerange": Arg(
|
"timerange": Arg(
|
||||||
|
@ -102,8 +102,8 @@ def start_list_timeframes(args: Dict[str, Any]) -> None:
|
|||||||
Print ticker intervals (timeframes) available on Exchange
|
Print ticker intervals (timeframes) available on Exchange
|
||||||
"""
|
"""
|
||||||
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
|
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
|
||||||
# Do not use ticker_interval set in the config
|
# Do not use timeframe set in the config
|
||||||
config['ticker_interval'] = None
|
config['timeframe'] = 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)
|
||||||
|
@ -25,7 +25,6 @@ def start_test_pairlist(args: Dict[str, Any]) -> None:
|
|||||||
results = {}
|
results = {}
|
||||||
for curr in quote_currencies:
|
for curr in quote_currencies:
|
||||||
config['stake_currency'] = curr
|
config['stake_currency'] = curr
|
||||||
# Do not use ticker_interval set in the config
|
|
||||||
pairlists = PairListManager(exchange, config)
|
pairlists = PairListManager(exchange, config)
|
||||||
pairlists.refresh_pairlist()
|
pairlists.refresh_pairlist()
|
||||||
results[curr] = pairlists.whitelist
|
results[curr] = pairlists.whitelist
|
||||||
|
@ -204,9 +204,9 @@ class Configuration:
|
|||||||
def _process_optimize_options(self, config: Dict[str, Any]) -> None:
|
def _process_optimize_options(self, config: Dict[str, Any]) -> None:
|
||||||
|
|
||||||
# This will override the strategy configuration
|
# This will override the strategy configuration
|
||||||
self._args_to_config(config, argname='ticker_interval',
|
self._args_to_config(config, argname='timeframe',
|
||||||
logstring='Parameter -i/--ticker-interval detected ... '
|
logstring='Parameter -i/--timeframe detected ... '
|
||||||
'Using ticker_interval: {} ...')
|
'Using timeframe: {} ...')
|
||||||
|
|
||||||
self._args_to_config(config, argname='position_stacking',
|
self._args_to_config(config, argname='position_stacking',
|
||||||
logstring='Parameter --enable-position-stacking detected ...')
|
logstring='Parameter --enable-position-stacking detected ...')
|
||||||
@ -242,8 +242,8 @@ class Configuration:
|
|||||||
self._args_to_config(config, argname='strategy_list',
|
self._args_to_config(config, argname='strategy_list',
|
||||||
logstring='Using strategy list of {} strategies', logfun=len)
|
logstring='Using strategy list of {} strategies', logfun=len)
|
||||||
|
|
||||||
self._args_to_config(config, argname='ticker_interval',
|
self._args_to_config(config, argname='timeframe',
|
||||||
logstring='Overriding ticker interval with Command line argument')
|
logstring='Overriding timeframe with Command line argument')
|
||||||
|
|
||||||
self._args_to_config(config, argname='export',
|
self._args_to_config(config, argname='export',
|
||||||
logstring='Parameter --export detected: {} ...')
|
logstring='Parameter --export detected: {} ...')
|
||||||
|
@ -60,10 +60,21 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None:
|
|||||||
|
|
||||||
if (config.get('edge', {}).get('enabled', False)
|
if (config.get('edge', {}).get('enabled', False)
|
||||||
and 'capital_available_percentage' in config.get('edge', {})):
|
and 'capital_available_percentage' in config.get('edge', {})):
|
||||||
logger.warning(
|
raise OperationalException(
|
||||||
"DEPRECATED: "
|
"DEPRECATED: "
|
||||||
"Using 'edge.capital_available_percentage' has been deprecated in favor of "
|
"Using 'edge.capital_available_percentage' has been deprecated in favor of "
|
||||||
"'tradable_balance_ratio'. Please migrate your configuration to "
|
"'tradable_balance_ratio'. Please migrate your configuration to "
|
||||||
"'tradable_balance_ratio' and remove 'capital_available_percentage' "
|
"'tradable_balance_ratio' and remove 'capital_available_percentage' "
|
||||||
"from the edge configuration."
|
"from the edge configuration."
|
||||||
)
|
)
|
||||||
|
if 'ticker_interval' in config:
|
||||||
|
logger.warning(
|
||||||
|
"DEPRECATED: "
|
||||||
|
"Please use 'timeframe' instead of 'ticker_interval."
|
||||||
|
)
|
||||||
|
if 'timeframe' in config:
|
||||||
|
raise OperationalException(
|
||||||
|
"Both 'timeframe' and 'ticker_interval' detected."
|
||||||
|
"Please remove 'ticker_interval' from your configuration to continue operating."
|
||||||
|
)
|
||||||
|
config['timeframe'] = config['ticker_interval']
|
||||||
|
@ -22,7 +22,8 @@ ORDERBOOK_SIDES = ['ask', 'bid']
|
|||||||
ORDERTYPE_POSSIBILITIES = ['limit', 'market']
|
ORDERTYPE_POSSIBILITIES = ['limit', 'market']
|
||||||
ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
|
ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
|
||||||
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList',
|
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList',
|
||||||
'PrecisionFilter', 'PriceFilter', 'ShuffleFilter', 'SpreadFilter']
|
'AgeFilter', 'PrecisionFilter', 'PriceFilter',
|
||||||
|
'ShuffleFilter', 'SpreadFilter']
|
||||||
AVAILABLE_DATAHANDLERS = ['json', 'jsongz']
|
AVAILABLE_DATAHANDLERS = ['json', 'jsongz']
|
||||||
DRY_RUN_WALLET = 1000
|
DRY_RUN_WALLET = 1000
|
||||||
MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons
|
MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons
|
||||||
@ -71,7 +72,7 @@ CONF_SCHEMA = {
|
|||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
'max_open_trades': {'type': ['integer', 'number'], 'minimum': -1},
|
'max_open_trades': {'type': ['integer', 'number'], 'minimum': -1},
|
||||||
'ticker_interval': {'type': 'string'},
|
'timeframe': {'type': 'string'},
|
||||||
'stake_currency': {'type': 'string'},
|
'stake_currency': {'type': 'string'},
|
||||||
'stake_amount': {
|
'stake_amount': {
|
||||||
'type': ['number', 'string'],
|
'type': ['number', 'string'],
|
||||||
@ -221,12 +222,16 @@ CONF_SCHEMA = {
|
|||||||
},
|
},
|
||||||
'username': {'type': 'string'},
|
'username': {'type': 'string'},
|
||||||
'password': {'type': 'string'},
|
'password': {'type': 'string'},
|
||||||
|
'jwt_secret_key': {'type': 'string'},
|
||||||
|
'CORS_origins': {'type': 'array', 'items': {'type': 'string'}},
|
||||||
|
'verbosity': {'type': 'string', 'enum': ['error', 'info']},
|
||||||
},
|
},
|
||||||
'required': ['enabled', 'listen_ip_address', 'listen_port', 'username', 'password']
|
'required': ['enabled', 'listen_ip_address', 'listen_port', 'username', 'password']
|
||||||
},
|
},
|
||||||
'db_url': {'type': 'string'},
|
'db_url': {'type': 'string'},
|
||||||
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
|
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
|
||||||
'forcebuy_enable': {'type': 'boolean'},
|
'forcebuy_enable': {'type': 'boolean'},
|
||||||
|
'disable_dataframe_checks': {'type': 'boolean'},
|
||||||
'internals': {
|
'internals': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'default': {},
|
'default': {},
|
||||||
@ -285,7 +290,6 @@ CONF_SCHEMA = {
|
|||||||
'process_throttle_secs': {'type': 'integer', 'minimum': 600},
|
'process_throttle_secs': {'type': 'integer', 'minimum': 600},
|
||||||
'calculate_since_number_of_days': {'type': 'integer'},
|
'calculate_since_number_of_days': {'type': 'integer'},
|
||||||
'allowed_risk': {'type': 'number'},
|
'allowed_risk': {'type': 'number'},
|
||||||
'capital_available_percentage': {'type': 'number'},
|
|
||||||
'stoploss_range_min': {'type': 'number'},
|
'stoploss_range_min': {'type': 'number'},
|
||||||
'stoploss_range_max': {'type': 'number'},
|
'stoploss_range_max': {'type': 'number'},
|
||||||
'stoploss_range_step': {'type': 'number'},
|
'stoploss_range_step': {'type': 'number'},
|
||||||
@ -302,6 +306,7 @@ CONF_SCHEMA = {
|
|||||||
|
|
||||||
SCHEMA_TRADE_REQUIRED = [
|
SCHEMA_TRADE_REQUIRED = [
|
||||||
'exchange',
|
'exchange',
|
||||||
|
'timeframe',
|
||||||
'max_open_trades',
|
'max_open_trades',
|
||||||
'stake_currency',
|
'stake_currency',
|
||||||
'stake_amount',
|
'stake_amount',
|
||||||
|
@ -16,7 +16,7 @@ from freqtrade.persistence import Trade
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# must align with columns in backtest.py
|
# must align with columns in backtest.py
|
||||||
BT_DATA_COLUMNS = ["pair", "profitperc", "open_time", "close_time", "index", "duration",
|
BT_DATA_COLUMNS = ["pair", "profit_percent", "open_time", "close_time", "index", "duration",
|
||||||
"open_rate", "close_rate", "open_at_end", "sell_reason"]
|
"open_rate", "close_rate", "open_at_end", "sell_reason"]
|
||||||
|
|
||||||
|
|
||||||
@ -99,11 +99,11 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame:
|
|||||||
trades: pd.DataFrame = pd.DataFrame([], columns=BT_DATA_COLUMNS)
|
trades: pd.DataFrame = pd.DataFrame([], columns=BT_DATA_COLUMNS)
|
||||||
persistence.init(db_url, clean_open_orders=False)
|
persistence.init(db_url, clean_open_orders=False)
|
||||||
|
|
||||||
columns = ["pair", "open_time", "close_time", "profit", "profitperc",
|
columns = ["pair", "open_time", "close_time", "profit", "profit_percent",
|
||||||
"open_rate", "close_rate", "amount", "duration", "sell_reason",
|
"open_rate", "close_rate", "amount", "duration", "sell_reason",
|
||||||
"fee_open", "fee_close", "open_rate_requested", "close_rate_requested",
|
"fee_open", "fee_close", "open_rate_requested", "close_rate_requested",
|
||||||
"stake_amount", "max_rate", "min_rate", "id", "exchange",
|
"stake_amount", "max_rate", "min_rate", "id", "exchange",
|
||||||
"stop_loss", "initial_stop_loss", "strategy", "ticker_interval"]
|
"stop_loss", "initial_stop_loss", "strategy", "timeframe"]
|
||||||
|
|
||||||
trades = pd.DataFrame([(t.pair,
|
trades = pd.DataFrame([(t.pair,
|
||||||
t.open_date.replace(tzinfo=timezone.utc),
|
t.open_date.replace(tzinfo=timezone.utc),
|
||||||
@ -121,7 +121,7 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame:
|
|||||||
t.min_rate,
|
t.min_rate,
|
||||||
t.id, t.exchange,
|
t.id, t.exchange,
|
||||||
t.stop_loss, t.initial_stop_loss,
|
t.stop_loss, t.initial_stop_loss,
|
||||||
t.strategy, t.ticker_interval
|
t.strategy, t.timeframe
|
||||||
)
|
)
|
||||||
for t in Trade.get_trades().all()],
|
for t in Trade.get_trades().all()],
|
||||||
columns=columns)
|
columns=columns)
|
||||||
@ -190,7 +190,7 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str,
|
|||||||
"""
|
"""
|
||||||
Adds a column `col_name` with the cumulative profit for the given trades array.
|
Adds a column `col_name` with the cumulative profit for the given trades array.
|
||||||
:param df: DataFrame with date index
|
:param df: DataFrame with date index
|
||||||
:param trades: DataFrame containing trades (requires columns close_time and profitperc)
|
:param trades: DataFrame containing trades (requires columns close_time and profit_percent)
|
||||||
:param col_name: Column name that will be assigned the results
|
:param col_name: Column name that will be assigned the results
|
||||||
:param timeframe: Timeframe used during the operations
|
:param timeframe: Timeframe used during the operations
|
||||||
:return: Returns df with one additional column, col_name, containing the cumulative profit.
|
:return: Returns df with one additional column, col_name, containing the cumulative profit.
|
||||||
@ -201,7 +201,8 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str,
|
|||||||
from freqtrade.exchange import timeframe_to_minutes
|
from freqtrade.exchange import timeframe_to_minutes
|
||||||
timeframe_minutes = timeframe_to_minutes(timeframe)
|
timeframe_minutes = timeframe_to_minutes(timeframe)
|
||||||
# Resample to timeframe to make sure trades match candles
|
# Resample to timeframe to make sure trades match candles
|
||||||
_trades_sum = trades.resample(f'{timeframe_minutes}min', on='close_time')[['profitperc']].sum()
|
_trades_sum = trades.resample(f'{timeframe_minutes}min', on='close_time'
|
||||||
|
)[['profit_percent']].sum()
|
||||||
df.loc[:, col_name] = _trades_sum.cumsum()
|
df.loc[:, col_name] = _trades_sum.cumsum()
|
||||||
# Set first value to 0
|
# Set first value to 0
|
||||||
df.loc[df.iloc[0].name, col_name] = 0
|
df.loc[df.iloc[0].name, col_name] = 0
|
||||||
@ -211,13 +212,13 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str,
|
|||||||
|
|
||||||
|
|
||||||
def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_time',
|
def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_time',
|
||||||
value_col: str = 'profitperc'
|
value_col: str = 'profit_percent'
|
||||||
) -> Tuple[float, pd.Timestamp, pd.Timestamp]:
|
) -> Tuple[float, pd.Timestamp, pd.Timestamp]:
|
||||||
"""
|
"""
|
||||||
Calculate max drawdown and the corresponding close dates
|
Calculate max drawdown and the corresponding close dates
|
||||||
:param trades: DataFrame containing trades (requires columns close_time and profitperc)
|
:param trades: DataFrame containing trades (requires columns close_time and profit_percent)
|
||||||
:param date_col: Column in DataFrame to use for dates (defaults to 'close_time')
|
:param date_col: Column in DataFrame to use for dates (defaults to 'close_time')
|
||||||
:param value_col: Column in DataFrame to use for values (defaults to 'profitperc')
|
:param value_col: Column in DataFrame to use for values (defaults to 'profit_percent')
|
||||||
:return: Tuple (float, highdate, lowdate) with absolute max drawdown, high and low time
|
:return: Tuple (float, highdate, lowdate) with absolute max drawdown, high and low time
|
||||||
:raise: ValueError if trade-dataframe was found empty.
|
:raise: ValueError if trade-dataframe was found empty.
|
||||||
"""
|
"""
|
||||||
|
@ -197,7 +197,7 @@ def trades_to_ohlcv(trades: List, timeframe: str) -> DataFrame:
|
|||||||
df_new['date'] = df_new.index
|
df_new['date'] = df_new.index
|
||||||
# Drop 0 volume rows
|
# Drop 0 volume rows
|
||||||
df_new = df_new.dropna()
|
df_new = df_new.dropna()
|
||||||
return df_new[DEFAULT_DATAFRAME_COLUMNS]
|
return df_new.loc[:, DEFAULT_DATAFRAME_COLUMNS]
|
||||||
|
|
||||||
|
|
||||||
def convert_trades_format(config: Dict[str, Any], convert_from: str, convert_to: str, erase: bool):
|
def convert_trades_format(config: Dict[str, Any], convert_from: str, convert_to: str, erase: bool):
|
||||||
@ -236,12 +236,12 @@ def convert_ohlcv_format(config: Dict[str, Any], convert_from: str, convert_to:
|
|||||||
from freqtrade.data.history.idatahandler import get_datahandler
|
from freqtrade.data.history.idatahandler import get_datahandler
|
||||||
src = get_datahandler(config['datadir'], convert_from)
|
src = get_datahandler(config['datadir'], convert_from)
|
||||||
trg = get_datahandler(config['datadir'], convert_to)
|
trg = get_datahandler(config['datadir'], convert_to)
|
||||||
timeframes = config.get('timeframes', [config.get('ticker_interval')])
|
timeframes = config.get('timeframes', [config.get('timeframe')])
|
||||||
logger.info(f"Converting candle (OHLCV) for timeframe {timeframes}")
|
logger.info(f"Converting candle (OHLCV) for timeframe {timeframes}")
|
||||||
|
|
||||||
if 'pairs' not in config:
|
if 'pairs' not in config:
|
||||||
config['pairs'] = []
|
config['pairs'] = []
|
||||||
# Check timeframes or fall back to ticker_interval.
|
# Check timeframes or fall back to timeframe.
|
||||||
for timeframe in timeframes:
|
for timeframe in timeframes:
|
||||||
config['pairs'].extend(src.ohlcv_get_pairs(config['datadir'],
|
config['pairs'].extend(src.ohlcv_get_pairs(config['datadir'],
|
||||||
timeframe))
|
timeframe))
|
||||||
|
@ -55,7 +55,7 @@ class DataProvider:
|
|||||||
Use False only for read-only operations (where the dataframe is not modified)
|
Use False only for read-only operations (where the dataframe is not modified)
|
||||||
"""
|
"""
|
||||||
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
|
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
|
||||||
return self._exchange.klines((pair, timeframe or self._config['ticker_interval']),
|
return self._exchange.klines((pair, timeframe or self._config['timeframe']),
|
||||||
copy=copy)
|
copy=copy)
|
||||||
else:
|
else:
|
||||||
return DataFrame()
|
return DataFrame()
|
||||||
@ -67,7 +67,7 @@ class DataProvider:
|
|||||||
:param timeframe: timeframe to get data for
|
:param timeframe: timeframe to get data for
|
||||||
"""
|
"""
|
||||||
return load_pair_history(pair=pair,
|
return load_pair_history(pair=pair,
|
||||||
timeframe=timeframe or self._config['ticker_interval'],
|
timeframe=timeframe or self._config['timeframe'],
|
||||||
datadir=self._config['datadir']
|
datadir=self._config['datadir']
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -270,6 +270,11 @@ def _download_trades_history(exchange: Exchange,
|
|||||||
# DEFAULT_TRADES_COLUMNS: 0 -> timestamp
|
# DEFAULT_TRADES_COLUMNS: 0 -> timestamp
|
||||||
# DEFAULT_TRADES_COLUMNS: 1 -> id
|
# DEFAULT_TRADES_COLUMNS: 1 -> id
|
||||||
|
|
||||||
|
if trades and since < trades[0][0]:
|
||||||
|
# since is before the first trade
|
||||||
|
logger.info(f"Start earlier than available data. Redownloading trades for {pair}...")
|
||||||
|
trades = []
|
||||||
|
|
||||||
from_id = trades[-1][1] if trades else None
|
from_id = trades[-1][1] if trades else None
|
||||||
if trades and since < trades[-1][0]:
|
if trades and since < trades[-1][0]:
|
||||||
# Reset since to the last available point
|
# Reset since to the last available point
|
||||||
|
@ -57,9 +57,7 @@ class Edge:
|
|||||||
if self.config['stake_amount'] != UNLIMITED_STAKE_AMOUNT:
|
if self.config['stake_amount'] != UNLIMITED_STAKE_AMOUNT:
|
||||||
raise OperationalException('Edge works only with unlimited stake amount')
|
raise OperationalException('Edge works only with unlimited stake amount')
|
||||||
|
|
||||||
# Deprecated capital_available_percentage. Will use tradable_balance_ratio in the future.
|
self._capital_ratio: float = self.config['tradable_balance_ratio']
|
||||||
self._capital_percentage: float = self.edge_config.get(
|
|
||||||
'capital_available_percentage', self.config['tradable_balance_ratio'])
|
|
||||||
self._allowed_risk: float = self.edge_config.get('allowed_risk')
|
self._allowed_risk: float = self.edge_config.get('allowed_risk')
|
||||||
self._since_number_of_days: int = self.edge_config.get('calculate_since_number_of_days', 14)
|
self._since_number_of_days: int = self.edge_config.get('calculate_since_number_of_days', 14)
|
||||||
self._last_updated: int = 0 # Timestamp of pairs last updated time
|
self._last_updated: int = 0 # Timestamp of pairs last updated time
|
||||||
@ -100,14 +98,14 @@ class Edge:
|
|||||||
datadir=self.config['datadir'],
|
datadir=self.config['datadir'],
|
||||||
pairs=pairs,
|
pairs=pairs,
|
||||||
exchange=self.exchange,
|
exchange=self.exchange,
|
||||||
timeframe=self.strategy.ticker_interval,
|
timeframe=self.strategy.timeframe,
|
||||||
timerange=self._timerange,
|
timerange=self._timerange,
|
||||||
)
|
)
|
||||||
|
|
||||||
data = load_data(
|
data = load_data(
|
||||||
datadir=self.config['datadir'],
|
datadir=self.config['datadir'],
|
||||||
pairs=pairs,
|
pairs=pairs,
|
||||||
timeframe=self.strategy.ticker_interval,
|
timeframe=self.strategy.timeframe,
|
||||||
timerange=self._timerange,
|
timerange=self._timerange,
|
||||||
startup_candles=self.strategy.startup_candle_count,
|
startup_candles=self.strategy.startup_candle_count,
|
||||||
data_format=self.config.get('dataformat_ohlcv', 'json'),
|
data_format=self.config.get('dataformat_ohlcv', 'json'),
|
||||||
@ -157,7 +155,7 @@ class Edge:
|
|||||||
def stake_amount(self, pair: str, free_capital: float,
|
def stake_amount(self, pair: str, free_capital: float,
|
||||||
total_capital: float, capital_in_trade: float) -> float:
|
total_capital: float, capital_in_trade: float) -> float:
|
||||||
stoploss = self.stoploss(pair)
|
stoploss = self.stoploss(pair)
|
||||||
available_capital = (total_capital + capital_in_trade) * self._capital_percentage
|
available_capital = (total_capital + capital_in_trade) * self._capital_ratio
|
||||||
allowed_capital_at_risk = available_capital * self._allowed_risk
|
allowed_capital_at_risk = available_capital * self._allowed_risk
|
||||||
max_position_size = abs(allowed_capital_at_risk / stoploss)
|
max_position_size = abs(allowed_capital_at_risk / stoploss)
|
||||||
position_size = min(max_position_size, free_capital)
|
position_size = min(max_position_size, free_capital)
|
||||||
|
@ -79,7 +79,7 @@ class Exchange:
|
|||||||
|
|
||||||
if config['dry_run']:
|
if config['dry_run']:
|
||||||
logger.info('Instance is running with dry_run enabled')
|
logger.info('Instance is running with dry_run enabled')
|
||||||
|
logger.info(f"Using CCXT {ccxt.__version__}")
|
||||||
exchange_config = config['exchange']
|
exchange_config = config['exchange']
|
||||||
|
|
||||||
# Deep merge ft_has with default ft_has options
|
# Deep merge ft_has with default ft_has options
|
||||||
@ -98,12 +98,14 @@ class Exchange:
|
|||||||
|
|
||||||
# Initialize ccxt objects
|
# Initialize ccxt objects
|
||||||
ccxt_config = self._ccxt_config.copy()
|
ccxt_config = self._ccxt_config.copy()
|
||||||
ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}),
|
ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}), ccxt_config)
|
||||||
ccxt_config)
|
ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_sync_config', {}), ccxt_config)
|
||||||
self._api = self._init_ccxt(
|
|
||||||
exchange_config, ccxt_kwargs=ccxt_config)
|
self._api = self._init_ccxt(exchange_config, ccxt_kwargs=ccxt_config)
|
||||||
|
|
||||||
ccxt_async_config = self._ccxt_config.copy()
|
ccxt_async_config = self._ccxt_config.copy()
|
||||||
|
ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}),
|
||||||
|
ccxt_async_config)
|
||||||
ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_async_config', {}),
|
ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_async_config', {}),
|
||||||
ccxt_async_config)
|
ccxt_async_config)
|
||||||
self._api_async = self._init_ccxt(
|
self._api_async = self._init_ccxt(
|
||||||
@ -113,7 +115,7 @@ class Exchange:
|
|||||||
|
|
||||||
if validate:
|
if validate:
|
||||||
# Check if timeframe is available
|
# Check if timeframe is available
|
||||||
self.validate_timeframes(config.get('ticker_interval'))
|
self.validate_timeframes(config.get('timeframe'))
|
||||||
|
|
||||||
# Initial markets load
|
# Initial markets load
|
||||||
self._load_markets()
|
self._load_markets()
|
||||||
@ -188,7 +190,7 @@ class Exchange:
|
|||||||
def markets(self) -> Dict:
|
def markets(self) -> Dict:
|
||||||
"""exchange ccxt markets"""
|
"""exchange ccxt markets"""
|
||||||
if not self._api.markets:
|
if not self._api.markets:
|
||||||
logger.warning("Markets were not loaded. Loading them now..")
|
logger.info("Markets were not loaded. Loading them now..")
|
||||||
self._load_markets()
|
self._load_markets()
|
||||||
return self._api.markets
|
return self._api.markets
|
||||||
|
|
||||||
@ -273,8 +275,8 @@ class Exchange:
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
logger.warning('Unable to initialize markets. Reason: %s', e)
|
logger.warning('Unable to initialize markets. Reason: %s', e)
|
||||||
|
|
||||||
def _reload_markets(self) -> None:
|
def reload_markets(self) -> None:
|
||||||
"""Reload markets both sync and async, if refresh interval has passed"""
|
"""Reload markets both sync and async if refresh interval has passed """
|
||||||
# Check whether markets have to be reloaded
|
# Check whether markets have to be reloaded
|
||||||
if (self._last_markets_refresh > 0) and (
|
if (self._last_markets_refresh > 0) and (
|
||||||
self._last_markets_refresh + self.markets_refresh_interval
|
self._last_markets_refresh + self.markets_refresh_interval
|
||||||
@ -283,6 +285,8 @@ class Exchange:
|
|||||||
logger.debug("Performing scheduled market reload..")
|
logger.debug("Performing scheduled market reload..")
|
||||||
try:
|
try:
|
||||||
self._api.load_markets(reload=True)
|
self._api.load_markets(reload=True)
|
||||||
|
# Also reload async markets to avoid issues with newly listed pairs
|
||||||
|
self._load_async_markets(reload=True)
|
||||||
self._last_markets_refresh = arrow.utcnow().timestamp
|
self._last_markets_refresh = arrow.utcnow().timestamp
|
||||||
except ccxt.BaseError:
|
except ccxt.BaseError:
|
||||||
logger.exception("Could not reload markets.")
|
logger.exception("Could not reload markets.")
|
||||||
@ -887,14 +891,19 @@ class Exchange:
|
|||||||
Async wrapper handling downloading trades using either time or id based methods.
|
Async wrapper handling downloading trades using either time or id based methods.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
logger.debug(f"_async_get_trade_history(), pair: {pair}, "
|
||||||
|
f"since: {since}, until: {until}, from_id: {from_id}")
|
||||||
|
|
||||||
|
if until is None:
|
||||||
|
until = ccxt.Exchange.milliseconds()
|
||||||
|
logger.debug(f"Exchange milliseconds: {until}")
|
||||||
|
|
||||||
if self._trades_pagination == 'time':
|
if self._trades_pagination == 'time':
|
||||||
return await self._async_get_trade_history_time(
|
return await self._async_get_trade_history_time(
|
||||||
pair=pair, since=since,
|
pair=pair, since=since, until=until)
|
||||||
until=until or ccxt.Exchange.milliseconds())
|
|
||||||
elif self._trades_pagination == 'id':
|
elif self._trades_pagination == 'id':
|
||||||
return await self._async_get_trade_history_id(
|
return await self._async_get_trade_history_id(
|
||||||
pair=pair, since=since,
|
pair=pair, since=since, until=until, from_id=from_id
|
||||||
until=until or ccxt.Exchange.milliseconds(), from_id=from_id
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise OperationalException(f"Exchange {self.name} does use neither time, "
|
raise OperationalException(f"Exchange {self.name} does use neither time, "
|
||||||
@ -945,6 +954,9 @@ class Exchange:
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
|
# Assign method to get_stoploss_order to allow easy overriding in other classes
|
||||||
|
cancel_stoploss_order = cancel_order
|
||||||
|
|
||||||
def is_cancel_order_result_suitable(self, corder) -> bool:
|
def is_cancel_order_result_suitable(self, corder) -> bool:
|
||||||
if not isinstance(corder, dict):
|
if not isinstance(corder, dict):
|
||||||
return False
|
return False
|
||||||
@ -997,6 +1009,9 @@ class Exchange:
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
|
# Assign method to get_stoploss_order to allow easy overriding in other classes
|
||||||
|
get_stoploss_order = get_order
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict:
|
def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict:
|
||||||
"""
|
"""
|
||||||
@ -1102,9 +1117,12 @@ class Exchange:
|
|||||||
order['fee']['cost'] / safe_value_fallback(order, order, 'filled', 'amount'), 8)
|
order['fee']['cost'] / safe_value_fallback(order, order, 'filled', 'amount'), 8)
|
||||||
elif fee_curr in self.get_pair_quote_currency(order['symbol']):
|
elif fee_curr in self.get_pair_quote_currency(order['symbol']):
|
||||||
# Quote currency - divide by cost
|
# Quote currency - divide by cost
|
||||||
return round(order['fee']['cost'] / order['cost'], 8)
|
return round(order['fee']['cost'] / order['cost'], 8) if order['cost'] else None
|
||||||
else:
|
else:
|
||||||
# If Fee currency is a different currency
|
# If Fee currency is a different currency
|
||||||
|
if not order['cost']:
|
||||||
|
# If cost is None or 0.0 -> falsy, return None
|
||||||
|
return None
|
||||||
try:
|
try:
|
||||||
comb = self.get_valid_pair_combination(fee_curr, self._config['stake_currency'])
|
comb = self.get_valid_pair_combination(fee_curr, self._config['stake_currency'])
|
||||||
tick = self.fetch_ticker(comb)
|
tick = self.fetch_ticker(comb)
|
||||||
|
@ -2,7 +2,12 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
|
import ccxt
|
||||||
|
|
||||||
|
from freqtrade.exceptions import (DependencyException, InvalidOrderException,
|
||||||
|
OperationalException, TemporaryError)
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
|
from freqtrade.exchange.common import retrier
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -10,5 +15,104 @@ logger = logging.getLogger(__name__)
|
|||||||
class Ftx(Exchange):
|
class Ftx(Exchange):
|
||||||
|
|
||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
|
"stoploss_on_exchange": True,
|
||||||
"ohlcv_candle_limit": 1500,
|
"ohlcv_candle_limit": 1500,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
|
||||||
|
"""
|
||||||
|
Verify stop_loss against stoploss-order value (limit or price)
|
||||||
|
Returns True if adjustment is necessary.
|
||||||
|
"""
|
||||||
|
return order['type'] == 'stop' and stop_loss > float(order['price'])
|
||||||
|
|
||||||
|
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
|
||||||
|
"""
|
||||||
|
Creates a stoploss order.
|
||||||
|
depending on order_types.stoploss configuration, uses 'market' or limit order.
|
||||||
|
|
||||||
|
Limit orders are defined by having orderPrice set, otherwise a market order is used.
|
||||||
|
"""
|
||||||
|
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
||||||
|
limit_rate = stop_price * limit_price_pct
|
||||||
|
|
||||||
|
ordertype = "stop"
|
||||||
|
|
||||||
|
stop_price = self.price_to_precision(pair, stop_price)
|
||||||
|
|
||||||
|
if self._config['dry_run']:
|
||||||
|
dry_order = self.dry_run_order(
|
||||||
|
pair, ordertype, "sell", amount, stop_price)
|
||||||
|
return dry_order
|
||||||
|
|
||||||
|
try:
|
||||||
|
params = self._params.copy()
|
||||||
|
if order_types.get('stoploss', 'market') == 'limit':
|
||||||
|
# set orderPrice to place limit order, otherwise it's a market order
|
||||||
|
params['orderPrice'] = limit_rate
|
||||||
|
|
||||||
|
amount = self.amount_to_precision(pair, amount)
|
||||||
|
|
||||||
|
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
|
||||||
|
amount=amount, price=stop_price, params=params)
|
||||||
|
logger.info('stoploss order added for %s. '
|
||||||
|
'stop price: %s.', pair, stop_price)
|
||||||
|
return order
|
||||||
|
except ccxt.InsufficientFunds as e:
|
||||||
|
raise DependencyException(
|
||||||
|
f'Insufficient funds to create {ordertype} sell order on market {pair}. '
|
||||||
|
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
|
||||||
|
f'Message: {e}') from e
|
||||||
|
except ccxt.InvalidOrder as e:
|
||||||
|
raise InvalidOrderException(
|
||||||
|
f'Could not create {ordertype} sell order on market {pair}. '
|
||||||
|
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
|
||||||
|
f'Message: {e}') from e
|
||||||
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
|
@retrier
|
||||||
|
def get_stoploss_order(self, order_id: str, pair: str) -> Dict:
|
||||||
|
if self._config['dry_run']:
|
||||||
|
try:
|
||||||
|
order = self._dry_run_open_orders[order_id]
|
||||||
|
return order
|
||||||
|
except KeyError as e:
|
||||||
|
# Gracefully handle errors with dry-run orders.
|
||||||
|
raise InvalidOrderException(
|
||||||
|
f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e
|
||||||
|
try:
|
||||||
|
orders = self._api.fetch_orders(pair, None, params={'type': 'stop'})
|
||||||
|
|
||||||
|
order = [order for order in orders if order['id'] == order_id]
|
||||||
|
if len(order) == 1:
|
||||||
|
return order[0]
|
||||||
|
else:
|
||||||
|
raise InvalidOrderException(f"Could not get stoploss order for id {order_id}")
|
||||||
|
|
||||||
|
except ccxt.InvalidOrder as e:
|
||||||
|
raise InvalidOrderException(
|
||||||
|
f'Tried to get an invalid order (id: {order_id}). Message: {e}') from e
|
||||||
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
|
@retrier
|
||||||
|
def cancel_stoploss_order(self, order_id: str, pair: str) -> Dict:
|
||||||
|
if self._config['dry_run']:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return self._api.cancel_order(order_id, pair, params={'type': 'stop'})
|
||||||
|
except ccxt.InvalidOrder as e:
|
||||||
|
raise InvalidOrderException(
|
||||||
|
f'Could not cancel order. Message: {e}') from e
|
||||||
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e) from e
|
||||||
|
@ -139,8 +139,8 @@ class FreqtradeBot:
|
|||||||
:return: True if one or more trades has been created or closed, False otherwise
|
:return: True if one or more trades has been created or closed, False otherwise
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Check whether markets have to be reloaded
|
# Check whether markets have to be reloaded and reload them when it's needed
|
||||||
self.exchange._reload_markets()
|
self.exchange.reload_markets()
|
||||||
|
|
||||||
# Query trades from persistence layer
|
# Query trades from persistence layer
|
||||||
trades = Trade.get_open_trades()
|
trades = Trade.get_open_trades()
|
||||||
@ -421,8 +421,8 @@ class FreqtradeBot:
|
|||||||
|
|
||||||
# running get_signal on historical data fetched
|
# running get_signal on historical data fetched
|
||||||
(buy, sell) = self.strategy.get_signal(
|
(buy, sell) = self.strategy.get_signal(
|
||||||
pair, self.strategy.ticker_interval,
|
pair, self.strategy.timeframe,
|
||||||
self.dataprovider.ohlcv(pair, self.strategy.ticker_interval))
|
self.dataprovider.ohlcv(pair, self.strategy.timeframe))
|
||||||
|
|
||||||
if buy and not sell:
|
if buy and not sell:
|
||||||
stake_amount = self.get_trade_stake_amount(pair)
|
stake_amount = self.get_trade_stake_amount(pair)
|
||||||
@ -547,7 +547,7 @@ class FreqtradeBot:
|
|||||||
exchange=self.exchange.id,
|
exchange=self.exchange.id,
|
||||||
open_order_id=order_id,
|
open_order_id=order_id,
|
||||||
strategy=self.strategy.get_strategy_name(),
|
strategy=self.strategy.get_strategy_name(),
|
||||||
ticker_interval=timeframe_to_minutes(self.config['ticker_interval'])
|
timeframe=timeframe_to_minutes(self.config['timeframe'])
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update fees if order is closed
|
# Update fees if order is closed
|
||||||
@ -676,6 +676,8 @@ class FreqtradeBot:
|
|||||||
raise PricingError from e
|
raise PricingError from e
|
||||||
else:
|
else:
|
||||||
rate = self.exchange.fetch_ticker(pair)[ask_strategy['price_side']]
|
rate = self.exchange.fetch_ticker(pair)[ask_strategy['price_side']]
|
||||||
|
if rate is None:
|
||||||
|
raise PricingError(f"Sell-Rate for {pair} was empty.")
|
||||||
self._sell_rate_cache[pair] = rate
|
self._sell_rate_cache[pair] = rate
|
||||||
return rate
|
return rate
|
||||||
|
|
||||||
@ -696,15 +698,14 @@ class FreqtradeBot:
|
|||||||
if (config_ask_strategy.get('use_sell_signal', True) or
|
if (config_ask_strategy.get('use_sell_signal', True) or
|
||||||
config_ask_strategy.get('ignore_roi_if_buy_signal', False)):
|
config_ask_strategy.get('ignore_roi_if_buy_signal', False)):
|
||||||
(buy, sell) = self.strategy.get_signal(
|
(buy, sell) = self.strategy.get_signal(
|
||||||
trade.pair, self.strategy.ticker_interval,
|
trade.pair, self.strategy.timeframe,
|
||||||
self.dataprovider.ohlcv(trade.pair, self.strategy.ticker_interval))
|
self.dataprovider.ohlcv(trade.pair, self.strategy.timeframe))
|
||||||
|
|
||||||
if config_ask_strategy.get('use_order_book', False):
|
if config_ask_strategy.get('use_order_book', False):
|
||||||
# logger.debug('Order book %s',orderBook)
|
|
||||||
order_book_min = config_ask_strategy.get('order_book_min', 1)
|
order_book_min = config_ask_strategy.get('order_book_min', 1)
|
||||||
order_book_max = config_ask_strategy.get('order_book_max', 1)
|
order_book_max = config_ask_strategy.get('order_book_max', 1)
|
||||||
logger.info(f'Using order book between {order_book_min} and {order_book_max} '
|
logger.debug(f'Using order book between {order_book_min} and {order_book_max} '
|
||||||
f'for selling {trade.pair}...')
|
f'for selling {trade.pair}...')
|
||||||
|
|
||||||
order_book = self._order_book_gen(trade.pair, f"{config_ask_strategy['price_side']}s",
|
order_book = self._order_book_gen(trade.pair, f"{config_ask_strategy['price_side']}s",
|
||||||
order_book_min=order_book_min,
|
order_book_min=order_book_min,
|
||||||
@ -719,6 +720,9 @@ class FreqtradeBot:
|
|||||||
raise PricingError from e
|
raise PricingError from e
|
||||||
logger.debug(f" order book {config_ask_strategy['price_side']} top {i}: "
|
logger.debug(f" order book {config_ask_strategy['price_side']} top {i}: "
|
||||||
f"{sell_rate:0.8f}")
|
f"{sell_rate:0.8f}")
|
||||||
|
# Assign sell-rate to cache - otherwise sell-rate is never updated in the cache,
|
||||||
|
# resulting in outdated RPC messages
|
||||||
|
self._sell_rate_cache[trade.pair] = sell_rate
|
||||||
|
|
||||||
if self._check_and_execute_sell(trade, sell_rate, buy, sell):
|
if self._check_and_execute_sell(trade, sell_rate, buy, sell):
|
||||||
return True
|
return True
|
||||||
@ -769,18 +773,18 @@ class FreqtradeBot:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# First we check if there is already a stoploss on exchange
|
# First we check if there is already a stoploss on exchange
|
||||||
stoploss_order = self.exchange.get_order(trade.stoploss_order_id, trade.pair) \
|
stoploss_order = self.exchange.get_stoploss_order(trade.stoploss_order_id, trade.pair) \
|
||||||
if trade.stoploss_order_id else None
|
if trade.stoploss_order_id else None
|
||||||
except InvalidOrderException as exception:
|
except InvalidOrderException as exception:
|
||||||
logger.warning('Unable to fetch stoploss order: %s', exception)
|
logger.warning('Unable to fetch stoploss order: %s', exception)
|
||||||
|
|
||||||
# We check if stoploss order is fulfilled
|
# We check if stoploss order is fulfilled
|
||||||
if stoploss_order and stoploss_order['status'] == 'closed':
|
if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'):
|
||||||
trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
|
trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
|
||||||
self.update_trade_state(trade, stoploss_order, sl_order=True)
|
self.update_trade_state(trade, stoploss_order, sl_order=True)
|
||||||
# Lock pair for one candle to prevent immediate rebuys
|
# Lock pair for one candle to prevent immediate rebuys
|
||||||
self.strategy.lock_pair(trade.pair,
|
self.strategy.lock_pair(trade.pair,
|
||||||
timeframe_to_next_date(self.config['ticker_interval']))
|
timeframe_to_next_date(self.config['timeframe']))
|
||||||
self._notify_sell(trade, "stoploss")
|
self._notify_sell(trade, "stoploss")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -802,7 +806,7 @@ class FreqtradeBot:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# If stoploss order is canceled for some reason we add it
|
# If stoploss order is canceled for some reason we add it
|
||||||
if stoploss_order and stoploss_order['status'] == 'canceled':
|
if stoploss_order and stoploss_order['status'] in ('canceled', 'cancelled'):
|
||||||
if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss,
|
if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss,
|
||||||
rate=trade.stop_loss):
|
rate=trade.stop_loss):
|
||||||
return False
|
return False
|
||||||
@ -835,7 +839,7 @@ class FreqtradeBot:
|
|||||||
logger.info('Trailing stoploss: cancelling current stoploss on exchange (id:{%s}) '
|
logger.info('Trailing stoploss: cancelling current stoploss on exchange (id:{%s}) '
|
||||||
'in order to add another one ...', order['id'])
|
'in order to add another one ...', order['id'])
|
||||||
try:
|
try:
|
||||||
self.exchange.cancel_order(order['id'], trade.pair)
|
self.exchange.cancel_stoploss_order(order['id'], trade.pair)
|
||||||
except InvalidOrderException:
|
except InvalidOrderException:
|
||||||
logger.exception(f"Could not cancel stoploss order {order['id']} "
|
logger.exception(f"Could not cancel stoploss order {order['id']} "
|
||||||
f"for pair {trade.pair}")
|
f"for pair {trade.pair}")
|
||||||
@ -1063,7 +1067,7 @@ class FreqtradeBot:
|
|||||||
# First cancelling stoploss on exchange ...
|
# First cancelling stoploss on exchange ...
|
||||||
if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id:
|
if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id:
|
||||||
try:
|
try:
|
||||||
self.exchange.cancel_order(trade.stoploss_order_id, trade.pair)
|
self.exchange.cancel_stoploss_order(trade.stoploss_order_id, trade.pair)
|
||||||
except InvalidOrderException:
|
except InvalidOrderException:
|
||||||
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
|
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
|
||||||
|
|
||||||
@ -1090,7 +1094,7 @@ class FreqtradeBot:
|
|||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
|
|
||||||
# Lock pair for one candle to prevent immediate rebuys
|
# Lock pair for one candle to prevent immediate rebuys
|
||||||
self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['ticker_interval']))
|
self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['timeframe']))
|
||||||
|
|
||||||
self._notify_sell(trade, order_type)
|
self._notify_sell(trade, order_type)
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ from freqtrade.exceptions import OperationalException
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _set_loggers(verbosity: int = 0) -> None:
|
def _set_loggers(verbosity: int = 0, api_verbosity: str = 'info') -> None:
|
||||||
"""
|
"""
|
||||||
Set the logging level for third party libraries
|
Set the logging level for third party libraries
|
||||||
:return: None
|
:return: None
|
||||||
@ -28,6 +28,10 @@ def _set_loggers(verbosity: int = 0) -> None:
|
|||||||
)
|
)
|
||||||
logging.getLogger('telegram').setLevel(logging.INFO)
|
logging.getLogger('telegram').setLevel(logging.INFO)
|
||||||
|
|
||||||
|
logging.getLogger('werkzeug').setLevel(
|
||||||
|
logging.ERROR if api_verbosity == 'error' else logging.INFO
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_logging(config: Dict[str, Any]) -> None:
|
def setup_logging(config: Dict[str, Any]) -> None:
|
||||||
"""
|
"""
|
||||||
@ -77,5 +81,5 @@ def setup_logging(config: Dict[str, Any]) -> None:
|
|||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
handlers=log_handlers
|
handlers=log_handlers
|
||||||
)
|
)
|
||||||
_set_loggers(verbosity)
|
_set_loggers(verbosity, config.get('api_server', {}).get('verbosity', 'info'))
|
||||||
logger.info('Verbosity set to %s', verbosity)
|
logger.info('Verbosity set to %s', verbosity)
|
||||||
|
@ -18,7 +18,8 @@ from freqtrade.data.converter import trim_dataframe
|
|||||||
from freqtrade.data.dataprovider import DataProvider
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
|
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
|
||||||
from freqtrade.optimize.optimize_reports import (show_backtest_results,
|
from freqtrade.optimize.optimize_reports import (generate_backtest_stats,
|
||||||
|
show_backtest_results,
|
||||||
store_backtest_result)
|
store_backtest_result)
|
||||||
from freqtrade.pairlist.pairlistmanager import PairListManager
|
from freqtrade.pairlist.pairlistmanager import PairListManager
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
@ -64,20 +65,6 @@ class Backtesting:
|
|||||||
self.strategylist: List[IStrategy] = []
|
self.strategylist: List[IStrategy] = []
|
||||||
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
|
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
|
||||||
|
|
||||||
self.pairlists = PairListManager(self.exchange, self.config)
|
|
||||||
if 'VolumePairList' in self.pairlists.name_list:
|
|
||||||
raise OperationalException("VolumePairList not allowed for backtesting.")
|
|
||||||
|
|
||||||
self.pairlists.refresh_pairlist()
|
|
||||||
|
|
||||||
if len(self.pairlists.whitelist) == 0:
|
|
||||||
raise OperationalException("No pair in whitelist.")
|
|
||||||
|
|
||||||
if config.get('fee'):
|
|
||||||
self.fee = config['fee']
|
|
||||||
else:
|
|
||||||
self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0])
|
|
||||||
|
|
||||||
if self.config.get('runmode') != RunMode.HYPEROPT:
|
if self.config.get('runmode') != RunMode.HYPEROPT:
|
||||||
self.dataprovider = DataProvider(self.config, self.exchange)
|
self.dataprovider = DataProvider(self.config, self.exchange)
|
||||||
IStrategy.dp = self.dataprovider
|
IStrategy.dp = self.dataprovider
|
||||||
@ -94,12 +81,31 @@ class Backtesting:
|
|||||||
self.strategylist.append(StrategyResolver.load_strategy(self.config))
|
self.strategylist.append(StrategyResolver.load_strategy(self.config))
|
||||||
validate_config_consistency(self.config)
|
validate_config_consistency(self.config)
|
||||||
|
|
||||||
if "ticker_interval" not in self.config:
|
if "timeframe" not in self.config:
|
||||||
raise OperationalException("Timeframe (ticker interval) needs to be set in either "
|
raise OperationalException("Timeframe (ticker interval) needs to be set in either "
|
||||||
"configuration or as cli argument `--ticker-interval 5m`")
|
"configuration or as cli argument `--timeframe 5m`")
|
||||||
self.timeframe = str(self.config.get('ticker_interval'))
|
self.timeframe = str(self.config.get('timeframe'))
|
||||||
self.timeframe_min = timeframe_to_minutes(self.timeframe)
|
self.timeframe_min = timeframe_to_minutes(self.timeframe)
|
||||||
|
|
||||||
|
self.pairlists = PairListManager(self.exchange, self.config)
|
||||||
|
if 'VolumePairList' in self.pairlists.name_list:
|
||||||
|
raise OperationalException("VolumePairList not allowed for backtesting.")
|
||||||
|
|
||||||
|
if len(self.strategylist) > 1 and 'PrecisionFilter' in self.pairlists.name_list:
|
||||||
|
raise OperationalException(
|
||||||
|
"PrecisionFilter not allowed for backtesting multiple strategies."
|
||||||
|
)
|
||||||
|
|
||||||
|
self.pairlists.refresh_pairlist()
|
||||||
|
|
||||||
|
if len(self.pairlists.whitelist) == 0:
|
||||||
|
raise OperationalException("No pair in whitelist.")
|
||||||
|
|
||||||
|
if config.get('fee'):
|
||||||
|
self.fee = config['fee']
|
||||||
|
else:
|
||||||
|
self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0])
|
||||||
|
|
||||||
# Get maximum required startup period
|
# Get maximum required startup period
|
||||||
self.required_startup = max([strat.startup_candle_count for strat in self.strategylist])
|
self.required_startup = max([strat.startup_candle_count for strat in self.strategylist])
|
||||||
# Load one (first) strategy
|
# Load one (first) strategy
|
||||||
@ -411,4 +417,5 @@ class Backtesting:
|
|||||||
if self.config.get('export', False):
|
if self.config.get('export', False):
|
||||||
store_backtest_result(self.config['exportfilename'], all_results)
|
store_backtest_result(self.config['exportfilename'], all_results)
|
||||||
# Show backtest results
|
# Show backtest results
|
||||||
show_backtest_results(self.config, data, all_results)
|
stats = generate_backtest_stats(self.config, data, all_results)
|
||||||
|
show_backtest_results(self.config, stats)
|
||||||
|
@ -42,8 +42,8 @@ class DefaultHyperOptLoss(IHyperOptLoss):
|
|||||||
* 0.25: Avoiding trade loss
|
* 0.25: Avoiding trade loss
|
||||||
* 1.0 to total profit, compared to the expected value (`EXPECTED_MAX_PROFIT`) defined above
|
* 1.0 to total profit, compared to the expected value (`EXPECTED_MAX_PROFIT`) defined above
|
||||||
"""
|
"""
|
||||||
total_profit = results.profit_percent.sum()
|
total_profit = results['profit_percent'].sum()
|
||||||
trade_duration = results.trade_duration.mean()
|
trade_duration = results['trade_duration'].mean()
|
||||||
|
|
||||||
trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8)
|
trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8)
|
||||||
profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT)
|
profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT)
|
||||||
|
@ -12,7 +12,7 @@ from math import ceil
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from pprint import pprint
|
from pprint import pformat
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
import rapidjson
|
import rapidjson
|
||||||
@ -230,6 +230,9 @@ class Hyperopt:
|
|||||||
if space in ['buy', 'sell']:
|
if space in ['buy', 'sell']:
|
||||||
result_dict.setdefault('params', {}).update(space_params)
|
result_dict.setdefault('params', {}).update(space_params)
|
||||||
elif space == 'roi':
|
elif space == 'roi':
|
||||||
|
# TODO: get rid of OrderedDict when support for python 3.6 will be
|
||||||
|
# dropped (dicts keep the order as the language feature)
|
||||||
|
|
||||||
# Convert keys in min_roi dict to strings because
|
# Convert keys in min_roi dict to strings because
|
||||||
# rapidjson cannot dump dicts with integer keys...
|
# rapidjson cannot dump dicts with integer keys...
|
||||||
# OrderedDict is used to keep the numeric order of the items
|
# OrderedDict is used to keep the numeric order of the items
|
||||||
@ -244,11 +247,24 @@ class Hyperopt:
|
|||||||
def _params_pretty_print(params, space: str, header: str) -> None:
|
def _params_pretty_print(params, space: str, header: str) -> None:
|
||||||
if space in params:
|
if space in params:
|
||||||
space_params = Hyperopt._space_params(params, space, 5)
|
space_params = Hyperopt._space_params(params, space, 5)
|
||||||
|
params_result = f"\n# {header}\n"
|
||||||
if space == 'stoploss':
|
if space == 'stoploss':
|
||||||
print(header, space_params.get('stoploss'))
|
params_result += f"stoploss = {space_params.get('stoploss')}"
|
||||||
|
elif space == 'roi':
|
||||||
|
# TODO: get rid of OrderedDict when support for python 3.6 will be
|
||||||
|
# dropped (dicts keep the order as the language feature)
|
||||||
|
minimal_roi_result = rapidjson.dumps(
|
||||||
|
OrderedDict(
|
||||||
|
(str(k), v) for k, v in space_params.items()
|
||||||
|
),
|
||||||
|
default=str, indent=4, number_mode=rapidjson.NM_NATIVE)
|
||||||
|
params_result += f"minimal_roi = {minimal_roi_result}"
|
||||||
else:
|
else:
|
||||||
print(header)
|
params_result += f"{space}_params = {pformat(space_params, indent=4)}"
|
||||||
pprint(space_params, indent=4)
|
params_result = params_result.replace("}", "\n}").replace("{", "{\n ")
|
||||||
|
|
||||||
|
params_result = params_result.replace("\n", "\n ")
|
||||||
|
print(params_result)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _space_params(params, space: str, r: int = None) -> Dict:
|
def _space_params(params, space: str, r: int = None) -> Dict:
|
||||||
|
@ -31,13 +31,15 @@ class IHyperOpt(ABC):
|
|||||||
Class attributes you can use:
|
Class attributes you can use:
|
||||||
ticker_interval -> int: value of the ticker interval to use for the strategy
|
ticker_interval -> int: value of the ticker interval to use for the strategy
|
||||||
"""
|
"""
|
||||||
ticker_interval: str
|
ticker_interval: str # DEPRECATED
|
||||||
|
timeframe: str
|
||||||
|
|
||||||
def __init__(self, config: dict) -> None:
|
def __init__(self, config: dict) -> None:
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
# Assign ticker_interval to be used in hyperopt
|
# Assign ticker_interval to be used in hyperopt
|
||||||
IHyperOpt.ticker_interval = str(config['ticker_interval'])
|
IHyperOpt.ticker_interval = str(config['timeframe']) # DEPRECATED
|
||||||
|
IHyperOpt.timeframe = str(config['timeframe'])
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
|
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
|
||||||
@ -218,9 +220,10 @@ class IHyperOpt(ABC):
|
|||||||
# Why do I still need such shamanic mantras in modern python?
|
# Why do I still need such shamanic mantras in modern python?
|
||||||
def __getstate__(self):
|
def __getstate__(self):
|
||||||
state = self.__dict__.copy()
|
state = self.__dict__.copy()
|
||||||
state['ticker_interval'] = self.ticker_interval
|
state['timeframe'] = self.timeframe
|
||||||
return state
|
return state
|
||||||
|
|
||||||
def __setstate__(self, state):
|
def __setstate__(self, state):
|
||||||
self.__dict__.update(state)
|
self.__dict__.update(state)
|
||||||
IHyperOpt.ticker_interval = state['ticker_interval']
|
IHyperOpt.ticker_interval = state['timeframe']
|
||||||
|
IHyperOpt.timeframe = state['timeframe']
|
||||||
|
@ -14,7 +14,7 @@ class IHyperOptLoss(ABC):
|
|||||||
Interface for freqtrade hyperopt Loss functions.
|
Interface for freqtrade hyperopt Loss functions.
|
||||||
Defines the custom loss function (`hyperopt_loss_function()` which is evaluated every epoch.)
|
Defines the custom loss function (`hyperopt_loss_function()` which is evaluated every epoch.)
|
||||||
"""
|
"""
|
||||||
ticker_interval: str
|
timeframe: str
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
@ -34,5 +34,5 @@ class OnlyProfitHyperOptLoss(IHyperOptLoss):
|
|||||||
"""
|
"""
|
||||||
Objective function, returns smaller number for better results.
|
Objective function, returns smaller number for better results.
|
||||||
"""
|
"""
|
||||||
total_profit = results.profit_percent.sum()
|
total_profit = results['profit_percent'].sum()
|
||||||
return 1 - total_profit / EXPECTED_MAX_PROFIT
|
return 1 - total_profit / EXPECTED_MAX_PROFIT
|
||||||
|
@ -18,10 +18,7 @@ def store_backtest_result(recordfilename: Path, all_results: Dict[str, DataFrame
|
|||||||
:param all_results: Dict of Dataframes, one results dataframe per strategy
|
:param all_results: Dict of Dataframes, one results dataframe per strategy
|
||||||
"""
|
"""
|
||||||
for strategy, results in all_results.items():
|
for strategy, results in all_results.items():
|
||||||
records = [(t.pair, t.profit_percent, t.open_time.timestamp(),
|
records = backtest_result_to_list(results)
|
||||||
t.close_time.timestamp(), t.open_index - 1, t.trade_duration,
|
|
||||||
t.open_rate, t.close_rate, t.open_at_end, t.sell_reason.value)
|
|
||||||
for index, t in results.iterrows()]
|
|
||||||
|
|
||||||
if records:
|
if records:
|
||||||
filename = recordfilename
|
filename = recordfilename
|
||||||
@ -34,6 +31,18 @@ def store_backtest_result(recordfilename: Path, all_results: Dict[str, DataFrame
|
|||||||
file_dump_json(filename, records)
|
file_dump_json(filename, records)
|
||||||
|
|
||||||
|
|
||||||
|
def backtest_result_to_list(results: DataFrame) -> List[List]:
|
||||||
|
"""
|
||||||
|
Converts a list of Backtest-results to list
|
||||||
|
:param results: Dataframe containing results for one strategy
|
||||||
|
:return: List of Lists containing the trades
|
||||||
|
"""
|
||||||
|
return [[t.pair, t.profit_percent, t.open_time.timestamp(),
|
||||||
|
t.close_time.timestamp(), t.open_index - 1, t.trade_duration,
|
||||||
|
t.open_rate, t.close_rate, t.open_at_end, t.sell_reason.value]
|
||||||
|
for index, t in results.iterrows()]
|
||||||
|
|
||||||
|
|
||||||
def _get_line_floatfmt() -> List[str]:
|
def _get_line_floatfmt() -> List[str]:
|
||||||
"""
|
"""
|
||||||
Generate floatformat (goes in line with _generate_result_line())
|
Generate floatformat (goes in line with _generate_result_line())
|
||||||
@ -56,25 +65,25 @@ def _generate_result_line(result: DataFrame, max_open_trades: int, first_column:
|
|||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
'key': first_column,
|
'key': first_column,
|
||||||
'trades': len(result.index),
|
'trades': len(result),
|
||||||
'profit_mean': result.profit_percent.mean(),
|
'profit_mean': result['profit_percent'].mean(),
|
||||||
'profit_mean_pct': result.profit_percent.mean() * 100.0,
|
'profit_mean_pct': result['profit_percent'].mean() * 100.0,
|
||||||
'profit_sum': result.profit_percent.sum(),
|
'profit_sum': result['profit_percent'].sum(),
|
||||||
'profit_sum_pct': result.profit_percent.sum() * 100.0,
|
'profit_sum_pct': result['profit_percent'].sum() * 100.0,
|
||||||
'profit_total_abs': result.profit_abs.sum(),
|
'profit_total_abs': result['profit_abs'].sum(),
|
||||||
'profit_total_pct': result.profit_percent.sum() * 100.0 / max_open_trades,
|
'profit_total_pct': result['profit_percent'].sum() * 100.0 / max_open_trades,
|
||||||
'duration_avg': str(timedelta(
|
'duration_avg': str(timedelta(
|
||||||
minutes=round(result.trade_duration.mean()))
|
minutes=round(result['trade_duration'].mean()))
|
||||||
) if not result.empty else '0:00',
|
) if not result.empty else '0:00',
|
||||||
# 'duration_max': str(timedelta(
|
# 'duration_max': str(timedelta(
|
||||||
# minutes=round(result.trade_duration.max()))
|
# minutes=round(result['trade_duration'].max()))
|
||||||
# ) if not result.empty else '0:00',
|
# ) if not result.empty else '0:00',
|
||||||
# 'duration_min': str(timedelta(
|
# 'duration_min': str(timedelta(
|
||||||
# minutes=round(result.trade_duration.min()))
|
# minutes=round(result['trade_duration'].min()))
|
||||||
# ) if not result.empty else '0:00',
|
# ) if not result.empty else '0:00',
|
||||||
'wins': len(result[result.profit_abs > 0]),
|
'wins': len(result[result['profit_abs'] > 0]),
|
||||||
'draws': len(result[result.profit_abs == 0]),
|
'draws': len(result[result['profit_abs'] == 0]),
|
||||||
'losses': len(result[result.profit_abs < 0]),
|
'losses': len(result[result['profit_abs'] < 0]),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -93,8 +102,8 @@ def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, max_open_t
|
|||||||
tabular_data = []
|
tabular_data = []
|
||||||
|
|
||||||
for pair in data:
|
for pair in data:
|
||||||
result = results[results.pair == pair]
|
result = results[results['pair'] == pair]
|
||||||
if skip_nan and result.profit_abs.isnull().all():
|
if skip_nan and result['profit_abs'].isnull().all():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
tabular_data.append(_generate_result_line(result, max_open_trades, pair))
|
tabular_data.append(_generate_result_line(result, max_open_trades, pair))
|
||||||
@ -104,25 +113,6 @@ def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, max_open_t
|
|||||||
return tabular_data
|
return tabular_data
|
||||||
|
|
||||||
|
|
||||||
def generate_text_table(pair_results: List[Dict[str, Any]], stake_currency: str) -> str:
|
|
||||||
"""
|
|
||||||
Generates and returns a text table for the given backtest data and the results dataframe
|
|
||||||
:param pair_results: List of Dictionaries - one entry per pair + final TOTAL row
|
|
||||||
:param stake_currency: stake-currency - used to correctly name headers
|
|
||||||
:return: pretty printed table with tabulate as string
|
|
||||||
"""
|
|
||||||
|
|
||||||
headers = _get_line_header('Pair', stake_currency)
|
|
||||||
floatfmt = _get_line_floatfmt()
|
|
||||||
output = [[
|
|
||||||
t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'],
|
|
||||||
t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses']
|
|
||||||
] for t in pair_results]
|
|
||||||
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
|
||||||
return tabulate(output, headers=headers,
|
|
||||||
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]:
|
def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
Generate small table outlining Backtest results
|
Generate small table outlining Backtest results
|
||||||
@ -157,33 +147,6 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List
|
|||||||
return tabular_data
|
return tabular_data
|
||||||
|
|
||||||
|
|
||||||
def generate_text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]],
|
|
||||||
stake_currency: str) -> str:
|
|
||||||
"""
|
|
||||||
Generate small table outlining Backtest results
|
|
||||||
:param sell_reason_stats: Sell reason metrics
|
|
||||||
:param stake_currency: Stakecurrency used
|
|
||||||
:return: pretty printed table with tabulate as string
|
|
||||||
"""
|
|
||||||
headers = [
|
|
||||||
'Sell Reason',
|
|
||||||
'Sells',
|
|
||||||
'Wins',
|
|
||||||
'Draws',
|
|
||||||
'Losses',
|
|
||||||
'Avg Profit %',
|
|
||||||
'Cum Profit %',
|
|
||||||
f'Tot Profit {stake_currency}',
|
|
||||||
'Tot Profit %',
|
|
||||||
]
|
|
||||||
|
|
||||||
output = [[
|
|
||||||
t['sell_reason'], t['trades'], t['wins'], t['draws'], t['losses'],
|
|
||||||
t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], t['profit_pct_total'],
|
|
||||||
] for t in sell_reason_stats]
|
|
||||||
return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right")
|
|
||||||
|
|
||||||
|
|
||||||
def generate_strategy_metrics(stake_currency: str, max_open_trades: int,
|
def generate_strategy_metrics(stake_currency: str, max_open_trades: int,
|
||||||
all_results: Dict) -> List[Dict]:
|
all_results: Dict) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
@ -200,26 +163,6 @@ def generate_strategy_metrics(stake_currency: str, max_open_trades: int,
|
|||||||
return tabular_data
|
return tabular_data
|
||||||
|
|
||||||
|
|
||||||
def generate_text_table_strategy(strategy_results, stake_currency: str) -> str:
|
|
||||||
"""
|
|
||||||
Generate summary table per strategy
|
|
||||||
:param stake_currency: stake-currency - used to correctly name headers
|
|
||||||
:param max_open_trades: Maximum allowed open trades used for backtest
|
|
||||||
:param all_results: Dict of <Strategyname: BacktestResult> containing results for all strategies
|
|
||||||
:return: pretty printed table with tabulate as string
|
|
||||||
"""
|
|
||||||
floatfmt = _get_line_floatfmt()
|
|
||||||
headers = _get_line_header('Strategy', stake_currency)
|
|
||||||
|
|
||||||
output = [[
|
|
||||||
t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'],
|
|
||||||
t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses']
|
|
||||||
] for t in strategy_results]
|
|
||||||
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
|
||||||
return tabulate(output, headers=headers,
|
|
||||||
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
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')
|
||||||
@ -246,12 +189,20 @@ def generate_edge_table(results: dict) -> str:
|
|||||||
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore
|
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore
|
||||||
|
|
||||||
|
|
||||||
def show_backtest_results(config: Dict, btdata: Dict[str, DataFrame],
|
def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame],
|
||||||
all_results: Dict[str, DataFrame]):
|
all_results: Dict[str, DataFrame]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
:param config: Configuration object used for backtest
|
||||||
|
:param btdata: Backtest data
|
||||||
|
:param all_results: backtest result - dictionary with { Strategy: results}.
|
||||||
|
:return:
|
||||||
|
Dictionary containing results per strategy and a stratgy summary.
|
||||||
|
"""
|
||||||
stake_currency = config['stake_currency']
|
stake_currency = config['stake_currency']
|
||||||
max_open_trades = config['max_open_trades']
|
max_open_trades = config['max_open_trades']
|
||||||
|
result: Dict[str, Any] = {'strategy': {}}
|
||||||
for strategy, results in all_results.items():
|
for strategy, results in all_results.items():
|
||||||
|
|
||||||
pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
|
pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
|
||||||
max_open_trades=max_open_trades,
|
max_open_trades=max_open_trades,
|
||||||
results=results, skip_nan=False)
|
results=results, skip_nan=False)
|
||||||
@ -261,21 +212,111 @@ def show_backtest_results(config: Dict, btdata: Dict[str, DataFrame],
|
|||||||
max_open_trades=max_open_trades,
|
max_open_trades=max_open_trades,
|
||||||
results=results.loc[results['open_at_end']],
|
results=results.loc[results['open_at_end']],
|
||||||
skip_nan=True)
|
skip_nan=True)
|
||||||
|
strat_stats = {
|
||||||
|
'trades': backtest_result_to_list(results),
|
||||||
|
'results_per_pair': pair_results,
|
||||||
|
'sell_reason_summary': sell_reason_stats,
|
||||||
|
'left_open_trades': left_open_results,
|
||||||
|
}
|
||||||
|
result['strategy'][strategy] = strat_stats
|
||||||
|
|
||||||
|
strategy_results = generate_strategy_metrics(stake_currency=stake_currency,
|
||||||
|
max_open_trades=max_open_trades,
|
||||||
|
all_results=all_results)
|
||||||
|
|
||||||
|
result['strategy_comparison'] = strategy_results
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
###
|
||||||
|
# Start output section
|
||||||
|
###
|
||||||
|
|
||||||
|
def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: str) -> str:
|
||||||
|
"""
|
||||||
|
Generates and returns a text table for the given backtest data and the results dataframe
|
||||||
|
:param pair_results: List of Dictionaries - one entry per pair + final TOTAL row
|
||||||
|
:param stake_currency: stake-currency - used to correctly name headers
|
||||||
|
:return: pretty printed table with tabulate as string
|
||||||
|
"""
|
||||||
|
|
||||||
|
headers = _get_line_header('Pair', stake_currency)
|
||||||
|
floatfmt = _get_line_floatfmt()
|
||||||
|
output = [[
|
||||||
|
t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'],
|
||||||
|
t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses']
|
||||||
|
] for t in pair_results]
|
||||||
|
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
||||||
|
return tabulate(output, headers=headers,
|
||||||
|
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right")
|
||||||
|
|
||||||
|
|
||||||
|
def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_currency: str) -> str:
|
||||||
|
"""
|
||||||
|
Generate small table outlining Backtest results
|
||||||
|
:param sell_reason_stats: Sell reason metrics
|
||||||
|
:param stake_currency: Stakecurrency used
|
||||||
|
:return: pretty printed table with tabulate as string
|
||||||
|
"""
|
||||||
|
headers = [
|
||||||
|
'Sell Reason',
|
||||||
|
'Sells',
|
||||||
|
'Wins',
|
||||||
|
'Draws',
|
||||||
|
'Losses',
|
||||||
|
'Avg Profit %',
|
||||||
|
'Cum Profit %',
|
||||||
|
f'Tot Profit {stake_currency}',
|
||||||
|
'Tot Profit %',
|
||||||
|
]
|
||||||
|
|
||||||
|
output = [[
|
||||||
|
t['sell_reason'], t['trades'], t['wins'], t['draws'], t['losses'],
|
||||||
|
t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], t['profit_pct_total'],
|
||||||
|
] for t in sell_reason_stats]
|
||||||
|
return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right")
|
||||||
|
|
||||||
|
|
||||||
|
def text_table_strategy(strategy_results, stake_currency: str) -> str:
|
||||||
|
"""
|
||||||
|
Generate summary table per strategy
|
||||||
|
:param stake_currency: stake-currency - used to correctly name headers
|
||||||
|
:param max_open_trades: Maximum allowed open trades used for backtest
|
||||||
|
:param all_results: Dict of <Strategyname: BacktestResult> containing results for all strategies
|
||||||
|
:return: pretty printed table with tabulate as string
|
||||||
|
"""
|
||||||
|
floatfmt = _get_line_floatfmt()
|
||||||
|
headers = _get_line_header('Strategy', stake_currency)
|
||||||
|
|
||||||
|
output = [[
|
||||||
|
t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'],
|
||||||
|
t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses']
|
||||||
|
] for t in strategy_results]
|
||||||
|
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
||||||
|
return tabulate(output, headers=headers,
|
||||||
|
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right")
|
||||||
|
|
||||||
|
|
||||||
|
def show_backtest_results(config: Dict, backtest_stats: Dict):
|
||||||
|
stake_currency = config['stake_currency']
|
||||||
|
|
||||||
|
for strategy, results in backtest_stats['strategy'].items():
|
||||||
|
|
||||||
# Print results
|
# Print results
|
||||||
print(f"Result for strategy {strategy}")
|
print(f"Result for strategy {strategy}")
|
||||||
table = generate_text_table(pair_results, stake_currency=stake_currency)
|
table = text_table_bt_results(results['results_per_pair'], stake_currency=stake_currency)
|
||||||
if isinstance(table, str):
|
if isinstance(table, str):
|
||||||
print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '='))
|
print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '='))
|
||||||
print(table)
|
print(table)
|
||||||
|
|
||||||
table = generate_text_table_sell_reason(sell_reason_stats=sell_reason_stats,
|
table = text_table_sell_reason(sell_reason_stats=results['sell_reason_summary'],
|
||||||
stake_currency=stake_currency,
|
stake_currency=stake_currency)
|
||||||
)
|
|
||||||
if isinstance(table, str):
|
if isinstance(table, str):
|
||||||
print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '='))
|
print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '='))
|
||||||
print(table)
|
print(table)
|
||||||
|
|
||||||
table = generate_text_table(left_open_results, stake_currency=stake_currency)
|
table = text_table_bt_results(results['left_open_trades'], stake_currency=stake_currency)
|
||||||
if isinstance(table, str):
|
if isinstance(table, str):
|
||||||
print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '='))
|
print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '='))
|
||||||
print(table)
|
print(table)
|
||||||
@ -283,13 +324,10 @@ def show_backtest_results(config: Dict, btdata: Dict[str, DataFrame],
|
|||||||
print('=' * len(table.splitlines()[0]))
|
print('=' * len(table.splitlines()[0]))
|
||||||
print()
|
print()
|
||||||
|
|
||||||
if len(all_results) > 1:
|
if len(backtest_stats['strategy']) > 1:
|
||||||
# Print Strategy summary table
|
# Print Strategy summary table
|
||||||
strategy_results = generate_strategy_metrics(stake_currency=stake_currency,
|
|
||||||
max_open_trades=max_open_trades,
|
|
||||||
all_results=all_results)
|
|
||||||
|
|
||||||
table = generate_text_table_strategy(strategy_results, stake_currency)
|
table = text_table_strategy(backtest_stats['strategy_comparison'], stake_currency)
|
||||||
print(' STRATEGY SUMMARY '.center(len(table.splitlines()[0]), '='))
|
print(' STRATEGY SUMMARY '.center(len(table.splitlines()[0]), '='))
|
||||||
print(table)
|
print(table)
|
||||||
print('=' * len(table.splitlines()[0]))
|
print('=' * len(table.splitlines()[0]))
|
||||||
|
76
freqtrade/pairlist/AgeFilter.py
Normal file
76
freqtrade/pairlist/AgeFilter.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
"""
|
||||||
|
Minimum age (days listed) pair list filter
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import arrow
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from freqtrade.misc import plural
|
||||||
|
from freqtrade.pairlist.IPairList import IPairList
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AgeFilter(IPairList):
|
||||||
|
|
||||||
|
# Checked symbols cache (dictionary of ticker symbol => timestamp)
|
||||||
|
_symbolsChecked: Dict[str, int] = {}
|
||||||
|
|
||||||
|
def __init__(self, exchange, pairlistmanager,
|
||||||
|
config: Dict[str, Any], pairlistconfig: Dict[str, Any],
|
||||||
|
pairlist_pos: int) -> None:
|
||||||
|
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||||
|
|
||||||
|
self._min_days_listed = pairlistconfig.get('min_days_listed', 10)
|
||||||
|
self._enabled = self._min_days_listed >= 1
|
||||||
|
|
||||||
|
@property
|
||||||
|
def needstickers(self) -> bool:
|
||||||
|
"""
|
||||||
|
Boolean property defining if tickers are necessary.
|
||||||
|
If no Pairlist requires tickers, an empty List is passed
|
||||||
|
as tickers argument to filter_pairlist
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def short_desc(self) -> str:
|
||||||
|
"""
|
||||||
|
Short whitelist method description - used for startup-messages
|
||||||
|
"""
|
||||||
|
return (f"{self.name} - Filtering pairs with age less than "
|
||||||
|
f"{self._min_days_listed} {plural(self._min_days_listed, 'day')}.")
|
||||||
|
|
||||||
|
def _validate_pair(self, ticker: dict) -> bool:
|
||||||
|
"""
|
||||||
|
Validate age for the ticker
|
||||||
|
:param ticker: ticker dict as returned from ccxt.load_markets()
|
||||||
|
:return: True if the pair can stay, False if it should be removed
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Check symbol in cache
|
||||||
|
if ticker['symbol'] in self._symbolsChecked:
|
||||||
|
return True
|
||||||
|
|
||||||
|
since_ms = int(arrow.utcnow()
|
||||||
|
.floor('day')
|
||||||
|
.shift(days=-self._min_days_listed)
|
||||||
|
.float_timestamp) * 1000
|
||||||
|
|
||||||
|
daily_candles = self._exchange.get_historic_ohlcv(pair=ticker['symbol'],
|
||||||
|
timeframe='1d',
|
||||||
|
since_ms=since_ms)
|
||||||
|
|
||||||
|
if daily_candles is not None:
|
||||||
|
if len(daily_candles) > self._min_days_listed:
|
||||||
|
# We have fetched at least the minimum required number of daily candles
|
||||||
|
# Add to cache, store the time we last checked this symbol
|
||||||
|
self._symbolsChecked[ticker['symbol']] = int(arrow.utcnow().float_timestamp) * 1000
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, "
|
||||||
|
f"because age is less than "
|
||||||
|
f"{self._min_days_listed} "
|
||||||
|
f"{plural(self._min_days_listed, 'day')}")
|
||||||
|
return False
|
||||||
|
return False
|
@ -8,6 +8,7 @@ from typing import Any, Dict, List
|
|||||||
|
|
||||||
from cachetools import TTLCache, cached
|
from cachetools import TTLCache, cached
|
||||||
|
|
||||||
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange import market_is_active
|
from freqtrade.exchange import market_is_active
|
||||||
|
|
||||||
|
|
||||||
@ -67,7 +68,7 @@ class IPairList(ABC):
|
|||||||
def needstickers(self) -> bool:
|
def needstickers(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Boolean property defining if tickers are necessary.
|
Boolean property defining if tickers are necessary.
|
||||||
If no Pairlist requries tickers, an empty List is passed
|
If no Pairlist requires tickers, an empty List is passed
|
||||||
as tickers argument to filter_pairlist
|
as tickers argument to filter_pairlist
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -90,6 +91,24 @@ class IPairList(ABC):
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def gen_pairlist(self, cached_pairlist: List[str], tickers: Dict) -> List[str]:
|
||||||
|
"""
|
||||||
|
Generate the pairlist.
|
||||||
|
|
||||||
|
This method is called once by the pairlistmanager in the refresh_pairlist()
|
||||||
|
method to supply the starting pairlist for the chain of the Pairlist Handlers.
|
||||||
|
Pairlist Filters (those Pairlist Handlers that cannot be used at the first
|
||||||
|
position in the chain) shall not override this base implementation --
|
||||||
|
it will raise the exception if a Pairlist Handler is used at the first
|
||||||
|
position in the chain.
|
||||||
|
|
||||||
|
:param cached_pairlist: Previously generated pairlist (cached)
|
||||||
|
:param tickers: Tickers (from exchange.get_tickers()).
|
||||||
|
:return: List of pairs
|
||||||
|
"""
|
||||||
|
raise OperationalException("This Pairlist Handler should not be used "
|
||||||
|
"at the first position in the list of Pairlist Handlers.")
|
||||||
|
|
||||||
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
|
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Filters and sorts pairlist and returns the whitelist again.
|
Filters and sorts pairlist and returns the whitelist again.
|
||||||
@ -131,6 +150,9 @@ class IPairList(ABC):
|
|||||||
black_listed
|
black_listed
|
||||||
"""
|
"""
|
||||||
markets = self._exchange.markets
|
markets = self._exchange.markets
|
||||||
|
if not markets:
|
||||||
|
raise OperationalException(
|
||||||
|
'Markets not loaded. Make sure that exchange is initialized correctly.')
|
||||||
|
|
||||||
sanitized_whitelist: List[str] = []
|
sanitized_whitelist: List[str] = []
|
||||||
for pair in pairlist:
|
for pair in pairlist:
|
||||||
|
@ -5,7 +5,7 @@ import logging
|
|||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
from freqtrade.pairlist.IPairList import IPairList
|
from freqtrade.pairlist.IPairList import IPairList
|
||||||
|
from freqtrade.exceptions import OperationalException
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -17,6 +17,10 @@ class PrecisionFilter(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 'stoploss' not in self._config:
|
||||||
|
raise OperationalException(
|
||||||
|
'PrecisionFilter can only work with stoploss defined. Please add the '
|
||||||
|
'stoploss key to your configuration (overwrites eventual strategy settings).')
|
||||||
self._stoploss = self._config['stoploss']
|
self._stoploss = self._config['stoploss']
|
||||||
self._enabled = self._stoploss != 0
|
self._enabled = self._stoploss != 0
|
||||||
|
|
||||||
@ -27,7 +31,7 @@ class PrecisionFilter(IPairList):
|
|||||||
def needstickers(self) -> bool:
|
def needstickers(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Boolean property defining if tickers are necessary.
|
Boolean property defining if tickers are necessary.
|
||||||
If no Pairlist requries tickers, an empty List is passed
|
If no Pairlist requires tickers, an empty List is passed
|
||||||
as tickers argument to filter_pairlist
|
as tickers argument to filter_pairlist
|
||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
@ -24,7 +24,7 @@ class PriceFilter(IPairList):
|
|||||||
def needstickers(self) -> bool:
|
def needstickers(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Boolean property defining if tickers are necessary.
|
Boolean property defining if tickers are necessary.
|
||||||
If no Pairlist requries tickers, an empty List is passed
|
If no Pairlist requires tickers, an empty List is passed
|
||||||
as tickers argument to filter_pairlist
|
as tickers argument to filter_pairlist
|
||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
@ -25,7 +25,7 @@ class ShuffleFilter(IPairList):
|
|||||||
def needstickers(self) -> bool:
|
def needstickers(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Boolean property defining if tickers are necessary.
|
Boolean property defining if tickers are necessary.
|
||||||
If no Pairlist requries tickers, an empty List is passed
|
If no Pairlist requires tickers, an empty List is passed
|
||||||
as tickers argument to filter_pairlist
|
as tickers argument to filter_pairlist
|
||||||
"""
|
"""
|
||||||
return False
|
return False
|
||||||
|
@ -24,7 +24,7 @@ class SpreadFilter(IPairList):
|
|||||||
def needstickers(self) -> bool:
|
def needstickers(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Boolean property defining if tickers are necessary.
|
Boolean property defining if tickers are necessary.
|
||||||
If no Pairlist requries tickers, an empty List is passed
|
If no Pairlist requires tickers, an empty List is passed
|
||||||
as tickers argument to filter_pairlist
|
as tickers argument to filter_pairlist
|
||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
@ -4,8 +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 typing import Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.pairlist.IPairList import IPairList
|
from freqtrade.pairlist.IPairList import IPairList
|
||||||
|
|
||||||
|
|
||||||
@ -14,11 +15,20 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class StaticPairList(IPairList):
|
class StaticPairList(IPairList):
|
||||||
|
|
||||||
|
def __init__(self, exchange, pairlistmanager,
|
||||||
|
config: Dict[str, Any], pairlistconfig: Dict[str, Any],
|
||||||
|
pairlist_pos: int) -> None:
|
||||||
|
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.")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def needstickers(self) -> bool:
|
def needstickers(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Boolean property defining if tickers are necessary.
|
Boolean property defining if tickers are necessary.
|
||||||
If no Pairlist requries tickers, an empty List is passed
|
If no Pairlist requires tickers, an empty List is passed
|
||||||
as tickers argument to filter_pairlist
|
as tickers argument to filter_pairlist
|
||||||
"""
|
"""
|
||||||
return False
|
return False
|
||||||
@ -30,6 +40,15 @@ class StaticPairList(IPairList):
|
|||||||
"""
|
"""
|
||||||
return f"{self.name}"
|
return f"{self.name}"
|
||||||
|
|
||||||
|
def gen_pairlist(self, cached_pairlist: List[str], tickers: Dict) -> List[str]:
|
||||||
|
"""
|
||||||
|
Generate the pairlist
|
||||||
|
:param cached_pairlist: Previously generated pairlist (cached)
|
||||||
|
:param tickers: Tickers (from exchange.get_tickers()).
|
||||||
|
:return: List of pairs
|
||||||
|
"""
|
||||||
|
return self._whitelist_for_active_markets(self._config['exchange']['pair_whitelist'])
|
||||||
|
|
||||||
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
|
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Filters and sorts pairlist and returns the whitelist again.
|
Filters and sorts pairlist and returns the whitelist again.
|
||||||
@ -38,4 +57,4 @@ 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 self._whitelist_for_active_markets(self._config['exchange']['pair_whitelist'])
|
return pairlist
|
||||||
|
@ -54,7 +54,7 @@ class VolumePairList(IPairList):
|
|||||||
def needstickers(self) -> bool:
|
def needstickers(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Boolean property defining if tickers are necessary.
|
Boolean property defining if tickers are necessary.
|
||||||
If no Pairlist requries tickers, an empty List is passed
|
If no Pairlist requires tickers, an empty List is passed
|
||||||
as tickers argument to filter_pairlist
|
as tickers argument to filter_pairlist
|
||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
@ -68,6 +68,31 @@ class VolumePairList(IPairList):
|
|||||||
"""
|
"""
|
||||||
return f"{self.name} - top {self._pairlistconfig['number_assets']} volume pairs."
|
return f"{self.name} - top {self._pairlistconfig['number_assets']} volume pairs."
|
||||||
|
|
||||||
|
def gen_pairlist(self, cached_pairlist: List[str], tickers: Dict) -> List[str]:
|
||||||
|
"""
|
||||||
|
Generate the pairlist
|
||||||
|
:param cached_pairlist: Previously generated pairlist (cached)
|
||||||
|
:param tickers: Tickers (from exchange.get_tickers()).
|
||||||
|
:return: List of pairs
|
||||||
|
"""
|
||||||
|
# Generate dynamic whitelist
|
||||||
|
# Must always run if this pairlist is not the first in the list.
|
||||||
|
if self._last_refresh + self.refresh_period < datetime.now().timestamp():
|
||||||
|
self._last_refresh = int(datetime.now().timestamp())
|
||||||
|
|
||||||
|
# Use fresh pairlist
|
||||||
|
# Check if pair quote currency equals to the stake currency.
|
||||||
|
filtered_tickers = [
|
||||||
|
v for k, v in tickers.items()
|
||||||
|
if (self._exchange.get_pair_quote_currency(k) == self._stake_currency
|
||||||
|
and v[self._sort_key] is not None)]
|
||||||
|
pairlist = [s['symbol'] for s in filtered_tickers]
|
||||||
|
else:
|
||||||
|
# Use the cached pairlist if it's not time yet to refresh
|
||||||
|
pairlist = cached_pairlist
|
||||||
|
|
||||||
|
return pairlist
|
||||||
|
|
||||||
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
|
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Filters and sorts pairlist and returns the whitelist again.
|
Filters and sorts pairlist and returns the whitelist again.
|
||||||
@ -76,37 +101,8 @@ class VolumePairList(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
|
||||||
"""
|
"""
|
||||||
# Generate dynamic whitelist
|
# Use the incoming pairlist.
|
||||||
# Must always run if this pairlist is not the first in the list.
|
filtered_tickers = [v for k, v in tickers.items() if k in pairlist]
|
||||||
if (self._pairlist_pos != 0 or
|
|
||||||
(self._last_refresh + self.refresh_period < datetime.now().timestamp())):
|
|
||||||
|
|
||||||
self._last_refresh = int(datetime.now().timestamp())
|
|
||||||
pairs = self._gen_pair_whitelist(pairlist, tickers)
|
|
||||||
else:
|
|
||||||
pairs = pairlist
|
|
||||||
|
|
||||||
self.log_on_refresh(logger.info, f"Searching {self._number_pairs} pairs: {pairs}")
|
|
||||||
|
|
||||||
return pairs
|
|
||||||
|
|
||||||
def _gen_pair_whitelist(self, pairlist: List[str], tickers: Dict) -> List[str]:
|
|
||||||
"""
|
|
||||||
Updates the whitelist with with a dynamically generated list
|
|
||||||
:param pairlist: pairlist to filter or sort
|
|
||||||
:param tickers: Tickers (from exchange.get_tickers()).
|
|
||||||
:return: List of pairs
|
|
||||||
"""
|
|
||||||
if self._pairlist_pos == 0:
|
|
||||||
# If VolumePairList is the first in the list, use fresh pairlist
|
|
||||||
# Check if pair quote currency equals to the stake currency.
|
|
||||||
filtered_tickers = [
|
|
||||||
v for k, v in tickers.items()
|
|
||||||
if (self._exchange.get_pair_quote_currency(k) == self._stake_currency
|
|
||||||
and v[self._sort_key] is not None)]
|
|
||||||
else:
|
|
||||||
# If other pairlist is in front, use the incoming pairlist.
|
|
||||||
filtered_tickers = [v for k, v in tickers.items() if k in pairlist]
|
|
||||||
|
|
||||||
if self._min_value > 0:
|
if self._min_value > 0:
|
||||||
filtered_tickers = [
|
filtered_tickers = [
|
||||||
@ -120,4 +116,6 @@ class VolumePairList(IPairList):
|
|||||||
# Limit pairlist to the requested number of pairs
|
# Limit pairlist to the requested number of pairs
|
||||||
pairs = pairs[:self._number_pairs]
|
pairs = pairs[:self._number_pairs]
|
||||||
|
|
||||||
|
self.log_on_refresh(logger.info, f"Searching {self._number_pairs} pairs: {pairs}")
|
||||||
|
|
||||||
return pairs
|
return pairs
|
||||||
|
@ -87,6 +87,9 @@ class PairListManager():
|
|||||||
# Adjust whitelist if filters are using tickers
|
# Adjust whitelist if filters are using tickers
|
||||||
pairlist = self._prepare_whitelist(self._whitelist.copy(), tickers)
|
pairlist = self._prepare_whitelist(self._whitelist.copy(), tickers)
|
||||||
|
|
||||||
|
# Generate the pairlist with first Pairlist Handler in the chain
|
||||||
|
pairlist = self._pairlist_handlers[0].gen_pairlist(self._whitelist, tickers)
|
||||||
|
|
||||||
# Process all Pairlist Handlers in the chain
|
# Process all Pairlist Handlers in the chain
|
||||||
for pairlist_handler in self._pairlist_handlers:
|
for pairlist_handler in self._pairlist_handlers:
|
||||||
pairlist = pairlist_handler.filter_pairlist(pairlist, tickers)
|
pairlist = pairlist_handler.filter_pairlist(pairlist, tickers)
|
||||||
@ -128,6 +131,6 @@ class PairListManager():
|
|||||||
|
|
||||||
def create_pair_list(self, pairs: List[str], timeframe: str = None) -> ListPairsWithTimeframes:
|
def create_pair_list(self, pairs: List[str], timeframe: str = None) -> ListPairsWithTimeframes:
|
||||||
"""
|
"""
|
||||||
Create list of pair tuples with (pair, ticker_interval)
|
Create list of pair tuples with (pair, timeframe)
|
||||||
"""
|
"""
|
||||||
return [(pair, timeframe or self._config['ticker_interval']) for pair in pairs]
|
return [(pair, timeframe or self._config['timeframe']) for pair in pairs]
|
||||||
|
@ -86,7 +86,7 @@ def check_migrate(engine) -> None:
|
|||||||
logger.debug(f'trying {table_back_name}')
|
logger.debug(f'trying {table_back_name}')
|
||||||
|
|
||||||
# Check for latest column
|
# Check for latest column
|
||||||
if not has_column(cols, 'sell_order_status'):
|
if not has_column(cols, 'timeframe'):
|
||||||
logger.info(f'Running database migration - backup available as {table_back_name}')
|
logger.info(f'Running database migration - backup available as {table_back_name}')
|
||||||
|
|
||||||
fee_open = get_column_def(cols, 'fee_open', 'fee')
|
fee_open = get_column_def(cols, 'fee_open', 'fee')
|
||||||
@ -107,7 +107,12 @@ def check_migrate(engine) -> None:
|
|||||||
min_rate = get_column_def(cols, 'min_rate', 'null')
|
min_rate = get_column_def(cols, 'min_rate', 'null')
|
||||||
sell_reason = get_column_def(cols, 'sell_reason', 'null')
|
sell_reason = get_column_def(cols, 'sell_reason', 'null')
|
||||||
strategy = get_column_def(cols, 'strategy', 'null')
|
strategy = get_column_def(cols, 'strategy', 'null')
|
||||||
ticker_interval = get_column_def(cols, 'ticker_interval', 'null')
|
# If ticker-interval existed use that, else null.
|
||||||
|
if has_column(cols, 'ticker_interval'):
|
||||||
|
timeframe = get_column_def(cols, 'timeframe', 'ticker_interval')
|
||||||
|
else:
|
||||||
|
timeframe = get_column_def(cols, 'timeframe', 'null')
|
||||||
|
|
||||||
open_trade_price = get_column_def(cols, 'open_trade_price',
|
open_trade_price = get_column_def(cols, 'open_trade_price',
|
||||||
f'amount * open_rate * (1 + {fee_open})')
|
f'amount * open_rate * (1 + {fee_open})')
|
||||||
close_profit_abs = get_column_def(
|
close_profit_abs = get_column_def(
|
||||||
@ -133,7 +138,7 @@ def check_migrate(engine) -> None:
|
|||||||
stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
|
stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
|
||||||
stoploss_order_id, stoploss_last_update,
|
stoploss_order_id, stoploss_last_update,
|
||||||
max_rate, min_rate, sell_reason, sell_order_status, strategy,
|
max_rate, min_rate, sell_reason, sell_order_status, strategy,
|
||||||
ticker_interval, open_trade_price, close_profit_abs
|
timeframe, open_trade_price, close_profit_abs
|
||||||
)
|
)
|
||||||
select id, lower(exchange),
|
select id, lower(exchange),
|
||||||
case
|
case
|
||||||
@ -155,7 +160,7 @@ def check_migrate(engine) -> None:
|
|||||||
{stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update,
|
{stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update,
|
||||||
{max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason,
|
{max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason,
|
||||||
{sell_order_status} sell_order_status,
|
{sell_order_status} sell_order_status,
|
||||||
{strategy} strategy, {ticker_interval} ticker_interval,
|
{strategy} strategy, {timeframe} timeframe,
|
||||||
{open_trade_price} open_trade_price, {close_profit_abs} close_profit_abs
|
{open_trade_price} open_trade_price, {close_profit_abs} close_profit_abs
|
||||||
from {table_back_name}
|
from {table_back_name}
|
||||||
""")
|
""")
|
||||||
@ -232,7 +237,7 @@ class Trade(_DECL_BASE):
|
|||||||
sell_reason = Column(String, nullable=True)
|
sell_reason = Column(String, nullable=True)
|
||||||
sell_order_status = Column(String, nullable=True)
|
sell_order_status = Column(String, nullable=True)
|
||||||
strategy = Column(String, nullable=True)
|
strategy = Column(String, nullable=True)
|
||||||
ticker_interval = Column(Integer, nullable=True)
|
timeframe = Column(Integer, nullable=True)
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
@ -249,39 +254,57 @@ class Trade(_DECL_BASE):
|
|||||||
'trade_id': self.id,
|
'trade_id': self.id,
|
||||||
'pair': self.pair,
|
'pair': self.pair,
|
||||||
'is_open': self.is_open,
|
'is_open': self.is_open,
|
||||||
|
'exchange': self.exchange,
|
||||||
|
'amount': round(self.amount, 8),
|
||||||
|
'stake_amount': round(self.stake_amount, 8),
|
||||||
|
'strategy': self.strategy,
|
||||||
|
'ticker_interval': self.timeframe, # DEPRECATED
|
||||||
|
'timeframe': self.timeframe,
|
||||||
|
|
||||||
'fee_open': self.fee_open,
|
'fee_open': self.fee_open,
|
||||||
'fee_open_cost': self.fee_open_cost,
|
'fee_open_cost': self.fee_open_cost,
|
||||||
'fee_open_currency': self.fee_open_currency,
|
'fee_open_currency': self.fee_open_currency,
|
||||||
'fee_close': self.fee_close,
|
'fee_close': self.fee_close,
|
||||||
'fee_close_cost': self.fee_close_cost,
|
'fee_close_cost': self.fee_close_cost,
|
||||||
'fee_close_currency': self.fee_close_currency,
|
'fee_close_currency': self.fee_close_currency,
|
||||||
|
|
||||||
'open_date_hum': arrow.get(self.open_date).humanize(),
|
'open_date_hum': arrow.get(self.open_date).humanize(),
|
||||||
'open_date': self.open_date.strftime("%Y-%m-%d %H:%M:%S"),
|
'open_date': self.open_date.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
'open_timestamp': int(self.open_date.timestamp() * 1000),
|
'open_timestamp': int(self.open_date.timestamp() * 1000),
|
||||||
|
'open_rate': self.open_rate,
|
||||||
|
'open_rate_requested': self.open_rate_requested,
|
||||||
|
'open_trade_price': self.open_trade_price,
|
||||||
|
|
||||||
'close_date_hum': (arrow.get(self.close_date).humanize()
|
'close_date_hum': (arrow.get(self.close_date).humanize()
|
||||||
if self.close_date else None),
|
if self.close_date else None),
|
||||||
'close_date': (self.close_date.strftime("%Y-%m-%d %H:%M:%S")
|
'close_date': (self.close_date.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
if self.close_date else None),
|
if self.close_date else None),
|
||||||
'close_timestamp': int(self.close_date.timestamp() * 1000) if self.close_date else None,
|
'close_timestamp': int(self.close_date.timestamp() * 1000) if self.close_date else None,
|
||||||
'open_rate': self.open_rate,
|
|
||||||
'open_rate_requested': self.open_rate_requested,
|
|
||||||
'open_trade_price': self.open_trade_price,
|
|
||||||
'close_rate': self.close_rate,
|
'close_rate': self.close_rate,
|
||||||
'close_rate_requested': self.close_rate_requested,
|
'close_rate_requested': self.close_rate_requested,
|
||||||
'amount': round(self.amount, 8),
|
|
||||||
'stake_amount': round(self.stake_amount, 8),
|
|
||||||
'close_profit': self.close_profit,
|
'close_profit': self.close_profit,
|
||||||
|
'close_profit_abs': self.close_profit_abs,
|
||||||
|
|
||||||
'sell_reason': self.sell_reason,
|
'sell_reason': self.sell_reason,
|
||||||
'sell_order_status': self.sell_order_status,
|
'sell_order_status': self.sell_order_status,
|
||||||
'stop_loss': self.stop_loss,
|
'stop_loss': self.stop_loss, # Deprecated - should not be used
|
||||||
|
'stop_loss_abs': self.stop_loss,
|
||||||
|
'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None,
|
||||||
'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None,
|
'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None,
|
||||||
'initial_stop_loss': self.initial_stop_loss,
|
'stoploss_order_id': self.stoploss_order_id,
|
||||||
|
'stoploss_last_update': (self.stoploss_last_update.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
if self.stoploss_last_update else None),
|
||||||
|
'stoploss_last_update_timestamp': (int(self.stoploss_last_update.timestamp() * 1000)
|
||||||
|
if self.stoploss_last_update else None),
|
||||||
|
'initial_stop_loss': self.initial_stop_loss, # Deprecated - should not be used
|
||||||
|
'initial_stop_loss_abs': self.initial_stop_loss,
|
||||||
|
'initial_stop_loss_ratio': (self.initial_stop_loss_pct
|
||||||
|
if self.initial_stop_loss_pct else None),
|
||||||
'initial_stop_loss_pct': (self.initial_stop_loss_pct * 100
|
'initial_stop_loss_pct': (self.initial_stop_loss_pct * 100
|
||||||
if self.initial_stop_loss_pct else None),
|
if self.initial_stop_loss_pct else None),
|
||||||
'min_rate': self.min_rate,
|
'min_rate': self.min_rate,
|
||||||
'max_rate': self.max_rate,
|
'max_rate': self.max_rate,
|
||||||
'strategy': self.strategy,
|
|
||||||
'ticker_interval': self.ticker_interval,
|
|
||||||
'open_order_id': self.open_order_id,
|
'open_order_id': self.open_order_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -357,7 +380,7 @@ class Trade(_DECL_BASE):
|
|||||||
elif order_type in ('market', 'limit') and order['side'] == 'sell':
|
elif order_type in ('market', 'limit') and order['side'] == 'sell':
|
||||||
self.close(order['price'])
|
self.close(order['price'])
|
||||||
logger.info('%s_SELL has been fulfilled for %s.', order_type.upper(), self)
|
logger.info('%s_SELL has been fulfilled for %s.', order_type.upper(), self)
|
||||||
elif order_type in ('stop_loss_limit', 'stop-loss'):
|
elif order_type in ('stop_loss_limit', 'stop-loss', 'stop'):
|
||||||
self.stoploss_order_id = None
|
self.stoploss_order_id = None
|
||||||
self.close_rate_requested = self.stop_loss
|
self.close_rate_requested = self.stop_loss
|
||||||
logger.info('%s is hit for %s.', order_type.upper(), self)
|
logger.info('%s is hit for %s.', order_type.upper(), self)
|
||||||
@ -546,6 +569,7 @@ class Trade(_DECL_BASE):
|
|||||||
def get_best_pair():
|
def get_best_pair():
|
||||||
"""
|
"""
|
||||||
Get best pair with closed trade.
|
Get best pair with closed trade.
|
||||||
|
:returns: Tuple containing (pair, profit_sum)
|
||||||
"""
|
"""
|
||||||
best_pair = Trade.session.query(
|
best_pair = Trade.session.query(
|
||||||
Trade.pair, func.sum(Trade.close_profit).label('profit_sum')
|
Trade.pair, func.sum(Trade.close_profit).label('profit_sum')
|
||||||
|
@ -45,7 +45,7 @@ def init_plotscript(config):
|
|||||||
data = load_data(
|
data = load_data(
|
||||||
datadir=config.get("datadir"),
|
datadir=config.get("datadir"),
|
||||||
pairs=pairs,
|
pairs=pairs,
|
||||||
timeframe=config.get('ticker_interval', '5m'),
|
timeframe=config.get('timeframe', '5m'),
|
||||||
timerange=timerange,
|
timerange=timerange,
|
||||||
data_format=config.get('dataformat_ohlcv', 'json'),
|
data_format=config.get('dataformat_ohlcv', 'json'),
|
||||||
)
|
)
|
||||||
@ -162,7 +162,7 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
|
|||||||
# Trades can be empty
|
# Trades can be empty
|
||||||
if trades is not None and len(trades) > 0:
|
if trades is not None and len(trades) > 0:
|
||||||
# Create description for sell summarizing the trade
|
# Create description for sell summarizing the trade
|
||||||
trades['desc'] = trades.apply(lambda row: f"{round(row['profitperc'] * 100, 1)}%, "
|
trades['desc'] = trades.apply(lambda row: f"{round(row['profit_percent'] * 100, 1)}%, "
|
||||||
f"{row['sell_reason']}, {row['duration']} min",
|
f"{row['sell_reason']}, {row['duration']} min",
|
||||||
axis=1)
|
axis=1)
|
||||||
trade_buys = go.Scatter(
|
trade_buys = go.Scatter(
|
||||||
@ -181,9 +181,9 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
|
|||||||
)
|
)
|
||||||
|
|
||||||
trade_sells = go.Scatter(
|
trade_sells = go.Scatter(
|
||||||
x=trades.loc[trades['profitperc'] > 0, "close_time"],
|
x=trades.loc[trades['profit_percent'] > 0, "close_time"],
|
||||||
y=trades.loc[trades['profitperc'] > 0, "close_rate"],
|
y=trades.loc[trades['profit_percent'] > 0, "close_rate"],
|
||||||
text=trades.loc[trades['profitperc'] > 0, "desc"],
|
text=trades.loc[trades['profit_percent'] > 0, "desc"],
|
||||||
mode='markers',
|
mode='markers',
|
||||||
name='Sell - Profit',
|
name='Sell - Profit',
|
||||||
marker=dict(
|
marker=dict(
|
||||||
@ -194,9 +194,9 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
trade_sells_loss = go.Scatter(
|
trade_sells_loss = go.Scatter(
|
||||||
x=trades.loc[trades['profitperc'] <= 0, "close_time"],
|
x=trades.loc[trades['profit_percent'] <= 0, "close_time"],
|
||||||
y=trades.loc[trades['profitperc'] <= 0, "close_rate"],
|
y=trades.loc[trades['profit_percent'] <= 0, "close_rate"],
|
||||||
text=trades.loc[trades['profitperc'] <= 0, "desc"],
|
text=trades.loc[trades['profit_percent'] <= 0, "desc"],
|
||||||
mode='markers',
|
mode='markers',
|
||||||
name='Sell - Loss',
|
name='Sell - Loss',
|
||||||
marker=dict(
|
marker=dict(
|
||||||
@ -487,7 +487,7 @@ def load_and_plot_trades(config: Dict[str, Any]):
|
|||||||
plot_config=strategy.plot_config if hasattr(strategy, 'plot_config') else {}
|
plot_config=strategy.plot_config if hasattr(strategy, 'plot_config') else {}
|
||||||
)
|
)
|
||||||
|
|
||||||
store_plot_file(fig, filename=generate_plot_filename(pair, config['ticker_interval']),
|
store_plot_file(fig, filename=generate_plot_filename(pair, config['timeframe']),
|
||||||
directory=config['user_data_dir'] / "plot")
|
directory=config['user_data_dir'] / "plot")
|
||||||
|
|
||||||
logger.info('End of plotting process. %s plots generated', pair_counter)
|
logger.info('End of plotting process. %s plots generated', pair_counter)
|
||||||
@ -515,6 +515,6 @@ def plot_profit(config: Dict[str, Any]) -> None:
|
|||||||
# Create an average close price of all the pairs that were involved.
|
# Create an average close price of all the pairs that were involved.
|
||||||
# this could be useful to gauge the overall market trend
|
# this could be useful to gauge the overall market trend
|
||||||
fig = generate_profit_graph(plot_elements["pairs"], plot_elements["ohlcv"],
|
fig = generate_profit_graph(plot_elements["pairs"], plot_elements["ohlcv"],
|
||||||
trades, config.get('ticker_interval', '5m'))
|
trades, config.get('timeframe', '5m'))
|
||||||
store_plot_file(fig, filename='freqtrade-profit-plot.html',
|
store_plot_file(fig, filename='freqtrade-profit-plot.html',
|
||||||
directory=config['user_data_dir'] / "plot", auto_open=True)
|
directory=config['user_data_dir'] / "plot", auto_open=True)
|
||||||
|
@ -77,8 +77,9 @@ class HyperOptLossResolver(IResolver):
|
|||||||
config, kwargs={},
|
config, kwargs={},
|
||||||
extra_dir=config.get('hyperopt_path'))
|
extra_dir=config.get('hyperopt_path'))
|
||||||
|
|
||||||
# Assign ticker_interval to be used in hyperopt
|
# Assign timeframe to be used in hyperopt
|
||||||
hyperoptloss.__class__.ticker_interval = str(config['ticker_interval'])
|
hyperoptloss.__class__.ticker_interval = str(config['timeframe'])
|
||||||
|
hyperoptloss.__class__.timeframe = str(config['timeframe'])
|
||||||
|
|
||||||
if not hasattr(hyperoptloss, 'hyperopt_loss_function'):
|
if not hasattr(hyperoptloss, 'hyperopt_loss_function'):
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
|
@ -50,39 +50,51 @@ class StrategyResolver(IResolver):
|
|||||||
if 'ask_strategy' not in config:
|
if 'ask_strategy' not in config:
|
||||||
config['ask_strategy'] = {}
|
config['ask_strategy'] = {}
|
||||||
|
|
||||||
|
if hasattr(strategy, 'ticker_interval') and not hasattr(strategy, 'timeframe'):
|
||||||
|
# Assign ticker_interval to timeframe to keep compatibility
|
||||||
|
if 'timeframe' not in config:
|
||||||
|
logger.warning(
|
||||||
|
"DEPRECATED: Please migrate to using 'timeframe' instead of 'ticker_interval'."
|
||||||
|
)
|
||||||
|
strategy.timeframe = strategy.ticker_interval
|
||||||
|
|
||||||
# Set attributes
|
# Set attributes
|
||||||
# Check if we need to override configuration
|
# Check if we need to override configuration
|
||||||
# (Attribute name, default, ask_strategy)
|
# (Attribute name, default, subkey)
|
||||||
attributes = [("minimal_roi", {"0": 10.0}, False),
|
attributes = [("minimal_roi", {"0": 10.0}, None),
|
||||||
("ticker_interval", None, False),
|
("timeframe", None, None),
|
||||||
("stoploss", None, False),
|
("stoploss", None, None),
|
||||||
("trailing_stop", None, False),
|
("trailing_stop", None, None),
|
||||||
("trailing_stop_positive", None, False),
|
("trailing_stop_positive", None, None),
|
||||||
("trailing_stop_positive_offset", 0.0, False),
|
("trailing_stop_positive_offset", 0.0, None),
|
||||||
("trailing_only_offset_is_reached", None, False),
|
("trailing_only_offset_is_reached", None, None),
|
||||||
("process_only_new_candles", None, False),
|
("process_only_new_candles", None, None),
|
||||||
("order_types", None, False),
|
("order_types", None, None),
|
||||||
("order_time_in_force", None, False),
|
("order_time_in_force", None, None),
|
||||||
("stake_currency", None, False),
|
("stake_currency", None, None),
|
||||||
("stake_amount", None, False),
|
("stake_amount", None, None),
|
||||||
("startup_candle_count", None, False),
|
("startup_candle_count", None, None),
|
||||||
("unfilledtimeout", None, False),
|
("unfilledtimeout", None, None),
|
||||||
("use_sell_signal", True, True),
|
("use_sell_signal", True, 'ask_strategy'),
|
||||||
("sell_profit_only", False, True),
|
("sell_profit_only", False, 'ask_strategy'),
|
||||||
("ignore_roi_if_buy_signal", False, True),
|
("ignore_roi_if_buy_signal", False, 'ask_strategy'),
|
||||||
|
("disable_dataframe_checks", False, None),
|
||||||
]
|
]
|
||||||
for attribute, default, ask_strategy in attributes:
|
for attribute, default, subkey in attributes:
|
||||||
if ask_strategy:
|
if subkey:
|
||||||
StrategyResolver._override_attribute_helper(strategy, config['ask_strategy'],
|
StrategyResolver._override_attribute_helper(strategy, config.get(subkey, {}),
|
||||||
attribute, default)
|
attribute, default)
|
||||||
else:
|
else:
|
||||||
StrategyResolver._override_attribute_helper(strategy, config,
|
StrategyResolver._override_attribute_helper(strategy, config,
|
||||||
attribute, default)
|
attribute, default)
|
||||||
|
|
||||||
|
# Assign deprecated variable - to not break users code relying on this.
|
||||||
|
strategy.ticker_interval = strategy.timeframe
|
||||||
|
|
||||||
# Loop this list again to have output combined
|
# Loop this list again to have output combined
|
||||||
for attribute, _, exp in attributes:
|
for attribute, _, subkey in attributes:
|
||||||
if exp and attribute in config['ask_strategy']:
|
if subkey and attribute in config[subkey]:
|
||||||
logger.info("Strategy using %s: %s", attribute, config['ask_strategy'][attribute])
|
logger.info("Strategy using %s: %s", attribute, config[subkey][attribute])
|
||||||
elif attribute in config:
|
elif attribute in config:
|
||||||
logger.info("Strategy using %s: %s", attribute, config[attribute])
|
logger.info("Strategy using %s: %s", attribute, config[attribute])
|
||||||
|
|
||||||
|
@ -90,7 +90,9 @@ class ApiServer(RPC):
|
|||||||
self._config = freqtrade.config
|
self._config = freqtrade.config
|
||||||
self.app = Flask(__name__)
|
self.app = Flask(__name__)
|
||||||
self._cors = CORS(self.app,
|
self._cors = CORS(self.app,
|
||||||
resources={r"/api/*": {"supports_credentials": True, }}
|
resources={r"/api/*": {
|
||||||
|
"supports_credentials": True,
|
||||||
|
"origins": self._config['api_server'].get('CORS_origins', [])}}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Setup the Flask-JWT-Extended extension
|
# Setup the Flask-JWT-Extended extension
|
||||||
@ -172,8 +174,8 @@ class ApiServer(RPC):
|
|||||||
self.app.add_url_rule(f'{BASE_URI}/stop', 'stop', view_func=self._stop, methods=['POST'])
|
self.app.add_url_rule(f'{BASE_URI}/stop', 'stop', view_func=self._stop, methods=['POST'])
|
||||||
self.app.add_url_rule(f'{BASE_URI}/stopbuy', 'stopbuy',
|
self.app.add_url_rule(f'{BASE_URI}/stopbuy', 'stopbuy',
|
||||||
view_func=self._stopbuy, methods=['POST'])
|
view_func=self._stopbuy, methods=['POST'])
|
||||||
self.app.add_url_rule(f'{BASE_URI}/reload_conf', 'reload_conf',
|
self.app.add_url_rule(f'{BASE_URI}/reload_config', 'reload_config',
|
||||||
view_func=self._reload_conf, methods=['POST'])
|
view_func=self._reload_config, methods=['POST'])
|
||||||
# Info commands
|
# Info commands
|
||||||
self.app.add_url_rule(f'{BASE_URI}/balance', 'balance',
|
self.app.add_url_rule(f'{BASE_URI}/balance', 'balance',
|
||||||
view_func=self._balance, methods=['GET'])
|
view_func=self._balance, methods=['GET'])
|
||||||
@ -304,12 +306,12 @@ class ApiServer(RPC):
|
|||||||
|
|
||||||
@require_login
|
@require_login
|
||||||
@rpc_catch_errors
|
@rpc_catch_errors
|
||||||
def _reload_conf(self):
|
def _reload_config(self):
|
||||||
"""
|
"""
|
||||||
Handler for /reload_conf.
|
Handler for /reload_config.
|
||||||
Triggers a config file reload
|
Triggers a config file reload
|
||||||
"""
|
"""
|
||||||
msg = self._rpc_reload_conf()
|
msg = self._rpc_reload_config()
|
||||||
return self.rest_dump(msg)
|
return self.rest_dump(msg)
|
||||||
|
|
||||||
@require_login
|
@require_login
|
||||||
@ -360,7 +362,6 @@ class ApiServer(RPC):
|
|||||||
Returns a cumulative profit statistics
|
Returns a cumulative profit statistics
|
||||||
:return: stats
|
:return: stats
|
||||||
"""
|
"""
|
||||||
logger.info("LocalRPC - Profit Command Called")
|
|
||||||
|
|
||||||
stats = self._rpc_trade_statistics(self._config['stake_currency'],
|
stats = self._rpc_trade_statistics(self._config['stake_currency'],
|
||||||
self._config.get('fiat_display_currency')
|
self._config.get('fiat_display_currency')
|
||||||
@ -377,8 +378,6 @@ class ApiServer(RPC):
|
|||||||
Returns a cumulative performance statistics
|
Returns a cumulative performance statistics
|
||||||
:return: stats
|
:return: stats
|
||||||
"""
|
"""
|
||||||
logger.info("LocalRPC - performance Command Called")
|
|
||||||
|
|
||||||
stats = self._rpc_performance()
|
stats = self._rpc_performance()
|
||||||
|
|
||||||
return self.rest_dump(stats)
|
return self.rest_dump(stats)
|
||||||
|
@ -101,10 +101,13 @@ class RPC:
|
|||||||
'trailing_stop_positive': config.get('trailing_stop_positive'),
|
'trailing_stop_positive': config.get('trailing_stop_positive'),
|
||||||
'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'),
|
'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'),
|
||||||
'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'),
|
'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'),
|
||||||
'ticker_interval': config['ticker_interval'],
|
'ticker_interval': config['timeframe'], # DEPRECATED
|
||||||
|
'timeframe': config['timeframe'],
|
||||||
'exchange': config['exchange']['name'],
|
'exchange': config['exchange']['name'],
|
||||||
'strategy': config['strategy'],
|
'strategy': config['strategy'],
|
||||||
'forcebuy_enabled': config.get('forcebuy_enable', False),
|
'forcebuy_enabled': config.get('forcebuy_enable', False),
|
||||||
|
'ask_strategy': config.get('ask_strategy', {}),
|
||||||
|
'bid_strategy': config.get('bid_strategy', {}),
|
||||||
'state': str(self._freqtrade.state)
|
'state': str(self._freqtrade.state)
|
||||||
}
|
}
|
||||||
return val
|
return val
|
||||||
@ -130,6 +133,14 @@ class RPC:
|
|||||||
except DependencyException:
|
except DependencyException:
|
||||||
current_rate = NAN
|
current_rate = NAN
|
||||||
current_profit = trade.calc_profit_ratio(current_rate)
|
current_profit = trade.calc_profit_ratio(current_rate)
|
||||||
|
current_profit_abs = trade.calc_profit(current_rate)
|
||||||
|
# Calculate guaranteed profit (in case of trailing stop)
|
||||||
|
stoploss_entry_dist = trade.calc_profit(trade.stop_loss)
|
||||||
|
stoploss_entry_dist_ratio = trade.calc_profit_ratio(trade.stop_loss)
|
||||||
|
# calculate distance to stoploss
|
||||||
|
stoploss_current_dist = trade.stop_loss - current_rate
|
||||||
|
stoploss_current_dist_ratio = stoploss_current_dist / current_rate
|
||||||
|
|
||||||
fmt_close_profit = (f'{round(trade.close_profit * 100, 2):.2f}%'
|
fmt_close_profit = (f'{round(trade.close_profit * 100, 2):.2f}%'
|
||||||
if trade.close_profit is not None else None)
|
if trade.close_profit is not None else None)
|
||||||
trade_dict = trade.to_json()
|
trade_dict = trade.to_json()
|
||||||
@ -140,6 +151,11 @@ class RPC:
|
|||||||
current_rate=current_rate,
|
current_rate=current_rate,
|
||||||
current_profit=current_profit,
|
current_profit=current_profit,
|
||||||
current_profit_pct=round(current_profit * 100, 2),
|
current_profit_pct=round(current_profit * 100, 2),
|
||||||
|
current_profit_abs=current_profit_abs,
|
||||||
|
stoploss_current_dist=stoploss_current_dist,
|
||||||
|
stoploss_current_dist_ratio=round(stoploss_current_dist_ratio, 8),
|
||||||
|
stoploss_entry_dist=stoploss_entry_dist,
|
||||||
|
stoploss_entry_dist_ratio=round(stoploss_entry_dist_ratio, 8),
|
||||||
open_order='({} {} rem={:.8f})'.format(
|
open_order='({} {} rem={:.8f})'.format(
|
||||||
order['type'], order['side'], order['remaining']
|
order['type'], order['side'], order['remaining']
|
||||||
) if order else None,
|
) if order else None,
|
||||||
@ -281,15 +297,11 @@ class RPC:
|
|||||||
|
|
||||||
best_pair = Trade.get_best_pair()
|
best_pair = Trade.get_best_pair()
|
||||||
|
|
||||||
if not best_pair:
|
|
||||||
raise RPCException('no closed trade')
|
|
||||||
|
|
||||||
bp_pair, bp_rate = best_pair
|
|
||||||
|
|
||||||
# Prepare data to display
|
# Prepare data to display
|
||||||
profit_closed_coin_sum = round(sum(profit_closed_coin), 8)
|
profit_closed_coin_sum = round(sum(profit_closed_coin), 8)
|
||||||
profit_closed_percent = (round(mean(profit_closed_ratio) * 100, 2) if profit_closed_ratio
|
profit_closed_ratio_mean = mean(profit_closed_ratio) if profit_closed_ratio else 0.0
|
||||||
else 0.0)
|
profit_closed_ratio_sum = sum(profit_closed_ratio) if profit_closed_ratio else 0.0
|
||||||
|
|
||||||
profit_closed_fiat = self._fiat_converter.convert_amount(
|
profit_closed_fiat = self._fiat_converter.convert_amount(
|
||||||
profit_closed_coin_sum,
|
profit_closed_coin_sum,
|
||||||
stake_currency,
|
stake_currency,
|
||||||
@ -297,29 +309,41 @@ class RPC:
|
|||||||
) if self._fiat_converter else 0
|
) if self._fiat_converter else 0
|
||||||
|
|
||||||
profit_all_coin_sum = round(sum(profit_all_coin), 8)
|
profit_all_coin_sum = round(sum(profit_all_coin), 8)
|
||||||
profit_all_percent = round(mean(profit_all_ratio) * 100, 2) if profit_all_ratio else 0.0
|
profit_all_ratio_mean = mean(profit_all_ratio) if profit_all_ratio else 0.0
|
||||||
|
profit_all_ratio_sum = sum(profit_all_ratio) if profit_all_ratio else 0.0
|
||||||
profit_all_fiat = self._fiat_converter.convert_amount(
|
profit_all_fiat = self._fiat_converter.convert_amount(
|
||||||
profit_all_coin_sum,
|
profit_all_coin_sum,
|
||||||
stake_currency,
|
stake_currency,
|
||||||
fiat_display_currency
|
fiat_display_currency
|
||||||
) if self._fiat_converter else 0
|
) if self._fiat_converter else 0
|
||||||
|
|
||||||
|
first_date = trades[0].open_date if trades else None
|
||||||
|
last_date = trades[-1].open_date if trades else None
|
||||||
num = float(len(durations) or 1)
|
num = float(len(durations) or 1)
|
||||||
return {
|
return {
|
||||||
'profit_closed_coin': profit_closed_coin_sum,
|
'profit_closed_coin': profit_closed_coin_sum,
|
||||||
'profit_closed_percent': profit_closed_percent,
|
'profit_closed_percent': round(profit_closed_ratio_mean * 100, 2), # DEPRECATED
|
||||||
|
'profit_closed_percent_mean': round(profit_closed_ratio_mean * 100, 2),
|
||||||
|
'profit_closed_ratio_mean': profit_closed_ratio_mean,
|
||||||
|
'profit_closed_percent_sum': round(profit_closed_ratio_sum * 100, 2),
|
||||||
|
'profit_closed_ratio_sum': profit_closed_ratio_sum,
|
||||||
'profit_closed_fiat': profit_closed_fiat,
|
'profit_closed_fiat': profit_closed_fiat,
|
||||||
'profit_all_coin': profit_all_coin_sum,
|
'profit_all_coin': profit_all_coin_sum,
|
||||||
'profit_all_percent': profit_all_percent,
|
'profit_all_percent': round(profit_all_ratio_mean * 100, 2), # DEPRECATED
|
||||||
|
'profit_all_percent_mean': round(profit_all_ratio_mean * 100, 2),
|
||||||
|
'profit_all_ratio_mean': profit_all_ratio_mean,
|
||||||
|
'profit_all_percent_sum': round(profit_all_ratio_sum * 100, 2),
|
||||||
|
'profit_all_ratio_sum': profit_all_ratio_sum,
|
||||||
'profit_all_fiat': profit_all_fiat,
|
'profit_all_fiat': profit_all_fiat,
|
||||||
'trade_count': len(trades),
|
'trade_count': len(trades),
|
||||||
'first_trade_date': arrow.get(trades[0].open_date).humanize(),
|
'closed_trade_count': len([t for t in trades if not t.is_open]),
|
||||||
'first_trade_timestamp': int(trades[0].open_date.timestamp() * 1000),
|
'first_trade_date': arrow.get(first_date).humanize() if first_date else '',
|
||||||
'latest_trade_date': arrow.get(trades[-1].open_date).humanize(),
|
'first_trade_timestamp': int(first_date.timestamp() * 1000) if first_date else 0,
|
||||||
'latest_trade_timestamp': int(trades[-1].open_date.timestamp() * 1000),
|
'latest_trade_date': arrow.get(last_date).humanize() if last_date else '',
|
||||||
|
'latest_trade_timestamp': int(last_date.timestamp() * 1000) if last_date else 0,
|
||||||
'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0],
|
'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0],
|
||||||
'best_pair': bp_pair,
|
'best_pair': best_pair[0] if best_pair else '',
|
||||||
'best_rate': round(bp_rate * 100, 2),
|
'best_rate': round(best_pair[1] * 100, 2) if best_pair else 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict:
|
def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict:
|
||||||
@ -395,9 +419,9 @@ class RPC:
|
|||||||
|
|
||||||
return {'status': 'already stopped'}
|
return {'status': 'already stopped'}
|
||||||
|
|
||||||
def _rpc_reload_conf(self) -> Dict[str, str]:
|
def _rpc_reload_config(self) -> Dict[str, str]:
|
||||||
""" Handler for reload_conf. """
|
""" Handler for reload_config. """
|
||||||
self._freqtrade.state = State.RELOAD_CONF
|
self._freqtrade.state = State.RELOAD_CONFIG
|
||||||
return {'status': 'reloading config ...'}
|
return {'status': 'reloading config ...'}
|
||||||
|
|
||||||
def _rpc_stopbuy(self) -> Dict[str, str]:
|
def _rpc_stopbuy(self) -> Dict[str, str]:
|
||||||
@ -408,7 +432,7 @@ class RPC:
|
|||||||
# Set 'max_open_trades' to 0
|
# Set 'max_open_trades' to 0
|
||||||
self._freqtrade.config['max_open_trades'] = 0
|
self._freqtrade.config['max_open_trades'] = 0
|
||||||
|
|
||||||
return {'status': 'No more buy will occur from now. Run /reload_conf to reset.'}
|
return {'status': 'No more buy will occur from now. Run /reload_config to reset.'}
|
||||||
|
|
||||||
def _rpc_forcesell(self, trade_id: str) -> Dict[str, str]:
|
def _rpc_forcesell(self, trade_id: str) -> Dict[str, str]:
|
||||||
"""
|
"""
|
||||||
@ -533,16 +557,26 @@ class RPC:
|
|||||||
|
|
||||||
def _rpc_blacklist(self, add: List[str] = None) -> Dict:
|
def _rpc_blacklist(self, add: List[str] = None) -> Dict:
|
||||||
""" Returns the currently active blacklist"""
|
""" Returns the currently active blacklist"""
|
||||||
|
errors = {}
|
||||||
if add:
|
if add:
|
||||||
stake_currency = self._freqtrade.config.get('stake_currency')
|
stake_currency = self._freqtrade.config.get('stake_currency')
|
||||||
for pair in add:
|
for pair in add:
|
||||||
if (self._freqtrade.exchange.get_pair_quote_currency(pair) == stake_currency
|
if self._freqtrade.exchange.get_pair_quote_currency(pair) == stake_currency:
|
||||||
and pair not in self._freqtrade.pairlists.blacklist):
|
if pair not in self._freqtrade.pairlists.blacklist:
|
||||||
self._freqtrade.pairlists.blacklist.append(pair)
|
self._freqtrade.pairlists.blacklist.append(pair)
|
||||||
|
else:
|
||||||
|
errors[pair] = {
|
||||||
|
'error_msg': f'Pair {pair} already in pairlist.'}
|
||||||
|
|
||||||
|
else:
|
||||||
|
errors[pair] = {
|
||||||
|
'error_msg': f"Pair {pair} does not match stake currency."
|
||||||
|
}
|
||||||
|
|
||||||
res = {'method': self._freqtrade.pairlists.name_list,
|
res = {'method': self._freqtrade.pairlists.name_list,
|
||||||
'length': len(self._freqtrade.pairlists.blacklist),
|
'length': len(self._freqtrade.pairlists.blacklist),
|
||||||
'blacklist': self._freqtrade.pairlists.blacklist,
|
'blacklist': self._freqtrade.pairlists.blacklist,
|
||||||
|
'errors': errors,
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
@ -72,7 +72,7 @@ class RPCManager:
|
|||||||
minimal_roi = config['minimal_roi']
|
minimal_roi = config['minimal_roi']
|
||||||
stoploss = config['stoploss']
|
stoploss = config['stoploss']
|
||||||
trailing_stop = config['trailing_stop']
|
trailing_stop = config['trailing_stop']
|
||||||
ticker_interval = config['ticker_interval']
|
timeframe = config['timeframe']
|
||||||
exchange_name = config['exchange']['name']
|
exchange_name = config['exchange']['name']
|
||||||
strategy_name = config.get('strategy', '')
|
strategy_name = config.get('strategy', '')
|
||||||
self.send_msg({
|
self.send_msg({
|
||||||
@ -81,7 +81,7 @@ class RPCManager:
|
|||||||
f'*Stake per trade:* `{stake_amount} {stake_currency}`\n'
|
f'*Stake per trade:* `{stake_amount} {stake_currency}`\n'
|
||||||
f'*Minimum ROI:* `{minimal_roi}`\n'
|
f'*Minimum ROI:* `{minimal_roi}`\n'
|
||||||
f'*{"Trailing " if trailing_stop else ""}Stoploss:* `{stoploss}`\n'
|
f'*{"Trailing " if trailing_stop else ""}Stoploss:* `{stoploss}`\n'
|
||||||
f'*Ticker Interval:* `{ticker_interval}`\n'
|
f'*Timeframe:* `{timeframe}`\n'
|
||||||
f'*Strategy:* `{strategy_name}`'
|
f'*Strategy:* `{strategy_name}`'
|
||||||
})
|
})
|
||||||
self.send_msg({
|
self.send_msg({
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
"""
|
"""
|
||||||
This module manage Telegram communication
|
This module manage Telegram communication
|
||||||
"""
|
"""
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Callable, Dict
|
from typing import Any, Callable, Dict
|
||||||
|
|
||||||
@ -19,7 +20,6 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
logger.debug('Included module rpc.telegram ...')
|
logger.debug('Included module rpc.telegram ...')
|
||||||
|
|
||||||
|
|
||||||
MAX_TELEGRAM_MESSAGE_LENGTH = 4096
|
MAX_TELEGRAM_MESSAGE_LENGTH = 4096
|
||||||
|
|
||||||
|
|
||||||
@ -29,6 +29,7 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]:
|
|||||||
:param command_handler: Telegram CommandHandler
|
:param command_handler: Telegram CommandHandler
|
||||||
:return: decorated function
|
:return: decorated function
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def wrapper(self, *args, **kwargs):
|
def wrapper(self, *args, **kwargs):
|
||||||
""" Decorator logic """
|
""" Decorator logic """
|
||||||
update = kwargs.get('update') or args[0]
|
update = kwargs.get('update') or args[0]
|
||||||
@ -94,8 +95,8 @@ class Telegram(RPC):
|
|||||||
CommandHandler('performance', self._performance),
|
CommandHandler('performance', self._performance),
|
||||||
CommandHandler('daily', self._daily),
|
CommandHandler('daily', self._daily),
|
||||||
CommandHandler('count', self._count),
|
CommandHandler('count', self._count),
|
||||||
CommandHandler('reload_conf', self._reload_conf),
|
CommandHandler(['reload_config', 'reload_conf'], self._reload_config),
|
||||||
CommandHandler('show_config', self._show_config),
|
CommandHandler(['show_config', 'show_conf'], self._show_config),
|
||||||
CommandHandler('stopbuy', self._stopbuy),
|
CommandHandler('stopbuy', self._stopbuy),
|
||||||
CommandHandler('whitelist', self._whitelist),
|
CommandHandler('whitelist', self._whitelist),
|
||||||
CommandHandler('blacklist', self._blacklist),
|
CommandHandler('blacklist', self._blacklist),
|
||||||
@ -133,7 +134,7 @@ class Telegram(RPC):
|
|||||||
else:
|
else:
|
||||||
msg['stake_amount_fiat'] = 0
|
msg['stake_amount_fiat'] = 0
|
||||||
|
|
||||||
message = ("*{exchange}:* Buying {pair}\n"
|
message = ("\N{LARGE BLUE CIRCLE} *{exchange}:* Buying {pair}\n"
|
||||||
"*Amount:* `{amount:.8f}`\n"
|
"*Amount:* `{amount:.8f}`\n"
|
||||||
"*Open Rate:* `{limit:.8f}`\n"
|
"*Open Rate:* `{limit:.8f}`\n"
|
||||||
"*Current Rate:* `{current_rate:.8f}`\n"
|
"*Current Rate:* `{current_rate:.8f}`\n"
|
||||||
@ -144,7 +145,8 @@ class Telegram(RPC):
|
|||||||
message += ")`"
|
message += ")`"
|
||||||
|
|
||||||
elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION:
|
elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION:
|
||||||
message = "*{exchange}:* Cancelling Open Buy Order for {pair}".format(**msg)
|
message = ("\N{WARNING SIGN} *{exchange}:* "
|
||||||
|
"Cancelling Open Buy Order for {pair}".format(**msg))
|
||||||
|
|
||||||
elif msg['type'] == RPCMessageType.SELL_NOTIFICATION:
|
elif msg['type'] == RPCMessageType.SELL_NOTIFICATION:
|
||||||
msg['amount'] = round(msg['amount'], 8)
|
msg['amount'] = round(msg['amount'], 8)
|
||||||
@ -153,7 +155,9 @@ class Telegram(RPC):
|
|||||||
microsecond=0) - msg['open_date'].replace(microsecond=0)
|
microsecond=0) - msg['open_date'].replace(microsecond=0)
|
||||||
msg['duration_min'] = msg['duration'].total_seconds() / 60
|
msg['duration_min'] = msg['duration'].total_seconds() / 60
|
||||||
|
|
||||||
message = ("*{exchange}:* Selling {pair}\n"
|
msg['emoji'] = self._get_sell_emoji(msg)
|
||||||
|
|
||||||
|
message = ("{emoji} *{exchange}:* Selling {pair}\n"
|
||||||
"*Amount:* `{amount:.8f}`\n"
|
"*Amount:* `{amount:.8f}`\n"
|
||||||
"*Open Rate:* `{open_rate:.8f}`\n"
|
"*Open Rate:* `{open_rate:.8f}`\n"
|
||||||
"*Current Rate:* `{current_rate:.8f}`\n"
|
"*Current Rate:* `{current_rate:.8f}`\n"
|
||||||
@ -165,21 +169,21 @@ class Telegram(RPC):
|
|||||||
# Check if all sell properties are available.
|
# Check if all sell properties are available.
|
||||||
# This might not be the case if the message origin is triggered by /forcesell
|
# This might not be the case if the message origin is triggered by /forcesell
|
||||||
if (all(prop in msg for prop in ['gain', 'fiat_currency', 'stake_currency'])
|
if (all(prop in msg for prop in ['gain', 'fiat_currency', 'stake_currency'])
|
||||||
and self._fiat_converter):
|
and self._fiat_converter):
|
||||||
msg['profit_fiat'] = self._fiat_converter.convert_amount(
|
msg['profit_fiat'] = self._fiat_converter.convert_amount(
|
||||||
msg['profit_amount'], msg['stake_currency'], msg['fiat_currency'])
|
msg['profit_amount'], msg['stake_currency'], msg['fiat_currency'])
|
||||||
message += (' `({gain}: {profit_amount:.8f} {stake_currency}'
|
message += (' `({gain}: {profit_amount:.8f} {stake_currency}'
|
||||||
' / {profit_fiat:.3f} {fiat_currency})`').format(**msg)
|
' / {profit_fiat:.3f} {fiat_currency})`').format(**msg)
|
||||||
|
|
||||||
elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION:
|
elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION:
|
||||||
message = ("*{exchange}:* Cancelling Open Sell Order "
|
message = ("\N{WARNING SIGN} *{exchange}:* Cancelling Open Sell Order "
|
||||||
"for {pair}. Reason: {reason}").format(**msg)
|
"for {pair}. Reason: {reason}").format(**msg)
|
||||||
|
|
||||||
elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION:
|
elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION:
|
||||||
message = '*Status:* `{status}`'.format(**msg)
|
message = '*Status:* `{status}`'.format(**msg)
|
||||||
|
|
||||||
elif msg['type'] == RPCMessageType.WARNING_NOTIFICATION:
|
elif msg['type'] == RPCMessageType.WARNING_NOTIFICATION:
|
||||||
message = '*Warning:* `{status}`'.format(**msg)
|
message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg)
|
||||||
|
|
||||||
elif msg['type'] == RPCMessageType.CUSTOM_NOTIFICATION:
|
elif msg['type'] == RPCMessageType.CUSTOM_NOTIFICATION:
|
||||||
message = '{status}'.format(**msg)
|
message = '{status}'.format(**msg)
|
||||||
@ -189,6 +193,20 @@ class Telegram(RPC):
|
|||||||
|
|
||||||
self._send_msg(message)
|
self._send_msg(message)
|
||||||
|
|
||||||
|
def _get_sell_emoji(self, msg):
|
||||||
|
"""
|
||||||
|
Get emoji for sell-side
|
||||||
|
"""
|
||||||
|
|
||||||
|
if float(msg['profit_percent']) >= 5.0:
|
||||||
|
return "\N{ROCKET}"
|
||||||
|
elif float(msg['profit_percent']) >= 0.0:
|
||||||
|
return "\N{EIGHT SPOKED ASTERISK}"
|
||||||
|
elif msg['sell_reason'] == "stop_loss":
|
||||||
|
return"\N{WARNING SIGN}"
|
||||||
|
else:
|
||||||
|
return "\N{CROSS MARK}"
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _status(self, update: Update, context: CallbackContext) -> None:
|
def _status(self, update: Update, context: CallbackContext) -> None:
|
||||||
"""
|
"""
|
||||||
@ -222,8 +240,8 @@ class Telegram(RPC):
|
|||||||
# Adding initial stoploss only if it is different from stoploss
|
# Adding initial stoploss only if it is different from stoploss
|
||||||
"*Initial Stoploss:* `{initial_stop_loss:.8f}` " +
|
"*Initial Stoploss:* `{initial_stop_loss:.8f}` " +
|
||||||
("`({initial_stop_loss_pct:.2f}%)`") if (
|
("`({initial_stop_loss_pct:.2f}%)`") if (
|
||||||
r['stop_loss'] != r['initial_stop_loss']
|
r['stop_loss'] != r['initial_stop_loss']
|
||||||
and r['initial_stop_loss_pct'] is not None) else "",
|
and r['initial_stop_loss_pct'] is not None) else "",
|
||||||
|
|
||||||
# Adding stoploss and stoploss percentage only if it is not None
|
# Adding stoploss and stoploss percentage only if it is not None
|
||||||
"*Stoploss:* `{stop_loss:.8f}` " +
|
"*Stoploss:* `{stop_loss:.8f}` " +
|
||||||
@ -311,38 +329,48 @@ class Telegram(RPC):
|
|||||||
stake_cur = self._config['stake_currency']
|
stake_cur = self._config['stake_currency']
|
||||||
fiat_disp_cur = self._config.get('fiat_display_currency', '')
|
fiat_disp_cur = self._config.get('fiat_display_currency', '')
|
||||||
|
|
||||||
try:
|
stats = self._rpc_trade_statistics(
|
||||||
stats = self._rpc_trade_statistics(
|
stake_cur,
|
||||||
stake_cur,
|
fiat_disp_cur)
|
||||||
fiat_disp_cur)
|
profit_closed_coin = stats['profit_closed_coin']
|
||||||
profit_closed_coin = stats['profit_closed_coin']
|
profit_closed_percent_mean = stats['profit_closed_percent_mean']
|
||||||
profit_closed_percent = stats['profit_closed_percent']
|
profit_closed_percent_sum = stats['profit_closed_percent_sum']
|
||||||
profit_closed_fiat = stats['profit_closed_fiat']
|
profit_closed_fiat = stats['profit_closed_fiat']
|
||||||
profit_all_coin = stats['profit_all_coin']
|
profit_all_coin = stats['profit_all_coin']
|
||||||
profit_all_percent = stats['profit_all_percent']
|
profit_all_percent_mean = stats['profit_all_percent_mean']
|
||||||
profit_all_fiat = stats['profit_all_fiat']
|
profit_all_percent_sum = stats['profit_all_percent_sum']
|
||||||
trade_count = stats['trade_count']
|
profit_all_fiat = stats['profit_all_fiat']
|
||||||
first_trade_date = stats['first_trade_date']
|
trade_count = stats['trade_count']
|
||||||
latest_trade_date = stats['latest_trade_date']
|
first_trade_date = stats['first_trade_date']
|
||||||
avg_duration = stats['avg_duration']
|
latest_trade_date = stats['latest_trade_date']
|
||||||
best_pair = stats['best_pair']
|
avg_duration = stats['avg_duration']
|
||||||
best_rate = stats['best_rate']
|
best_pair = stats['best_pair']
|
||||||
|
best_rate = stats['best_rate']
|
||||||
|
if stats['trade_count'] == 0:
|
||||||
|
markdown_msg = 'No trades yet.'
|
||||||
|
else:
|
||||||
# Message to display
|
# Message to display
|
||||||
markdown_msg = "*ROI:* Close trades\n" \
|
if stats['closed_trade_count'] > 0:
|
||||||
f"∙ `{profit_closed_coin:.8f} {stake_cur} "\
|
markdown_msg = ("*ROI:* Closed trades\n"
|
||||||
f"({profit_closed_percent:.2f}%)`\n" \
|
f"∙ `{profit_closed_coin:.8f} {stake_cur} "
|
||||||
f"∙ `{profit_closed_fiat:.3f} {fiat_disp_cur}`\n" \
|
f"({profit_closed_percent_mean:.2f}%) "
|
||||||
f"*ROI:* All trades\n" \
|
f"({profit_closed_percent_sum} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
|
||||||
f"∙ `{profit_all_coin:.8f} {stake_cur} ({profit_all_percent:.2f}%)`\n" \
|
f"∙ `{profit_closed_fiat:.3f} {fiat_disp_cur}`\n")
|
||||||
f"∙ `{profit_all_fiat:.3f} {fiat_disp_cur}`\n" \
|
else:
|
||||||
f"*Total Trade Count:* `{trade_count}`\n" \
|
markdown_msg = "`No closed trade` \n"
|
||||||
f"*First Trade opened:* `{first_trade_date}`\n" \
|
|
||||||
f"*Latest Trade opened:* `{latest_trade_date}`\n" \
|
markdown_msg += (f"*ROI:* All trades\n"
|
||||||
f"*Avg. Duration:* `{avg_duration}`\n" \
|
f"∙ `{profit_all_coin:.8f} {stake_cur} "
|
||||||
f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`"
|
f"({profit_all_percent_mean:.2f}%) "
|
||||||
self._send_msg(markdown_msg)
|
f"({profit_all_percent_sum} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
|
||||||
except RPCException as e:
|
f"∙ `{profit_all_fiat:.3f} {fiat_disp_cur}`\n"
|
||||||
self._send_msg(str(e))
|
f"*Total Trade Count:* `{trade_count}`\n"
|
||||||
|
f"*First Trade opened:* `{first_trade_date}`\n"
|
||||||
|
f"*Latest Trade opened:* `{latest_trade_date}`")
|
||||||
|
if stats['closed_trade_count'] > 0:
|
||||||
|
markdown_msg += (f"\n*Avg. Duration:* `{avg_duration}`\n"
|
||||||
|
f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`")
|
||||||
|
self._send_msg(markdown_msg)
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _balance(self, update: Update, context: CallbackContext) -> None:
|
def _balance(self, update: Update, context: CallbackContext) -> None:
|
||||||
@ -358,14 +386,14 @@ class Telegram(RPC):
|
|||||||
"This mode is still experimental!\n"
|
"This mode is still experimental!\n"
|
||||||
"Starting capital: "
|
"Starting capital: "
|
||||||
f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n"
|
f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n"
|
||||||
)
|
)
|
||||||
for currency in result['currencies']:
|
for currency in result['currencies']:
|
||||||
if currency['est_stake'] > 0.0001:
|
if currency['est_stake'] > 0.0001:
|
||||||
curr_output = "*{currency}:*\n" \
|
curr_output = ("*{currency}:*\n"
|
||||||
"\t`Available: {free: .8f}`\n" \
|
"\t`Available: {free: .8f}`\n"
|
||||||
"\t`Balance: {balance: .8f}`\n" \
|
"\t`Balance: {balance: .8f}`\n"
|
||||||
"\t`Pending: {used: .8f}`\n" \
|
"\t`Pending: {used: .8f}`\n"
|
||||||
"\t`Est. {stake}: {est_stake: .8f}`\n".format(**currency)
|
"\t`Est. {stake}: {est_stake: .8f}`\n").format(**currency)
|
||||||
else:
|
else:
|
||||||
curr_output = "*{currency}:* not showing <1$ amount \n".format(**currency)
|
curr_output = "*{currency}:* not showing <1$ amount \n".format(**currency)
|
||||||
|
|
||||||
@ -376,9 +404,9 @@ class Telegram(RPC):
|
|||||||
else:
|
else:
|
||||||
output += curr_output
|
output += curr_output
|
||||||
|
|
||||||
output += "\n*Estimated Value*:\n" \
|
output += ("\n*Estimated Value*:\n"
|
||||||
"\t`{stake}: {total: .8f}`\n" \
|
"\t`{stake}: {total: .8f}`\n"
|
||||||
"\t`{symbol}: {value: .2f}`\n".format(**result)
|
"\t`{symbol}: {value: .2f}`\n").format(**result)
|
||||||
self._send_msg(output)
|
self._send_msg(output)
|
||||||
except RPCException as e:
|
except RPCException as e:
|
||||||
self._send_msg(str(e))
|
self._send_msg(str(e))
|
||||||
@ -408,15 +436,15 @@ class Telegram(RPC):
|
|||||||
self._send_msg('Status: `{status}`'.format(**msg))
|
self._send_msg('Status: `{status}`'.format(**msg))
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _reload_conf(self, update: Update, context: CallbackContext) -> None:
|
def _reload_config(self, update: Update, context: CallbackContext) -> None:
|
||||||
"""
|
"""
|
||||||
Handler for /reload_conf.
|
Handler for /reload_config.
|
||||||
Triggers a config file reload
|
Triggers a config file reload
|
||||||
:param bot: telegram bot
|
:param bot: telegram bot
|
||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
msg = self._rpc_reload_conf()
|
msg = self._rpc_reload_config()
|
||||||
self._send_msg('Status: `{status}`'.format(**msg))
|
self._send_msg('Status: `{status}`'.format(**msg))
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
@ -534,6 +562,11 @@ class Telegram(RPC):
|
|||||||
try:
|
try:
|
||||||
|
|
||||||
blacklist = self._rpc_blacklist(context.args)
|
blacklist = self._rpc_blacklist(context.args)
|
||||||
|
errmsgs = []
|
||||||
|
for pair, error in blacklist['errors'].items():
|
||||||
|
errmsgs.append(f"Error adding `{pair}` to blacklist: `{error['error_msg']}`")
|
||||||
|
if errmsgs:
|
||||||
|
self._send_msg('\n'.join(errmsgs))
|
||||||
|
|
||||||
message = f"Blacklist contains {blacklist['length']} pairs\n"
|
message = f"Blacklist contains {blacklist['length']} pairs\n"
|
||||||
message += f"`{', '.join(blacklist['blacklist'])}`"
|
message += f"`{', '.join(blacklist['blacklist'])}`"
|
||||||
@ -566,32 +599,32 @@ class Telegram(RPC):
|
|||||||
:param update: message update
|
:param update: message update
|
||||||
: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.` \n")
|
||||||
message = "*/start:* `Starts the trader`\n" \
|
message = ("*/start:* `Starts the trader`\n"
|
||||||
"*/stop:* `Stops the trader`\n" \
|
"*/stop:* `Stops the trader`\n"
|
||||||
"*/status [table]:* `Lists all open trades`\n" \
|
"*/status [table]:* `Lists all open trades`\n"
|
||||||
" *table :* `will display trades in a table`\n" \
|
" *table :* `will display trades in a table`\n"
|
||||||
" `pending buy orders are marked with an asterisk (*)`\n" \
|
" `pending buy orders are marked with an asterisk (*)`\n"
|
||||||
" `pending sell orders are marked with a double asterisk (**)`\n" \
|
" `pending sell orders are marked with a double asterisk (**)`\n"
|
||||||
"*/profit:* `Lists cumulative profit from all finished trades`\n" \
|
"*/profit:* `Lists cumulative profit from all finished trades`\n"
|
||||||
"*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, " \
|
"*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, "
|
||||||
"regardless of profit`\n" \
|
"regardless of profit`\n"
|
||||||
f"{forcebuy_text if self._config.get('forcebuy_enable', False) else '' }" \
|
f"{forcebuy_text if self._config.get('forcebuy_enable', False) else ''}"
|
||||||
"*/performance:* `Show performance of each finished trade grouped by pair`\n" \
|
"*/performance:* `Show performance of each finished trade grouped by pair`\n"
|
||||||
"*/daily <n>:* `Shows profit or loss per day, over the last n days`\n" \
|
"*/daily <n>:* `Shows profit or loss per day, over the last n days`\n"
|
||||||
"*/count:* `Show number of trades running compared to allowed number of trades`" \
|
"*/count:* `Show number of trades running compared to allowed number of trades`"
|
||||||
"\n" \
|
"\n"
|
||||||
"*/balance:* `Show account balance per currency`\n" \
|
"*/balance:* `Show account balance per currency`\n"
|
||||||
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" \
|
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n"
|
||||||
"*/reload_conf:* `Reload configuration file` \n" \
|
"*/reload_config:* `Reload configuration file` \n"
|
||||||
"*/show_config:* `Show running configuration` \n" \
|
"*/show_config:* `Show running configuration` \n"
|
||||||
"*/whitelist:* `Show current whitelist` \n" \
|
"*/whitelist:* `Show current whitelist` \n"
|
||||||
"*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs " \
|
"*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs "
|
||||||
"to the blacklist.` \n" \
|
"to the blacklist.` \n"
|
||||||
"*/edge:* `Shows validated pairs by Edge if it is enabled` \n" \
|
"*/edge:* `Shows validated pairs by Edge if it is enabled` \n"
|
||||||
"*/help:* `This help message`\n" \
|
"*/help:* `This help message`\n"
|
||||||
"*/version:* `Show version`"
|
"*/version:* `Show version`")
|
||||||
|
|
||||||
self._send_msg(message)
|
self._send_msg(message)
|
||||||
|
|
||||||
@ -633,8 +666,10 @@ class Telegram(RPC):
|
|||||||
f"*Stake per trade:* `{val['stake_amount']} {val['stake_currency']}`\n"
|
f"*Stake per trade:* `{val['stake_amount']} {val['stake_currency']}`\n"
|
||||||
f"*Max open Trades:* `{val['max_open_trades']}`\n"
|
f"*Max open Trades:* `{val['max_open_trades']}`\n"
|
||||||
f"*Minimum ROI:* `{val['minimal_roi']}`\n"
|
f"*Minimum ROI:* `{val['minimal_roi']}`\n"
|
||||||
|
f"*Ask strategy:* ```\n{json.dumps(val['ask_strategy'])}```\n"
|
||||||
|
f"*Bid strategy:* ```\n{json.dumps(val['bid_strategy'])}```\n"
|
||||||
f"{sl_info}"
|
f"{sl_info}"
|
||||||
f"*Ticker Interval:* `{val['ticker_interval']}`\n"
|
f"*Timeframe:* `{val['timeframe']}`\n"
|
||||||
f"*Strategy:* `{val['strategy']}`\n"
|
f"*Strategy:* `{val['strategy']}`\n"
|
||||||
f"*Current state:* `{val['state']}`"
|
f"*Current state:* `{val['state']}`"
|
||||||
)
|
)
|
||||||
|
@ -12,7 +12,7 @@ class State(Enum):
|
|||||||
"""
|
"""
|
||||||
RUNNING = 1
|
RUNNING = 1
|
||||||
STOPPED = 2
|
STOPPED = 2
|
||||||
RELOAD_CONF = 3
|
RELOAD_CONFIG = 3
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name.lower()}"
|
return f"{self.name.lower()}"
|
||||||
|
@ -62,7 +62,7 @@ class IStrategy(ABC):
|
|||||||
Attributes you can use:
|
Attributes you can use:
|
||||||
minimal_roi -> Dict: Minimal ROI designed for the strategy
|
minimal_roi -> Dict: Minimal ROI designed for the strategy
|
||||||
stoploss -> float: optimal stoploss designed for the strategy
|
stoploss -> float: optimal stoploss designed for the strategy
|
||||||
ticker_interval -> str: value of the timeframe (ticker interval) to use with the strategy
|
timeframe -> str: value of the timeframe (ticker interval) to use with the strategy
|
||||||
"""
|
"""
|
||||||
# Strategy interface version
|
# Strategy interface version
|
||||||
# Default to version 2
|
# Default to version 2
|
||||||
@ -85,8 +85,9 @@ class IStrategy(ABC):
|
|||||||
trailing_stop_positive_offset: float = 0.0
|
trailing_stop_positive_offset: float = 0.0
|
||||||
trailing_only_offset_is_reached = False
|
trailing_only_offset_is_reached = False
|
||||||
|
|
||||||
# associated ticker interval
|
# associated timeframe
|
||||||
ticker_interval: str
|
ticker_interval: str # DEPRECATED
|
||||||
|
timeframe: str
|
||||||
|
|
||||||
# Optional order types
|
# Optional order types
|
||||||
order_types: Dict = {
|
order_types: Dict = {
|
||||||
@ -106,6 +107,9 @@ class IStrategy(ABC):
|
|||||||
# run "populate_indicators" only for new candle
|
# run "populate_indicators" only for new candle
|
||||||
process_only_new_candles: bool = False
|
process_only_new_candles: bool = False
|
||||||
|
|
||||||
|
# Disable checking the dataframe (converts the error into a warning message)
|
||||||
|
disable_dataframe_checks: bool = False
|
||||||
|
|
||||||
# Count of candles the strategy requires before producing valid signals
|
# Count of candles the strategy requires before producing valid signals
|
||||||
startup_candle_count: int = 0
|
startup_candle_count: int = 0
|
||||||
|
|
||||||
@ -285,8 +289,7 @@ class IStrategy(ABC):
|
|||||||
""" keep some data for dataframes """
|
""" keep some data for dataframes """
|
||||||
return len(dataframe), dataframe["close"].iloc[-1], dataframe["date"].iloc[-1]
|
return len(dataframe), dataframe["close"].iloc[-1], dataframe["date"].iloc[-1]
|
||||||
|
|
||||||
@staticmethod
|
def assert_df(self, dataframe: DataFrame, df_len: int, df_close: float, df_date: datetime):
|
||||||
def assert_df(dataframe: DataFrame, df_len: int, df_close: float, df_date: datetime):
|
|
||||||
""" make sure data is unmodified """
|
""" make sure data is unmodified """
|
||||||
message = ""
|
message = ""
|
||||||
if df_len != len(dataframe):
|
if df_len != len(dataframe):
|
||||||
@ -296,7 +299,10 @@ class IStrategy(ABC):
|
|||||||
elif df_date != dataframe["date"].iloc[-1]:
|
elif df_date != dataframe["date"].iloc[-1]:
|
||||||
message = "last date"
|
message = "last date"
|
||||||
if message:
|
if message:
|
||||||
raise StrategyError(f"Dataframe returned from strategy has mismatching {message}.")
|
if self.disable_dataframe_checks:
|
||||||
|
logger.warning(f"Dataframe returned from strategy has mismatching {message}.")
|
||||||
|
else:
|
||||||
|
raise StrategyError(f"Dataframe returned from strategy has mismatching {message}.")
|
||||||
|
|
||||||
def get_signal(self, pair: str, interval: str, dataframe: DataFrame) -> Tuple[bool, bool]:
|
def get_signal(self, pair: str, interval: str, dataframe: DataFrame) -> Tuple[bool, bool]:
|
||||||
"""
|
"""
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
"stake_amount": {{ stake_amount }},
|
"stake_amount": {{ stake_amount }},
|
||||||
"tradable_balance_ratio": 0.99,
|
"tradable_balance_ratio": 0.99,
|
||||||
"fiat_display_currency": "{{ fiat_display_currency }}",
|
"fiat_display_currency": "{{ fiat_display_currency }}",
|
||||||
"ticker_interval": "{{ ticker_interval }}",
|
"timeframe": "{{ timeframe }}",
|
||||||
"dry_run": {{ dry_run | lower }},
|
"dry_run": {{ dry_run | lower }},
|
||||||
"cancel_open_orders_on_exit": false,
|
"cancel_open_orders_on_exit": false,
|
||||||
"unfilledtimeout": {
|
"unfilledtimeout": {
|
||||||
@ -53,6 +53,16 @@
|
|||||||
"token": "{{ telegram_token }}",
|
"token": "{{ telegram_token }}",
|
||||||
"chat_id": "{{ telegram_chat_id }}"
|
"chat_id": "{{ telegram_chat_id }}"
|
||||||
},
|
},
|
||||||
|
"api_server": {
|
||||||
|
"enabled": false,
|
||||||
|
"listen_ip_address": "127.0.0.1",
|
||||||
|
"listen_port": 8080,
|
||||||
|
"verbosity": "info",
|
||||||
|
"jwt_secret_key": "somethingrandom",
|
||||||
|
"CORS_origins": [],
|
||||||
|
"username": "",
|
||||||
|
"password": ""
|
||||||
|
},
|
||||||
"initial_state": "running",
|
"initial_state": "running",
|
||||||
"forcebuy_enable": false,
|
"forcebuy_enable": false,
|
||||||
"internals": {
|
"internals": {
|
||||||
|
@ -51,8 +51,8 @@ class {{ strategy }}(IStrategy):
|
|||||||
# trailing_stop_positive = 0.01
|
# trailing_stop_positive = 0.01
|
||||||
# trailing_stop_positive_offset = 0.0 # Disabled / not configured
|
# trailing_stop_positive_offset = 0.0 # Disabled / not configured
|
||||||
|
|
||||||
# Optimal ticker interval for the strategy.
|
# Optimal timeframe for the strategy.
|
||||||
ticker_interval = '5m'
|
timeframe = '5m'
|
||||||
|
|
||||||
# Run "populate_indicators()" only for new candle.
|
# Run "populate_indicators()" only for new candle.
|
||||||
process_only_new_candles = False
|
process_only_new_candles = False
|
||||||
|
@ -53,7 +53,7 @@ class SampleStrategy(IStrategy):
|
|||||||
# trailing_stop_positive_offset = 0.0 # Disabled / not configured
|
# trailing_stop_positive_offset = 0.0 # Disabled / not configured
|
||||||
|
|
||||||
# Optimal ticker interval for the strategy.
|
# Optimal ticker interval for the strategy.
|
||||||
ticker_interval = '5m'
|
timeframe = '5m'
|
||||||
|
|
||||||
# Run "populate_indicators()" only for new candle.
|
# Run "populate_indicators()" only for new candle.
|
||||||
process_only_new_candles = False
|
process_only_new_candles = False
|
||||||
|
@ -71,7 +71,7 @@ class Worker:
|
|||||||
state = None
|
state = None
|
||||||
while True:
|
while True:
|
||||||
state = self._worker(old_state=state)
|
state = self._worker(old_state=state)
|
||||||
if state == State.RELOAD_CONF:
|
if state == State.RELOAD_CONFIG:
|
||||||
self._reconfigure()
|
self._reconfigure()
|
||||||
|
|
||||||
def _worker(self, old_state: Optional[State]) -> State:
|
def _worker(self, old_state: Optional[State]) -> State:
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
# requirements without requirements installable via conda
|
# requirements without requirements installable via conda
|
||||||
# mainly used for Raspberry pi installs
|
# mainly used for Raspberry pi installs
|
||||||
ccxt==1.28.49
|
ccxt==1.30.48
|
||||||
SQLAlchemy==1.3.17
|
SQLAlchemy==1.3.18
|
||||||
python-telegram-bot==12.7
|
python-telegram-bot==12.8
|
||||||
arrow==0.15.6
|
arrow==0.15.7
|
||||||
cachetools==4.1.0
|
cachetools==4.1.1
|
||||||
requests==2.23.0
|
requests==2.24.0
|
||||||
urllib3==1.25.9
|
urllib3==1.25.9
|
||||||
wrapt==1.12.1
|
wrapt==1.12.1
|
||||||
jsonschema==3.2.0
|
jsonschema==3.2.0
|
||||||
|
@ -4,14 +4,14 @@
|
|||||||
-r requirements-hyperopt.txt
|
-r requirements-hyperopt.txt
|
||||||
|
|
||||||
coveralls==2.0.0
|
coveralls==2.0.0
|
||||||
flake8==3.8.2
|
flake8==3.8.3
|
||||||
flake8-type-annotations==0.1.0
|
flake8-type-annotations==0.1.0
|
||||||
flake8-tidy-imports==4.1.0
|
flake8-tidy-imports==4.1.0
|
||||||
mypy==0.770
|
mypy==0.782
|
||||||
pytest==5.4.2
|
pytest==5.4.3
|
||||||
pytest-asyncio==0.12.0
|
pytest-asyncio==0.14.0
|
||||||
pytest-cov==2.9.0
|
pytest-cov==2.10.0
|
||||||
pytest-mock==3.1.0
|
pytest-mock==3.1.1
|
||||||
pytest-random-order==1.0.4
|
pytest-random-order==1.0.4
|
||||||
|
|
||||||
# Convert jupyter notebooks to markdown documents
|
# Convert jupyter notebooks to markdown documents
|
||||||
|
@ -2,9 +2,9 @@
|
|||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
|
|
||||||
# Required for hyperopt
|
# Required for hyperopt
|
||||||
scipy==1.4.1
|
scipy==1.5.0
|
||||||
scikit-learn==0.23.1
|
scikit-learn==0.23.1
|
||||||
scikit-optimize==0.7.4
|
scikit-optimize==0.7.4
|
||||||
filelock==3.0.12
|
filelock==3.0.12
|
||||||
joblib==0.15.1
|
joblib==0.15.1
|
||||||
progressbar2==3.51.3
|
progressbar2==3.51.4
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Include all requirements to run the bot.
|
# Include all requirements to run the bot.
|
||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
|
|
||||||
plotly==4.7.1
|
plotly==4.8.2
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Load common requirements
|
# Load common requirements
|
||||||
-r requirements-common.txt
|
-r requirements-common.txt
|
||||||
|
|
||||||
numpy==1.18.4
|
numpy==1.19.0
|
||||||
pandas==1.0.4
|
pandas==1.0.5
|
||||||
|
@ -80,18 +80,18 @@ class FtRestClient():
|
|||||||
return self._post("stop")
|
return self._post("stop")
|
||||||
|
|
||||||
def stopbuy(self):
|
def stopbuy(self):
|
||||||
"""Stop buying (but handle sells gracefully). Use `reload_conf` to reset.
|
"""Stop buying (but handle sells gracefully). Use `reload_config` to reset.
|
||||||
|
|
||||||
:return: json object
|
:return: json object
|
||||||
"""
|
"""
|
||||||
return self._post("stopbuy")
|
return self._post("stopbuy")
|
||||||
|
|
||||||
def reload_conf(self):
|
def reload_config(self):
|
||||||
"""Reload configuration.
|
"""Reload configuration.
|
||||||
|
|
||||||
:return: json object
|
:return: json object
|
||||||
"""
|
"""
|
||||||
return self._post("reload_conf")
|
return self._post("reload_config")
|
||||||
|
|
||||||
def balance(self):
|
def balance(self):
|
||||||
"""Get the account balance.
|
"""Get the account balance.
|
||||||
|
2
setup.py
2
setup.py
@ -63,7 +63,7 @@ setup(name='freqtrade',
|
|||||||
tests_require=['pytest', 'pytest-asyncio', 'pytest-cov', 'pytest-mock', ],
|
tests_require=['pytest', 'pytest-asyncio', 'pytest-cov', 'pytest-mock', ],
|
||||||
install_requires=[
|
install_requires=[
|
||||||
# from requirements-common.txt
|
# from requirements-common.txt
|
||||||
'ccxt>=1.18.1080',
|
'ccxt>=1.24.96',
|
||||||
'SQLAlchemy',
|
'SQLAlchemy',
|
||||||
'python-telegram-bot',
|
'python-telegram-bot',
|
||||||
'arrow',
|
'arrow',
|
||||||
|
@ -44,7 +44,7 @@ def test_start_new_config(mocker, caplog, exchange):
|
|||||||
'stake_currency': 'USDT',
|
'stake_currency': 'USDT',
|
||||||
'stake_amount': 100,
|
'stake_amount': 100,
|
||||||
'fiat_display_currency': 'EUR',
|
'fiat_display_currency': 'EUR',
|
||||||
'ticker_interval': '15m',
|
'timeframe': '15m',
|
||||||
'dry_run': True,
|
'dry_run': True,
|
||||||
'exchange_name': exchange,
|
'exchange_name': exchange,
|
||||||
'exchange_key': 'sampleKey',
|
'exchange_key': 'sampleKey',
|
||||||
@ -68,7 +68,7 @@ def test_start_new_config(mocker, caplog, exchange):
|
|||||||
result = rapidjson.loads(wt_mock.call_args_list[0][0][0],
|
result = rapidjson.loads(wt_mock.call_args_list[0][0][0],
|
||||||
parse_mode=rapidjson.PM_COMMENTS | rapidjson.PM_TRAILING_COMMAS)
|
parse_mode=rapidjson.PM_COMMENTS | rapidjson.PM_TRAILING_COMMAS)
|
||||||
assert result['exchange']['name'] == exchange
|
assert result['exchange']['name'] == exchange
|
||||||
assert result['ticker_interval'] == '15m'
|
assert result['timeframe'] == '15m'
|
||||||
|
|
||||||
|
|
||||||
def test_start_new_config_exists(mocker, caplog):
|
def test_start_new_config_exists(mocker, caplog):
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
"fiat_display_currency": "USD", // C++-style comment
|
"fiat_display_currency": "USD", // C++-style comment
|
||||||
"amount_reserve_percent" : 0.05, // And more, tabs before this comment
|
"amount_reserve_percent" : 0.05, // And more, tabs before this comment
|
||||||
"dry_run": false,
|
"dry_run": false,
|
||||||
"ticker_interval": "5m",
|
"timeframe": "5m",
|
||||||
"trailing_stop": false,
|
"trailing_stop": false,
|
||||||
"trailing_stop_positive": 0.005,
|
"trailing_stop_positive": 0.005,
|
||||||
"trailing_stop_positive_offset": 0.0051,
|
"trailing_stop_positive_offset": 0.0051,
|
||||||
@ -92,7 +92,6 @@
|
|||||||
"enabled": false,
|
"enabled": false,
|
||||||
"process_throttle_secs": 3600,
|
"process_throttle_secs": 3600,
|
||||||
"calculate_since_number_of_days": 7,
|
"calculate_since_number_of_days": 7,
|
||||||
"capital_available_percentage": 0.5,
|
|
||||||
"allowed_risk": 0.01,
|
"allowed_risk": 0.01,
|
||||||
"stoploss_range_min": -0.01,
|
"stoploss_range_min": -0.01,
|
||||||
"stoploss_range_max": -0.1,
|
"stoploss_range_max": -0.1,
|
||||||
|
@ -56,6 +56,7 @@ def patched_configuration_load_config_file(mocker, config) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def patch_exchange(mocker, api_mock=None, id='bittrex', mock_markets=True) -> None:
|
def patch_exchange(mocker, api_mock=None, id='bittrex', mock_markets=True) -> None:
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock(return_value={}))
|
||||||
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
@ -247,7 +248,7 @@ def default_conf(testdatadir):
|
|||||||
"stake_currency": "BTC",
|
"stake_currency": "BTC",
|
||||||
"stake_amount": 0.001,
|
"stake_amount": 0.001,
|
||||||
"fiat_display_currency": "USD",
|
"fiat_display_currency": "USD",
|
||||||
"ticker_interval": '5m',
|
"timeframe": '5m',
|
||||||
"dry_run": True,
|
"dry_run": True,
|
||||||
"cancel_open_orders_on_exit": False,
|
"cancel_open_orders_on_exit": False,
|
||||||
"minimal_roi": {
|
"minimal_roi": {
|
||||||
@ -1423,7 +1424,7 @@ def trades_for_order():
|
|||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def trades_history():
|
def trades_history():
|
||||||
return [[1565798399463, '126181329', None, 'buy', 0.019627, 0.04, 0.00078508],
|
return [[1565798389463, '126181329', None, 'buy', 0.019627, 0.04, 0.00078508],
|
||||||
[1565798399629, '126181330', None, 'buy', 0.019627, 0.244, 0.004788987999999999],
|
[1565798399629, '126181330', None, 'buy', 0.019627, 0.244, 0.004788987999999999],
|
||||||
[1565798399752, '126181331', None, 'sell', 0.019626, 0.011, 0.00021588599999999999],
|
[1565798399752, '126181331', None, 'sell', 0.019626, 0.011, 0.00021588599999999999],
|
||||||
[1565798399862, '126181332', None, 'sell', 0.019626, 0.011, 0.00021588599999999999],
|
[1565798399862, '126181332', None, 'sell', 0.019626, 0.011, 0.00021588599999999999],
|
||||||
@ -1590,6 +1591,7 @@ def buy_order_fee():
|
|||||||
'datetime': str(arrow.utcnow().shift(minutes=-601).datetime),
|
'datetime': str(arrow.utcnow().shift(minutes=-601).datetime),
|
||||||
'price': 0.245441,
|
'price': 0.245441,
|
||||||
'amount': 8.0,
|
'amount': 8.0,
|
||||||
|
'cost': 1.963528,
|
||||||
'remaining': 90.99181073,
|
'remaining': 90.99181073,
|
||||||
'status': 'closed',
|
'status': 'closed',
|
||||||
'fee': None
|
'fee': None
|
||||||
|
@ -47,7 +47,7 @@ def test_load_trades_from_db(default_conf, fee, mocker):
|
|||||||
assert isinstance(trades, DataFrame)
|
assert isinstance(trades, DataFrame)
|
||||||
assert "pair" in trades.columns
|
assert "pair" in trades.columns
|
||||||
assert "open_time" in trades.columns
|
assert "open_time" in trades.columns
|
||||||
assert "profitperc" in trades.columns
|
assert "profit_percent" in trades.columns
|
||||||
|
|
||||||
for col in BT_DATA_COLUMNS:
|
for col in BT_DATA_COLUMNS:
|
||||||
if col not in ['index', 'open_at_end']:
|
if col not in ['index', 'open_at_end']:
|
||||||
|
@ -12,7 +12,7 @@ from tests.conftest import get_patched_exchange
|
|||||||
|
|
||||||
def test_ohlcv(mocker, default_conf, ohlcv_history):
|
def test_ohlcv(mocker, default_conf, ohlcv_history):
|
||||||
default_conf["runmode"] = RunMode.DRY_RUN
|
default_conf["runmode"] = RunMode.DRY_RUN
|
||||||
timeframe = default_conf["ticker_interval"]
|
timeframe = default_conf["timeframe"]
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
exchange._klines[("XRP/BTC", timeframe)] = ohlcv_history
|
exchange._klines[("XRP/BTC", timeframe)] = ohlcv_history
|
||||||
exchange._klines[("UNITTEST/BTC", timeframe)] = ohlcv_history
|
exchange._klines[("UNITTEST/BTC", timeframe)] = ohlcv_history
|
||||||
@ -53,47 +53,47 @@ def test_historic_ohlcv(mocker, default_conf, ohlcv_history):
|
|||||||
|
|
||||||
def test_get_pair_dataframe(mocker, default_conf, ohlcv_history):
|
def test_get_pair_dataframe(mocker, default_conf, ohlcv_history):
|
||||||
default_conf["runmode"] = RunMode.DRY_RUN
|
default_conf["runmode"] = RunMode.DRY_RUN
|
||||||
ticker_interval = default_conf["ticker_interval"]
|
timeframe = default_conf["timeframe"]
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
exchange._klines[("XRP/BTC", ticker_interval)] = ohlcv_history
|
exchange._klines[("XRP/BTC", timeframe)] = ohlcv_history
|
||||||
exchange._klines[("UNITTEST/BTC", ticker_interval)] = ohlcv_history
|
exchange._klines[("UNITTEST/BTC", timeframe)] = ohlcv_history
|
||||||
|
|
||||||
dp = DataProvider(default_conf, exchange)
|
dp = DataProvider(default_conf, exchange)
|
||||||
assert dp.runmode == RunMode.DRY_RUN
|
assert dp.runmode == RunMode.DRY_RUN
|
||||||
assert ohlcv_history.equals(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval))
|
assert ohlcv_history.equals(dp.get_pair_dataframe("UNITTEST/BTC", timeframe))
|
||||||
assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval), DataFrame)
|
assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", timeframe), DataFrame)
|
||||||
assert dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval) is not ohlcv_history
|
assert dp.get_pair_dataframe("UNITTEST/BTC", timeframe) is not ohlcv_history
|
||||||
assert not dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval).empty
|
assert not dp.get_pair_dataframe("UNITTEST/BTC", timeframe).empty
|
||||||
assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty
|
assert dp.get_pair_dataframe("NONESENSE/AAA", timeframe).empty
|
||||||
|
|
||||||
# Test with and without parameter
|
# Test with and without parameter
|
||||||
assert dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval)\
|
assert dp.get_pair_dataframe("UNITTEST/BTC", timeframe)\
|
||||||
.equals(dp.get_pair_dataframe("UNITTEST/BTC"))
|
.equals(dp.get_pair_dataframe("UNITTEST/BTC"))
|
||||||
|
|
||||||
default_conf["runmode"] = RunMode.LIVE
|
default_conf["runmode"] = RunMode.LIVE
|
||||||
dp = DataProvider(default_conf, exchange)
|
dp = DataProvider(default_conf, exchange)
|
||||||
assert dp.runmode == RunMode.LIVE
|
assert dp.runmode == RunMode.LIVE
|
||||||
assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval), DataFrame)
|
assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", timeframe), DataFrame)
|
||||||
assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty
|
assert dp.get_pair_dataframe("NONESENSE/AAA", timeframe).empty
|
||||||
|
|
||||||
historymock = MagicMock(return_value=ohlcv_history)
|
historymock = MagicMock(return_value=ohlcv_history)
|
||||||
mocker.patch("freqtrade.data.dataprovider.load_pair_history", historymock)
|
mocker.patch("freqtrade.data.dataprovider.load_pair_history", historymock)
|
||||||
default_conf["runmode"] = RunMode.BACKTEST
|
default_conf["runmode"] = RunMode.BACKTEST
|
||||||
dp = DataProvider(default_conf, exchange)
|
dp = DataProvider(default_conf, exchange)
|
||||||
assert dp.runmode == RunMode.BACKTEST
|
assert dp.runmode == RunMode.BACKTEST
|
||||||
assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval), DataFrame)
|
assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", timeframe), DataFrame)
|
||||||
# assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty
|
# assert dp.get_pair_dataframe("NONESENSE/AAA", timeframe).empty
|
||||||
|
|
||||||
|
|
||||||
def test_available_pairs(mocker, default_conf, ohlcv_history):
|
def test_available_pairs(mocker, default_conf, ohlcv_history):
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
ticker_interval = default_conf["ticker_interval"]
|
timeframe = default_conf["timeframe"]
|
||||||
exchange._klines[("XRP/BTC", ticker_interval)] = ohlcv_history
|
exchange._klines[("XRP/BTC", timeframe)] = ohlcv_history
|
||||||
exchange._klines[("UNITTEST/BTC", ticker_interval)] = ohlcv_history
|
exchange._klines[("UNITTEST/BTC", timeframe)] = ohlcv_history
|
||||||
|
|
||||||
dp = DataProvider(default_conf, exchange)
|
dp = DataProvider(default_conf, exchange)
|
||||||
assert len(dp.available_pairs) == 2
|
assert len(dp.available_pairs) == 2
|
||||||
assert dp.available_pairs == [("XRP/BTC", ticker_interval), ("UNITTEST/BTC", ticker_interval), ]
|
assert dp.available_pairs == [("XRP/BTC", timeframe), ("UNITTEST/BTC", timeframe), ]
|
||||||
|
|
||||||
|
|
||||||
def test_refresh(mocker, default_conf, ohlcv_history):
|
def test_refresh(mocker, default_conf, ohlcv_history):
|
||||||
@ -101,10 +101,10 @@ def test_refresh(mocker, default_conf, ohlcv_history):
|
|||||||
mocker.patch("freqtrade.exchange.Exchange.refresh_latest_ohlcv", refresh_mock)
|
mocker.patch("freqtrade.exchange.Exchange.refresh_latest_ohlcv", refresh_mock)
|
||||||
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, id="binance")
|
exchange = get_patched_exchange(mocker, default_conf, id="binance")
|
||||||
ticker_interval = default_conf["ticker_interval"]
|
timeframe = default_conf["timeframe"]
|
||||||
pairs = [("XRP/BTC", ticker_interval), ("UNITTEST/BTC", ticker_interval)]
|
pairs = [("XRP/BTC", timeframe), ("UNITTEST/BTC", timeframe)]
|
||||||
|
|
||||||
pairs_non_trad = [("ETH/USDT", ticker_interval), ("BTC/TUSD", "1h")]
|
pairs_non_trad = [("ETH/USDT", timeframe), ("BTC/TUSD", "1h")]
|
||||||
|
|
||||||
dp = DataProvider(default_conf, exchange)
|
dp = DataProvider(default_conf, exchange)
|
||||||
dp.refresh(pairs)
|
dp.refresh(pairs)
|
||||||
|
@ -354,7 +354,7 @@ def test_init(default_conf, mocker) -> None:
|
|||||||
assert {} == load_data(
|
assert {} == load_data(
|
||||||
datadir=Path(''),
|
datadir=Path(''),
|
||||||
pairs=[],
|
pairs=[],
|
||||||
timeframe=default_conf['ticker_interval']
|
timeframe=default_conf['timeframe']
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -363,13 +363,13 @@ def test_init_with_refresh(default_conf, mocker) -> None:
|
|||||||
refresh_data(
|
refresh_data(
|
||||||
datadir=Path(''),
|
datadir=Path(''),
|
||||||
pairs=[],
|
pairs=[],
|
||||||
timeframe=default_conf['ticker_interval'],
|
timeframe=default_conf['timeframe'],
|
||||||
exchange=exchange
|
exchange=exchange
|
||||||
)
|
)
|
||||||
assert {} == load_data(
|
assert {} == load_data(
|
||||||
datadir=Path(''),
|
datadir=Path(''),
|
||||||
pairs=[],
|
pairs=[],
|
||||||
timeframe=default_conf['ticker_interval']
|
timeframe=default_conf['timeframe']
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -557,6 +557,7 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad
|
|||||||
assert ght_mock.call_count == 1
|
assert ght_mock.call_count == 1
|
||||||
# Check this in seconds - since we had to convert to seconds above too.
|
# Check this in seconds - since we had to convert to seconds above too.
|
||||||
assert int(ght_mock.call_args_list[0][1]['since'] // 1000) == since_time2 - 5
|
assert int(ght_mock.call_args_list[0][1]['since'] // 1000) == since_time2 - 5
|
||||||
|
assert ght_mock.call_args_list[0][1]['from_id'] is not None
|
||||||
|
|
||||||
# clean files freshly downloaded
|
# clean files freshly downloaded
|
||||||
_clean_test_file(file1)
|
_clean_test_file(file1)
|
||||||
@ -568,6 +569,27 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad
|
|||||||
pair='ETH/BTC')
|
pair='ETH/BTC')
|
||||||
assert log_has_re('Failed to download historic trades for pair: "ETH/BTC".*', caplog)
|
assert log_has_re('Failed to download historic trades for pair: "ETH/BTC".*', caplog)
|
||||||
|
|
||||||
|
file2 = testdatadir / 'XRP_ETH-trades.json.gz'
|
||||||
|
|
||||||
|
_backup_file(file2, True)
|
||||||
|
|
||||||
|
ght_mock.reset_mock()
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_historic_trades',
|
||||||
|
ght_mock)
|
||||||
|
# Since before first start date
|
||||||
|
since_time = int(trades_history[0][0] // 1000) - 500
|
||||||
|
timerange = TimeRange('date', None, since_time, 0)
|
||||||
|
|
||||||
|
assert _download_trades_history(data_handler=data_handler, exchange=exchange,
|
||||||
|
pair='XRP/ETH', timerange=timerange)
|
||||||
|
|
||||||
|
assert ght_mock.call_count == 1
|
||||||
|
|
||||||
|
assert int(ght_mock.call_args_list[0][1]['since'] // 1000) == since_time
|
||||||
|
assert ght_mock.call_args_list[0][1]['from_id'] is None
|
||||||
|
assert log_has_re(r'Start earlier than available data. Redownloading trades for.*', caplog)
|
||||||
|
_clean_test_file(file2)
|
||||||
|
|
||||||
|
|
||||||
def test_convert_trades_to_ohlcv(mocker, default_conf, testdatadir, caplog):
|
def test_convert_trades_to_ohlcv(mocker, default_conf, testdatadir, caplog):
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe,
|
|||||||
####################################################################
|
####################################################################
|
||||||
|
|
||||||
tests_start_time = arrow.get(2018, 10, 3)
|
tests_start_time = arrow.get(2018, 10, 3)
|
||||||
ticker_interval_in_minute = 60
|
timeframe_in_minute = 60
|
||||||
_ohlc = {'date': 0, 'buy': 1, 'open': 2, 'high': 3, 'low': 4, 'close': 5, 'sell': 6, 'volume': 7}
|
_ohlc = {'date': 0, 'buy': 1, 'open': 2, 'high': 3, 'low': 4, 'close': 5, 'sell': 6, 'volume': 7}
|
||||||
|
|
||||||
# Helpers for this test file
|
# Helpers for this test file
|
||||||
@ -49,7 +49,7 @@ def _build_dataframe(buy_ohlc_sell_matrice):
|
|||||||
'date': tests_start_time.shift(
|
'date': tests_start_time.shift(
|
||||||
minutes=(
|
minutes=(
|
||||||
ohlc[0] *
|
ohlc[0] *
|
||||||
ticker_interval_in_minute)).timestamp *
|
timeframe_in_minute)).timestamp *
|
||||||
1000,
|
1000,
|
||||||
'buy': ohlc[1],
|
'buy': ohlc[1],
|
||||||
'open': ohlc[2],
|
'open': ohlc[2],
|
||||||
@ -70,7 +70,7 @@ def _build_dataframe(buy_ohlc_sell_matrice):
|
|||||||
|
|
||||||
def _time_on_candle(number):
|
def _time_on_candle(number):
|
||||||
return np.datetime64(tests_start_time.shift(
|
return np.datetime64(tests_start_time.shift(
|
||||||
minutes=(number * ticker_interval_in_minute)).timestamp * 1000, 'ms')
|
minutes=(number * timeframe_in_minute)).timestamp * 1000, 'ms')
|
||||||
|
|
||||||
|
|
||||||
# End helper functions
|
# End helper functions
|
||||||
@ -262,7 +262,7 @@ def mocked_load_data(datadir, pairs=[], timeframe='0m',
|
|||||||
|
|
||||||
NEOBTC = [
|
NEOBTC = [
|
||||||
[
|
[
|
||||||
tests_start_time.shift(minutes=(x * ticker_interval_in_minute)).timestamp * 1000,
|
tests_start_time.shift(minutes=(x * timeframe_in_minute)).timestamp * 1000,
|
||||||
math.sin(x * hz) / 1000 + base,
|
math.sin(x * hz) / 1000 + base,
|
||||||
math.sin(x * hz) / 1000 + base + 0.0001,
|
math.sin(x * hz) / 1000 + base + 0.0001,
|
||||||
math.sin(x * hz) / 1000 + base - 0.0001,
|
math.sin(x * hz) / 1000 + base - 0.0001,
|
||||||
@ -274,7 +274,7 @@ def mocked_load_data(datadir, pairs=[], timeframe='0m',
|
|||||||
base = 0.002
|
base = 0.002
|
||||||
LTCBTC = [
|
LTCBTC = [
|
||||||
[
|
[
|
||||||
tests_start_time.shift(minutes=(x * ticker_interval_in_minute)).timestamp * 1000,
|
tests_start_time.shift(minutes=(x * timeframe_in_minute)).timestamp * 1000,
|
||||||
math.sin(x * hz) / 1000 + base,
|
math.sin(x * hz) / 1000 + base,
|
||||||
math.sin(x * hz) / 1000 + base + 0.0001,
|
math.sin(x * hz) / 1000 + base + 0.0001,
|
||||||
math.sin(x * hz) / 1000 + base - 0.0001,
|
math.sin(x * hz) / 1000 + base - 0.0001,
|
||||||
|
@ -25,7 +25,7 @@ from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
|||||||
from tests.conftest import get_patched_exchange, log_has, log_has_re
|
from tests.conftest import get_patched_exchange, log_has, log_has_re
|
||||||
|
|
||||||
# Make sure to always keep one exchange here which is NOT subclassed!!
|
# Make sure to always keep one exchange here which is NOT subclassed!!
|
||||||
EXCHANGES = ['bittrex', 'binance', 'kraken', ]
|
EXCHANGES = ['bittrex', 'binance', 'kraken', 'ftx']
|
||||||
|
|
||||||
|
|
||||||
# Source: https://stackoverflow.com/questions/29881236/how-to-mock-asyncio-coroutines
|
# Source: https://stackoverflow.com/questions/29881236/how-to-mock-asyncio-coroutines
|
||||||
@ -88,15 +88,19 @@ def test_init_ccxt_kwargs(default_conf, mocker, caplog):
|
|||||||
caplog.clear()
|
caplog.clear()
|
||||||
conf = copy.deepcopy(default_conf)
|
conf = copy.deepcopy(default_conf)
|
||||||
conf['exchange']['ccxt_config'] = {'TestKWARG': 11}
|
conf['exchange']['ccxt_config'] = {'TestKWARG': 11}
|
||||||
|
conf['exchange']['ccxt_sync_config'] = {'TestKWARG44': 11}
|
||||||
conf['exchange']['ccxt_async_config'] = {'asyncio_loop': True}
|
conf['exchange']['ccxt_async_config'] = {'asyncio_loop': True}
|
||||||
|
asynclogmsg = "Applying additional ccxt config: {'TestKWARG': 11, 'asyncio_loop': True}"
|
||||||
ex = Exchange(conf)
|
ex = Exchange(conf)
|
||||||
assert not log_has("Applying additional ccxt config: {'aiohttp_trust_env': True}", caplog)
|
|
||||||
assert not ex._api_async.aiohttp_trust_env
|
assert not ex._api_async.aiohttp_trust_env
|
||||||
assert hasattr(ex._api, 'TestKWARG')
|
assert hasattr(ex._api, 'TestKWARG')
|
||||||
assert ex._api.TestKWARG == 11
|
assert ex._api.TestKWARG == 11
|
||||||
assert not hasattr(ex._api_async, 'TestKWARG')
|
# ccxt_config is assigned to both sync and async
|
||||||
assert log_has("Applying additional ccxt config: {'TestKWARG': 11}", caplog)
|
assert not hasattr(ex._api_async, 'TestKWARG44')
|
||||||
|
|
||||||
|
assert hasattr(ex._api_async, 'TestKWARG')
|
||||||
|
assert log_has("Applying additional ccxt config: {'TestKWARG': 11, 'TestKWARG44': 11}", caplog)
|
||||||
|
assert log_has(asynclogmsg, caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_destroy(default_conf, mocker, caplog):
|
def test_destroy(default_conf, mocker, caplog):
|
||||||
@ -315,7 +319,12 @@ def test_set_sandbox_exception(default_conf, mocker):
|
|||||||
|
|
||||||
|
|
||||||
def test__load_async_markets(default_conf, mocker, caplog):
|
def test__load_async_markets(default_conf, mocker, caplog):
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt')
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._load_markets')
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
||||||
|
exchange = Exchange(default_conf)
|
||||||
exchange._api_async.load_markets = get_mock_coro(None)
|
exchange._api_async.load_markets = get_mock_coro(None)
|
||||||
exchange._load_async_markets()
|
exchange._load_async_markets()
|
||||||
assert exchange._api_async.load_markets.call_count == 1
|
assert exchange._api_async.load_markets.call_count == 1
|
||||||
@ -348,7 +357,7 @@ def test__load_markets(default_conf, mocker, caplog):
|
|||||||
assert ex.markets == expected_return
|
assert ex.markets == expected_return
|
||||||
|
|
||||||
|
|
||||||
def test__reload_markets(default_conf, mocker, caplog):
|
def test_reload_markets(default_conf, mocker, caplog):
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
initial_markets = {'ETH/BTC': {}}
|
initial_markets = {'ETH/BTC': {}}
|
||||||
|
|
||||||
@ -361,23 +370,26 @@ def test__reload_markets(default_conf, mocker, caplog):
|
|||||||
default_conf['exchange']['markets_refresh_interval'] = 10
|
default_conf['exchange']['markets_refresh_interval'] = 10
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance",
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance",
|
||||||
mock_markets=False)
|
mock_markets=False)
|
||||||
|
exchange._load_async_markets = MagicMock()
|
||||||
exchange._last_markets_refresh = arrow.utcnow().timestamp
|
exchange._last_markets_refresh = arrow.utcnow().timestamp
|
||||||
updated_markets = {'ETH/BTC': {}, "LTC/BTC": {}}
|
updated_markets = {'ETH/BTC': {}, "LTC/BTC": {}}
|
||||||
|
|
||||||
assert exchange.markets == initial_markets
|
assert exchange.markets == initial_markets
|
||||||
|
|
||||||
# less than 10 minutes have passed, no reload
|
# less than 10 minutes have passed, no reload
|
||||||
exchange._reload_markets()
|
exchange.reload_markets()
|
||||||
assert exchange.markets == initial_markets
|
assert exchange.markets == initial_markets
|
||||||
|
assert exchange._load_async_markets.call_count == 0
|
||||||
|
|
||||||
# more than 10 minutes have passed, reload is executed
|
# more than 10 minutes have passed, reload is executed
|
||||||
exchange._last_markets_refresh = arrow.utcnow().timestamp - 15 * 60
|
exchange._last_markets_refresh = arrow.utcnow().timestamp - 15 * 60
|
||||||
exchange._reload_markets()
|
exchange.reload_markets()
|
||||||
assert exchange.markets == updated_markets
|
assert exchange.markets == updated_markets
|
||||||
|
assert exchange._load_async_markets.call_count == 1
|
||||||
assert log_has('Performing scheduled market reload..', caplog)
|
assert log_has('Performing scheduled market reload..', caplog)
|
||||||
|
|
||||||
|
|
||||||
def test__reload_markets_exception(default_conf, mocker, caplog):
|
def test_reload_markets_exception(default_conf, mocker, caplog):
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
|
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
@ -386,7 +398,7 @@ def test__reload_markets_exception(default_conf, mocker, caplog):
|
|||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance")
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance")
|
||||||
|
|
||||||
# less than 10 minutes have passed, no reload
|
# less than 10 minutes have passed, no reload
|
||||||
exchange._reload_markets()
|
exchange.reload_markets()
|
||||||
assert exchange._last_markets_refresh == 0
|
assert exchange._last_markets_refresh == 0
|
||||||
assert log_has_re(r"Could not reload markets.*", caplog)
|
assert log_has_re(r"Could not reload markets.*", caplog)
|
||||||
|
|
||||||
@ -574,7 +586,7 @@ def test_validate_pairs_stakecompatibility_fail(default_conf, mocker, caplog):
|
|||||||
('5m'), ("1m"), ("15m"), ("1h")
|
('5m'), ("1m"), ("15m"), ("1h")
|
||||||
])
|
])
|
||||||
def test_validate_timeframes(default_conf, mocker, timeframe):
|
def test_validate_timeframes(default_conf, mocker, timeframe):
|
||||||
default_conf["ticker_interval"] = timeframe
|
default_conf["timeframe"] = timeframe
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
id_mock = PropertyMock(return_value='test_exchange')
|
id_mock = PropertyMock(return_value='test_exchange')
|
||||||
type(api_mock).id = id_mock
|
type(api_mock).id = id_mock
|
||||||
@ -592,7 +604,7 @@ def test_validate_timeframes(default_conf, mocker, timeframe):
|
|||||||
|
|
||||||
|
|
||||||
def test_validate_timeframes_failed(default_conf, mocker):
|
def test_validate_timeframes_failed(default_conf, mocker):
|
||||||
default_conf["ticker_interval"] = "3m"
|
default_conf["timeframe"] = "3m"
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
id_mock = PropertyMock(return_value='test_exchange')
|
id_mock = PropertyMock(return_value='test_exchange')
|
||||||
type(api_mock).id = id_mock
|
type(api_mock).id = id_mock
|
||||||
@ -609,7 +621,7 @@ def test_validate_timeframes_failed(default_conf, mocker):
|
|||||||
with pytest.raises(OperationalException,
|
with pytest.raises(OperationalException,
|
||||||
match=r"Invalid timeframe '3m'. This exchange supports.*"):
|
match=r"Invalid timeframe '3m'. This exchange supports.*"):
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
default_conf["ticker_interval"] = "15s"
|
default_conf["timeframe"] = "15s"
|
||||||
|
|
||||||
with pytest.raises(OperationalException,
|
with pytest.raises(OperationalException,
|
||||||
match=r"Timeframes < 1m are currently not supported by Freqtrade."):
|
match=r"Timeframes < 1m are currently not supported by Freqtrade."):
|
||||||
@ -617,7 +629,7 @@ def test_validate_timeframes_failed(default_conf, mocker):
|
|||||||
|
|
||||||
|
|
||||||
def test_validate_timeframes_emulated_ohlcv_1(default_conf, mocker):
|
def test_validate_timeframes_emulated_ohlcv_1(default_conf, mocker):
|
||||||
default_conf["ticker_interval"] = "3m"
|
default_conf["timeframe"] = "3m"
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
id_mock = PropertyMock(return_value='test_exchange')
|
id_mock = PropertyMock(return_value='test_exchange')
|
||||||
type(api_mock).id = id_mock
|
type(api_mock).id = id_mock
|
||||||
@ -637,7 +649,7 @@ def test_validate_timeframes_emulated_ohlcv_1(default_conf, mocker):
|
|||||||
|
|
||||||
|
|
||||||
def test_validate_timeframes_emulated_ohlcvi_2(default_conf, mocker):
|
def test_validate_timeframes_emulated_ohlcvi_2(default_conf, mocker):
|
||||||
default_conf["ticker_interval"] = "3m"
|
default_conf["timeframe"] = "3m"
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
id_mock = PropertyMock(return_value='test_exchange')
|
id_mock = PropertyMock(return_value='test_exchange')
|
||||||
type(api_mock).id = id_mock
|
type(api_mock).id = id_mock
|
||||||
@ -658,7 +670,7 @@ def test_validate_timeframes_emulated_ohlcvi_2(default_conf, mocker):
|
|||||||
|
|
||||||
|
|
||||||
def test_validate_timeframes_not_in_config(default_conf, mocker):
|
def test_validate_timeframes_not_in_config(default_conf, mocker):
|
||||||
del default_conf["ticker_interval"]
|
del default_conf["timeframe"]
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
id_mock = PropertyMock(return_value='test_exchange')
|
id_mock = PropertyMock(return_value='test_exchange')
|
||||||
type(api_mock).id = id_mock
|
type(api_mock).id = id_mock
|
||||||
@ -1254,7 +1266,8 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name):
|
|||||||
|
|
||||||
exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
|
exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
|
||||||
# one_call calculation * 1.8 should do 2 calls
|
# one_call calculation * 1.8 should do 2 calls
|
||||||
since = 5 * 60 * 500 * 1.8
|
|
||||||
|
since = 5 * 60 * exchange._ft_has['ohlcv_candle_limit'] * 1.8
|
||||||
ret = exchange.get_historic_ohlcv(pair, "5m", int((arrow.utcnow().timestamp - since) * 1000))
|
ret = exchange.get_historic_ohlcv(pair, "5m", int((arrow.utcnow().timestamp - since) * 1000))
|
||||||
|
|
||||||
assert exchange._async_get_candle_history.call_count == 2
|
assert exchange._async_get_candle_history.call_count == 2
|
||||||
@ -1346,7 +1359,7 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_
|
|||||||
# exchange = Exchange(default_conf)
|
# exchange = Exchange(default_conf)
|
||||||
await async_ccxt_exception(mocker, default_conf, MagicMock(),
|
await async_ccxt_exception(mocker, default_conf, MagicMock(),
|
||||||
"_async_get_candle_history", "fetch_ohlcv",
|
"_async_get_candle_history", "fetch_ohlcv",
|
||||||
pair='ABCD/BTC', timeframe=default_conf['ticker_interval'])
|
pair='ABCD/BTC', timeframe=default_conf['timeframe'])
|
||||||
|
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
with pytest.raises(OperationalException,
|
with pytest.raises(OperationalException,
|
||||||
@ -1476,7 +1489,7 @@ async def test___async_get_candle_history_sort(default_conf, mocker, exchange_na
|
|||||||
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
|
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
|
||||||
sort_mock = mocker.patch('freqtrade.exchange.exchange.sorted', MagicMock(side_effect=sort_data))
|
sort_mock = mocker.patch('freqtrade.exchange.exchange.sorted', MagicMock(side_effect=sort_data))
|
||||||
# Test the OHLCV data sort
|
# Test the OHLCV data sort
|
||||||
res = await exchange._async_get_candle_history('ETH/BTC', default_conf['ticker_interval'])
|
res = await exchange._async_get_candle_history('ETH/BTC', default_conf['timeframe'])
|
||||||
assert res[0] == 'ETH/BTC'
|
assert res[0] == 'ETH/BTC'
|
||||||
res_ohlcv = res[2]
|
res_ohlcv = res[2]
|
||||||
|
|
||||||
@ -1513,9 +1526,9 @@ async def test___async_get_candle_history_sort(default_conf, mocker, exchange_na
|
|||||||
# Reset sort mock
|
# Reset sort mock
|
||||||
sort_mock = mocker.patch('freqtrade.exchange.sorted', MagicMock(side_effect=sort_data))
|
sort_mock = mocker.patch('freqtrade.exchange.sorted', MagicMock(side_effect=sort_data))
|
||||||
# Test the OHLCV data sort
|
# Test the OHLCV data sort
|
||||||
res = await exchange._async_get_candle_history('ETH/BTC', default_conf['ticker_interval'])
|
res = await exchange._async_get_candle_history('ETH/BTC', default_conf['timeframe'])
|
||||||
assert res[0] == 'ETH/BTC'
|
assert res[0] == 'ETH/BTC'
|
||||||
assert res[1] == default_conf['ticker_interval']
|
assert res[1] == default_conf['timeframe']
|
||||||
res_ohlcv = res[2]
|
res_ohlcv = res[2]
|
||||||
# Sorted not called again - data is already in order
|
# Sorted not called again - data is already in order
|
||||||
assert sort_mock.call_count == 0
|
assert sort_mock.call_count == 0
|
||||||
@ -1729,6 +1742,7 @@ def test_cancel_order_dry_run(default_conf, mocker, exchange_name):
|
|||||||
default_conf['dry_run'] = True
|
default_conf['dry_run'] = True
|
||||||
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||||
assert exchange.cancel_order(order_id='123', pair='TKN/BTC') == {}
|
assert exchange.cancel_order(order_id='123', pair='TKN/BTC') == {}
|
||||||
|
assert exchange.cancel_stoploss_order(order_id='123', pair='TKN/BTC') == {}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
@ -1813,6 +1827,25 @@ def test_cancel_order(default_conf, mocker, exchange_name):
|
|||||||
order_id='_', pair='TKN/BTC')
|
order_id='_', pair='TKN/BTC')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
|
def test_cancel_stoploss_order(default_conf, mocker, exchange_name):
|
||||||
|
default_conf['dry_run'] = False
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.cancel_order = MagicMock(return_value=123)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
|
assert exchange.cancel_stoploss_order(order_id='_', pair='TKN/BTC') == 123
|
||||||
|
|
||||||
|
with pytest.raises(InvalidOrderException):
|
||||||
|
api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order"))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
|
exchange.cancel_stoploss_order(order_id='_', pair='TKN/BTC')
|
||||||
|
assert api_mock.cancel_order.call_count == 1
|
||||||
|
|
||||||
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
|
||||||
|
"cancel_stoploss_order", "cancel_order",
|
||||||
|
order_id='_', pair='TKN/BTC')
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
def test_get_order(default_conf, mocker, exchange_name):
|
def test_get_order(default_conf, mocker, exchange_name):
|
||||||
default_conf['dry_run'] = True
|
default_conf['dry_run'] = True
|
||||||
@ -1842,6 +1875,38 @@ def test_get_order(default_conf, mocker, exchange_name):
|
|||||||
order_id='_', pair='TKN/BTC')
|
order_id='_', pair='TKN/BTC')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
|
def test_get_stoploss_order(default_conf, mocker, exchange_name):
|
||||||
|
# Don't test FTX here - that needs a seperate test
|
||||||
|
if exchange_name == 'ftx':
|
||||||
|
return
|
||||||
|
default_conf['dry_run'] = True
|
||||||
|
order = MagicMock()
|
||||||
|
order.myid = 123
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||||
|
exchange._dry_run_open_orders['X'] = order
|
||||||
|
assert exchange.get_stoploss_order('X', 'TKN/BTC').myid == 123
|
||||||
|
|
||||||
|
with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'):
|
||||||
|
exchange.get_stoploss_order('Y', 'TKN/BTC')
|
||||||
|
|
||||||
|
default_conf['dry_run'] = False
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.fetch_order = MagicMock(return_value=456)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
|
assert exchange.get_stoploss_order('X', 'TKN/BTC') == 456
|
||||||
|
|
||||||
|
with pytest.raises(InvalidOrderException):
|
||||||
|
api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
|
exchange.get_stoploss_order(order_id='_', pair='TKN/BTC')
|
||||||
|
assert api_mock.fetch_order.call_count == 1
|
||||||
|
|
||||||
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
|
||||||
|
'get_stoploss_order', 'fetch_order',
|
||||||
|
order_id='_', pair='TKN/BTC')
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
def test_name(default_conf, mocker, exchange_name):
|
def test_name(default_conf, mocker, exchange_name):
|
||||||
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||||
@ -2188,12 +2253,18 @@ def test_extract_cost_curr_rate(mocker, default_conf, order, expected) -> None:
|
|||||||
'fee': {'currency': 'NEO', 'cost': 0.0012}}, 0.001944),
|
'fee': {'currency': 'NEO', 'cost': 0.0012}}, 0.001944),
|
||||||
({'symbol': 'ETH/BTC', 'amount': 2.21, 'cost': 0.02992561,
|
({'symbol': 'ETH/BTC', 'amount': 2.21, 'cost': 0.02992561,
|
||||||
'fee': {'currency': 'NEO', 'cost': 0.00027452}}, 0.00074305),
|
'fee': {'currency': 'NEO', 'cost': 0.00027452}}, 0.00074305),
|
||||||
# TODO: More tests here!
|
|
||||||
# Rate included in return - return as is
|
# Rate included in return - return as is
|
||||||
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
|
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
|
||||||
'fee': {'currency': 'USDT', 'cost': 0.34, 'rate': 0.01}}, 0.01),
|
'fee': {'currency': 'USDT', 'cost': 0.34, 'rate': 0.01}}, 0.01),
|
||||||
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
|
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
|
||||||
'fee': {'currency': 'USDT', 'cost': 0.34, 'rate': 0.005}}, 0.005),
|
'fee': {'currency': 'USDT', 'cost': 0.34, 'rate': 0.005}}, 0.005),
|
||||||
|
# 0.1% filled - no costs (kraken - #3431)
|
||||||
|
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.0,
|
||||||
|
'fee': {'currency': 'BTC', 'cost': 0.0, 'rate': None}}, None),
|
||||||
|
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.0,
|
||||||
|
'fee': {'currency': 'ETH', 'cost': 0.0, 'rate': None}}, 0.0),
|
||||||
|
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.0,
|
||||||
|
'fee': {'currency': 'NEO', 'cost': 0.0, 'rate': None}}, None),
|
||||||
])
|
])
|
||||||
def test_calculate_fee_rate(mocker, default_conf, order, expected) -> None:
|
def test_calculate_fee_rate(mocker, default_conf, order, expected) -> None:
|
||||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value={'last': 0.081})
|
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value={'last': 0.081})
|
||||||
|
163
tests/exchange/test_ftx.py
Normal file
163
tests/exchange/test_ftx.py
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement
|
||||||
|
# pragma pylint: disable=protected-access
|
||||||
|
from random import randint
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import ccxt
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from freqtrade.exceptions import (DependencyException, InvalidOrderException,
|
||||||
|
OperationalException, TemporaryError)
|
||||||
|
from tests.conftest import get_patched_exchange
|
||||||
|
from .test_exchange import ccxt_exceptionhandlers
|
||||||
|
|
||||||
|
STOPLOSS_ORDERTYPE = 'stop'
|
||||||
|
|
||||||
|
|
||||||
|
def test_stoploss_order_ftx(default_conf, mocker):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||||
|
|
||||||
|
api_mock.create_order = MagicMock(return_value={
|
||||||
|
'id': order_id,
|
||||||
|
'info': {
|
||||||
|
'foo': 'bar'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
default_conf['dry_run'] = False
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||||
|
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
|
||||||
|
|
||||||
|
# stoploss_on_exchange_limit_ratio is irrelevant for ftx market orders
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
||||||
|
order_types={'stoploss_on_exchange_limit_ratio': 1.05})
|
||||||
|
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['price'] == 190
|
||||||
|
assert 'orderPrice' not in api_mock.create_order.call_args_list[0][1]['params']
|
||||||
|
|
||||||
|
assert api_mock.create_order.call_count == 1
|
||||||
|
|
||||||
|
api_mock.create_order.reset_mock()
|
||||||
|
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
assert 'id' in order
|
||||||
|
assert 'info' in order
|
||||||
|
assert order['id'] == order_id
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['price'] == 220
|
||||||
|
assert 'orderPrice' not in api_mock.create_order.call_args_list[0][1]['params']
|
||||||
|
|
||||||
|
api_mock.create_order.reset_mock()
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220,
|
||||||
|
order_types={'stoploss': 'limit'})
|
||||||
|
|
||||||
|
assert 'id' in order
|
||||||
|
assert 'info' in order
|
||||||
|
assert order['id'] == order_id
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['price'] == 220
|
||||||
|
assert 'orderPrice' in api_mock.create_order.call_args_list[0][1]['params']
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['params']['orderPrice'] == 217.8
|
||||||
|
|
||||||
|
# test exception handling
|
||||||
|
with pytest.raises(DependencyException):
|
||||||
|
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
|
||||||
|
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
with pytest.raises(InvalidOrderException):
|
||||||
|
api_mock.create_order = MagicMock(
|
||||||
|
side_effect=ccxt.InvalidOrder("ftx Order would trigger immediately."))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
|
||||||
|
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
with pytest.raises(TemporaryError):
|
||||||
|
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection"))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
|
||||||
|
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException, match=r".*DeadBeef.*"):
|
||||||
|
api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef"))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
|
||||||
|
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
|
||||||
|
def test_stoploss_order_dry_run_ftx(default_conf, mocker):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
default_conf['dry_run'] = True
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||||
|
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
|
||||||
|
|
||||||
|
api_mock.create_order.reset_mock()
|
||||||
|
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
assert 'id' in order
|
||||||
|
assert 'info' in order
|
||||||
|
assert 'type' in order
|
||||||
|
|
||||||
|
assert order['type'] == STOPLOSS_ORDERTYPE
|
||||||
|
assert order['price'] == 220
|
||||||
|
assert order['amount'] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_stoploss_adjust_ftx(mocker, default_conf):
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id='ftx')
|
||||||
|
order = {
|
||||||
|
'type': STOPLOSS_ORDERTYPE,
|
||||||
|
'price': 1500,
|
||||||
|
}
|
||||||
|
assert exchange.stoploss_adjust(1501, order)
|
||||||
|
assert not exchange.stoploss_adjust(1499, order)
|
||||||
|
# Test with invalid order case ...
|
||||||
|
order['type'] = 'stop_loss_limit'
|
||||||
|
assert not exchange.stoploss_adjust(1501, order)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_stoploss_order(default_conf, mocker):
|
||||||
|
default_conf['dry_run'] = True
|
||||||
|
order = MagicMock()
|
||||||
|
order.myid = 123
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id='ftx')
|
||||||
|
exchange._dry_run_open_orders['X'] = order
|
||||||
|
assert exchange.get_stoploss_order('X', 'TKN/BTC').myid == 123
|
||||||
|
|
||||||
|
with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'):
|
||||||
|
exchange.get_stoploss_order('Y', 'TKN/BTC')
|
||||||
|
|
||||||
|
default_conf['dry_run'] = False
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.fetch_orders = MagicMock(return_value=[{'id': 'X', 'status': '456'}])
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx')
|
||||||
|
assert exchange.get_stoploss_order('X', 'TKN/BTC')['status'] == '456'
|
||||||
|
|
||||||
|
api_mock.fetch_orders = MagicMock(return_value=[{'id': 'Y', 'status': '456'}])
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx')
|
||||||
|
with pytest.raises(InvalidOrderException, match=r"Could not get stoploss order for id X"):
|
||||||
|
exchange.get_stoploss_order('X', 'TKN/BTC')['status']
|
||||||
|
|
||||||
|
with pytest.raises(InvalidOrderException):
|
||||||
|
api_mock.fetch_orders = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx')
|
||||||
|
exchange.get_stoploss_order(order_id='_', pair='TKN/BTC')
|
||||||
|
assert api_mock.fetch_orders.call_count == 1
|
||||||
|
|
||||||
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'ftx',
|
||||||
|
'get_stoploss_order', 'fetch_orders',
|
||||||
|
order_id='_', pair='TKN/BTC')
|
@ -11,6 +11,8 @@ from freqtrade.exceptions import (DependencyException, InvalidOrderException,
|
|||||||
from tests.conftest import get_patched_exchange
|
from tests.conftest import get_patched_exchange
|
||||||
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||||
|
|
||||||
|
STOPLOSS_ORDERTYPE = 'stop-loss'
|
||||||
|
|
||||||
|
|
||||||
def test_buy_kraken_trading_agreement(default_conf, mocker):
|
def test_buy_kraken_trading_agreement(default_conf, mocker):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
@ -159,7 +161,6 @@ def test_get_balances_prod(default_conf, mocker):
|
|||||||
def test_stoploss_order_kraken(default_conf, mocker):
|
def test_stoploss_order_kraken(default_conf, mocker):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||||
order_type = 'stop-loss'
|
|
||||||
|
|
||||||
api_mock.create_order = MagicMock(return_value={
|
api_mock.create_order = MagicMock(return_value={
|
||||||
'id': order_id,
|
'id': order_id,
|
||||||
@ -187,7 +188,7 @@ def test_stoploss_order_kraken(default_conf, mocker):
|
|||||||
assert 'info' in order
|
assert 'info' in order
|
||||||
assert order['id'] == order_id
|
assert order['id'] == order_id
|
||||||
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
|
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
|
||||||
assert api_mock.create_order.call_args_list[0][1]['type'] == order_type
|
assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE
|
||||||
assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
|
assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
|
||||||
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
|
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
|
||||||
assert api_mock.create_order.call_args_list[0][1]['price'] == 220
|
assert api_mock.create_order.call_args_list[0][1]['price'] == 220
|
||||||
@ -218,7 +219,6 @@ def test_stoploss_order_kraken(default_conf, mocker):
|
|||||||
|
|
||||||
def test_stoploss_order_dry_run_kraken(default_conf, mocker):
|
def test_stoploss_order_dry_run_kraken(default_conf, mocker):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
order_type = 'stop-loss'
|
|
||||||
default_conf['dry_run'] = True
|
default_conf['dry_run'] = True
|
||||||
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||||
@ -233,7 +233,7 @@ def test_stoploss_order_dry_run_kraken(default_conf, mocker):
|
|||||||
assert 'info' in order
|
assert 'info' in order
|
||||||
assert 'type' in order
|
assert 'type' in order
|
||||||
|
|
||||||
assert order['type'] == order_type
|
assert order['type'] == STOPLOSS_ORDERTYPE
|
||||||
assert order['price'] == 220
|
assert order['price'] == 220
|
||||||
assert order['amount'] == 1
|
assert order['amount'] == 1
|
||||||
|
|
||||||
@ -241,7 +241,7 @@ def test_stoploss_order_dry_run_kraken(default_conf, mocker):
|
|||||||
def test_stoploss_adjust_kraken(mocker, default_conf):
|
def test_stoploss_adjust_kraken(mocker, default_conf):
|
||||||
exchange = get_patched_exchange(mocker, default_conf, id='kraken')
|
exchange = get_patched_exchange(mocker, default_conf, id='kraken')
|
||||||
order = {
|
order = {
|
||||||
'type': 'stop-loss',
|
'type': STOPLOSS_ORDERTYPE,
|
||||||
'price': 1500,
|
'price': 1500,
|
||||||
}
|
}
|
||||||
assert exchange.stoploss_adjust(1501, order)
|
assert exchange.stoploss_adjust(1501, order)
|
||||||
|
@ -360,7 +360,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
|
|||||||
"""
|
"""
|
||||||
default_conf["stoploss"] = data.stop_loss
|
default_conf["stoploss"] = data.stop_loss
|
||||||
default_conf["minimal_roi"] = data.roi
|
default_conf["minimal_roi"] = data.roi
|
||||||
default_conf["ticker_interval"] = tests_timeframe
|
default_conf["timeframe"] = tests_timeframe
|
||||||
default_conf["trailing_stop"] = data.trailing_stop
|
default_conf["trailing_stop"] = data.trailing_stop
|
||||||
default_conf["trailing_only_offset_is_reached"] = data.trailing_only_offset_is_reached
|
default_conf["trailing_only_offset_is_reached"] = data.trailing_only_offset_is_reached
|
||||||
# Only add this to configuration If it's necessary
|
# Only add this to configuration If it's necessary
|
||||||
|
@ -81,7 +81,7 @@ def load_data_test(what, testdatadir):
|
|||||||
|
|
||||||
def simple_backtest(config, contour, num_results, mocker, testdatadir) -> None:
|
def simple_backtest(config, contour, num_results, mocker, testdatadir) -> None:
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
config['ticker_interval'] = '1m'
|
config['timeframe'] = '1m'
|
||||||
backtesting = Backtesting(config)
|
backtesting = Backtesting(config)
|
||||||
|
|
||||||
data = load_data_test(contour, testdatadir)
|
data = load_data_test(contour, testdatadir)
|
||||||
@ -165,7 +165,7 @@ def test_setup_optimize_configuration_without_arguments(mocker, default_conf, ca
|
|||||||
assert 'pair_whitelist' in config['exchange']
|
assert 'pair_whitelist' in config['exchange']
|
||||||
assert 'datadir' in config
|
assert 'datadir' in config
|
||||||
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
||||||
assert 'ticker_interval' in config
|
assert 'timeframe' in config
|
||||||
assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog)
|
assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog)
|
||||||
|
|
||||||
assert 'position_stacking' not in config
|
assert 'position_stacking' not in config
|
||||||
@ -189,7 +189,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) ->
|
|||||||
'--config', 'config.json',
|
'--config', 'config.json',
|
||||||
'--strategy', 'DefaultStrategy',
|
'--strategy', 'DefaultStrategy',
|
||||||
'--datadir', '/foo/bar',
|
'--datadir', '/foo/bar',
|
||||||
'--ticker-interval', '1m',
|
'--timeframe', '1m',
|
||||||
'--enable-position-stacking',
|
'--enable-position-stacking',
|
||||||
'--disable-max-market-positions',
|
'--disable-max-market-positions',
|
||||||
'--timerange', ':100',
|
'--timerange', ':100',
|
||||||
@ -208,8 +208,8 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) ->
|
|||||||
assert config['runmode'] == RunMode.BACKTEST
|
assert config['runmode'] == RunMode.BACKTEST
|
||||||
|
|
||||||
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
||||||
assert 'ticker_interval' in config
|
assert 'timeframe' in config
|
||||||
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
assert log_has('Parameter -i/--timeframe detected ... Using timeframe: 1m ...',
|
||||||
caplog)
|
caplog)
|
||||||
|
|
||||||
assert 'position_stacking' in config
|
assert 'position_stacking' in config
|
||||||
@ -286,9 +286,9 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None:
|
|||||||
assert not backtesting.strategy.order_types["stoploss_on_exchange"]
|
assert not backtesting.strategy.order_types["stoploss_on_exchange"]
|
||||||
|
|
||||||
|
|
||||||
def test_backtesting_init_no_ticker_interval(mocker, default_conf, caplog) -> None:
|
def test_backtesting_init_no_timeframe(mocker, default_conf, caplog) -> None:
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
del default_conf['ticker_interval']
|
del default_conf['timeframe']
|
||||||
default_conf['strategy_list'] = ['DefaultStrategy',
|
default_conf['strategy_list'] = ['DefaultStrategy',
|
||||||
'SampleStrategy']
|
'SampleStrategy']
|
||||||
|
|
||||||
@ -333,11 +333,12 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None:
|
|||||||
mocker.patch('freqtrade.data.history.get_timerange', get_timerange)
|
mocker.patch('freqtrade.data.history.get_timerange', get_timerange)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest')
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest')
|
||||||
|
mocker.patch('freqtrade.optimize.backtesting.generate_backtest_stats')
|
||||||
mocker.patch('freqtrade.optimize.backtesting.show_backtest_results')
|
mocker.patch('freqtrade.optimize.backtesting.show_backtest_results')
|
||||||
mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist',
|
mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist',
|
||||||
PropertyMock(return_value=['UNITTEST/BTC']))
|
PropertyMock(return_value=['UNITTEST/BTC']))
|
||||||
|
|
||||||
default_conf['ticker_interval'] = '1m'
|
default_conf['timeframe'] = '1m'
|
||||||
default_conf['datadir'] = testdatadir
|
default_conf['datadir'] = testdatadir
|
||||||
default_conf['export'] = None
|
default_conf['export'] = None
|
||||||
default_conf['timerange'] = '-1510694220'
|
default_conf['timerange'] = '-1510694220'
|
||||||
@ -367,7 +368,7 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) ->
|
|||||||
mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist',
|
mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist',
|
||||||
PropertyMock(return_value=['UNITTEST/BTC']))
|
PropertyMock(return_value=['UNITTEST/BTC']))
|
||||||
|
|
||||||
default_conf['ticker_interval'] = "1m"
|
default_conf['timeframe'] = "1m"
|
||||||
default_conf['datadir'] = testdatadir
|
default_conf['datadir'] = testdatadir
|
||||||
default_conf['export'] = None
|
default_conf['export'] = None
|
||||||
default_conf['timerange'] = '20180101-20180102'
|
default_conf['timerange'] = '20180101-20180102'
|
||||||
@ -387,7 +388,7 @@ def test_backtesting_no_pair_left(default_conf, mocker, caplog, testdatadir) ->
|
|||||||
mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist',
|
mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist',
|
||||||
PropertyMock(return_value=[]))
|
PropertyMock(return_value=[]))
|
||||||
|
|
||||||
default_conf['ticker_interval'] = "1m"
|
default_conf['timeframe'] = "1m"
|
||||||
default_conf['datadir'] = testdatadir
|
default_conf['datadir'] = testdatadir
|
||||||
default_conf['export'] = None
|
default_conf['export'] = None
|
||||||
default_conf['timerange'] = '20180101-20180102'
|
default_conf['timerange'] = '20180101-20180102'
|
||||||
@ -400,6 +401,38 @@ def test_backtesting_no_pair_left(default_conf, mocker, caplog, testdatadir) ->
|
|||||||
Backtesting(default_conf)
|
Backtesting(default_conf)
|
||||||
|
|
||||||
|
|
||||||
|
def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, tickers) -> None:
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||||
|
mocker.patch('freqtrade.data.history.get_timerange', get_timerange)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest')
|
||||||
|
mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist',
|
||||||
|
PropertyMock(return_value=['XRP/BTC']))
|
||||||
|
mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.refresh_pairlist')
|
||||||
|
|
||||||
|
default_conf['ticker_interval'] = "1m"
|
||||||
|
default_conf['datadir'] = testdatadir
|
||||||
|
default_conf['export'] = None
|
||||||
|
# Use stoploss from strategy
|
||||||
|
del default_conf['stoploss']
|
||||||
|
default_conf['timerange'] = '20180101-20180102'
|
||||||
|
|
||||||
|
default_conf['pairlists'] = [{"method": "VolumePairList", "number_assets": 5}]
|
||||||
|
with pytest.raises(OperationalException, match='VolumePairList not allowed for backtesting.'):
|
||||||
|
Backtesting(default_conf)
|
||||||
|
|
||||||
|
default_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PrecisionFilter"}, ]
|
||||||
|
Backtesting(default_conf)
|
||||||
|
|
||||||
|
# Multiple strategies
|
||||||
|
default_conf['strategy_list'] = ['DefaultStrategy', 'TestStrategyLegacy']
|
||||||
|
with pytest.raises(OperationalException,
|
||||||
|
match='PrecisionFilter not allowed for backtesting multiple strategies.'):
|
||||||
|
Backtesting(default_conf)
|
||||||
|
|
||||||
|
|
||||||
def test_backtest(default_conf, fee, mocker, testdatadir) -> None:
|
def test_backtest(default_conf, fee, mocker, testdatadir) -> None:
|
||||||
default_conf['ask_strategy']['use_sell_signal'] = False
|
default_conf['ask_strategy']['use_sell_signal'] = False
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
@ -453,7 +486,7 @@ def test_backtest(default_conf, fee, mocker, testdatadir) -> None:
|
|||||||
t["close_rate"], 6) < round(ln.iloc[0]["high"], 6))
|
t["close_rate"], 6) < round(ln.iloc[0]["high"], 6))
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_1min_ticker_interval(default_conf, fee, mocker, testdatadir) -> None:
|
def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None:
|
||||||
default_conf['ask_strategy']['use_sell_signal'] = False
|
default_conf['ask_strategy']['use_sell_signal'] = False
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
@ -534,7 +567,7 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir):
|
|||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
backtest_conf = _make_backtest_conf(mocker, conf=default_conf,
|
backtest_conf = _make_backtest_conf(mocker, conf=default_conf,
|
||||||
pair='UNITTEST/BTC', datadir=testdatadir)
|
pair='UNITTEST/BTC', datadir=testdatadir)
|
||||||
default_conf['ticker_interval'] = '1m'
|
default_conf['timeframe'] = '1m'
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
backtesting.strategy.advise_buy = _trend_alternate # Override
|
backtesting.strategy.advise_buy = _trend_alternate # Override
|
||||||
backtesting.strategy.advise_sell = _trend_alternate # Override
|
backtesting.strategy.advise_sell = _trend_alternate # Override
|
||||||
@ -573,7 +606,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
|
|||||||
|
|
||||||
# Remove data for one pair from the beginning of the data
|
# Remove data for one pair from the beginning of the data
|
||||||
data[pair] = data[pair][tres:].reset_index()
|
data[pair] = data[pair][tres:].reset_index()
|
||||||
default_conf['ticker_interval'] = '5m'
|
default_conf['timeframe'] = '5m'
|
||||||
|
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
backtesting.strategy.advise_buy = _trend_alternate_hold # Override
|
backtesting.strategy.advise_buy = _trend_alternate_hold # Override
|
||||||
@ -612,8 +645,9 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
|
|||||||
def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir):
|
def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir):
|
||||||
|
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock())
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest')
|
||||||
mocker.patch('freqtrade.optimize.backtesting.show_backtest_results', MagicMock())
|
mocker.patch('freqtrade.optimize.backtesting.generate_backtest_stats')
|
||||||
|
mocker.patch('freqtrade.optimize.backtesting.show_backtest_results')
|
||||||
mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist',
|
mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist',
|
||||||
PropertyMock(return_value=['UNITTEST/BTC']))
|
PropertyMock(return_value=['UNITTEST/BTC']))
|
||||||
patched_configuration_load_config_file(mocker, default_conf)
|
patched_configuration_load_config_file(mocker, default_conf)
|
||||||
@ -623,7 +657,7 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir):
|
|||||||
'--config', 'config.json',
|
'--config', 'config.json',
|
||||||
'--strategy', 'DefaultStrategy',
|
'--strategy', 'DefaultStrategy',
|
||||||
'--datadir', str(testdatadir),
|
'--datadir', str(testdatadir),
|
||||||
'--ticker-interval', '1m',
|
'--timeframe', '1m',
|
||||||
'--timerange', '1510694220-1510700340',
|
'--timerange', '1510694220-1510700340',
|
||||||
'--enable-position-stacking',
|
'--enable-position-stacking',
|
||||||
'--disable-max-market-positions'
|
'--disable-max-market-positions'
|
||||||
@ -632,7 +666,7 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir):
|
|||||||
start_backtesting(args)
|
start_backtesting(args)
|
||||||
# check the logs, that will contain the backtest result
|
# check the logs, that will contain the backtest result
|
||||||
exists = [
|
exists = [
|
||||||
'Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
'Parameter -i/--timeframe detected ... Using timeframe: 1m ...',
|
||||||
'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
|
'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
|
||||||
'Parameter --timerange detected: 1510694220-1510700340 ...',
|
'Parameter --timerange detected: 1510694220-1510700340 ...',
|
||||||
f'Using data directory: {testdatadir} ...',
|
f'Using data directory: {testdatadir} ...',
|
||||||
@ -657,17 +691,17 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
|
|||||||
mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist',
|
mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist',
|
||||||
PropertyMock(return_value=['UNITTEST/BTC']))
|
PropertyMock(return_value=['UNITTEST/BTC']))
|
||||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
|
||||||
gen_table_mock = MagicMock()
|
text_table_mock = MagicMock()
|
||||||
sell_reason_mock = MagicMock()
|
sell_reason_mock = MagicMock()
|
||||||
gen_strattable_mock = MagicMock()
|
strattable_mock = MagicMock()
|
||||||
gen_strat_summary = MagicMock()
|
strat_summary = MagicMock()
|
||||||
|
|
||||||
mocker.patch.multiple('freqtrade.optimize.optimize_reports',
|
mocker.patch.multiple('freqtrade.optimize.optimize_reports',
|
||||||
generate_text_table=gen_table_mock,
|
text_table_bt_results=text_table_mock,
|
||||||
generate_text_table_strategy=gen_strattable_mock,
|
text_table_strategy=strattable_mock,
|
||||||
generate_pair_metrics=MagicMock(),
|
generate_pair_metrics=MagicMock(),
|
||||||
generate_sell_reason_stats=sell_reason_mock,
|
generate_sell_reason_stats=sell_reason_mock,
|
||||||
generate_strategy_metrics=gen_strat_summary,
|
generate_strategy_metrics=strat_summary,
|
||||||
)
|
)
|
||||||
patched_configuration_load_config_file(mocker, default_conf)
|
patched_configuration_load_config_file(mocker, default_conf)
|
||||||
|
|
||||||
@ -676,7 +710,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
|
|||||||
'--config', 'config.json',
|
'--config', 'config.json',
|
||||||
'--datadir', str(testdatadir),
|
'--datadir', str(testdatadir),
|
||||||
'--strategy-path', str(Path(__file__).parents[1] / 'strategy/strats'),
|
'--strategy-path', str(Path(__file__).parents[1] / 'strategy/strats'),
|
||||||
'--ticker-interval', '1m',
|
'--timeframe', '1m',
|
||||||
'--timerange', '1510694220-1510700340',
|
'--timerange', '1510694220-1510700340',
|
||||||
'--enable-position-stacking',
|
'--enable-position-stacking',
|
||||||
'--disable-max-market-positions',
|
'--disable-max-market-positions',
|
||||||
@ -688,14 +722,14 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
|
|||||||
start_backtesting(args)
|
start_backtesting(args)
|
||||||
# 2 backtests, 4 tables
|
# 2 backtests, 4 tables
|
||||||
assert backtestmock.call_count == 2
|
assert backtestmock.call_count == 2
|
||||||
assert gen_table_mock.call_count == 4
|
assert text_table_mock.call_count == 4
|
||||||
assert gen_strattable_mock.call_count == 1
|
assert strattable_mock.call_count == 1
|
||||||
assert sell_reason_mock.call_count == 2
|
assert sell_reason_mock.call_count == 2
|
||||||
assert gen_strat_summary.call_count == 1
|
assert strat_summary.call_count == 1
|
||||||
|
|
||||||
# check the logs, that will contain the backtest result
|
# check the logs, that will contain the backtest result
|
||||||
exists = [
|
exists = [
|
||||||
'Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
'Parameter -i/--timeframe detected ... Using timeframe: 1m ...',
|
||||||
'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
|
'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
|
||||||
'Parameter --timerange detected: 1510694220-1510700340 ...',
|
'Parameter --timerange detected: 1510694220-1510700340 ...',
|
||||||
f'Using data directory: {testdatadir} ...',
|
f'Using data directory: {testdatadir} ...',
|
||||||
@ -765,7 +799,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
|
|||||||
'--config', 'config.json',
|
'--config', 'config.json',
|
||||||
'--datadir', str(testdatadir),
|
'--datadir', str(testdatadir),
|
||||||
'--strategy-path', str(Path(__file__).parents[1] / 'strategy/strats'),
|
'--strategy-path', str(Path(__file__).parents[1] / 'strategy/strats'),
|
||||||
'--ticker-interval', '1m',
|
'--timeframe', '1m',
|
||||||
'--timerange', '1510694220-1510700340',
|
'--timerange', '1510694220-1510700340',
|
||||||
'--enable-position-stacking',
|
'--enable-position-stacking',
|
||||||
'--disable-max-market-positions',
|
'--disable-max-market-positions',
|
||||||
@ -778,7 +812,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
|
|||||||
|
|
||||||
# check the logs, that will contain the backtest result
|
# check the logs, that will contain the backtest result
|
||||||
exists = [
|
exists = [
|
||||||
'Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
'Parameter -i/--timeframe detected ... Using timeframe: 1m ...',
|
||||||
'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
|
'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
|
||||||
'Parameter --timerange detected: 1510694220-1510700340 ...',
|
'Parameter --timerange detected: 1510694220-1510700340 ...',
|
||||||
f'Using data directory: {testdatadir} ...',
|
f'Using data directory: {testdatadir} ...',
|
||||||
|
@ -29,7 +29,7 @@ def test_setup_optimize_configuration_without_arguments(mocker, default_conf, ca
|
|||||||
assert 'pair_whitelist' in config['exchange']
|
assert 'pair_whitelist' in config['exchange']
|
||||||
assert 'datadir' in config
|
assert 'datadir' in config
|
||||||
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
||||||
assert 'ticker_interval' in config
|
assert 'timeframe' in config
|
||||||
assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog)
|
assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog)
|
||||||
|
|
||||||
assert 'timerange' not in config
|
assert 'timerange' not in config
|
||||||
@ -48,7 +48,7 @@ def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> N
|
|||||||
'--config', 'config.json',
|
'--config', 'config.json',
|
||||||
'--strategy', 'DefaultStrategy',
|
'--strategy', 'DefaultStrategy',
|
||||||
'--datadir', '/foo/bar',
|
'--datadir', '/foo/bar',
|
||||||
'--ticker-interval', '1m',
|
'--timeframe', '1m',
|
||||||
'--timerange', ':100',
|
'--timerange', ':100',
|
||||||
'--stoplosses=-0.01,-0.10,-0.001'
|
'--stoplosses=-0.01,-0.10,-0.001'
|
||||||
]
|
]
|
||||||
@ -62,8 +62,8 @@ def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> N
|
|||||||
assert 'datadir' in config
|
assert 'datadir' in config
|
||||||
assert config['runmode'] == RunMode.EDGE
|
assert config['runmode'] == RunMode.EDGE
|
||||||
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
||||||
assert 'ticker_interval' in config
|
assert 'timeframe' in config
|
||||||
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
assert log_has('Parameter -i/--timeframe detected ... Using timeframe: 1m ...',
|
||||||
caplog)
|
caplog)
|
||||||
|
|
||||||
assert 'timerange' in config
|
assert 'timerange' in config
|
||||||
|
@ -94,7 +94,7 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca
|
|||||||
assert 'pair_whitelist' in config['exchange']
|
assert 'pair_whitelist' in config['exchange']
|
||||||
assert 'datadir' in config
|
assert 'datadir' in config
|
||||||
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
||||||
assert 'ticker_interval' in config
|
assert 'timeframe' in config
|
||||||
assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog)
|
assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog)
|
||||||
|
|
||||||
assert 'position_stacking' not in config
|
assert 'position_stacking' not in config
|
||||||
@ -117,7 +117,7 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo
|
|||||||
'--config', 'config.json',
|
'--config', 'config.json',
|
||||||
'--hyperopt', 'DefaultHyperOpt',
|
'--hyperopt', 'DefaultHyperOpt',
|
||||||
'--datadir', '/foo/bar',
|
'--datadir', '/foo/bar',
|
||||||
'--ticker-interval', '1m',
|
'--timeframe', '1m',
|
||||||
'--timerange', ':100',
|
'--timerange', ':100',
|
||||||
'--enable-position-stacking',
|
'--enable-position-stacking',
|
||||||
'--disable-max-market-positions',
|
'--disable-max-market-positions',
|
||||||
@ -136,8 +136,8 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo
|
|||||||
assert config['runmode'] == RunMode.HYPEROPT
|
assert config['runmode'] == RunMode.HYPEROPT
|
||||||
|
|
||||||
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
||||||
assert 'ticker_interval' in config
|
assert 'timeframe' in config
|
||||||
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
assert log_has('Parameter -i/--timeframe detected ... Using timeframe: 1m ...',
|
||||||
caplog)
|
caplog)
|
||||||
|
|
||||||
assert 'position_stacking' in config
|
assert 'position_stacking' in config
|
||||||
@ -197,7 +197,8 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None:
|
|||||||
"Using populate_sell_trend from the strategy.", caplog)
|
"Using populate_sell_trend from the strategy.", caplog)
|
||||||
assert log_has("Hyperopt class does not provide populate_buy_trend() method. "
|
assert log_has("Hyperopt class does not provide populate_buy_trend() method. "
|
||||||
"Using populate_buy_trend from the strategy.", caplog)
|
"Using populate_buy_trend from the strategy.", caplog)
|
||||||
assert hasattr(x, "ticker_interval")
|
assert hasattr(x, "ticker_interval") # DEPRECATED
|
||||||
|
assert hasattr(x, "timeframe")
|
||||||
|
|
||||||
|
|
||||||
def test_hyperoptresolver_wrongname(mocker, default_conf, caplog) -> None:
|
def test_hyperoptresolver_wrongname(mocker, default_conf, caplog) -> None:
|
||||||
@ -544,7 +545,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None:
|
|||||||
)
|
)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
# Co-test loading timeframe from strategy
|
# Co-test loading timeframe from strategy
|
||||||
del default_conf['ticker_interval']
|
del default_conf['timeframe']
|
||||||
default_conf.update({'config': 'config.json.example',
|
default_conf.update({'config': 'config.json.example',
|
||||||
'hyperopt': 'DefaultHyperOpt',
|
'hyperopt': 'DefaultHyperOpt',
|
||||||
'epochs': 1,
|
'epochs': 1,
|
||||||
|
@ -7,13 +7,13 @@ from arrow import Arrow
|
|||||||
from freqtrade.edge import PairInfo
|
from freqtrade.edge import PairInfo
|
||||||
from freqtrade.optimize.optimize_reports import (
|
from freqtrade.optimize.optimize_reports import (
|
||||||
generate_pair_metrics, generate_edge_table, generate_sell_reason_stats,
|
generate_pair_metrics, generate_edge_table, generate_sell_reason_stats,
|
||||||
generate_text_table, generate_text_table_sell_reason, generate_strategy_metrics,
|
text_table_bt_results, text_table_sell_reason, generate_strategy_metrics,
|
||||||
generate_text_table_strategy, store_backtest_result)
|
text_table_strategy, store_backtest_result)
|
||||||
from freqtrade.strategy.interface import SellType
|
from freqtrade.strategy.interface import SellType
|
||||||
from tests.conftest import patch_exchange
|
from tests.conftest import patch_exchange
|
||||||
|
|
||||||
|
|
||||||
def test_generate_text_table(default_conf, mocker):
|
def test_text_table_bt_results(default_conf, mocker):
|
||||||
|
|
||||||
results = pd.DataFrame(
|
results = pd.DataFrame(
|
||||||
{
|
{
|
||||||
@ -40,8 +40,7 @@ def test_generate_text_table(default_conf, mocker):
|
|||||||
|
|
||||||
pair_results = generate_pair_metrics(data={'ETH/BTC': {}}, stake_currency='BTC',
|
pair_results = generate_pair_metrics(data={'ETH/BTC': {}}, stake_currency='BTC',
|
||||||
max_open_trades=2, results=results)
|
max_open_trades=2, results=results)
|
||||||
assert generate_text_table(pair_results,
|
assert text_table_bt_results(pair_results, stake_currency='BTC') == result_str
|
||||||
stake_currency='BTC') == result_str
|
|
||||||
|
|
||||||
|
|
||||||
def test_generate_pair_metrics(default_conf, mocker):
|
def test_generate_pair_metrics(default_conf, mocker):
|
||||||
@ -69,7 +68,7 @@ def test_generate_pair_metrics(default_conf, mocker):
|
|||||||
pytest.approx(pair_results[-1]['profit_sum_pct']) == pair_results[-1]['profit_sum'] * 100)
|
pytest.approx(pair_results[-1]['profit_sum_pct']) == pair_results[-1]['profit_sum'] * 100)
|
||||||
|
|
||||||
|
|
||||||
def test_generate_text_table_sell_reason(default_conf):
|
def test_text_table_sell_reason(default_conf):
|
||||||
|
|
||||||
results = pd.DataFrame(
|
results = pd.DataFrame(
|
||||||
{
|
{
|
||||||
@ -97,8 +96,8 @@ def test_generate_text_table_sell_reason(default_conf):
|
|||||||
|
|
||||||
sell_reason_stats = generate_sell_reason_stats(max_open_trades=2,
|
sell_reason_stats = generate_sell_reason_stats(max_open_trades=2,
|
||||||
results=results)
|
results=results)
|
||||||
assert generate_text_table_sell_reason(sell_reason_stats=sell_reason_stats,
|
assert text_table_sell_reason(sell_reason_stats=sell_reason_stats,
|
||||||
stake_currency='BTC') == result_str
|
stake_currency='BTC') == result_str
|
||||||
|
|
||||||
|
|
||||||
def test_generate_sell_reason_stats(default_conf):
|
def test_generate_sell_reason_stats(default_conf):
|
||||||
@ -136,7 +135,7 @@ def test_generate_sell_reason_stats(default_conf):
|
|||||||
assert stop_result['profit_mean_pct'] == round(stop_result['profit_mean'] * 100, 2)
|
assert stop_result['profit_mean_pct'] == round(stop_result['profit_mean'] * 100, 2)
|
||||||
|
|
||||||
|
|
||||||
def test_generate_text_table_strategy(default_conf, mocker):
|
def test_text_table_strategy(default_conf, mocker):
|
||||||
results = {}
|
results = {}
|
||||||
results['TestStrategy1'] = pd.DataFrame(
|
results['TestStrategy1'] = pd.DataFrame(
|
||||||
{
|
{
|
||||||
@ -178,7 +177,7 @@ def test_generate_text_table_strategy(default_conf, mocker):
|
|||||||
max_open_trades=2,
|
max_open_trades=2,
|
||||||
all_results=results)
|
all_results=results)
|
||||||
|
|
||||||
assert generate_text_table_strategy(strategy_results, 'BTC') == result_str
|
assert text_table_strategy(strategy_results, 'BTC') == result_str
|
||||||
|
|
||||||
|
|
||||||
def test_generate_edge_table(edge_conf, mocker):
|
def test_generate_edge_table(edge_conf, mocker):
|
||||||
|
@ -19,7 +19,8 @@ def whitelist_conf(default_conf):
|
|||||||
'TKN/BTC',
|
'TKN/BTC',
|
||||||
'TRST/BTC',
|
'TRST/BTC',
|
||||||
'SWT/BTC',
|
'SWT/BTC',
|
||||||
'BCC/BTC'
|
'BCC/BTC',
|
||||||
|
'HOT/BTC',
|
||||||
]
|
]
|
||||||
default_conf['exchange']['pair_blacklist'] = [
|
default_conf['exchange']['pair_blacklist'] = [
|
||||||
'BLK/BTC'
|
'BLK/BTC'
|
||||||
@ -56,6 +57,31 @@ def whitelist_conf_2(default_conf):
|
|||||||
return default_conf
|
return default_conf
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def whitelist_conf_3(default_conf):
|
||||||
|
default_conf['stake_currency'] = 'BTC'
|
||||||
|
default_conf['exchange']['pair_whitelist'] = [
|
||||||
|
'ETH/BTC', 'TKN/BTC', 'BLK/BTC', 'LTC/BTC',
|
||||||
|
'BTT/BTC', 'HOT/BTC', 'FUEL/BTC', 'XRP/BTC'
|
||||||
|
]
|
||||||
|
default_conf['exchange']['pair_blacklist'] = [
|
||||||
|
'BLK/BTC'
|
||||||
|
]
|
||||||
|
default_conf['pairlists'] = [
|
||||||
|
{
|
||||||
|
"method": "VolumePairList",
|
||||||
|
"number_assets": 5,
|
||||||
|
"sort_key": "quoteVolume",
|
||||||
|
"refresh_period": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "AgeFilter",
|
||||||
|
"min_days_listed": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
return default_conf
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def static_pl_conf(whitelist_conf):
|
def static_pl_conf(whitelist_conf):
|
||||||
whitelist_conf['pairlists'] = [
|
whitelist_conf['pairlists'] = [
|
||||||
@ -219,17 +245,28 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
|
|||||||
# No pair for ETH, all handlers
|
# No pair for ETH, all handlers
|
||||||
([{"method": "StaticPairList"},
|
([{"method": "StaticPairList"},
|
||||||
{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
|
{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
|
||||||
|
{"method": "AgeFilter", "min_days_listed": 2},
|
||||||
{"method": "PrecisionFilter"},
|
{"method": "PrecisionFilter"},
|
||||||
{"method": "PriceFilter", "low_price_ratio": 0.03},
|
{"method": "PriceFilter", "low_price_ratio": 0.03},
|
||||||
{"method": "SpreadFilter", "max_spread_ratio": 0.005},
|
{"method": "SpreadFilter", "max_spread_ratio": 0.005},
|
||||||
{"method": "ShuffleFilter"}],
|
{"method": "ShuffleFilter"}],
|
||||||
"ETH", []),
|
"ETH", []),
|
||||||
|
# AgeFilter and VolumePairList (require 2 days only, all should pass age test)
|
||||||
|
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
|
||||||
|
{"method": "AgeFilter", "min_days_listed": 2}],
|
||||||
|
"BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC', 'HOT/BTC']),
|
||||||
|
# AgeFilter and VolumePairList (require 10 days, all should fail age test)
|
||||||
|
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
|
||||||
|
{"method": "AgeFilter", "min_days_listed": 10}],
|
||||||
|
"BTC", []),
|
||||||
# Precisionfilter and quote volume
|
# Precisionfilter and quote volume
|
||||||
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
|
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
|
||||||
{"method": "PrecisionFilter"}], "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC']),
|
{"method": "PrecisionFilter"}],
|
||||||
|
"BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC']),
|
||||||
# Precisionfilter bid
|
# Precisionfilter bid
|
||||||
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"},
|
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"},
|
||||||
{"method": "PrecisionFilter"}], "BTC", ['FUEL/BTC', 'XRP/BTC', 'LTC/BTC', 'TKN/BTC']),
|
{"method": "PrecisionFilter"}],
|
||||||
|
"BTC", ['FUEL/BTC', 'XRP/BTC', 'LTC/BTC', 'TKN/BTC']),
|
||||||
# PriceFilter and VolumePairList
|
# PriceFilter and VolumePairList
|
||||||
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
|
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
|
||||||
{"method": "PriceFilter", "low_price_ratio": 0.03}],
|
{"method": "PriceFilter", "low_price_ratio": 0.03}],
|
||||||
@ -249,11 +286,11 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
|
|||||||
"BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']),
|
"BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']),
|
||||||
# StaticPairlist only
|
# StaticPairlist only
|
||||||
([{"method": "StaticPairList"}],
|
([{"method": "StaticPairList"}],
|
||||||
"BTC", ['ETH/BTC', 'TKN/BTC']),
|
"BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']),
|
||||||
# Static Pairlist before VolumePairList - sorting changes
|
# Static Pairlist before VolumePairList - sorting changes
|
||||||
([{"method": "StaticPairList"},
|
([{"method": "StaticPairList"},
|
||||||
{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}],
|
{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}],
|
||||||
"BTC", ['TKN/BTC', 'ETH/BTC']),
|
"BTC", ['HOT/BTC', 'TKN/BTC', 'ETH/BTC']),
|
||||||
# SpreadFilter
|
# SpreadFilter
|
||||||
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
|
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
|
||||||
{"method": "SpreadFilter", "max_spread_ratio": 0.005}],
|
{"method": "SpreadFilter", "max_spread_ratio": 0.005}],
|
||||||
@ -269,48 +306,116 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
|
|||||||
# ShuffleFilter, no seed
|
# ShuffleFilter, no seed
|
||||||
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
|
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
|
||||||
{"method": "ShuffleFilter"}],
|
{"method": "ShuffleFilter"}],
|
||||||
"USDT", 3),
|
"USDT", 3), # whitelist_result is integer -- check only length of randomized pairlist
|
||||||
|
# AgeFilter only
|
||||||
|
([{"method": "AgeFilter", "min_days_listed": 2}],
|
||||||
|
"BTC", 'filter_at_the_beginning'), # OperationalException expected
|
||||||
|
# PrecisionFilter after StaticPairList
|
||||||
|
([{"method": "StaticPairList"},
|
||||||
|
{"method": "PrecisionFilter"}],
|
||||||
|
"BTC", ['ETH/BTC', 'TKN/BTC']),
|
||||||
|
# PrecisionFilter only
|
||||||
|
([{"method": "PrecisionFilter"}],
|
||||||
|
"BTC", 'filter_at_the_beginning'), # OperationalException expected
|
||||||
|
# PriceFilter after StaticPairList
|
||||||
|
([{"method": "StaticPairList"},
|
||||||
|
{"method": "PriceFilter", "low_price_ratio": 0.02}],
|
||||||
|
"BTC", ['ETH/BTC', 'TKN/BTC']),
|
||||||
|
# PriceFilter only
|
||||||
|
([{"method": "PriceFilter", "low_price_ratio": 0.02}],
|
||||||
|
"BTC", 'filter_at_the_beginning'), # OperationalException expected
|
||||||
|
# ShuffleFilter after StaticPairList
|
||||||
|
([{"method": "StaticPairList"},
|
||||||
|
{"method": "ShuffleFilter", "seed": 42}],
|
||||||
|
"BTC", ['TKN/BTC', 'ETH/BTC', 'HOT/BTC']),
|
||||||
|
# ShuffleFilter only
|
||||||
|
([{"method": "ShuffleFilter", "seed": 42}],
|
||||||
|
"BTC", 'filter_at_the_beginning'), # OperationalException expected
|
||||||
|
# SpreadFilter after StaticPairList
|
||||||
|
([{"method": "StaticPairList"},
|
||||||
|
{"method": "SpreadFilter", "max_spread_ratio": 0.005}],
|
||||||
|
"BTC", ['ETH/BTC', 'TKN/BTC']),
|
||||||
|
# SpreadFilter only
|
||||||
|
([{"method": "SpreadFilter", "max_spread_ratio": 0.005}],
|
||||||
|
"BTC", 'filter_at_the_beginning'), # OperationalException expected
|
||||||
|
# Static Pairlist after VolumePairList, on a non-first position
|
||||||
|
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"},
|
||||||
|
{"method": "StaticPairList"}],
|
||||||
|
"BTC", 'static_in_the_middle'),
|
||||||
])
|
])
|
||||||
def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers,
|
def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers,
|
||||||
pairlists, base_currency, whitelist_result,
|
ohlcv_history_list, pairlists, base_currency,
|
||||||
caplog) -> None:
|
whitelist_result, caplog) -> None:
|
||||||
whitelist_conf['pairlists'] = pairlists
|
whitelist_conf['pairlists'] = pairlists
|
||||||
whitelist_conf['stake_currency'] = base_currency
|
whitelist_conf['stake_currency'] = base_currency
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||||
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
|
|
||||||
|
|
||||||
|
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)
|
||||||
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||||
get_tickers=tickers,
|
get_tickers=tickers,
|
||||||
markets=PropertyMock(return_value=shitcoinmarkets),
|
markets=PropertyMock(return_value=shitcoinmarkets)
|
||||||
)
|
)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list),
|
||||||
|
)
|
||||||
|
|
||||||
freqtrade.pairlists.refresh_pairlist()
|
# Set whitelist_result to None if pairlist is invalid and should produce exception
|
||||||
whitelist = freqtrade.pairlists.whitelist
|
if whitelist_result == 'filter_at_the_beginning':
|
||||||
|
with pytest.raises(OperationalException,
|
||||||
assert isinstance(whitelist, list)
|
match=r"This Pairlist Handler should not be used at the first position "
|
||||||
|
r"in the list of Pairlist Handlers."):
|
||||||
# Verify length of pairlist matches (used for ShuffleFilter without seed)
|
freqtrade.pairlists.refresh_pairlist()
|
||||||
if type(whitelist_result) is list:
|
|
||||||
assert whitelist == whitelist_result
|
|
||||||
else:
|
else:
|
||||||
len(whitelist) == whitelist_result
|
freqtrade.pairlists.refresh_pairlist()
|
||||||
|
whitelist = freqtrade.pairlists.whitelist
|
||||||
|
|
||||||
for pairlist in pairlists:
|
assert isinstance(whitelist, list)
|
||||||
if pairlist['method'] == 'PrecisionFilter' and whitelist_result:
|
|
||||||
assert log_has_re(r'^Removed .* from whitelist, because stop price .* '
|
# Verify length of pairlist matches (used for ShuffleFilter without seed)
|
||||||
r'would be <= stop limit.*', caplog)
|
if type(whitelist_result) is list:
|
||||||
if pairlist['method'] == 'PriceFilter' and whitelist_result:
|
assert whitelist == whitelist_result
|
||||||
assert (log_has_re(r'^Removed .* from whitelist, because 1 unit is .*%$', caplog) or
|
else:
|
||||||
log_has_re(r"^Removed .* from whitelist, because ticker\['last'\] is empty.*",
|
len(whitelist) == whitelist_result
|
||||||
caplog))
|
|
||||||
if pairlist['method'] == 'VolumePairList':
|
for pairlist in pairlists:
|
||||||
logmsg = ("DEPRECATED: using any key other than quoteVolume for "
|
if pairlist['method'] == 'AgeFilter' and pairlist['min_days_listed'] and \
|
||||||
"VolumePairList is deprecated.")
|
len(ohlcv_history_list) <= pairlist['min_days_listed']:
|
||||||
if pairlist['sort_key'] != 'quoteVolume':
|
assert log_has_re(r'^Removed .* from whitelist, because age is less than '
|
||||||
assert log_has(logmsg, caplog)
|
r'.* day.*', caplog)
|
||||||
else:
|
if pairlist['method'] == 'PrecisionFilter' and whitelist_result:
|
||||||
assert not log_has(logmsg, caplog)
|
assert log_has_re(r'^Removed .* from whitelist, because stop price .* '
|
||||||
|
r'would be <= stop limit.*', caplog)
|
||||||
|
if pairlist['method'] == 'PriceFilter' and whitelist_result:
|
||||||
|
assert (log_has_re(r'^Removed .* from whitelist, because 1 unit is .*%$', caplog) or
|
||||||
|
log_has_re(r"^Removed .* from whitelist, because ticker\['last'\] "
|
||||||
|
r"is empty.*", caplog))
|
||||||
|
if pairlist['method'] == 'VolumePairList':
|
||||||
|
logmsg = ("DEPRECATED: using any key other than quoteVolume for "
|
||||||
|
"VolumePairList is deprecated.")
|
||||||
|
if pairlist['sort_key'] != 'quoteVolume':
|
||||||
|
assert log_has(logmsg, caplog)
|
||||||
|
else:
|
||||||
|
assert not log_has(logmsg, caplog)
|
||||||
|
|
||||||
|
|
||||||
|
def test_PrecisionFilter_error(mocker, whitelist_conf, tickers) -> None:
|
||||||
|
whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PrecisionFilter"}]
|
||||||
|
del whitelist_conf['stoploss']
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException,
|
||||||
|
match=r"PrecisionFilter can only work with stoploss defined\..*"):
|
||||||
|
PairListManager(MagicMock, whitelist_conf)
|
||||||
|
|
||||||
|
|
||||||
def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None:
|
def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None:
|
||||||
@ -372,6 +477,23 @@ def test__whitelist_for_active_markets(mocker, whitelist_conf, markets, pairlist
|
|||||||
assert log_message in caplog.text
|
assert log_message in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("pairlist", AVAILABLE_PAIRLISTS)
|
||||||
|
def test__whitelist_for_active_markets_empty(mocker, whitelist_conf, markets, pairlist, tickers):
|
||||||
|
whitelist_conf['pairlists'][0]['method'] = pairlist
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True)
|
||||||
|
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||||
|
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||||
|
markets=PropertyMock(return_value=None),
|
||||||
|
get_tickers=tickers
|
||||||
|
)
|
||||||
|
# Assign starting whitelist
|
||||||
|
pairlist_handler = freqtrade.pairlists._pairlist_handlers[0]
|
||||||
|
with pytest.raises(OperationalException, match=r'Markets not loaded.*'):
|
||||||
|
pairlist_handler._whitelist_for_active_markets(['ETH/BTC'])
|
||||||
|
|
||||||
|
|
||||||
def test_volumepairlist_invalid_sortvalue(mocker, markets, whitelist_conf):
|
def test_volumepairlist_invalid_sortvalue(mocker, markets, whitelist_conf):
|
||||||
whitelist_conf['pairlists'][0].update({"sort_key": "asdf"})
|
whitelist_conf['pairlists'][0].update({"sort_key": "asdf"})
|
||||||
|
|
||||||
@ -402,6 +524,29 @@ def test_volumepairlist_caching(mocker, markets, whitelist_conf, tickers):
|
|||||||
assert freqtrade.pairlists._pairlist_handlers[0]._last_refresh == lrf
|
assert freqtrade.pairlists._pairlist_handlers[0]._last_refresh == lrf
|
||||||
|
|
||||||
|
|
||||||
|
def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_history_list):
|
||||||
|
|
||||||
|
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||||
|
markets=PropertyMock(return_value=markets),
|
||||||
|
exchange_has=MagicMock(return_value=True),
|
||||||
|
get_tickers=tickers
|
||||||
|
)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list),
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf_3)
|
||||||
|
assert freqtrade.exchange.get_historic_ohlcv.call_count == 0
|
||||||
|
freqtrade.pairlists.refresh_pairlist()
|
||||||
|
assert freqtrade.exchange.get_historic_ohlcv.call_count > 0
|
||||||
|
|
||||||
|
previous_call_count = freqtrade.exchange.get_historic_ohlcv.call_count
|
||||||
|
freqtrade.pairlists.refresh_pairlist()
|
||||||
|
# Should not have increased since first call.
|
||||||
|
assert freqtrade.exchange.get_historic_ohlcv.call_count == previous_call_count
|
||||||
|
|
||||||
|
|
||||||
def test_pairlistmanager_no_pairlist(mocker, markets, whitelist_conf, caplog):
|
def test_pairlistmanager_no_pairlist(mocker, markets, whitelist_conf, caplog):
|
||||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||||
|
|
||||||
|
@ -42,8 +42,12 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
rpc._rpc_trade_status()
|
rpc._rpc_trade_status()
|
||||||
|
|
||||||
freqtradebot.enter_positions()
|
freqtradebot.enter_positions()
|
||||||
|
trades = Trade.get_open_trades()
|
||||||
|
trades[0].open_order_id = None
|
||||||
|
freqtradebot.exit_positions(trades)
|
||||||
|
|
||||||
results = rpc._rpc_trade_status()
|
results = rpc._rpc_trade_status()
|
||||||
assert {
|
assert results[0] == {
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'base_currency': 'BTC',
|
'base_currency': 'BTC',
|
||||||
@ -54,11 +58,11 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'fee_open': ANY,
|
'fee_open': ANY,
|
||||||
'fee_open_cost': ANY,
|
'fee_open_cost': ANY,
|
||||||
'fee_open_currency': ANY,
|
'fee_open_currency': ANY,
|
||||||
'fee_close': ANY,
|
'fee_close': fee.return_value,
|
||||||
'fee_close_cost': ANY,
|
'fee_close_cost': ANY,
|
||||||
'fee_close_currency': ANY,
|
'fee_close_currency': ANY,
|
||||||
'open_rate_requested': ANY,
|
'open_rate_requested': ANY,
|
||||||
'open_trade_price': ANY,
|
'open_trade_price': 0.0010025,
|
||||||
'close_rate_requested': ANY,
|
'close_rate_requested': ANY,
|
||||||
'sell_reason': ANY,
|
'sell_reason': ANY,
|
||||||
'sell_order_status': ANY,
|
'sell_order_status': ANY,
|
||||||
@ -66,6 +70,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'max_rate': ANY,
|
'max_rate': ANY,
|
||||||
'strategy': ANY,
|
'strategy': ANY,
|
||||||
'ticker_interval': ANY,
|
'ticker_interval': ANY,
|
||||||
|
'timeframe': ANY,
|
||||||
'open_order_id': ANY,
|
'open_order_id': ANY,
|
||||||
'close_date': None,
|
'close_date': None,
|
||||||
'close_date_hum': None,
|
'close_date_hum': None,
|
||||||
@ -77,21 +82,35 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'stake_amount': 0.001,
|
'stake_amount': 0.001,
|
||||||
'close_profit': None,
|
'close_profit': None,
|
||||||
'close_profit_pct': None,
|
'close_profit_pct': None,
|
||||||
|
'close_profit_abs': None,
|
||||||
'current_profit': -0.00408133,
|
'current_profit': -0.00408133,
|
||||||
'current_profit_pct': -0.41,
|
'current_profit_pct': -0.41,
|
||||||
'stop_loss': 0.0,
|
'current_profit_abs': -4.09e-06,
|
||||||
'initial_stop_loss': 0.0,
|
'stop_loss': 9.882e-06,
|
||||||
'initial_stop_loss_pct': None,
|
'stop_loss_abs': 9.882e-06,
|
||||||
'stop_loss_pct': None,
|
'stop_loss_pct': -10.0,
|
||||||
'open_order': '(limit buy rem=0.00000000)'
|
'stop_loss_ratio': -0.1,
|
||||||
} == results[0]
|
'stoploss_order_id': None,
|
||||||
|
'stoploss_last_update': ANY,
|
||||||
|
'stoploss_last_update_timestamp': ANY,
|
||||||
|
'initial_stop_loss': 9.882e-06,
|
||||||
|
'initial_stop_loss_abs': 9.882e-06,
|
||||||
|
'initial_stop_loss_pct': -10.0,
|
||||||
|
'initial_stop_loss_ratio': -0.1,
|
||||||
|
'stoploss_current_dist': -1.1080000000000002e-06,
|
||||||
|
'stoploss_current_dist_ratio': -0.10081893,
|
||||||
|
'stoploss_entry_dist': -0.00010475,
|
||||||
|
'stoploss_entry_dist_ratio': -0.10448878,
|
||||||
|
'open_order': None,
|
||||||
|
'exchange': 'bittrex',
|
||||||
|
}
|
||||||
|
|
||||||
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate',
|
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate',
|
||||||
MagicMock(side_effect=DependencyException("Pair 'ETH/BTC' not available")))
|
MagicMock(side_effect=DependencyException("Pair 'ETH/BTC' not available")))
|
||||||
results = rpc._rpc_trade_status()
|
results = rpc._rpc_trade_status()
|
||||||
assert isnan(results[0]['current_profit'])
|
assert isnan(results[0]['current_profit'])
|
||||||
assert isnan(results[0]['current_rate'])
|
assert isnan(results[0]['current_rate'])
|
||||||
assert {
|
assert results[0] == {
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'base_currency': 'BTC',
|
'base_currency': 'BTC',
|
||||||
@ -102,7 +121,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'fee_open': ANY,
|
'fee_open': ANY,
|
||||||
'fee_open_cost': ANY,
|
'fee_open_cost': ANY,
|
||||||
'fee_open_currency': ANY,
|
'fee_open_currency': ANY,
|
||||||
'fee_close': ANY,
|
'fee_close': fee.return_value,
|
||||||
'fee_close_cost': ANY,
|
'fee_close_cost': ANY,
|
||||||
'fee_close_currency': ANY,
|
'fee_close_currency': ANY,
|
||||||
'open_rate_requested': ANY,
|
'open_rate_requested': ANY,
|
||||||
@ -114,6 +133,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'max_rate': ANY,
|
'max_rate': ANY,
|
||||||
'strategy': ANY,
|
'strategy': ANY,
|
||||||
'ticker_interval': ANY,
|
'ticker_interval': ANY,
|
||||||
|
'timeframe': ANY,
|
||||||
'open_order_id': ANY,
|
'open_order_id': ANY,
|
||||||
'close_date': None,
|
'close_date': None,
|
||||||
'close_date_hum': None,
|
'close_date_hum': None,
|
||||||
@ -125,14 +145,28 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'stake_amount': 0.001,
|
'stake_amount': 0.001,
|
||||||
'close_profit': None,
|
'close_profit': None,
|
||||||
'close_profit_pct': None,
|
'close_profit_pct': None,
|
||||||
|
'close_profit_abs': None,
|
||||||
'current_profit': ANY,
|
'current_profit': ANY,
|
||||||
'current_profit_pct': ANY,
|
'current_profit_pct': ANY,
|
||||||
'stop_loss': 0.0,
|
'current_profit_abs': ANY,
|
||||||
'initial_stop_loss': 0.0,
|
'stop_loss': 9.882e-06,
|
||||||
'initial_stop_loss_pct': None,
|
'stop_loss_abs': 9.882e-06,
|
||||||
'stop_loss_pct': None,
|
'stop_loss_pct': -10.0,
|
||||||
'open_order': '(limit buy rem=0.00000000)'
|
'stop_loss_ratio': -0.1,
|
||||||
} == results[0]
|
'stoploss_order_id': None,
|
||||||
|
'stoploss_last_update': ANY,
|
||||||
|
'stoploss_last_update_timestamp': ANY,
|
||||||
|
'initial_stop_loss': 9.882e-06,
|
||||||
|
'initial_stop_loss_abs': 9.882e-06,
|
||||||
|
'initial_stop_loss_pct': -10.0,
|
||||||
|
'initial_stop_loss_ratio': -0.1,
|
||||||
|
'stoploss_current_dist': ANY,
|
||||||
|
'stoploss_current_dist_ratio': ANY,
|
||||||
|
'stoploss_entry_dist': -0.00010475,
|
||||||
|
'stoploss_entry_dist_ratio': -0.10448878,
|
||||||
|
'open_order': None,
|
||||||
|
'exchange': 'bittrex',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
|
def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
|
||||||
@ -279,8 +313,12 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
|||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
rpc._fiat_converter = CryptoToFiatConverter()
|
rpc._fiat_converter = CryptoToFiatConverter()
|
||||||
|
|
||||||
with pytest.raises(RPCException, match=r'.*no closed trade*'):
|
res = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
||||||
rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
assert res['trade_count'] == 0
|
||||||
|
assert res['first_trade_date'] == ''
|
||||||
|
assert res['first_trade_timestamp'] == 0
|
||||||
|
assert res['latest_trade_date'] == ''
|
||||||
|
assert res['latest_trade_timestamp'] == 0
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtradebot.enter_positions()
|
freqtradebot.enter_positions()
|
||||||
@ -556,7 +594,7 @@ def test_rpc_stopbuy(mocker, default_conf) -> None:
|
|||||||
|
|
||||||
assert freqtradebot.config['max_open_trades'] != 0
|
assert freqtradebot.config['max_open_trades'] != 0
|
||||||
result = rpc._rpc_stopbuy()
|
result = rpc._rpc_stopbuy()
|
||||||
assert {'status': 'No more buy will occur from now. Run /reload_conf to reset.'} == result
|
assert {'status': 'No more buy will occur from now. Run /reload_config to reset.'} == result
|
||||||
assert freqtradebot.config['max_open_trades'] == 0
|
assert freqtradebot.config['max_open_trades'] == 0
|
||||||
|
|
||||||
|
|
||||||
@ -833,6 +871,20 @@ def test_rpc_blacklist(mocker, default_conf) -> None:
|
|||||||
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']
|
assert ret['blacklist'] == ['DOGE/BTC', 'HOT/BTC', 'ETH/BTC']
|
||||||
|
|
||||||
|
ret = rpc._rpc_blacklist(["ETH/BTC"])
|
||||||
|
assert 'errors' in ret
|
||||||
|
assert isinstance(ret['errors'], dict)
|
||||||
|
assert ret['errors']['ETH/BTC']['error_msg'] == 'Pair ETH/BTC already in pairlist.'
|
||||||
|
|
||||||
|
ret = rpc._rpc_blacklist(["ETH/ETH"])
|
||||||
|
assert 'StaticPairList' in ret['method']
|
||||||
|
assert len(ret['blacklist']) == 3
|
||||||
|
assert ret['blacklist'] == default_conf['exchange']['pair_blacklist']
|
||||||
|
assert ret['blacklist'] == ['DOGE/BTC', 'HOT/BTC', 'ETH/BTC']
|
||||||
|
assert 'errors' in ret
|
||||||
|
assert isinstance(ret['errors'], dict)
|
||||||
|
assert ret['errors']['ETH/ETH']['error_msg'] == 'Pair ETH/ETH does not match stake currency.'
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_edge_disabled(mocker, default_conf) -> None:
|
def test_rpc_edge_disabled(mocker, default_conf) -> None:
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
|
@ -24,6 +24,7 @@ def botclient(default_conf, mocker):
|
|||||||
default_conf.update({"api_server": {"enabled": True,
|
default_conf.update({"api_server": {"enabled": True,
|
||||||
"listen_ip_address": "127.0.0.1",
|
"listen_ip_address": "127.0.0.1",
|
||||||
"listen_port": 8080,
|
"listen_port": 8080,
|
||||||
|
"CORS_origins": ['http://example.com'],
|
||||||
"username": _TEST_USER,
|
"username": _TEST_USER,
|
||||||
"password": _TEST_PASS,
|
"password": _TEST_PASS,
|
||||||
}})
|
}})
|
||||||
@ -40,13 +41,13 @@ def client_post(client, url, data={}):
|
|||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
data=data,
|
data=data,
|
||||||
headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS),
|
headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS),
|
||||||
'Origin': 'example.com'})
|
'Origin': 'http://example.com'})
|
||||||
|
|
||||||
|
|
||||||
def client_get(client, url):
|
def client_get(client, url):
|
||||||
# Add fake Origin to ensure CORS kicks in
|
# Add fake Origin to ensure CORS kicks in
|
||||||
return client.get(url, headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS),
|
return client.get(url, headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS),
|
||||||
'Origin': 'example.com'})
|
'Origin': 'http://example.com'})
|
||||||
|
|
||||||
|
|
||||||
def assert_response(response, expected_code=200, needs_cors=True):
|
def assert_response(response, expected_code=200, needs_cors=True):
|
||||||
@ -54,6 +55,7 @@ def assert_response(response, expected_code=200, needs_cors=True):
|
|||||||
assert response.content_type == "application/json"
|
assert response.content_type == "application/json"
|
||||||
if needs_cors:
|
if needs_cors:
|
||||||
assert ('Access-Control-Allow-Credentials', 'true') in response.headers._list
|
assert ('Access-Control-Allow-Credentials', 'true') in response.headers._list
|
||||||
|
assert ('Access-Control-Allow-Origin', 'http://example.com') in response.headers._list
|
||||||
|
|
||||||
|
|
||||||
def test_api_not_found(botclient):
|
def test_api_not_found(botclient):
|
||||||
@ -110,7 +112,7 @@ def test_api_token_login(botclient):
|
|||||||
rc = client.get(f"{BASE_URI}/count",
|
rc = client.get(f"{BASE_URI}/count",
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
headers={'Authorization': f'Bearer {rc.json["access_token"]}',
|
headers={'Authorization': f'Bearer {rc.json["access_token"]}',
|
||||||
'Origin': 'example.com'})
|
'Origin': 'http://example.com'})
|
||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
|
|
||||||
|
|
||||||
@ -122,7 +124,7 @@ def test_api_token_refresh(botclient):
|
|||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
data=None,
|
data=None,
|
||||||
headers={'Authorization': f'Bearer {rc.json["refresh_token"]}',
|
headers={'Authorization': f'Bearer {rc.json["refresh_token"]}',
|
||||||
'Origin': 'example.com'})
|
'Origin': 'http://example.com'})
|
||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
assert 'access_token' in rc.json
|
assert 'access_token' in rc.json
|
||||||
assert 'refresh_token' not in rc.json
|
assert 'refresh_token' not in rc.json
|
||||||
@ -251,10 +253,10 @@ def test_api_cleanup(default_conf, mocker, caplog):
|
|||||||
def test_api_reloadconf(botclient):
|
def test_api_reloadconf(botclient):
|
||||||
ftbot, client = botclient
|
ftbot, client = botclient
|
||||||
|
|
||||||
rc = client_post(client, f"{BASE_URI}/reload_conf")
|
rc = client_post(client, f"{BASE_URI}/reload_config")
|
||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
assert rc.json == {'status': 'reloading config ...'}
|
assert rc.json == {'status': 'reloading config ...'}
|
||||||
assert ftbot.state == State.RELOAD_CONF
|
assert ftbot.state == State.RELOAD_CONFIG
|
||||||
|
|
||||||
|
|
||||||
def test_api_stopbuy(botclient):
|
def test_api_stopbuy(botclient):
|
||||||
@ -263,7 +265,7 @@ def test_api_stopbuy(botclient):
|
|||||||
|
|
||||||
rc = client_post(client, f"{BASE_URI}/stopbuy")
|
rc = client_post(client, f"{BASE_URI}/stopbuy")
|
||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
assert rc.json == {'status': 'No more buy will occur from now. Run /reload_conf to reset.'}
|
assert rc.json == {'status': 'No more buy will occur from now. Run /reload_config to reset.'}
|
||||||
assert ftbot.config['max_open_trades'] == 0
|
assert ftbot.config['max_open_trades'] == 0
|
||||||
|
|
||||||
|
|
||||||
@ -323,8 +325,11 @@ def test_api_show_config(botclient, mocker):
|
|||||||
assert 'dry_run' in rc.json
|
assert 'dry_run' in rc.json
|
||||||
assert rc.json['exchange'] == 'bittrex'
|
assert rc.json['exchange'] == 'bittrex'
|
||||||
assert rc.json['ticker_interval'] == '5m'
|
assert rc.json['ticker_interval'] == '5m'
|
||||||
|
assert rc.json['timeframe'] == '5m'
|
||||||
assert rc.json['state'] == 'running'
|
assert rc.json['state'] == 'running'
|
||||||
assert not rc.json['trailing_stop']
|
assert not rc.json['trailing_stop']
|
||||||
|
assert 'bid_strategy' in rc.json
|
||||||
|
assert 'ask_strategy' in rc.json
|
||||||
|
|
||||||
|
|
||||||
def test_api_daily(botclient, mocker, ticker, fee, markets):
|
def test_api_daily(botclient, mocker, ticker, fee, markets):
|
||||||
@ -396,9 +401,8 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li
|
|||||||
)
|
)
|
||||||
|
|
||||||
rc = client_get(client, f"{BASE_URI}/profit")
|
rc = client_get(client, f"{BASE_URI}/profit")
|
||||||
assert_response(rc, 502)
|
assert_response(rc, 200)
|
||||||
assert len(rc.json) == 1
|
assert rc.json['trade_count'] == 0
|
||||||
assert rc.json == {"error": "Error querying _profit: no closed trade"}
|
|
||||||
|
|
||||||
ftbot.enter_positions()
|
ftbot.enter_positions()
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
@ -406,8 +410,11 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li
|
|||||||
# Simulate fulfilled LIMIT_BUY order for trade
|
# Simulate fulfilled LIMIT_BUY order for trade
|
||||||
trade.update(limit_buy_order)
|
trade.update(limit_buy_order)
|
||||||
rc = client_get(client, f"{BASE_URI}/profit")
|
rc = client_get(client, f"{BASE_URI}/profit")
|
||||||
assert_response(rc, 502)
|
assert_response(rc, 200)
|
||||||
assert rc.json == {"error": "Error querying _profit: no closed trade"}
|
# One open trade
|
||||||
|
assert rc.json['trade_count'] == 1
|
||||||
|
assert rc.json['best_pair'] == ''
|
||||||
|
assert rc.json['best_rate'] == 0
|
||||||
|
|
||||||
trade.update(limit_sell_order)
|
trade.update(limit_sell_order)
|
||||||
|
|
||||||
@ -426,10 +433,19 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li
|
|||||||
'profit_all_coin': 6.217e-05,
|
'profit_all_coin': 6.217e-05,
|
||||||
'profit_all_fiat': 0,
|
'profit_all_fiat': 0,
|
||||||
'profit_all_percent': 6.2,
|
'profit_all_percent': 6.2,
|
||||||
|
'profit_all_percent_mean': 6.2,
|
||||||
|
'profit_all_ratio_mean': 0.06201058,
|
||||||
|
'profit_all_percent_sum': 6.2,
|
||||||
|
'profit_all_ratio_sum': 0.06201058,
|
||||||
'profit_closed_coin': 6.217e-05,
|
'profit_closed_coin': 6.217e-05,
|
||||||
'profit_closed_fiat': 0,
|
'profit_closed_fiat': 0,
|
||||||
'profit_closed_percent': 6.2,
|
'profit_closed_percent': 6.2,
|
||||||
'trade_count': 1
|
'profit_closed_ratio_mean': 0.06201058,
|
||||||
|
'profit_closed_percent_mean': 6.2,
|
||||||
|
'profit_closed_ratio_sum': 0.06201058,
|
||||||
|
'profit_closed_percent_sum': 6.2,
|
||||||
|
'trade_count': 1,
|
||||||
|
'closed_trade_count': 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -492,6 +508,10 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
|
|||||||
assert rc.json == []
|
assert rc.json == []
|
||||||
|
|
||||||
ftbot.enter_positions()
|
ftbot.enter_positions()
|
||||||
|
trades = Trade.get_open_trades()
|
||||||
|
trades[0].open_order_id = None
|
||||||
|
ftbot.exit_positions(trades)
|
||||||
|
|
||||||
rc = client_get(client, f"{BASE_URI}/status")
|
rc = client_get(client, f"{BASE_URI}/status")
|
||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
assert len(rc.json) == 1
|
assert len(rc.json) == 1
|
||||||
@ -502,21 +522,34 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
|
|||||||
'close_timestamp': None,
|
'close_timestamp': None,
|
||||||
'close_profit': None,
|
'close_profit': None,
|
||||||
'close_profit_pct': None,
|
'close_profit_pct': None,
|
||||||
|
'close_profit_abs': None,
|
||||||
'close_rate': None,
|
'close_rate': None,
|
||||||
'current_profit': -0.00408133,
|
'current_profit': -0.00408133,
|
||||||
'current_profit_pct': -0.41,
|
'current_profit_pct': -0.41,
|
||||||
|
'current_profit_abs': -4.09e-06,
|
||||||
'current_rate': 1.099e-05,
|
'current_rate': 1.099e-05,
|
||||||
'initial_stop_loss': 0.0,
|
|
||||||
'initial_stop_loss_pct': None,
|
|
||||||
'open_date': ANY,
|
'open_date': ANY,
|
||||||
'open_date_hum': 'just now',
|
'open_date_hum': 'just now',
|
||||||
'open_timestamp': ANY,
|
'open_timestamp': ANY,
|
||||||
'open_order': '(limit buy rem=0.00000000)',
|
'open_order': None,
|
||||||
'open_rate': 1.098e-05,
|
'open_rate': 1.098e-05,
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'stake_amount': 0.001,
|
'stake_amount': 0.001,
|
||||||
'stop_loss': 0.0,
|
'stop_loss': 9.882e-06,
|
||||||
'stop_loss_pct': None,
|
'stop_loss_abs': 9.882e-06,
|
||||||
|
'stop_loss_pct': -10.0,
|
||||||
|
'stop_loss_ratio': -0.1,
|
||||||
|
'stoploss_order_id': None,
|
||||||
|
'stoploss_last_update': ANY,
|
||||||
|
'stoploss_last_update_timestamp': ANY,
|
||||||
|
'initial_stop_loss': 9.882e-06,
|
||||||
|
'initial_stop_loss_abs': 9.882e-06,
|
||||||
|
'initial_stop_loss_pct': -10.0,
|
||||||
|
'initial_stop_loss_ratio': -0.1,
|
||||||
|
'stoploss_current_dist': -1.1080000000000002e-06,
|
||||||
|
'stoploss_current_dist_ratio': -0.10081893,
|
||||||
|
'stoploss_entry_dist': -0.00010475,
|
||||||
|
'stoploss_entry_dist_ratio': -0.10448878,
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
'close_rate_requested': None,
|
'close_rate_requested': None,
|
||||||
'current_rate': 1.099e-05,
|
'current_rate': 1.099e-05,
|
||||||
@ -528,15 +561,18 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
|
|||||||
'fee_open_currency': None,
|
'fee_open_currency': None,
|
||||||
'open_date': ANY,
|
'open_date': ANY,
|
||||||
'is_open': True,
|
'is_open': True,
|
||||||
'max_rate': 0.0,
|
'max_rate': 1.099e-05,
|
||||||
'min_rate': None,
|
'min_rate': 1.098e-05,
|
||||||
'open_order_id': ANY,
|
'open_order_id': None,
|
||||||
'open_rate_requested': 1.098e-05,
|
'open_rate_requested': 1.098e-05,
|
||||||
'open_trade_price': 0.0010025,
|
'open_trade_price': 0.0010025,
|
||||||
'sell_reason': None,
|
'sell_reason': None,
|
||||||
'sell_order_status': None,
|
'sell_order_status': None,
|
||||||
'strategy': 'DefaultStrategy',
|
'strategy': 'DefaultStrategy',
|
||||||
'ticker_interval': 5}]
|
'ticker_interval': 5,
|
||||||
|
'timeframe': 5,
|
||||||
|
'exchange': 'bittrex',
|
||||||
|
}]
|
||||||
|
|
||||||
|
|
||||||
def test_api_version(botclient):
|
def test_api_version(botclient):
|
||||||
@ -554,7 +590,9 @@ def test_api_blacklist(botclient, mocker):
|
|||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC"],
|
assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC"],
|
||||||
"length": 2,
|
"length": 2,
|
||||||
"method": ["StaticPairList"]}
|
"method": ["StaticPairList"],
|
||||||
|
"errors": {},
|
||||||
|
}
|
||||||
|
|
||||||
# Add ETH/BTC to blacklist
|
# Add ETH/BTC to blacklist
|
||||||
rc = client_post(client, f"{BASE_URI}/blacklist",
|
rc = client_post(client, f"{BASE_URI}/blacklist",
|
||||||
@ -562,7 +600,9 @@ def test_api_blacklist(botclient, mocker):
|
|||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC", "ETH/BTC"],
|
assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC", "ETH/BTC"],
|
||||||
"length": 3,
|
"length": 3,
|
||||||
"method": ["StaticPairList"]}
|
"method": ["StaticPairList"],
|
||||||
|
"errors": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_api_whitelist(botclient):
|
def test_api_whitelist(botclient):
|
||||||
@ -613,12 +653,11 @@ def test_api_forcebuy(botclient, mocker, fee):
|
|||||||
data='{"pair": "ETH/BTC"}')
|
data='{"pair": "ETH/BTC"}')
|
||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
assert rc.json == {'amount': 1,
|
assert rc.json == {'amount': 1,
|
||||||
|
'trade_id': None,
|
||||||
'close_date': None,
|
'close_date': None,
|
||||||
'close_date_hum': None,
|
'close_date_hum': None,
|
||||||
'close_timestamp': None,
|
'close_timestamp': None,
|
||||||
'close_rate': 0.265441,
|
'close_rate': 0.265441,
|
||||||
'initial_stop_loss': None,
|
|
||||||
'initial_stop_loss_pct': None,
|
|
||||||
'open_date': ANY,
|
'open_date': ANY,
|
||||||
'open_date_hum': 'just now',
|
'open_date_hum': 'just now',
|
||||||
'open_timestamp': ANY,
|
'open_timestamp': ANY,
|
||||||
@ -626,9 +665,18 @@ def test_api_forcebuy(botclient, mocker, fee):
|
|||||||
'pair': 'ETH/ETH',
|
'pair': 'ETH/ETH',
|
||||||
'stake_amount': 1,
|
'stake_amount': 1,
|
||||||
'stop_loss': None,
|
'stop_loss': None,
|
||||||
|
'stop_loss_abs': None,
|
||||||
'stop_loss_pct': None,
|
'stop_loss_pct': None,
|
||||||
'trade_id': None,
|
'stop_loss_ratio': None,
|
||||||
|
'stoploss_order_id': None,
|
||||||
|
'stoploss_last_update': None,
|
||||||
|
'stoploss_last_update_timestamp': None,
|
||||||
|
'initial_stop_loss': None,
|
||||||
|
'initial_stop_loss_abs': None,
|
||||||
|
'initial_stop_loss_pct': None,
|
||||||
|
'initial_stop_loss_ratio': None,
|
||||||
'close_profit': None,
|
'close_profit': None,
|
||||||
|
'close_profit_abs': None,
|
||||||
'close_rate_requested': None,
|
'close_rate_requested': None,
|
||||||
'fee_close': 0.0025,
|
'fee_close': 0.0025,
|
||||||
'fee_close_cost': None,
|
'fee_close_cost': None,
|
||||||
@ -645,7 +693,9 @@ def test_api_forcebuy(botclient, mocker, fee):
|
|||||||
'sell_reason': None,
|
'sell_reason': None,
|
||||||
'sell_order_status': None,
|
'sell_order_status': None,
|
||||||
'strategy': None,
|
'strategy': None,
|
||||||
'ticker_interval': None
|
'ticker_interval': None,
|
||||||
|
'timeframe': None,
|
||||||
|
'exchange': 'bittrex',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -71,10 +71,11 @@ def test_init(default_conf, mocker, caplog) -> None:
|
|||||||
assert start_polling.dispatcher.add_handler.call_count > 0
|
assert start_polling.dispatcher.add_handler.call_count > 0
|
||||||
assert start_polling.start_polling.call_count == 1
|
assert start_polling.start_polling.call_count == 1
|
||||||
|
|
||||||
message_str = "rpc.telegram is listening for following commands: [['status'], ['profit'], " \
|
message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], "
|
||||||
"['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], " \
|
"['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], "
|
||||||
"['performance'], ['daily'], ['count'], ['reload_conf'], ['show_config'], " \
|
"['performance'], ['daily'], ['count'], ['reload_config', 'reload_conf'], "
|
||||||
"['stopbuy'], ['whitelist'], ['blacklist'], ['edge'], ['help'], ['version']]"
|
"['show_config', 'show_conf'], ['stopbuy'], ['whitelist'], ['blacklist'], "
|
||||||
|
"['edge'], ['help'], ['version']]")
|
||||||
|
|
||||||
assert log_has(message_str, caplog)
|
assert log_has(message_str, caplog)
|
||||||
|
|
||||||
@ -420,7 +421,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
|||||||
|
|
||||||
telegram._profit(update=update, context=MagicMock())
|
telegram._profit(update=update, context=MagicMock())
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'no closed trade' in msg_mock.call_args_list[0][0][0]
|
assert 'No trades yet.' in msg_mock.call_args_list[0][0][0]
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
@ -432,7 +433,10 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
|||||||
|
|
||||||
telegram._profit(update=update, context=MagicMock())
|
telegram._profit(update=update, context=MagicMock())
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'no closed trade' in msg_mock.call_args_list[-1][0][0]
|
assert 'No closed trade' in msg_mock.call_args_list[-1][0][0]
|
||||||
|
assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0]
|
||||||
|
assert ('∙ `-0.00000500 BTC (-0.50%) (-0.5 \N{GREEK CAPITAL LETTER SIGMA}%)`'
|
||||||
|
in msg_mock.call_args_list[-1][0][0])
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
|
|
||||||
# Update the ticker with a market going up
|
# Update the ticker with a market going up
|
||||||
@ -444,11 +448,13 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
|||||||
|
|
||||||
telegram._profit(update=update, context=MagicMock())
|
telegram._profit(update=update, context=MagicMock())
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert '*ROI:* Close trades' in msg_mock.call_args_list[-1][0][0]
|
assert '*ROI:* Closed trades' in msg_mock.call_args_list[-1][0][0]
|
||||||
assert '∙ `0.00006217 BTC (6.20%)`' in msg_mock.call_args_list[-1][0][0]
|
assert ('∙ `0.00006217 BTC (6.20%) (6.2 \N{GREEK CAPITAL LETTER SIGMA}%)`'
|
||||||
|
in msg_mock.call_args_list[-1][0][0])
|
||||||
assert '∙ `0.933 USD`' in msg_mock.call_args_list[-1][0][0]
|
assert '∙ `0.933 USD`' in msg_mock.call_args_list[-1][0][0]
|
||||||
assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0]
|
assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0]
|
||||||
assert '∙ `0.00006217 BTC (6.20%)`' in msg_mock.call_args_list[-1][0][0]
|
assert ('∙ `0.00006217 BTC (6.20%) (6.2 \N{GREEK CAPITAL LETTER SIGMA}%)`'
|
||||||
|
in msg_mock.call_args_list[-1][0][0])
|
||||||
assert '∙ `0.933 USD`' in msg_mock.call_args_list[-1][0][0]
|
assert '∙ `0.933 USD`' in msg_mock.call_args_list[-1][0][0]
|
||||||
|
|
||||||
assert '*Best Performing:* `ETH/BTC: 6.20%`' in msg_mock.call_args_list[-1][0][0]
|
assert '*Best Performing:* `ETH/BTC: 6.20%`' in msg_mock.call_args_list[-1][0][0]
|
||||||
@ -661,11 +667,11 @@ def test_stopbuy_handle(default_conf, update, mocker) -> None:
|
|||||||
telegram._stopbuy(update=update, context=MagicMock())
|
telegram._stopbuy(update=update, context=MagicMock())
|
||||||
assert freqtradebot.config['max_open_trades'] == 0
|
assert freqtradebot.config['max_open_trades'] == 0
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'No more buy will occur from now. Run /reload_conf to reset.' \
|
assert 'No more buy will occur from now. Run /reload_config to reset.' \
|
||||||
in msg_mock.call_args_list[0][0][0]
|
in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_reload_conf_handle(default_conf, update, mocker) -> None:
|
def test_reload_config_handle(default_conf, update, mocker) -> None:
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
@ -678,8 +684,8 @@ def test_reload_conf_handle(default_conf, update, mocker) -> None:
|
|||||||
|
|
||||||
freqtradebot.state = State.RUNNING
|
freqtradebot.state = State.RUNNING
|
||||||
assert freqtradebot.state == State.RUNNING
|
assert freqtradebot.state == State.RUNNING
|
||||||
telegram._reload_conf(update=update, context=MagicMock())
|
telegram._reload_config(update=update, context=MagicMock())
|
||||||
assert freqtradebot.state == State.RELOAD_CONF
|
assert freqtradebot.state == State.RELOAD_CONFIG
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'reloading config' in msg_mock.call_args_list[0][0][0]
|
assert 'reloading config' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
@ -1011,9 +1017,8 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None:
|
|||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
telegram._count(update=update, context=MagicMock())
|
telegram._count(update=update, context=MagicMock())
|
||||||
|
|
||||||
msg = '<pre> current max total stake\n--------- ----- -------------\n' \
|
msg = ('<pre> current max total stake\n--------- ----- -------------\n'
|
||||||
' 1 {} {}</pre>'\
|
' 1 {} {}</pre>').format(
|
||||||
.format(
|
|
||||||
default_conf['max_open_trades'],
|
default_conf['max_open_trades'],
|
||||||
default_conf['stake_amount']
|
default_conf['stake_amount']
|
||||||
)
|
)
|
||||||
@ -1085,6 +1090,18 @@ def test_blacklist_static(default_conf, update, mocker) -> None:
|
|||||||
in msg_mock.call_args_list[0][0][0])
|
in msg_mock.call_args_list[0][0][0])
|
||||||
assert freqtradebot.pairlists.blacklist == ["DOGE/BTC", "HOT/BTC", "ETH/BTC"]
|
assert freqtradebot.pairlists.blacklist == ["DOGE/BTC", "HOT/BTC", "ETH/BTC"]
|
||||||
|
|
||||||
|
msg_mock.reset_mock()
|
||||||
|
context = MagicMock()
|
||||||
|
context.args = ["ETH/ETH"]
|
||||||
|
telegram._blacklist(update=update, context=context)
|
||||||
|
assert msg_mock.call_count == 2
|
||||||
|
assert ("Error adding `ETH/ETH` to blacklist: `Pair ETH/ETH does not match stake currency.`"
|
||||||
|
in msg_mock.call_args_list[0][0][0])
|
||||||
|
|
||||||
|
assert ("Blacklist contains 3 pairs\n`DOGE/BTC, HOT/BTC, ETH/BTC`"
|
||||||
|
in msg_mock.call_args_list[1][0][0])
|
||||||
|
assert freqtradebot.pairlists.blacklist == ["DOGE/BTC", "HOT/BTC", "ETH/BTC"]
|
||||||
|
|
||||||
|
|
||||||
def test_edge_disabled(default_conf, update, mocker) -> None:
|
def test_edge_disabled(default_conf, update, mocker) -> None:
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
@ -1208,7 +1225,7 @@ def test_send_msg_buy_notification(default_conf, mocker) -> None:
|
|||||||
'open_date': arrow.utcnow().shift(hours=-1)
|
'open_date': arrow.utcnow().shift(hours=-1)
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] \
|
assert msg_mock.call_args[0][0] \
|
||||||
== '*Bittrex:* Buying ETH/BTC\n' \
|
== '\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC\n' \
|
||||||
'*Amount:* `1333.33333333`\n' \
|
'*Amount:* `1333.33333333`\n' \
|
||||||
'*Open Rate:* `0.00001099`\n' \
|
'*Open Rate:* `0.00001099`\n' \
|
||||||
'*Current Rate:* `0.00001099`\n' \
|
'*Current Rate:* `0.00001099`\n' \
|
||||||
@ -1230,7 +1247,7 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None:
|
|||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] \
|
assert msg_mock.call_args[0][0] \
|
||||||
== ('*Bittrex:* Cancelling Open Buy Order for ETH/BTC')
|
== ('\N{WARNING SIGN} *Bittrex:* Cancelling Open Buy Order for ETH/BTC')
|
||||||
|
|
||||||
|
|
||||||
def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
||||||
@ -1263,7 +1280,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
|||||||
'close_date': arrow.utcnow(),
|
'close_date': arrow.utcnow(),
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] \
|
assert msg_mock.call_args[0][0] \
|
||||||
== ('*Binance:* Selling KEY/ETH\n'
|
== ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH\n'
|
||||||
'*Amount:* `1333.33333333`\n'
|
'*Amount:* `1333.33333333`\n'
|
||||||
'*Open Rate:* `0.00007500`\n'
|
'*Open Rate:* `0.00007500`\n'
|
||||||
'*Current Rate:* `0.00003201`\n'
|
'*Current Rate:* `0.00003201`\n'
|
||||||
@ -1291,7 +1308,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
|||||||
'close_date': arrow.utcnow(),
|
'close_date': arrow.utcnow(),
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] \
|
assert msg_mock.call_args[0][0] \
|
||||||
== ('*Binance:* Selling KEY/ETH\n'
|
== ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH\n'
|
||||||
'*Amount:* `1333.33333333`\n'
|
'*Amount:* `1333.33333333`\n'
|
||||||
'*Open Rate:* `0.00007500`\n'
|
'*Open Rate:* `0.00007500`\n'
|
||||||
'*Current Rate:* `0.00003201`\n'
|
'*Current Rate:* `0.00003201`\n'
|
||||||
@ -1321,7 +1338,8 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None:
|
|||||||
'reason': 'Cancelled on exchange'
|
'reason': 'Cancelled on exchange'
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] \
|
assert msg_mock.call_args[0][0] \
|
||||||
== ('*Binance:* Cancelling Open Sell Order for KEY/ETH. Reason: Cancelled on exchange')
|
== ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH. '
|
||||||
|
'Reason: Cancelled on exchange')
|
||||||
|
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
telegram.send_msg({
|
telegram.send_msg({
|
||||||
@ -1331,7 +1349,7 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None:
|
|||||||
'reason': 'timeout'
|
'reason': 'timeout'
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] \
|
assert msg_mock.call_args[0][0] \
|
||||||
== ('*Binance:* Cancelling Open Sell Order for KEY/ETH. Reason: timeout')
|
== ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH. Reason: timeout')
|
||||||
# Reset singleton function to avoid random breaks
|
# Reset singleton function to avoid random breaks
|
||||||
telegram._fiat_converter.convert_amount = old_convamount
|
telegram._fiat_converter.convert_amount = old_convamount
|
||||||
|
|
||||||
@ -1365,7 +1383,7 @@ def test_warning_notification(default_conf, mocker) -> None:
|
|||||||
'type': RPCMessageType.WARNING_NOTIFICATION,
|
'type': RPCMessageType.WARNING_NOTIFICATION,
|
||||||
'status': 'message'
|
'status': 'message'
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] == '*Warning:* `message`'
|
assert msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Warning:* `message`'
|
||||||
|
|
||||||
|
|
||||||
def test_custom_notification(default_conf, mocker) -> None:
|
def test_custom_notification(default_conf, mocker) -> None:
|
||||||
@ -1423,12 +1441,11 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None:
|
|||||||
'amount': 1333.3333333333335,
|
'amount': 1333.3333333333335,
|
||||||
'open_date': arrow.utcnow().shift(hours=-1)
|
'open_date': arrow.utcnow().shift(hours=-1)
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] \
|
assert msg_mock.call_args[0][0] == ('\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC\n'
|
||||||
== '*Bittrex:* Buying ETH/BTC\n' \
|
'*Amount:* `1333.33333333`\n'
|
||||||
'*Amount:* `1333.33333333`\n' \
|
'*Open Rate:* `0.00001099`\n'
|
||||||
'*Open Rate:* `0.00001099`\n' \
|
'*Current Rate:* `0.00001099`\n'
|
||||||
'*Current Rate:* `0.00001099`\n' \
|
'*Total:* `(0.001000 BTC)`')
|
||||||
'*Total:* `(0.001000 BTC)`'
|
|
||||||
|
|
||||||
|
|
||||||
def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
|
def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
|
||||||
@ -1459,15 +1476,37 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
|
|||||||
'open_date': arrow.utcnow().shift(hours=-2, minutes=-35, seconds=-3),
|
'open_date': arrow.utcnow().shift(hours=-2, minutes=-35, seconds=-3),
|
||||||
'close_date': arrow.utcnow(),
|
'close_date': arrow.utcnow(),
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] \
|
assert msg_mock.call_args[0][0] == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH\n'
|
||||||
== '*Binance:* Selling KEY/ETH\n' \
|
'*Amount:* `1333.33333333`\n'
|
||||||
'*Amount:* `1333.33333333`\n' \
|
'*Open Rate:* `0.00007500`\n'
|
||||||
'*Open Rate:* `0.00007500`\n' \
|
'*Current Rate:* `0.00003201`\n'
|
||||||
'*Current Rate:* `0.00003201`\n' \
|
'*Close Rate:* `0.00003201`\n'
|
||||||
'*Close Rate:* `0.00003201`\n' \
|
'*Sell Reason:* `stop_loss`\n'
|
||||||
'*Sell Reason:* `stop_loss`\n' \
|
'*Duration:* `2:35:03 (155.1 min)`\n'
|
||||||
'*Duration:* `2:35:03 (155.1 min)`\n' \
|
'*Profit:* `-57.41%`')
|
||||||
'*Profit:* `-57.41%`'
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('msg,expected', [
|
||||||
|
({'profit_percent': 20.1, 'sell_reason': 'roi'}, "\N{ROCKET}"),
|
||||||
|
({'profit_percent': 5.1, 'sell_reason': 'roi'}, "\N{ROCKET}"),
|
||||||
|
({'profit_percent': 2.56, 'sell_reason': 'roi'}, "\N{EIGHT SPOKED ASTERISK}"),
|
||||||
|
({'profit_percent': 1.0, 'sell_reason': 'roi'}, "\N{EIGHT SPOKED ASTERISK}"),
|
||||||
|
({'profit_percent': 0.0, 'sell_reason': 'roi'}, "\N{EIGHT SPOKED ASTERISK}"),
|
||||||
|
({'profit_percent': -5.0, 'sell_reason': 'stop_loss'}, "\N{WARNING SIGN}"),
|
||||||
|
({'profit_percent': -2.0, 'sell_reason': 'sell_signal'}, "\N{CROSS MARK}"),
|
||||||
|
])
|
||||||
|
def test__sell_emoji(default_conf, mocker, msg, expected):
|
||||||
|
del default_conf['fiat_display_currency']
|
||||||
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
|
_init=MagicMock(),
|
||||||
|
_send_msg=msg_mock
|
||||||
|
)
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
|
assert telegram._get_sell_emoji(msg) == expected
|
||||||
|
|
||||||
|
|
||||||
def test__send_msg(default_conf, mocker) -> None:
|
def test__send_msg(default_conf, mocker) -> None:
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user