From ac2e8d760e2359ca5d26a8ce7dabf370873b0a6c Mon Sep 17 00:00:00 2001 From: rzrymiak <106121613+rzrymiak@users.noreply.github.com> Date: Tue, 19 Jul 2022 14:24:44 -0700 Subject: [PATCH 01/56] Added description heading to README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 881895c9a..828a3d1f9 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ [![Documentation](https://readthedocs.org/projects/freqtrade/badge/)](https://www.freqtrade.io) [![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability) +## Description + Freqtrade is a free and open source crypto trading bot written in Python. It is designed to support all major exchanges and be controlled via Telegram or webUI. It contains backtesting, plotting and money management tools as well as strategy optimization by machine learning. ![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade-screenshot.png) @@ -193,7 +195,7 @@ Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/ The clock must be accurate, synchronized to a NTP server very frequently to avoid problems with communication to the exchanges. -### Min hardware required +### Minimum hardware required To run this bot we recommend you a cloud instance with a minimum of: From 229e8864bbeb33bbd0b4c3bc525131939e276ad2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 26 Jul 2022 20:15:49 +0200 Subject: [PATCH 02/56] Add send_msg capability to dataprovider --- freqtrade/data/dataprovider.py | 21 +++++++++++++++++++++ freqtrade/enums/rpcmessagetype.py | 2 ++ freqtrade/freqtradebot.py | 1 + freqtrade/rpc/rpc_manager.py | 12 ++++++++++++ freqtrade/rpc/telegram.py | 3 ++- 5 files changed, 38 insertions(+), 1 deletion(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index b9b118c00..800254533 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -5,12 +5,14 @@ including ticker and orderbook data, live and historical candle (OHLCV) data Common Interface for bot and strategy to access data. """ import logging +from collections import deque from datetime import datetime, timezone from typing import Any, Dict, List, Optional, Tuple from pandas import DataFrame from freqtrade.configuration import TimeRange +from freqtrade.configuration.PeriodicCache import PeriodicCache from freqtrade.constants import ListPairsWithTimeframes, PairWithTimeframe from freqtrade.data.history import load_pair_history from freqtrade.enums import CandleType, RunMode @@ -33,6 +35,9 @@ class DataProvider: self.__cached_pairs: Dict[PairWithTimeframe, Tuple[DataFrame, datetime]] = {} self.__slice_index: Optional[int] = None self.__cached_pairs_backtesting: Dict[PairWithTimeframe, DataFrame] = {} + self._msg_queue: deque = deque() + self.__msg_cache = PeriodicCache( + maxsize=1000, ttl=timeframe_to_seconds(self._config['timeframe'])) def _set_dataframe_max_index(self, limit_index: int): """ @@ -265,3 +270,19 @@ class DataProvider: if self._exchange is None: raise OperationalException(NO_EXCHANGE_EXCEPTION) return self._exchange.fetch_l2_order_book(pair, maximum) + + def send_msg(self, message: str, always_send: bool = False) -> None: + """ + TODO: Document me + :param message: Message to be sent. Must be below 4096. + :param always_send: If False, will send the message only once per candle, and surpress + identical messages. + Careful as this can end up spaming your chat. + Defaults to False + """ + if self.runmode not in (RunMode.DRY_RUN, RunMode.LIVE): + return + + if always_send or message not in self.__msg_cache: + self._msg_queue.append(message) + self.__msg_cache[message] = True diff --git a/freqtrade/enums/rpcmessagetype.py b/freqtrade/enums/rpcmessagetype.py index 584a011c2..415d8f18c 100644 --- a/freqtrade/enums/rpcmessagetype.py +++ b/freqtrade/enums/rpcmessagetype.py @@ -17,6 +17,8 @@ class RPCMessageType(Enum): PROTECTION_TRIGGER = 'protection_trigger' PROTECTION_TRIGGER_GLOBAL = 'protection_trigger_global' + STRATEGY_MSG = 'strategy_msg' + def __repr__(self): return self.value diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 43608cae7..9ea195c45 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -214,6 +214,7 @@ class FreqtradeBot(LoggingMixin): if self.trading_mode == TradingMode.FUTURES: self._schedule.run_pending() Trade.commit() + self.rpc.process_msg_queue(self.dataprovider._msg_queue) self.last_process = datetime.now(timezone.utc) def process_stopped(self) -> None: diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index 66e84029f..3ccf23228 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -2,6 +2,7 @@ This module contains class to manage RPC communications (Telegram, API, ...) """ import logging +from collections import deque from typing import Any, Dict, List from freqtrade.enums import RPCMessageType @@ -77,6 +78,17 @@ class RPCManager: except NotImplementedError: logger.error(f"Message type '{msg['type']}' not implemented by handler {mod.name}.") + def process_msg_queue(self, queue: deque) -> None: + """ + Process all messages in the queue. + """ + while queue: + msg = queue.popleft() + self.send_msg({ + 'type': RPCMessageType.STRATEGY_MSG, + 'msg': msg, + }) + def startup_messages(self, config: Dict[str, Any], pairlist, protections) -> None: if config['dry_run']: self.send_msg({ diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 2aff1d210..121324d90 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -376,7 +376,8 @@ class Telegram(RPCHandler): elif msg_type == RPCMessageType.STARTUP: message = f"{msg['status']}" - + elif msg_type == RPCMessageType.STRATEGY_MSG: + message = f"{msg['msg']}" else: raise NotImplementedError(f"Unknown message type: {msg_type}") return message From 7bac0546681e770d3156db987620a666e4e1a489 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 26 Jul 2022 20:24:52 +0200 Subject: [PATCH 03/56] Add documentation and clarity for send_msg --- docs/strategy-customization.md | 17 +++++++++++++++++ docs/telegram-usage.md | 4 +++- freqtrade/constants.py | 4 ++++ freqtrade/data/dataprovider.py | 8 +++++--- 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 6947380dd..38d34d51b 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -731,6 +731,23 @@ if self.dp: !!! Warning "Warning about backtesting" This method will always return up-to-date values - so usage during backtesting / hyperopt will lead to wrong results. +### Send Notification + +The dataprovider `.send_msg()` function allows you to send custom notifications from your strategy. +Identical notifications will only be sent once per candle, unless the 2nd argument (`always_send`) is set to True. + +``` python + self.dp.send_msg(f"{metadata['pair']} just got hot!") + + # Force send this notification, avoid caching (Please read warning below!) + self.dp.send_msg(f"{metadata['pair']} just got hot!", always_send=True) +``` + +Notifications will only be sent in trading modes (Live/Dry-run) - so this method can be called without conditions for backtesting. + +!!! Warning "Spamming" + You can spam yourself pretty good by setting `always_send=True` in this method. Use this with great care and only in conditions you know will not happen throughout a candle to avoid a message every 5 seconds. + ### Complete Data-provider sample ```python diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 9853e15c6..a690e18b9 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -98,6 +98,7 @@ Example configuration showing the different settings: "exit_fill": "off", "protection_trigger": "off", "protection_trigger_global": "on", + "strategy_msg": "off", "show_candle": "off" }, "reload": true, @@ -109,7 +110,8 @@ Example configuration showing the different settings: `exit` notifications are sent when the order is placed, while `exit_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. -`show_candle` - show candle values as part of entry/exit messages. Only possible value is "ohlc". +`strategy_msg` - Receive notifications from the strategy, sent via `self.dp.send_msg()` from the strategy [more details](strategy-customization.md#send-notification). +`show_candle` - show candle values as part of entry/exit messages. Only possible values are `"ohlc"` or `"off"`. `balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown. `reload` allows you to disable reload-buttons on selected messages. diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 6d74ceafd..1d83d21a0 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -317,6 +317,10 @@ CONF_SCHEMA = { 'type': 'string', 'enum': ['off', 'ohlc'], }, + 'strategy_msg': { + 'type': 'string', + 'enum': TELEGRAM_SETTING_OPTIONS, + }, } }, 'reload': {'type': 'boolean'}, diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 800254533..e21f10193 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -36,8 +36,9 @@ class DataProvider: self.__slice_index: Optional[int] = None self.__cached_pairs_backtesting: Dict[PairWithTimeframe, DataFrame] = {} self._msg_queue: deque = deque() + self.__msg_cache = PeriodicCache( - maxsize=1000, ttl=timeframe_to_seconds(self._config['timeframe'])) + maxsize=1000, ttl=timeframe_to_seconds(self._config.get('timeframe', '1h'))) def _set_dataframe_max_index(self, limit_index: int): """ @@ -271,9 +272,10 @@ class DataProvider: raise OperationalException(NO_EXCHANGE_EXCEPTION) return self._exchange.fetch_l2_order_book(pair, maximum) - def send_msg(self, message: str, always_send: bool = False) -> None: + def send_msg(self, message: str, *, always_send: bool = False) -> None: """ - TODO: Document me + Send custom RPC Notifications from your bot. + Will not send any bot in modes other than Dry-run or Live. :param message: Message to be sent. Must be below 4096. :param always_send: If False, will send the message only once per candle, and surpress identical messages. From 0adfa4d9efe26d9423d769ca8829e723569c0545 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 27 Jul 2022 06:32:23 +0200 Subject: [PATCH 04/56] Add tests for dataprovider send-message methods --- tests/data/test_dataprovider.py | 19 +++++++++++++++++++ tests/rpc/test_rpc_manager.py | 21 +++++++++++++++++++-- tests/rpc/test_rpc_telegram.py | 10 ++++++++++ tests/test_freqtradebot.py | 6 ++++++ 4 files changed, 54 insertions(+), 2 deletions(-) diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index 93f82de5d..843d60786 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -311,3 +311,22 @@ def test_no_exchange_mode(default_conf): with pytest.raises(OperationalException, match=message): dp.available_pairs() + + +def test_dp_send_msg(default_conf): + + default_conf["runmode"] = RunMode.DRY_RUN + + default_conf["timeframe"] = '1h' + dp = DataProvider(default_conf, None) + msg = 'Test message' + dp.send_msg(msg) + + assert msg in dp._msg_queue + dp._msg_queue.pop() + assert msg not in dp._msg_queue + # Message is not resent due to caching + dp.send_msg(msg) + assert msg not in dp._msg_queue + dp.send_msg(msg, always_send=True) + assert msg in dp._msg_queue diff --git a/tests/rpc/test_rpc_manager.py b/tests/rpc/test_rpc_manager.py index 596b5ae20..b9ae16a20 100644 --- a/tests/rpc/test_rpc_manager.py +++ b/tests/rpc/test_rpc_manager.py @@ -1,6 +1,7 @@ # pragma pylint: disable=missing-docstring, C0103 import logging import time +from collections import deque from unittest.mock import MagicMock from freqtrade.enums import RPCMessageType @@ -81,9 +82,25 @@ def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None: assert telegram_mock.call_count == 0 +def test_process_msg_queue(mocker, default_conf, caplog) -> None: + telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg') + mocker.patch('freqtrade.rpc.telegram.Telegram._init') + + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + rpc_manager = RPCManager(freqtradebot) + queue = deque() + queue.append('Test message') + queue.append('Test message 2') + rpc_manager.process_msg_queue(queue) + + assert log_has("Sending rpc message: {'type': strategy_msg, 'msg': 'Test message'}", caplog) + assert log_has("Sending rpc message: {'type': strategy_msg, 'msg': 'Test message 2'}", caplog) + assert telegram_mock.call_count == 2 + + def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None: - telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) - mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) + telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg') + mocker.patch('freqtrade.rpc.telegram.Telegram._init') freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc_manager = RPCManager(freqtradebot) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index f69b7e878..8d244f3fd 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1994,6 +1994,16 @@ def test_startup_notification(default_conf, mocker) -> None: assert msg_mock.call_args[0][0] == '*Custom:* `Hello World`' +def test_send_msg_strategy_msg_notification(default_conf, mocker) -> None: + + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) + telegram.send_msg({ + 'type': RPCMessageType.STRATEGY_MSG, + 'msg': 'hello world, Test msg' + }) + assert msg_mock.call_args[0][0] == 'hello world, Test msg' + + def test_send_msg_unknown_type(default_conf, mocker) -> None: telegram, _, _ = get_telegram_testobject(mocker, default_conf) with pytest.raises(NotImplementedError, match=r'Unknown message type: None'): diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 66cbd7d9b..438a2704c 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -68,6 +68,12 @@ def test_process_stopped(mocker, default_conf_usdt) -> None: assert coo_mock.call_count == 1 +def test_process_calls_sendmsg(mocker, default_conf_usdt) -> None: + freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) + freqtrade.process() + assert freqtrade.rpc.process_msg_queue.call_count == 1 + + def test_bot_cleanup(mocker, default_conf_usdt, caplog) -> None: mock_cleanup = mocker.patch('freqtrade.freqtradebot.cleanup_db') coo_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cancel_all_open_orders') From 31ddec834816d980d2802810868b62f44df787d1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 27 Jul 2022 06:51:56 +0200 Subject: [PATCH 05/56] Add missing test to confirm backtesting won't send messages --- tests/data/test_dataprovider.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index 843d60786..49603feac 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -330,3 +330,8 @@ def test_dp_send_msg(default_conf): assert msg not in dp._msg_queue dp.send_msg(msg, always_send=True) assert msg in dp._msg_queue + + default_conf["runmode"] = RunMode.BACKTEST + dp = DataProvider(default_conf, None) + dp.send_msg(msg, always_send=True) + assert msg not in dp._msg_queue From d70650b074e023c649a04ce15ca84946b3409eb6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Jul 2022 08:20:22 +0200 Subject: [PATCH 06/56] Add note for plot-dataframe and current-whitelist closes #7142 --- docs/strategy-customization.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 6947380dd..78256e0ee 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -646,6 +646,9 @@ This is where calling `self.dp.current_whitelist()` comes in handy. return informative_pairs ``` +??? Note "Plotting with current_whitelist" + Current whitelist is not supported for `plot-dataframe`, as this command is usually used by providing an explicit pairlist - and would therefore make the return values of this method misleading. + ### *get_pair_dataframe(pair, timeframe)* ``` python From 995be90f91a21c351a5c22264135694f3e11d62b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 28 Jul 2022 20:35:23 +0200 Subject: [PATCH 07/56] Liquidation should be a separate exit type --- freqtrade/enums/exittype.py | 1 + freqtrade/optimize/backtesting.py | 14 ++++++++++---- freqtrade/persistence/trade_model.py | 12 ++---------- freqtrade/plugins/protections/stoploss_guard.py | 2 +- freqtrade/strategy/interface.py | 13 ++++++++++++- 5 files changed, 26 insertions(+), 16 deletions(-) diff --git a/freqtrade/enums/exittype.py b/freqtrade/enums/exittype.py index b2c5b62ea..1e15e70cd 100644 --- a/freqtrade/enums/exittype.py +++ b/freqtrade/enums/exittype.py @@ -9,6 +9,7 @@ class ExitType(Enum): STOP_LOSS = "stop_loss" STOPLOSS_ON_EXCHANGE = "stoploss_on_exchange" TRAILING_STOP_LOSS = "trailing_stop_loss" + LIQUIDATION = "liquidation" EXIT_SIGNAL = "exit_signal" FORCE_EXIT = "force_exit" EMERGENCY_EXIT = "emergency_exit" diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 4d16dc0f1..598ce6710 100755 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -381,7 +381,8 @@ class Backtesting: Get close rate for backtesting result """ # Special handling if high or low hit STOP_LOSS or ROI - if exit.exit_type in (ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS): + if exit.exit_type in ( + ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS, ExitType.LIQUIDATION): return self._get_close_rate_for_stoploss(row, trade, exit, trade_dur) elif exit.exit_type == (ExitType.ROI): return self._get_close_rate_for_roi(row, trade, exit, trade_dur) @@ -396,11 +397,16 @@ class Backtesting: is_short = trade.is_short or False leverage = trade.leverage or 1.0 side_1 = -1 if is_short else 1 + if exit.exit_type == ExitType.LIQUIDATION and trade.liquidation_price: + stoploss_value = trade.liquidation_price + else: + stoploss_value = trade.stop_loss + if is_short: - if trade.stop_loss < row[LOW_IDX]: + if stoploss_value < row[LOW_IDX]: return row[OPEN_IDX] else: - if trade.stop_loss > row[HIGH_IDX]: + if stoploss_value > row[HIGH_IDX]: return row[OPEN_IDX] # Special case: trailing triggers within same candle as trade opened. Assume most @@ -433,7 +439,7 @@ class Backtesting: return max(row[LOW_IDX], stop_rate) # Set close_rate to stoploss - return trade.stop_loss + return stoploss_value def _get_close_rate_for_roi(self, row: Tuple, trade: LocalTrade, exit: ExitCheckTuple, trade_dur: int) -> float: diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 5f302de71..2ff65d9d0 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -511,17 +511,9 @@ class LocalTrade(): Method you should use to set self.stop_loss. Assures stop_loss is not passed the liquidation price """ - if self.liquidation_price is not None: - if self.is_short: - sl = min(stop_loss, self.liquidation_price) - else: - sl = max(stop_loss, self.liquidation_price) - else: - sl = stop_loss - if not self.stop_loss: - self.initial_stop_loss = sl - self.stop_loss = sl + self.initial_stop_loss = stop_loss + self.stop_loss = stop_loss self.stop_loss_pct = -1 * abs(percent) self.stoploss_last_update = datetime.utcnow() diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index abc90a685..e80d13e9d 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -49,7 +49,7 @@ class StoplossGuard(IProtection): trades1 = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until) trades = [trade for trade in trades1 if (str(trade.exit_reason) in ( ExitType.TRAILING_STOP_LOSS.value, ExitType.STOP_LOSS.value, - ExitType.STOPLOSS_ON_EXCHANGE.value) + ExitType.STOPLOSS_ON_EXCHANGE.value, ExitType.LIQUIDATION.value) and trade.close_profit and trade.close_profit < self._profit_limit)] if self._only_per_side: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index c60817c99..f50721583 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -963,7 +963,7 @@ class IStrategy(ABC, HyperStrategyMixin): # ROI # Trailing stoploss - if stoplossflag.exit_type == ExitType.STOP_LOSS: + if stoplossflag.exit_type in (ExitType.STOP_LOSS, ExitType.LIQUIDATION): logger.debug(f"{trade.pair} - Stoploss hit. exit_type={stoplossflag.exit_type}") exits.append(stoplossflag) @@ -1035,6 +1035,17 @@ class IStrategy(ABC, HyperStrategyMixin): sl_higher_long = (trade.stop_loss >= (low or current_rate) and not trade.is_short) sl_lower_short = (trade.stop_loss <= (high or current_rate) and trade.is_short) + liq_higher_long = (trade.liquidation_price + and trade.liquidation_price >= (low or current_rate) + and not trade.is_short) + liq_lower_short = (trade.liquidation_price + and trade.liquidation_price <= (high or current_rate) + and trade.is_short) + + if (liq_higher_long or liq_lower_short): + logger.debug(f"{trade.pair} - Liquidation price hit. exit_type=ExitType.LIQUIDATION") + return ExitCheckTuple(exit_type=ExitType.LIQUIDATION) + # evaluate if the stoploss was hit if stoploss is not on exchange # in Dry-Run, this handles stoploss logic as well, as the logic will not be different to # regular stoploss handling. From 8711b7d99f224b3d0ee26553e32774dd247d605a Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 28 Jul 2022 20:37:10 +0200 Subject: [PATCH 08/56] Liquidations cannot be rejected. --- freqtrade/freqtradebot.py | 5 +++-- freqtrade/optimize/backtesting.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 43608cae7..d58c05d7f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1463,11 +1463,12 @@ class FreqtradeBot(LoggingMixin): amount = self._safe_exit_amount(trade.pair, trade.amount) time_in_force = self.strategy.order_time_in_force['exit'] - if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( + if (exit_check.exit_type != ExitType.LIQUIDATION and not strategy_safe_wrapper( + self.strategy.confirm_trade_exit, default_retval=True)( pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit, time_in_force=time_in_force, exit_reason=exit_reason, sell_reason=exit_reason, # sellreason -> compatibility - current_time=datetime.now(timezone.utc)): + current_time=datetime.now(timezone.utc))): logger.info(f"User denied exit for {trade.pair}.") return False diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 598ce6710..6bbace185 100755 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -598,7 +598,8 @@ class Backtesting: # Confirm trade exit: time_in_force = self.strategy.order_time_in_force['exit'] - if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( + if (exit_.exit_type != ExitType.LIQUIDATION and not strategy_safe_wrapper( + self.strategy.confirm_trade_exit, default_retval=True)( pair=trade.pair, trade=trade, # type: ignore[arg-type] order_type='limit', @@ -607,7 +608,7 @@ class Backtesting: time_in_force=time_in_force, sell_reason=exit_reason, # deprecated exit_reason=exit_reason, - current_time=exit_candle_time): + current_time=exit_candle_time)): return None trade.exit_reason = exit_reason From f57ecb18615dc2e75fc73d0da78540ea0ebd6804 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 29 Jul 2022 06:55:42 +0200 Subject: [PATCH 09/56] Simplify adjust_stop test --- tests/test_persistence.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 838c4c22a..4703075eb 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1537,26 +1537,26 @@ def test_adjust_stop_loss(fee): # Get percent of profit with a custom rate (Higher than open rate) trade.adjust_stop_loss(1.3, -0.1) - assert round(trade.stop_loss, 8) == 1.17 + assert pytest.approx(trade.stop_loss) == 1.17 assert trade.stop_loss_pct == -0.1 assert trade.initial_stop_loss == 0.95 assert trade.initial_stop_loss_pct == -0.05 # current rate lower again ... should not change trade.adjust_stop_loss(1.2, 0.1) - assert round(trade.stop_loss, 8) == 1.17 + assert pytest.approx(trade.stop_loss) == 1.17 assert trade.initial_stop_loss == 0.95 assert trade.initial_stop_loss_pct == -0.05 # current rate higher... should raise stoploss trade.adjust_stop_loss(1.4, 0.1) - assert round(trade.stop_loss, 8) == 1.26 + assert pytest.approx(trade.stop_loss) == 1.26 assert trade.initial_stop_loss == 0.95 assert trade.initial_stop_loss_pct == -0.05 # Initial is true but stop_loss set - so doesn't do anything trade.adjust_stop_loss(1.7, 0.1, True) - assert round(trade.stop_loss, 8) == 1.26 + assert pytest.approx(trade.stop_loss) == 1.26 assert trade.initial_stop_loss == 0.95 assert trade.initial_stop_loss_pct == -0.05 assert trade.stop_loss_pct == -0.1 From 9852733ef7f7284765b8092eb6cd27edd2b7c0e8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 29 Jul 2022 06:58:59 +0200 Subject: [PATCH 10/56] Improve tests to align with modified logic --- freqtrade/persistence/trade_model.py | 6 ------ tests/test_persistence.py | 17 ++++++++++------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 2ff65d9d0..919750886 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -535,14 +535,8 @@ class LocalTrade(): leverage = self.leverage or 1.0 if self.is_short: new_loss = float(current_price * (1 + abs(stoploss / leverage))) - # If trading with leverage, don't set the stoploss below the liquidation price - if self.liquidation_price: - new_loss = min(self.liquidation_price, new_loss) else: new_loss = float(current_price * (1 - abs(stoploss / leverage))) - # If trading with leverage, don't set the stoploss below the liquidation price - if self.liquidation_price: - new_loss = max(self.liquidation_price, new_loss) # no stop loss assigned yet if self.initial_stop_loss_pct is None or refresh: diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 4703075eb..5476e1d50 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -133,13 +133,14 @@ def test_set_stop_loss_isolated_liq(fee): trade.set_isolated_liq(0.11) trade._set_stop_loss(0.1, 0) assert trade.liquidation_price == 0.11 - assert trade.stop_loss == 0.11 + # Stoploss does not change from liquidation price + assert trade.stop_loss == 0.1 assert trade.initial_stop_loss == 0.1 # lower stop doesn't move stoploss trade._set_stop_loss(0.1, 0) assert trade.liquidation_price == 0.11 - assert trade.stop_loss == 0.11 + assert trade.stop_loss == 0.1 assert trade.initial_stop_loss == 0.1 trade.stop_loss = None @@ -174,13 +175,14 @@ def test_set_stop_loss_isolated_liq(fee): trade.set_isolated_liq(0.07) trade._set_stop_loss(0.1, (1.0 / 8.0)) assert trade.liquidation_price == 0.07 - assert trade.stop_loss == 0.07 + # Stoploss does not change from liquidation price + assert trade.stop_loss == 0.1 assert trade.initial_stop_loss == 0.08 # Stop doesn't move stop higher trade._set_stop_loss(0.1, (1.0 / 9.0)) assert trade.liquidation_price == 0.07 - assert trade.stop_loss == 0.07 + assert trade.stop_loss == 0.1 assert trade.initial_stop_loss == 0.08 @@ -1609,9 +1611,10 @@ def test_adjust_stop_loss_short(fee): assert trade.initial_stop_loss == 1.05 assert trade.initial_stop_loss_pct == -0.05 assert trade.stop_loss_pct == -0.1 + # Liquidation price is lower than stoploss - so liquidation would trigger first. trade.set_isolated_liq(0.63) trade.adjust_stop_loss(0.59, -0.1) - assert trade.stop_loss == 0.63 + assert trade.stop_loss == 0.649 assert trade.liquidation_price == 0.63 @@ -2011,8 +2014,8 @@ def test_stoploss_reinitialization_short(default_conf, fee): # Stoploss can't go above liquidation price trade_adj.set_isolated_liq(0.985) trade.adjust_stop_loss(0.9799, -0.05) - assert trade_adj.stop_loss == 0.985 - assert trade_adj.stop_loss == 0.985 + assert trade_adj.stop_loss == 0.989699 + assert trade_adj.liquidation_price == 0.985 def test_update_fee(fee): From ff4cc5d3165b53058c1b80f8f94bb16df2345269 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 29 Jul 2022 07:14:25 +0200 Subject: [PATCH 11/56] Revamp liquidation test to actually make sense --- tests/test_persistence.py | 79 +++++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 33 deletions(-) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 5476e1d50..5dbd6b86b 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -120,70 +120,83 @@ def test_set_stop_loss_isolated_liq(fee): assert trade.stop_loss is None assert trade.initial_stop_loss is None - trade._set_stop_loss(0.1, (1.0 / 9.0)) + trade.adjust_stop_loss(2.0, 0.2, True) assert trade.liquidation_price == 0.09 - assert trade.stop_loss == 0.1 - assert trade.initial_stop_loss == 0.1 + assert trade.stop_loss == 1.8 + assert trade.initial_stop_loss == 1.8 trade.set_isolated_liq(0.08) assert trade.liquidation_price == 0.08 - assert trade.stop_loss == 0.1 - assert trade.initial_stop_loss == 0.1 + assert trade.stop_loss == 1.8 + assert trade.initial_stop_loss == 1.8 trade.set_isolated_liq(0.11) - trade._set_stop_loss(0.1, 0) + trade.adjust_stop_loss(2.0, 0.2) assert trade.liquidation_price == 0.11 # Stoploss does not change from liquidation price - assert trade.stop_loss == 0.1 - assert trade.initial_stop_loss == 0.1 + assert trade.stop_loss == 1.8 + assert trade.initial_stop_loss == 1.8 # lower stop doesn't move stoploss - trade._set_stop_loss(0.1, 0) + trade.adjust_stop_loss(1.8, 0.2) assert trade.liquidation_price == 0.11 - assert trade.stop_loss == 0.1 - assert trade.initial_stop_loss == 0.1 + assert trade.stop_loss == 1.8 + assert trade.initial_stop_loss == 1.8 + + # higher stop does move stoploss + trade.adjust_stop_loss(2.1, 0.1) + assert trade.liquidation_price == 0.11 + assert pytest.approx(trade.stop_loss) == 1.994999 + assert trade.initial_stop_loss == 1.8 trade.stop_loss = None trade.liquidation_price = None trade.initial_stop_loss = None + trade.initial_stop_loss_pct = None - trade._set_stop_loss(0.07, 0) + trade.adjust_stop_loss(2.0, 0.1, True) assert trade.liquidation_price is None - assert trade.stop_loss == 0.07 - assert trade.initial_stop_loss == 0.07 + assert trade.stop_loss == 1.9 + assert trade.initial_stop_loss == 1.9 trade.is_short = True trade.recalc_open_trade_value() trade.stop_loss = None trade.initial_stop_loss = None + trade.initial_stop_loss_pct = None - trade.set_isolated_liq(0.09) - assert trade.liquidation_price == 0.09 + trade.set_isolated_liq(3.09) + assert trade.liquidation_price == 3.09 assert trade.stop_loss is None assert trade.initial_stop_loss is None - trade._set_stop_loss(0.08, (1.0 / 9.0)) - assert trade.liquidation_price == 0.09 - assert trade.stop_loss == 0.08 - assert trade.initial_stop_loss == 0.08 + trade.adjust_stop_loss(2.0, 0.2) + assert trade.liquidation_price == 3.09 + assert trade.stop_loss == 2.2 + assert trade.initial_stop_loss == 2.2 - trade.set_isolated_liq(0.1) - assert trade.liquidation_price == 0.1 - assert trade.stop_loss == 0.08 - assert trade.initial_stop_loss == 0.08 + trade.set_isolated_liq(3.1) + assert trade.liquidation_price == 3.1 + assert trade.stop_loss == 2.2 + assert trade.initial_stop_loss == 2.2 - trade.set_isolated_liq(0.07) - trade._set_stop_loss(0.1, (1.0 / 8.0)) - assert trade.liquidation_price == 0.07 + trade.set_isolated_liq(3.8) + assert trade.liquidation_price == 3.8 # Stoploss does not change from liquidation price - assert trade.stop_loss == 0.1 - assert trade.initial_stop_loss == 0.08 + assert trade.stop_loss == 2.2 + assert trade.initial_stop_loss == 2.2 # Stop doesn't move stop higher - trade._set_stop_loss(0.1, (1.0 / 9.0)) - assert trade.liquidation_price == 0.07 - assert trade.stop_loss == 0.1 - assert trade.initial_stop_loss == 0.08 + trade.adjust_stop_loss(2.0, 0.3) + assert trade.liquidation_price == 3.8 + assert trade.stop_loss == 2.2 + assert trade.initial_stop_loss == 2.2 + + # Stoploss does move lower + trade.adjust_stop_loss(1.8, 0.1) + assert trade.liquidation_price == 3.8 + assert pytest.approx(trade.stop_loss) == 1.89 + assert trade.initial_stop_loss == 2.2 @pytest.mark.parametrize('exchange,is_short,lev,minutes,rate,interest,trading_mode', [ From 15752ce3c2d4b2131e9141c3aa496dda25c5d9d7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 29 Jul 2022 07:15:01 +0200 Subject: [PATCH 12/56] Rename set_stoploss method to be fully private --- freqtrade/persistence/trade_model.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 919750886..1b8bcc42f 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -506,10 +506,9 @@ class LocalTrade(): return self.liquidation_price = liquidation_price - def _set_stop_loss(self, stop_loss: float, percent: float): + def __set_stop_loss(self, stop_loss: float, percent: float): """ - Method you should use to set self.stop_loss. - Assures stop_loss is not passed the liquidation price + Method used internally to set self.stop_loss. """ if not self.stop_loss: self.initial_stop_loss = stop_loss @@ -540,7 +539,7 @@ class LocalTrade(): # no stop loss assigned yet if self.initial_stop_loss_pct is None or refresh: - self._set_stop_loss(new_loss, stoploss) + self.__set_stop_loss(new_loss, stoploss) self.initial_stop_loss = new_loss self.initial_stop_loss_pct = -1 * abs(stoploss) @@ -555,7 +554,7 @@ class LocalTrade(): # ? decreasing the minimum stoploss if (higher_stop and not self.is_short) or (lower_stop and self.is_short): logger.debug(f"{self.pair} - Adjusting stoploss...") - self._set_stop_loss(new_loss, stoploss) + self.__set_stop_loss(new_loss, stoploss) else: logger.debug(f"{self.pair} - Keeping current stoploss...") From 4da96bc511c1a01ee717f304d60696141797d200 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Jul 2022 07:27:24 +0200 Subject: [PATCH 13/56] Update docs --- docs/strategy-callbacks.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index f584bd1bb..59d221bfc 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -623,6 +623,7 @@ class AwesomeStrategy(IStrategy): !!! Warning `confirm_trade_exit()` can prevent stoploss exits, causing significant losses as this would ignore stoploss exits. + `confirm_trade_exit()` will not be called for Liquidations - as liquidations are forced by the exchange, and therefore cannot be rejected. ## Adjust trade position From 845cecd38fe0acfed72d99eddccc5aea37234cfc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Jul 2022 08:12:48 +0200 Subject: [PATCH 14/56] Add stoploss or liquidation property --- freqtrade/persistence/trade_model.py | 10 ++++++++++ freqtrade/strategy/interface.py | 7 ------- tests/test_persistence.py | 10 +++++++++- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 1b8bcc42f..244ca79cd 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -302,6 +302,16 @@ class LocalTrade(): # Futures properties funding_fees: Optional[float] = None + @property + def stoploss_or_liquidation(self) -> float: + if self.liquidation_price: + if self.is_short: + return min(self.stop_loss, self.liquidation_price) + else: + return max(self.stop_loss, self.liquidation_price) + + return self.stop_loss + @property def buy_tag(self) -> Optional[str]: """ diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index f50721583..824f31258 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -1063,13 +1063,6 @@ class IStrategy(ABC, HyperStrategyMixin): f"stoploss is {trade.stop_loss:.6f}, " f"initial stoploss was at {trade.initial_stop_loss:.6f}, " f"trade opened at {trade.open_rate:.6f}") - new_stoploss = ( - trade.stop_loss + trade.initial_stop_loss - if trade.is_short else - trade.stop_loss - trade.initial_stop_loss - ) - logger.debug(f"{trade.pair} - Trailing stop saved " - f"{new_stoploss:.6f}") return ExitCheckTuple(exit_type=exit_type) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 5dbd6b86b..3eca035c9 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -148,6 +148,7 @@ def test_set_stop_loss_isolated_liq(fee): assert trade.liquidation_price == 0.11 assert pytest.approx(trade.stop_loss) == 1.994999 assert trade.initial_stop_loss == 1.8 + assert trade.stoploss_or_liquidation == trade.stop_loss trade.stop_loss = None trade.liquidation_price = None @@ -158,6 +159,7 @@ def test_set_stop_loss_isolated_liq(fee): assert trade.liquidation_price is None assert trade.stop_loss == 1.9 assert trade.initial_stop_loss == 1.9 + assert trade.stoploss_or_liquidation == 1.9 trade.is_short = True trade.recalc_open_trade_value() @@ -174,11 +176,13 @@ def test_set_stop_loss_isolated_liq(fee): assert trade.liquidation_price == 3.09 assert trade.stop_loss == 2.2 assert trade.initial_stop_loss == 2.2 + assert trade.stoploss_or_liquidation == 2.2 trade.set_isolated_liq(3.1) assert trade.liquidation_price == 3.1 assert trade.stop_loss == 2.2 assert trade.initial_stop_loss == 2.2 + assert trade.stoploss_or_liquidation == 2.2 trade.set_isolated_liq(3.8) assert trade.liquidation_price == 3.8 @@ -193,10 +197,14 @@ def test_set_stop_loss_isolated_liq(fee): assert trade.initial_stop_loss == 2.2 # Stoploss does move lower + trade.set_isolated_liq(1.5) trade.adjust_stop_loss(1.8, 0.1) - assert trade.liquidation_price == 3.8 + assert trade.liquidation_price == 1.5 assert pytest.approx(trade.stop_loss) == 1.89 assert trade.initial_stop_loss == 2.2 + assert trade.stoploss_or_liquidation == 1.5 + + @pytest.mark.parametrize('exchange,is_short,lev,minutes,rate,interest,trading_mode', [ From dba7a7257dec1371c1bea14c099dbca06474e3ff Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Jul 2022 09:18:15 +0200 Subject: [PATCH 15/56] Use stop_or_liquidation instead of stop_loss --- freqtrade/freqtradebot.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d58c05d7f..7440212c3 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1015,7 +1015,7 @@ class FreqtradeBot(LoggingMixin): trade.stoploss_order_id = None logger.error(f'Unable to place a stoploss order on exchange. {e}') logger.warning('Exiting the trade forcefully') - self.execute_trade_exit(trade, trade.stop_loss, exit_check=ExitCheckTuple( + self.execute_trade_exit(trade, stop_price, exit_check=ExitCheckTuple( exit_type=ExitType.EMERGENCY_EXIT)) except ExchangeError: @@ -1114,7 +1114,7 @@ class FreqtradeBot(LoggingMixin): :param order: Current on exchange stoploss order :return: None """ - stoploss_norm = self.exchange.price_to_precision(trade.pair, trade.stop_loss) + stoploss_norm = self.exchange.price_to_precision(trade.pair, trade.stoploss_or_liquidation) if self.exchange.stoploss_adjust(stoploss_norm, order, side=trade.exit_side): # we check if the update is necessary @@ -1132,7 +1132,7 @@ class FreqtradeBot(LoggingMixin): f"for pair {trade.pair}") # Create new stoploss order - if not self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss): + if not self.create_stoploss_order(trade=trade, stop_price=stoploss_norm): logger.warning(f"Could not create trailing stoploss order " f"for pair {trade.pair}.") @@ -1431,14 +1431,15 @@ class FreqtradeBot(LoggingMixin): ) exit_type = 'exit' exit_reason = exit_tag or exit_check.exit_reason - if exit_check.exit_type in (ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS): + if exit_check.exit_type in ( + ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS, ExitType.LIQUIDATION): exit_type = 'stoploss' # if stoploss is on exchange and we are on dry_run mode, # we consider the sell price stop price if (self.config['dry_run'] and exit_type == 'stoploss' and self.strategy.order_types['stoploss_on_exchange']): - limit = trade.stop_loss + limit = trade.stoploss_or_liquidation # set custom_exit_price if available proposed_limit_rate = limit From d046f0cc5e766ec24f5c88fd967b44ea88698481 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Jul 2022 09:19:48 +0200 Subject: [PATCH 16/56] Improve method wording for liquidation price setter --- freqtrade/freqtradebot.py | 4 ++-- freqtrade/optimize/backtesting.py | 2 +- freqtrade/persistence/trade_model.py | 2 +- tests/test_persistence.py | 22 ++++++++++------------ 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7440212c3..3490b58d6 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1085,7 +1085,7 @@ class FreqtradeBot(LoggingMixin): if (trade.is_open and stoploss_order and stoploss_order['status'] in ('canceled', 'cancelled')): - if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss): + if self.create_stoploss_order(trade=trade, stop_price=trade.stoploss_or_liquidation): return False else: trade.stoploss_order_id = None @@ -1662,7 +1662,7 @@ class FreqtradeBot(LoggingMixin): trade = self.cancel_stoploss_on_exchange(trade) # TODO: Margin will need to use interest_rate as well. # interest_rate = self.exchange.get_interest_rate() - trade.set_isolated_liq(self.exchange.get_liquidation_price( + trade.set_liquidation_price(self.exchange.get_liquidation_price( leverage=trade.leverage, pair=trade.pair, amount=trade.amount, diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 6bbace185..2c6cfb0e9 100755 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -814,7 +814,7 @@ class Backtesting: trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True) - trade.set_isolated_liq(self.exchange.get_liquidation_price( + trade.set_liquidation_price(self.exchange.get_liquidation_price( pair=pair, open_rate=propose_rate, amount=amount, diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 244ca79cd..44e148a0c 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -507,7 +507,7 @@ class LocalTrade(): self.max_rate = max(current_price, self.max_rate or self.open_rate) self.min_rate = min(current_price_low, self.min_rate or self.open_rate) - def set_isolated_liq(self, liquidation_price: Optional[float]): + def set_liquidation_price(self, liquidation_price: Optional[float]): """ Method you should use to set self.liquidation price. Assures stop_loss is not passed the liquidation price diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 3eca035c9..0c1fc01a5 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -99,7 +99,7 @@ def test_enter_exit_side(fee, is_short): @pytest.mark.usefixtures("init_persistence") -def test_set_stop_loss_isolated_liq(fee): +def test_set_stop_loss_liquidation(fee): trade = Trade( id=2, pair='ADA/USDT', @@ -115,7 +115,7 @@ def test_set_stop_loss_isolated_liq(fee): leverage=2.0, trading_mode=margin ) - trade.set_isolated_liq(0.09) + trade.set_liquidation_price(0.09) assert trade.liquidation_price == 0.09 assert trade.stop_loss is None assert trade.initial_stop_loss is None @@ -125,12 +125,12 @@ def test_set_stop_loss_isolated_liq(fee): assert trade.stop_loss == 1.8 assert trade.initial_stop_loss == 1.8 - trade.set_isolated_liq(0.08) + trade.set_liquidation_price(0.08) assert trade.liquidation_price == 0.08 assert trade.stop_loss == 1.8 assert trade.initial_stop_loss == 1.8 - trade.set_isolated_liq(0.11) + trade.set_liquidation_price(0.11) trade.adjust_stop_loss(2.0, 0.2) assert trade.liquidation_price == 0.11 # Stoploss does not change from liquidation price @@ -167,7 +167,7 @@ def test_set_stop_loss_isolated_liq(fee): trade.initial_stop_loss = None trade.initial_stop_loss_pct = None - trade.set_isolated_liq(3.09) + trade.set_liquidation_price(3.09) assert trade.liquidation_price == 3.09 assert trade.stop_loss is None assert trade.initial_stop_loss is None @@ -178,13 +178,13 @@ def test_set_stop_loss_isolated_liq(fee): assert trade.initial_stop_loss == 2.2 assert trade.stoploss_or_liquidation == 2.2 - trade.set_isolated_liq(3.1) + trade.set_liquidation_price(3.1) assert trade.liquidation_price == 3.1 assert trade.stop_loss == 2.2 assert trade.initial_stop_loss == 2.2 assert trade.stoploss_or_liquidation == 2.2 - trade.set_isolated_liq(3.8) + trade.set_liquidation_price(3.8) assert trade.liquidation_price == 3.8 # Stoploss does not change from liquidation price assert trade.stop_loss == 2.2 @@ -197,7 +197,7 @@ def test_set_stop_loss_isolated_liq(fee): assert trade.initial_stop_loss == 2.2 # Stoploss does move lower - trade.set_isolated_liq(1.5) + trade.set_liquidation_price(1.5) trade.adjust_stop_loss(1.8, 0.1) assert trade.liquidation_price == 1.5 assert pytest.approx(trade.stop_loss) == 1.89 @@ -205,8 +205,6 @@ def test_set_stop_loss_isolated_liq(fee): assert trade.stoploss_or_liquidation == 1.5 - - @pytest.mark.parametrize('exchange,is_short,lev,minutes,rate,interest,trading_mode', [ ("binance", False, 3, 10, 0.0005, round(0.0008333333333333334, 8), margin), ("binance", True, 3, 10, 0.0005, 0.000625, margin), @@ -1633,7 +1631,7 @@ def test_adjust_stop_loss_short(fee): assert trade.initial_stop_loss_pct == -0.05 assert trade.stop_loss_pct == -0.1 # Liquidation price is lower than stoploss - so liquidation would trigger first. - trade.set_isolated_liq(0.63) + trade.set_liquidation_price(0.63) trade.adjust_stop_loss(0.59, -0.1) assert trade.stop_loss == 0.649 assert trade.liquidation_price == 0.63 @@ -2033,7 +2031,7 @@ def test_stoploss_reinitialization_short(default_conf, fee): assert trade_adj.initial_stop_loss == 1.01 assert trade_adj.initial_stop_loss_pct == -0.05 # Stoploss can't go above liquidation price - trade_adj.set_isolated_liq(0.985) + trade_adj.set_liquidation_price(0.985) trade.adjust_stop_loss(0.9799, -0.05) assert trade_adj.stop_loss == 0.989699 assert trade_adj.liquidation_price == 0.985 From dc82675f00e5ed1b006eeb1490a3f14147cde979 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Jul 2022 17:28:16 +0200 Subject: [PATCH 17/56] Add Test for liquidation in stop-loss-reached --- tests/strategy/test_interface.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index f6996a7a2..4257b2cf9 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -408,28 +408,31 @@ def test_min_roi_reached3(default_conf, fee) -> None: @pytest.mark.parametrize( - 'profit,adjusted,expected,trailing,custom,profit2,adjusted2,expected2,custom_stop', [ + 'profit,adjusted,expected,liq,trailing,custom,profit2,adjusted2,expected2,custom_stop', [ # Profit, adjusted stoploss(absolute), profit for 2nd call, enable trailing, # enable custom stoploss, expected after 1st call, expected after 2nd call - (0.2, 0.9, ExitType.NONE, False, False, 0.3, 0.9, ExitType.NONE, None), - (0.2, 0.9, ExitType.NONE, False, False, -0.2, 0.9, ExitType.STOP_LOSS, None), - (0.2, 1.14, ExitType.NONE, True, False, 0.05, 1.14, ExitType.TRAILING_STOP_LOSS, None), - (0.01, 0.96, ExitType.NONE, True, False, 0.05, 1, ExitType.NONE, None), - (0.05, 1, ExitType.NONE, True, False, -0.01, 1, ExitType.TRAILING_STOP_LOSS, None), + (0.2, 0.9, ExitType.NONE, None, False, False, 0.3, 0.9, ExitType.NONE, None), + (0.2, 0.9, ExitType.NONE, None, False, False, -0.2, 0.9, ExitType.STOP_LOSS, None), + (0.2, 0.9, ExitType.NONE, 0.8, False, False, -0.2, 0.9, ExitType.LIQUIDATION, None), + (0.2, 1.14, ExitType.NONE, None, True, False, 0.05, 1.14, ExitType.TRAILING_STOP_LOSS, + None), + (0.01, 0.96, ExitType.NONE, None, True, False, 0.05, 1, ExitType.NONE, None), + (0.05, 1, ExitType.NONE, None, True, False, -0.01, 1, ExitType.TRAILING_STOP_LOSS, None), # Default custom case - trails with 10% - (0.05, 0.95, ExitType.NONE, False, True, -0.02, 0.95, ExitType.NONE, None), - (0.05, 0.95, ExitType.NONE, False, True, -0.06, 0.95, ExitType.TRAILING_STOP_LOSS, None), - (0.05, 1, ExitType.NONE, False, True, -0.06, 1, ExitType.TRAILING_STOP_LOSS, + (0.05, 0.95, ExitType.NONE, None, False, True, -0.02, 0.95, ExitType.NONE, None), + (0.05, 0.95, ExitType.NONE, None, False, True, -0.06, 0.95, ExitType.TRAILING_STOP_LOSS, + None), + (0.05, 1, ExitType.NONE, None, False, True, -0.06, 1, ExitType.TRAILING_STOP_LOSS, lambda **kwargs: -0.05), - (0.05, 1, ExitType.NONE, False, True, 0.09, 1.04, ExitType.NONE, + (0.05, 1, ExitType.NONE, None, False, True, 0.09, 1.04, ExitType.NONE, lambda **kwargs: -0.05), - (0.05, 0.95, ExitType.NONE, False, True, 0.09, 0.98, ExitType.NONE, + (0.05, 0.95, ExitType.NONE, None, False, True, 0.09, 0.98, ExitType.NONE, lambda current_profit, **kwargs: -0.1 if current_profit < 0.6 else -(current_profit * 2)), # Error case - static stoploss in place - (0.05, 0.9, ExitType.NONE, False, True, 0.09, 0.9, ExitType.NONE, + (0.05, 0.9, ExitType.NONE, None, False, True, 0.09, 0.9, ExitType.NONE, lambda **kwargs: None), ]) -def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, trailing, custom, +def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, liq, trailing, custom, profit2, adjusted2, expected2, custom_stop) -> None: strategy = StrategyResolver.load_strategy(default_conf) @@ -442,6 +445,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili fee_close=fee.return_value, exchange='binance', open_rate=1, + liquidation_price=liq, ) trade.adjust_min_max_rates(trade.open_rate, trade.open_rate) strategy.trailing_stop = trailing From bad15f077c66710314b7dea75d0e5f7abee767a3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Jul 2022 17:49:06 +0200 Subject: [PATCH 18/56] Simplify fetch_positions by using already existing method --- freqtrade/exchange/exchange.py | 33 ++++++++++++++++----------------- tests/exchange/test_exchange.py | 14 -------------- 2 files changed, 16 insertions(+), 31 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 79bc769e6..e180c90b2 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1332,11 +1332,19 @@ class Exchange: raise OperationalException(e) from e @retrier - def fetch_positions(self) -> List[Dict]: + def fetch_positions(self, pair: str = None) -> List[Dict]: + """ + Fetch positions from the exchange. + If no pair is given, all positions are returned. + :param pair: Pair for the query + """ if self._config['dry_run'] or self.trading_mode != TradingMode.FUTURES: return [] try: - positions: List[Dict] = self._api.fetch_positions() + symbols = [] + if pair: + symbols.append(pair) + positions: List[Dict] = self._api.fetch_positions(symbols) self._log_exchange_response('fetch_positions', positions) return positions except ccxt.DDoSProtection as e: @@ -2539,7 +2547,6 @@ class Exchange: else: return 0.0 - @retrier def get_or_calculate_liquidation_price( self, pair: str, @@ -2573,20 +2580,12 @@ class Exchange: upnl_ex_1=upnl_ex_1 ) else: - try: - positions = self._api.fetch_positions([pair]) - if len(positions) > 0: - pos = positions[0] - isolated_liq = pos['liquidationPrice'] - else: - return None - except ccxt.DDoSProtection as e: - raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e - except ccxt.BaseError as e: - raise OperationalException(e) from e + positions = self.fetch_positions(pair) + if len(positions) > 0: + pos = positions[0] + isolated_liq = pos['liquidationPrice'] + else: + return None if isolated_liq: buffer_amount = abs(open_rate - isolated_liq) * self.liquidation_buffer diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 9252040ea..e968b12c2 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -4099,20 +4099,6 @@ def test_get_or_calculate_liquidation_price(mocker, default_conf): ) assert liq_price == 17.540699999999998 - ccxt_exceptionhandlers( - mocker, - default_conf, - api_mock, - "binance", - "get_or_calculate_liquidation_price", - "fetch_positions", - pair="XRP/USDT", - open_rate=0.0, - is_short=False, - position=0.0, - wallet_balance=0.0, - ) - @pytest.mark.parametrize('exchange,rate_start,rate_end,d1,d2,amount,expected_fees', [ ('binance', 0, 2, "2021-09-01 01:00:00", "2021-09-01 04:00:00", 30.0, 0.0), From 09e5fb2f55cf368b8a3126fb1f00be2f8eb1f613 Mon Sep 17 00:00:00 2001 From: rzrymiak <106121613+rzrymiak@users.noreply.github.com> Date: Sat, 30 Jul 2022 22:37:46 +0000 Subject: [PATCH 19/56] Removed description header --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 828a3d1f9..aa446ad54 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,6 @@ [![Documentation](https://readthedocs.org/projects/freqtrade/badge/)](https://www.freqtrade.io) [![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability) -## Description - Freqtrade is a free and open source crypto trading bot written in Python. It is designed to support all major exchanges and be controlled via Telegram or webUI. It contains backtesting, plotting and money management tools as well as strategy optimization by machine learning. ![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade-screenshot.png) From a4bada3ebe5ff927503b067545260ef5fe608c87 Mon Sep 17 00:00:00 2001 From: Kavinkumar <33546454+mkavinkumar1@users.noreply.github.com> Date: Sun, 31 Jul 2022 17:49:04 +0530 Subject: [PATCH 20/56] Partial exit using average price (#6545) Introduce Partial exits --- docs/strategy-callbacks.md | 67 ++- freqtrade/enums/exittype.py | 1 + freqtrade/exchange/exchange.py | 44 +- freqtrade/freqtradebot.py | 194 +++++--- freqtrade/optimize/backtesting.py | 115 +++-- freqtrade/persistence/migrations.py | 10 +- freqtrade/persistence/trade_model.py | 160 +++++-- freqtrade/rpc/rpc.py | 2 +- freqtrade/rpc/telegram.py | 101 +++-- freqtrade/strategy/interface.py | 19 +- .../subtemplates/strategy_methods_advanced.j2 | 26 +- tests/conftest.py | 3 +- tests/exchange/test_exchange.py | 199 ++++++--- .../test_backtesting_adjust_position.py | 86 ++++ tests/rpc/test_rpc.py | 5 +- tests/rpc/test_rpc_telegram.py | 102 ++++- tests/strategy/strats/strategy_test_v3.py | 9 +- tests/test_freqtradebot.py | 414 ++++++++++++++++-- tests/test_integration.py | 59 ++- tests/test_persistence.py | 193 +++++++- 20 files changed, 1462 insertions(+), 347 deletions(-) mode change 100755 => 100644 freqtrade/optimize/backtesting.py diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 59d221bfc..18de3513b 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -629,7 +629,7 @@ class AwesomeStrategy(IStrategy): The `position_adjustment_enable` strategy property enables the usage of `adjust_trade_position()` callback in the strategy. For performance reasons, it's disabled by default and freqtrade will show a warning message on startup if enabled. -`adjust_trade_position()` can be used to perform additional orders, for example to manage risk with DCA (Dollar Cost Averaging). +`adjust_trade_position()` can be used to perform additional orders, for example to manage risk with DCA (Dollar Cost Averaging) or to increase or decrease positions. `max_entry_position_adjustment` property is used to limit the number of additional buys per trade (on top of the first buy) that the bot can execute. By default, the value is -1 which means the bot have no limit on number of adjustment buys. @@ -637,10 +637,13 @@ The strategy is expected to return a stake_amount (in stake currency) between `m If there are not enough funds in the wallet (the return value is above `max_stake`) then the signal will be ignored. Additional orders also result in additional fees and those orders don't count towards `max_open_trades`. -This callback is **not** called when there is an open order (either buy or sell) waiting for execution, or when you have reached the maximum amount of extra buys that you have set on `max_entry_position_adjustment`. +This callback is **not** called when there is an open order (either buy or sell) waiting for execution. + `adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible. -Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position, no matter if it's a long or short trade. Modifications to leverage are not possible. +Additional Buys are ignored once you have reached the maximum amount of extra buys that you have set on `max_entry_position_adjustment`, but the callback is called anyway looking for partial exits. + +Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position (negative values will decrease your position), no matter if it's a long or short trade. Modifications to leverage are not possible. !!! Note "About stake size" Using fixed stake size means it will be the amount used for the first order, just like without position adjustment. @@ -649,12 +652,12 @@ Position adjustments will always be applied in the direction of the trade, so a !!! Warning Stoploss is still calculated from the initial opening price, not averaged price. + Regular stoploss rules still apply (cannot move down). -!!! Warning "/stopbuy" While `/stopbuy` command stops the bot from entering new trades, the position adjustment feature will continue buying new orders on existing trades. !!! Warning "Backtesting" - During backtesting this callback is called for each candle in `timeframe` or `timeframe_detail`, so performance will be affected. + During backtesting this callback is called for each candle in `timeframe` or `timeframe_detail`, so run-time performance will be affected. ``` python from freqtrade.persistence import Trade @@ -675,7 +678,7 @@ class DigDeeperStrategy(IStrategy): max_dca_multiplier = 5.5 # This is called when placing the initial order (opening trade) -def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, + def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, proposed_stake: float, min_stake: Optional[float], max_stake: float, leverage: float, entry_tag: Optional[str], side: str, **kwargs) -> float: @@ -685,22 +688,41 @@ def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: f return proposed_stake / self.max_dca_multiplier def adjust_trade_position(self, trade: Trade, current_time: datetime, - current_rate: float, current_profit: float, min_stake: Optional[float], - max_stake: float, **kwargs): + current_rate: float, current_profit: float, + min_stake: Optional[float], max_stake: float, + current_entry_rate: float, current_exit_rate: float, + current_entry_profit: float, current_exit_profit: float, + **kwargs) -> Optional[float]: """ - Custom trade adjustment logic, returning the stake amount that a trade should be increased. - This means extra buy orders with additional fees. + Custom trade adjustment logic, returning the stake amount that a trade should be + increased or decreased. + This means extra buy or sell orders with additional fees. + Only called when `position_adjustment_enable` is set to True. + + For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ + + When not implemented by a strategy, returns None :param trade: trade object. :param current_time: datetime object, containing the current datetime :param current_rate: Current buy rate. :param current_profit: Current profit (as ratio), calculated based on current_rate. - :param min_stake: Minimal stake size allowed by exchange. - :param max_stake: Balance available for trading. + :param min_stake: Minimal stake size allowed by exchange (for both entries and exits) + :param max_stake: Maximum stake allowed (either through balance, or by exchange limits). + :param current_entry_rate: Current rate using entry pricing. + :param current_exit_rate: Current rate using exit pricing. + :param current_entry_profit: Current profit using entry pricing. + :param current_exit_profit: Current profit using exit pricing. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. - :return float: Stake amount to adjust your trade + :return float: Stake amount to adjust your trade, + Positive values to increase position, Negative values to decrease position. + Return None for no action. """ + if current_profit > 0.05 and trade.nr_of_successful_exits == 0: + # Take half of the profit at +5% + return -(trade.amount / 2) + if current_profit > -0.05: return None @@ -735,6 +757,25 @@ def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: f ``` +### Position adjust calculations + +* Entry rates are calculated using weighted averages. +* Exits will not influence the average entry rate. +* Partial exit relative profit is relative to the average entry price at this point. +* Final exit relative profit is calculated based on the total invested capital. (See example below) + +??? example "Calculation example" + *This example assumes 0 fees for simplicity, and a long position on an imaginary coin.* + + * Buy 100@8\$ + * Buy 100@9\$ -> Avg price: 8.5\$ + * Sell 100@10\$ -> Avg price: 8.5\$, realized profit 150\$, 17.65% + * Buy 150@11\$ -> Avg price: 10\$, realized profit 150\$, 17.65% + * Sell 100@12\$ -> Avg price: 10\$, total realized profit 350\$, 20% + * Sell 150@14\$ -> Avg price: 10\$, total realized profit 950\$, 40% + + The total profit for this trade was 950$ on a 3350$ investment (`100@8$ + 100@9$ + 150@11$`). As such - the final relative profit is 28.35% (`950 / 3350`). + ## Adjust Entry Price The `adjust_entry_price()` callback may be used by strategy developer to refresh/replace limit orders upon arrival of new candles. diff --git a/freqtrade/enums/exittype.py b/freqtrade/enums/exittype.py index 1e15e70cd..b025230ba 100644 --- a/freqtrade/enums/exittype.py +++ b/freqtrade/enums/exittype.py @@ -14,6 +14,7 @@ class ExitType(Enum): FORCE_EXIT = "force_exit" EMERGENCY_EXIT = "emergency_exit" CUSTOM_EXIT = "custom_exit" + PARTIAL_EXIT = "partial_exit" NONE = "" def __str__(self): diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index e180c90b2..b6996211f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1507,7 +1507,8 @@ class Exchange: return price_side def get_rate(self, pair: str, refresh: bool, - side: EntryExit, is_short: bool) -> float: + side: EntryExit, is_short: bool, + order_book: Optional[dict] = None, ticker: Optional[dict] = None) -> float: """ Calculates bid/ask target bid rate - between current ask price and last price @@ -1539,22 +1540,24 @@ class Exchange: if conf_strategy.get('use_order_book', False): order_book_top = conf_strategy.get('order_book_top', 1) - order_book = self.fetch_l2_order_book(pair, order_book_top) + if order_book is None: + order_book = self.fetch_l2_order_book(pair, order_book_top) logger.debug('order_book %s', order_book) # top 1 = index 0 try: rate = order_book[f"{price_side}s"][order_book_top - 1][0] except (IndexError, KeyError) as e: logger.warning( - f"{name} Price at location {order_book_top} from orderbook could not be " - f"determined. Orderbook: {order_book}" + f"{pair} - {name} Price at location {order_book_top} from orderbook " + f"could not be determined. Orderbook: {order_book}" ) raise PricingError from e - logger.debug(f"{name} price from orderbook {price_side_word}" + logger.debug(f"{pair} - {name} price from orderbook {price_side_word}" f"side - top {order_book_top} order book {side} rate {rate:.8f}") else: logger.debug(f"Using Last {price_side_word} / Last Price") - ticker = self.fetch_ticker(pair) + if ticker is None: + ticker = self.fetch_ticker(pair) ticker_rate = ticker[price_side] if ticker['last'] and ticker_rate: if side == 'entry' and ticker_rate > ticker['last']: @@ -1571,6 +1574,33 @@ class Exchange: return rate + def get_rates(self, pair: str, refresh: bool, is_short: bool) -> Tuple[float, float]: + entry_rate = None + exit_rate = None + if not refresh: + entry_rate = self._entry_rate_cache.get(pair) + exit_rate = self._exit_rate_cache.get(pair) + if entry_rate: + logger.debug(f"Using cached buy rate for {pair}.") + if exit_rate: + logger.debug(f"Using cached sell rate for {pair}.") + + entry_pricing = self._config.get('entry_pricing', {}) + exit_pricing = self._config.get('exit_pricing', {}) + order_book = ticker = None + if not entry_rate and entry_pricing.get('use_order_book', False): + order_book_top = max(entry_pricing.get('order_book_top', 1), + exit_pricing.get('order_book_top', 1)) + order_book = self.fetch_l2_order_book(pair, order_book_top) + entry_rate = self.get_rate(pair, refresh, 'entry', is_short, order_book=order_book) + elif not entry_rate: + ticker = self.fetch_ticker(pair) + entry_rate = self.get_rate(pair, refresh, 'entry', is_short, ticker=ticker) + if not exit_rate: + exit_rate = self.get_rate(pair, refresh, 'exit', + is_short, order_book=order_book, ticker=ticker) + return entry_rate, exit_rate + # Fee handling @retrier @@ -1989,7 +2019,7 @@ class Exchange: else: logger.debug( "Fetching trades for pair %s, since %s %s...", - pair, since, + pair, since, '(' + arrow.get(since // 1000).isoformat() + ') ' if since is not None else '' ) trades = await self._api_async.fetch_trades(pair, since=since, limit=1000) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 50cfb9d7b..757449c8c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -5,6 +5,7 @@ import copy import logging import traceback from datetime import datetime, time, timedelta, timezone +from decimal import Decimal from math import isclose from threading import Lock from typing import Any, Dict, List, Optional, Tuple @@ -525,39 +526,61 @@ class FreqtradeBot(LoggingMixin): If the strategy triggers the adjustment, a new order gets issued. Once that completes, the existing trade is modified to match new data. """ - if self.strategy.max_entry_position_adjustment > -1: - count_of_buys = trade.nr_of_successful_entries - if count_of_buys > self.strategy.max_entry_position_adjustment: - logger.debug(f"Max adjustment entries for {trade.pair} has been reached.") - return - else: - logger.debug("Max adjustment entries is set to unlimited.") - current_rate = self.exchange.get_rate( - trade.pair, side='entry', is_short=trade.is_short, refresh=True) - current_profit = trade.calc_profit_ratio(current_rate) + current_entry_rate, current_exit_rate = self.exchange.get_rates( + trade.pair, True, trade.is_short) - min_stake_amount = self.exchange.get_min_pair_stake_amount(trade.pair, - current_rate, - self.strategy.stoploss) - max_stake_amount = self.exchange.get_max_pair_stake_amount(trade.pair, current_rate) + current_entry_profit = trade.calc_profit_ratio(current_entry_rate) + current_exit_profit = trade.calc_profit_ratio(current_exit_rate) + + min_entry_stake = self.exchange.get_min_pair_stake_amount(trade.pair, + current_entry_rate, + self.strategy.stoploss) + min_exit_stake = self.exchange.get_min_pair_stake_amount(trade.pair, + current_exit_rate, + self.strategy.stoploss) + max_entry_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_entry_rate) stake_available = self.wallets.get_available_stake_amount() logger.debug(f"Calling adjust_trade_position for pair {trade.pair}") stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position, default_retval=None)( - trade=trade, current_time=datetime.now(timezone.utc), current_rate=current_rate, - current_profit=current_profit, min_stake=min_stake_amount, - max_stake=min(max_stake_amount, stake_available)) + trade=trade, + current_time=datetime.now(timezone.utc), current_rate=current_entry_rate, + current_profit=current_entry_profit, min_stake=min_entry_stake, + max_stake=min(max_entry_stake, stake_available), + current_entry_rate=current_entry_rate, current_exit_rate=current_exit_rate, + current_entry_profit=current_entry_profit, current_exit_profit=current_exit_profit + ) if stake_amount is not None and stake_amount > 0.0: # We should increase our position - self.execute_entry(trade.pair, stake_amount, price=current_rate, + if self.strategy.max_entry_position_adjustment > -1: + count_of_entries = trade.nr_of_successful_entries + if count_of_entries > self.strategy.max_entry_position_adjustment: + logger.debug(f"Max adjustment entries for {trade.pair} has been reached.") + return + else: + logger.debug("Max adjustment entries is set to unlimited.") + self.execute_entry(trade.pair, stake_amount, price=current_entry_rate, trade=trade, is_short=trade.is_short) if stake_amount is not None and stake_amount < 0.0: # We should decrease our position - # TODO: Selling part of the trade not implemented yet. - logger.error(f"Unable to decrease trade position / sell partially" - f" for pair {trade.pair}, feature not implemented.") + amount = abs(float(Decimal(stake_amount) / Decimal(current_exit_rate))) + if amount > trade.amount: + # This is currently ineffective as remaining would become < min tradable + # Fixing this would require checking for 0.0 there - + # if we decide that this callback is allowed to "fully exit" + logger.info( + f"Adjusting amount to trade.amount as it is higher. {amount} > {trade.amount}") + amount = trade.amount + + remaining = (trade.amount - amount) * current_exit_rate + if remaining < min_exit_stake: + logger.info(f'Remaining amount of {remaining} would be too small.') + return + + self.execute_trade_exit(trade, current_exit_rate, exit_check=ExitCheckTuple( + exit_type=ExitType.PARTIAL_EXIT), sub_trade_amt=amount) def _check_depth_of_market(self, pair: str, conf: Dict, side: SignalDirection) -> bool: """ @@ -731,7 +754,7 @@ class FreqtradeBot(LoggingMixin): # Updating wallets self.wallets.update() - self._notify_enter(trade, order, order_type) + self._notify_enter(trade, order_obj, order_type, sub_trade=pos_adjust) if pos_adjust: if order_status == 'closed': @@ -740,8 +763,8 @@ class FreqtradeBot(LoggingMixin): else: logger.info(f"DCA order {order_status}, will wait for resolution: {trade}") - # Update fees if order is closed - if order_status == 'closed': + # Update fees if order is non-opened + if order_status in constants.NON_OPEN_EXCHANGE_STATES: self.update_trade_state(trade, order_id, order) return True @@ -830,13 +853,14 @@ class FreqtradeBot(LoggingMixin): return enter_limit_requested, stake_amount, leverage - def _notify_enter(self, trade: Trade, order: Dict, order_type: Optional[str] = None, - fill: bool = False) -> None: + def _notify_enter(self, trade: Trade, order: Order, order_type: Optional[str] = None, + fill: bool = False, sub_trade: bool = False) -> None: """ Sends rpc notification when a entry order occurred. """ msg_type = RPCMessageType.ENTRY_FILL if fill else RPCMessageType.ENTRY - open_rate = safe_value_fallback(order, 'average', 'price') + open_rate = order.safe_price + if open_rate is None: open_rate = trade.open_rate @@ -860,15 +884,17 @@ class FreqtradeBot(LoggingMixin): 'stake_amount': trade.stake_amount, 'stake_currency': self.config['stake_currency'], 'fiat_currency': self.config.get('fiat_display_currency', None), - 'amount': safe_value_fallback(order, 'filled', 'amount') or trade.amount, + 'amount': order.safe_amount_after_fee, 'open_date': trade.open_date or datetime.utcnow(), 'current_rate': current_rate, + 'sub_trade': sub_trade, } # Send the message self.rpc.send_msg(msg) - def _notify_enter_cancel(self, trade: Trade, order_type: str, reason: str) -> None: + def _notify_enter_cancel(self, trade: Trade, order_type: str, reason: str, + sub_trade: bool = False) -> None: """ Sends rpc notification when a entry order cancel occurred. """ @@ -893,6 +919,7 @@ class FreqtradeBot(LoggingMixin): 'open_date': trade.open_date, 'current_rate': current_rate, 'reason': reason, + 'sub_trade': sub_trade, } # Send the message @@ -1366,16 +1393,22 @@ class FreqtradeBot(LoggingMixin): trade.open_order_id = None trade.exit_reason = None cancelled = True + self.wallets.update() else: # TODO: figure out how to handle partially complete sell orders reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] cancelled = False - self.wallets.update() + order_obj = trade.select_order_by_order_id(order['id']) + if not order_obj: + raise DependencyException( + f"Order_obj not found for {order['id']}. This should not have happened.") + + sub_trade = order_obj.amount != trade.amount self._notify_exit_cancel( trade, order_type=self.strategy.order_types['exit'], - reason=reason + reason=reason, order=order_obj, sub_trade=sub_trade ) return cancelled @@ -1416,6 +1449,7 @@ class FreqtradeBot(LoggingMixin): *, exit_tag: Optional[str] = None, ordertype: Optional[str] = None, + sub_trade_amt: float = None, ) -> bool: """ Executes a trade exit for the given trade and limit @@ -1439,7 +1473,7 @@ class FreqtradeBot(LoggingMixin): # if stoploss is on exchange and we are on dry_run mode, # we consider the sell price stop price if (self.config['dry_run'] and exit_type == 'stoploss' - and self.strategy.order_types['stoploss_on_exchange']): + and self.strategy.order_types['stoploss_on_exchange']): limit = trade.stoploss_or_liquidation # set custom_exit_price if available @@ -1462,15 +1496,17 @@ class FreqtradeBot(LoggingMixin): # Emergency sells (default to market!) order_type = self.strategy.order_types.get("emergency_exit", "market") - amount = self._safe_exit_amount(trade.pair, trade.amount) + amount = self._safe_exit_amount(trade.pair, sub_trade_amt or trade.amount) time_in_force = self.strategy.order_time_in_force['exit'] - if (exit_check.exit_type != ExitType.LIQUIDATION and not strategy_safe_wrapper( - self.strategy.confirm_trade_exit, default_retval=True)( - pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit, - time_in_force=time_in_force, exit_reason=exit_reason, - sell_reason=exit_reason, # sellreason -> compatibility - current_time=datetime.now(timezone.utc))): + if (exit_check.exit_type != ExitType.LIQUIDATION + and not sub_trade_amt + and not strategy_safe_wrapper( + self.strategy.confirm_trade_exit, default_retval=True)( + pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit, + time_in_force=time_in_force, exit_reason=exit_reason, + sell_reason=exit_reason, # sellreason -> compatibility + current_time=datetime.now(timezone.utc))): logger.info(f"User denied exit for {trade.pair}.") return False @@ -1504,7 +1540,7 @@ class FreqtradeBot(LoggingMixin): self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), reason='Auto lock') - self._notify_exit(trade, order_type) + self._notify_exit(trade, order_type, sub_trade=bool(sub_trade_amt), order=order_obj) # In case of market sell orders the order can be closed immediately if order.get('status', 'unknown') in ('closed', 'expired'): self.update_trade_state(trade, trade.open_order_id, order) @@ -1512,16 +1548,27 @@ class FreqtradeBot(LoggingMixin): return True - def _notify_exit(self, trade: Trade, order_type: str, fill: bool = False) -> None: + def _notify_exit(self, trade: Trade, order_type: str, fill: bool = False, + sub_trade: bool = False, order: Order = None) -> None: """ Sends rpc notification when a sell occurred. """ - profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested - profit_trade = trade.calc_profit(rate=profit_rate) # Use cached rates here - it was updated seconds ago. current_rate = self.exchange.get_rate( trade.pair, side='exit', is_short=trade.is_short, refresh=False) if not fill else None - profit_ratio = trade.calc_profit_ratio(profit_rate) + + # second condition is for mypy only; order will always be passed during sub trade + if sub_trade and order is not None: + amount = order.safe_filled if fill else order.amount + profit_rate = order.safe_price + + profit = trade.calc_profit(rate=profit_rate, amount=amount, open_rate=trade.open_rate) + profit_ratio = trade.calc_profit_ratio(profit_rate, amount, trade.open_rate) + else: + profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested + profit = trade.calc_profit(rate=profit_rate) + trade.realized_profit + profit_ratio = trade.calc_profit_ratio(profit_rate) + amount = trade.amount gain = "profit" if profit_ratio > 0 else "loss" msg = { @@ -1535,11 +1582,11 @@ class FreqtradeBot(LoggingMixin): 'gain': gain, 'limit': profit_rate, 'order_type': order_type, - 'amount': trade.amount, + 'amount': amount, 'open_rate': trade.open_rate, - 'close_rate': trade.close_rate, + 'close_rate': profit_rate, 'current_rate': current_rate, - 'profit_amount': profit_trade, + 'profit_amount': profit, 'profit_ratio': profit_ratio, 'buy_tag': trade.enter_tag, 'enter_tag': trade.enter_tag, @@ -1547,19 +1594,18 @@ class FreqtradeBot(LoggingMixin): 'exit_reason': trade.exit_reason, 'open_date': trade.open_date, 'close_date': trade.close_date or datetime.utcnow(), + 'stake_amount': trade.stake_amount, 'stake_currency': self.config['stake_currency'], 'fiat_currency': self.config.get('fiat_display_currency'), + 'sub_trade': sub_trade, + 'cumulative_profit': trade.realized_profit, } - if 'fiat_display_currency' in self.config: - msg.update({ - 'fiat_currency': self.config['fiat_display_currency'], - }) - # Send the message self.rpc.send_msg(msg) - def _notify_exit_cancel(self, trade: Trade, order_type: str, reason: str) -> None: + def _notify_exit_cancel(self, trade: Trade, order_type: str, reason: str, + order: Order, sub_trade: bool = False) -> None: """ Sends rpc notification when a sell cancel occurred. """ @@ -1585,7 +1631,7 @@ class FreqtradeBot(LoggingMixin): 'gain': gain, 'limit': profit_rate or 0, 'order_type': order_type, - 'amount': trade.amount, + 'amount': order.safe_amount_after_fee, 'open_rate': trade.open_rate, 'current_rate': current_rate, 'profit_amount': profit_trade, @@ -1599,6 +1645,8 @@ class FreqtradeBot(LoggingMixin): 'stake_currency': self.config['stake_currency'], 'fiat_currency': self.config.get('fiat_display_currency', None), 'reason': reason, + 'sub_trade': sub_trade, + 'stake_amount': trade.stake_amount, } if 'fiat_display_currency' in self.config: @@ -1653,14 +1701,18 @@ class FreqtradeBot(LoggingMixin): self.handle_order_fee(trade, order_obj, order) trade.update_trade(order_obj) - # TODO: is the below necessary? it's already done in update_trade for filled buys - trade.recalc_trade_from_orders() Trade.commit() - if order['status'] in constants.NON_OPEN_EXCHANGE_STATES: + if order.get('status') in constants.NON_OPEN_EXCHANGE_STATES: # If a entry order was closed, force update on stoploss on exchange if order.get('side') == trade.entry_side: trade = self.cancel_stoploss_on_exchange(trade) + if not self.edge: + # TODO: should shorting/leverage be supported by Edge, + # then this will need to be fixed. + trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True) + if order.get('side') == trade.entry_side or trade.amount > 0: + # Must also run for partial exits # TODO: Margin will need to use interest_rate as well. # interest_rate = self.exchange.get_interest_rate() trade.set_liquidation_price(self.exchange.get_liquidation_price( @@ -1670,24 +1722,30 @@ class FreqtradeBot(LoggingMixin): open_rate=trade.open_rate, is_short=trade.is_short )) - if not self.edge: - # TODO: should shorting/leverage be supported by Edge, - # then this will need to be fixed. - trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True) # Updating wallets when order is closed self.wallets.update() - if not trade.is_open: - if send_msg and not stoploss_order and not trade.open_order_id: - self._notify_exit(trade, '', True) - self.handle_protections(trade.pair, trade.trade_direction) - elif send_msg and not trade.open_order_id and not stoploss_order: - # Enter fill - self._notify_enter(trade, order, fill=True) + self.order_close_notify(trade, order_obj, stoploss_order, send_msg) return False + def order_close_notify( + self, trade: Trade, order: Order, stoploss_order: bool, send_msg: bool): + """send "fill" notifications""" + + sub_trade = not isclose(order.safe_amount_after_fee, + trade.amount, abs_tol=constants.MATH_CLOSE_PREC) + if order.ft_order_side == trade.exit_side: + # Exit notification + if send_msg and not stoploss_order and not trade.open_order_id: + self._notify_exit(trade, '', fill=True, sub_trade=sub_trade, order=order) + if not trade.is_open: + self.handle_protections(trade.pair, trade.trade_direction) + elif send_msg and not trade.open_order_id and not stoploss_order: + # Enter fill + self._notify_enter(trade, order, fill=True, sub_trade=sub_trade) + def handle_protections(self, pair: str, side: LongShort) -> None: prot_trig = self.protections.stop_per_pair(pair, side=side) if prot_trig: diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py old mode 100755 new mode 100644 index 2c6cfb0e9..46774e8a5 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -287,8 +287,8 @@ class Backtesting: if unavailable_pairs: raise OperationalException( - f"Pairs {', '.join(unavailable_pairs)} got no leverage tiers available. " - "It is therefore impossible to backtest with this pair at the moment.") + f"Pairs {', '.join(unavailable_pairs)} got no leverage tiers available. " + "It is therefore impossible to backtest with this pair at the moment.") else: self.futures_data = {} @@ -503,16 +503,20 @@ class Backtesting: def _get_adjust_trade_entry_for_candle(self, trade: LocalTrade, row: Tuple ) -> LocalTrade: - current_profit = trade.calc_profit_ratio(row[OPEN_IDX]) - min_stake = self.exchange.get_min_pair_stake_amount(trade.pair, row[OPEN_IDX], -0.1) - max_stake = self.exchange.get_max_pair_stake_amount(trade.pair, row[OPEN_IDX]) + current_rate = row[OPEN_IDX] + current_date = row[DATE_IDX].to_pydatetime() + current_profit = trade.calc_profit_ratio(current_rate) + min_stake = self.exchange.get_min_pair_stake_amount(trade.pair, current_rate, -0.1) + max_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_rate) stake_available = self.wallets.get_available_stake_amount() stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position, default_retval=None)( trade=trade, # type: ignore[arg-type] - current_time=row[DATE_IDX].to_pydatetime(), current_rate=row[OPEN_IDX], + current_time=current_date, current_rate=current_rate, current_profit=current_profit, min_stake=min_stake, - max_stake=min(max_stake, stake_available)) + max_stake=min(max_stake, stake_available), + current_entry_rate=current_rate, current_exit_rate=current_rate, + current_entry_profit=current_profit, current_exit_profit=current_profit) # Check if we should increase our position if stake_amount is not None and stake_amount > 0.0: @@ -523,6 +527,24 @@ class Backtesting: self.wallets.update() return pos_trade + if stake_amount is not None and stake_amount < 0.0: + amount = abs(stake_amount) / current_rate + if amount > trade.amount: + # This is currently ineffective as remaining would become < min tradable + amount = trade.amount + remaining = (trade.amount - amount) * current_rate + if remaining < min_stake: + # Remaining stake is too low to be sold. + return trade + pos_trade = self._exit_trade(trade, row, current_rate, amount) + if pos_trade is not None: + order = pos_trade.orders[-1] + if self._get_order_filled(order.price, row): + order.close_bt_order(current_date, trade) + trade.recalc_trade_from_orders() + self.wallets.update() + return pos_trade + return trade def _get_order_filled(self, rate: float, row: Tuple) -> bool: @@ -602,7 +624,7 @@ class Backtesting: self.strategy.confirm_trade_exit, default_retval=True)( pair=trade.pair, trade=trade, # type: ignore[arg-type] - order_type='limit', + order_type=order_type, amount=trade.amount, rate=close_rate, time_in_force=time_in_force, @@ -613,32 +635,38 @@ class Backtesting: trade.exit_reason = exit_reason - self.order_id_counter += 1 - order = Order( - id=self.order_id_counter, - ft_trade_id=trade.id, - order_date=exit_candle_time, - order_update_date=exit_candle_time, - ft_is_open=True, - ft_pair=trade.pair, - order_id=str(self.order_id_counter), - symbol=trade.pair, - ft_order_side=trade.exit_side, - side=trade.exit_side, - order_type=order_type, - status="open", - price=close_rate, - average=close_rate, - amount=trade.amount, - filled=0, - remaining=trade.amount, - cost=trade.amount * close_rate, - ) - trade.orders.append(order) - return trade - + return self._exit_trade(trade, row, close_rate, trade.amount) return None + def _exit_trade(self, trade: LocalTrade, sell_row: Tuple, + close_rate: float, amount: float = None) -> Optional[LocalTrade]: + self.order_id_counter += 1 + exit_candle_time = sell_row[DATE_IDX].to_pydatetime() + order_type = self.strategy.order_types['exit'] + amount = amount or trade.amount + order = Order( + id=self.order_id_counter, + ft_trade_id=trade.id, + order_date=exit_candle_time, + order_update_date=exit_candle_time, + ft_is_open=True, + ft_pair=trade.pair, + order_id=str(self.order_id_counter), + symbol=trade.pair, + ft_order_side=trade.exit_side, + side=trade.exit_side, + order_type=order_type, + status="open", + price=close_rate, + average=close_rate, + amount=amount, + filled=0, + remaining=amount, + cost=amount * close_rate, + ) + trade.orders.append(order) + return trade + def _get_exit_trade_entry(self, trade: LocalTrade, row: Tuple) -> Optional[LocalTrade]: exit_candle_time: datetime = row[DATE_IDX].to_pydatetime() @@ -865,6 +893,8 @@ class Backtesting: # Ignore trade if entry-order did not fill yet continue exit_row = data[pair][-1] + self._exit_trade(trade, exit_row, exit_row[OPEN_IDX], trade.amount) + trade.orders[-1].close_bt_order(exit_row[DATE_IDX].to_pydatetime(), trade) trade.close_date = exit_row[DATE_IDX].to_pydatetime() trade.exit_reason = ExitType.FORCE_EXIT.value @@ -1006,7 +1036,7 @@ class Backtesting: return None return row - def backtest(self, processed: Dict, + def backtest(self, processed: Dict, # noqa: max-complexity: 13 start_date: datetime, end_date: datetime, max_open_trades: int = 0, position_stacking: bool = False, enable_protections: bool = False) -> Dict[str, Any]: @@ -1108,14 +1138,19 @@ class Backtesting: if order and self._get_order_filled(order.price, row): order.close_bt_order(current_time, trade) trade.open_order_id = None - trade.close_date = current_time - trade.close(order.price, show_msg=False) + sub_trade = order.safe_amount_after_fee != trade.amount + if sub_trade: + order.close_bt_order(current_time, trade) + trade.recalc_trade_from_orders() + else: + trade.close_date = current_time + trade.close(order.price, show_msg=False) - # logger.debug(f"{pair} - Backtesting exit {trade}") - open_trade_count -= 1 - open_trades[pair].remove(trade) - LocalTrade.close_bt_trade(trade) - trades.append(trade) + # logger.debug(f"{pair} - Backtesting exit {trade}") + open_trade_count -= 1 + open_trades[pair].remove(trade) + LocalTrade.close_bt_trade(trade) + trades.append(trade) self.wallets.update() self.run_protections( enable_protections, pair, current_time, trade.trade_direction) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 2a8e34cdf..81757a7de 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -95,6 +95,7 @@ def migrate_trades_and_orders_table( exit_reason = get_column_def(cols, 'sell_reason', get_column_def(cols, 'exit_reason', 'null')) strategy = get_column_def(cols, 'strategy', 'null') enter_tag = get_column_def(cols, 'buy_tag', get_column_def(cols, 'enter_tag', 'null')) + realized_profit = get_column_def(cols, 'realized_profit', '0.0') trading_mode = get_column_def(cols, 'trading_mode', 'null') @@ -155,7 +156,7 @@ def migrate_trades_and_orders_table( max_rate, min_rate, exit_reason, exit_order_status, strategy, enter_tag, timeframe, open_trade_value, close_profit_abs, trading_mode, leverage, liquidation_price, is_short, - interest_rate, funding_fees + interest_rate, funding_fees, realized_profit ) select id, lower(exchange), pair, {base_currency} base_currency, {stake_currency} stake_currency, @@ -181,7 +182,7 @@ def migrate_trades_and_orders_table( {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs, {trading_mode} trading_mode, {leverage} leverage, {liquidation_price} liquidation_price, {is_short} is_short, {interest_rate} interest_rate, - {funding_fees} funding_fees + {funding_fees} funding_fees, {realized_profit} realized_profit from {trade_back_name} """)) @@ -297,8 +298,9 @@ def check_migrate(engine, decl_base, previous_tables) -> None: # Check if migration necessary # Migrates both trades and orders table! - if not has_column(cols_orders, 'stop_price'): - # if not has_column(cols_trades, 'base_currency'): + # if ('orders' not in previous_tables + # or not has_column(cols_orders, 'stop_price')): + if not has_column(cols_trades, 'realized_profit'): logger.info(f"Running database migration for trades - " f"backup: {table_back_name}, {order_table_bak_name}") migrate_trades_and_orders_table( diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 44e148a0c..fcb84a59a 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -4,13 +4,15 @@ This module contains the class to persist trades into SQLite import logging from datetime import datetime, timedelta, timezone from decimal import Decimal +from math import isclose from typing import Any, Dict, List, Optional from sqlalchemy import (Boolean, Column, DateTime, Enum, Float, ForeignKey, Integer, String, UniqueConstraint, desc, func) from sqlalchemy.orm import Query, lazyload, relationship -from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES, BuySell, LongShort +from freqtrade.constants import (DATETIME_PRINT_FORMAT, MATH_CLOSE_PREC, NON_OPEN_EXCHANGE_STATES, + BuySell, LongShort) from freqtrade.enums import ExitType, TradingMode from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.leverage import interest @@ -176,10 +178,9 @@ class Order(_DECL_BASE): self.remaining = 0 self.status = 'closed' self.ft_is_open = False - if (self.ft_order_side == trade.entry_side - and len(trade.select_filled_orders(trade.entry_side)) == 1): + if (self.ft_order_side == trade.entry_side): trade.open_rate = self.price - trade.recalc_open_trade_value() + trade.recalc_trade_from_orders() trade.adjust_stop_loss(trade.open_rate, trade.stop_loss_pct, refresh=True) @staticmethod @@ -237,6 +238,7 @@ class LocalTrade(): trades: List['LocalTrade'] = [] trades_open: List['LocalTrade'] = [] total_profit: float = 0 + realized_profit: float = 0 id: int = 0 @@ -447,6 +449,7 @@ class LocalTrade(): if self.close_date else None), 'close_timestamp': int(self.close_date.replace( tzinfo=timezone.utc).timestamp() * 1000) if self.close_date else None, + 'realized_profit': self.realized_profit or 0.0, 'close_rate': self.close_rate, 'close_rate_requested': self.close_rate_requested, 'close_profit': self.close_profit, # Deprecated @@ -596,14 +599,28 @@ class LocalTrade(): if self.is_open: payment = "SELL" if self.is_short else "BUY" logger.info(f'{order.order_type.upper()}_{payment} has been fulfilled for {self}.') - self.open_order_id = None + # condition to avoid reset value when updating fees + if self.open_order_id == order.order_id: + self.open_order_id = None + else: + logger.warning( + f'Got different open_order_id {self.open_order_id} != {order.order_id}') self.recalc_trade_from_orders() elif order.ft_order_side == self.exit_side: if self.is_open: payment = "BUY" if self.is_short else "SELL" # * On margin shorts, you buy a little bit more than the amount (amount + interest) logger.info(f'{order.order_type.upper()}_{payment} has been fulfilled for {self}.') - self.close(order.safe_price) + # condition to avoid reset value when updating fees + if self.open_order_id == order.order_id: + self.open_order_id = None + else: + logger.warning( + f'Got different open_order_id {self.open_order_id} != {order.order_id}') + if isclose(order.safe_amount_after_fee, self.amount, abs_tol=MATH_CLOSE_PREC): + self.close(order.safe_price) + else: + self.recalc_trade_from_orders() elif order.ft_order_side == 'stoploss': self.stoploss_order_id = None self.close_rate_requested = self.stop_loss @@ -622,11 +639,11 @@ class LocalTrade(): """ self.close_rate = rate self.close_date = self.close_date or datetime.utcnow() - self.close_profit = self.calc_profit_ratio(rate) - self.close_profit_abs = self.calc_profit(rate) + self.close_profit_abs = self.calc_profit(rate) + self.realized_profit self.is_open = False self.exit_order_status = 'closed' self.open_order_id = None + self.recalc_trade_from_orders(is_closing=True) if show_msg: logger.info( 'Marking %s as closed as the trade is fulfilled and found no open orders for it.', @@ -672,12 +689,12 @@ class LocalTrade(): """ return len([o for o in self.orders if o.ft_order_side == self.exit_side]) - def _calc_open_trade_value(self) -> float: + def _calc_open_trade_value(self, amount: float, open_rate: float) -> float: """ Calculate the open_rate including open_fee. :return: Price in of the open trade incl. Fees """ - open_trade = Decimal(self.amount) * Decimal(self.open_rate) + open_trade = Decimal(amount) * Decimal(open_rate) fees = open_trade * Decimal(self.fee_open) if self.is_short: return float(open_trade - fees) @@ -689,7 +706,7 @@ class LocalTrade(): Recalculate open_trade_value. Must be called whenever open_rate, fee_open is changed. """ - self.open_trade_value = self._calc_open_trade_value() + self.open_trade_value = self._calc_open_trade_value(self.amount, self.open_rate) def calculate_interest(self) -> Decimal: """ @@ -721,7 +738,7 @@ class LocalTrade(): else: return close_trade - fees - def calc_close_trade_value(self, rate: float) -> float: + def calc_close_trade_value(self, rate: float, amount: float = None) -> float: """ Calculate the Trade's close value including fees :param rate: rate to compare with. @@ -730,96 +747,143 @@ class LocalTrade(): if rate is None and not self.close_rate: return 0.0 - amount = Decimal(self.amount) + amount1 = Decimal(amount or self.amount) trading_mode = self.trading_mode or TradingMode.SPOT if trading_mode == TradingMode.SPOT: - return float(self._calc_base_close(amount, rate, self.fee_close)) + return float(self._calc_base_close(amount1, rate, self.fee_close)) elif (trading_mode == TradingMode.MARGIN): total_interest = self.calculate_interest() if self.is_short: - amount = amount + total_interest - return float(self._calc_base_close(amount, rate, self.fee_close)) + amount1 = amount1 + total_interest + return float(self._calc_base_close(amount1, rate, self.fee_close)) else: # Currency already owned for longs, no need to purchase - return float(self._calc_base_close(amount, rate, self.fee_close) - total_interest) + return float(self._calc_base_close(amount1, rate, self.fee_close) - total_interest) elif (trading_mode == TradingMode.FUTURES): funding_fees = self.funding_fees or 0.0 # Positive funding_fees -> Trade has gained from fees. # Negative funding_fees -> Trade had to pay the fees. if self.is_short: - return float(self._calc_base_close(amount, rate, self.fee_close)) - funding_fees + return float(self._calc_base_close(amount1, rate, self.fee_close)) - funding_fees else: - return float(self._calc_base_close(amount, rate, self.fee_close)) + funding_fees + return float(self._calc_base_close(amount1, rate, self.fee_close)) + funding_fees else: raise OperationalException( f"{self.trading_mode.value} trading is not yet available using freqtrade") - def calc_profit(self, rate: float) -> float: + def calc_profit(self, rate: float, amount: float = None, open_rate: float = None) -> float: """ Calculate the absolute profit in stake currency between Close and Open trade :param rate: close rate to compare with. + :param amount: Amount to use for the calculation. Falls back to trade.amount if not set. + :param open_rate: open_rate to use. Defaults to self.open_rate if not provided. :return: profit in stake currency as float """ - close_trade_value = self.calc_close_trade_value(rate) + close_trade_value = self.calc_close_trade_value(rate, amount) + if amount is None or open_rate is None: + open_trade_value = self.open_trade_value + else: + open_trade_value = self._calc_open_trade_value(amount, open_rate) if self.is_short: - profit = self.open_trade_value - close_trade_value + profit = open_trade_value - close_trade_value else: - profit = close_trade_value - self.open_trade_value + profit = close_trade_value - open_trade_value return float(f"{profit:.8f}") - def calc_profit_ratio(self, rate: float) -> float: + def calc_profit_ratio( + self, rate: float, amount: float = None, open_rate: float = None) -> float: """ Calculates the profit as ratio (including fee). :param rate: rate to compare with. + :param amount: Amount to use for the calculation. Falls back to trade.amount if not set. + :param open_rate: open_rate to use. Defaults to self.open_rate if not provided. :return: profit ratio as float """ - close_trade_value = self.calc_close_trade_value(rate) + close_trade_value = self.calc_close_trade_value(rate, amount) + + if amount is None or open_rate is None: + open_trade_value = self.open_trade_value + else: + open_trade_value = self._calc_open_trade_value(amount, open_rate) short_close_zero = (self.is_short and close_trade_value == 0.0) - long_close_zero = (not self.is_short and self.open_trade_value == 0.0) + long_close_zero = (not self.is_short and open_trade_value == 0.0) leverage = self.leverage or 1.0 if (short_close_zero or long_close_zero): return 0.0 else: if self.is_short: - profit_ratio = (1 - (close_trade_value / self.open_trade_value)) * leverage + profit_ratio = (1 - (close_trade_value / open_trade_value)) * leverage else: - profit_ratio = ((close_trade_value / self.open_trade_value) - 1) * leverage + profit_ratio = ((close_trade_value / open_trade_value) - 1) * leverage return float(f"{profit_ratio:.8f}") - def recalc_trade_from_orders(self): + def recalc_trade_from_orders(self, is_closing: bool = False): + + current_amount = 0.0 + current_stake = 0.0 + total_stake = 0.0 # Total stake after all buy orders (does not subtract!) + avg_price = 0.0 + close_profit = 0.0 + close_profit_abs = 0.0 - total_amount = 0.0 - total_stake = 0.0 for o in self.orders: - if (o.ft_is_open or - (o.ft_order_side != self.entry_side) or - (o.status not in NON_OPEN_EXCHANGE_STATES)): + if o.ft_is_open or not o.filled: continue tmp_amount = o.safe_amount_after_fee - tmp_price = o.average or o.price - if tmp_amount > 0.0 and tmp_price is not None: - total_amount += tmp_amount - total_stake += tmp_price * tmp_amount + tmp_price = o.safe_price - if total_amount > 0: + is_exit = o.ft_order_side != self.entry_side + side = -1 if is_exit else 1 + if tmp_amount > 0.0 and tmp_price is not None: + current_amount += tmp_amount * side + price = avg_price if is_exit else tmp_price + current_stake += price * tmp_amount * side + + if current_amount > 0: + avg_price = current_stake / current_amount + + if is_exit: + # Process partial exits + exit_rate = o.safe_price + exit_amount = o.safe_amount_after_fee + profit = self.calc_profit(rate=exit_rate, amount=exit_amount, open_rate=avg_price) + close_profit_abs += profit + close_profit = self.calc_profit_ratio( + exit_rate, amount=exit_amount, open_rate=avg_price) + if current_amount <= 0: + profit = close_profit_abs + else: + total_stake = total_stake + self._calc_open_trade_value(tmp_amount, price) + + if close_profit: + self.close_profit = close_profit + self.realized_profit = close_profit_abs + self.close_profit_abs = profit + + if current_amount > 0: + # Trade is still open # Leverage not updated, as we don't allow changing leverage through DCA at the moment. - self.open_rate = total_stake / total_amount - self.stake_amount = total_stake / (self.leverage or 1.0) - self.amount = total_amount - self.fee_open_cost = self.fee_open * total_stake + self.open_rate = current_stake / current_amount + self.stake_amount = current_stake / (self.leverage or 1.0) + self.amount = current_amount + self.fee_open_cost = self.fee_open * current_stake self.recalc_open_trade_value() if self.stop_loss_pct is not None and self.open_rate is not None: self.adjust_stop_loss(self.open_rate, self.stop_loss_pct) + elif is_closing and total_stake > 0: + # Close profit abs / maximum owned + # Fees are considered as they are part of close_profit_abs + self.close_profit = (close_profit_abs / total_stake) * self.leverage def select_order_by_order_id(self, order_id: str) -> Optional[Order]: """ @@ -841,7 +905,7 @@ class LocalTrade(): """ orders = self.orders if order_side: - orders = [o for o in self.orders if o.ft_order_side == order_side] + orders = [o for o in orders if o.ft_order_side == order_side] if is_open is not None: orders = [o for o in orders if o.ft_is_open == is_open] if len(orders) > 0: @@ -856,9 +920,9 @@ class LocalTrade(): :return: array of Order objects """ return [o for o in self.orders if ((o.ft_order_side == order_side) or (order_side is None)) - and o.ft_is_open is False and - (o.filled or 0) > 0 and - o.status in NON_OPEN_EXCHANGE_STATES] + and o.ft_is_open is False + and o.filled + and o.status in NON_OPEN_EXCHANGE_STATES] def select_filled_or_open_orders(self) -> List['Order']: """ @@ -1023,6 +1087,7 @@ class Trade(_DECL_BASE, LocalTrade): open_trade_value = Column(Float) close_rate: Optional[float] = Column(Float) close_rate_requested = Column(Float) + realized_profit = Column(Float, default=0.0) close_profit = Column(Float) close_profit_abs = Column(Float) stake_amount = Column(Float, nullable=False) @@ -1068,6 +1133,7 @@ class Trade(_DECL_BASE, LocalTrade): def __init__(self, **kwargs): super().__init__(**kwargs) + self.realized_profit = 0 self.recalc_open_trade_value() def delete(self) -> None: diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index e6948c9e2..9d6696803 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -201,7 +201,7 @@ class RPC: trade_dict = trade.to_json() trade_dict.update(dict( - close_profit=trade.close_profit if trade.close_profit is not None else None, + close_profit=trade.close_profit if not trade.is_open else None, current_rate=current_rate, current_profit=current_profit, # Deprecated current_profit_pct=round(current_profit * 100, 2), # Deprecated diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 121324d90..66192fb16 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -274,7 +274,7 @@ class Telegram(RPCHandler): f"{emoji} *{self._exchange_from_msg(msg)}:*" f" {entry_side['entered'] if is_fill else entry_side['enter']} {msg['pair']}" f" (#{msg['trade_id']})\n" - ) + ) message += self._add_analyzed_candle(msg['pair']) message += f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag') else "" message += f"*Amount:* `{msg['amount']:.8f}`\n" @@ -315,20 +315,36 @@ class Telegram(RPCHandler): msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount( msg['profit_amount'], msg['stake_currency'], msg['fiat_currency']) msg['profit_extra'] = ( - f" ({msg['gain']}: {msg['profit_amount']:.8f} {msg['stake_currency']}" - f" / {msg['profit_fiat']:.3f} {msg['fiat_currency']})") + f" / {msg['profit_fiat']:.3f} {msg['fiat_currency']}") else: msg['profit_extra'] = '' + msg['profit_extra'] = ( + f" ({msg['gain']}: {msg['profit_amount']:.8f} {msg['stake_currency']}" + f"{msg['profit_extra']})") is_fill = msg['type'] == RPCMessageType.EXIT_FILL + is_sub_trade = msg.get('sub_trade') + is_sub_profit = msg['profit_amount'] != msg.get('cumulative_profit') + profit_prefix = ('Sub ' if is_sub_profit + else 'Cumulative ') if is_sub_trade else '' + cp_extra = '' + if is_sub_profit and is_sub_trade: + if self._rpc._fiat_converter: + cp_fiat = self._rpc._fiat_converter.convert_amount( + msg['cumulative_profit'], msg['stake_currency'], msg['fiat_currency']) + cp_extra = f" / {cp_fiat:.3f} {msg['fiat_currency']}" + else: + cp_extra = '' + cp_extra = f"*Cumulative Profit:* (`{msg['cumulative_profit']:.8f} " \ + f"{msg['stake_currency']}{cp_extra}`)\n" message = ( f"{msg['emoji']} *{self._exchange_from_msg(msg)}:* " f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n" f"{self._add_analyzed_candle(msg['pair'])}" - f"*{'Profit' if is_fill else 'Unrealized Profit'}:* " + f"*{f'{profit_prefix}Profit' if is_fill else f'Unrealized {profit_prefix}Profit'}:* " f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n" + f"{cp_extra}" f"*Enter Tag:* `{msg['enter_tag']}`\n" f"*Exit Reason:* `{msg['exit_reason']}`\n" - f"*Duration:* `{msg['duration']} ({msg['duration_min']:.1f} min)`\n" f"*Direction:* `{msg['direction']}`\n" f"{msg['leverage_text']}" f"*Amount:* `{msg['amount']:.8f}`\n" @@ -336,11 +352,25 @@ class Telegram(RPCHandler): ) if msg['type'] == RPCMessageType.EXIT: message += (f"*Current Rate:* `{msg['current_rate']:.8f}`\n" - f"*Close Rate:* `{msg['limit']:.8f}`") + f"*Exit Rate:* `{msg['limit']:.8f}`") elif msg['type'] == RPCMessageType.EXIT_FILL: - message += f"*Close Rate:* `{msg['close_rate']:.8f}`" + message += f"*Exit Rate:* `{msg['close_rate']:.8f}`" + if msg.get('sub_trade'): + if self._rpc._fiat_converter: + msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount( + msg['stake_amount'], msg['stake_currency'], msg['fiat_currency']) + else: + msg['stake_amount_fiat'] = 0 + rem = round_coin_value(msg['stake_amount'], msg['stake_currency']) + message += f"\n*Remaining:* `({rem}" + if msg.get('fiat_currency', None): + message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}" + + message += ")`" + else: + message += f"\n*Duration:* `{msg['duration']} ({msg['duration_min']:.1f} min)`" return message def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> str: @@ -353,7 +383,8 @@ class Telegram(RPCHandler): elif msg_type in (RPCMessageType.ENTRY_CANCEL, RPCMessageType.EXIT_CANCEL): msg['message_side'] = 'enter' if msg_type in [RPCMessageType.ENTRY_CANCEL] else 'exit' message = (f"\N{WARNING SIGN} *{self._exchange_from_msg(msg)}:* " - f"Cancelling {msg['message_side']} Order for {msg['pair']} " + f"Cancelling {'partial ' if msg.get('sub_trade') else ''}" + f"{msg['message_side']} Order for {msg['pair']} " f"(#{msg['trade_id']}). Reason: {msg['reason']}.") elif msg_type == RPCMessageType.PROTECTION_TRIGGER: @@ -424,7 +455,7 @@ class Telegram(RPCHandler): else: return "\N{CROSS MARK}" - def _prepare_entry_details(self, filled_orders: List, quote_currency: str, is_open: bool): + def _prepare_order_details(self, filled_orders: List, quote_currency: str, is_open: bool): """ Prepare details of trade with entry adjustment enabled """ @@ -433,44 +464,51 @@ class Telegram(RPCHandler): first_avg = filled_orders[0]["safe_price"] for x, order in enumerate(filled_orders): - if not order['ft_is_entry'] or order['is_open'] is True: + if order['is_open'] is True: continue + wording = 'Entry' if order['ft_is_entry'] else 'Exit' + cur_entry_datetime = arrow.get(order["order_filled_date"]) - cur_entry_amount = order["amount"] + cur_entry_amount = order["filled"] or order["amount"] cur_entry_average = order["safe_price"] lines.append(" ") if x == 0: - lines.append(f"*Entry #{x+1}:*") + lines.append(f"*{wording} #{x+1}:*") lines.append( - f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})") - lines.append(f"*Average Entry Price:* {cur_entry_average}") + f"*Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})") + lines.append(f"*Average Price:* {cur_entry_average}") else: sumA = 0 sumB = 0 for y in range(x): - sumA += (filled_orders[y]["amount"] * filled_orders[y]["safe_price"]) - sumB += filled_orders[y]["amount"] + amount = filled_orders[y]["filled"] or filled_orders[y]["amount"] + sumA += amount * filled_orders[y]["safe_price"] + sumB += amount prev_avg_price = sumA / sumB + # TODO: This calculation ignores fees. price_to_1st_entry = ((cur_entry_average - first_avg) / first_avg) minus_on_entry = 0 if prev_avg_price: minus_on_entry = (cur_entry_average - prev_avg_price) / prev_avg_price - dur_entry = cur_entry_datetime - arrow.get( - filled_orders[x - 1]["order_filled_date"]) - days = dur_entry.days - hours, remainder = divmod(dur_entry.seconds, 3600) - minutes, seconds = divmod(remainder, 60) - lines.append(f"*Entry #{x+1}:* at {minus_on_entry:.2%} avg profit") + lines.append(f"*{wording} #{x+1}:* at {minus_on_entry:.2%} avg profit") if is_open: lines.append("({})".format(cur_entry_datetime .humanize(granularity=["day", "hour", "minute"]))) lines.append( - f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})") - lines.append(f"*Average Entry Price:* {cur_entry_average} " + f"*Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})") + lines.append(f"*Average {wording} Price:* {cur_entry_average} " f"({price_to_1st_entry:.2%} from 1st entry rate)") - lines.append(f"*Order filled at:* {order['order_filled_date']}") - lines.append(f"({days}d {hours}h {minutes}m {seconds}s from previous entry)") + lines.append(f"*Order filled:* {order['order_filled_date']}") + + # TODO: is this really useful? + # dur_entry = cur_entry_datetime - arrow.get( + # filled_orders[x - 1]["order_filled_date"]) + # days = dur_entry.days + # hours, remainder = divmod(dur_entry.seconds, 3600) + # minutes, seconds = divmod(remainder, 60) + # lines.append( + # f"({days}d {hours}h {minutes}m {seconds}s from previous {wording.lower()})") return lines @authorized_only @@ -486,7 +524,14 @@ class Telegram(RPCHandler): if context.args and 'table' in context.args: self._status_table(update, context) return + else: + self._status_msg(update, context) + def _status_msg(self, update: Update, context: CallbackContext) -> None: + """ + handler for `/status` and `/status `. + + """ try: # Check if there's at least one numerical ID provided. @@ -529,6 +574,8 @@ class Telegram(RPCHandler): ]) if r['is_open']: + if r.get('realized_profit'): + lines.append("*Realized Profit:* `{realized_profit:.8f}`") if (r['stop_loss_abs'] != r['initial_stop_loss_abs'] and r['initial_stop_loss_ratio'] is not None): # Adding initial stoploss only if it is different from stoploss @@ -546,7 +593,7 @@ class Telegram(RPCHandler): else: lines.append("*Open Order:* `{open_order}`") - lines_detail = self._prepare_entry_details( + lines_detail = self._prepare_order_details( r['orders'], r['quote_currency'], r['is_open']) lines.extend(lines_detail if lines_detail else "") diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 824f31258..5e0aba2fe 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -463,10 +463,13 @@ class IStrategy(ABC, HyperStrategyMixin): def adjust_trade_position(self, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, min_stake: Optional[float], max_stake: float, + current_entry_rate: float, current_exit_rate: float, + current_entry_profit: float, current_exit_profit: float, **kwargs) -> Optional[float]: """ - Custom trade adjustment logic, returning the stake amount that a trade should be increased. - This means extra buy orders with additional fees. + Custom trade adjustment logic, returning the stake amount that a trade should be + increased or decreased. + This means extra buy or sell orders with additional fees. Only called when `position_adjustment_enable` is set to True. For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ @@ -477,10 +480,16 @@ class IStrategy(ABC, HyperStrategyMixin): :param current_time: datetime object, containing the current datetime :param current_rate: Current buy rate. :param current_profit: Current profit (as ratio), calculated based on current_rate. - :param min_stake: Minimal stake size allowed by exchange. - :param max_stake: Balance available for trading. + :param min_stake: Minimal stake size allowed by exchange (for both entries and exits) + :param max_stake: Maximum stake allowed (either through balance, or by exchange limits). + :param current_entry_rate: Current rate using entry pricing. + :param current_exit_rate: Current rate using exit pricing. + :param current_entry_profit: Current profit using entry pricing. + :param current_exit_profit: Current profit using exit pricing. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. - :return float: Stake amount to adjust your trade + :return float: Stake amount to adjust your trade, + Positive values to increase position, Negative values to decrease position. + Return None for no action. """ return None diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index 989f1d37a..488ca2fd7 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -247,12 +247,16 @@ def check_exit_timeout(self, pair: str, trade: 'Trade', order: 'Order', """ return False -def adjust_trade_position(self, trade: 'Trade', current_time: 'datetime', - current_rate: float, current_profit: float, min_stake: Optional[float], - max_stake: float, **kwargs) -> 'Optional[float]': +def adjust_trade_position(self, trade: 'Trade', current_time: datetime, + current_rate: float, current_profit: float, + min_stake: Optional[float], max_stake: float, + current_entry_rate: float, current_exit_rate: float, + current_entry_profit: float, current_exit_profit: float, + **kwargs) -> Optional[float]: """ - Custom trade adjustment logic, returning the stake amount that a trade should be increased. - This means extra buy orders with additional fees. + Custom trade adjustment logic, returning the stake amount that a trade should be + increased or decreased. + This means extra buy or sell orders with additional fees. Only called when `position_adjustment_enable` is set to True. For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ @@ -263,10 +267,16 @@ def adjust_trade_position(self, trade: 'Trade', current_time: 'datetime', :param current_time: datetime object, containing the current datetime :param current_rate: Current buy rate. :param current_profit: Current profit (as ratio), calculated based on current_rate. - :param min_stake: Minimal stake size allowed by exchange. - :param max_stake: Balance available for trading. + :param min_stake: Minimal stake size allowed by exchange (for both entries and exits) + :param max_stake: Maximum stake allowed (either through balance, or by exchange limits). + :param current_entry_rate: Current rate using entry pricing. + :param current_exit_rate: Current rate using exit pricing. + :param current_entry_profit: Current profit using entry pricing. + :param current_exit_profit: Current profit using exit pricing. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. - :return float: Stake amount to adjust your trade + :return float: Stake amount to adjust your trade, + Positive values to increase position, Negative values to decrease position. + Return None for no action. """ return None diff --git a/tests/conftest.py b/tests/conftest.py index ff3e1007f..a02fc4566 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1627,8 +1627,8 @@ def limit_buy_order_open(): 'timestamp': arrow.utcnow().int_timestamp * 1000, 'datetime': arrow.utcnow().isoformat(), 'price': 0.00001099, + 'average': 0.00001099, 'amount': 90.99181073, - 'average': None, 'filled': 0.0, 'cost': 0.0009999, 'remaining': 90.99181073, @@ -2817,6 +2817,7 @@ def limit_buy_order_usdt_open(): 'datetime': arrow.utcnow().isoformat(), 'timestamp': arrow.utcnow().int_timestamp * 1000, 'price': 2.00, + 'average': 2.00, 'amount': 30.0, 'filled': 0.0, 'cost': 60.0, diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index e968b12c2..d73e26683 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -27,6 +27,57 @@ from tests.conftest import get_mock_coro, get_patched_exchange, log_has, log_has # Make sure to always keep one exchange here which is NOT subclassed!! EXCHANGES = ['bittrex', 'binance', 'kraken', 'ftx', 'gateio'] +get_entry_rate_data = [ + ('other', 20, 19, 10, 0.0, 20), # Full ask side + ('ask', 20, 19, 10, 0.0, 20), # Full ask side + ('ask', 20, 19, 10, 1.0, 10), # Full last side + ('ask', 20, 19, 10, 0.5, 15), # Between ask and last + ('ask', 20, 19, 10, 0.7, 13), # Between ask and last + ('ask', 20, 19, 10, 0.3, 17), # Between ask and last + ('ask', 5, 6, 10, 1.0, 5), # last bigger than ask + ('ask', 5, 6, 10, 0.5, 5), # last bigger than ask + ('ask', 20, 19, 10, None, 20), # price_last_balance missing + ('ask', 10, 20, None, 0.5, 10), # last not available - uses ask + ('ask', 4, 5, None, 0.5, 4), # last not available - uses ask + ('ask', 4, 5, None, 1, 4), # last not available - uses ask + ('ask', 4, 5, None, 0, 4), # last not available - uses ask + ('same', 21, 20, 10, 0.0, 20), # Full bid side + ('bid', 21, 20, 10, 0.0, 20), # Full bid side + ('bid', 21, 20, 10, 1.0, 10), # Full last side + ('bid', 21, 20, 10, 0.5, 15), # Between bid and last + ('bid', 21, 20, 10, 0.7, 13), # Between bid and last + ('bid', 21, 20, 10, 0.3, 17), # Between bid and last + ('bid', 6, 5, 10, 1.0, 5), # last bigger than bid + ('bid', 21, 20, 10, None, 20), # price_last_balance missing + ('bid', 6, 5, 10, 0.5, 5), # last bigger than bid + ('bid', 21, 20, None, 0.5, 20), # last not available - uses bid + ('bid', 6, 5, None, 0.5, 5), # last not available - uses bid + ('bid', 6, 5, None, 1, 5), # last not available - uses bid + ('bid', 6, 5, None, 0, 5), # last not available - uses bid +] + +get_sell_rate_data = [ + ('bid', 12.0, 11.0, 11.5, 0.0, 11.0), # full bid side + ('bid', 12.0, 11.0, 11.5, 1.0, 11.5), # full last side + ('bid', 12.0, 11.0, 11.5, 0.5, 11.25), # between bid and lat + ('bid', 12.0, 11.2, 10.5, 0.0, 11.2), # Last smaller than bid + ('bid', 12.0, 11.2, 10.5, 1.0, 11.2), # Last smaller than bid - uses bid + ('bid', 12.0, 11.2, 10.5, 0.5, 11.2), # Last smaller than bid - uses bid + ('bid', 0.003, 0.002, 0.005, 0.0, 0.002), + ('bid', 0.003, 0.002, 0.005, None, 0.002), + ('ask', 12.0, 11.0, 12.5, 0.0, 12.0), # full ask side + ('ask', 12.0, 11.0, 12.5, 1.0, 12.5), # full last side + ('ask', 12.0, 11.0, 12.5, 0.5, 12.25), # between bid and lat + ('ask', 12.2, 11.2, 10.5, 0.0, 12.2), # Last smaller than ask + ('ask', 12.0, 11.0, 10.5, 1.0, 12.0), # Last smaller than ask - uses ask + ('ask', 12.0, 11.2, 10.5, 0.5, 12.0), # Last smaller than ask - uses ask + ('ask', 10.0, 11.0, 11.0, 0.0, 10.0), + ('ask', 10.11, 11.2, 11.0, 0.0, 10.11), + ('ask', 0.001, 0.002, 11.0, 0.0, 0.001), + ('ask', 0.006, 1.0, 11.0, 0.0, 0.006), + ('ask', 0.006, 1.0, 11.0, None, 0.006), +] + def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, fun, mock_ccxt_fun, retries=API_RETRY_COUNT + 1, **kwargs): @@ -2360,34 +2411,7 @@ def test_fetch_l2_order_book_exception(default_conf, mocker, exchange_name): exchange.fetch_l2_order_book(pair='ETH/BTC', limit=50) -@pytest.mark.parametrize("side,ask,bid,last,last_ab,expected", [ - ('other', 20, 19, 10, 0.0, 20), # Full ask side - ('ask', 20, 19, 10, 0.0, 20), # Full ask side - ('ask', 20, 19, 10, 1.0, 10), # Full last side - ('ask', 20, 19, 10, 0.5, 15), # Between ask and last - ('ask', 20, 19, 10, 0.7, 13), # Between ask and last - ('ask', 20, 19, 10, 0.3, 17), # Between ask and last - ('ask', 5, 6, 10, 1.0, 5), # last bigger than ask - ('ask', 5, 6, 10, 0.5, 5), # last bigger than ask - ('ask', 20, 19, 10, None, 20), # price_last_balance missing - ('ask', 10, 20, None, 0.5, 10), # last not available - uses ask - ('ask', 4, 5, None, 0.5, 4), # last not available - uses ask - ('ask', 4, 5, None, 1, 4), # last not available - uses ask - ('ask', 4, 5, None, 0, 4), # last not available - uses ask - ('same', 21, 20, 10, 0.0, 20), # Full bid side - ('bid', 21, 20, 10, 0.0, 20), # Full bid side - ('bid', 21, 20, 10, 1.0, 10), # Full last side - ('bid', 21, 20, 10, 0.5, 15), # Between bid and last - ('bid', 21, 20, 10, 0.7, 13), # Between bid and last - ('bid', 21, 20, 10, 0.3, 17), # Between bid and last - ('bid', 6, 5, 10, 1.0, 5), # last bigger than bid - ('bid', 21, 20, 10, None, 20), # price_last_balance missing - ('bid', 6, 5, 10, 0.5, 5), # last bigger than bid - ('bid', 21, 20, None, 0.5, 20), # last not available - uses bid - ('bid', 6, 5, None, 0.5, 5), # last not available - uses bid - ('bid', 6, 5, None, 1, 5), # last not available - uses bid - ('bid', 6, 5, None, 0, 5), # last not available - uses bid -]) +@pytest.mark.parametrize("side,ask,bid,last,last_ab,expected", get_entry_rate_data) def test_get_entry_rate(mocker, default_conf, caplog, side, ask, bid, last, last_ab, expected) -> None: caplog.set_level(logging.DEBUG) @@ -2411,27 +2435,7 @@ def test_get_entry_rate(mocker, default_conf, caplog, side, ask, bid, assert not log_has("Using cached entry rate for ETH/BTC.", caplog) -@pytest.mark.parametrize('side,ask,bid,last,last_ab,expected', [ - ('bid', 12.0, 11.0, 11.5, 0.0, 11.0), # full bid side - ('bid', 12.0, 11.0, 11.5, 1.0, 11.5), # full last side - ('bid', 12.0, 11.0, 11.5, 0.5, 11.25), # between bid and lat - ('bid', 12.0, 11.2, 10.5, 0.0, 11.2), # Last smaller than bid - ('bid', 12.0, 11.2, 10.5, 1.0, 11.2), # Last smaller than bid - uses bid - ('bid', 12.0, 11.2, 10.5, 0.5, 11.2), # Last smaller than bid - uses bid - ('bid', 0.003, 0.002, 0.005, 0.0, 0.002), - ('bid', 0.003, 0.002, 0.005, None, 0.002), - ('ask', 12.0, 11.0, 12.5, 0.0, 12.0), # full ask side - ('ask', 12.0, 11.0, 12.5, 1.0, 12.5), # full last side - ('ask', 12.0, 11.0, 12.5, 0.5, 12.25), # between bid and lat - ('ask', 12.2, 11.2, 10.5, 0.0, 12.2), # Last smaller than ask - ('ask', 12.0, 11.0, 10.5, 1.0, 12.0), # Last smaller than ask - uses ask - ('ask', 12.0, 11.2, 10.5, 0.5, 12.0), # Last smaller than ask - uses ask - ('ask', 10.0, 11.0, 11.0, 0.0, 10.0), - ('ask', 10.11, 11.2, 11.0, 0.0, 10.11), - ('ask', 0.001, 0.002, 11.0, 0.0, 0.001), - ('ask', 0.006, 1.0, 11.0, 0.0, 0.006), - ('ask', 0.006, 1.0, 11.0, None, 0.006), -]) +@pytest.mark.parametrize('side,ask,bid,last,last_ab,expected', get_sell_rate_data) def test_get_exit_rate(default_conf, mocker, caplog, side, bid, ask, last, last_ab, expected) -> None: caplog.set_level(logging.DEBUG) @@ -2481,14 +2485,14 @@ def test_get_ticker_rate_error(mocker, entry, default_conf, caplog, side, is_sho @pytest.mark.parametrize('is_short,side,expected', [ - (False, 'bid', 0.043936), # Value from order_book_l2 fitxure - bids side - (False, 'ask', 0.043949), # Value from order_book_l2 fitxure - asks side - (False, 'other', 0.043936), # Value from order_book_l2 fitxure - bids side - (False, 'same', 0.043949), # Value from order_book_l2 fitxure - asks side - (True, 'bid', 0.043936), # Value from order_book_l2 fitxure - bids side - (True, 'ask', 0.043949), # Value from order_book_l2 fitxure - asks side - (True, 'other', 0.043949), # Value from order_book_l2 fitxure - asks side - (True, 'same', 0.043936), # Value from order_book_l2 fitxure - bids side + (False, 'bid', 0.043936), # Value from order_book_l2 fixture - bids side + (False, 'ask', 0.043949), # Value from order_book_l2 fixture - asks side + (False, 'other', 0.043936), # Value from order_book_l2 fixture - bids side + (False, 'same', 0.043949), # Value from order_book_l2 fixture - asks side + (True, 'bid', 0.043936), # Value from order_book_l2 fixture - bids side + (True, 'ask', 0.043949), # Value from order_book_l2 fixture - asks side + (True, 'other', 0.043949), # Value from order_book_l2 fixture - asks side + (True, 'same', 0.043936), # Value from order_book_l2 fixture - bids side ]) def test_get_exit_rate_orderbook( default_conf, mocker, caplog, is_short, side, expected, order_book_l2): @@ -2521,7 +2525,8 @@ def test_get_exit_rate_orderbook_exception(default_conf, mocker, caplog): exchange = get_patched_exchange(mocker, default_conf) with pytest.raises(PricingError): exchange.get_rate(pair, refresh=True, side="exit", is_short=False) - assert log_has_re(r"Exit Price at location 1 from orderbook could not be determined\..*", + assert log_has_re(rf"{pair} - Exit Price at location 1 from orderbook " + rf"could not be determined\..*", caplog) @@ -2548,6 +2553,84 @@ def test_get_exit_rate_exception(default_conf, mocker, is_short): assert exchange.get_rate(pair, refresh=True, side="exit", is_short=is_short) == 0.13 +@pytest.mark.parametrize("side,ask,bid,last,last_ab,expected", get_entry_rate_data) +@pytest.mark.parametrize("side2", ['bid', 'ask']) +@pytest.mark.parametrize("use_order_book", [True, False]) +def test_get_rates_testing_buy(mocker, default_conf, caplog, side, ask, bid, + last, last_ab, expected, + side2, use_order_book, order_book_l2) -> None: + caplog.set_level(logging.DEBUG) + if last_ab is None: + del default_conf['entry_pricing']['price_last_balance'] + else: + default_conf['entry_pricing']['price_last_balance'] = last_ab + default_conf['entry_pricing']['price_side'] = side + default_conf['exit_pricing']['price_side'] = side2 + default_conf['exit_pricing']['use_order_book'] = use_order_book + api_mock = MagicMock() + api_mock.fetch_l2_order_book = order_book_l2 + api_mock.fetch_ticker = MagicMock( + return_value={'ask': ask, 'last': last, 'bid': bid}) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + + assert exchange.get_rates('ETH/BTC', refresh=True, is_short=False)[0] == expected + assert not log_has("Using cached buy rate for ETH/BTC.", caplog) + + api_mock.fetch_l2_order_book.reset_mock() + api_mock.fetch_ticker.reset_mock() + assert exchange.get_rates('ETH/BTC', refresh=False, is_short=False)[0] == expected + assert log_has("Using cached buy rate for ETH/BTC.", caplog) + assert api_mock.fetch_l2_order_book.call_count == 0 + assert api_mock.fetch_ticker.call_count == 0 + # Running a 2nd time with Refresh on! + caplog.clear() + + assert exchange.get_rates('ETH/BTC', refresh=True, is_short=False)[0] == expected + assert not log_has("Using cached buy rate for ETH/BTC.", caplog) + + assert api_mock.fetch_l2_order_book.call_count == int(use_order_book) + assert api_mock.fetch_ticker.call_count == 1 + + +@pytest.mark.parametrize('side,ask,bid,last,last_ab,expected', get_sell_rate_data) +@pytest.mark.parametrize("side2", ['bid', 'ask']) +@pytest.mark.parametrize("use_order_book", [True, False]) +def test_get_rates_testing_sell(default_conf, mocker, caplog, side, bid, ask, + last, last_ab, expected, + side2, use_order_book, order_book_l2) -> None: + caplog.set_level(logging.DEBUG) + + default_conf['exit_pricing']['price_side'] = side + if last_ab is not None: + default_conf['exit_pricing']['price_last_balance'] = last_ab + + default_conf['entry_pricing']['price_side'] = side2 + default_conf['entry_pricing']['use_order_book'] = use_order_book + api_mock = MagicMock() + api_mock.fetch_l2_order_book = order_book_l2 + api_mock.fetch_ticker = MagicMock( + return_value={'ask': ask, 'last': last, 'bid': bid}) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + + pair = "ETH/BTC" + + # Test regular mode + rate = exchange.get_rates(pair, refresh=True, is_short=False)[1] + assert not log_has("Using cached sell rate for ETH/BTC.", caplog) + assert isinstance(rate, float) + assert rate == expected + # Use caching + api_mock.fetch_l2_order_book.reset_mock() + api_mock.fetch_ticker.reset_mock() + + rate = exchange.get_rates(pair, refresh=False, is_short=False)[1] + assert rate == expected + assert log_has("Using cached sell rate for ETH/BTC.", caplog) + + assert api_mock.fetch_l2_order_book.call_count == 0 + assert api_mock.fetch_ticker.call_count == 0 + + @pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.asyncio async def test___async_get_candle_history_sort(default_conf, mocker, exchange_name): diff --git a/tests/optimize/test_backtesting_adjust_position.py b/tests/optimize/test_backtesting_adjust_position.py index fca9c01b2..2bb7de574 100644 --- a/tests/optimize/test_backtesting_adjust_position.py +++ b/tests/optimize/test_backtesting_adjust_position.py @@ -1,8 +1,10 @@ # pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, unused-argument from copy import deepcopy +from unittest.mock import MagicMock import pandas as pd +import pytest from arrow import Arrow from freqtrade.configuration import TimeRange @@ -87,3 +89,87 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) -> assert (round(ln.iloc[0]["open"], 6) == round(t["close_rate"], 6) or round(ln.iloc[0]["low"], 6) < round( t["close_rate"], 6) < round(ln.iloc[0]["high"], 6)) + + +def test_backtest_position_adjustment_detailed(default_conf, fee, mocker) -> None: + default_conf['use_exit_signal'] = False + mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=10) + mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) + patch_exchange(mocker) + default_conf.update({ + "stake_amount": 100.0, + "dry_run_wallet": 1000.0, + "strategy": "StrategyTestV3" + }) + backtesting = Backtesting(default_conf) + backtesting._set_strategy(backtesting.strategylist[0]) + pair = 'XRP/USDT' + row = [ + pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0), + 2.1, # Open + 2.2, # High + 1.9, # Low + 2.1, # Close + 1, # enter_long + 0, # exit_long + 0, # enter_short + 0, # exit_short + '', # enter_tag + '', # exit_tag + ] + trade = backtesting._enter_trade(pair, row=row, direction='long') + trade.orders[0].close_bt_order(row[0], trade) + assert trade + assert pytest.approx(trade.stake_amount) == 100.0 + assert pytest.approx(trade.amount) == 47.61904762 + assert len(trade.orders) == 1 + backtesting.strategy.adjust_trade_position = MagicMock(return_value=None) + + trade = backtesting._get_adjust_trade_entry_for_candle(trade, row) + assert trade + assert pytest.approx(trade.stake_amount) == 100.0 + assert pytest.approx(trade.amount) == 47.61904762 + assert len(trade.orders) == 1 + # Increase position by 100 + backtesting.strategy.adjust_trade_position = MagicMock(return_value=100) + + trade = backtesting._get_adjust_trade_entry_for_candle(trade, row) + + assert trade + assert pytest.approx(trade.stake_amount) == 200.0 + assert pytest.approx(trade.amount) == 95.23809524 + assert len(trade.orders) == 2 + + # Reduce by more than amount - no change to trade. + backtesting.strategy.adjust_trade_position = MagicMock(return_value=-500) + + trade = backtesting._get_adjust_trade_entry_for_candle(trade, row) + + assert trade + assert pytest.approx(trade.stake_amount) == 200.0 + assert pytest.approx(trade.amount) == 95.23809524 + assert len(trade.orders) == 2 + assert trade.nr_of_successful_entries == 2 + + # Reduce position by 50 + backtesting.strategy.adjust_trade_position = MagicMock(return_value=-100) + trade = backtesting._get_adjust_trade_entry_for_candle(trade, row) + + assert trade + assert pytest.approx(trade.stake_amount) == 100.0 + assert pytest.approx(trade.amount) == 47.61904762 + assert len(trade.orders) == 3 + assert trade.nr_of_successful_entries == 2 + assert trade.nr_of_successful_exits == 1 + + # Adjust below minimum + backtesting.strategy.adjust_trade_position = MagicMock(return_value=-99) + trade = backtesting._get_adjust_trade_entry_for_candle(trade, row) + + assert trade + assert pytest.approx(trade.stake_amount) == 100.0 + assert pytest.approx(trade.amount) == 47.61904762 + assert len(trade.orders) == 3 + assert trade.nr_of_successful_entries == 2 + assert trade.nr_of_successful_exits == 1 diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 6e19fcaf3..02c62e337 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -111,6 +111,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_entry_dist': -0.00010475, 'stoploss_entry_dist_ratio': -0.10448878, 'open_order': None, + 'realized_profit': 0.0, 'exchange': 'binance', 'leverage': 1.0, 'interest_rate': 0.0, @@ -196,6 +197,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_entry_dist_ratio': -0.10448878, 'open_order': None, 'exchange': 'binance', + 'realized_profit': 0.0, 'leverage': 1.0, 'interest_rate': 0.0, 'liquidation_price': None, @@ -841,7 +843,8 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None: 'side': 'sell', 'amount': amount, 'remaining': amount, - 'filled': 0.0 + 'filled': 0.0, + 'id': trade.orders[0].order_id, } ) msg = rpc._rpc_force_exit('3') diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 8d244f3fd..98c06c8e9 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -272,7 +272,7 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None: msg = msg_mock.call_args_list[0][0][0] assert re.search(r'Number of Entries.*2', msg) assert re.search(r'Average Entry Price', msg) - assert re.search(r'Order filled at', msg) + assert re.search(r'Order filled', msg) assert re.search(r'Close Date:', msg) is None assert re.search(r'Close Profit:', msg) is None @@ -959,6 +959,9 @@ def test_telegram_forceexit_handle(default_conf, update, ticker, fee, 'open_date': ANY, 'close_date': ANY, 'close_rate': ANY, + 'stake_amount': 0.0009999999999054, + 'sub_trade': False, + 'cumulative_profit': 0.0, } == last_msg @@ -1028,6 +1031,9 @@ def test_telegram_force_exit_down_handle(default_conf, update, ticker, fee, 'open_date': ANY, 'close_date': ANY, 'close_rate': ANY, + 'stake_amount': 0.0009999999999054, + 'sub_trade': False, + 'cumulative_profit': 0.0, } == last_msg @@ -1087,6 +1093,9 @@ def test_forceexit_all_handle(default_conf, update, ticker, fee, mocker) -> None 'open_date': ANY, 'close_date': ANY, 'close_rate': ANY, + 'stake_amount': 0.0009999999999054, + 'sub_trade': False, + 'cumulative_profit': 0.0, } == msg @@ -1437,7 +1446,7 @@ def test_whitelist_static(default_conf, update, mocker) -> None: def test_whitelist_dynamic(default_conf, update, mocker) -> None: mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) default_conf['pairlists'] = [{'method': 'VolumePairList', - 'number_assets': 4 + 'number_assets': 4 }] telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) @@ -1789,7 +1798,6 @@ def test_send_msg_entry_fill_notification(default_conf, mocker, message_type, en 'leverage': leverage, 'stake_amount': 0.01465333, 'direction': entered, - # 'stake_amount_fiat': 0.0, 'stake_currency': 'BTC', 'fiat_currency': 'USD', 'open_rate': 1.099e-05, @@ -1806,6 +1814,33 @@ def test_send_msg_entry_fill_notification(default_conf, mocker, message_type, en '*Total:* `(0.01465333 BTC, 180.895 USD)`' ) + msg_mock.reset_mock() + telegram.send_msg({ + 'type': message_type, + 'trade_id': 1, + 'enter_tag': enter_signal, + 'exchange': 'Binance', + 'pair': 'ETH/BTC', + 'leverage': leverage, + 'stake_amount': 0.01465333, + 'sub_trade': True, + 'direction': entered, + 'stake_currency': 'BTC', + 'fiat_currency': 'USD', + 'open_rate': 1.099e-05, + 'amount': 1333.3333333333335, + 'open_date': arrow.utcnow().shift(hours=-1) + }) + + assert msg_mock.call_args[0][0] == ( + f'\N{CHECK MARK} *Binance (dry):* {entered}ed ETH/BTC (#1)\n' + f'*Enter Tag:* `{enter_signal}`\n' + '*Amount:* `1333.33333333`\n' + f"{leverage_text}" + '*Open Rate:* `0.00001099`\n' + '*Total:* `(0.01465333 BTC, 180.895 USD)`' + ) + def test_send_msg_sell_notification(default_conf, mocker) -> None: @@ -1840,14 +1875,53 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: '*Unrealized Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n' '*Enter Tag:* `buy_signal1`\n' '*Exit Reason:* `stop_loss`\n' - '*Duration:* `1:00:00 (60.0 min)`\n' '*Direction:* `Long`\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' - '*Close Rate:* `0.00003201`' + '*Exit Rate:* `0.00003201`\n' + '*Duration:* `1:00:00 (60.0 min)`' ) + msg_mock.reset_mock() + telegram.send_msg({ + 'type': RPCMessageType.EXIT, + 'trade_id': 1, + 'exchange': 'Binance', + 'pair': 'KEY/ETH', + 'direction': 'Long', + 'gain': 'loss', + 'limit': 3.201e-05, + 'amount': 1333.3333333333335, + 'order_type': 'market', + 'open_rate': 7.5e-05, + 'current_rate': 3.201e-05, + 'cumulative_profit': -0.15746268, + 'profit_amount': -0.05746268, + 'profit_ratio': -0.57405275, + 'stake_currency': 'ETH', + 'fiat_currency': 'USD', + 'enter_tag': 'buy_signal1', + 'exit_reason': ExitType.STOP_LOSS.value, + 'open_date': arrow.utcnow().shift(days=-1, hours=-2, minutes=-30), + 'close_date': arrow.utcnow(), + 'stake_amount': 0.01, + 'sub_trade': True, + }) + assert msg_mock.call_args[0][0] == ( + '\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n' + '*Unrealized Sub Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n' + '*Cumulative Profit:* (`-0.15746268 ETH / -24.812 USD`)\n' + '*Enter Tag:* `buy_signal1`\n' + '*Exit Reason:* `stop_loss`\n' + '*Direction:* `Long`\n' + '*Amount:* `1333.33333333`\n' + '*Open Rate:* `0.00007500`\n' + '*Current Rate:* `0.00003201`\n' + '*Exit Rate:* `0.00003201`\n' + '*Remaining:* `(0.01 ETH, -24.812 USD)`' + ) + msg_mock.reset_mock() telegram.send_msg({ 'type': RPCMessageType.EXIT, @@ -1871,15 +1945,15 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: }) assert msg_mock.call_args[0][0] == ( '\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n' - '*Unrealized Profit:* `-57.41%`\n' + '*Unrealized Profit:* `-57.41% (loss: -0.05746268 ETH)`\n' '*Enter Tag:* `buy_signal1`\n' '*Exit Reason:* `stop_loss`\n' - '*Duration:* `1 day, 2:30:00 (1590.0 min)`\n' '*Direction:* `Long`\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' - '*Close Rate:* `0.00003201`' + '*Exit Rate:* `0.00003201`\n' + '*Duration:* `1 day, 2:30:00 (1590.0 min)`' ) # Reset singleton function to avoid random breaks telegram._rpc._fiat_converter.convert_amount = old_convamount @@ -1954,15 +2028,15 @@ def test_send_msg_sell_fill_notification(default_conf, mocker, direction, leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else '' assert msg_mock.call_args[0][0] == ( '\N{WARNING SIGN} *Binance (dry):* Exited KEY/ETH (#1)\n' - '*Profit:* `-57.41%`\n' + '*Profit:* `-57.41% (loss: -0.05746268 ETH)`\n' f'*Enter Tag:* `{enter_signal}`\n' '*Exit Reason:* `stop_loss`\n' - '*Duration:* `1 day, 2:30:00 (1590.0 min)`\n' f"*Direction:* `{direction}`\n" f"{leverage_text}" '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' - '*Close Rate:* `0.00003201`' + '*Exit Rate:* `0.00003201`\n' + '*Duration:* `1 day, 2:30:00 (1590.0 min)`' ) @@ -2090,16 +2164,16 @@ def test_send_msg_sell_notification_no_fiat( leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else '' assert msg_mock.call_args[0][0] == ( '\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n' - '*Unrealized Profit:* `-57.41%`\n' + '*Unrealized Profit:* `-57.41% (loss: -0.05746268 ETH)`\n' f'*Enter Tag:* `{enter_signal}`\n' '*Exit Reason:* `stop_loss`\n' - '*Duration:* `2:35:03 (155.1 min)`\n' f'*Direction:* `{direction}`\n' f'{leverage_text}' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' - '*Close Rate:* `0.00003201`' + '*Exit Rate:* `0.00003201`\n' + '*Duration:* `2:35:03 (155.1 min)`' ) diff --git a/tests/strategy/strats/strategy_test_v3.py b/tests/strategy/strats/strategy_test_v3.py index 2c7ccbdf2..088ab21d4 100644 --- a/tests/strategy/strats/strategy_test_v3.py +++ b/tests/strategy/strats/strategy_test_v3.py @@ -185,9 +185,12 @@ class StrategyTestV3(IStrategy): return 3.0 - def adjust_trade_position(self, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, - min_stake: Optional[float], max_stake: float, **kwargs): + def adjust_trade_position(self, trade: Trade, current_time: datetime, + current_rate: float, current_profit: float, + min_stake: Optional[float], max_stake: float, + current_entry_rate: float, current_exit_rate: float, + current_entry_profit: float, current_exit_profit: float, + **kwargs) -> Optional[float]: if current_profit < -0.0075: orders = trade.select_filled_orders(trade.entry_side) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 438a2704c..0b073a062 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -843,8 +843,8 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, # In case of closed order order['status'] = 'closed' - order['price'] = 10 - order['cost'] = 100 + order['average'] = 10 + order['cost'] = 300 order['id'] = '444' mocker.patch('freqtrade.exchange.Exchange.create_order', @@ -855,7 +855,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, assert trade assert trade.open_order_id is None assert trade.open_rate == 10 - assert trade.stake_amount == round(order['price'] * order['filled'] / leverage, 8) + assert trade.stake_amount == round(order['average'] * order['filled'] / leverage, 8) assert pytest.approx(trade.liquidation_price) == liq_price # In case of rejected or expired order and partially filled @@ -863,8 +863,8 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, order['amount'] = 30.0 order['filled'] = 20.0 order['remaining'] = 10.00 - order['price'] = 0.5 - order['cost'] = 15.0 + order['average'] = 0.5 + order['cost'] = 10.0 order['id'] = '555' mocker.patch('freqtrade.exchange.Exchange.create_order', MagicMock(return_value=order)) @@ -872,9 +872,9 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, trade = Trade.query.all()[3] trade.is_short = is_short assert trade - assert trade.open_order_id == '555' + assert trade.open_order_id is None assert trade.open_rate == 0.5 - assert trade.stake_amount == round(order['price'] * order['filled'] / leverage, 8) + assert trade.stake_amount == round(order['average'] * order['filled'] / leverage, 8) # Test with custom stake order['status'] = 'open' @@ -901,7 +901,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, order['amount'] = 30.0 * leverage order['filled'] = 0.0 order['remaining'] = 30.0 - order['price'] = 0.5 + order['average'] = 0.5 order['cost'] = 0.0 order['id'] = '66' mocker.patch('freqtrade.exchange.Exchange.create_order', @@ -1083,7 +1083,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ 'last': 1.9 }), create_order=MagicMock(side_effect=[ - {'id': enter_order['id']}, + enter_order, exit_order, ]), get_fee=fee, @@ -1109,20 +1109,20 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ # should do nothing and return false trade.is_open = True trade.open_order_id = None - trade.stoploss_order_id = 100 + trade.stoploss_order_id = "100" hanging_stoploss_order = MagicMock(return_value={'status': 'open'}) mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', hanging_stoploss_order) assert freqtrade.handle_stoploss_on_exchange(trade) is False - assert trade.stoploss_order_id == 100 + assert trade.stoploss_order_id == "100" # Third case: when stoploss was set but it was canceled for some reason # should set a stoploss immediately and return False caplog.clear() trade.is_open = True trade.open_order_id = None - trade.stoploss_order_id = 100 + trade.stoploss_order_id = "100" canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'}) mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', canceled_stoploss_order) @@ -2039,6 +2039,7 @@ def test_update_trade_state_exception(mocker, default_conf_usdt, is_short, limit trade = MagicMock() trade.open_order_id = '123' + trade.amount = 123 # Test raise of OperationalException exception mocker.patch( @@ -2352,9 +2353,9 @@ def test_close_trade( trade.is_short = is_short assert trade - oobj = Order.parse_from_ccxt_object(enter_order, enter_order['symbol'], 'buy') + oobj = Order.parse_from_ccxt_object(enter_order, enter_order['symbol'], trade.enter_side) trade.update_trade(oobj) - oobj = Order.parse_from_ccxt_object(exit_order, exit_order['symbol'], 'sell') + oobj = Order.parse_from_ccxt_object(exit_order, exit_order['symbol'], trade.exit_side) trade.update_trade(oobj) assert trade.is_open is False @@ -2397,8 +2398,8 @@ def test_manage_open_orders_entry_usercustom( 'freqtrade.exchange.Exchange', fetch_ticker=ticker_usdt, fetch_order=MagicMock(return_value=old_order), - cancel_order_with_result=cancel_order_wr_mock, cancel_order=cancel_order_mock, + cancel_order_with_result=cancel_order_wr_mock, get_fee=fee ) freqtrade = FreqtradeBot(default_conf_usdt) @@ -2446,7 +2447,9 @@ def test_manage_open_orders_entry( ) -> None: old_order = limit_sell_order_old if is_short else limit_buy_order_old rpc_mock = patch_RPCManager(mocker) - old_order['id'] = open_trade.open_order_id + open_trade.open_order_id = old_order['id'] + order = Order.parse_from_ccxt_object(old_order, 'mocked', 'buy') + open_trade.orders[0] = order limit_buy_cancel = deepcopy(old_order) limit_buy_cancel['status'] = 'canceled' cancel_order_mock = MagicMock(return_value=limit_buy_cancel) @@ -2637,7 +2640,9 @@ def test_manage_open_orders_exit_usercustom( is_short, open_trade_usdt, caplog ) -> None: default_conf_usdt["unfilledtimeout"] = {"entry": 1440, "exit": 1440, "exit_timeout_count": 1} - limit_sell_order_old['id'] = open_trade_usdt.open_order_id + open_trade_usdt.open_order_id = limit_sell_order_old['id'] + order = Order.parse_from_ccxt_object(limit_sell_order_old, 'mocked', 'sell') + open_trade_usdt.orders[0] = order if is_short: limit_sell_order_old['side'] = 'buy' open_trade_usdt.is_short = is_short @@ -3250,6 +3255,9 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_ 'open_date': ANY, 'close_date': ANY, 'close_rate': ANY, + 'sub_trade': False, + 'cumulative_profit': 0.0, + 'stake_amount': pytest.approx(60), } == last_msg @@ -3310,6 +3318,9 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd 'open_date': ANY, 'close_date': ANY, 'close_rate': ANY, + 'sub_trade': False, + 'cumulative_profit': 0.0, + 'stake_amount': pytest.approx(60), } == last_msg @@ -3391,6 +3402,9 @@ def test_execute_trade_exit_custom_exit_price( 'open_date': ANY, 'close_date': ANY, 'close_rate': ANY, + 'sub_trade': False, + 'cumulative_profit': 0.0, + 'stake_amount': pytest.approx(60), } == last_msg @@ -3459,6 +3473,9 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run( 'open_date': ANY, 'close_date': ANY, 'close_rate': ANY, + 'sub_trade': False, + 'cumulative_profit': 0.0, + 'stake_amount': pytest.approx(60), } == last_msg @@ -3690,7 +3707,7 @@ def test_execute_trade_exit_market_order( ) assert not trade.is_open - assert trade.close_profit == profit_ratio + assert pytest.approx(trade.close_profit) == profit_ratio assert rpc_mock.call_count == 4 last_msg = rpc_mock.call_args_list[-2][0][0] @@ -3718,6 +3735,9 @@ def test_execute_trade_exit_market_order( 'open_date': ANY, 'close_date': ANY, 'close_rate': ANY, + 'sub_trade': False, + 'cumulative_profit': 0.0, + 'stake_amount': pytest.approx(60), } == last_msg @@ -3789,7 +3809,7 @@ def test_exit_profit_only( 'last': bid }), create_order=MagicMock(side_effect=[ - limit_order_open[eside], + limit_order[eside], {'id': 1234553382}, ]), get_fee=fee, @@ -4081,7 +4101,7 @@ def test_trailing_stop_loss_positive( 'last': enter_price - (-0.01 if is_short else 0.01), }), create_order=MagicMock(side_effect=[ - limit_order_open[eside], + limit_order[eside], {'id': 1234553382}, ]), get_fee=fee, @@ -4632,7 +4652,7 @@ def test_order_book_entry_pricing1(mocker, default_conf_usdt, order_book_l2, exc with pytest.raises(PricingError): freqtrade.exchange.get_rate('ETH/USDT', side="entry", is_short=False, refresh=True) assert log_has_re( - r'Entry Price at location 1 from orderbook could not be determined.', caplog) + r'ETH/USDT - Entry Price at location 1 from orderbook could not be determined.', caplog) else: assert freqtrade.exchange.get_rate( 'ETH/USDT', side="entry", is_short=False, refresh=True) == 0.043935 @@ -4711,8 +4731,9 @@ def test_order_book_exit_pricing( return_value={'bids': [[]], 'asks': [[]]}) with pytest.raises(PricingError): freqtrade.handle_trade(trade) - assert log_has_re(r'Exit Price at location 1 from orderbook could not be determined\..*', - caplog) + assert log_has_re( + r"ETH/USDT - Exit Price at location 1 from orderbook could not be determined\..*", + caplog) def test_startup_state(default_conf_usdt, mocker): @@ -5385,7 +5406,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: 'status': None, 'price': 9, 'amount': 12, - 'cost': 100, + 'cost': 108, 'ft_is_open': True, 'id': '651', 'order_id': '651' @@ -5480,7 +5501,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: assert trade.open_order_id is None assert pytest.approx(trade.open_rate) == 9.90909090909 assert trade.amount == 22 - assert trade.stake_amount == 218 + assert pytest.approx(trade.stake_amount) == 218 orders = Order.query.all() assert orders @@ -5533,6 +5554,329 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: # Make sure the closed order is found as the second order. order = trade.select_order('buy', False) assert order.order_id == '652' + closed_sell_dca_order_1 = { + 'ft_pair': pair, + 'status': 'closed', + 'ft_order_side': 'sell', + 'side': 'sell', + 'type': 'limit', + 'price': 8, + 'average': 8, + 'amount': 15, + 'filled': 15, + 'cost': 120, + 'ft_is_open': False, + 'id': '653', + 'order_id': '653' + } + mocker.patch('freqtrade.exchange.Exchange.create_order', + MagicMock(return_value=closed_sell_dca_order_1)) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', + MagicMock(return_value=closed_sell_dca_order_1)) + mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + MagicMock(return_value=closed_sell_dca_order_1)) + assert freqtrade.execute_trade_exit(trade=trade, limit=8, + exit_check=ExitCheckTuple(exit_type=ExitType.PARTIAL_EXIT), + sub_trade_amt=15) + + # Assert trade is as expected (averaged dca) + trade = Trade.query.first() + assert trade + assert trade.open_order_id is None + assert trade.is_open + assert trade.amount == 22 + assert trade.stake_amount == 192.05405405405406 + assert pytest.approx(trade.open_rate) == 8.729729729729 + + orders = Order.query.all() + assert orders + assert len(orders) == 4 + + # Make sure the closed order is found as the second order. + order = trade.select_order('sell', False) + assert order.order_id == '653' + + +def test_position_adjust2(mocker, default_conf_usdt, fee) -> None: + """ + TODO: Should be adjusted to test both long and short + buy 100 @ 11 + sell 50 @ 8 + sell 50 @ 16 + """ + patch_RPCManager(mocker) + patch_exchange(mocker) + patch_wallet(mocker, free=10000) + default_conf_usdt.update({ + "position_adjustment_enable": True, + "dry_run": False, + "stake_amount": 200.0, + "dry_run_wallet": 1000.0, + }) + freqtrade = FreqtradeBot(default_conf_usdt) + freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True) + bid = 11 + amount = 100 + buy_rate_mock = MagicMock(return_value=bid) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_rate=buy_rate_mock, + fetch_ticker=MagicMock(return_value={ + 'bid': 10, + 'ask': 12, + 'last': 11 + }), + get_min_pair_stake_amount=MagicMock(return_value=1), + get_fee=fee, + ) + pair = 'ETH/USDT' + # Initial buy + closed_successful_buy_order = { + 'pair': pair, + 'ft_pair': pair, + 'ft_order_side': 'buy', + 'side': 'buy', + 'type': 'limit', + 'status': 'closed', + 'price': bid, + 'average': bid, + 'cost': bid * amount, + 'amount': amount, + 'filled': amount, + 'ft_is_open': False, + 'id': '600', + 'order_id': '600' + } + mocker.patch('freqtrade.exchange.Exchange.create_order', + MagicMock(return_value=closed_successful_buy_order)) + mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + MagicMock(return_value=closed_successful_buy_order)) + assert freqtrade.execute_entry(pair, amount) + # Should create an closed trade with an no open order id + # Order is filled and trade is open + orders = Order.query.all() + assert orders + assert len(orders) == 1 + trade = Trade.query.first() + assert trade + assert trade.is_open is True + assert trade.open_order_id is None + assert trade.open_rate == bid + assert trade.stake_amount == bid * amount + + # Assume it does nothing since order is closed and trade is open + freqtrade.update_closed_trades_without_assigned_fees() + + trade = Trade.query.first() + assert trade + assert trade.is_open is True + assert trade.open_order_id is None + assert trade.open_rate == bid + assert trade.stake_amount == bid * amount + assert not trade.fee_updated(trade.entry_side) + + freqtrade.manage_open_orders() + + trade = Trade.query.first() + assert trade + assert trade.is_open is True + assert trade.open_order_id is None + assert trade.open_rate == bid + assert trade.stake_amount == bid * amount + assert not trade.fee_updated(trade.entry_side) + + amount = 50 + ask = 8 + closed_sell_dca_order_1 = { + 'ft_pair': pair, + 'status': 'closed', + 'ft_order_side': 'sell', + 'side': 'sell', + 'type': 'limit', + 'price': ask, + 'average': ask, + 'amount': amount, + 'filled': amount, + 'cost': amount * ask, + 'ft_is_open': False, + 'id': '601', + 'order_id': '601' + } + mocker.patch('freqtrade.exchange.Exchange.create_order', + MagicMock(return_value=closed_sell_dca_order_1)) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', + MagicMock(return_value=closed_sell_dca_order_1)) + mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + MagicMock(return_value=closed_sell_dca_order_1)) + assert freqtrade.execute_trade_exit(trade=trade, limit=ask, + exit_check=ExitCheckTuple(exit_type=ExitType.PARTIAL_EXIT), + sub_trade_amt=amount) + trades: List[Trade] = trade.get_open_trades_without_assigned_fees() + assert len(trades) == 1 + # Assert trade is as expected (averaged dca) + + trade = Trade.query.first() + assert trade + assert trade.open_order_id is None + assert trade.amount == 50 + assert trade.open_rate == 11 + assert trade.stake_amount == 550 + assert pytest.approx(trade.realized_profit) == -152.375 + assert pytest.approx(trade.close_profit_abs) == -152.375 + + orders = Order.query.all() + assert orders + assert len(orders) == 2 + # Make sure the closed order is found as the second order. + order = trade.select_order('sell', False) + assert order.order_id == '601' + + amount = 50 + ask = 16 + closed_sell_dca_order_2 = { + 'ft_pair': pair, + 'status': 'closed', + 'ft_order_side': 'sell', + 'side': 'sell', + 'type': 'limit', + 'price': ask, + 'average': ask, + 'amount': amount, + 'filled': amount, + 'cost': amount * ask, + 'ft_is_open': False, + 'id': '602', + 'order_id': '602' + } + mocker.patch('freqtrade.exchange.Exchange.create_order', + MagicMock(return_value=closed_sell_dca_order_2)) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', + MagicMock(return_value=closed_sell_dca_order_2)) + mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + MagicMock(return_value=closed_sell_dca_order_2)) + assert freqtrade.execute_trade_exit(trade=trade, limit=ask, + exit_check=ExitCheckTuple(exit_type=ExitType.PARTIAL_EXIT), + sub_trade_amt=amount) + # Assert trade is as expected (averaged dca) + + trade = Trade.query.first() + assert trade + assert trade.open_order_id is None + assert trade.amount == 50 + assert trade.open_rate == 11 + assert trade.stake_amount == 550 + # Trade fully realized + assert pytest.approx(trade.realized_profit) == 94.25 + assert pytest.approx(trade.close_profit_abs) == 94.25 + orders = Order.query.all() + assert orders + assert len(orders) == 3 + + # Make sure the closed order is found as the second order. + order = trade.select_order('sell', False) + assert order.order_id == '602' + assert trade.is_open is False + + +@pytest.mark.parametrize('data', [ + ( + # tuple 1 - side amount, price + # tuple 2 - amount, open_rate, stake_amount, cumulative_profit, realized_profit, rel_profit + (('buy', 100, 10), (100.0, 10.0, 1000.0, 0.0, None, None)), + (('buy', 100, 15), (200.0, 12.5, 2500.0, 0.0, None, None)), + (('sell', 50, 12), (150.0, 12.5, 1875.0, -28.0625, -28.0625, -0.044788)), + (('sell', 100, 20), (50.0, 12.5, 625.0, 713.8125, 741.875, 0.59201995)), + (('sell', 50, 5), (50.0, 12.5, 625.0, 336.625, 336.625, 0.1343142)), # final profit (sum) + ), + ( + (('buy', 100, 3), (100.0, 3.0, 300.0, 0.0, None, None)), + (('buy', 100, 7), (200.0, 5.0, 1000.0, 0.0, None, None)), + (('sell', 100, 11), (100.0, 5.0, 500.0, 596.0, 596.0, 1.189027)), + (('buy', 150, 15), (250.0, 11.0, 2750.0, 596.0, 596.0, 1.189027)), + (('sell', 100, 19), (150.0, 11.0, 1650.0, 1388.5, 792.5, 0.7186579)), + (('sell', 150, 23), (150.0, 11.0, 1650.0, 3175.75, 3175.75, 0.9747170)), # final profit + ) +]) +def test_position_adjust3(mocker, default_conf_usdt, fee, data) -> None: + default_conf_usdt.update({ + "position_adjustment_enable": True, + "dry_run": False, + "stake_amount": 200.0, + "dry_run_wallet": 1000.0, + }) + patch_RPCManager(mocker) + patch_exchange(mocker) + patch_wallet(mocker, free=10000) + freqtrade = FreqtradeBot(default_conf_usdt) + trade = None + freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True) + for idx, (order, result) in enumerate(data): + amount = order[1] + price = order[2] + price_mock = MagicMock(return_value=price) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_rate=price_mock, + fetch_ticker=MagicMock(return_value={ + 'bid': 10, + 'ask': 12, + 'last': 11 + }), + get_min_pair_stake_amount=MagicMock(return_value=1), + get_fee=fee, + ) + pair = 'ETH/USDT' + closed_successful_order = { + 'pair': pair, + 'ft_pair': pair, + 'ft_order_side': order[0], + 'side': order[0], + 'type': 'limit', + 'status': 'closed', + 'price': price, + 'average': price, + 'cost': price * amount, + 'amount': amount, + 'filled': amount, + 'ft_is_open': False, + 'id': f'60{idx}', + 'order_id': f'60{idx}' + } + mocker.patch('freqtrade.exchange.Exchange.create_order', + MagicMock(return_value=closed_successful_order)) + mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + MagicMock(return_value=closed_successful_order)) + if order[0] == 'buy': + assert freqtrade.execute_entry(pair, amount, trade=trade) + else: + assert freqtrade.execute_trade_exit( + trade=trade, limit=price, + exit_check=ExitCheckTuple(exit_type=ExitType.PARTIAL_EXIT), + sub_trade_amt=amount) + + orders1 = Order.query.all() + assert orders1 + assert len(orders1) == idx + 1 + + trade = Trade.query.first() + assert trade + if idx < len(data) - 1: + assert trade.is_open is True + assert trade.open_order_id is None + assert trade.amount == result[0] + assert trade.open_rate == result[1] + assert trade.stake_amount == result[2] + assert pytest.approx(trade.realized_profit) == result[3] + assert pytest.approx(trade.close_profit_abs) == result[4] + assert pytest.approx(trade.close_profit) == result[5] + + order_obj = trade.select_order(order[0], False) + assert order_obj.order_id == f'60{idx}' + + trade = Trade.query.first() + assert trade + assert trade.open_order_id is None + assert trade.is_open is False def test_process_open_trade_positions_exception(mocker, default_conf_usdt, fee, caplog) -> None: @@ -5556,9 +5900,25 @@ def test_check_and_call_adjust_trade_position(mocker, default_conf_usdt, fee, ca "max_entry_position_adjustment": 0, }) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) - + buy_rate_mock = MagicMock(return_value=10) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_rate=buy_rate_mock, + fetch_ticker=MagicMock(return_value={ + 'bid': 10, + 'ask': 12, + 'last': 11 + }), + get_min_pair_stake_amount=MagicMock(return_value=1), + get_fee=fee, + ) create_mock_trades(fee) caplog.set_level(logging.DEBUG) - + freqtrade.strategy.adjust_trade_position = MagicMock(return_value=10) freqtrade.process_open_trade_positions() assert log_has_re(r"Max adjustment entries for .* has been reached\.", caplog) + + caplog.clear() + freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-10) + freqtrade.process_open_trade_positions() + assert log_has_re(r"LIMIT_SELL has been fulfilled.*", caplog) diff --git a/tests/test_integration.py b/tests/test_integration.py index 83f54becb..40fdb4277 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -6,7 +6,7 @@ from freqtrade.enums import ExitCheckTuple, ExitType from freqtrade.persistence import Trade from freqtrade.persistence.models import Order from freqtrade.rpc.rpc import RPC -from tests.conftest import get_patched_freqtradebot, patch_get_signal +from tests.conftest import get_patched_freqtradebot, log_has_re, patch_get_signal def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee, @@ -455,3 +455,60 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, fee, mocker) -> None: # Check the 2 filled orders equal the above amount assert pytest.approx(trade.orders[1].amount) == 30.150753768 assert pytest.approx(trade.orders[-1].amount) == 61.538461232 + + +def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog) -> None: + default_conf_usdt['position_adjustment_enable'] = True + + freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=ticker_usdt, + get_fee=fee, + amount_to_precision=lambda s, x, y: y, + price_to_precision=lambda s, x, y: y, + get_min_pair_stake_amount=MagicMock(return_value=10), + ) + + patch_get_signal(freqtrade) + freqtrade.enter_positions() + + assert len(Trade.get_trades().all()) == 1 + trade = Trade.get_trades().first() + assert len(trade.orders) == 1 + assert pytest.approx(trade.stake_amount) == 60 + assert pytest.approx(trade.amount) == 30.0 + assert trade.open_rate == 2.0 + + # Too small size + freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-59) + freqtrade.process() + trade = Trade.get_trades().first() + assert len(trade.orders) == 1 + assert pytest.approx(trade.stake_amount) == 60 + assert pytest.approx(trade.amount) == 30.0 + assert log_has_re("Remaining amount of 1.6.* would be too small.", caplog) + + freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-20) + + freqtrade.process() + trade = Trade.get_trades().first() + assert len(trade.orders) == 2 + assert trade.orders[-1].ft_order_side == 'sell' + assert pytest.approx(trade.stake_amount) == 40.198 + assert pytest.approx(trade.amount) == 20.099 + assert trade.open_rate == 2.0 + assert trade.is_open + caplog.clear() + + # Sell more than what we got (we got ~20 coins left) + # First adjusts the amount to 20 - then rejects. + freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-50) + freqtrade.process() + assert log_has_re("Adjusting amount to trade.amount as it is higher.*", caplog) + assert log_has_re("Remaining amount of 0.0 would be too small.", caplog) + trade = Trade.get_trades().first() + assert len(trade.orders) == 2 + assert trade.orders[-1].ft_order_side == 'sell' + assert pytest.approx(trade.stake_amount) == 40.198 + assert trade.is_open diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 0c1fc01a5..42fcc7413 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -500,7 +500,7 @@ def test_update_limit_order(fee, caplog, limit_buy_order_usdt, limit_sell_order_ assert trade.close_profit is None assert trade.close_date is None - trade.open_order_id = 'something' + trade.open_order_id = enter_order['id'] oobj = Order.parse_from_ccxt_object(enter_order, 'ADA/USDT', entry_side) trade.orders.append(oobj) trade.update_trade(oobj) @@ -515,7 +515,7 @@ def test_update_limit_order(fee, caplog, limit_buy_order_usdt, limit_sell_order_ caplog) caplog.clear() - trade.open_order_id = 'something' + trade.open_order_id = enter_order['id'] time_machine.move_to("2022-03-31 21:45:05 +00:00") oobj = Order.parse_from_ccxt_object(exit_order, 'ADA/USDT', exit_side) trade.orders.append(oobj) @@ -550,7 +550,7 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee, leverage=1.0, ) - trade.open_order_id = 'something' + trade.open_order_id = 'mocked_market_buy' oobj = Order.parse_from_ccxt_object(market_buy_order_usdt, 'ADA/USDT', 'buy') trade.orders.append(oobj) trade.update_trade(oobj) @@ -565,7 +565,7 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee, caplog.clear() trade.is_open = True - trade.open_order_id = 'something' + trade.open_order_id = 'mocked_market_sell' oobj = Order.parse_from_ccxt_object(market_sell_order_usdt, 'ADA/USDT', 'sell') trade.orders.append(oobj) trade.update_trade(oobj) @@ -630,14 +630,14 @@ def test_calc_open_close_trade_price( trade.open_rate = 2.0 trade.close_rate = 2.2 trade.recalc_open_trade_value() - assert isclose(trade._calc_open_trade_value(), open_value) + assert isclose(trade._calc_open_trade_value(trade.amount, trade.open_rate), open_value) assert isclose(trade.calc_close_trade_value(trade.close_rate), close_value) assert isclose(trade.calc_profit(trade.close_rate), round(profit, 8)) assert pytest.approx(trade.calc_profit_ratio(trade.close_rate)) == profit_ratio @pytest.mark.usefixtures("init_persistence") -def test_trade_close(limit_buy_order_usdt, limit_sell_order_usdt, fee): +def test_trade_close(fee): trade = Trade( pair='ADA/USDT', stake_amount=60.0, @@ -815,7 +815,7 @@ def test_calc_open_trade_value( trade.update_trade(oobj) # Buy @ 2.0 # Get the open rate price with the standard fee rate - assert trade._calc_open_trade_value() == result + assert trade._calc_open_trade_value(trade.amount, trade.open_rate) == result @pytest.mark.parametrize( @@ -905,7 +905,7 @@ def test_calc_close_trade_price( ('binance', False, 1, 1.9, 0.003, -3.3209999, -0.055211970, spot, 0), ('binance', False, 1, 2.2, 0.003, 5.6520000, 0.093965087, spot, 0), - # # FUTURES, funding_fee=1 + # FUTURES, funding_fee=1 ('binance', False, 1, 2.1, 0.0025, 3.6925, 0.06138819, futures, 1), ('binance', False, 3, 2.1, 0.0025, 3.6925, 0.18416458, futures, 1), ('binance', True, 1, 2.1, 0.0025, -2.3074999, -0.03855472, futures, 1), @@ -1191,6 +1191,11 @@ def test_calc_profit( assert pytest.approx(trade.calc_profit(rate=close_rate)) == round(profit, 8) assert pytest.approx(trade.calc_profit_ratio(rate=close_rate)) == round(profit_ratio, 8) + assert pytest.approx(trade.calc_profit(close_rate, trade.amount, + trade.open_rate)) == round(profit, 8) + assert pytest.approx(trade.calc_profit_ratio(close_rate, trade.amount, + trade.open_rate)) == round(profit_ratio, 8) + def test_migrate_new(mocker, default_conf, fee, caplog): """ @@ -1382,7 +1387,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): assert log_has("trying trades_bak2", caplog) assert log_has("Running database migration for trades - backup: trades_bak2, orders_bak0", caplog) - assert trade.open_trade_value == trade._calc_open_trade_value() + assert trade.open_trade_value == trade._calc_open_trade_value(trade.amount, trade.open_rate) assert trade.close_profit_abs is None orders = trade.orders @@ -1744,6 +1749,7 @@ def test_to_json(fee): 'stake_amount': 0.001, 'trade_duration': None, 'trade_duration_s': None, + 'realized_profit': 0.0, 'close_profit': None, 'close_profit_pct': None, 'close_profit_abs': None, @@ -1820,6 +1826,7 @@ def test_to_json(fee): 'initial_stop_loss_abs': None, 'initial_stop_loss_pct': None, 'initial_stop_loss_ratio': None, + 'realized_profit': 0.0, 'close_profit': None, 'close_profit_pct': None, 'close_profit_abs': None, @@ -2262,7 +2269,7 @@ def test_update_order_from_ccxt(caplog): 'symbol': 'ADA/USDT', 'type': 'limit', 'price': 1234.5, - 'amount': 20.0, + 'amount': 20.0, 'filled': 9, 'remaining': 11, 'status': 'open', @@ -2421,7 +2428,7 @@ def test_recalc_trade_from_orders(fee): ) assert fee.return_value == 0.0025 - assert trade._calc_open_trade_value() == o1_trade_val + assert trade._calc_open_trade_value(trade.amount, trade.open_rate) == o1_trade_val assert trade.amount == o1_amount assert trade.stake_amount == o1_cost assert trade.open_rate == o1_rate @@ -2533,7 +2540,8 @@ def test_recalc_trade_from_orders(fee): assert pytest.approx(trade.fee_open_cost) == o1_fee_cost + o2_fee_cost + o3_fee_cost assert pytest.approx(trade.open_trade_value) == o1_trade_val + o2_trade_val + o3_trade_val - # Just to make sure sell orders are ignored, let's calculate one more time. + # Just to make sure full sell orders are ignored, let's calculate one more time. + sell1 = Order( ft_order_side='sell', ft_pair=trade.pair, @@ -2695,7 +2703,7 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee, is_short): assert trade.open_trade_value == 2 * o1_trade_val assert trade.nr_of_successful_entries == 2 - # Just to make sure exit orders are ignored, let's calculate one more time. + # Reduce position - this will reduce amount again. sell1 = Order( ft_order_side=exit_side, ft_pair=trade.pair, @@ -2706,7 +2714,7 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee, is_short): side=exit_side, price=4, average=3, - filled=2, + filled=o1_amount, remaining=1, cost=5, order_date=trade.open_date, @@ -2715,11 +2723,11 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee, is_short): trade.orders.append(sell1) trade.recalc_trade_from_orders() - assert trade.amount == 2 * o1_amount - assert trade.stake_amount == 2 * o1_amount + assert trade.amount == o1_amount + assert trade.stake_amount == o1_amount assert trade.open_rate == o1_rate - assert trade.fee_open_cost == 2 * o1_fee_cost - assert trade.open_trade_value == 2 * o1_trade_val + assert trade.fee_open_cost == o1_fee_cost + assert trade.open_trade_value == o1_trade_val assert trade.nr_of_successful_entries == 2 # Check with 1 order @@ -2743,11 +2751,11 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee, is_short): trade.recalc_trade_from_orders() # Calling recalc with single initial order should not change anything - assert trade.amount == 3 * o1_amount - assert trade.stake_amount == 3 * o1_amount + assert trade.amount == 2 * o1_amount + assert trade.stake_amount == 2 * o1_amount assert trade.open_rate == o1_rate - assert trade.fee_open_cost == 3 * o1_fee_cost - assert trade.open_trade_value == 3 * o1_trade_val + assert trade.fee_open_cost == 2 * o1_fee_cost + assert trade.open_trade_value == 2 * o1_trade_val assert trade.nr_of_successful_entries == 3 @@ -2815,3 +2823,144 @@ def test_order_to_ccxt(limit_buy_order_open): del raw_order['stopPrice'] del limit_buy_order_open['datetime'] assert raw_order == limit_buy_order_open + + +@pytest.mark.usefixtures("init_persistence") +@pytest.mark.parametrize('data', [ + { + # tuple 1 - side, amount, price + # tuple 2 - amount, open_rate, stake_amount, cumulative_profit, realized_profit, rel_profit + 'orders': [ + (('buy', 100, 10), (100.0, 10.0, 1000.0, 0.0, None, None)), + (('buy', 100, 15), (200.0, 12.5, 2500.0, 0.0, None, None)), + (('sell', 50, 12), (150.0, 12.5, 1875.0, -25.0, -25.0, -0.04)), + (('sell', 100, 20), (50.0, 12.5, 625.0, 725.0, 750.0, 0.60)), + (('sell', 50, 5), (50.0, 12.5, 625.0, 350.0, -375.0, -0.60)), + ], + 'end_profit': 350.0, + 'end_profit_ratio': 0.14, + 'fee': 0.0, + }, + { + 'orders': [ + (('buy', 100, 10), (100.0, 10.0, 1000.0, 0.0, None, None)), + (('buy', 100, 15), (200.0, 12.5, 2500.0, 0.0, None, None)), + (('sell', 50, 12), (150.0, 12.5, 1875.0, -28.0625, -28.0625, -0.044788)), + (('sell', 100, 20), (50.0, 12.5, 625.0, 713.8125, 741.875, 0.59201995)), + (('sell', 50, 5), (50.0, 12.5, 625.0, 336.625, -377.1875, -0.60199501)), + ], + 'end_profit': 336.625, + 'end_profit_ratio': 0.1343142, + 'fee': 0.0025, + }, + { + 'orders': [ + (('buy', 100, 3), (100.0, 3.0, 300.0, 0.0, None, None)), + (('buy', 100, 7), (200.0, 5.0, 1000.0, 0.0, None, None)), + (('sell', 100, 11), (100.0, 5.0, 500.0, 596.0, 596.0, 1.189027)), + (('buy', 150, 15), (250.0, 11.0, 2750.0, 596.0, 596.0, 1.189027)), + (('sell', 100, 19), (150.0, 11.0, 1650.0, 1388.5, 792.5, 0.7186579)), + (('sell', 150, 23), (150.0, 11.0, 1650.0, 3175.75, 1787.25, 1.08048062)), + ], + 'end_profit': 3175.75, + 'end_profit_ratio': 0.9747170, + 'fee': 0.0025, + }, + { + # Test above without fees + 'orders': [ + (('buy', 100, 3), (100.0, 3.0, 300.0, 0.0, None, None)), + (('buy', 100, 7), (200.0, 5.0, 1000.0, 0.0, None, None)), + (('sell', 100, 11), (100.0, 5.0, 500.0, 600.0, 600.0, 1.2)), + (('buy', 150, 15), (250.0, 11.0, 2750.0, 600.0, 600.0, 1.2)), + (('sell', 100, 19), (150.0, 11.0, 1650.0, 1400.0, 800.0, 0.72727273)), + (('sell', 150, 23), (150.0, 11.0, 1650.0, 3200.0, 1800.0, 1.09090909)), + ], + 'end_profit': 3200.0, + 'end_profit_ratio': 0.98461538, + 'fee': 0.0, + }, + { + 'orders': [ + (('buy', 100, 8), (100.0, 8.0, 800.0, 0.0, None, None)), + (('buy', 100, 9), (200.0, 8.5, 1700.0, 0.0, None, None)), + (('sell', 100, 10), (100.0, 8.5, 850.0, 150.0, 150.0, 0.17647059)), + (('buy', 150, 11), (250.0, 10, 2500.0, 150.0, 150.0, 0.17647059)), + (('sell', 100, 12), (150.0, 10.0, 1500.0, 350.0, 350.0, 0.2)), + (('sell', 150, 14), (150.0, 10.0, 1500.0, 950.0, 950.0, 0.40)), + ], + 'end_profit': 950.0, + 'end_profit_ratio': 0.283582, + 'fee': 0.0, + }, +]) +def test_recalc_trade_from_orders_dca(data) -> None: + + pair = 'ETH/USDT' + trade = Trade( + id=2, + pair=pair, + stake_amount=1000, + open_rate=data['orders'][0][0][2], + amount=data['orders'][0][0][1], + is_open=True, + open_date=arrow.utcnow().datetime, + fee_open=data['fee'], + fee_close=data['fee'], + exchange='binance', + is_short=False, + leverage=1.0, + trading_mode=TradingMode.SPOT + ) + Trade.query.session.add(trade) + + for idx, (order, result) in enumerate(data['orders']): + amount = order[1] + price = order[2] + + order_obj = Order( + ft_order_side=order[0], + ft_pair=trade.pair, + order_id=f"order_{order[0]}_{idx}", + ft_is_open=False, + status="closed", + symbol=trade.pair, + order_type="market", + side=order[0], + price=price, + average=price, + filled=amount, + remaining=0, + cost=amount * price, + order_date=arrow.utcnow().shift(hours=-10 + idx).datetime, + order_filled_date=arrow.utcnow().shift(hours=-10 + idx).datetime, + ) + trade.orders.append(order_obj) + trade.recalc_trade_from_orders() + Trade.commit() + + orders1 = Order.query.all() + assert orders1 + assert len(orders1) == idx + 1 + + trade = Trade.query.first() + assert trade + assert len(trade.orders) == idx + 1 + if idx < len(data) - 1: + assert trade.is_open is True + assert trade.open_order_id is None + assert trade.amount == result[0] + assert trade.open_rate == result[1] + assert trade.stake_amount == result[2] + # TODO: enable the below. + assert pytest.approx(trade.realized_profit) == result[3] + # assert pytest.approx(trade.close_profit_abs) == result[4] + assert pytest.approx(trade.close_profit) == result[5] + + trade.close(price) + assert pytest.approx(trade.close_profit_abs) == data['end_profit'] + assert pytest.approx(trade.close_profit) == data['end_profit_ratio'] + assert not trade.is_open + trade = Trade.query.first() + assert trade + assert trade.open_order_id is None From 7a696f58f95839c917d3a95225e86c166fb21afd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Aug 2022 03:01:30 +0000 Subject: [PATCH 21/56] Bump ccxt from 1.91.29 to 1.91.52 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.91.29 to 1.91.52. - [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.91.29...1.91.52) --- 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 b9e87749d..9aec6af63 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.23.1 pandas==1.4.3 pandas-ta==0.3.14b -ccxt==1.91.29 +ccxt==1.91.52 # Pin cryptography for now due to rust build errors with piwheels cryptography==37.0.4 aiohttp==3.8.1 From a75fa26caf915d3235b9e0e7bda794a43441a8b6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Aug 2022 03:01:38 +0000 Subject: [PATCH 22/56] Bump scipy from 1.8.1 to 1.9.0 Bumps [scipy](https://github.com/scipy/scipy) from 1.8.1 to 1.9.0. - [Release notes](https://github.com/scipy/scipy/releases) - [Commits](https://github.com/scipy/scipy/compare/v1.8.1...v1.9.0) --- updated-dependencies: - dependency-name: scipy dependency-type: direct:production update-type: version-update:semver-minor ... 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 94e59ec15..cc659fc50 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -2,7 +2,7 @@ -r requirements.txt # Required for hyperopt -scipy==1.8.1 +scipy==1.9.0 scikit-learn==1.1.1 scikit-optimize==0.9.0 filelock==3.7.1 From b4ded59c63f6c2fa92e6e1722f0bb6cf758589e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Aug 2022 03:01:43 +0000 Subject: [PATCH 23/56] Bump flake8 from 4.0.1 to 5.0.1 Bumps [flake8](https://github.com/pycqa/flake8) from 4.0.1 to 5.0.1. - [Release notes](https://github.com/pycqa/flake8/releases) - [Commits](https://github.com/pycqa/flake8/compare/4.0.1...5.0.1) --- updated-dependencies: - dependency-name: flake8 dependency-type: direct:development update-type: version-update:semver-major ... 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 3b98e20db..ee7899eeb 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,7 +5,7 @@ -r docs/requirements-docs.txt coveralls==3.3.1 -flake8==4.0.1 +flake8==5.0.1 flake8-tidy-imports==4.8.0 mypy==0.971 pre-commit==2.20.0 From 372be542528457a22ce16f5d408551abe170377e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Aug 2022 03:01:46 +0000 Subject: [PATCH 24/56] Bump types-requests from 2.28.3 to 2.28.6 Bumps [types-requests](https://github.com/python/typeshed) from 2.28.3 to 2.28.6. - [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 3b98e20db..d34e551d2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -24,6 +24,6 @@ nbconvert==6.5.0 # mypy types types-cachetools==5.2.1 types-filelock==3.2.7 -types-requests==2.28.3 +types-requests==2.28.6 types-tabulate==0.8.11 types-python-dateutil==2.8.19 From ed230dd750e48ecc2148f91ea72bb39cf95dac09 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Aug 2022 03:01:52 +0000 Subject: [PATCH 25/56] Bump orjson from 3.7.8 to 3.7.11 Bumps [orjson](https://github.com/ijl/orjson) from 3.7.8 to 3.7.11. - [Release notes](https://github.com/ijl/orjson/releases) - [Changelog](https://github.com/ijl/orjson/blob/master/CHANGELOG.md) - [Commits](https://github.com/ijl/orjson/compare/3.7.8...3.7.11) --- updated-dependencies: - dependency-name: orjson 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 b9e87749d..7aee44d85 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,7 +28,7 @@ py_find_1st==1.1.5 # Load ticker files 30% faster python-rapidjson==1.8 # Properly format api responses -orjson==3.7.8 +orjson==3.7.11 # Notify systemd sdnotify==0.3.2 From 79b650258ee719b5438b37036ef255379aa0fafe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Aug 2022 03:02:02 +0000 Subject: [PATCH 26/56] Bump urllib3 from 1.26.10 to 1.26.11 Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.10 to 1.26.11. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/1.26.11/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/1.26.10...1.26.11) --- 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 b9e87749d..36989119c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ python-telegram-bot==13.13 arrow==1.2.2 cachetools==4.2.2 requests==2.28.1 -urllib3==1.26.10 +urllib3==1.26.11 jsonschema==4.7.2 TA-Lib==0.4.24 technical==1.3.0 From 97064a9ce3bbc25350966a47d1e549f985b076ab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Aug 2022 03:13:38 +0000 Subject: [PATCH 27/56] Bump pypa/gh-action-pypi-publish from 1.5.0 to 1.5.1 Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.5.0 to 1.5.1. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.5.0...v1.5.1) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7b077be04..bb5bc209e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -351,7 +351,7 @@ jobs: python setup.py sdist bdist_wheel - name: Publish to PyPI (Test) - uses: pypa/gh-action-pypi-publish@v1.5.0 + uses: pypa/gh-action-pypi-publish@v1.5.1 if: (github.event_name == 'release') with: user: __token__ @@ -359,7 +359,7 @@ jobs: repository_url: https://test.pypi.org/legacy/ - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@v1.5.0 + uses: pypa/gh-action-pypi-publish@v1.5.1 if: (github.event_name == 'release') with: user: __token__ From f3154a4313cb03ea09f3517272b554ccb9b6664a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Aug 2022 04:35:30 +0000 Subject: [PATCH 28/56] Bump jsonschema from 4.7.2 to 4.9.0 Bumps [jsonschema](https://github.com/python-jsonschema/jsonschema) from 4.7.2 to 4.9.0. - [Release notes](https://github.com/python-jsonschema/jsonschema/releases) - [Changelog](https://github.com/python-jsonschema/jsonschema/blob/main/CHANGELOG.rst) - [Commits](https://github.com/python-jsonschema/jsonschema/compare/v4.7.2...v4.9.0) --- updated-dependencies: - dependency-name: jsonschema 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 69f6d1d24..f6ba2a444 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ arrow==1.2.2 cachetools==4.2.2 requests==2.28.1 urllib3==1.26.11 -jsonschema==4.7.2 +jsonschema==4.9.0 TA-Lib==0.4.24 technical==1.3.0 tabulate==0.8.10 From 707a4e7c9eee26ec29749465dda9aa12de69c9bd Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Aug 2022 06:41:08 +0200 Subject: [PATCH 29/56] types-requests bump pre-commit --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 759ac0a6a..398d09875 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: additional_dependencies: - types-cachetools==5.2.1 - types-filelock==3.2.7 - - types-requests==2.28.3 + - types-requests==2.28.6 - types-tabulate==0.8.11 - types-python-dateutil==2.8.19 # stages: [push] From d75e0a982091bbe99e5f7795ca83fb56c210617d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Aug 2022 06:43:59 +0200 Subject: [PATCH 30/56] Fix Flake8 errors after flake update --- freqtrade/optimize/backtesting.py | 2 +- freqtrade/optimize/optimize_reports.py | 2 +- freqtrade/persistence/trade_model.py | 8 ++++---- tests/edge/test_edge.py | 2 +- tests/exchange/test_exchange.py | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 46774e8a5..029946cfb 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -595,7 +595,7 @@ class Backtesting: if exit_.exit_type in (ExitType.EXIT_SIGNAL, ExitType.CUSTOM_EXIT): # Checks and adds an exit tag, after checking that the length of the # row has the length for an exit tag column - if( + if ( len(row) > EXIT_TAG_IDX and row[EXIT_TAG_IDX] is not None and len(row[EXIT_TAG_IDX]) > 0 diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 44ac4a5b3..519022db2 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -639,7 +639,7 @@ def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_curr :param stake_currency: stake-currency - used to correctly name headers :return: pretty printed table with tabulate as string """ - if(tag_type == "enter_tag"): + if (tag_type == "enter_tag"): headers = _get_line_header("TAG", stake_currency) else: headers = _get_line_header("TAG", stake_currency, 'Sells') diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index fcb84a59a..19d9361b6 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -1300,7 +1300,7 @@ class Trade(_DECL_BASE, LocalTrade): """ filters = [Trade.is_open.is_(False)] - if(pair is not None): + if (pair is not None): filters.append(Trade.pair == pair) enter_tag_perf = Trade.query.with_entities( @@ -1333,7 +1333,7 @@ class Trade(_DECL_BASE, LocalTrade): """ filters = [Trade.is_open.is_(False)] - if(pair is not None): + if (pair is not None): filters.append(Trade.pair == pair) sell_tag_perf = Trade.query.with_entities( @@ -1366,7 +1366,7 @@ class Trade(_DECL_BASE, LocalTrade): """ filters = [Trade.is_open.is_(False)] - if(pair is not None): + if (pair is not None): filters.append(Trade.pair == pair) mix_tag_perf = Trade.query.with_entities( @@ -1386,7 +1386,7 @@ class Trade(_DECL_BASE, LocalTrade): enter_tag = enter_tag if enter_tag is not None else "Other" exit_reason = exit_reason if exit_reason is not None else "Other" - if(exit_reason is not None and enter_tag is not None): + if (exit_reason is not None and enter_tag is not None): mix_tag = enter_tag + " " + exit_reason i = 0 if not any(item["mix_tag"] == mix_tag for item in return_list): diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index b30d6f998..1b0191fda 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -136,7 +136,7 @@ def test_adjust(mocker, edge_conf): )) pairs = ['A/B', 'C/D', 'E/F', 'G/H'] - assert(edge.adjust(pairs) == ['E/F', 'C/D']) + assert (edge.adjust(pairs) == ['E/F', 'C/D']) def test_stoploss(mocker, edge_conf): diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index d73e26683..bbe424430 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3810,8 +3810,8 @@ def test__get_funding_fees_from_exchange(default_conf, mocker, exchange_name): since=unix_time ) - assert(isclose(expected_fees, fees_from_datetime)) - assert(isclose(expected_fees, fees_from_unix_time)) + assert (isclose(expected_fees, fees_from_datetime)) + assert (isclose(expected_fees, fees_from_unix_time)) ccxt_exceptionhandlers( mocker, From ae0d6f63fa458cc2435dcfd8704a2258b7a82358 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Aug 2022 19:43:13 +0200 Subject: [PATCH 31/56] Version bump ccxt to 1.91.55 closes #7151 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f6ba2a444..4cb6519b3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.23.1 pandas==1.4.3 pandas-ta==0.3.14b -ccxt==1.91.52 +ccxt==1.91.55 # Pin cryptography for now due to rust build errors with piwheels cryptography==37.0.4 aiohttp==3.8.1 From 95327750dca68592d552e9cbe59347435d2f7fac Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Aug 2022 07:07:54 +0200 Subject: [PATCH 32/56] Final abs. profit should not be doubled in rpc messages --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 757449c8c..657e0bd82 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1566,7 +1566,7 @@ class FreqtradeBot(LoggingMixin): profit_ratio = trade.calc_profit_ratio(profit_rate, amount, trade.open_rate) else: profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested - profit = trade.calc_profit(rate=profit_rate) + trade.realized_profit + profit = trade.calc_profit(rate=profit_rate) + (0.0 if fill else trade.realized_profit) profit_ratio = trade.calc_profit_ratio(profit_rate) amount = trade.amount gain = "profit" if profit_ratio > 0 else "loss" From febd809119f1d3cfe30444fbe7978c0ea51e990e Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Thu, 4 Aug 2022 20:55:52 +0900 Subject: [PATCH 33/56] Fix typo adjust_trade_position should return stake_amount, not amount --- docs/strategy-callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 18de3513b..a9b032818 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -721,7 +721,7 @@ class DigDeeperStrategy(IStrategy): if current_profit > 0.05 and trade.nr_of_successful_exits == 0: # Take half of the profit at +5% - return -(trade.amount / 2) + return -(trade.stake_amount / 2) if current_profit > -0.05: return None From df4a5a75731742dddaa76a76db167f8ccb14eda0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Aug 2022 03:02:12 +0000 Subject: [PATCH 34/56] Bump python from 3.10.5-slim-bullseye to 3.10.6-slim-bullseye Bumps python from 3.10.5-slim-bullseye to 3.10.6-slim-bullseye. --- updated-dependencies: - dependency-name: python dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 5138ecec9..14a67edc8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.10.5-slim-bullseye as base +FROM python:3.10.6-slim-bullseye as base # Setup env ENV LANG C.UTF-8 From debc73b65465a5b9ee9407d854dc30cf9a666fa7 Mon Sep 17 00:00:00 2001 From: OGSK <53548542+Jetsukda@users.noreply.github.com> Date: Fri, 5 Aug 2022 00:28:28 +0700 Subject: [PATCH 35/56] Edit Typo Custom-stake-amount Edit Custom-stake-amount to `custom_stake_amount` --- docs/strategy_migration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy_migration.md b/docs/strategy_migration.md index 471ffa601..042c2494f 100644 --- a/docs/strategy_migration.md +++ b/docs/strategy_migration.md @@ -192,7 +192,7 @@ class AwesomeStrategy(IStrategy): return False ``` -### Custom-stake-amount +### `custom_stake_amount` New string argument `side` - which can be either `"long"` or `"short"`. From a8541d86fbf0406208e1122f0d346474b6327fb0 Mon Sep 17 00:00:00 2001 From: OGSK <53548542+Jetsukda@users.noreply.github.com> Date: Fri, 5 Aug 2022 09:36:26 +0700 Subject: [PATCH 36/56] Edit index of `custom_stake_amount` --- docs/strategy_migration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy_migration.md b/docs/strategy_migration.md index 042c2494f..064e7a59d 100644 --- a/docs/strategy_migration.md +++ b/docs/strategy_migration.md @@ -18,7 +18,7 @@ Note : `forcesell`, `forcebuy`, `emergencysell` are changed to `force_exit`, `fo * [`check_buy_timeout()` -> `check_entry_timeout()`](#custom_entry_timeout) * [`check_sell_timeout()` -> `check_exit_timeout()`](#custom_entry_timeout) * New `side` argument to callbacks without trade object - * [`custom_stake_amount`](#custom-stake-amount) + * [`custom_stake_amount`](#custom_stake_amount) * [`confirm_trade_entry`](#confirm_trade_entry) * [`custom_entry_price`](#custom_entry_price) * [Changed argument name in `confirm_trade_exit`](#confirm_trade_exit) From c6e121ffb4cde7fa15326d006fc2d0a062146f4d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Aug 2022 07:17:58 +0200 Subject: [PATCH 37/56] Update tests with correct usdt mock trades --- tests/conftest_trades_usdt.py | 4 ++-- tests/rpc/test_rpc.py | 32 +++++++++++++------------- tests/rpc/test_rpc_apiserver.py | 32 +++++++++++++------------- tests/rpc/test_rpc_telegram.py | 40 ++++++++++++++++----------------- 4 files changed, 54 insertions(+), 54 deletions(-) diff --git a/tests/conftest_trades_usdt.py b/tests/conftest_trades_usdt.py index 41d705c01..9a89eaca6 100644 --- a/tests/conftest_trades_usdt.py +++ b/tests/conftest_trades_usdt.py @@ -63,7 +63,7 @@ def mock_trade_usdt_1(fee, is_short: bool): open_rate=10.0, close_rate=8.0, close_profit=-0.2, - close_profit_abs=-4.0, + close_profit_abs=-4.09, exchange='binance', strategy='SampleStrategy', open_order_id=f'prod_exit_1_{direc(is_short)}', @@ -183,7 +183,7 @@ def mock_trade_usdt_3(fee, is_short: bool): open_rate=1.0, close_rate=1.1, close_profit=0.1, - close_profit_abs=9.8425, + close_profit_abs=2.8425, exchange='binance', is_open=False, strategy='StrategyTestV2', diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 02c62e337..4c580c3c2 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -314,10 +314,10 @@ def test__rpc_timeunit_profit(default_conf_usdt, ticker, fee, # {'date': datetime.date(2022, 6, 11), 'abs_profit': 13.8299999, # 'starting_balance': 1055.37, 'rel_profit': 0.0131044, # 'fiat_value': 0.0, 'trade_count': 2} - assert day['abs_profit'] in (0.0, pytest.approx(13.8299999), pytest.approx(-4.0)) - assert day['rel_profit'] in (0.0, pytest.approx(0.01310441), pytest.approx(-0.00377583)) + assert day['abs_profit'] in (0.0, pytest.approx(6.83), pytest.approx(-4.09)) + assert day['rel_profit'] in (0.0, pytest.approx(0.00642902), pytest.approx(-0.00383512)) assert day['trade_count'] in (0, 1, 2) - assert day['starting_balance'] in (pytest.approx(1059.37), pytest.approx(1055.37)) + assert day['starting_balance'] in (pytest.approx(1062.37), pytest.approx(1066.46)) assert day['fiat_value'] in (0.0, ) # ensure first day is current date assert str(days['data'][0]['date']) == str(datetime.utcnow().date()) @@ -435,9 +435,9 @@ def test_rpc_trade_statistics(default_conf_usdt, ticker, fee, mocker) -> None: create_mock_trades_usdt(fee) stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency) - assert pytest.approx(stats['profit_closed_coin']) == 9.83 + assert pytest.approx(stats['profit_closed_coin']) == 2.74 assert pytest.approx(stats['profit_closed_percent_mean']) == -1.67 - assert pytest.approx(stats['profit_closed_fiat']) == 10.813 + assert pytest.approx(stats['profit_closed_fiat']) == 3.014 assert pytest.approx(stats['profit_all_coin']) == -77.45964918 assert pytest.approx(stats['profit_all_percent_mean']) == -57.86 assert pytest.approx(stats['profit_all_fiat']) == -85.205614098 @@ -870,9 +870,9 @@ def test_performance_handle(default_conf_usdt, ticker, fee, mocker) -> None: res = rpc._rpc_performance() assert len(res) == 3 - assert res[0]['pair'] == 'XRP/USDT' + assert res[0]['pair'] == 'ETC/USDT' assert res[0]['count'] == 1 - assert res[0]['profit_pct'] == 10.0 + assert res[0]['profit_pct'] == 5.0 def test_enter_tag_performance_handle(default_conf, ticker, fee, mocker) -> None: @@ -896,16 +896,16 @@ def test_enter_tag_performance_handle(default_conf, ticker, fee, mocker) -> None res = rpc._rpc_enter_tag_performance(None) assert len(res) == 3 - assert res[0]['enter_tag'] == 'TEST3' + assert res[0]['enter_tag'] == 'TEST1' assert res[0]['count'] == 1 - assert res[0]['profit_pct'] == 10.0 + assert res[0]['profit_pct'] == 5.0 res = rpc._rpc_enter_tag_performance(None) assert len(res) == 3 - assert res[0]['enter_tag'] == 'TEST3' + assert res[0]['enter_tag'] == 'TEST1' assert res[0]['count'] == 1 - assert res[0]['profit_pct'] == 10.0 + assert res[0]['profit_pct'] == 5.0 def test_enter_tag_performance_handle_2(mocker, default_conf, markets, fee): @@ -956,11 +956,11 @@ def test_exit_reason_performance_handle(default_conf_usdt, ticker, fee, mocker) res = rpc._rpc_exit_reason_performance(None) assert len(res) == 3 - assert res[0]['exit_reason'] == 'roi' + assert res[0]['exit_reason'] == 'exit_signal' assert res[0]['count'] == 1 - assert res[0]['profit_pct'] == 10.0 + assert res[0]['profit_pct'] == 5.0 - assert res[1]['exit_reason'] == 'exit_signal' + assert res[1]['exit_reason'] == 'roi' assert res[2]['exit_reason'] == 'Other' @@ -1012,9 +1012,9 @@ def test_mix_tag_performance_handle(default_conf, ticker, fee, mocker) -> None: res = rpc._rpc_mix_tag_performance(None) assert len(res) == 3 - assert res[0]['mix_tag'] == 'TEST3 roi' + assert res[0]['mix_tag'] == 'TEST1 exit_signal' assert res[0]['count'] == 1 - assert res[0]['profit_pct'] == 10.0 + assert res[0]['profit_pct'] == 5.0 def test_mix_tag_performance_handle_2(mocker, default_conf, markets, fee): diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 57ba8e9f1..c6507d0ae 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -790,22 +790,22 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, is_short, expected) 'first_trade_timestamp': ANY, 'latest_trade_date': '5 minutes ago', 'latest_trade_timestamp': ANY, - 'profit_all_coin': expected['profit_all_coin'], - 'profit_all_fiat': expected['profit_all_fiat'], - 'profit_all_percent_mean': expected['profit_all_percent_mean'], - 'profit_all_ratio_mean': expected['profit_all_ratio_mean'], - 'profit_all_percent_sum': expected['profit_all_percent_sum'], - 'profit_all_ratio_sum': expected['profit_all_ratio_sum'], - 'profit_all_percent': expected['profit_all_percent'], - 'profit_all_ratio': expected['profit_all_ratio'], - 'profit_closed_coin': expected['profit_closed_coin'], - 'profit_closed_fiat': expected['profit_closed_fiat'], - 'profit_closed_ratio_mean': expected['profit_closed_ratio_mean'], - 'profit_closed_percent_mean': expected['profit_closed_percent_mean'], - 'profit_closed_ratio_sum': expected['profit_closed_ratio_sum'], - 'profit_closed_percent_sum': expected['profit_closed_percent_sum'], - 'profit_closed_ratio': expected['profit_closed_ratio'], - 'profit_closed_percent': expected['profit_closed_percent'], + 'profit_all_coin': pytest.approx(expected['profit_all_coin']), + 'profit_all_fiat': pytest.approx(expected['profit_all_fiat']), + 'profit_all_percent_mean': pytest.approx(expected['profit_all_percent_mean']), + 'profit_all_ratio_mean': pytest.approx(expected['profit_all_ratio_mean']), + 'profit_all_percent_sum': pytest.approx(expected['profit_all_percent_sum']), + 'profit_all_ratio_sum': pytest.approx(expected['profit_all_ratio_sum']), + 'profit_all_percent': pytest.approx(expected['profit_all_percent']), + 'profit_all_ratio': pytest.approx(expected['profit_all_ratio']), + 'profit_closed_coin': pytest.approx(expected['profit_closed_coin']), + 'profit_closed_fiat': pytest.approx(expected['profit_closed_fiat']), + 'profit_closed_ratio_mean': pytest.approx(expected['profit_closed_ratio_mean']), + 'profit_closed_percent_mean': pytest.approx(expected['profit_closed_percent_mean']), + 'profit_closed_ratio_sum': pytest.approx(expected['profit_closed_ratio_sum']), + 'profit_closed_percent_sum': pytest.approx(expected['profit_closed_percent_sum']), + 'profit_closed_ratio': pytest.approx(expected['profit_closed_ratio']), + 'profit_closed_percent': pytest.approx(expected['profit_closed_percent']), 'trade_count': 6, 'closed_trade_count': 2, 'winning_trades': expected['winning_trades'], diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 98c06c8e9..9508a6b42 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -433,10 +433,10 @@ def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machi assert "Daily Profit over the last 2 days:" in msg_mock.call_args_list[0][0][0] assert 'Day ' in msg_mock.call_args_list[0][0][0] assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] - assert ' 13.83 USDT' in msg_mock.call_args_list[0][0][0] - assert ' 15.21 USD' in msg_mock.call_args_list[0][0][0] + assert ' 6.83 USDT' in msg_mock.call_args_list[0][0][0] + assert ' 7.51 USD' in msg_mock.call_args_list[0][0][0] assert '(2)' in msg_mock.call_args_list[0][0][0] - assert '(2) 13.83 USDT 15.21 USD 1.31%' in msg_mock.call_args_list[0][0][0] + assert '(2) 6.83 USDT 7.51 USD 0.64%' in msg_mock.call_args_list[0][0][0] assert '(0)' in msg_mock.call_args_list[0][0][0] # Reset msg_mock @@ -447,8 +447,8 @@ def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machi assert "Daily Profit over the last 7 days:" in msg_mock.call_args_list[0][0][0] assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] assert str((datetime.utcnow() - timedelta(days=5)).date()) in msg_mock.call_args_list[0][0][0] - assert ' 13.83 USDT' in msg_mock.call_args_list[0][0][0] - assert ' 15.21 USD' in msg_mock.call_args_list[0][0][0] + assert ' 6.83 USDT' in msg_mock.call_args_list[0][0][0] + assert ' 7.51 USD' in msg_mock.call_args_list[0][0][0] assert '(2)' in msg_mock.call_args_list[0][0][0] assert '(1)' in msg_mock.call_args_list[0][0][0] assert '(0)' in msg_mock.call_args_list[0][0][0] @@ -460,8 +460,8 @@ def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machi context = MagicMock() context.args = ["1"] telegram._daily(update=update, context=context) - assert ' 13.83 USDT' in msg_mock.call_args_list[0][0][0] - assert ' 15.21 USD' in msg_mock.call_args_list[0][0][0] + assert ' 6.83 USDT' in msg_mock.call_args_list[0][0][0] + assert ' 7.51 USD' in msg_mock.call_args_list[0][0][0] assert '(2)' in msg_mock.call_args_list[0][0][0] @@ -523,8 +523,8 @@ def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mach today = datetime.utcnow().date() first_iso_day_of_current_week = today - timedelta(days=today.weekday()) assert str(first_iso_day_of_current_week) in msg_mock.call_args_list[0][0][0] - assert ' 9.83 USDT' in msg_mock.call_args_list[0][0][0] - assert ' 10.81 USD' in msg_mock.call_args_list[0][0][0] + assert ' 2.74 USDT' in msg_mock.call_args_list[0][0][0] + assert ' 3.01 USD' in msg_mock.call_args_list[0][0][0] assert '(3)' in msg_mock.call_args_list[0][0][0] assert '(0)' in msg_mock.call_args_list[0][0][0] @@ -536,8 +536,8 @@ def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mach assert "Weekly Profit over the last 8 weeks (starting from Monday):" \ in msg_mock.call_args_list[0][0][0] assert 'Weekly' in msg_mock.call_args_list[0][0][0] - assert ' 9.83 USDT' in msg_mock.call_args_list[0][0][0] - assert ' 10.81 USD' in msg_mock.call_args_list[0][0][0] + assert ' 2.74 USDT' in msg_mock.call_args_list[0][0][0] + assert ' 3.01 USD' in msg_mock.call_args_list[0][0][0] assert '(3)' in msg_mock.call_args_list[0][0][0] assert '(0)' in msg_mock.call_args_list[0][0][0] @@ -592,8 +592,8 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac today = datetime.utcnow().date() current_month = f"{today.year}-{today.month:02} " assert current_month in msg_mock.call_args_list[0][0][0] - assert ' 9.83 USDT' in msg_mock.call_args_list[0][0][0] - assert ' 10.81 USD' in msg_mock.call_args_list[0][0][0] + assert ' 2.74 USDT' in msg_mock.call_args_list[0][0][0] + assert ' 3.01 USD' in msg_mock.call_args_list[0][0][0] assert '(3)' in msg_mock.call_args_list[0][0][0] assert '(0)' in msg_mock.call_args_list[0][0][0] @@ -606,8 +606,8 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac assert 'Monthly Profit over the last 6 months:' in msg_mock.call_args_list[0][0][0] assert 'Month ' in msg_mock.call_args_list[0][0][0] assert current_month in msg_mock.call_args_list[0][0][0] - assert ' 9.83 USDT' in msg_mock.call_args_list[0][0][0] - assert ' 10.81 USD' in msg_mock.call_args_list[0][0][0] + assert ' 2.74 USDT' in msg_mock.call_args_list[0][0][0] + assert ' 3.01 USD' in msg_mock.call_args_list[0][0][0] assert '(3)' in msg_mock.call_args_list[0][0][0] assert '(0)' in msg_mock.call_args_list[0][0][0] @@ -620,8 +620,8 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac telegram._monthly(update=update, context=context) assert msg_mock.call_count == 1 assert 'Monthly Profit over the last 12 months:' in msg_mock.call_args_list[0][0][0] - assert ' 9.83 USDT' in msg_mock.call_args_list[0][0][0] - assert ' 10.81 USD' in msg_mock.call_args_list[0][0][0] + assert ' 2.74 USDT' in msg_mock.call_args_list[0][0][0] + assert ' 3.01 USD' in msg_mock.call_args_list[0][0][0] assert '(3)' in msg_mock.call_args_list[0][0][0] # The one-digit months should contain a zero, Eg: September 2021 = "2021-09" @@ -1268,7 +1268,7 @@ def test_telegram_performance_handle(default_conf_usdt, update, ticker, fee, moc telegram._performance(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'Performance' in msg_mock.call_args_list[0][0][0] - assert 'XRP/USDT\t9.842 USDT (10.00%) (1)' in msg_mock.call_args_list[0][0][0] + assert 'XRP/USDT\t2.842 USDT (10.00%) (1)' in msg_mock.call_args_list[0][0][0] def test_telegram_entry_tag_performance_handle( @@ -1318,7 +1318,7 @@ def test_telegram_exit_reason_performance_handle(default_conf_usdt, update, tick telegram._exit_reason_performance(update=update, context=context) assert msg_mock.call_count == 1 assert 'Exit Reason Performance' in msg_mock.call_args_list[0][0][0] - assert 'roi\t9.842 USDT (10.00%) (1)' in msg_mock.call_args_list[0][0][0] + assert 'roi\t2.842 USDT (10.00%) (1)' in msg_mock.call_args_list[0][0][0] context.args = ['XRP/USDT'] telegram._exit_reason_performance(update=update, context=context) @@ -1350,7 +1350,7 @@ def test_telegram_mix_tag_performance_handle(default_conf_usdt, update, ticker, telegram._mix_tag_performance(update=update, context=context) assert msg_mock.call_count == 1 assert 'Mix Tag Performance' in msg_mock.call_args_list[0][0][0] - assert ('TEST3 roi\t9.842 USDT (10.00%) (1)' + assert ('TEST3 roi\t2.842 USDT (10.00%) (1)' in msg_mock.call_args_list[0][0][0]) context.args = ['XRP/USDT'] From cffc7695491d39e653b229d7a1ec75f2cb0ca48b Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Aug 2022 07:26:41 +0200 Subject: [PATCH 38/56] Fix /profit endpoint calculations for partial sells * don't recalculate for closed trades * include realized_profit in the calculation part of #7178 --- freqtrade/rpc/rpc.py | 13 +++++++------ tests/rpc/test_rpc_apiserver.py | 24 ++++++++++++------------ 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 9d6696803..7e431ece9 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -431,14 +431,15 @@ class RPC: if not trade.is_open: profit_ratio = trade.close_profit - profit_closed_coin.append(trade.close_profit_abs) + profit_abs = trade.close_profit_abs + profit_closed_coin.append(profit_abs) profit_closed_ratio.append(profit_ratio) if trade.close_profit >= 0: winning_trades += 1 - winning_profit += trade.close_profit_abs + winning_profit += profit_abs else: losing_trades += 1 - losing_profit += trade.close_profit_abs + losing_profit += profit_abs else: # Get current rate try: @@ -447,10 +448,10 @@ class RPC: except (PricingError, ExchangeError): current_rate = NAN profit_ratio = trade.calc_profit_ratio(rate=current_rate) + profit_abs = trade.calc_profit( + rate=trade.close_rate or current_rate) + trade.realized_profit - profit_all_coin.append( - trade.calc_profit(rate=trade.close_rate or current_rate) - ) + profit_all_coin.append(profit_abs) profit_all_ratio.append(profit_ratio) best_pair = Trade.get_best_pair(start_date) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index c6507d0ae..b7161e680 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -717,11 +717,11 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): ( True, {'best_pair': 'ETC/BTC', 'best_rate': -0.5, 'best_pair_profit_ratio': -0.005, - 'profit_all_coin': 43.61269123, - 'profit_all_fiat': 538398.67323435, 'profit_all_percent_mean': 66.41, + 'profit_all_coin': 45.561959, + 'profit_all_fiat': 562462.39126200, 'profit_all_percent_mean': 66.41, 'profit_all_ratio_mean': 0.664109545, 'profit_all_percent_sum': 398.47, - 'profit_all_ratio_sum': 3.98465727, 'profit_all_percent': 4.36, - 'profit_all_ratio': 0.043612222872799825, 'profit_closed_coin': -0.00673913, + 'profit_all_ratio_sum': 3.98465727, 'profit_all_percent': 4.56, + 'profit_all_ratio': 0.04556147, 'profit_closed_coin': -0.00673913, 'profit_closed_fiat': -83.19455985, 'profit_closed_ratio_mean': -0.0075, 'profit_closed_percent_mean': -0.75, 'profit_closed_ratio_sum': -0.015, 'profit_closed_percent_sum': -1.5, 'profit_closed_ratio': -6.739057628404269e-06, @@ -732,11 +732,11 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): ( False, {'best_pair': 'XRP/BTC', 'best_rate': 1.0, 'best_pair_profit_ratio': 0.01, - 'profit_all_coin': -44.0631579, - 'profit_all_fiat': -543959.6842755, 'profit_all_percent_mean': -66.41, + 'profit_all_coin': -45.79641127, + 'profit_all_fiat': -565356.69712815, 'profit_all_percent_mean': -66.41, 'profit_all_ratio_mean': -0.6641100666666667, 'profit_all_percent_sum': -398.47, - 'profit_all_ratio_sum': -3.9846604, 'profit_all_percent': -4.41, - 'profit_all_ratio': -0.044063014216106644, 'profit_closed_coin': 0.00073913, + 'profit_all_ratio_sum': -3.9846604, 'profit_all_percent': -4.58, + 'profit_all_ratio': -0.045796261934205953, 'profit_closed_coin': 0.00073913, 'profit_closed_fiat': 9.124559849999999, 'profit_closed_ratio_mean': 0.0075, 'profit_closed_percent_mean': 0.75, 'profit_closed_ratio_sum': 0.015, 'profit_closed_percent_sum': 1.5, 'profit_closed_ratio': 7.391275897987988e-07, @@ -747,11 +747,11 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): ( None, {'best_pair': 'XRP/BTC', 'best_rate': 1.0, 'best_pair_profit_ratio': 0.01, - 'profit_all_coin': -14.43790415, - 'profit_all_fiat': -178235.92673175, 'profit_all_percent_mean': 0.08, + 'profit_all_coin': -14.94732578, + 'profit_all_fiat': -184524.7367541, 'profit_all_percent_mean': 0.08, 'profit_all_ratio_mean': 0.000835751666666662, 'profit_all_percent_sum': 0.5, - 'profit_all_ratio_sum': 0.005014509999999972, 'profit_all_percent': -1.44, - 'profit_all_ratio': -0.014437768014451796, 'profit_closed_coin': -0.00542913, + 'profit_all_ratio_sum': 0.005014509999999972, 'profit_all_percent': -1.49, + 'profit_all_ratio': -0.014947184841095841, 'profit_closed_coin': -0.00542913, 'profit_closed_fiat': -67.02260985, 'profit_closed_ratio_mean': 0.0025, 'profit_closed_percent_mean': 0.25, 'profit_closed_ratio_sum': 0.005, 'profit_closed_percent_sum': 0.5, 'profit_closed_ratio': -5.429078808526421e-06, From 7675187c377865abe6d4fc3f58543e294ae7cb53 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Aug 2022 07:29:49 +0200 Subject: [PATCH 39/56] Use telegram message length to avoid constants --- freqtrade/rpc/telegram.py | 18 ++++++++---------- tests/rpc/test_rpc_telegram.py | 2 +- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 66192fb16..84b051255 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -16,8 +16,8 @@ from typing import Any, Callable, Dict, List, Optional, Union import arrow from tabulate import tabulate -from telegram import (CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, - ParseMode, ReplyKeyboardMarkup, Update) +from telegram import (MAX_MESSAGE_LENGTH, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, + KeyboardButton, ParseMode, ReplyKeyboardMarkup, Update) from telegram.error import BadRequest, NetworkError, TelegramError from telegram.ext import CallbackContext, CallbackQueryHandler, CommandHandler, Updater from telegram.utils.helpers import escape_markdown @@ -35,8 +35,6 @@ logger = logging.getLogger(__name__) logger.debug('Included module rpc.telegram ...') -MAX_TELEGRAM_MESSAGE_LENGTH = 4096 - @dataclass class TimeunitMappings: @@ -908,7 +906,7 @@ class Telegram(RPCHandler): total_dust_currencies += 1 # Handle overflowing message length - if len(output + curr_output) >= MAX_TELEGRAM_MESSAGE_LENGTH: + if len(output + curr_output) >= MAX_MESSAGE_LENGTH: self._send_msg(output) output = curr_output else: @@ -1171,7 +1169,7 @@ class Telegram(RPCHandler): f"({trade['profit_ratio']:.2%}) " f"({trade['count']})\n") - if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH: + if len(output + stat_line) >= MAX_MESSAGE_LENGTH: self._send_msg(output, parse_mode=ParseMode.HTML) output = stat_line else: @@ -1206,7 +1204,7 @@ class Telegram(RPCHandler): f"({trade['profit_ratio']:.2%}) " f"({trade['count']})\n") - if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH: + if len(output + stat_line) >= MAX_MESSAGE_LENGTH: self._send_msg(output, parse_mode=ParseMode.HTML) output = stat_line else: @@ -1241,7 +1239,7 @@ class Telegram(RPCHandler): f"({trade['profit_ratio']:.2%}) " f"({trade['count']})\n") - if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH: + if len(output + stat_line) >= MAX_MESSAGE_LENGTH: self._send_msg(output, parse_mode=ParseMode.HTML) output = stat_line else: @@ -1276,7 +1274,7 @@ class Telegram(RPCHandler): f"({trade['profit']:.2%}) " f"({trade['count']})\n") - if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH: + if len(output + stat_line) >= MAX_MESSAGE_LENGTH: self._send_msg(output, parse_mode=ParseMode.HTML) output = stat_line else: @@ -1415,7 +1413,7 @@ class Telegram(RPCHandler): escape_markdown(logrec[2], version=2), escape_markdown(logrec[3], version=2), escape_markdown(logrec[4], version=2)) - if len(msgs + msg) + 10 >= MAX_TELEGRAM_MESSAGE_LENGTH: + if len(msgs + msg) + 10 >= MAX_MESSAGE_LENGTH: # Send message immediately if it would become too long self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2) msgs = msg + '\n' diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 9508a6b42..164ed0bc6 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1516,7 +1516,7 @@ def test_telegram_logs(default_conf, update, mocker) -> None: msg_mock.reset_mock() # Test with changed MaxMessageLength - mocker.patch('freqtrade.rpc.telegram.MAX_TELEGRAM_MESSAGE_LENGTH', 200) + mocker.patch('freqtrade.rpc.telegram.MAX_MESSAGE_LENGTH', 200) context = MagicMock() context.args = [] telegram._logs(update=update, context=context) From 29e41cc8179f3c8514452e583c5037245131290b Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Aug 2022 11:15:44 +0200 Subject: [PATCH 40/56] Update docs to reflect correct result closes #7181 --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 412571674..d5c0b3d8b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -105,7 +105,7 @@ This is similar to using multiple `--config` parameters, but simpler in usage as ``` json title="Result" { - "max_open_trades": 10, + "max_open_trades": 3, "stake_currency": "USDT", "stake_amount": "unlimited" } From 954540245209b1a79f707ba51e9e1b7c0f65c18e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Aug 2022 11:49:51 +0200 Subject: [PATCH 41/56] Improve defaults for config builder --- freqtrade/commands/build_config_commands.py | 6 +++--- freqtrade/templates/base_config.json.j2 | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/commands/build_config_commands.py b/freqtrade/commands/build_config_commands.py index be881c8ed..01cfa800a 100644 --- a/freqtrade/commands/build_config_commands.py +++ b/freqtrade/commands/build_config_commands.py @@ -67,7 +67,7 @@ def ask_user_config() -> Dict[str, Any]: "type": "text", "name": "stake_amount", "message": f"Please insert your stake amount (Number or '{UNLIMITED_STAKE_AMOUNT}'):", - "default": "100", + "default": "unlimited", "validate": lambda val: val == UNLIMITED_STAKE_AMOUNT or validate_is_float(val), "filter": lambda val: '"' + UNLIMITED_STAKE_AMOUNT + '"' if val == UNLIMITED_STAKE_AMOUNT @@ -164,7 +164,7 @@ def ask_user_config() -> Dict[str, Any]: "when": lambda x: x['telegram'] }, { - "type": "text", + "type": "password", "name": "telegram_chat_id", "message": "Insert Telegram chat id", "when": lambda x: x['telegram'] @@ -191,7 +191,7 @@ def ask_user_config() -> Dict[str, Any]: "when": lambda x: x['api_server'] }, { - "type": "text", + "type": "password", "name": "api_server_password", "message": "Insert api-server password", "when": lambda x: x['api_server'] diff --git a/freqtrade/templates/base_config.json.j2 b/freqtrade/templates/base_config.json.j2 index 914aa964b..681af84c6 100644 --- a/freqtrade/templates/base_config.json.j2 +++ b/freqtrade/templates/base_config.json.j2 @@ -12,6 +12,7 @@ "tradable_balance_ratio": 0.99, "fiat_display_currency": "{{ fiat_display_currency }}",{{ ('\n "timeframe": "' + timeframe + '",') if timeframe else '' }} "dry_run": {{ dry_run | lower }}, + "dry_run_wallet": 1000, "cancel_open_orders_on_exit": false, "trading_mode": "{{ trading_mode }}", "margin_mode": "{{ margin_mode }}", From b12dd15f4fcf41f912d0448a56725ab32b6fe98e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Aug 2022 09:10:12 +0200 Subject: [PATCH 42/56] Send multiple messages in /status if required --- freqtrade/rpc/telegram.py | 21 +++++++++++++-------- tests/rpc/test_rpc_telegram.py | 4 ++-- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 84b051255..c879676dc 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -457,11 +457,12 @@ class Telegram(RPCHandler): """ Prepare details of trade with entry adjustment enabled """ - lines: List[str] = [] + lines_detail: List[str] = [] if len(filled_orders) > 0: first_avg = filled_orders[0]["safe_price"] for x, order in enumerate(filled_orders): + lines: List[str] = [] if order['is_open'] is True: continue wording = 'Entry' if order['ft_is_entry'] else 'Exit' @@ -507,7 +508,8 @@ class Telegram(RPCHandler): # minutes, seconds = divmod(remainder, 60) # lines.append( # f"({days}d {hours}h {minutes}m {seconds}s from previous {wording.lower()})") - return lines + lines_detail.append("\n".join(lines)) + return lines_detail @authorized_only def _status(self, update: Update, context: CallbackContext) -> None: @@ -541,7 +543,6 @@ class Telegram(RPCHandler): results = self._rpc._rpc_trade_status(trade_ids=trade_ids) position_adjust = self._config.get('position_adjustment_enable', False) max_entries = self._config.get('max_entry_position_adjustment', -1) - messages = [] for r in results: r['open_date_hum'] = arrow.get(r['open_date']).humanize() r['num_entries'] = len([o for o in r['orders'] if o['ft_is_entry']]) @@ -594,12 +595,16 @@ class Telegram(RPCHandler): lines_detail = self._prepare_order_details( r['orders'], r['quote_currency'], r['is_open']) lines.extend(lines_detail if lines_detail else "") + msg = '' + for line in lines: + if line: + if (len(msg) + len(line) + 1) < MAX_MESSAGE_LENGTH: + msg += line + '\n' + else: + self._send_msg(msg.format(**r)) + msg = "*Trade ID:* `{trade_id}` - continued\n" + line + '\n' - # Filter empty lines using list-comprehension - messages.append("\n".join([line for line in lines if line]).format(**r)) - - for msg in messages: - self._send_msg(msg) + self._send_msg(msg.format(**r)) except RPCException as e: self._send_msg(str(e)) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 164ed0bc6..5b96a8068 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -342,7 +342,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: # close_rate should not be included in the message as the trade is not closed # and no line should be empty lines = msg_mock.call_args_list[0][0][0].split('\n') - assert '' not in lines + assert '' not in lines[:-1] assert 'Close Rate' not in ''.join(lines) assert 'Close Profit' not in ''.join(lines) @@ -357,7 +357,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: telegram._status(update=update, context=context) lines = msg_mock.call_args_list[0][0][0].split('\n') - assert '' not in lines + assert '' not in lines[:-1] assert 'Close Rate' not in ''.join(lines) assert 'Close Profit' not in ''.join(lines) From 2687633941eb1c5cee8c0e2ea2a20da86bc20c84 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Aug 2022 09:15:25 +0200 Subject: [PATCH 43/56] Test iterative sending of /status --- freqtrade/rpc/telegram.py | 2 +- tests/rpc/test_rpc_telegram.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index c879676dc..65e16cd35 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -549,7 +549,7 @@ class Telegram(RPCHandler): r['exit_reason'] = r.get('exit_reason', "") lines = [ "*Trade ID:* `{trade_id}`" + - ("` (since {open_date_hum})`" if r['is_open'] else ""), + (" `(since {open_date_hum})`" if r['is_open'] else ""), "*Current Pair:* {pair}", "*Direction:* " + ("`Short`" if r.get('is_short') else "`Long`"), "*Leverage:* `{leverage}`" if r.get('leverage') else "", diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 5b96a8068..2c9528b5e 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -364,6 +364,22 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: assert msg_mock.call_count == 2 assert 'LTC/BTC' in msg_mock.call_args_list[0][0][0] + mocker.patch('freqtrade.rpc.telegram.MAX_MESSAGE_LENGTH', 500) + + msg_mock.reset_mock() + context = MagicMock() + context.args = ["2"] + telegram._status(update=update, context=context) + + assert msg_mock.call_count == 2 + + msg1 = msg_mock.call_args_list[0][0][0] + msg2 = msg_mock.call_args_list[1][0][0] + + assert 'Close Rate' not in msg1 + assert 'Trade ID:* `2`' in msg1 + assert 'Trade ID:* `2` - continued' in msg2 + def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: mocker.patch.multiple( From f8f1ade16387b327a7cfa6db33b3dec71557f558 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Aug 2022 09:21:11 +0200 Subject: [PATCH 44/56] Reduce function complexity by extracting message sending --- freqtrade/rpc/telegram.py | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 65e16cd35..02fb5b4ee 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -587,28 +587,34 @@ class Telegram(RPCHandler): lines.append("*Stoploss distance:* `{stoploss_current_dist:.8f}` " "`({stoploss_current_dist_ratio:.2%})`") if r['open_order']: - if r['exit_order_status']: - lines.append("*Open Order:* `{open_order}` - `{exit_order_status}`") - else: - lines.append("*Open Order:* `{open_order}`") + lines.append( + "*Open Order:* `{open_order}`" + + "- `{exit_order_status}`" if r['exit_order_status'] else "") lines_detail = self._prepare_order_details( r['orders'], r['quote_currency'], r['is_open']) lines.extend(lines_detail if lines_detail else "") - msg = '' - for line in lines: - if line: - if (len(msg) + len(line) + 1) < MAX_MESSAGE_LENGTH: - msg += line + '\n' - else: - self._send_msg(msg.format(**r)) - msg = "*Trade ID:* `{trade_id}` - continued\n" + line + '\n' - - self._send_msg(msg.format(**r)) + self.__send_status_msg(lines, r) except RPCException as e: self._send_msg(str(e)) + def __send_status_msg(self, lines: List[str], r: Dict[str, Any]) -> None: + """ + Send status message. + """ + msg = '' + + for line in lines: + if line: + if (len(msg) + len(line) + 1) < MAX_MESSAGE_LENGTH: + msg += line + '\n' + else: + self._send_msg(msg.format(**r)) + msg = "*Trade ID:* `{trade_id}` - continued\n" + line + '\n' + + self._send_msg(msg.format(**r)) + @authorized_only def _status_table(self, update: Update, context: CallbackContext) -> None: """ From 45d68222a177e62b81fd5d760e17cbf9614aa36a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Aug 2022 13:18:40 +0200 Subject: [PATCH 45/56] Reduce verbosity of Fiat Converter --- freqtrade/rpc/fiat_convert.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/fiat_convert.py b/freqtrade/rpc/fiat_convert.py index df33693ac..cbe4c0045 100644 --- a/freqtrade/rpc/fiat_convert.py +++ b/freqtrade/rpc/fiat_convert.py @@ -12,6 +12,7 @@ from pycoingecko import CoinGeckoAPI from requests.exceptions import RequestException from freqtrade.constants import SUPPORTED_FIAT +from freqtrade.mixins.logging_mixin import LoggingMixin logger = logging.getLogger(__name__) @@ -27,7 +28,7 @@ coingecko_mapping = { } -class CryptoToFiatConverter: +class CryptoToFiatConverter(LoggingMixin): """ Main class to initiate Crypto to FIAT. This object contains a list of pair Crypto, FIAT @@ -54,6 +55,7 @@ class CryptoToFiatConverter: # Timeout: 6h self._pair_price: TTLCache = TTLCache(maxsize=500, ttl=6 * 60 * 60) + LoggingMixin.__init__(self, logger, 3600) self._load_cryptomap() def _load_cryptomap(self) -> None: @@ -177,7 +179,9 @@ class CryptoToFiatConverter: if not _gekko_id: # return 0 for unsupported stake currencies (fiat-convert should not break the bot) - logger.warning("unsupported crypto-symbol %s - returning 0.0", crypto_symbol) + self.log_once( + f"unsupported crypto-symbol {crypto_symbol.upper()} - returning 0.0", + logger.warning) return 0.0 try: From 5250189f77bcc4c6c017fdcc3a60ae2a6104f714 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Aug 2022 15:08:13 +0200 Subject: [PATCH 46/56] Add Rollback function to Trade simplifies Session work --- freqtrade/persistence/trade_model.py | 6 +++++- freqtrade/rpc/api_server/deps.py | 4 ++-- freqtrade/rpc/rpc.py | 2 +- freqtrade/rpc/telegram.py | 2 +- tests/test_persistence.py | 1 + 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 19d9361b6..3ea6ddf2d 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -196,7 +196,7 @@ class Order(_DECL_BASE): if filtered_orders: oobj = filtered_orders[0] oobj.update_from_ccxt_object(order) - Order.query.session.commit() + Trade.commit() else: logger.warning(f"Did not find order for {order}.") @@ -1148,6 +1148,10 @@ class Trade(_DECL_BASE, LocalTrade): def commit(): Trade.query.session.commit() + @staticmethod + def rollback(): + Trade.query.session.rollback() + @staticmethod def get_trades_proxy(*, pair: str = None, is_open: bool = None, open_date: datetime = None, close_date: datetime = None, diff --git a/freqtrade/rpc/api_server/deps.py b/freqtrade/rpc/api_server/deps.py index 81c013efa..66654c0b1 100644 --- a/freqtrade/rpc/api_server/deps.py +++ b/freqtrade/rpc/api_server/deps.py @@ -18,9 +18,9 @@ def get_rpc_optional() -> Optional[RPC]: def get_rpc() -> Optional[Iterator[RPC]]: _rpc = get_rpc_optional() if _rpc: - Trade.query.session.rollback() + Trade.rollback() yield _rpc - Trade.query.session.rollback() + Trade.rollback() else: raise RPCException('Bot is not in the correct state') diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 7e431ece9..d848da546 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -876,7 +876,7 @@ class RPC: lock.active = False lock.lock_end_time = datetime.now(timezone.utc) - PairLock.query.session.commit() + Trade.commit() return self._rpc_locks() diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 02fb5b4ee..9e0cd7d86 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -70,7 +70,7 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]: ) return wrapper # Rollback session to avoid getting data stored in a transaction. - Trade.query.session.rollback() + Trade.rollback() logger.debug( 'Executing handler: %s for chat_id: %s', command_handler.__name__, diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 42fcc7413..eea6d6fa1 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -2375,6 +2375,7 @@ def test_Trade_object_idem(): 'delete', 'session', 'commit', + 'rollback', 'query', 'open_date', 'get_best_pair', From bfa859e6188b60fb95cf14429b2bd5189f18902d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Aug 2022 17:45:18 +0200 Subject: [PATCH 47/56] Remove unnecessary method (simplify) --- freqtrade/freqtradebot.py | 6 +++--- freqtrade/persistence/__init__.py | 2 +- freqtrade/persistence/models.py | 8 -------- tests/test_freqtradebot.py | 2 +- 4 files changed, 5 insertions(+), 13 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 657e0bd82..390c8e8f6 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -26,7 +26,7 @@ from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange.exchange import timeframe_to_next_date from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.mixins import LoggingMixin -from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db +from freqtrade.persistence import Order, PairLocks, Trade, init_db from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver @@ -150,7 +150,7 @@ class FreqtradeBot(LoggingMixin): self.check_for_open_trades() self.rpc.cleanup() - cleanup_db() + Trade.commit() self.exchange.close() def startup(self) -> None: @@ -1701,7 +1701,6 @@ class FreqtradeBot(LoggingMixin): self.handle_order_fee(trade, order_obj, order) trade.update_trade(order_obj) - Trade.commit() if order.get('status') in constants.NON_OPEN_EXCHANGE_STATES: # If a entry order was closed, force update on stoploss on exchange @@ -1725,6 +1724,7 @@ class FreqtradeBot(LoggingMixin): # Updating wallets when order is closed self.wallets.update() + Trade.commit() self.order_close_notify(trade, order_obj, stoploss_order, send_msg) diff --git a/freqtrade/persistence/__init__.py b/freqtrade/persistence/__init__.py index f4e7470a7..9e1a7e922 100644 --- a/freqtrade/persistence/__init__.py +++ b/freqtrade/persistence/__init__.py @@ -1,5 +1,5 @@ # flake8: noqa: F401 -from freqtrade.persistence.models import cleanup_db, init_db +from freqtrade.persistence.models import init_db from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.persistence.trade_model import LocalTrade, Order, Trade diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 86d2f9f9c..f0fa05343 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -61,11 +61,3 @@ def init_db(db_url: str) -> None: previous_tables = inspect(engine).get_table_names() _DECL_BASE.metadata.create_all(engine) check_migrate(engine, decl_base=_DECL_BASE, previous_tables=previous_tables) - - -def cleanup_db() -> None: - """ - Flushes all pending operations to disk. - :return: None - """ - Trade.commit() diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 0b073a062..f274e2119 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -75,7 +75,7 @@ def test_process_calls_sendmsg(mocker, default_conf_usdt) -> None: def test_bot_cleanup(mocker, default_conf_usdt, caplog) -> None: - mock_cleanup = mocker.patch('freqtrade.freqtradebot.cleanup_db') + mock_cleanup = mocker.patch('freqtrade.freqtradebot.Trade.commit') coo_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cancel_all_open_orders') freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) freqtrade.cleanup() From 78e129034ee64cc60e2975a70338767d7206dc73 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Aug 2022 17:59:08 +0200 Subject: [PATCH 48/56] Update docs to specify trading limit behaviour closes #7183 --- docs/backtesting.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 50fc96923..a7baf6932 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -514,6 +514,7 @@ You can then load the trades to perform further analysis as shown in the [data a Since backtesting lacks some detailed information about what happens within a candle, it needs to take a few assumptions: +- Exchange [trading limits](#trading-limits-in-backtesting) are respected - Buys happen at open-price - All orders are filled at the requested price (no slippage, no unfilled orders) - Exit-signal exits happen at open-price of the consecutive candle @@ -543,7 +544,24 @@ Also, keep in mind that past results don't guarantee future success. In addition to the above assumptions, strategy authors should carefully read the [Common Mistakes](strategy-customization.md#common-mistakes-when-developing-strategies) section, to avoid using data in backtesting which is not available in real market conditions. -### Improved backtest accuracy +### Trading limits in backtesting + +Exchanges have certain trading limits, like minimum base currency, or minimum stake (quote) currency. +These limits are usually listed in the exchange documentation as "trading rules" or similar. + +Backtesting (as well as live and dry-run) does honor these limits, and will ensure that a stoploss can be placed below this value - so the value will be slightly higher than what the exchange specifies. +Freqtrade has however no information about historic limits. + +This can lead to situations where trading-limits are inflated by using a historic price, resulting in minimum amounts > 50$. + +For example: + +BTC minimum tradable amount is 0.001. +BTC trades at 22.000\$ today (0.001 BTC is related to this) - but the backtesting period includes prices as high as 50.000\$. +Today's minimum would be `0.001 * 22_000` - or 22\$. +However the limit could also be 50$ - based on `0.001 * 50_000` in some historic setting. + +## Improved backtest accuracy One big limitation of backtesting is it's inability to know how prices moved intra-candle (was high before close, or viceversa?). So assuming you run backtesting with a 1h timeframe, there will be 4 prices for that candle (Open, High, Low, Close). From 5182f755f1aa67bae976c5314e0a3fb2ab970662 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Aug 2022 10:08:35 +0200 Subject: [PATCH 49/56] Add debug setup documentation closes #7167 --- docs/developer.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/developer.md b/docs/developer.md index 0209d220a..aca4ce4ed 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -68,6 +68,36 @@ def test_method_to_test(caplog): ``` +### Debug configuration + +To debug freqtrade, we recommend VSCode with the following launch configuration (located in `.vscode/launch.json`). +Details will obviously vary between setups - but this should work to get you started. + +``` json +{ + "name": "freqtrade trade", + "type": "python", + "request": "launch", + "module": "freqtrade", + "console": "integratedTerminal", + "args": [ + "trade", + // Optional: + // "--userdir", "user_data", + "--strategy", + "MyAwesomeStrategy", + ] +}, +``` + +Command line arguments can be added in the `"args"` array. +This method can also be used to debug a strategy, by setting the breakpoints within the strategy. + +A similar setup can also be taken for Pycharm - using `freqtrade` as module name, and setting the command line arguments as "parameters". + +!!! Note "Startup directory" + This assumes that you have the repository checked out, and the editor is started at the repository root level (so setup.py is at the top level of your repository). + ## ErrorHandling Freqtrade Exceptions all inherit from `FreqtradeException`. From e48e82232d67695d412ba89df7dcea725a991f3d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Aug 2022 10:42:56 +0200 Subject: [PATCH 50/56] Force response API to js to fix faulty system configs closes #7147 --- freqtrade/rpc/api_server/web_ui.py | 7 ++++++- tests/rpc/test_rpc_apiserver.py | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server/web_ui.py b/freqtrade/rpc/api_server/web_ui.py index b04269c61..e1a277b30 100644 --- a/freqtrade/rpc/api_server/web_ui.py +++ b/freqtrade/rpc/api_server/web_ui.py @@ -1,4 +1,5 @@ from pathlib import Path +from typing import Optional from fastapi import APIRouter from fastapi.exceptions import HTTPException @@ -50,8 +51,12 @@ async def index_html(rest_of_path: str): filename = uibase / rest_of_path # It's security relevant to check "relative_to". # Without this, Directory-traversal is possible. + media_type: Optional[str] = None + if filename.suffix == '.js': + # Force text/javascript for .js files - Circumvent faulty system configuration + media_type = 'application/javascript' if filename.is_file() and is_relative_to(filename, uibase): - return FileResponse(str(filename)) + return FileResponse(str(filename), media_type=media_type) index_file = uibase / 'index.html' if not index_file.is_file(): diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index b7161e680..6bbf3cff6 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -109,6 +109,9 @@ def test_api_ui_fallback(botclient, mocker): rc = client_get(client, "/something") assert rc.status_code == 200 + rc = client_get(client, "/something.js") + assert rc.status_code == 200 + # Test directory traversal without mock rc = client_get(client, '%2F%2F%2Fetc/passwd') assert rc.status_code == 200 From 11a2eb6cc5598f837478864fba9139b78a1603db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Aug 2022 03:01:16 +0000 Subject: [PATCH 51/56] Bump flake8 from 5.0.1 to 5.0.4 Bumps [flake8](https://github.com/pycqa/flake8) from 5.0.1 to 5.0.4. - [Release notes](https://github.com/pycqa/flake8/releases) - [Commits](https://github.com/pycqa/flake8/compare/5.0.1...5.0.4) --- updated-dependencies: - dependency-name: flake8 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 79021e591..16fc1b046 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,7 +5,7 @@ -r docs/requirements-docs.txt coveralls==3.3.1 -flake8==5.0.1 +flake8==5.0.4 flake8-tidy-imports==4.8.0 mypy==0.971 pre-commit==2.20.0 From 7fd3f98ae8e730d964b10260e604d6b45592176a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Aug 2022 03:01:20 +0000 Subject: [PATCH 52/56] Bump scikit-learn from 1.1.1 to 1.1.2 Bumps [scikit-learn](https://github.com/scikit-learn/scikit-learn) from 1.1.1 to 1.1.2. - [Release notes](https://github.com/scikit-learn/scikit-learn/releases) - [Commits](https://github.com/scikit-learn/scikit-learn/compare/1.1.1...1.1.2) --- updated-dependencies: - dependency-name: scikit-learn 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 cc659fc50..11b9511f2 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -3,7 +3,7 @@ # Required for hyperopt scipy==1.9.0 -scikit-learn==1.1.1 +scikit-learn==1.1.2 scikit-optimize==0.9.0 filelock==3.7.1 progressbar2==4.0.0 From a45a35f38c616acf2565816163d2f2bf50e15d84 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Aug 2022 03:01:25 +0000 Subject: [PATCH 53/56] Bump jsonschema from 4.9.0 to 4.9.1 Bumps [jsonschema](https://github.com/python-jsonschema/jsonschema) from 4.9.0 to 4.9.1. - [Release notes](https://github.com/python-jsonschema/jsonschema/releases) - [Changelog](https://github.com/python-jsonschema/jsonschema/blob/main/CHANGELOG.rst) - [Commits](https://github.com/python-jsonschema/jsonschema/compare/v4.9.0...v4.9.1) --- updated-dependencies: - dependency-name: jsonschema 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 4cb6519b3..202ded414 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ arrow==1.2.2 cachetools==4.2.2 requests==2.28.1 urllib3==1.26.11 -jsonschema==4.9.0 +jsonschema==4.9.1 TA-Lib==0.4.24 technical==1.3.0 tabulate==0.8.10 From 97c077171ac63b16839baf0651d8e526e02a8d88 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Aug 2022 03:01:28 +0000 Subject: [PATCH 54/56] Bump types-requests from 2.28.6 to 2.28.8 Bumps [types-requests](https://github.com/python/typeshed) from 2.28.6 to 2.28.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 79021e591..b0533d415 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -24,6 +24,6 @@ nbconvert==6.5.0 # mypy types types-cachetools==5.2.1 types-filelock==3.2.7 -types-requests==2.28.6 +types-requests==2.28.8 types-tabulate==0.8.11 types-python-dateutil==2.8.19 From 71c88244fedae929b9189d2560d6a8c9c9528cec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Aug 2022 03:01:38 +0000 Subject: [PATCH 55/56] Bump ccxt from 1.91.55 to 1.91.93 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.91.55 to 1.91.93. - [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.91.55...1.91.93) --- 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 4cb6519b3..93c41c4c9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.23.1 pandas==1.4.3 pandas-ta==0.3.14b -ccxt==1.91.55 +ccxt==1.91.93 # Pin cryptography for now due to rust build errors with piwheels cryptography==37.0.4 aiohttp==3.8.1 From 0c7d862aae1d1b44d57af0177578edd586bc49bb Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Aug 2022 06:54:00 +0200 Subject: [PATCH 56/56] types-requests bump pre-commit --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 398d09875..a205f24ec 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: additional_dependencies: - types-cachetools==5.2.1 - types-filelock==3.2.7 - - types-requests==2.28.6 + - types-requests==2.28.8 - types-tabulate==0.8.11 - types-python-dateutil==2.8.19 # stages: [push]