From c17595b314fb1e6dcfff38695c51e5c07f2b38fe Mon Sep 17 00:00:00 2001 From: LoveIsGrief Date: Sat, 28 Aug 2021 19:39:50 +0200 Subject: [PATCH 01/46] Docs: Mention Performance Warning for strategies Related to #5408 --- docs/strategy-customization.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index cfea60d22..194517b2b 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -781,6 +781,8 @@ Printing more than a few rows is also possible (simply use `print(dataframe)` i ## 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. 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. @@ -791,6 +793,33 @@ The following lists some common patterns which should be avoided to prevent frus - don't use `dataframe['volume'].mean()`. This uses the full DataFrame for backtesting, including data from the future. Use `dataframe['volume'].rolling().mean()` instead - don't use `.resample('1h')`. This uses the left border of the interval, so moves data from an hour to the start of the hour. Use `.resample('1h', label='right')` instead. +### 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)`. For example + +```python +for i in range(100): + dataframe[i] = ta.indicator(dataframe, param=i) +``` + +should be rewritten to + +```python +frames = [dataframe] +for i in range(100): + frames.append({ + str(i): ta.indicator(dataframe, param=i) + }) + +# Append columns to existing dataframe +merged_frame = pd.concat(frames, axis=1) +``` + ## Further strategy ideas To get additional Ideas for strategies, head over to our [strategy repository](https://github.com/freqtrade/freqtrade-strategies). Feel free to use them as they are - but results will depend on the current market situation, pairs used etc. - therefore please backtest the strategy for your exchange/desired pairs first, evaluate carefully, use at your own risk. From 879bf47b32ff692e35f2509665b1ef9899eb3621 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Sep 2021 19:25:36 +0200 Subject: [PATCH 02/46] Refactor telegram.py to simplify send_msg --- freqtrade/rpc/telegram.py | 51 +++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 19c58b63d..898446ea0 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -260,29 +260,7 @@ class Telegram(RPCHandler): return message - def send_msg(self, msg: Dict[str, Any]) -> None: - """ Send a message to telegram channel """ - - default_noti = 'on' - - msg_type = msg['type'] - noti = '' - if msg_type == RPCMessageType.SELL: - sell_noti = self._config['telegram'] \ - .get('notification_settings', {}).get(str(msg_type), {}) - # For backward compatibility sell still can be string - if isinstance(sell_noti, str): - noti = sell_noti - else: - noti = sell_noti.get(str(msg['sell_reason']), default_noti) - else: - noti = self._config['telegram'] \ - .get('notification_settings', {}).get(str(msg_type), default_noti) - - if noti == 'off': - logger.info(f"Notification '{msg_type}' not sent.") - # Notification disabled - return + def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> str: if msg_type == RPCMessageType.BUY: message = self._format_buy_msg(msg) @@ -315,6 +293,33 @@ class Telegram(RPCHandler): else: raise NotImplementedError('Unknown message type: {}'.format(msg_type)) + return message + + def send_msg(self, msg: Dict[str, Any]) -> None: + """ Send a message to telegram channel """ + + default_noti = 'on' + + msg_type = msg['type'] + noti = '' + if msg_type == RPCMessageType.SELL: + sell_noti = self._config['telegram'] \ + .get('notification_settings', {}).get(str(msg_type), {}) + # For backward compatibility sell still can be string + if isinstance(sell_noti, str): + noti = sell_noti + else: + noti = sell_noti.get(str(msg['sell_reason']), default_noti) + else: + noti = self._config['telegram'] \ + .get('notification_settings', {}).get(str(msg_type), default_noti) + + if noti == 'off': + logger.info(f"Notification '{msg_type}' not sent.") + # Notification disabled + return + + message = self.compose_message(msg, msg_type) self._send_msg(message, disable_notification=(noti == 'silent')) From 1da091dea3006b67e2400cf1390981354626f4fb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Sep 2021 19:41:19 +0200 Subject: [PATCH 03/46] ProtectionManager should return the lock just created --- freqtrade/persistence/pairlock_middleware.py | 4 +++- freqtrade/plugins/protectionmanager.py | 23 ++++++++++---------- tests/plugins/test_protections.py | 4 ++-- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index af904f693..8662fc36d 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -30,7 +30,8 @@ class PairLocks(): PairLocks.locks = [] @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". Uses database by default, unless PairLocks.use_db is set to False, @@ -52,6 +53,7 @@ class PairLocks(): PairLock.query.session.commit() else: PairLocks.locks.append(lock) + return lock @staticmethod def get_pair_locks(pair: Optional[str], now: Optional[datetime] = None) -> List[PairLock]: diff --git a/freqtrade/plugins/protectionmanager.py b/freqtrade/plugins/protectionmanager.py index f33e5b4bc..2510d6fee 100644 --- a/freqtrade/plugins/protectionmanager.py +++ b/freqtrade/plugins/protectionmanager.py @@ -6,6 +6,7 @@ from datetime import datetime, timezone from typing import Dict, List, Optional from freqtrade.persistence import PairLocks +from freqtrade.persistence.models import PairLock from freqtrade.plugins.protections import IProtection from freqtrade.resolvers import ProtectionResolver @@ -43,30 +44,28 @@ class ProtectionManager(): """ 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: now = datetime.now(timezone.utc) - result = False + result = None for protection_handler in self._protection_handlers: 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 - if result and until: + if lock and until: if not PairLocks.is_global_lock(until): - PairLocks.lock_pair('*', until, reason, now=now) - result = True + result = PairLocks.lock_pair('*', until, reason, now=now) 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: now = datetime.now(timezone.utc) - result = False + result = None for protection_handler in self._protection_handlers: if protection_handler.has_local_stop: - result, until, reason = protection_handler.stop_per_pair(pair, now) - if result and until: + lock, until, reason = protection_handler.stop_per_pair(pair, now) + if lock and until: if not PairLocks.is_pair_locked(pair, until): - PairLocks.lock_pair(pair, until, reason, now=now) - result = True + result = PairLocks.lock_pair(pair, until, reason, now=now) return result diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index c0a9ae72a..a3cb29c9d 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -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 # should be the previous end-time 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) @@ -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, )) - assert freqtrade.protections.stop_per_pair(pair) + freqtrade.protections.stop_per_pair(pair) assert freqtrade.protections.global_stop() != only_per_pair assert PairLocks.is_pair_locked(pair) assert PairLocks.is_global_lock() != only_per_pair From c91a9a92f2ccecc5aac46f25a999c0b4a97b20a7 Mon Sep 17 00:00:00 2001 From: Bernhard Millauer Date: Mon, 20 Sep 2021 14:22:24 +0200 Subject: [PATCH 04/46] Add troubleshooting information The time in wsl docker container shifts over time. Added information how to fix this issue. --- docs/docker_quickstart.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index 1fa229225..f4f8c366d 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -149,6 +149,20 @@ 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. +### 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" + ``` + ## 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. From a0fb43c6caca7d0b0f06cf9814d43f69341339b6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 Sep 2021 19:12:59 +0200 Subject: [PATCH 05/46] Add pairlock-notification --- freqtrade/enums/rpcmessagetype.py | 1 + freqtrade/freqtradebot.py | 16 ++++++++++++++-- freqtrade/rpc/telegram.py | 5 +++++ tests/rpc/test_rpc_telegram.py | 14 ++++++++++++++ 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/freqtrade/enums/rpcmessagetype.py b/freqtrade/enums/rpcmessagetype.py index 9c59f6108..889d67cf4 100644 --- a/freqtrade/enums/rpcmessagetype.py +++ b/freqtrade/enums/rpcmessagetype.py @@ -11,6 +11,7 @@ class RPCMessageType(Enum): SELL = 'sell' SELL_FILL = 'sell_fill' SELL_CANCEL = 'sell_cancel' + PROTECTION_TRIGGER = 'protection_trigger' def __repr__(self): return self.value diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 1cb8988ff..451b5764a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1292,8 +1292,7 @@ class FreqtradeBot(LoggingMixin): if not trade.is_open: if not stoploss_order and not trade.open_order_id: self._notify_exit(trade, '', True) - self.protections.stop_per_pair(trade.pair) - self.protections.global_stop() + self.handle_protections(trade.pair) self.wallets.update() elif not trade.open_order_id: # Buy fill @@ -1301,6 +1300,19 @@ class FreqtradeBot(LoggingMixin): 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, } + msg.update(prot_trig_glb.to_json()) + self.rpc.send_msg(msg) + def apply_fee_conditional(self, trade: Trade, trade_base_currency: str, amount: float, fee_abs: float) -> float: """ diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 898446ea0..934a5182a 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -281,6 +281,11 @@ class Telegram(RPCHandler): "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.STATUS: message = '*Status:* `{status}`'.format(**msg) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 21f1cd000..0675e4d5f 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1313,6 +1313,20 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: 'Reason: cancelled due to timeout.') +def test_send_msg_protection_notification(default_conf, mocker, time_machine) -> None: + + 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.") + + def test_send_msg_buy_fill_notification(default_conf, mocker) -> None: default_conf['telegram']['notification_settings']['buy_fill'] = 'on' From dd0db7ee5db04fb98b542e9541d46768b9bb2c05 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 Sep 2021 19:23:40 +0200 Subject: [PATCH 06/46] Split protection-notification into global and per-pair --- config_examples/config_full.example.json | 4 +++- docs/telegram-usage.md | 4 +++- freqtrade/enums/rpcmessagetype.py | 1 + freqtrade/freqtradebot.py | 2 +- freqtrade/rpc/telegram.py | 6 +++++- tests/rpc/test_rpc_telegram.py | 13 +++++++++++++ 6 files changed, 26 insertions(+), 4 deletions(-) diff --git a/config_examples/config_full.example.json b/config_examples/config_full.example.json index d0f3f0df6..c415d70b0 100644 --- a/config_examples/config_full.example.json +++ b/config_examples/config_full.example.json @@ -149,7 +149,9 @@ }, "sell_fill": "on", "buy_cancel": "on", - "sell_cancel": "on" + "sell_cancel": "on", + "protection_trigger": "off", + "protection_trigger_global": "on" }, "reload": true, "balance_dust_level": 0.01 diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index b020b00db..1e6fa9dae 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -93,7 +93,9 @@ Example configuration showing the different settings: "buy_cancel": "silent", "sell_cancel": "on", "buy_fill": "off", - "sell_fill": "off" + "sell_fill": "off", + "protection_trigger": "off", + "protection_trigger_global": "on" }, "reload": true, "balance_dust_level": 0.01 diff --git a/freqtrade/enums/rpcmessagetype.py b/freqtrade/enums/rpcmessagetype.py index 889d67cf4..4e3f693e5 100644 --- a/freqtrade/enums/rpcmessagetype.py +++ b/freqtrade/enums/rpcmessagetype.py @@ -12,6 +12,7 @@ class RPCMessageType(Enum): SELL_FILL = 'sell_fill' SELL_CANCEL = 'sell_cancel' PROTECTION_TRIGGER = 'protection_trigger' + PROTECTION_TRIGGER_GLOBAL = 'protection_trigger_global' def __repr__(self): return self.value diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 451b5764a..37be3173a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1309,7 +1309,7 @@ class FreqtradeBot(LoggingMixin): prot_trig_glb = self.protections.global_stop() if prot_trig_glb: - msg = {'type': RPCMessageType.PROTECTION_TRIGGER, } + msg = {'type': RPCMessageType.PROTECTION_TRIGGER_GLOBAL, } msg.update(prot_trig_glb.to_json()) self.rpc.send_msg(msg) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 934a5182a..0687f95a2 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -286,7 +286,11 @@ class Telegram(RPCHandler): "*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) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 0675e4d5f..d37a74187 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1326,6 +1326,19 @@ def test_send_msg_protection_notification(default_conf, mocker, time_machine) -> 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: From fd23ab3d647db7444d12129755ddf001e014dbcd Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 Sep 2021 19:49:18 +0200 Subject: [PATCH 07/46] improve formatting, add tests --- freqtrade/rpc/telegram.py | 4 ++-- tests/rpc/test_rpc_telegram.py | 5 ++--- tests/test_freqtradebot.py | 23 +++++++++++++++++++++++ 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 0687f95a2..059ba9c41 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -284,12 +284,12 @@ class Telegram(RPCHandler): elif msg_type == RPCMessageType.PROTECTION_TRIGGER: message = ( "*Protection* triggered due to {reason}. " - "{pair} will be locked until {lock_end_time}." + "`{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}." + "*All pairs* will be locked until `{lock_end_time}`." ).format(**msg) elif msg_type == RPCMessageType.STATUS: message = '*Status:* `{status}`'.format(**msg) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index d37a74187..8c285a76e 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1324,7 +1324,7 @@ def test_send_msg_protection_notification(default_conf, mocker, time_machine) -> 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.") + "`ETH/BTC` will be locked until `2021-09-01 05:10:00`.") msg_mock.reset_mock() # Test global protection @@ -1336,8 +1336,7 @@ def test_send_msg_protection_notification(default_conf, mocker, time_machine) -> 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.") - + "*All pairs* will be locked until `2021-09-01 06:45:00`.") def test_send_msg_buy_fill_notification(default_conf, mocker) -> None: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index eb3c77cc7..e1df6dad9 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -526,6 +526,29 @@ def test_enter_positions_global_pairlock(default_conf, ticker, limit_buy_order, 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: default_conf['dry_run'] = True From 3ce05c0d548119fd5618ba2700313a69ec10f3c5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 Sep 2021 20:08:48 +0200 Subject: [PATCH 08/46] Add "sane" defaults to protection triggers --- docs/telegram-usage.md | 1 + freqtrade/constants.py | 9 +++++++++ tests/rpc/test_rpc_telegram.py | 2 ++ 3 files changed, 12 insertions(+) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 1e6fa9dae..b9d01a236 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -105,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. `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. +`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. diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 9ca43d459..4997108bc 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -284,6 +284,15 @@ CONF_SCHEMA = { 'enum': TELEGRAM_SETTING_OPTIONS, 'default': 'off' }, + 'protection_trigger': { + 'type': 'string', + 'enum': TELEGRAM_SETTING_OPTIONS, + 'default': 'off' + }, + 'protection_trigger_global': { + 'type': 'string', + 'enum': TELEGRAM_SETTING_OPTIONS, + }, } }, 'reload': {'type': 'boolean'}, diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 8c285a76e..7dde7b803 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1315,6 +1315,8 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: 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') From abddb0db66b431abf5902a8fa74dbf5f2cf16e56 Mon Sep 17 00:00:00 2001 From: Bernhard Millauer Date: Tue, 21 Sep 2021 10:13:19 +0200 Subject: [PATCH 09/46] Fix header indention Co-authored-by: Matthias --- docs/docker_quickstart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index f4f8c366d..33b1c7ea1 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -151,7 +151,7 @@ You can then run `docker-compose build` to build the docker image, and run it us ### Troubleshooting -### Docker on Windows +#### 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. From 6fc770d97dd8d4380be3fb94e8f3dcedfa981cd7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 21 Sep 2021 15:12:35 +0200 Subject: [PATCH 10/46] Add warning about running with docker on windows --- docs/docker_quickstart.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index 33b1c7ea1..2f350d207 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -163,6 +163,10 @@ You can then run `docker-compose build` to build the docker image, and run it us start "" "C:\Program Files\Docker\Docker\Docker Desktop.exe" ``` +!!! Warning + Due to the above, we do not recommend the usage of docker on windows for production setups, but only for experimentation, datadownload and backtesting. + Best use a linux-VPS for running freqtrade reliably. + ## Plotting with docker-compose Commands `freqtrade plot-profit` and `freqtrade plot-dataframe` ([Documentation](plotting.md)) are available by changing the image to `*_plot` in your docker-compose.yml file. From 277828bf0ebac1c370b02b4dfb6ca15628d63b1e Mon Sep 17 00:00:00 2001 From: matt ferrante Date: Tue, 21 Sep 2021 07:56:16 -0600 Subject: [PATCH 11/46] parameterize some tests --- tests/test_freqtradebot.py | 219 ++++++++++++++----------------------- 1 file changed, 84 insertions(+), 135 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 72d1f6150..d96ef71d6 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3545,8 +3545,34 @@ def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker, f caplog) -def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, fee, mocker): - trades_for_order[0]['fee']['currency'] = 'ETH' +@pytest.mark.parametrize( + 'fee_cost, fee_currency, fee_reduction_amount, use_ticker_rate, expected_log', + [ + (None, 'ETH', 0, True, None), + (0.004, None, 0, True, None), + (0.00094518, "BNB", 0, True, None), + ( + 0.004, + "LTC", + 0.004, + False, + ( + 'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' + 'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.996).' + ) + ), + (0.008, None, 0, True, None), + ] +) +def test_get_real_amount( + default_conf, trades_for_order, buy_order_fee, fee, mocker, caplog, + fee_cost, fee_currency, fee_reduction_amount, use_ticker_rate, expected_log +): + + buy_order = deepcopy(buy_order_fee) + buy_order['fee'] = {'cost': fee_cost, 'currency': fee_currency} + trades_for_order[0]['fee']['cost'] = fee_cost + trades_for_order[0]['fee']['currency'] = fee_currency mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) amount = sum(x['amount'] for x in trades_for_order) @@ -3561,19 +3587,58 @@ def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, fe ) freqtrade = get_patched_freqtradebot(mocker, default_conf) + if not use_ticker_rate: + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', side_effect=ExchangeError) + # Amount does not change - assert freqtrade.get_real_amount(trade, buy_order_fee) == amount + 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, - fee, mocker): +@pytest.mark.parametrize( + 'stake_currency, fee_cost, fee_currency, fee_reduction_amount, expected_fee, expected_log', + [ + ( + "BTC", + None, + None, + 0.001, + 0.001, + ( + 'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' + 'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.992).' + ) + ), + ( + "ETH", + 0.02, + 'BNB', + 0.0005, + 0.001518575, + ( + 'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' + 'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.996).' + ) + ), + ] +) +def test_get_real_amount_multi( + default_conf, trades_for_order2, buy_order_fee, caplog, fee, mocker, markets, + stake_currency, fee_cost, fee_currency, fee_reduction_amount, expected_fee, expected_log, +): - limit_buy_order = deepcopy(buy_order_fee) - limit_buy_order['fee'] = {'cost': 0.004, 'currency': None} - trades_for_order[0]['fee']['currency'] = None + trades_for_order = deepcopy(trades_for_order2) + if fee_cost: + 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) - 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'] = stake_currency + trade = Trade( pair='LTC/ETH', amount=amount, @@ -3583,124 +3648,29 @@ def test_get_real_amount_no_currency_in_fee(default_conf, trades_for_order, buy_ 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, 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, ' - '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 markets['BNB/ETH'] = markets['ETH/BTC'] freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', - return_value={'ask': 0.19, 'last': 0.2}) + mocker.patch( + 'freqtrade.exchange.Exchange.fetch_ticker', + return_value={'ask': 0.19, 'last': 0.2} + ) # Amount is reduced by "fee" - assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.0005) - assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' - 'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.996).', - caplog) - # Overall fee is average of both trade's fee - assert trade.fee_open == 0.001518575 + expected_amount = amount - (amount * fee_reduction_amount) + assert freqtrade.get_real_amount(trade, buy_order_fee) == expected_amount + assert log_has(expected_log, caplog) + + assert trade.fee_open == expected_fee + assert trade.fee_close == expected_fee 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_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, ' - '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): limit_buy_order = deepcopy(buy_order_fee) limit_buy_order['fee'] = {'cost': 0.004} @@ -3768,27 +3738,6 @@ def test_get_real_amount_wrong_amount_rounding(default_conf, trades_for_order, b 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): amount = 12345 trade = Trade( From 707d0ef795868961fe652046f12a190047809de3 Mon Sep 17 00:00:00 2001 From: matt ferrante Date: Tue, 21 Sep 2021 12:16:10 -0600 Subject: [PATCH 12/46] remove trades_for_order3 --- tests/conftest.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 5e08e7097..7354c0b2c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1685,14 +1685,6 @@ def trades_for_order2(): '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 def buy_order_fee(): return { From b0de4d333e86bfe23f065ab365e2ab18bb7ca41d Mon Sep 17 00:00:00 2001 From: Peter Willemsen Date: Tue, 21 Sep 2021 23:20:40 +0200 Subject: [PATCH 13/46] fixed webhook error --- freqtrade/freqtradebot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 1cb8988ff..95f2280ac 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1217,7 +1217,7 @@ class FreqtradeBot(LoggingMixin): 'exchange': trade.exchange.capitalize(), 'pair': trade.pair, 'gain': gain, - 'limit': profit_rate, + 'limit': profit_rate or 0, 'order_type': order_type, 'amount': trade.amount, 'open_rate': trade.open_rate, @@ -1226,7 +1226,7 @@ class FreqtradeBot(LoggingMixin): 'profit_ratio': profit_ratio, 'sell_reason': trade.sell_reason, 'open_date': trade.open_date, - 'close_date': trade.close_date, + 'close_date': trade.close_date or datetime.utcnow(), 'stake_currency': self.config['stake_currency'], 'fiat_currency': self.config.get('fiat_display_currency', None), 'reason': reason, From 553c868d7f2a8d4e7edafaccdf814cc7249e0ac9 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 21 Sep 2021 16:40:24 -0600 Subject: [PATCH 14/46] combined test_order_book_depth_of_market and test_order_book_depth_of_market_high_delta --- tests/test_freqtradebot.py | 122 +++++++++++++------------------------ 1 file changed, 42 insertions(+), 80 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index d96ef71d6..0a2f73263 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3546,24 +3546,19 @@ def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker, f @pytest.mark.parametrize( - 'fee_cost, fee_currency, fee_reduction_amount, use_ticker_rate, expected_log', - [ + 'fee_cost, fee_currency, fee_reduction_amount, use_ticker_rate, expected_log', [ (None, 'ETH', 0, True, None), (0.004, None, 0, True, None), - (0.00094518, "BNB", 0, True, None), - ( - 0.004, - "LTC", - 0.004, - False, - ( - 'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' - 'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.996).' - ) - ), + (0.00094518, "BNB", 0, True, ( + 'Fee for Trade Trade(id=None, pair=LTC/ETH, amount=8.00000000, open_rate=0.24544100,' + ' open_since=closed) [buy]: 0.00094518 BNB - rate: None' + )), + (0.004, "LTC", 0.004, False, ( + 'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' + 'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.996).' + )), (0.008, None, 0, True, None), - ] -) + ]) def test_get_real_amount( default_conf, trades_for_order, buy_order_fee, fee, mocker, caplog, fee_cost, fee_currency, fee_reduction_amount, use_ticker_rate, expected_log @@ -3590,6 +3585,7 @@ def test_get_real_amount( if not use_ticker_rate: mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', side_effect=ExchangeError) + caplog.clear() # Amount does not change assert freqtrade.get_real_amount(trade, buy_order) == amount - fee_reduction_amount @@ -3598,32 +3594,16 @@ def test_get_real_amount( @pytest.mark.parametrize( - 'stake_currency, fee_cost, fee_currency, fee_reduction_amount, expected_fee, expected_log', - [ - ( - "BTC", - None, - None, - 0.001, - 0.001, - ( - 'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' - 'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.992).' - ) - ), - ( - "ETH", - 0.02, - 'BNB', - 0.0005, - 0.001518575, - ( - 'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' - 'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.996).' - ) - ), - ] -) + 'stake_currency, fee_cost, fee_currency, fee_reduction_amount, expected_fee, expected_log', [ + ("BTC", None, None, 0.001, 0.001, ( + 'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' + 'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.992).' + )), + ("ETH", 0.02, 'BNB', 0.0005, 0.001518575, ( + 'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' + 'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.996).' + )), + ]) def test_get_real_amount_multi( default_conf, trades_for_order2, buy_order_fee, caplog, fee, mocker, markets, stake_currency, fee_cost, fee_currency, fee_reduction_amount, expected_fee, expected_log, @@ -3653,10 +3633,8 @@ def test_get_real_amount_multi( markets['BNB/ETH'] = markets['ETH/BTC'] freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) - mocker.patch( - 'freqtrade.exchange.Exchange.fetch_ticker', - return_value={'ask': 0.19, 'last': 0.2} - ) + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', + return_value={'ask': 0.19, 'last': 0.2}) # Amount is reduced by "fee" expected_amount = amount - (amount * fee_reduction_amount) @@ -3788,10 +3766,14 @@ def test_apply_fee_conditional(default_conf, fee, caplog, mocker, 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, - fee, mocker, order_book_l2): + 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']['bids_to_ask_delta'] = 0.1 + default_conf['bid_strategy']['check_depth_of_market']['bids_to_ask_delta'] = delta patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book', order_book_l2) @@ -3809,42 +3791,22 @@ def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order_open, freqtrade.enter_positions() trade = Trade.query.first() - assert trade is not None - assert trade.stake_amount == 0.001 - assert trade.is_open - assert trade.open_date is not None - assert trade.exchange == 'binance' + if is_high_delta: + assert trade is None + else: + assert trade is not None + 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 - trade.update(limit_buy_order) + # Simulate fulfilled LIMIT_BUY order for trade + trade.update(limit_buy_order) - assert trade.open_rate == 0.00001099 - 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 + assert trade.open_rate == 0.00001099 + assert whitelist == default_conf['exchange']['pair_whitelist'] @pytest.mark.parametrize('exception_thrown,ask,last,order_book_top,order_book', [ From f768bdea503780b6c220afe36691fbbd09752689 Mon Sep 17 00:00:00 2001 From: matt ferrante Date: Wed, 22 Sep 2021 10:32:30 -0600 Subject: [PATCH 15/46] cleanup based on feedback --- tests/test_freqtradebot.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 0a2f73263..eaaadbcb1 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3594,19 +3594,13 @@ def test_get_real_amount( @pytest.mark.parametrize( - 'stake_currency, fee_cost, fee_currency, fee_reduction_amount, expected_fee, expected_log', [ - ("BTC", None, None, 0.001, 0.001, ( - 'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' - 'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.992).' - )), - ("ETH", 0.02, 'BNB', 0.0005, 0.001518575, ( - 'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' - 'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.996).' - )), + 'stake_currency, fee_cost, fee_currency, fee_reduction_amount, expected_fee, expected_log_amount', [ + (None, None, None, 0.001, 0.001, 7.992), + ("ETH", 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, - stake_currency, fee_cost, fee_currency, fee_reduction_amount, expected_fee, expected_log, + stake_currency, fee_cost, fee_currency, fee_reduction_amount, expected_fee, expected_log_amount, ): trades_for_order = deepcopy(trades_for_order2) @@ -3617,7 +3611,8 @@ def test_get_real_amount_multi( 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)) - default_conf['stake_currency'] = stake_currency + if stake_currency: + default_conf['stake_currency'] = stake_currency trade = Trade( pair='LTC/ETH', @@ -3639,7 +3634,13 @@ def test_get_real_amount_multi( # Amount is reduced by "fee" expected_amount = amount - (amount * fee_reduction_amount) assert freqtrade.get_real_amount(trade, buy_order_fee) == expected_amount - assert log_has(expected_log, caplog) + assert log_has( + ( + 'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' + f'open_rate=0.24544100, open_since=closed) (from 8.0 to {expected_log_amount}).' + ), + caplog + ) assert trade.fee_open == expected_fee assert trade.fee_close == expected_fee From 8cfb6ddd518bc4edf910c9f07afdc1ee85d103aa Mon Sep 17 00:00:00 2001 From: matt ferrante Date: Wed, 22 Sep 2021 10:48:13 -0600 Subject: [PATCH 16/46] fix long line --- tests/test_freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index eaaadbcb1..b987d54d8 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3594,7 +3594,7 @@ def test_get_real_amount( @pytest.mark.parametrize( - 'stake_currency, fee_cost, fee_currency, fee_reduction_amount, expected_fee, expected_log_amount', [ + 'stake_currency,fee_cost,fee_currency,fee_reduction_amount,expected_fee,expected_log_amount', [ (None, None, None, 0.001, 0.001, 7.992), ("ETH", 0.02, 'BNB', 0.0005, 0.001518575, 7.996), ]) From 30cc69c880cf736225f6246331527ab202398588 Mon Sep 17 00:00:00 2001 From: matt ferrante Date: Wed, 22 Sep 2021 11:28:42 -0600 Subject: [PATCH 17/46] set all to eth for multi test --- tests/test_freqtradebot.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index b987d54d8..8e036e80a 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3594,13 +3594,13 @@ def test_get_real_amount( @pytest.mark.parametrize( - 'stake_currency,fee_cost,fee_currency,fee_reduction_amount,expected_fee,expected_log_amount', [ - (None, None, None, 0.001, 0.001, 7.992), - ("ETH", 0.02, 'BNB', 0.0005, 0.001518575, 7.996), + 'fee_cost, fee_currency, fee_reduction_amount, expected_fee, expected_log_amount', [ + (None, None, 0.001, 0.001, 7.992), + (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, - stake_currency, fee_cost, fee_currency, fee_reduction_amount, expected_fee, expected_log_amount, + fee_cost, fee_currency, fee_reduction_amount, expected_fee, expected_log_amount, ): trades_for_order = deepcopy(trades_for_order2) @@ -3611,8 +3611,7 @@ def test_get_real_amount_multi( 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)) - if stake_currency: - default_conf['stake_currency'] = stake_currency + default_conf['stake_currency'] = "ETH" trade = Trade( pair='LTC/ETH', From 2bf49445b7db8a15681f8fb690d3db6162f0e8c1 Mon Sep 17 00:00:00 2001 From: matt ferrante Date: Wed, 22 Sep 2021 16:11:27 -0600 Subject: [PATCH 18/46] add parameterized names --- tests/test_freqtradebot.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 8e036e80a..9c9271810 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3547,16 +3547,21 @@ def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker, f @pytest.mark.parametrize( 'fee_cost, fee_currency, fee_reduction_amount, use_ticker_rate, expected_log', [ + # basic, amount does not change (None, 'ETH', 0, True, None), + # no currency in fee (0.004, None, 0, True, None), + # BNB no rate (0.00094518, "BNB", 0, True, ( 'Fee for Trade Trade(id=None, pair=LTC/ETH, amount=8.00000000, open_rate=0.24544100,' ' open_since=closed) [buy]: 0.00094518 BNB - rate: None' )), + # from order (0.004, "LTC", 0.004, False, ( 'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' 'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.996).' )), + # invalid, no currency in from fee dict (0.008, None, 0, True, None), ]) def test_get_real_amount( @@ -3586,7 +3591,6 @@ def test_get_real_amount( mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', side_effect=ExchangeError) caplog.clear() - # Amount does not change assert freqtrade.get_real_amount(trade, buy_order) == amount - fee_reduction_amount if expected_log: @@ -3595,7 +3599,9 @@ def test_get_real_amount( @pytest.mark.parametrize( '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( From d7903f012f7af5f3c19df527780456bab0ca986f Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 23 Sep 2021 07:25:11 +0200 Subject: [PATCH 19/46] Move PerformanceWarning to advanced section rewrite to use strategy parameters instead of plain range --- docs/strategy-advanced.md | 30 ++++++++++++++++++++++++++++++ docs/strategy-customization.md | 27 --------------------------- 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 4409af6ea..d7d0dd04a 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -695,3 +695,33 @@ The variable 'content', will contain the strategy file in a BASE64 encoded form. ``` 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) +``` diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 194517b2b..6d6eff2aa 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -793,33 +793,6 @@ The following lists some common patterns which should be avoided to prevent frus - don't use `dataframe['volume'].mean()`. This uses the full DataFrame for backtesting, including data from the future. Use `dataframe['volume'].rolling().mean()` instead - don't use `.resample('1h')`. This uses the left border of the interval, so moves data from an hour to the start of the hour. Use `.resample('1h', label='right')` instead. -### 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)`. For example - -```python -for i in range(100): - dataframe[i] = ta.indicator(dataframe, param=i) -``` - -should be rewritten to - -```python -frames = [dataframe] -for i in range(100): - frames.append({ - str(i): ta.indicator(dataframe, param=i) - }) - -# Append columns to existing dataframe -merged_frame = pd.concat(frames, axis=1) -``` - ## Further strategy ideas To get additional Ideas for strategies, head over to our [strategy repository](https://github.com/freqtrade/freqtrade-strategies). Feel free to use them as they are - but results will depend on the current market situation, pairs used etc. - therefore please backtest the strategy for your exchange/desired pairs first, evaluate carefully, use at your own risk. From 692e91a26dfa2ec1c746923d481421cc87a15b8c Mon Sep 17 00:00:00 2001 From: Peter Willemsen Date: Thu, 23 Sep 2021 10:28:15 +0200 Subject: [PATCH 20/46] changed close date from datetime.utcnow() to datetime.now(timezone.utc) --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 95f2280ac..9cfd219a8 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1226,7 +1226,7 @@ class FreqtradeBot(LoggingMixin): 'profit_ratio': profit_ratio, 'sell_reason': trade.sell_reason, 'open_date': trade.open_date, - 'close_date': trade.close_date or datetime.utcnow(), + 'close_date': trade.close_date or datetime.now(timezone.utc), 'stake_currency': self.config['stake_currency'], 'fiat_currency': self.config.get('fiat_display_currency', None), 'reason': reason, From 422d5601890f9749ab7f69a903d2de3f68ea9257 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Sep 2021 05:05:55 +0000 Subject: [PATCH 21/46] Bump types-requests from 2.25.6 to 2.25.8 Bumps [types-requests](https://github.com/python/typeshed) from 2.25.6 to 2.25.8. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-requests dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 4859e1cc6..d8d8ce916 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -23,5 +23,5 @@ nbconvert==6.1.0 # mypy types types-cachetools==4.2.0 types-filelock==0.1.5 -types-requests==2.25.6 +types-requests==2.25.8 types-tabulate==0.8.2 From e85dc6326302e83da897b06941ca725f6d5f242c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Sep 2021 05:06:01 +0000 Subject: [PATCH 22/46] Bump mkdocs-material from 7.2.6 to 7.3.0 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.2.6 to 7.3.0. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/7.2.6...7.3.0) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 9927740c2..9b7c12a43 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,4 +1,4 @@ mkdocs==1.2.2 -mkdocs-material==7.2.6 +mkdocs-material==7.3.0 mdx_truly_sane_lists==1.2 pymdown-extensions==8.2 From 766ef90b56cf190b8beeba1a249d9b53792fbba2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Sep 2021 05:06:16 +0000 Subject: [PATCH 23/46] Bump sqlalchemy from 1.4.23 to 1.4.25 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.4.23 to 1.4.25. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) --- updated-dependencies: - dependency-name: sqlalchemy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index aa729dd9f..74ce391ed 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ ccxt==1.56.30 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.8 aiohttp==3.7.4.post0 -SQLAlchemy==1.4.23 +SQLAlchemy==1.4.25 python-telegram-bot==13.7 arrow==1.1.1 cachetools==4.2.2 From 90d5af9a35211a51756603cfb3ffcb3cf67edc17 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Sep 2021 05:06:20 +0000 Subject: [PATCH 24/46] Bump urllib3 from 1.26.6 to 1.26.7 Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.6 to 1.26.7. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/1.26.7/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/1.26.6...1.26.7) --- updated-dependencies: - dependency-name: urllib3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index aa729dd9f..2f6a4a104 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ python-telegram-bot==13.7 arrow==1.1.1 cachetools==4.2.2 requests==2.26.0 -urllib3==1.26.6 +urllib3==1.26.7 wrapt==1.12.1 jsonschema==3.2.0 TA-Lib==0.4.21 From 0353f070f9714655e1ccb06448ffa03dbe3f5fb0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Sep 2021 05:06:24 +0000 Subject: [PATCH 25/46] Bump progressbar2 from 3.53.2 to 3.53.3 Bumps [progressbar2](https://github.com/WoLpH/python-progressbar) from 3.53.2 to 3.53.3. - [Release notes](https://github.com/WoLpH/python-progressbar/releases) - [Changelog](https://github.com/WoLpH/python-progressbar/blob/develop/CHANGES.rst) - [Commits](https://github.com/WoLpH/python-progressbar/compare/v3.53.2...v3.53.3) --- updated-dependencies: - dependency-name: progressbar2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 7dc55a9fc..9feec80f1 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -8,4 +8,4 @@ scikit-optimize==0.8.1 filelock==3.0.12 joblib==1.0.1 psutil==5.8.0 -progressbar2==3.53.2 +progressbar2==3.53.3 From 954c468191b3c130c62d7bedfde1ff138f427908 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 Sep 2021 07:12:38 +0200 Subject: [PATCH 26/46] Add pandas-ta to requirements --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index aa729dd9f..f0da0cdb9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ numpy==1.21.2 pandas==1.3.3 +pandas-ta==0.3.14b ccxt==1.56.30 # Pin cryptography for now due to rust build errors with piwheels From 72a1e27fc6cc0454dfb3035f4957820dd6c6423d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Sep 2021 07:31:20 +0000 Subject: [PATCH 27/46] Bump ccxt from 1.56.30 to 1.56.86 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.56.30 to 1.56.86. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/exchanges.cfg) - [Commits](https://github.com/ccxt/ccxt/compare/1.56.30...1.56.86) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3b004dfaa..ede0c74e3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.21.2 pandas==1.3.3 pandas-ta==0.3.14b -ccxt==1.56.30 +ccxt==1.56.86 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.8 aiohttp==3.7.4.post0 From b59906b117f300aefd828fa02c8b5c5658388df1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 Sep 2021 19:24:33 +0200 Subject: [PATCH 28/46] Update minimum for tradable_balance_ratio to 0.0 --- freqtrade/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 4997108bc..fca319a0f 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -110,7 +110,7 @@ CONF_SCHEMA = { }, 'tradable_balance_ratio': { 'type': 'number', - 'minimum': 0.1, + 'minimum': 0.0, 'maximum': 1, 'default': 0.99 }, From 4c268847d42043d53fc59751ffd1717125d9e0bd Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 Sep 2021 19:32:22 +0200 Subject: [PATCH 29/46] Add pandas-ta to setup.py --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 727c40c7c..cf381bdd3 100644 --- a/setup.py +++ b/setup.py @@ -54,6 +54,7 @@ setup( 'wrapt', 'jsonschema', 'TA-Lib', + 'pandas-ta', 'technical', 'tabulate', 'pycoingecko', From f4f204d849eb65eed74a1ac09e1ad52e5a641ee9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 Sep 2021 20:09:24 +0200 Subject: [PATCH 30/46] Update test to use cost dict --- tests/test_freqtradebot.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 9c9271810..b233a6267 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3546,33 +3546,32 @@ def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker, f @pytest.mark.parametrize( - 'fee_cost, fee_currency, fee_reduction_amount, use_ticker_rate, expected_log', [ + 'fee_par,fee_reduction_amount,use_ticker_rate,expected_log', [ # basic, amount does not change - (None, 'ETH', 0, True, None), + ({'cost': 0.008, 'currency': 'ETH'}, 0, False, None), # no currency in fee - (0.004, None, 0, True, None), + ({'cost': 0.004, 'currency': None}, 0, True, None), # BNB no rate - (0.00094518, "BNB", 0, True, ( + ({'cost': 0.00094518, 'currency': 'BNB'}, 0, True, ( 'Fee for Trade Trade(id=None, pair=LTC/ETH, amount=8.00000000, open_rate=0.24544100,' ' open_since=closed) [buy]: 0.00094518 BNB - rate: None' )), # from order - (0.004, "LTC", 0.004, False, ( + ({'cost': 0.004, 'currency': 'LTC'}, 0.004, False, ( 'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' 'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.996).' )), # invalid, no currency in from fee dict - (0.008, None, 0, True, None), + ({'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_cost, fee_currency, fee_reduction_amount, use_ticker_rate, expected_log + fee_par, fee_reduction_amount, use_ticker_rate, expected_log ): buy_order = deepcopy(buy_order_fee) - buy_order['fee'] = {'cost': fee_cost, 'currency': fee_currency} - trades_for_order[0]['fee']['cost'] = fee_cost - trades_for_order[0]['fee']['currency'] = fee_currency + 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) amount = sum(x['amount'] for x in trades_for_order) From 6319c104fe1c51e0343c08def9f4511cc537b377 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Sep 2021 15:07:48 +0200 Subject: [PATCH 31/46] Fix unreliable backtest-result when using webserver mode --- freqtrade/data/dataprovider.py | 2 + freqtrade/optimize/backtesting.py | 50 ++++++++++++------------ freqtrade/rpc/api_server/api_backtest.py | 21 +++++----- 3 files changed, 39 insertions(+), 34 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index cdee0f078..b197c159f 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -149,6 +149,8 @@ class DataProvider: Clear pair dataframe cache. """ self.__cached_pairs = {} + self.__cached_pairs_backtesting = {} + self.__slice_index = 0 # Exchange functions diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 79c861ee8..f406f89d7 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -85,18 +85,7 @@ class Backtesting: "configuration or as cli argument `--timeframe 5m`") self.timeframe = str(self.config.get('timeframe')) self.timeframe_min = timeframe_to_minutes(self.timeframe) - # 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] = {} - + self.init_backtest_detail() self.pairlists = PairListManager(self.exchange, self.config) if 'VolumePairList' in self.pairlists.name_list: raise OperationalException("VolumePairList not allowed for backtesting.") @@ -119,14 +108,6 @@ class Backtesting: else: 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( None if self.config.get('timerange') is None else str(self.config.get('timerange'))) @@ -135,9 +116,7 @@ class Backtesting: # Add maximum startup candle count to configuration for informative pairs support self.config['startup_candle_count'] = self.required_startup self.exchange.validate_required_startup_candles(self.required_startup, self.timeframe) - - self.progress = BTProgress() - self.abort = False + self.init_backtest() def __del__(self): self.cleanup() @@ -147,6 +126,28 @@ class Backtesting: PairLocks.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): """ Load strategy into backtesting @@ -226,7 +227,8 @@ class Backtesting: Trade.reset_trades() self.rejected_trades = 0 self.dataprovider.clear_cache() - self._load_protections(self.strategy) + if enable_protections: + self._load_protections(self.strategy) def check_abort(self): """ diff --git a/freqtrade/rpc/api_server/api_backtest.py b/freqtrade/rpc/api_server/api_backtest.py index 4623c187e..32278686c 100644 --- a/freqtrade/rpc/api_server/api_backtest.py +++ b/freqtrade/rpc/api_server/api_backtest.py @@ -47,33 +47,34 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac not ApiServer._bt or lastconfig.get('timeframe') != strat.timeframe 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'] ): from freqtrade.optimize.backtesting import Backtesting ApiServer._bt = Backtesting(btconfig) if ApiServer._bt.timeframe_detail: ApiServer._bt.load_bt_data_detail() - + else: + ApiServer._bt.config = btconfig + ApiServer._bt.init_backtest() # Only reload data if timeframe changed. if ( not ApiServer._bt_data 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 ): - 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() + 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 min_date, max_date = ApiServer._bt.backtest_one_strategy( strat, ApiServer._bt_data, ApiServer._bt_timerange) + ApiServer._bt.results = generate_backtest_stats( ApiServer._bt_data, ApiServer._bt.all_results, min_date=min_date, max_date=max_date) From 08b1f04ed5f9b7ce69827fb44acfc4c154aecc2e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Sep 2021 03:01:18 +0000 Subject: [PATCH 32/46] Bump types-requests from 2.25.8 to 2.25.9 Bumps [types-requests](https://github.com/python/typeshed) from 2.25.8 to 2.25.9. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-requests dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index d8d8ce916..1d61369eb 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -23,5 +23,5 @@ nbconvert==6.1.0 # mypy types types-cachetools==4.2.0 types-filelock==0.1.5 -types-requests==2.25.8 +types-requests==2.25.9 types-tabulate==0.8.2 From 905950230329688b81606c9fb78c3e5a631d8cbd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Sep 2021 03:01:27 +0000 Subject: [PATCH 33/46] Bump ccxt from 1.56.86 to 1.57.3 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.56.86 to 1.57.3. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/exchanges.cfg) - [Commits](https://github.com/ccxt/ccxt/compare/1.56.86...1.57.3) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d1d10dd1d..feeb4d942 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.21.2 pandas==1.3.3 pandas-ta==0.3.14b -ccxt==1.56.86 +ccxt==1.57.3 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.8 aiohttp==3.7.4.post0 From 78096c9eff08cc2aea47d44e7ae20751c0b3420a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Sep 2021 04:32:26 +0000 Subject: [PATCH 34/46] Bump nbconvert from 6.1.0 to 6.2.0 Bumps [nbconvert](https://github.com/jupyter/nbconvert) from 6.1.0 to 6.2.0. - [Release notes](https://github.com/jupyter/nbconvert/releases) - [Commits](https://github.com/jupyter/nbconvert/compare/6.1.0...6.2.0) --- updated-dependencies: - dependency-name: nbconvert dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 1d61369eb..2f03255a0 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -18,7 +18,7 @@ isort==5.9.3 time-machine==2.4.0 # Convert jupyter notebooks to markdown documents -nbconvert==6.1.0 +nbconvert==6.2.0 # mypy types types-cachetools==4.2.0 From 5b7a1f864257c62924760be45f2dc9c651c665a0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 27 Sep 2021 07:12:40 +0200 Subject: [PATCH 35/46] Validate config also in webserver mode --- freqtrade/rpc/api_server/api_backtest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/rpc/api_server/api_backtest.py b/freqtrade/rpc/api_server/api_backtest.py index 32278686c..7ce9f487f 100644 --- a/freqtrade/rpc/api_server/api_backtest.py +++ b/freqtrade/rpc/api_server/api_backtest.py @@ -4,6 +4,7 @@ from copy import deepcopy from fastapi import APIRouter, BackgroundTasks, Depends +from freqtrade.configuration.config_validation import validate_config_consistency from freqtrade.enums import BacktestState from freqtrade.exceptions import DependencyException from freqtrade.rpc.api_server.api_schemas import BacktestRequest, BacktestResponse @@ -42,6 +43,7 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac # Reload strategy lastconfig = ApiServer._bt_last_config strat = StrategyResolver.load_strategy(btconfig) + validate_config_consistency(btconfig) if ( not ApiServer._bt From 3fbf716f85b96a8d53f99523cd50ab703c034256 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 27 Sep 2021 17:51:01 +0200 Subject: [PATCH 36/46] Fix "sticking" timerange in webserver mode --- freqtrade/rpc/api_server/api_backtest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server/api_backtest.py b/freqtrade/rpc/api_server/api_backtest.py index 7ce9f487f..edbc39772 100644 --- a/freqtrade/rpc/api_server/api_backtest.py +++ b/freqtrade/rpc/api_server/api_backtest.py @@ -63,12 +63,12 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac not ApiServer._bt_data or not ApiServer._bt_timerange or lastconfig.get('timeframe') != strat.timeframe + or lastconfig.get('timerange') != btconfig['timerange'] ): ApiServer._bt_data, ApiServer._bt_timerange = ApiServer._bt.load_bt_data() - lastconfig['timerange'] = btconfig['timerange'] - lastconfig['timeframe'] = strat.timeframe - + 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') From 5726886b0620874be0d1c6c8f8b4737912e42786 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 27 Sep 2021 20:52:19 +0200 Subject: [PATCH 37/46] Reduce backtest-noise from "pandas slice" warning --- freqtrade/optimize/backtesting.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index f406f89d7..8328d61d3 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -385,12 +385,12 @@ class Backtesting: detail_data = detail_data.loc[ (detail_data['date'] >= sell_candle_time) & (detail_data['date'] < sell_candle_end) - ] + ].copy() if len(detail_data) == 0: # 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) - detail_data['buy'] = sell_row[BUY_IDX] - detail_data['sell'] = sell_row[SELL_IDX] + detail_data.loc[:, 'buy'] = sell_row[BUY_IDX] + detail_data.loc[:, 'sell'] = sell_row[SELL_IDX] headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high'] for det_row in detail_data[headers].values.tolist(): res = self._get_sell_trade_entry_for_candle(trade, det_row) From e025576d8cac9cb0227734efc18d4c978d01cc6f Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Wed, 29 Sep 2021 10:15:05 +0300 Subject: [PATCH 38/46] Introduce markets_static fixture serving an immutable list of markets. Adapt pairlist/markets tests to use this new fixture. This allows freely modifying markets in get_markets() without a need of updating pairlist/markets tests. --- tests/commands/test_commands.py | 9 ++++----- tests/conftest.py | 21 ++++++++++++++++++--- tests/exchange/test_exchange.py | 4 ++-- tests/plugins/test_pairlist.py | 4 ++-- 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 135510b38..b236f6a10 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -208,11 +208,10 @@ def test_list_timeframes(mocker, capsys): 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.markets = markets - patch_exchange(mocker, api_mock=api_mock, id='bittrex') + patch_exchange(mocker, api_mock=api_mock, id='bittrex', mock_markets=markets_static) # Test with no --config args = [ @@ -237,7 +236,7 @@ def test_list_markets(mocker, markets, capsys): "TKN/BTC, XLTCUSDT, XRP/BTC.\n" 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 args = [ "list-markets", @@ -250,7 +249,7 @@ def test_list_markets(mocker, markets, capsys): assert re.match("\nExchange Binance has 10 active markets:\n", 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 args = [ "list-markets", "--all", diff --git a/tests/conftest.py b/tests/conftest.py index 7354c0b2c..c908c0cb0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -90,8 +90,10 @@ def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> No mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title())) mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2)) if mock_markets: + if isinstance(mock_markets, bool): + mock_markets = get_markets() mocker.patch('freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=get_markets())) + PropertyMock(return_value=mock_markets)) if api_mock: mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) @@ -376,6 +378,8 @@ def markets(): def get_markets(): + # See get_markets_static() for immutable markets and do not modify them unless absolutely + # necessary! return { 'ETH/BTC': { 'id': 'ethbtc', @@ -675,11 +679,22 @@ def get_markets(): @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 """ - shitmarkets = deepcopy(markets) + shitmarkets = deepcopy(markets_static) shitmarkets.update({ 'HOT/BTC': { 'id': 'HOTBTC', diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 97bc33429..79b4a3ff5 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2735,7 +2735,7 @@ def test_get_valid_pair_combination(default_conf, mocker, markets): (['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, expected_keys): mocker.patch.multiple('freqtrade.exchange.Exchange', @@ -2743,7 +2743,7 @@ def test_get_markets(default_conf, mocker, markets, _load_async_markets=MagicMock(), validate_pairs=MagicMock(), validate_timeframes=MagicMock(), - markets=PropertyMock(return_value=markets)) + markets=PropertyMock(return_value=markets_static)) ex = Exchange(default_conf) pairs = ex.get_markets(base_currencies, quote_currencies, pairs_only, active_only) assert sorted(pairs.keys()) == sorted(expected_keys) diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 1ce8d172c..cf918e2a0 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -131,9 +131,9 @@ def test_load_pairlist_noexist(mocker, markets, default_conf): 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) - 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) # 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) From 656526c007dfa8fd80036966d601e8b873d1ccd5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 29 Sep 2021 16:50:05 +0200 Subject: [PATCH 39/46] Add trades-to-ohlcv command to simplify adding new timeframes --- freqtrade/commands/__init__.py | 4 +-- freqtrade/commands/arguments.py | 14 +++++++++- freqtrade/commands/data_commands.py | 41 +++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/freqtrade/commands/__init__.py b/freqtrade/commands/__init__.py index a6f14cff7..858c99acd 100644 --- a/freqtrade/commands/__init__.py +++ b/freqtrade/commands/__init__.py @@ -8,8 +8,8 @@ Note: Be careful with file-scoped imports in these subfiles. """ from freqtrade.commands.arguments import Arguments from freqtrade.commands.build_config_commands import start_new_config -from freqtrade.commands.data_commands import (start_convert_data, start_download_data, - start_list_data) +from freqtrade.commands.data_commands import (start_convert_data, start_convert_trades, + start_download_data, start_list_data) from freqtrade.commands.deploy_commands import (start_create_userdir, start_install_ui, start_new_strategy) from freqtrade.commands.hyperopt_commands import start_hyperopt_list, start_hyperopt_show diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index d424f3ce7..48dc48cf1 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -58,6 +58,8 @@ ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "template"] ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase"] ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes"] +ARGS_CONVERT_TRADES = ["pairs", "timeframes", "dataformat_ohlcv", "dataformat_trades"] + ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs"] ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "new_pairs_days", "timerange", @@ -169,7 +171,8 @@ class Arguments: self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot') 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_create_userdir, start_download_data, start_edge, start_hyperopt, start_hyperopt_list, start_hyperopt_show, start_install_ui, start_list_data, start_list_exchanges, start_list_markets, @@ -236,6 +239,15 @@ class Arguments: 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) + # 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 list_data_cmd = subparsers.add_parser( 'list-data', diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py index 141e85f14..7ef1ae5c7 100644 --- a/freqtrade/commands/data_commands.py +++ b/freqtrade/commands/data_commands.py @@ -89,6 +89,47 @@ def start_download_data(args: Dict[str, Any]) -> None: f"on exchange {exchange.name}.") +def start_convert_trades(args: Dict[str, Any]) -> None: + + config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) + + timerange = TimeRange() + if 'days' in config: + time_since = (datetime.now() - timedelta(days=config['days'])).strftime("%Y%m%d") + timerange = TimeRange.parse_timerange(f'{time_since}-') + + if 'timerange' in config: + timerange = timerange.parse_timerange(config['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: """ Convert data from one format to another From fc511aac4486dc99568e88edd01635453d0c1a59 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 29 Sep 2021 19:21:54 +0200 Subject: [PATCH 40/46] don't use %default when no default is defined --- freqtrade/commands/cli_options.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index e3c7fe464..d350a9426 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -381,12 +381,12 @@ AVAILABLE_CLI_OPTIONS = { ), "dataformat_ohlcv": Arg( '--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, ), "dataformat_trades": Arg( '--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, ), "exchange": Arg( From 248c61bb26399d3049f7ac2f116d6cf0e49d8665 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 29 Sep 2021 19:39:29 +0200 Subject: [PATCH 41/46] Add test for trades-to-ohlcv --- freqtrade/commands/arguments.py | 4 ++-- freqtrade/commands/data_commands.py | 6 ------ tests/commands/test_commands.py | 28 ++++++++++++++++++++++------ 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 48dc48cf1..2fadf047e 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -58,7 +58,7 @@ ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "template"] ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase"] ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes"] -ARGS_CONVERT_TRADES = ["pairs", "timeframes", "dataformat_ohlcv", "dataformat_trades"] +ARGS_CONVERT_TRADES = ["pairs", "timeframes", "exchange", "dataformat_ohlcv", "dataformat_trades"] ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs"] @@ -93,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", "list-markets", "list-pairs", "list-strategies", "list-data", "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"] diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py index 7ef1ae5c7..ee05e6c69 100644 --- a/freqtrade/commands/data_commands.py +++ b/freqtrade/commands/data_commands.py @@ -94,12 +94,6 @@ def start_convert_trades(args: Dict[str, Any]) -> None: config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) timerange = TimeRange() - if 'days' in config: - time_since = (datetime.now() - timedelta(days=config['days'])).strftime("%Y%m%d") - timerange = TimeRange.parse_timerange(f'{time_since}-') - - if 'timerange' in config: - timerange = timerange.parse_timerange(config['timerange']) # Remove stake-currency to skip checks which are not relevant for datadownload config['stake_currency'] = '' diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index b236f6a10..8889617ba 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -8,12 +8,12 @@ from zipfile import ZipFile import arrow import pytest -from freqtrade.commands import (start_convert_data, start_create_userdir, start_download_data, - start_hyperopt_list, start_hyperopt_show, start_install_ui, - start_list_data, start_list_exchanges, start_list_markets, - start_list_strategies, start_list_timeframes, start_new_strategy, - start_show_trades, start_test_pairlist, start_trading, - start_webserver) +from freqtrade.commands import (start_convert_data, start_convert_trades, start_create_userdir, + start_download_data, start_hyperopt_list, start_hyperopt_show, + start_install_ui, start_list_data, start_list_exchanges, + start_list_markets, start_list_strategies, start_list_timeframes, + start_new_strategy, start_show_trades, start_test_pairlist, + start_trading, start_webserver) from freqtrade.commands.deploy_commands import (clean_ui_subdir, download_and_install_ui, get_ui_download_url, read_ui_version) from freqtrade.configuration import setup_utils_configuration @@ -759,6 +759,22 @@ def test_download_data_trades(mocker, caplog): 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): args = [ From 178db516bf79dbaa37ca2ef9b779d77ed8850f75 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 29 Sep 2021 19:48:56 +0200 Subject: [PATCH 42/46] Add documentation for trade-to-ohlcv --- docs/data-download.md | 55 +++++++++++++++++++++++++++++++++ freqtrade/commands/arguments.py | 15 +++++---- 2 files changed, 62 insertions(+), 8 deletions(-) diff --git a/docs/data-download.md b/docs/data-download.md index 0ca86b0d3..5f605c404 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -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 ``` +### 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 You can get a list of downloaded data using the `list-data` sub-command. diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 2fadf047e..9643705a5 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -172,14 +172,13 @@ class Arguments: self._build_args(optionlist=['version'], parser=self.parser) from freqtrade.commands import (start_backtesting, start_convert_data, start_convert_trades, - start_create_userdir, - start_download_data, start_edge, start_hyperopt, - start_hyperopt_list, start_hyperopt_show, start_install_ui, - start_list_data, start_list_exchanges, start_list_markets, - start_list_strategies, start_list_timeframes, - start_new_config, start_new_strategy, start_plot_dataframe, - start_plot_profit, start_show_trades, start_test_pairlist, - start_trading, start_webserver) + start_create_userdir, start_download_data, start_edge, + start_hyperopt, start_hyperopt_list, start_hyperopt_show, + start_install_ui, start_list_data, start_list_exchanges, + start_list_markets, start_list_strategies, + start_list_timeframes, start_new_config, start_new_strategy, + start_plot_dataframe, start_plot_profit, start_show_trades, + start_test_pairlist, start_trading, start_webserver) subparsers = self.parser.add_subparsers(dest='command', # Use custom message when no subhandler is added From bd27993e797c8d83d54ac76b278a9779e9c4eee5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 30 Sep 2021 06:42:42 +0200 Subject: [PATCH 43/46] Add documentation segment about indicator libraries --- docs/strategy-customization.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 110365208..0bfc0a2f6 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -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). 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 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. From 5f23af580248a86a56d60e050bd5d0a7f5424236 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 30 Sep 2021 07:24:16 +0200 Subject: [PATCH 44/46] Rename update_open_trades to clarify it's only called at startup --- freqtrade/freqtradebot.py | 4 ++-- tests/test_freqtradebot.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 3a9b21b7c..bf4742fdc 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -139,7 +139,7 @@ class FreqtradeBot(LoggingMixin): # Only update open orders on startup # This will update the database after the initial migration - self.update_open_orders() + self.startup_update_open_orders() def process(self) -> None: """ @@ -237,7 +237,7 @@ class FreqtradeBot(LoggingMixin): open_trades = len(Trade.get_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. Mainly updates the state of orders - but may also close trades diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 760a9dee7..d312bdb11 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4033,16 +4033,16 @@ def test_check_for_open_trades(mocker, default_conf, fee): @pytest.mark.usefixtures("init_persistence") -def test_update_open_orders(mocker, default_conf, fee, caplog): +def test_startup_update_open_orders(mocker, default_conf, fee, caplog): freqtrade = get_patched_freqtradebot(mocker, default_conf) create_mock_trades(fee) - freqtrade.update_open_orders() + freqtrade.startup_update_open_orders() assert not log_has_re(r"Error updating Order .*", caplog) caplog.clear() freqtrade.config['dry_run'] = False - freqtrade.update_open_orders() + freqtrade.startup_update_open_orders() assert log_has_re(r"Error updating Order .*", caplog) caplog.clear() @@ -4053,7 +4053,7 @@ def test_update_open_orders(mocker, default_conf, fee, caplog): 'status': 'closed', }) 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 assert len(Order.get_open_orders()) == 2 From 15df5fd9c5c8533b14809147570e2a66d79241fe Mon Sep 17 00:00:00 2001 From: Robert Davey Date: Fri, 1 Oct 2021 13:49:16 +0100 Subject: [PATCH 45/46] Fix pair_candles to point to correct API call pair_candles pointed to available_pairs RPC call instead of pair_candles --- scripts/rest_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index ece0a253e..713b398c3 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -312,7 +312,7 @@ class FtRestClient(): :param limit: Limit result to the last n candles. :return: json object """ - return self._get("available_pairs", params={ + return self._get("pair_candles", params={ "pair": pair, "timeframe": timeframe, "limit": limit, From f69cb39a170a4a223de4f154c902c69903188a36 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 1 Oct 2021 19:26:51 +0200 Subject: [PATCH 46/46] Fix missing comma in kucoin template closes #5646 --- freqtrade/templates/subtemplates/exchange_kucoin.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/templates/subtemplates/exchange_kucoin.j2 b/freqtrade/templates/subtemplates/exchange_kucoin.j2 index f9dfff663..9882c51c7 100644 --- a/freqtrade/templates/subtemplates/exchange_kucoin.j2 +++ b/freqtrade/templates/subtemplates/exchange_kucoin.j2 @@ -4,7 +4,7 @@ "secret": "{{ exchange_secret }}", "password": "{{ exchange_key_password }}", "ccxt_config": { - "enableRateLimit": true + "enableRateLimit": true, "rateLimit": 200 }, "ccxt_async_config": {