Merge branch 'feat/short' into lev-freqtradebot
This commit is contained in:
commit
b3656ddfc9
@ -149,7 +149,9 @@
|
|||||||
},
|
},
|
||||||
"sell_fill": "on",
|
"sell_fill": "on",
|
||||||
"buy_cancel": "on",
|
"buy_cancel": "on",
|
||||||
"sell_cancel": "on"
|
"sell_cancel": "on",
|
||||||
|
"protection_trigger": "off",
|
||||||
|
"protection_trigger_global": "on"
|
||||||
},
|
},
|
||||||
"reload": true,
|
"reload": true,
|
||||||
"balance_dust_level": 0.01
|
"balance_dust_level": 0.01
|
||||||
|
@ -204,6 +204,61 @@ It'll also remove original jsongz data files (`--erase` parameter).
|
|||||||
freqtrade convert-trade-data --format-from jsongz --format-to json --datadir ~/.freqtrade/data/kraken --erase
|
freqtrade convert-trade-data --format-from jsongz --format-to json --datadir ~/.freqtrade/data/kraken --erase
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Sub-command trades to ohlcv
|
||||||
|
|
||||||
|
When you need to use `--dl-trades` (kraken only) to download data, conversion of trades data to ohlcv data is the last step.
|
||||||
|
This command will allow you to repeat this last step for additional timeframes without re-downloading the data.
|
||||||
|
|
||||||
|
```
|
||||||
|
usage: freqtrade trades-to-ohlcv [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||||
|
[-d PATH] [--userdir PATH]
|
||||||
|
[-p PAIRS [PAIRS ...]]
|
||||||
|
[-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...]]
|
||||||
|
[--exchange EXCHANGE]
|
||||||
|
[--data-format-ohlcv {json,jsongz,hdf5}]
|
||||||
|
[--data-format-trades {json,jsongz,hdf5}]
|
||||||
|
|
||||||
|
optional arguments:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
|
||||||
|
Limit command to these pairs. Pairs are space-
|
||||||
|
separated.
|
||||||
|
-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...], --timeframes {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...]
|
||||||
|
Specify which tickers to download. Space-separated
|
||||||
|
list. Default: `1m 5m`.
|
||||||
|
--exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no
|
||||||
|
config is provided.
|
||||||
|
--data-format-ohlcv {json,jsongz,hdf5}
|
||||||
|
Storage format for downloaded candle (OHLCV) data.
|
||||||
|
(default: `json`).
|
||||||
|
--data-format-trades {json,jsongz,hdf5}
|
||||||
|
Storage format for downloaded trades data. (default:
|
||||||
|
`jsongz`).
|
||||||
|
|
||||||
|
Common arguments:
|
||||||
|
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||||
|
--logfile FILE Log to the file specified. Special values are:
|
||||||
|
'syslog', 'journald'. See the documentation for more
|
||||||
|
details.
|
||||||
|
-V, --version show program's version number and exit
|
||||||
|
-c PATH, --config PATH
|
||||||
|
Specify configuration file (default:
|
||||||
|
`userdir/config.json` or `config.json` whichever
|
||||||
|
exists). Multiple --config options may be used. Can be
|
||||||
|
set to `-` to read config from stdin.
|
||||||
|
-d PATH, --datadir PATH
|
||||||
|
Path to directory with historical backtesting data.
|
||||||
|
--userdir PATH, --user-data-dir PATH
|
||||||
|
Path to userdata directory.
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example trade-to-ohlcv conversion
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
freqtrade trades-to-ohlcv --exchange kraken -t 5m 1h 1d --pairs BTC/EUR ETH/EUR
|
||||||
|
```
|
||||||
|
|
||||||
### Sub-command list-data
|
### Sub-command list-data
|
||||||
|
|
||||||
You can get a list of downloaded data using the `list-data` sub-command.
|
You can get a list of downloaded data using the `list-data` sub-command.
|
||||||
|
@ -149,6 +149,24 @@ You'll then also need to modify the `docker-compose.yml` file and uncomment the
|
|||||||
|
|
||||||
You can then run `docker-compose build` to build the docker image, and run it using the commands described above.
|
You can then run `docker-compose build` to build the docker image, and run it using the commands described above.
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
#### Docker on Windows
|
||||||
|
|
||||||
|
* Error: `"Timestamp for this request is outside of the recvWindow."`
|
||||||
|
* The market api requests require a synchronized clock but the time in the docker container shifts a bit over time into the past.
|
||||||
|
To fix this issue temporarily you need to run `wsl --shutdown` and restart docker again (a popup on windows 10 will ask you to do so).
|
||||||
|
A permanent solution is either to host the docker container on a linux host or restart the wsl from time to time with the scheduler.
|
||||||
|
```
|
||||||
|
taskkill /IM "Docker Desktop.exe" /F
|
||||||
|
wsl --shutdown
|
||||||
|
start "" "C:\Program Files\Docker\Docker\Docker Desktop.exe"
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Warning
|
||||||
|
Due to the above, we do not recommend the usage of docker on windows for production setups, but only for experimentation, datadownload and backtesting.
|
||||||
|
Best use a linux-VPS for running freqtrade reliably.
|
||||||
|
|
||||||
## Plotting with docker-compose
|
## Plotting with docker-compose
|
||||||
|
|
||||||
Commands `freqtrade plot-profit` and `freqtrade plot-dataframe` ([Documentation](plotting.md)) are available by changing the image to `*_plot` in your docker-compose.yml file.
|
Commands `freqtrade plot-profit` and `freqtrade plot-dataframe` ([Documentation](plotting.md)) are available by changing the image to `*_plot` in your docker-compose.yml file.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
mkdocs==1.2.2
|
mkdocs==1.2.2
|
||||||
mkdocs-material==7.2.6
|
mkdocs-material==7.3.0
|
||||||
mdx_truly_sane_lists==1.2
|
mdx_truly_sane_lists==1.2
|
||||||
pymdown-extensions==8.2
|
pymdown-extensions==8.2
|
||||||
|
@ -731,3 +731,33 @@ The variable 'content', will contain the strategy file in a BASE64 encoded form.
|
|||||||
```
|
```
|
||||||
|
|
||||||
Please ensure that 'NameOfStrategy' is identical to the strategy name!
|
Please ensure that 'NameOfStrategy' is identical to the strategy name!
|
||||||
|
|
||||||
|
## Performance warning
|
||||||
|
|
||||||
|
When executing a strategy, one can sometimes be greeted by the following in the logs
|
||||||
|
|
||||||
|
> PerformanceWarning: DataFrame is highly fragmented.
|
||||||
|
|
||||||
|
This is a warning from [`pandas`](https://github.com/pandas-dev/pandas) and as the warning continues to say:
|
||||||
|
use `pd.concat(axis=1)`.
|
||||||
|
This can have slight performance implications, which are usually only visible during hyperopt (when optimizing an indicator).
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
for val in self.buy_ema_short.range:
|
||||||
|
dataframe[f'ema_short_{val}'] = ta.EMA(dataframe, timeperiod=val)
|
||||||
|
```
|
||||||
|
|
||||||
|
should be rewritten to
|
||||||
|
|
||||||
|
```python
|
||||||
|
frames = [dataframe]
|
||||||
|
for val in self.buy_ema_short.range:
|
||||||
|
frames.append({
|
||||||
|
f'ema_short_{val}': ta.EMA(dataframe, timeperiod=val)
|
||||||
|
})
|
||||||
|
|
||||||
|
# Append columns to existing dataframe
|
||||||
|
merged_frame = pd.concat(frames, axis=1)
|
||||||
|
```
|
||||||
|
@ -122,6 +122,16 @@ def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame
|
|||||||
Look into the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_strategy.py).
|
Look into the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_strategy.py).
|
||||||
Then uncomment indicators you need.
|
Then uncomment indicators you need.
|
||||||
|
|
||||||
|
#### Indicator libraries
|
||||||
|
|
||||||
|
Out of the box, freqtrade installs the following technical libraries:
|
||||||
|
|
||||||
|
* [ta-lib](http://mrjbq7.github.io/ta-lib/)
|
||||||
|
* [pandas-ta](https://twopirllc.github.io/pandas-ta/)
|
||||||
|
* [technical](https://github.com/freqtrade/technical/)
|
||||||
|
|
||||||
|
Additional technical libraries can be installed as necessary, or custom indicators may be written / invented by the strategy author.
|
||||||
|
|
||||||
### Strategy startup period
|
### Strategy startup period
|
||||||
|
|
||||||
Most indicators have an instable startup period, in which they are either not available, or the calculation is incorrect. This can lead to inconsistencies, since Freqtrade does not know how long this instable period should be.
|
Most indicators have an instable startup period, in which they are either not available, or the calculation is incorrect. This can lead to inconsistencies, since Freqtrade does not know how long this instable period should be.
|
||||||
@ -942,6 +952,8 @@ Printing more than a few rows is also possible (simply use `print(dataframe)` i
|
|||||||
|
|
||||||
## Common mistakes when developing strategies
|
## Common mistakes when developing strategies
|
||||||
|
|
||||||
|
### Peeking into the future while backtesting
|
||||||
|
|
||||||
Backtesting analyzes the whole time-range at once for performance reasons. Because of this, strategy authors need to make sure that strategies do not look-ahead into the future.
|
Backtesting analyzes the whole time-range at once for performance reasons. Because of this, strategy authors need to make sure that strategies do not look-ahead into the future.
|
||||||
This is a common pain-point, which can cause huge differences between backtesting and dry/live run methods, since they all use data which is not available during dry/live runs, so these strategies will perform well during backtesting, but will fail / perform badly in real conditions.
|
This is a common pain-point, which can cause huge differences between backtesting and dry/live run methods, since they all use data which is not available during dry/live runs, so these strategies will perform well during backtesting, but will fail / perform badly in real conditions.
|
||||||
|
|
||||||
|
@ -93,7 +93,9 @@ Example configuration showing the different settings:
|
|||||||
"buy_cancel": "silent",
|
"buy_cancel": "silent",
|
||||||
"sell_cancel": "on",
|
"sell_cancel": "on",
|
||||||
"buy_fill": "off",
|
"buy_fill": "off",
|
||||||
"sell_fill": "off"
|
"sell_fill": "off",
|
||||||
|
"protection_trigger": "off",
|
||||||
|
"protection_trigger_global": "on"
|
||||||
},
|
},
|
||||||
"reload": true,
|
"reload": true,
|
||||||
"balance_dust_level": 0.01
|
"balance_dust_level": 0.01
|
||||||
@ -103,6 +105,7 @@ Example configuration showing the different settings:
|
|||||||
`buy` notifications are sent when the order is placed, while `buy_fill` notifications are sent when the order is filled on the exchange.
|
`buy` notifications are sent when the order is placed, while `buy_fill` notifications are sent when the order is filled on the exchange.
|
||||||
`sell` notifications are sent when the order is placed, while `sell_fill` notifications are sent when the order is filled on the exchange.
|
`sell` notifications are sent when the order is placed, while `sell_fill` notifications are sent when the order is filled on the exchange.
|
||||||
`*_fill` notifications are off by default and must be explicitly enabled.
|
`*_fill` notifications are off by default and must be explicitly enabled.
|
||||||
|
`protection_trigger` notifications are sent when a protection triggers and `protection_trigger_global` notifications trigger when global protections are triggered.
|
||||||
|
|
||||||
|
|
||||||
`balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown.
|
`balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown.
|
||||||
|
@ -8,8 +8,8 @@ Note: Be careful with file-scoped imports in these subfiles.
|
|||||||
"""
|
"""
|
||||||
from freqtrade.commands.arguments import Arguments
|
from freqtrade.commands.arguments import Arguments
|
||||||
from freqtrade.commands.build_config_commands import start_new_config
|
from freqtrade.commands.build_config_commands import start_new_config
|
||||||
from freqtrade.commands.data_commands import (start_convert_data, start_download_data,
|
from freqtrade.commands.data_commands import (start_convert_data, start_convert_trades,
|
||||||
start_list_data)
|
start_download_data, start_list_data)
|
||||||
from freqtrade.commands.deploy_commands import (start_create_userdir, start_install_ui,
|
from freqtrade.commands.deploy_commands import (start_create_userdir, start_install_ui,
|
||||||
start_new_strategy)
|
start_new_strategy)
|
||||||
from freqtrade.commands.hyperopt_commands import start_hyperopt_list, start_hyperopt_show
|
from freqtrade.commands.hyperopt_commands import start_hyperopt_list, start_hyperopt_show
|
||||||
|
@ -58,6 +58,8 @@ ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "template"]
|
|||||||
ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase"]
|
ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase"]
|
||||||
ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes"]
|
ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes"]
|
||||||
|
|
||||||
|
ARGS_CONVERT_TRADES = ["pairs", "timeframes", "exchange", "dataformat_ohlcv", "dataformat_trades"]
|
||||||
|
|
||||||
ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs"]
|
ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs"]
|
||||||
|
|
||||||
ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "new_pairs_days", "timerange",
|
ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "new_pairs_days", "timerange",
|
||||||
@ -91,7 +93,7 @@ ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperop
|
|||||||
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
|
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
|
||||||
"list-markets", "list-pairs", "list-strategies", "list-data",
|
"list-markets", "list-pairs", "list-strategies", "list-data",
|
||||||
"hyperopt-list", "hyperopt-show",
|
"hyperopt-list", "hyperopt-show",
|
||||||
"plot-dataframe", "plot-profit", "show-trades"]
|
"plot-dataframe", "plot-profit", "show-trades", "trades-to-ohlcv"]
|
||||||
|
|
||||||
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
|
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
|
||||||
|
|
||||||
@ -169,14 +171,14 @@ class Arguments:
|
|||||||
self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot')
|
self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot')
|
||||||
self._build_args(optionlist=['version'], parser=self.parser)
|
self._build_args(optionlist=['version'], parser=self.parser)
|
||||||
|
|
||||||
from freqtrade.commands import (start_backtesting, start_convert_data, start_create_userdir,
|
from freqtrade.commands import (start_backtesting, start_convert_data, start_convert_trades,
|
||||||
start_download_data, start_edge, start_hyperopt,
|
start_create_userdir, start_download_data, start_edge,
|
||||||
start_hyperopt_list, start_hyperopt_show, start_install_ui,
|
start_hyperopt, start_hyperopt_list, start_hyperopt_show,
|
||||||
start_list_data, start_list_exchanges, start_list_markets,
|
start_install_ui, start_list_data, start_list_exchanges,
|
||||||
start_list_strategies, start_list_timeframes,
|
start_list_markets, start_list_strategies,
|
||||||
start_new_config, start_new_strategy, start_plot_dataframe,
|
start_list_timeframes, start_new_config, start_new_strategy,
|
||||||
start_plot_profit, start_show_trades, start_test_pairlist,
|
start_plot_dataframe, start_plot_profit, start_show_trades,
|
||||||
start_trading, start_webserver)
|
start_test_pairlist, start_trading, start_webserver)
|
||||||
|
|
||||||
subparsers = self.parser.add_subparsers(dest='command',
|
subparsers = self.parser.add_subparsers(dest='command',
|
||||||
# Use custom message when no subhandler is added
|
# Use custom message when no subhandler is added
|
||||||
@ -236,6 +238,15 @@ class Arguments:
|
|||||||
convert_trade_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=False))
|
convert_trade_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=False))
|
||||||
self._build_args(optionlist=ARGS_CONVERT_DATA, parser=convert_trade_data_cmd)
|
self._build_args(optionlist=ARGS_CONVERT_DATA, parser=convert_trade_data_cmd)
|
||||||
|
|
||||||
|
# Add trades-to-ohlcv subcommand
|
||||||
|
convert_trade_data_cmd = subparsers.add_parser(
|
||||||
|
'trades-to-ohlcv',
|
||||||
|
help='Convert trade data to OHLCV data.',
|
||||||
|
parents=[_common_parser],
|
||||||
|
)
|
||||||
|
convert_trade_data_cmd.set_defaults(func=start_convert_trades)
|
||||||
|
self._build_args(optionlist=ARGS_CONVERT_TRADES, parser=convert_trade_data_cmd)
|
||||||
|
|
||||||
# Add list-data subcommand
|
# Add list-data subcommand
|
||||||
list_data_cmd = subparsers.add_parser(
|
list_data_cmd = subparsers.add_parser(
|
||||||
'list-data',
|
'list-data',
|
||||||
|
@ -381,12 +381,12 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
),
|
),
|
||||||
"dataformat_ohlcv": Arg(
|
"dataformat_ohlcv": Arg(
|
||||||
'--data-format-ohlcv',
|
'--data-format-ohlcv',
|
||||||
help='Storage format for downloaded candle (OHLCV) data. (default: `%(default)s`).',
|
help='Storage format for downloaded candle (OHLCV) data. (default: `json`).',
|
||||||
choices=constants.AVAILABLE_DATAHANDLERS,
|
choices=constants.AVAILABLE_DATAHANDLERS,
|
||||||
),
|
),
|
||||||
"dataformat_trades": Arg(
|
"dataformat_trades": Arg(
|
||||||
'--data-format-trades',
|
'--data-format-trades',
|
||||||
help='Storage format for downloaded trades data. (default: `%(default)s`).',
|
help='Storage format for downloaded trades data. (default: `jsongz`).',
|
||||||
choices=constants.AVAILABLE_DATAHANDLERS,
|
choices=constants.AVAILABLE_DATAHANDLERS,
|
||||||
),
|
),
|
||||||
"exchange": Arg(
|
"exchange": Arg(
|
||||||
|
@ -89,6 +89,41 @@ def start_download_data(args: Dict[str, Any]) -> None:
|
|||||||
f"on exchange {exchange.name}.")
|
f"on exchange {exchange.name}.")
|
||||||
|
|
||||||
|
|
||||||
|
def start_convert_trades(args: Dict[str, Any]) -> None:
|
||||||
|
|
||||||
|
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
|
||||||
|
|
||||||
|
timerange = TimeRange()
|
||||||
|
|
||||||
|
# Remove stake-currency to skip checks which are not relevant for datadownload
|
||||||
|
config['stake_currency'] = ''
|
||||||
|
|
||||||
|
if 'pairs' not in config:
|
||||||
|
raise OperationalException(
|
||||||
|
"Downloading data requires a list of pairs. "
|
||||||
|
"Please check the documentation on how to configure this.")
|
||||||
|
|
||||||
|
# Init exchange
|
||||||
|
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
|
||||||
|
# Manual validations of relevant settings
|
||||||
|
if not config['exchange'].get('skip_pair_validation', False):
|
||||||
|
exchange.validate_pairs(config['pairs'])
|
||||||
|
expanded_pairs = expand_pairlist(config['pairs'], list(exchange.markets))
|
||||||
|
|
||||||
|
logger.info(f"About to Convert pairs: {expanded_pairs}, "
|
||||||
|
f"intervals: {config['timeframes']} to {config['datadir']}")
|
||||||
|
|
||||||
|
for timeframe in config['timeframes']:
|
||||||
|
exchange.validate_timeframes(timeframe)
|
||||||
|
# Convert downloaded trade data to different timeframes
|
||||||
|
convert_trades_to_ohlcv(
|
||||||
|
pairs=expanded_pairs, timeframes=config['timeframes'],
|
||||||
|
datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')),
|
||||||
|
data_format_ohlcv=config['dataformat_ohlcv'],
|
||||||
|
data_format_trades=config['dataformat_trades'],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def start_convert_data(args: Dict[str, Any], ohlcv: bool = True) -> None:
|
def start_convert_data(args: Dict[str, Any], ohlcv: bool = True) -> None:
|
||||||
"""
|
"""
|
||||||
Convert data from one format to another
|
Convert data from one format to another
|
||||||
|
@ -110,7 +110,7 @@ CONF_SCHEMA = {
|
|||||||
},
|
},
|
||||||
'tradable_balance_ratio': {
|
'tradable_balance_ratio': {
|
||||||
'type': 'number',
|
'type': 'number',
|
||||||
'minimum': 0.1,
|
'minimum': 0.0,
|
||||||
'maximum': 1,
|
'maximum': 1,
|
||||||
'default': 0.99
|
'default': 0.99
|
||||||
},
|
},
|
||||||
@ -284,6 +284,15 @@ CONF_SCHEMA = {
|
|||||||
'enum': TELEGRAM_SETTING_OPTIONS,
|
'enum': TELEGRAM_SETTING_OPTIONS,
|
||||||
'default': 'off'
|
'default': 'off'
|
||||||
},
|
},
|
||||||
|
'protection_trigger': {
|
||||||
|
'type': 'string',
|
||||||
|
'enum': TELEGRAM_SETTING_OPTIONS,
|
||||||
|
'default': 'off'
|
||||||
|
},
|
||||||
|
'protection_trigger_global': {
|
||||||
|
'type': 'string',
|
||||||
|
'enum': TELEGRAM_SETTING_OPTIONS,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'reload': {'type': 'boolean'},
|
'reload': {'type': 'boolean'},
|
||||||
|
@ -149,6 +149,8 @@ class DataProvider:
|
|||||||
Clear pair dataframe cache.
|
Clear pair dataframe cache.
|
||||||
"""
|
"""
|
||||||
self.__cached_pairs = {}
|
self.__cached_pairs = {}
|
||||||
|
self.__cached_pairs_backtesting = {}
|
||||||
|
self.__slice_index = 0
|
||||||
|
|
||||||
# Exchange functions
|
# Exchange functions
|
||||||
|
|
||||||
|
@ -13,6 +13,8 @@ class RPCMessageType(Enum):
|
|||||||
SELL = 'sell'
|
SELL = 'sell'
|
||||||
SELL_FILL = 'sell_fill'
|
SELL_FILL = 'sell_fill'
|
||||||
SELL_CANCEL = 'sell_cancel'
|
SELL_CANCEL = 'sell_cancel'
|
||||||
|
PROTECTION_TRIGGER = 'protection_trigger'
|
||||||
|
PROTECTION_TRIGGER_GLOBAL = 'protection_trigger_global'
|
||||||
|
|
||||||
SHORT = 'short'
|
SHORT = 'short'
|
||||||
SHORT_FILL = 'short_fill'
|
SHORT_FILL = 'short_fill'
|
||||||
|
@ -154,7 +154,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
# Only update open orders on startup
|
# Only update open orders on startup
|
||||||
# This will update the database after the initial migration
|
# This will update the database after the initial migration
|
||||||
self.update_open_orders()
|
self.startup_update_open_orders()
|
||||||
|
|
||||||
def process(self) -> None:
|
def process(self) -> None:
|
||||||
"""
|
"""
|
||||||
@ -252,7 +252,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
open_trades = len(Trade.get_open_trades())
|
open_trades = len(Trade.get_open_trades())
|
||||||
return max(0, self.config['max_open_trades'] - open_trades)
|
return max(0, self.config['max_open_trades'] - open_trades)
|
||||||
|
|
||||||
def update_open_orders(self):
|
def startup_update_open_orders(self):
|
||||||
"""
|
"""
|
||||||
Updates open orders based on order list kept in the database.
|
Updates open orders based on order list kept in the database.
|
||||||
Mainly updates the state of orders - but may also close trades
|
Mainly updates the state of orders - but may also close trades
|
||||||
@ -1380,7 +1380,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
'exchange': trade.exchange.capitalize(),
|
'exchange': trade.exchange.capitalize(),
|
||||||
'pair': trade.pair,
|
'pair': trade.pair,
|
||||||
'gain': gain,
|
'gain': gain,
|
||||||
'limit': profit_rate,
|
'limit': profit_rate or 0,
|
||||||
'order_type': order_type,
|
'order_type': order_type,
|
||||||
'amount': trade.amount,
|
'amount': trade.amount,
|
||||||
'open_rate': trade.open_rate,
|
'open_rate': trade.open_rate,
|
||||||
@ -1389,7 +1389,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
'profit_ratio': profit_ratio,
|
'profit_ratio': profit_ratio,
|
||||||
'sell_reason': trade.sell_reason,
|
'sell_reason': trade.sell_reason,
|
||||||
'open_date': trade.open_date,
|
'open_date': trade.open_date,
|
||||||
'close_date': trade.close_date,
|
'close_date': trade.close_date or datetime.now(timezone.utc),
|
||||||
'stake_currency': self.config['stake_currency'],
|
'stake_currency': self.config['stake_currency'],
|
||||||
'fiat_currency': self.config.get('fiat_display_currency', None),
|
'fiat_currency': self.config.get('fiat_display_currency', None),
|
||||||
'reason': reason,
|
'reason': reason,
|
||||||
@ -1455,8 +1455,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
if not trade.is_open:
|
if not trade.is_open:
|
||||||
if not stoploss_order and not trade.open_order_id:
|
if not stoploss_order and not trade.open_order_id:
|
||||||
self._notify_exit(trade, '', True)
|
self._notify_exit(trade, '', True)
|
||||||
self.protections.stop_per_pair(trade.pair)
|
self.handle_protections(trade.pair)
|
||||||
self.protections.global_stop()
|
|
||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
elif not trade.open_order_id:
|
elif not trade.open_order_id:
|
||||||
# Buy fill
|
# Buy fill
|
||||||
@ -1464,6 +1463,19 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def handle_protections(self, pair: str) -> None:
|
||||||
|
prot_trig = self.protections.stop_per_pair(pair)
|
||||||
|
if prot_trig:
|
||||||
|
msg = {'type': RPCMessageType.PROTECTION_TRIGGER, }
|
||||||
|
msg.update(prot_trig.to_json())
|
||||||
|
self.rpc.send_msg(msg)
|
||||||
|
|
||||||
|
prot_trig_glb = self.protections.global_stop()
|
||||||
|
if prot_trig_glb:
|
||||||
|
msg = {'type': RPCMessageType.PROTECTION_TRIGGER_GLOBAL, }
|
||||||
|
msg.update(prot_trig_glb.to_json())
|
||||||
|
self.rpc.send_msg(msg)
|
||||||
|
|
||||||
def apply_fee_conditional(self, trade: Trade, trade_base_currency: str,
|
def apply_fee_conditional(self, trade: Trade, trade_base_currency: str,
|
||||||
amount: float, fee_abs: float) -> float:
|
amount: float, fee_abs: float) -> float:
|
||||||
"""
|
"""
|
||||||
|
@ -87,18 +87,7 @@ class Backtesting:
|
|||||||
"configuration or as cli argument `--timeframe 5m`")
|
"configuration or as cli argument `--timeframe 5m`")
|
||||||
self.timeframe = str(self.config.get('timeframe'))
|
self.timeframe = str(self.config.get('timeframe'))
|
||||||
self.timeframe_min = timeframe_to_minutes(self.timeframe)
|
self.timeframe_min = timeframe_to_minutes(self.timeframe)
|
||||||
# Load detail timeframe if specified
|
self.init_backtest_detail()
|
||||||
self.timeframe_detail = str(self.config.get('timeframe_detail', ''))
|
|
||||||
if self.timeframe_detail:
|
|
||||||
self.timeframe_detail_min = timeframe_to_minutes(self.timeframe_detail)
|
|
||||||
if self.timeframe_min <= self.timeframe_detail_min:
|
|
||||||
raise OperationalException(
|
|
||||||
"Detail timeframe must be smaller than strategy timeframe.")
|
|
||||||
|
|
||||||
else:
|
|
||||||
self.timeframe_detail_min = 0
|
|
||||||
self.detail_data: Dict[str, DataFrame] = {}
|
|
||||||
|
|
||||||
self.pairlists = PairListManager(self.exchange, self.config)
|
self.pairlists = PairListManager(self.exchange, self.config)
|
||||||
if 'VolumePairList' in self.pairlists.name_list:
|
if 'VolumePairList' in self.pairlists.name_list:
|
||||||
raise OperationalException("VolumePairList not allowed for backtesting.")
|
raise OperationalException("VolumePairList not allowed for backtesting.")
|
||||||
@ -121,14 +110,6 @@ class Backtesting:
|
|||||||
else:
|
else:
|
||||||
self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0])
|
self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0])
|
||||||
|
|
||||||
Trade.use_db = False
|
|
||||||
Trade.reset_trades()
|
|
||||||
PairLocks.timeframe = self.config['timeframe']
|
|
||||||
PairLocks.use_db = False
|
|
||||||
PairLocks.reset_locks()
|
|
||||||
|
|
||||||
self.wallets = Wallets(self.config, self.exchange, log=False)
|
|
||||||
|
|
||||||
self.timerange = TimeRange.parse_timerange(
|
self.timerange = TimeRange.parse_timerange(
|
||||||
None if self.config.get('timerange') is None else str(self.config.get('timerange')))
|
None if self.config.get('timerange') is None else str(self.config.get('timerange')))
|
||||||
|
|
||||||
@ -144,6 +125,7 @@ class Backtesting:
|
|||||||
|
|
||||||
self.progress = BTProgress()
|
self.progress = BTProgress()
|
||||||
self.abort = False
|
self.abort = False
|
||||||
|
self.init_backtest()
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
self.cleanup()
|
self.cleanup()
|
||||||
@ -153,6 +135,28 @@ class Backtesting:
|
|||||||
PairLocks.use_db = True
|
PairLocks.use_db = True
|
||||||
Trade.use_db = True
|
Trade.use_db = True
|
||||||
|
|
||||||
|
def init_backtest_detail(self):
|
||||||
|
# Load detail timeframe if specified
|
||||||
|
self.timeframe_detail = str(self.config.get('timeframe_detail', ''))
|
||||||
|
if self.timeframe_detail:
|
||||||
|
self.timeframe_detail_min = timeframe_to_minutes(self.timeframe_detail)
|
||||||
|
if self.timeframe_min <= self.timeframe_detail_min:
|
||||||
|
raise OperationalException(
|
||||||
|
"Detail timeframe must be smaller than strategy timeframe.")
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.timeframe_detail_min = 0
|
||||||
|
self.detail_data: Dict[str, DataFrame] = {}
|
||||||
|
|
||||||
|
def init_backtest(self):
|
||||||
|
|
||||||
|
self.prepare_backtest(False)
|
||||||
|
|
||||||
|
self.wallets = Wallets(self.config, self.exchange, log=False)
|
||||||
|
|
||||||
|
self.progress = BTProgress()
|
||||||
|
self.abort = False
|
||||||
|
|
||||||
def _set_strategy(self, strategy: IStrategy):
|
def _set_strategy(self, strategy: IStrategy):
|
||||||
"""
|
"""
|
||||||
Load strategy into backtesting
|
Load strategy into backtesting
|
||||||
@ -232,7 +236,8 @@ class Backtesting:
|
|||||||
Trade.reset_trades()
|
Trade.reset_trades()
|
||||||
self.rejected_trades = 0
|
self.rejected_trades = 0
|
||||||
self.dataprovider.clear_cache()
|
self.dataprovider.clear_cache()
|
||||||
self._load_protections(self.strategy)
|
if enable_protections:
|
||||||
|
self._load_protections(self.strategy)
|
||||||
|
|
||||||
def check_abort(self):
|
def check_abort(self):
|
||||||
"""
|
"""
|
||||||
@ -397,14 +402,14 @@ class Backtesting:
|
|||||||
detail_data = detail_data.loc[
|
detail_data = detail_data.loc[
|
||||||
(detail_data['date'] >= sell_candle_time) &
|
(detail_data['date'] >= sell_candle_time) &
|
||||||
(detail_data['date'] < sell_candle_end)
|
(detail_data['date'] < sell_candle_end)
|
||||||
]
|
].copy()
|
||||||
if len(detail_data) == 0:
|
if len(detail_data) == 0:
|
||||||
# Fall back to "regular" data if no detail data was found for this candle
|
# Fall back to "regular" data if no detail data was found for this candle
|
||||||
return self._get_sell_trade_entry_for_candle(trade, sell_row)
|
return self._get_sell_trade_entry_for_candle(trade, sell_row)
|
||||||
detail_data['enter_long'] = sell_row[LONG_IDX]
|
detail_data.loc[:, 'enter_long'] = sell_row[LONG_IDX]
|
||||||
detail_data['exit_long'] = sell_row[ELONG_IDX]
|
detail_data.loc[:, 'exit_long'] = sell_row[ELONG_IDX]
|
||||||
detail_data['enter_short'] = sell_row[SHORT_IDX]
|
detail_data.loc[:, 'enter_long'] = sell_row[LONG_IDX]
|
||||||
detail_data['exit_short'] = sell_row[ESHORT_IDX]
|
detail_data.loc[:, 'exit_long'] = sell_row[ELONG_IDX]
|
||||||
headers = ['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long',
|
headers = ['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long',
|
||||||
'enter_short', 'exit_short']
|
'enter_short', 'exit_short']
|
||||||
for det_row in detail_data[headers].values.tolist():
|
for det_row in detail_data[headers].values.tolist():
|
||||||
|
@ -30,7 +30,8 @@ class PairLocks():
|
|||||||
PairLocks.locks = []
|
PairLocks.locks = []
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def lock_pair(pair: str, until: datetime, reason: str = None, *, now: datetime = None) -> None:
|
def lock_pair(pair: str, until: datetime, reason: str = None, *,
|
||||||
|
now: datetime = None) -> PairLock:
|
||||||
"""
|
"""
|
||||||
Create PairLock from now to "until".
|
Create PairLock from now to "until".
|
||||||
Uses database by default, unless PairLocks.use_db is set to False,
|
Uses database by default, unless PairLocks.use_db is set to False,
|
||||||
@ -52,6 +53,7 @@ class PairLocks():
|
|||||||
PairLock.query.session.commit()
|
PairLock.query.session.commit()
|
||||||
else:
|
else:
|
||||||
PairLocks.locks.append(lock)
|
PairLocks.locks.append(lock)
|
||||||
|
return lock
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_pair_locks(pair: Optional[str], now: Optional[datetime] = None) -> List[PairLock]:
|
def get_pair_locks(pair: Optional[str], now: Optional[datetime] = None) -> List[PairLock]:
|
||||||
|
@ -6,6 +6,7 @@ from datetime import datetime, timezone
|
|||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
from freqtrade.persistence import PairLocks
|
from freqtrade.persistence import PairLocks
|
||||||
|
from freqtrade.persistence.models import PairLock
|
||||||
from freqtrade.plugins.protections import IProtection
|
from freqtrade.plugins.protections import IProtection
|
||||||
from freqtrade.resolvers import ProtectionResolver
|
from freqtrade.resolvers import ProtectionResolver
|
||||||
|
|
||||||
@ -43,30 +44,28 @@ class ProtectionManager():
|
|||||||
"""
|
"""
|
||||||
return [{p.name: p.short_desc()} for p in self._protection_handlers]
|
return [{p.name: p.short_desc()} for p in self._protection_handlers]
|
||||||
|
|
||||||
def global_stop(self, now: Optional[datetime] = None) -> bool:
|
def global_stop(self, now: Optional[datetime] = None) -> Optional[PairLock]:
|
||||||
if not now:
|
if not now:
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
result = False
|
result = None
|
||||||
for protection_handler in self._protection_handlers:
|
for protection_handler in self._protection_handlers:
|
||||||
if protection_handler.has_global_stop:
|
if protection_handler.has_global_stop:
|
||||||
result, until, reason = protection_handler.global_stop(now)
|
lock, until, reason = protection_handler.global_stop(now)
|
||||||
|
|
||||||
# Early stopping - first positive result blocks further trades
|
# Early stopping - first positive result blocks further trades
|
||||||
if result and until:
|
if lock and until:
|
||||||
if not PairLocks.is_global_lock(until):
|
if not PairLocks.is_global_lock(until):
|
||||||
PairLocks.lock_pair('*', until, reason, now=now)
|
result = PairLocks.lock_pair('*', until, reason, now=now)
|
||||||
result = True
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def stop_per_pair(self, pair, now: Optional[datetime] = None) -> bool:
|
def stop_per_pair(self, pair, now: Optional[datetime] = None) -> Optional[PairLock]:
|
||||||
if not now:
|
if not now:
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
result = False
|
result = None
|
||||||
for protection_handler in self._protection_handlers:
|
for protection_handler in self._protection_handlers:
|
||||||
if protection_handler.has_local_stop:
|
if protection_handler.has_local_stop:
|
||||||
result, until, reason = protection_handler.stop_per_pair(pair, now)
|
lock, until, reason = protection_handler.stop_per_pair(pair, now)
|
||||||
if result and until:
|
if lock and until:
|
||||||
if not PairLocks.is_pair_locked(pair, until):
|
if not PairLocks.is_pair_locked(pair, until):
|
||||||
PairLocks.lock_pair(pair, until, reason, now=now)
|
result = PairLocks.lock_pair(pair, until, reason, now=now)
|
||||||
result = True
|
|
||||||
return result
|
return result
|
||||||
|
@ -4,6 +4,7 @@ from copy import deepcopy
|
|||||||
|
|
||||||
from fastapi import APIRouter, BackgroundTasks, Depends
|
from fastapi import APIRouter, BackgroundTasks, Depends
|
||||||
|
|
||||||
|
from freqtrade.configuration.config_validation import validate_config_consistency
|
||||||
from freqtrade.enums import BacktestState
|
from freqtrade.enums import BacktestState
|
||||||
from freqtrade.exceptions import DependencyException
|
from freqtrade.exceptions import DependencyException
|
||||||
from freqtrade.rpc.api_server.api_schemas import BacktestRequest, BacktestResponse
|
from freqtrade.rpc.api_server.api_schemas import BacktestRequest, BacktestResponse
|
||||||
@ -42,38 +43,40 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac
|
|||||||
# Reload strategy
|
# Reload strategy
|
||||||
lastconfig = ApiServer._bt_last_config
|
lastconfig = ApiServer._bt_last_config
|
||||||
strat = StrategyResolver.load_strategy(btconfig)
|
strat = StrategyResolver.load_strategy(btconfig)
|
||||||
|
validate_config_consistency(btconfig)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
not ApiServer._bt
|
not ApiServer._bt
|
||||||
or lastconfig.get('timeframe') != strat.timeframe
|
or lastconfig.get('timeframe') != strat.timeframe
|
||||||
or lastconfig.get('timeframe_detail') != btconfig.get('timeframe_detail')
|
or lastconfig.get('timeframe_detail') != btconfig.get('timeframe_detail')
|
||||||
or lastconfig.get('dry_run_wallet') != btconfig.get('dry_run_wallet', 0)
|
|
||||||
or lastconfig.get('timerange') != btconfig['timerange']
|
or lastconfig.get('timerange') != btconfig['timerange']
|
||||||
):
|
):
|
||||||
from freqtrade.optimize.backtesting import Backtesting
|
from freqtrade.optimize.backtesting import Backtesting
|
||||||
ApiServer._bt = Backtesting(btconfig)
|
ApiServer._bt = Backtesting(btconfig)
|
||||||
if ApiServer._bt.timeframe_detail:
|
if ApiServer._bt.timeframe_detail:
|
||||||
ApiServer._bt.load_bt_data_detail()
|
ApiServer._bt.load_bt_data_detail()
|
||||||
|
else:
|
||||||
|
ApiServer._bt.config = btconfig
|
||||||
|
ApiServer._bt.init_backtest()
|
||||||
# Only reload data if timeframe changed.
|
# Only reload data if timeframe changed.
|
||||||
if (
|
if (
|
||||||
not ApiServer._bt_data
|
not ApiServer._bt_data
|
||||||
or not ApiServer._bt_timerange
|
or not ApiServer._bt_timerange
|
||||||
or lastconfig.get('stake_amount') != btconfig.get('stake_amount')
|
|
||||||
or lastconfig.get('enable_protections') != btconfig.get('enable_protections')
|
|
||||||
or lastconfig.get('protections') != btconfig.get('protections', [])
|
|
||||||
or lastconfig.get('timeframe') != strat.timeframe
|
or lastconfig.get('timeframe') != strat.timeframe
|
||||||
|
or lastconfig.get('timerange') != btconfig['timerange']
|
||||||
):
|
):
|
||||||
lastconfig['timerange'] = btconfig['timerange']
|
|
||||||
lastconfig['protections'] = btconfig.get('protections', [])
|
|
||||||
lastconfig['enable_protections'] = btconfig.get('enable_protections')
|
|
||||||
lastconfig['dry_run_wallet'] = btconfig.get('dry_run_wallet')
|
|
||||||
lastconfig['timeframe'] = strat.timeframe
|
|
||||||
ApiServer._bt_data, ApiServer._bt_timerange = ApiServer._bt.load_bt_data()
|
ApiServer._bt_data, ApiServer._bt_timerange = ApiServer._bt.load_bt_data()
|
||||||
|
|
||||||
|
lastconfig['timerange'] = btconfig['timerange']
|
||||||
|
lastconfig['timeframe'] = strat.timeframe
|
||||||
|
lastconfig['protections'] = btconfig.get('protections', [])
|
||||||
|
lastconfig['enable_protections'] = btconfig.get('enable_protections')
|
||||||
|
lastconfig['dry_run_wallet'] = btconfig.get('dry_run_wallet')
|
||||||
|
|
||||||
ApiServer._bt.abort = False
|
ApiServer._bt.abort = False
|
||||||
min_date, max_date = ApiServer._bt.backtest_one_strategy(
|
min_date, max_date = ApiServer._bt.backtest_one_strategy(
|
||||||
strat, ApiServer._bt_data, ApiServer._bt_timerange)
|
strat, ApiServer._bt_data, ApiServer._bt_timerange)
|
||||||
|
|
||||||
ApiServer._bt.results = generate_backtest_stats(
|
ApiServer._bt.results = generate_backtest_stats(
|
||||||
ApiServer._bt_data, ApiServer._bt.all_results,
|
ApiServer._bt_data, ApiServer._bt.all_results,
|
||||||
min_date=min_date, max_date=max_date)
|
min_date=min_date, max_date=max_date)
|
||||||
|
@ -260,6 +260,50 @@ class Telegram(RPCHandler):
|
|||||||
|
|
||||||
return message
|
return message
|
||||||
|
|
||||||
|
def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> str:
|
||||||
|
|
||||||
|
if msg_type == RPCMessageType.BUY:
|
||||||
|
message = self._format_buy_msg(msg)
|
||||||
|
|
||||||
|
elif msg_type in (RPCMessageType.BUY_CANCEL, RPCMessageType.SELL_CANCEL):
|
||||||
|
msg['message_side'] = 'buy' if msg_type == RPCMessageType.BUY_CANCEL else 'sell'
|
||||||
|
message = ("\N{WARNING SIGN} *{exchange}:* "
|
||||||
|
"Cancelling open {message_side} Order for {pair} (#{trade_id}). "
|
||||||
|
"Reason: {reason}.".format(**msg))
|
||||||
|
|
||||||
|
elif msg_type == RPCMessageType.BUY_FILL:
|
||||||
|
message = ("\N{LARGE CIRCLE} *{exchange}:* "
|
||||||
|
"Buy order for {pair} (#{trade_id}) filled "
|
||||||
|
"for {open_rate}.".format(**msg))
|
||||||
|
elif msg_type == RPCMessageType.SELL_FILL:
|
||||||
|
message = ("\N{LARGE CIRCLE} *{exchange}:* "
|
||||||
|
"Sell order for {pair} (#{trade_id}) filled "
|
||||||
|
"for {close_rate}.".format(**msg))
|
||||||
|
elif msg_type == RPCMessageType.SELL:
|
||||||
|
message = self._format_sell_msg(msg)
|
||||||
|
elif msg_type == RPCMessageType.PROTECTION_TRIGGER:
|
||||||
|
message = (
|
||||||
|
"*Protection* triggered due to {reason}. "
|
||||||
|
"`{pair}` will be locked until `{lock_end_time}`."
|
||||||
|
).format(**msg)
|
||||||
|
elif msg_type == RPCMessageType.PROTECTION_TRIGGER_GLOBAL:
|
||||||
|
message = (
|
||||||
|
"*Protection* triggered due to {reason}. "
|
||||||
|
"*All pairs* will be locked until `{lock_end_time}`."
|
||||||
|
).format(**msg)
|
||||||
|
elif msg_type == RPCMessageType.STATUS:
|
||||||
|
message = '*Status:* `{status}`'.format(**msg)
|
||||||
|
|
||||||
|
elif msg_type == RPCMessageType.WARNING:
|
||||||
|
message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg)
|
||||||
|
|
||||||
|
elif msg_type == RPCMessageType.STARTUP:
|
||||||
|
message = '{status}'.format(**msg)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise NotImplementedError('Unknown message type: {}'.format(msg_type))
|
||||||
|
return message
|
||||||
|
|
||||||
def send_msg(self, msg: Dict[str, Any]) -> None:
|
def send_msg(self, msg: Dict[str, Any]) -> None:
|
||||||
""" Send a message to telegram channel """
|
""" Send a message to telegram channel """
|
||||||
|
|
||||||
@ -284,37 +328,7 @@ class Telegram(RPCHandler):
|
|||||||
# Notification disabled
|
# Notification disabled
|
||||||
return
|
return
|
||||||
|
|
||||||
if msg_type == RPCMessageType.BUY:
|
message = self.compose_message(msg, msg_type)
|
||||||
message = self._format_buy_msg(msg)
|
|
||||||
|
|
||||||
elif msg_type in (RPCMessageType.BUY_CANCEL, RPCMessageType.SELL_CANCEL):
|
|
||||||
msg['message_side'] = 'buy' if msg_type == RPCMessageType.BUY_CANCEL else 'sell'
|
|
||||||
message = ("\N{WARNING SIGN} *{exchange}:* "
|
|
||||||
"Cancelling open {message_side} Order for {pair} (#{trade_id}). "
|
|
||||||
"Reason: {reason}.".format(**msg))
|
|
||||||
|
|
||||||
elif msg_type == RPCMessageType.BUY_FILL:
|
|
||||||
message = ("\N{LARGE CIRCLE} *{exchange}:* "
|
|
||||||
"Buy order for {pair} (#{trade_id}) filled "
|
|
||||||
"for {open_rate}.".format(**msg))
|
|
||||||
elif msg_type == RPCMessageType.SELL_FILL:
|
|
||||||
message = ("\N{LARGE CIRCLE} *{exchange}:* "
|
|
||||||
"Sell order for {pair} (#{trade_id}) filled "
|
|
||||||
"for {close_rate}.".format(**msg))
|
|
||||||
elif msg_type == RPCMessageType.SELL:
|
|
||||||
message = self._format_sell_msg(msg)
|
|
||||||
|
|
||||||
elif msg_type == RPCMessageType.STATUS:
|
|
||||||
message = '*Status:* `{status}`'.format(**msg)
|
|
||||||
|
|
||||||
elif msg_type == RPCMessageType.WARNING:
|
|
||||||
message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg)
|
|
||||||
|
|
||||||
elif msg_type == RPCMessageType.STARTUP:
|
|
||||||
message = '{status}'.format(**msg)
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise NotImplementedError('Unknown message type: {}'.format(msg_type))
|
|
||||||
|
|
||||||
self._send_msg(message, disable_notification=(noti == 'silent'))
|
self._send_msg(message, disable_notification=(noti == 'silent'))
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
"secret": "{{ exchange_secret }}",
|
"secret": "{{ exchange_secret }}",
|
||||||
"password": "{{ exchange_key_password }}",
|
"password": "{{ exchange_key_password }}",
|
||||||
"ccxt_config": {
|
"ccxt_config": {
|
||||||
"enableRateLimit": true
|
"enableRateLimit": true,
|
||||||
"rateLimit": 200
|
"rateLimit": 200
|
||||||
},
|
},
|
||||||
"ccxt_async_config": {
|
"ccxt_async_config": {
|
||||||
|
@ -18,10 +18,10 @@ isort==5.9.3
|
|||||||
time-machine==2.4.0
|
time-machine==2.4.0
|
||||||
|
|
||||||
# Convert jupyter notebooks to markdown documents
|
# Convert jupyter notebooks to markdown documents
|
||||||
nbconvert==6.1.0
|
nbconvert==6.2.0
|
||||||
|
|
||||||
# mypy types
|
# mypy types
|
||||||
types-cachetools==4.2.0
|
types-cachetools==4.2.0
|
||||||
types-filelock==0.1.5
|
types-filelock==0.1.5
|
||||||
types-requests==2.25.6
|
types-requests==2.25.9
|
||||||
types-tabulate==0.8.2
|
types-tabulate==0.8.2
|
||||||
|
@ -8,4 +8,4 @@ scikit-optimize==0.8.1
|
|||||||
filelock==3.0.12
|
filelock==3.0.12
|
||||||
joblib==1.0.1
|
joblib==1.0.1
|
||||||
psutil==5.8.0
|
psutil==5.8.0
|
||||||
progressbar2==3.53.2
|
progressbar2==3.53.3
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
numpy==1.21.2
|
numpy==1.21.2
|
||||||
pandas==1.3.3
|
pandas==1.3.3
|
||||||
|
pandas-ta==0.3.14b
|
||||||
|
|
||||||
ccxt==1.56.30
|
ccxt==1.57.3
|
||||||
# Pin cryptography for now due to rust build errors with piwheels
|
# Pin cryptography for now due to rust build errors with piwheels
|
||||||
cryptography==3.4.8
|
cryptography==3.4.8
|
||||||
aiohttp==3.7.4.post0
|
aiohttp==3.7.4.post0
|
||||||
SQLAlchemy==1.4.23
|
SQLAlchemy==1.4.25
|
||||||
python-telegram-bot==13.7
|
python-telegram-bot==13.7
|
||||||
arrow==1.1.1
|
arrow==1.1.1
|
||||||
cachetools==4.2.2
|
cachetools==4.2.2
|
||||||
requests==2.26.0
|
requests==2.26.0
|
||||||
urllib3==1.26.6
|
urllib3==1.26.7
|
||||||
wrapt==1.12.1
|
wrapt==1.12.1
|
||||||
jsonschema==3.2.0
|
jsonschema==3.2.0
|
||||||
TA-Lib==0.4.21
|
TA-Lib==0.4.21
|
||||||
|
@ -312,7 +312,7 @@ class FtRestClient():
|
|||||||
:param limit: Limit result to the last n candles.
|
:param limit: Limit result to the last n candles.
|
||||||
:return: json object
|
:return: json object
|
||||||
"""
|
"""
|
||||||
return self._get("available_pairs", params={
|
return self._get("pair_candles", params={
|
||||||
"pair": pair,
|
"pair": pair,
|
||||||
"timeframe": timeframe,
|
"timeframe": timeframe,
|
||||||
"limit": limit,
|
"limit": limit,
|
||||||
|
1
setup.py
1
setup.py
@ -54,6 +54,7 @@ setup(
|
|||||||
'wrapt',
|
'wrapt',
|
||||||
'jsonschema',
|
'jsonschema',
|
||||||
'TA-Lib',
|
'TA-Lib',
|
||||||
|
'pandas-ta',
|
||||||
'technical',
|
'technical',
|
||||||
'tabulate',
|
'tabulate',
|
||||||
'pycoingecko',
|
'pycoingecko',
|
||||||
|
@ -8,12 +8,12 @@ from zipfile import ZipFile
|
|||||||
import arrow
|
import arrow
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from freqtrade.commands import (start_convert_data, start_create_userdir, start_download_data,
|
from freqtrade.commands import (start_convert_data, start_convert_trades, start_create_userdir,
|
||||||
start_hyperopt_list, start_hyperopt_show, start_install_ui,
|
start_download_data, start_hyperopt_list, start_hyperopt_show,
|
||||||
start_list_data, start_list_exchanges, start_list_markets,
|
start_install_ui, start_list_data, start_list_exchanges,
|
||||||
start_list_strategies, start_list_timeframes, start_new_strategy,
|
start_list_markets, start_list_strategies, start_list_timeframes,
|
||||||
start_show_trades, start_test_pairlist, start_trading,
|
start_new_strategy, start_show_trades, start_test_pairlist,
|
||||||
start_webserver)
|
start_trading, start_webserver)
|
||||||
from freqtrade.commands.deploy_commands import (clean_ui_subdir, download_and_install_ui,
|
from freqtrade.commands.deploy_commands import (clean_ui_subdir, download_and_install_ui,
|
||||||
get_ui_download_url, read_ui_version)
|
get_ui_download_url, read_ui_version)
|
||||||
from freqtrade.configuration import setup_utils_configuration
|
from freqtrade.configuration import setup_utils_configuration
|
||||||
@ -208,11 +208,10 @@ def test_list_timeframes(mocker, capsys):
|
|||||||
assert re.search(r"^1d$", captured.out, re.MULTILINE)
|
assert re.search(r"^1d$", captured.out, re.MULTILINE)
|
||||||
|
|
||||||
|
|
||||||
def test_list_markets(mocker, markets, capsys):
|
def test_list_markets(mocker, markets_static, capsys):
|
||||||
|
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.markets = markets
|
patch_exchange(mocker, api_mock=api_mock, id='bittrex', mock_markets=markets_static)
|
||||||
patch_exchange(mocker, api_mock=api_mock, id='bittrex')
|
|
||||||
|
|
||||||
# Test with no --config
|
# Test with no --config
|
||||||
args = [
|
args = [
|
||||||
@ -237,7 +236,7 @@ def test_list_markets(mocker, markets, capsys):
|
|||||||
"TKN/BTC, XLTCUSDT, XRP/BTC.\n"
|
"TKN/BTC, XLTCUSDT, XRP/BTC.\n"
|
||||||
in captured.out)
|
in captured.out)
|
||||||
|
|
||||||
patch_exchange(mocker, api_mock=api_mock, id="binance")
|
patch_exchange(mocker, api_mock=api_mock, id="binance", mock_markets=markets_static)
|
||||||
# Test with --exchange
|
# Test with --exchange
|
||||||
args = [
|
args = [
|
||||||
"list-markets",
|
"list-markets",
|
||||||
@ -250,7 +249,7 @@ def test_list_markets(mocker, markets, capsys):
|
|||||||
assert re.match("\nExchange Binance has 10 active markets:\n",
|
assert re.match("\nExchange Binance has 10 active markets:\n",
|
||||||
captured.out)
|
captured.out)
|
||||||
|
|
||||||
patch_exchange(mocker, api_mock=api_mock, id="bittrex")
|
patch_exchange(mocker, api_mock=api_mock, id="bittrex", mock_markets=markets_static)
|
||||||
# Test with --all: all markets
|
# Test with --all: all markets
|
||||||
args = [
|
args = [
|
||||||
"list-markets", "--all",
|
"list-markets", "--all",
|
||||||
@ -760,6 +759,22 @@ def test_download_data_trades(mocker, caplog):
|
|||||||
assert convert_mock.call_count == 1
|
assert convert_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_start_convert_trades(mocker, caplog):
|
||||||
|
convert_mock = mocker.patch('freqtrade.commands.data_commands.convert_trades_to_ohlcv',
|
||||||
|
MagicMock(return_value=[]))
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})
|
||||||
|
)
|
||||||
|
args = [
|
||||||
|
"trades-to-ohlcv",
|
||||||
|
"--exchange", "kraken",
|
||||||
|
"--pairs", "ETH/BTC", "XRP/BTC",
|
||||||
|
]
|
||||||
|
start_convert_trades(get_args(args))
|
||||||
|
assert convert_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_start_list_strategies(mocker, caplog, capsys):
|
def test_start_list_strategies(mocker, caplog, capsys):
|
||||||
|
|
||||||
args = [
|
args = [
|
||||||
|
@ -110,8 +110,10 @@ def patch_exchange(
|
|||||||
mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2))
|
mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2))
|
||||||
|
|
||||||
if mock_markets:
|
if mock_markets:
|
||||||
|
if isinstance(mock_markets, bool):
|
||||||
|
mock_markets = get_markets()
|
||||||
mocker.patch('freqtrade.exchange.Exchange.markets',
|
mocker.patch('freqtrade.exchange.Exchange.markets',
|
||||||
PropertyMock(return_value=get_markets()))
|
PropertyMock(return_value=mock_markets))
|
||||||
|
|
||||||
if mock_supported_modes:
|
if mock_supported_modes:
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
@ -470,6 +472,8 @@ def markets():
|
|||||||
|
|
||||||
|
|
||||||
def get_markets():
|
def get_markets():
|
||||||
|
# See get_markets_static() for immutable markets and do not modify them unless absolutely
|
||||||
|
# necessary!
|
||||||
return {
|
return {
|
||||||
'ETH/BTC': {
|
'ETH/BTC': {
|
||||||
'id': 'ethbtc',
|
'id': 'ethbtc',
|
||||||
@ -786,11 +790,22 @@ def get_markets():
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def shitcoinmarkets(markets):
|
def markets_static():
|
||||||
|
# These markets are used in some tests that would need adaptation should anything change in
|
||||||
|
# market list. Do not modify this list without a good reason! Do not modify market parameters
|
||||||
|
# of listed pairs in get_markets() without a good reason either!
|
||||||
|
static_markets = ['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD',
|
||||||
|
'LTC/USDT', 'NEO/BTC', 'TKN/BTC', 'XLTCUSDT', 'XRP/BTC']
|
||||||
|
all_markets = get_markets()
|
||||||
|
return {m: all_markets[m] for m in static_markets}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def shitcoinmarkets(markets_static):
|
||||||
"""
|
"""
|
||||||
Fixture with shitcoin markets - used to test filters in pairlists
|
Fixture with shitcoin markets - used to test filters in pairlists
|
||||||
"""
|
"""
|
||||||
shitmarkets = deepcopy(markets)
|
shitmarkets = deepcopy(markets_static)
|
||||||
shitmarkets.update({
|
shitmarkets.update({
|
||||||
'HOT/BTC': {
|
'HOT/BTC': {
|
||||||
'id': 'HOTBTC',
|
'id': 'HOTBTC',
|
||||||
@ -1796,14 +1811,6 @@ def trades_for_order2():
|
|||||||
'fee': {'cost': 0.004, 'currency': 'LTC'}}]
|
'fee': {'cost': 0.004, 'currency': 'LTC'}}]
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
|
||||||
def trades_for_order3(trades_for_order2):
|
|
||||||
# Different fee currencies for each trade
|
|
||||||
trades_for_order = deepcopy(trades_for_order2)
|
|
||||||
trades_for_order[0]['fee'] = {'cost': 0.02, 'currency': 'BNB'}
|
|
||||||
return trades_for_order
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def buy_order_fee():
|
def buy_order_fee():
|
||||||
return {
|
return {
|
||||||
|
@ -2806,7 +2806,7 @@ def test_get_valid_pair_combination(default_conf, mocker, markets):
|
|||||||
(['LTC'], ['NONEXISTENT'], False, False,
|
(['LTC'], ['NONEXISTENT'], False, False,
|
||||||
[]),
|
[]),
|
||||||
])
|
])
|
||||||
def test_get_markets(default_conf, mocker, markets,
|
def test_get_markets(default_conf, mocker, markets_static,
|
||||||
base_currencies, quote_currencies, pairs_only, active_only,
|
base_currencies, quote_currencies, pairs_only, active_only,
|
||||||
expected_keys):
|
expected_keys):
|
||||||
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||||
@ -2814,7 +2814,7 @@ def test_get_markets(default_conf, mocker, markets,
|
|||||||
_load_async_markets=MagicMock(),
|
_load_async_markets=MagicMock(),
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
validate_timeframes=MagicMock(),
|
validate_timeframes=MagicMock(),
|
||||||
markets=PropertyMock(return_value=markets))
|
markets=PropertyMock(return_value=markets_static))
|
||||||
ex = Exchange(default_conf)
|
ex = Exchange(default_conf)
|
||||||
pairs = ex.get_markets(base_currencies, quote_currencies, pairs_only, active_only)
|
pairs = ex.get_markets(base_currencies, quote_currencies, pairs_only, active_only)
|
||||||
assert sorted(pairs.keys()) == sorted(expected_keys)
|
assert sorted(pairs.keys()) == sorted(expected_keys)
|
||||||
|
@ -131,9 +131,9 @@ def test_load_pairlist_noexist(mocker, markets, default_conf):
|
|||||||
default_conf, {}, 1)
|
default_conf, {}, 1)
|
||||||
|
|
||||||
|
|
||||||
def test_load_pairlist_verify_multi(mocker, markets, default_conf):
|
def test_load_pairlist_verify_multi(mocker, markets_static, default_conf):
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
|
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets_static))
|
||||||
plm = PairListManager(freqtrade.exchange, default_conf)
|
plm = PairListManager(freqtrade.exchange, default_conf)
|
||||||
# Call different versions one after the other, should always consider what was passed in
|
# Call different versions one after the other, should always consider what was passed in
|
||||||
# and have no side-effects (therefore the same check multiple times)
|
# and have no side-effects (therefore the same check multiple times)
|
||||||
|
@ -125,7 +125,7 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog):
|
|||||||
# Test 5m after lock-period - this should try and relock the pair, but end-time
|
# Test 5m after lock-period - this should try and relock the pair, but end-time
|
||||||
# should be the previous end-time
|
# should be the previous end-time
|
||||||
end_time = PairLocks.get_pair_longest_lock('*').lock_end_time + timedelta(minutes=5)
|
end_time = PairLocks.get_pair_longest_lock('*').lock_end_time + timedelta(minutes=5)
|
||||||
assert freqtrade.protections.global_stop(end_time)
|
freqtrade.protections.global_stop(end_time)
|
||||||
assert not PairLocks.is_global_lock(end_time)
|
assert not PairLocks.is_global_lock(end_time)
|
||||||
|
|
||||||
|
|
||||||
@ -182,7 +182,7 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair
|
|||||||
min_ago_open=180, min_ago_close=30, profit_rate=0.9,
|
min_ago_open=180, min_ago_close=30, profit_rate=0.9,
|
||||||
))
|
))
|
||||||
|
|
||||||
assert freqtrade.protections.stop_per_pair(pair)
|
freqtrade.protections.stop_per_pair(pair)
|
||||||
assert freqtrade.protections.global_stop() != only_per_pair
|
assert freqtrade.protections.global_stop() != only_per_pair
|
||||||
assert PairLocks.is_pair_locked(pair)
|
assert PairLocks.is_pair_locked(pair)
|
||||||
assert PairLocks.is_global_lock() != only_per_pair
|
assert PairLocks.is_global_lock() != only_per_pair
|
||||||
|
@ -1317,6 +1317,34 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None:
|
|||||||
'Reason: cancelled due to timeout.')
|
'Reason: cancelled due to timeout.')
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_msg_protection_notification(default_conf, mocker, time_machine) -> None:
|
||||||
|
|
||||||
|
default_conf['telegram']['notification_settings']['protection_trigger'] = 'on'
|
||||||
|
|
||||||
|
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||||
|
time_machine.move_to("2021-09-01 05:00:00 +00:00")
|
||||||
|
lock = PairLocks.lock_pair('ETH/BTC', arrow.utcnow().shift(minutes=6).datetime, 'randreason')
|
||||||
|
msg = {
|
||||||
|
'type': RPCMessageType.PROTECTION_TRIGGER,
|
||||||
|
}
|
||||||
|
msg.update(lock.to_json())
|
||||||
|
telegram.send_msg(msg)
|
||||||
|
assert (msg_mock.call_args[0][0] == "*Protection* triggered due to randreason. "
|
||||||
|
"`ETH/BTC` will be locked until `2021-09-01 05:10:00`.")
|
||||||
|
|
||||||
|
msg_mock.reset_mock()
|
||||||
|
# Test global protection
|
||||||
|
|
||||||
|
msg = {
|
||||||
|
'type': RPCMessageType.PROTECTION_TRIGGER_GLOBAL,
|
||||||
|
}
|
||||||
|
lock = PairLocks.lock_pair('*', arrow.utcnow().shift(minutes=100).datetime, 'randreason')
|
||||||
|
msg.update(lock.to_json())
|
||||||
|
telegram.send_msg(msg)
|
||||||
|
assert (msg_mock.call_args[0][0] == "*Protection* triggered due to randreason. "
|
||||||
|
"*All pairs* will be locked until `2021-09-01 06:45:00`.")
|
||||||
|
|
||||||
|
|
||||||
def test_send_msg_buy_fill_notification(default_conf, mocker) -> None:
|
def test_send_msg_buy_fill_notification(default_conf, mocker) -> None:
|
||||||
|
|
||||||
default_conf['telegram']['notification_settings']['buy_fill'] = 'on'
|
default_conf['telegram']['notification_settings']['buy_fill'] = 'on'
|
||||||
|
@ -446,6 +446,29 @@ def test_enter_positions_global_pairlock(default_conf, ticker, limit_buy_order,
|
|||||||
assert log_has_re(message, caplog)
|
assert log_has_re(message, caplog)
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_protections(mocker, default_conf, fee):
|
||||||
|
default_conf['protections'] = [
|
||||||
|
{"method": "CooldownPeriod", "stop_duration": 60},
|
||||||
|
{
|
||||||
|
"method": "StoplossGuard",
|
||||||
|
"lookback_period_candles": 24,
|
||||||
|
"trade_limit": 4,
|
||||||
|
"stop_duration_candles": 4,
|
||||||
|
"only_per_pair": False
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
freqtrade.protections._protection_handlers[1].global_stop = MagicMock(
|
||||||
|
return_value=(True, arrow.utcnow().shift(hours=1).datetime, "asdf"))
|
||||||
|
create_mock_trades(fee)
|
||||||
|
freqtrade.handle_protections('ETC/BTC')
|
||||||
|
send_msg_mock = freqtrade.rpc.send_msg
|
||||||
|
assert send_msg_mock.call_count == 2
|
||||||
|
assert send_msg_mock.call_args_list[0][0][0]['type'] == RPCMessageType.PROTECTION_TRIGGER
|
||||||
|
assert send_msg_mock.call_args_list[1][0][0]['type'] == RPCMessageType.PROTECTION_TRIGGER_GLOBAL
|
||||||
|
|
||||||
|
|
||||||
def test_create_trade_no_signal(default_conf, fee, mocker) -> None:
|
def test_create_trade_no_signal(default_conf, fee, mocker) -> None:
|
||||||
default_conf['dry_run'] = True
|
default_conf['dry_run'] = True
|
||||||
|
|
||||||
@ -3723,6 +3746,7 @@ def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, fe
|
|||||||
)
|
)
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
|
||||||
|
caplog.clear()
|
||||||
# Amount is reduced by "fee"
|
# Amount is reduced by "fee"
|
||||||
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.001)
|
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.001)
|
||||||
assert log_has(
|
assert log_has(
|
||||||
@ -3782,8 +3806,35 @@ def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker, f
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, fee, mocker):
|
@pytest.mark.parametrize(
|
||||||
trades_for_order[0]['fee']['currency'] = 'ETH'
|
'fee_par,fee_reduction_amount,use_ticker_rate,expected_log', [
|
||||||
|
# basic, amount does not change
|
||||||
|
({'cost': 0.008, 'currency': 'ETH'}, 0, False, None),
|
||||||
|
# no currency in fee
|
||||||
|
({'cost': 0.004, 'currency': None}, 0, True, None),
|
||||||
|
# BNB no rate
|
||||||
|
({'cost': 0.00094518, 'currency': 'BNB'}, 0, True, (
|
||||||
|
'Fee for Trade Trade(id=None, pair=LTC/ETH, amount=8.00000000, is_short=False, '
|
||||||
|
'leverage=1.0, open_rate=0.24544100, open_since=closed) [buy]: 0.00094518 BNB -'
|
||||||
|
' rate: None'
|
||||||
|
)),
|
||||||
|
# from order
|
||||||
|
({'cost': 0.004, 'currency': 'LTC'}, 0.004, False, (
|
||||||
|
'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
|
||||||
|
'is_short=False, leverage=1.0, open_rate=0.24544100, open_since=closed) (from'
|
||||||
|
' 8.0 to 7.996).'
|
||||||
|
)),
|
||||||
|
# invalid, no currency in from fee dict
|
||||||
|
({'cost': 0.008, 'currency': None}, 0, True, None),
|
||||||
|
])
|
||||||
|
def test_get_real_amount(
|
||||||
|
default_conf, trades_for_order, buy_order_fee, fee, mocker, caplog,
|
||||||
|
fee_par, fee_reduction_amount, use_ticker_rate, expected_log
|
||||||
|
):
|
||||||
|
|
||||||
|
buy_order = deepcopy(buy_order_fee)
|
||||||
|
buy_order['fee'] = fee_par
|
||||||
|
trades_for_order[0]['fee'] = fee_par
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
|
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
|
||||||
amount = sum(x['amount'] for x in trades_for_order)
|
amount = sum(x['amount'] for x in trades_for_order)
|
||||||
@ -3798,19 +3849,38 @@ def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, fe
|
|||||||
)
|
)
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
|
||||||
# Amount does not change
|
if not use_ticker_rate:
|
||||||
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
|
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', side_effect=ExchangeError)
|
||||||
|
|
||||||
|
caplog.clear()
|
||||||
|
assert freqtrade.get_real_amount(trade, buy_order) == amount - fee_reduction_amount
|
||||||
|
|
||||||
|
if expected_log:
|
||||||
|
assert log_has(expected_log, caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_get_real_amount_no_currency_in_fee(default_conf, trades_for_order, buy_order_fee,
|
@pytest.mark.parametrize(
|
||||||
fee, mocker):
|
'fee_cost, fee_currency, fee_reduction_amount, expected_fee, expected_log_amount', [
|
||||||
|
# basic, amount is reduced by fee
|
||||||
|
(None, None, 0.001, 0.001, 7.992),
|
||||||
|
# different fee currency on both trades, fee is average of both trade's fee
|
||||||
|
(0.02, 'BNB', 0.0005, 0.001518575, 7.996),
|
||||||
|
])
|
||||||
|
def test_get_real_amount_multi(
|
||||||
|
default_conf, trades_for_order2, buy_order_fee, caplog, fee, mocker, markets,
|
||||||
|
fee_cost, fee_currency, fee_reduction_amount, expected_fee, expected_log_amount,
|
||||||
|
):
|
||||||
|
|
||||||
limit_buy_order = deepcopy(buy_order_fee)
|
trades_for_order = deepcopy(trades_for_order2)
|
||||||
limit_buy_order['fee'] = {'cost': 0.004, 'currency': None}
|
if fee_cost:
|
||||||
trades_for_order[0]['fee']['currency'] = None
|
trades_for_order[0]['fee']['cost'] = fee_cost
|
||||||
|
if fee_currency:
|
||||||
|
trades_for_order[0]['fee']['currency'] = fee_currency
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
|
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
|
||||||
amount = sum(x['amount'] for x in trades_for_order)
|
amount = float(sum(x['amount'] for x in trades_for_order))
|
||||||
|
default_conf['stake_currency'] = "ETH"
|
||||||
|
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='LTC/ETH',
|
pair='LTC/ETH',
|
||||||
amount=amount,
|
amount=amount,
|
||||||
@ -3820,78 +3890,7 @@ def test_get_real_amount_no_currency_in_fee(default_conf, trades_for_order, buy_
|
|||||||
open_rate=0.245441,
|
open_rate=0.245441,
|
||||||
open_order_id="123456"
|
open_order_id="123456"
|
||||||
)
|
)
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
|
||||||
|
|
||||||
# Amount does not change
|
|
||||||
assert freqtrade.get_real_amount(trade, limit_buy_order) == amount
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_real_amount_BNB(default_conf, trades_for_order, buy_order_fee, fee, mocker):
|
|
||||||
trades_for_order[0]['fee']['currency'] = 'BNB'
|
|
||||||
trades_for_order[0]['fee']['cost'] = 0.00094518
|
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
|
|
||||||
amount = sum(x['amount'] for x in trades_for_order)
|
|
||||||
trade = Trade(
|
|
||||||
pair='LTC/ETH',
|
|
||||||
amount=amount,
|
|
||||||
exchange='binance',
|
|
||||||
fee_open=fee.return_value,
|
|
||||||
fee_close=fee.return_value,
|
|
||||||
open_rate=0.245441,
|
|
||||||
open_order_id="123456"
|
|
||||||
)
|
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
|
||||||
|
|
||||||
# Amount does not change
|
|
||||||
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_real_amount_multi(default_conf, trades_for_order2, buy_order_fee, caplog, fee, mocker):
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order2)
|
|
||||||
amount = float(sum(x['amount'] for x in trades_for_order2))
|
|
||||||
trade = Trade(
|
|
||||||
pair='LTC/ETH',
|
|
||||||
amount=amount,
|
|
||||||
exchange='binance',
|
|
||||||
fee_open=fee.return_value,
|
|
||||||
fee_close=fee.return_value,
|
|
||||||
open_rate=0.245441,
|
|
||||||
open_order_id="123456"
|
|
||||||
)
|
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
|
||||||
|
|
||||||
# Amount is reduced by "fee"
|
|
||||||
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.001)
|
|
||||||
assert log_has(
|
|
||||||
'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, is_short=False,'
|
|
||||||
' leverage=1.0, open_rate=0.24544100, open_since=closed) (from 8.0 to 7.992).',
|
|
||||||
caplog
|
|
||||||
)
|
|
||||||
|
|
||||||
assert trade.fee_open == 0.001
|
|
||||||
assert trade.fee_close == 0.001
|
|
||||||
assert trade.fee_open_cost is not None
|
|
||||||
assert trade.fee_open_currency is not None
|
|
||||||
assert trade.fee_close_cost is None
|
|
||||||
assert trade.fee_close_currency is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_real_amount_multi2(default_conf, trades_for_order3, buy_order_fee, caplog, fee,
|
|
||||||
mocker, markets):
|
|
||||||
# Different fee currency on both trades
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order3)
|
|
||||||
amount = float(sum(x['amount'] for x in trades_for_order3))
|
|
||||||
default_conf['stake_currency'] = 'ETH'
|
|
||||||
trade = Trade(
|
|
||||||
pair='LTC/ETH',
|
|
||||||
amount=amount,
|
|
||||||
exchange='binance',
|
|
||||||
fee_open=fee.return_value,
|
|
||||||
fee_close=fee.return_value,
|
|
||||||
open_rate=0.245441,
|
|
||||||
open_order_id="123456"
|
|
||||||
)
|
|
||||||
# Fake markets entry to enable fee parsing
|
# Fake markets entry to enable fee parsing
|
||||||
markets['BNB/ETH'] = markets['ETH/BTC']
|
markets['BNB/ETH'] = markets['ETH/BTC']
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
@ -3900,50 +3899,25 @@ def test_get_real_amount_multi2(default_conf, trades_for_order3, buy_order_fee,
|
|||||||
return_value={'ask': 0.19, 'last': 0.2})
|
return_value={'ask': 0.19, 'last': 0.2})
|
||||||
|
|
||||||
# Amount is reduced by "fee"
|
# Amount is reduced by "fee"
|
||||||
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.0005)
|
expected_amount = amount - (amount * fee_reduction_amount)
|
||||||
|
assert freqtrade.get_real_amount(trade, buy_order_fee) == expected_amount
|
||||||
assert log_has(
|
assert log_has(
|
||||||
'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, is_short=False,'
|
(
|
||||||
' leverage=1.0, open_rate=0.24544100, open_since=closed) (from 8.0 to 7.996).',
|
'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
|
||||||
|
'is_short=False, leverage=1.0, open_rate=0.24544100, open_since=closed) '
|
||||||
|
f'(from 8.0 to {expected_log_amount}).'
|
||||||
|
),
|
||||||
caplog
|
caplog
|
||||||
)
|
)
|
||||||
# Overall fee is average of both trade's fee
|
|
||||||
assert trade.fee_open == 0.001518575
|
assert trade.fee_open == expected_fee
|
||||||
|
assert trade.fee_close == expected_fee
|
||||||
assert trade.fee_open_cost is not None
|
assert trade.fee_open_cost is not None
|
||||||
assert trade.fee_open_currency is not None
|
assert trade.fee_open_currency is not None
|
||||||
assert trade.fee_close_cost is None
|
assert trade.fee_close_cost is None
|
||||||
assert trade.fee_close_currency is None
|
assert trade.fee_close_currency is None
|
||||||
|
|
||||||
|
|
||||||
def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee, fee,
|
|
||||||
caplog, mocker):
|
|
||||||
limit_buy_order = deepcopy(buy_order_fee)
|
|
||||||
limit_buy_order['fee'] = {'cost': 0.004, 'currency': 'LTC'}
|
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order',
|
|
||||||
return_value=[trades_for_order])
|
|
||||||
amount = float(sum(x['amount'] for x in trades_for_order))
|
|
||||||
trade = Trade(
|
|
||||||
pair='LTC/ETH',
|
|
||||||
amount=amount,
|
|
||||||
exchange='binance',
|
|
||||||
fee_open=fee.return_value,
|
|
||||||
fee_close=fee.return_value,
|
|
||||||
open_rate=0.245441,
|
|
||||||
open_order_id="123456"
|
|
||||||
)
|
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
|
||||||
# Ticker rate cannot be found for this to work.
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', side_effect=ExchangeError)
|
|
||||||
|
|
||||||
# Amount is reduced by "fee"
|
|
||||||
assert freqtrade.get_real_amount(trade, limit_buy_order) == amount - 0.004
|
|
||||||
assert log_has(
|
|
||||||
'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, is_short=False,'
|
|
||||||
' leverage=1.0, open_rate=0.24544100, open_since=closed) (from 8.0 to 7.996).',
|
|
||||||
caplog
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_real_amount_invalid_order(default_conf, trades_for_order, buy_order_fee, fee, mocker):
|
def test_get_real_amount_invalid_order(default_conf, trades_for_order, buy_order_fee, fee, mocker):
|
||||||
limit_buy_order = deepcopy(buy_order_fee)
|
limit_buy_order = deepcopy(buy_order_fee)
|
||||||
limit_buy_order['fee'] = {'cost': 0.004}
|
limit_buy_order['fee'] = {'cost': 0.004}
|
||||||
@ -4011,27 +3985,6 @@ def test_get_real_amount_wrong_amount_rounding(default_conf, trades_for_order, b
|
|||||||
abs_tol=MATH_CLOSE_PREC,)
|
abs_tol=MATH_CLOSE_PREC,)
|
||||||
|
|
||||||
|
|
||||||
def test_get_real_amount_invalid(default_conf, trades_for_order, buy_order_fee, fee, mocker):
|
|
||||||
# Remove "Currency" from fee dict
|
|
||||||
trades_for_order[0]['fee'] = {'cost': 0.008}
|
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
|
|
||||||
amount = sum(x['amount'] for x in trades_for_order)
|
|
||||||
trade = Trade(
|
|
||||||
pair='LTC/ETH',
|
|
||||||
amount=amount,
|
|
||||||
exchange='binance',
|
|
||||||
open_rate=0.245441,
|
|
||||||
fee_open=fee.return_value,
|
|
||||||
fee_close=fee.return_value,
|
|
||||||
|
|
||||||
open_order_id="123456"
|
|
||||||
)
|
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
|
||||||
# Amount does not change
|
|
||||||
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_real_amount_open_trade(default_conf, fee, mocker):
|
def test_get_real_amount_open_trade(default_conf, fee, mocker):
|
||||||
amount = 12345
|
amount = 12345
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
@ -4082,10 +4035,14 @@ def test_apply_fee_conditional(default_conf, fee, caplog, mocker,
|
|||||||
assert walletmock.call_count == 1
|
assert walletmock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("delta, is_high_delta", [
|
||||||
|
(0.1, False),
|
||||||
|
(100, True),
|
||||||
|
])
|
||||||
def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order_open, limit_buy_order,
|
def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order_open, limit_buy_order,
|
||||||
is_short, fee, mocker, order_book_l2):
|
is_short, fee, mocker, order_book_l2, delta, is_high_delta):
|
||||||
default_conf['bid_strategy']['check_depth_of_market']['enabled'] = True
|
default_conf['bid_strategy']['check_depth_of_market']['enabled'] = True
|
||||||
default_conf['bid_strategy']['check_depth_of_market']['bids_to_ask_delta'] = 0.1
|
default_conf['bid_strategy']['check_depth_of_market']['bids_to_ask_delta'] = delta
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book', order_book_l2)
|
mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book', order_book_l2)
|
||||||
@ -4103,42 +4060,22 @@ def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order_open,
|
|||||||
freqtrade.enter_positions()
|
freqtrade.enter_positions()
|
||||||
|
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
assert trade is not None
|
if is_high_delta:
|
||||||
assert trade.stake_amount == 0.001
|
assert trade is None
|
||||||
assert trade.is_open
|
else:
|
||||||
assert trade.open_date is not None
|
assert trade is not None
|
||||||
assert trade.exchange == 'binance'
|
assert trade.stake_amount == 0.001
|
||||||
|
assert trade.is_open
|
||||||
|
assert trade.open_date is not None
|
||||||
|
assert trade.exchange == 'binance'
|
||||||
|
|
||||||
assert len(Trade.query.all()) == 1
|
assert len(Trade.query.all()) == 1
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_BUY order for trade
|
# Simulate fulfilled LIMIT_BUY order for trade
|
||||||
trade.update(limit_buy_order)
|
trade.update(limit_buy_order)
|
||||||
|
|
||||||
assert trade.open_rate == 0.00001099
|
assert trade.open_rate == 0.00001099
|
||||||
assert whitelist == default_conf['exchange']['pair_whitelist']
|
assert whitelist == default_conf['exchange']['pair_whitelist']
|
||||||
|
|
||||||
|
|
||||||
def test_order_book_depth_of_market_high_delta(default_conf, ticker, limit_buy_order,
|
|
||||||
fee, mocker, order_book_l2):
|
|
||||||
default_conf['bid_strategy']['check_depth_of_market']['enabled'] = True
|
|
||||||
# delta is 100 which is impossible to reach. hence check_depth_of_market will return false
|
|
||||||
default_conf['bid_strategy']['check_depth_of_market']['bids_to_ask_delta'] = 100
|
|
||||||
patch_RPCManager(mocker)
|
|
||||||
patch_exchange(mocker)
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book', order_book_l2)
|
|
||||||
mocker.patch.multiple(
|
|
||||||
'freqtrade.exchange.Exchange',
|
|
||||||
fetch_ticker=ticker,
|
|
||||||
create_order=MagicMock(return_value={'id': limit_buy_order['id']}),
|
|
||||||
get_fee=fee,
|
|
||||||
)
|
|
||||||
# Save state of current whitelist
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
|
||||||
patch_get_signal(freqtrade)
|
|
||||||
freqtrade.enter_positions()
|
|
||||||
|
|
||||||
trade = Trade.query.first()
|
|
||||||
assert trade is None
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('exception_thrown,ask,last,order_book_top,order_book', [
|
@pytest.mark.parametrize('exception_thrown,ask,last,order_book_top,order_book', [
|
||||||
@ -4341,16 +4278,16 @@ def test_check_for_open_trades(mocker, default_conf, fee, is_short):
|
|||||||
|
|
||||||
@pytest.mark.parametrize("is_short", [False, True])
|
@pytest.mark.parametrize("is_short", [False, True])
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_update_open_orders(mocker, default_conf, fee, caplog, is_short):
|
def test_startup_update_open_orders(mocker, default_conf, fee, caplog, is_short):
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
create_mock_trades(fee, is_short=is_short)
|
create_mock_trades(fee, is_short=is_short)
|
||||||
|
|
||||||
freqtrade.update_open_orders()
|
freqtrade.startup_update_open_orders()
|
||||||
assert not log_has_re(r"Error updating Order .*", caplog)
|
assert not log_has_re(r"Error updating Order .*", caplog)
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
|
||||||
freqtrade.config['dry_run'] = False
|
freqtrade.config['dry_run'] = False
|
||||||
freqtrade.update_open_orders()
|
freqtrade.startup_update_open_orders()
|
||||||
|
|
||||||
assert log_has_re(r"Error updating Order .*", caplog)
|
assert log_has_re(r"Error updating Order .*", caplog)
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
@ -4361,7 +4298,7 @@ def test_update_open_orders(mocker, default_conf, fee, caplog, is_short):
|
|||||||
'status': 'closed',
|
'status': 'closed',
|
||||||
})
|
})
|
||||||
mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=matching_buy_order)
|
mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=matching_buy_order)
|
||||||
freqtrade.update_open_orders()
|
freqtrade.startup_update_open_orders()
|
||||||
# Only stoploss and sell orders are kept open
|
# Only stoploss and sell orders are kept open
|
||||||
assert len(Order.get_open_orders()) == 2
|
assert len(Order.get_open_orders()) == 2
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user