From 5938514e5d08edfe278760578ac75a4b57b2b4fc Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Sep 2021 07:03:26 +0200 Subject: [PATCH 01/47] Version bump to 2021.9 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 2747efc96..0b6152bbf 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = 'develop' +__version__ = '2021.9' if __version__ == 'develop': From 98ed7edb1131a6111a3d72145d0fc26c0d72b634 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 28 Oct 2021 06:21:40 +0200 Subject: [PATCH 02/47] Version bump to 2021.10 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 0b6152bbf..df3c5d4f6 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2021.9' +__version__ = '2021.10' if __version__ == 'develop': From 3f10430eb5070e7de6a7b64f4e209d3237e52315 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Sep 2021 07:03:26 +0200 Subject: [PATCH 03/47] Version bump to 2021.9 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 2747efc96..0b6152bbf 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = 'develop' +__version__ = '2021.9' if __version__ == 'develop': From a9cdb428d0bf328351c0d602a5e64a93b9057ee0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 28 Oct 2021 06:21:40 +0200 Subject: [PATCH 04/47] Version bump to 2021.10 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 0b6152bbf..df3c5d4f6 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2021.9' +__version__ = '2021.10' if __version__ == 'develop': From 7e1eedd7dfa7deb01ffef1518591c6c7b8df2f6d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Nov 2021 09:55:00 +0100 Subject: [PATCH 05/47] Version bump to 2021.11 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index df3c5d4f6..ee09a45f5 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2021.10' +__version__ = '2021.11' if __version__ == 'develop': From 043218cc7ee5eb08ce2e7d1c73fd18ec2099ce04 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 29 Dec 2021 16:18:14 +0100 Subject: [PATCH 06/47] Version bump to 2021.12 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index ee09a45f5..a18beb0a0 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2021.11' +__version__ = '2021.12' if __version__ == 'develop': From 2d45163f8f89e5b63298d3881bc80cc24cc89b70 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Feb 2022 19:46:48 +0100 Subject: [PATCH 07/47] Bump version to 2022.1 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index a18beb0a0..54cecbec2 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2021.12' +__version__ = '2022.1' if __version__ == 'develop': From cd54f1536e16846985a4a639a74978e0dac71144 Mon Sep 17 00:00:00 2001 From: Robert Davey Date: Mon, 14 Feb 2022 16:41:58 +0000 Subject: [PATCH 08/47] Update windows_installation.md Update links to include just the cpp build tools instead of the 4GB full Visual Studio link. --- docs/windows_installation.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/windows_installation.md b/docs/windows_installation.md index 9a068e152..c9964a94c 100644 --- a/docs/windows_installation.md +++ b/docs/windows_installation.md @@ -54,6 +54,8 @@ error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Unfortunately, many packages requiring compilation don't provide a pre-built wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use. -The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building C code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker compose](docker_quickstart.md) first. +You can download the Visual C++ build tools from [here](https://visualstudio.microsoft.com/visual-cpp-build-tools/). However, the easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building C code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker compose](docker_quickstart.md) first. + + --- From 30f6dbfc406453fd2783031b2a2817738c136327 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 14 Feb 2022 20:02:38 +0100 Subject: [PATCH 09/47] Attempt fix for #6261 --- freqtrade/optimize/backtesting.py | 14 +++++++++++++- tests/optimize/test_backtest_detail.py | 8 ++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 3f569649a..3126e1943 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -357,6 +357,15 @@ class Backtesting: # use Open rate if open_rate > calculated sell rate return sell_row[OPEN_IDX] + if ( + trade_dur == 0 + and trade.open_rate < sell_row[OPEN_IDX] # trade-open > open_rate + and sell_row[OPEN_IDX] > sell_row[CLOSE_IDX] # Red candle + ): + # ROI on opening candles with custom pricing can only + # trigger if the entry was at Open or lower. + # details: https: // github.com/freqtrade/freqtrade/issues/6261 + raise ValueError("Opening candle ROI on red candles.") # Use the maximum between close_rate and low as we # cannot sell outside of a candle. # Applies when a new ROI setting comes in place and the whole candle is above that. @@ -414,7 +423,10 @@ class Backtesting: trade.close_date = sell_candle_time trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) - closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) + try: + closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) + except ValueError: + return None # call the custom exit price,with default value as previous closerate current_profit = trade.calc_profit_ratio(closerate) order_type = self.strategy.order_types['sell'] diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index c23fd8f44..5ad67d9a0 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -562,9 +562,9 @@ tc35 = BTContainer(data=[ ) # Test 36: Custom-entry-price around candle low -# Causes immediate ROI exit. This is currently expected behavior (#6261) -# https://github.com/freqtrade/freqtrade/issues/6261 -# But may change at a later point. +# Would cause immediate ROI exit, but since the trade was entered +# below open, we treat this as cheating, and delay the sell by 1 candle. +# details: https://github.com/freqtrade/freqtrade/issues/6261 tc36 = BTContainer(data=[ # D O H L C V B S BT [0, 5000, 5050, 4950, 5000, 6172, 1, 0], @@ -574,7 +574,7 @@ tc36 = BTContainer(data=[ [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], stop_loss=-0.10, roi={"0": 0.01}, profit_perc=0.01, custom_entry_price=4952, - trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=1)] + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=2)] ) From 64b98989d2be20fe0fc85dc52929f63e6b4f9fce Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Feb 2022 19:25:32 +0100 Subject: [PATCH 10/47] Update open candle ROI condition --- freqtrade/optimize/backtesting.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 3126e1943..b423771ca 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -359,12 +359,15 @@ class Backtesting: if ( trade_dur == 0 - and trade.open_rate < sell_row[OPEN_IDX] # trade-open > open_rate + # Red candle (for longs), TODO: green candle (for shorts) and sell_row[OPEN_IDX] > sell_row[CLOSE_IDX] # Red candle + and trade.open_rate < sell_row[OPEN_IDX] # trade-open below open_rate + and close_rate > sell_row[CLOSE_IDX] ): # ROI on opening candles with custom pricing can only # trigger if the entry was at Open or lower. # details: https: // github.com/freqtrade/freqtrade/issues/6261 + # If open_rate is < open, only allow sells below the close on red candles. raise ValueError("Opening candle ROI on red candles.") # Use the maximum between close_rate and low as we # cannot sell outside of a candle. From 78a93b60526640c272fc725e3f37e4e76e6c9191 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Feb 2022 20:15:03 +0100 Subject: [PATCH 11/47] noqa --- freqtrade/rpc/api_server/api_backtest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/rpc/api_server/api_backtest.py b/freqtrade/rpc/api_server/api_backtest.py index a008702a9..8b86b8005 100644 --- a/freqtrade/rpc/api_server/api_backtest.py +++ b/freqtrade/rpc/api_server/api_backtest.py @@ -20,6 +20,7 @@ router = APIRouter() @router.post('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest']) +# flake8: noqa: C901 async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: BackgroundTasks, config=Depends(get_config)): """Start backtesting if not done so already""" From b043697d701d9169a2e21760933d1861ebfc2dab Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Wed, 16 Feb 2022 12:19:48 +0900 Subject: [PATCH 12/47] Update config_full.example.json --- config_examples/config_full.example.json | 1 + 1 file changed, 1 insertion(+) diff --git a/config_examples/config_full.example.json b/config_examples/config_full.example.json index 895a0af87..d675fb1a9 100644 --- a/config_examples/config_full.example.json +++ b/config_examples/config_full.example.json @@ -8,6 +8,7 @@ "amend_last_stake_amount": false, "last_stake_amount_min_ratio": 0.5, "dry_run": true, + "dry_run_wallet": 1000, "cancel_open_orders_on_exit": false, "timeframe": "5m", "trailing_stop": false, From e7bfb4fd5c22696fb87b1753a29f6c814c996ced Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 16 Feb 2022 13:42:39 +0100 Subject: [PATCH 13/47] Add test case for "sell below close" case --- tests/optimize/test_backtest_detail.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 5ad67d9a0..977563eeb 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -577,10 +577,24 @@ tc36 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=2)] ) - -# Test 37: Custom exit price below all candles -# Price adjusted to candle Low. +# Test 37: Custom-entry-price around candle low +# Would cause immediate ROI exit below close +# details: https://github.com/freqtrade/freqtrade/issues/6261 tc37 = BTContainer(data=[ + # D O H L C V B S BT + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5400, 5500, 4951, 5100, 6172, 0, 0], # Enter and immediate ROI + [2, 4900, 5250, 4500, 5100, 6172, 0, 0], + [3, 5100, 5100, 4650, 4750, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.01}, profit_perc=0.01, + custom_entry_price=4952, + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=1)] +) + +# Test 38: Custom exit price below all candles +# Price adjusted to candle Low. +tc38 = BTContainer(data=[ # D O H L C V B S BT [0, 5000, 5050, 4950, 5000, 6172, 1, 0], [1, 5000, 5500, 4951, 5000, 6172, 0, 0], @@ -593,9 +607,9 @@ tc37 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=3)] ) -# Test 38: Custom exit price above all candles +# Test 39: Custom exit price above all candles # causes sell signal timeout -tc38 = BTContainer(data=[ +tc39 = BTContainer(data=[ # D O H L C V B S BT [0, 5000, 5050, 4950, 5000, 6172, 1, 0], [1, 5000, 5500, 4951, 5000, 6172, 0, 0], @@ -649,6 +663,7 @@ TESTS = [ tc36, tc37, tc38, + tc39, ] From e60553b8f7b6400cddba5fafd04419ee5241f069 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 16 Feb 2022 19:21:04 +0100 Subject: [PATCH 14/47] Add max_entry_position hyperopt to docs closes #6356 --- docs/hyperopt.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 27d5a8761..19d8cd692 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -508,6 +508,46 @@ class MyAwesomeStrategy(IStrategy): You will then obviously also change potential interesting entries to parameters to allow hyper-optimization. +### Optimizing `max_entry_position_adjustment` + +While `max_entry_position_adjustment` is not a separate space, it can still be used in hyperopt by using the property approach shown above. + +``` python +from pandas import DataFrame +from functools import reduce + +import talib.abstract as ta + +from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter, + IStrategy, IntParameter) +import freqtrade.vendor.qtpylib.indicators as qtpylib + +class MyAwesomeStrategy(IStrategy): + stoploss = -0.05 + timeframe = '15m' + + # Define the parameter spaces + max_epa = CategoricalParameter([-1, 0, 1, 3, 5, 10], default=1, space="buy", optimize=True) + + @property + def max_entry_position_adjustment(self): + return self.max_epa.value + + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # ... +``` + +??? Tip "Using `IntParameter`" + You can also use the `IntParameter` for this optimization, but you must explicitly return an integer: + ``` python + max_epa = IntParameter(-1, 10, default=1, space="buy", optimize=True) + + @property + def max_entry_position_adjustment(self): + return int(self.max_epa.value) + ``` + ## Loss-functions Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results. From eb88c0f71b2dda0f5aab31f2fcaa7d1d95d0f0bc Mon Sep 17 00:00:00 2001 From: Kavinkumar <33546454+mkavinkumar1@users.noreply.github.com> Date: Fri, 18 Feb 2022 19:25:56 +0530 Subject: [PATCH 15/47] fixed stake amount --- tests/rpc/test_rpc_telegram.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 67a6c72fe..f39c627f1 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -2031,7 +2031,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: 'pair': 'ETH/BTC', 'limit': 1.099e-05, 'order_type': 'limit', - 'stake_amount': 0.001, + 'stake_amount': 0.015, 'stake_amount_fiat': 0.0, 'stake_currency': 'BTC', 'fiat_currency': None, @@ -2044,7 +2044,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00001099`\n' '*Current Rate:* `0.00001099`\n' - '*Total:* `(0.00100000 BTC)`') + '*Total:* `(0.01500000 BTC)`') def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: From 95d4a11bb1564bfc5a555f9ae02fedf6af6f8a6a Mon Sep 17 00:00:00 2001 From: Kavinkumar <33546454+mkavinkumar1@users.noreply.github.com> Date: Fri, 18 Feb 2022 19:32:37 +0530 Subject: [PATCH 16/47] add precision --- tests/rpc/test_rpc_telegram.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index f39c627f1..e42d6bfa7 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -2031,7 +2031,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: 'pair': 'ETH/BTC', 'limit': 1.099e-05, 'order_type': 'limit', - 'stake_amount': 0.015, + 'stake_amount': 0.01465333, 'stake_amount_fiat': 0.0, 'stake_currency': 'BTC', 'fiat_currency': None, @@ -2044,7 +2044,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00001099`\n' '*Current Rate:* `0.00001099`\n' - '*Total:* `(0.01500000 BTC)`') + '*Total:* `(0.01465333 BTC)`') def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: From 60d1e7fc6578e57ebd27ad05b37e4de63e1ed20f Mon Sep 17 00:00:00 2001 From: Kavinkumar <33546454+mkavinkumar1@users.noreply.github.com> Date: Fri, 18 Feb 2022 19:47:45 +0530 Subject: [PATCH 17/47] fix stake amt --- tests/rpc/test_rpc_telegram.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index e42d6bfa7..4d00095e4 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1734,7 +1734,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: 'pair': 'ETH/BTC', 'limit': 1.099e-05, 'order_type': 'limit', - 'stake_amount': 0.001, + 'stake_amount': 0.01465333, 'stake_amount_fiat': 0.0, 'stake_currency': 'BTC', 'fiat_currency': 'USD', @@ -1751,7 +1751,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: '*Amount:* `1333.33333333`\n' \ '*Open Rate:* `0.00001099`\n' \ '*Current Rate:* `0.00001099`\n' \ - '*Total:* `(0.00100000 BTC, 12.345 USD)`' + '*Total:* `(0.01465333 BTC, 180.895 USD)`' freqtradebot.config['telegram']['notification_settings'] = {'buy': 'off'} caplog.clear() @@ -1825,7 +1825,7 @@ def test_send_msg_buy_fill_notification(default_conf, mocker) -> None: 'buy_tag': 'buy_signal_01', 'exchange': 'Binance', 'pair': 'ETH/BTC', - 'stake_amount': 0.001, + 'stake_amount': 0.01465333, # 'stake_amount_fiat': 0.0, 'stake_currency': 'BTC', 'fiat_currency': 'USD', @@ -1839,7 +1839,7 @@ def test_send_msg_buy_fill_notification(default_conf, mocker) -> None: '*Buy Tag:* `buy_signal_01`\n' \ '*Amount:* `1333.33333333`\n' \ '*Open Rate:* `0.00001099`\n' \ - '*Total:* `(0.00100000 BTC, 12.345 USD)`' + '*Total:* `(0.01465333 BTC, 180.895 USD)`' def test_send_msg_sell_notification(default_conf, mocker) -> None: From 0bbbe2e96c5e58413a6611db7d8c967514727fe2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Feb 2022 06:36:23 +0100 Subject: [PATCH 18/47] Add test for #6429 --- tests/test_freqtradebot.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 4bbf26362..08d98b42d 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2436,6 +2436,9 @@ def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_buy_order_ mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', cancel_order_mock) assert not freqtrade.handle_cancel_enter(trade, limit_buy_order_usdt, reason) assert log_has_re(r"Order .* for .* not cancelled.", caplog) + # min_pair_stake empty should not crash + mocker.patch('freqtrade.exchange.Exchange.get_min_pair_stake_amount', return_value=None) + assert not freqtrade.handle_cancel_enter(trade, limit_buy_order_usdt, reason) @pytest.mark.parametrize("limit_buy_order_canceled_empty", ['binance', 'ftx', 'kraken', 'bittrex'], From 3785f04be7c8bbeb01e9f849ad194ac77dfeeeeb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Feb 2022 06:38:11 +0100 Subject: [PATCH 19/47] Handle empty min stake amount as observed on FTX (closes #6429) --- freqtrade/freqtradebot.py | 4 ++-- freqtrade/persistence/models.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index fce85baa3..5f2b72e1e 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1021,12 +1021,12 @@ class FreqtradeBot(LoggingMixin): # Cancelled orders may have the status of 'canceled' or 'closed' if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES: - filled_val = order.get('filled', 0.0) or 0.0 + filled_val: float = order.get('filled', 0.0) or 0.0 filled_stake = filled_val * trade.open_rate minstake = self.exchange.get_min_pair_stake_amount( trade.pair, trade.open_rate, self.strategy.stoploss) - if filled_val > 0 and filled_stake < minstake: + if filled_val > 0 and minstake and filled_stake < minstake: logger.warning( f"Order {trade.open_order_id} for {trade.pair} not cancelled, " f"as the filled amount of {filled_val} would result in an unsellable trade.") diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 5f2db1050..b8ea5848f 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -799,11 +799,11 @@ class Trade(_DECL_BASE, LocalTrade): fee_close = Column(Float, nullable=False, default=0.0) fee_close_cost = Column(Float, nullable=True) fee_close_currency = Column(String(25), nullable=True) - open_rate = Column(Float) + open_rate: float = Column(Float) open_rate_requested = Column(Float) # open_trade_value - calculated via _calc_open_trade_value open_trade_value = Column(Float) - close_rate = Column(Float) + close_rate: Optional[float] = Column(Float) close_rate_requested = Column(Float) close_profit = Column(Float) close_profit_abs = Column(Float) From a32aed2225544073eb020d91ce1806e04b2d4ce1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Feb 2022 10:07:32 +0100 Subject: [PATCH 20/47] Update FTX stoploss code to avoid exception for stoploss-market orders closes #6430, closes #6392 --- freqtrade/exchange/ftx.py | 19 +++++++++++-------- tests/exchange/test_ftx.py | 21 +++++++++++++++++++-- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index e9eb2fe19..a8bf9abac 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -106,15 +106,18 @@ class Ftx(Exchange): if order[0].get('status') == 'closed': # Trigger order was triggered ... real_order_id = order[0].get('info', {}).get('orderId') + # OrderId may be None for stoploss-market orders + # But contains "average" in these cases. + if real_order_id: + order1 = self._api.fetch_order(real_order_id, pair) + self._log_exchange_response('fetch_stoploss_order1', order1) + # Fake type to stop - as this was really a stop order. + order1['id_stop'] = order1['id'] + order1['id'] = order_id + order1['type'] = 'stop' + order1['status_stop'] = 'triggered' + return order1 - order1 = self._api.fetch_order(real_order_id, pair) - self._log_exchange_response('fetch_stoploss_order1', order1) - # Fake type to stop - as this was really a stop order. - order1['id_stop'] = order1['id'] - order1['id'] = order_id - order1['type'] = 'stop' - order1['status_stop'] = 'triggered' - return order1 return order[0] else: raise InvalidOrderException(f"Could not get stoploss order for id {order_id}") diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 3794bb79c..c2fb90c9d 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -125,7 +125,7 @@ def test_stoploss_adjust_ftx(mocker, default_conf): assert not exchange.stoploss_adjust(1501, order) -def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order): +def test_fetch_stoploss_order_ftx(default_conf, mocker, limit_sell_order): default_conf['dry_run'] = True order = MagicMock() order.myid = 123 @@ -147,9 +147,15 @@ def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order): with pytest.raises(InvalidOrderException, match=r"Could not get stoploss order for id X"): exchange.fetch_stoploss_order('X', 'TKN/BTC')['status'] - api_mock.fetch_orders = MagicMock(return_value=[{'id': 'X', 'status': 'closed'}]) + # stoploss Limit order + api_mock.fetch_orders = MagicMock(return_value=[ + {'id': 'X', 'status': 'closed', + 'info': { + 'orderId': 'mocked_limit_sell', + }}]) api_mock.fetch_order = MagicMock(return_value=limit_sell_order) + # No orderId field - no call to fetch_order resp = exchange.fetch_stoploss_order('X', 'TKN/BTC') assert resp assert api_mock.fetch_order.call_count == 1 @@ -158,6 +164,17 @@ def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order): assert resp['type'] == 'stop' assert resp['status_stop'] == 'triggered' + # Stoploss market order + # Contains no new Order, but "average" instead + order = {'id': 'X', 'status': 'closed', 'info': {'orderId': None}, 'average': 0.254} + api_mock.fetch_orders = MagicMock(return_value=[order]) + api_mock.fetch_order.reset_mock() + resp = exchange.fetch_stoploss_order('X', 'TKN/BTC') + assert resp + # fetch_order not called (no regular order ID) + assert api_mock.fetch_order.call_count == 0 + assert order == order + with pytest.raises(InvalidOrderException): api_mock.fetch_orders = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx') From a7a25bb28514426531702f260edaeb11b0c9d994 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Feb 2022 16:35:17 +0100 Subject: [PATCH 21/47] Update "round coin value" to trim trailing zeros --- freqtrade/misc.py | 13 +++++++++---- tests/test_misc.py | 7 +++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 2a27f1660..133014f39 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -29,18 +29,23 @@ def decimals_per_coin(coin: str): return DECIMALS_PER_COIN.get(coin, DECIMAL_PER_COIN_FALLBACK) -def round_coin_value(value: float, coin: str, show_coin_name=True) -> str: +def round_coin_value( + value: float, coin: str, show_coin_name=True, keep_trailing_zeros=False) -> str: """ Get price value for this coin :param value: Value to be printed :param coin: Which coin are we printing the price / value for :param show_coin_name: Return string in format: "222.22 USDT" or "222.22" + :param keep_trailing_zeros: Keep trailing zeros "222.200" vs. "222.2" :return: Formatted / rounded value (with or without coin name) """ + val = f"{value:.{decimals_per_coin(coin)}f}" + if not keep_trailing_zeros: + val = val.rstrip('0').rstrip('.') if show_coin_name: - return f"{value:.{decimals_per_coin(coin)}f} {coin}" - else: - return f"{value:.{decimals_per_coin(coin)}f}" + val = f"{val} {coin}" + + return val def shorten_date(_date: str) -> str: diff --git a/tests/test_misc.py b/tests/test_misc.py index 21a00f3be..4fd5338ad 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -21,16 +21,19 @@ def test_decimals_per_coin(): def test_round_coin_value(): assert round_coin_value(222.222222, 'USDT') == '222.222 USDT' - assert round_coin_value(222.2, 'USDT') == '222.200 USDT' + assert round_coin_value(222.2, 'USDT', keep_trailing_zeros=True) == '222.200 USDT' + assert round_coin_value(222.2, 'USDT') == '222.2 USDT' assert round_coin_value(222.12745, 'EUR') == '222.127 EUR' assert round_coin_value(0.1274512123, 'BTC') == '0.12745121 BTC' assert round_coin_value(0.1274512123, 'ETH') == '0.12745 ETH' assert round_coin_value(222.222222, 'USDT', False) == '222.222' - assert round_coin_value(222.2, 'USDT', False) == '222.200' + assert round_coin_value(222.2, 'USDT', False) == '222.2' + assert round_coin_value(222.00, 'USDT', False) == '222' assert round_coin_value(222.12745, 'EUR', False) == '222.127' assert round_coin_value(0.1274512123, 'BTC', False) == '0.12745121' assert round_coin_value(0.1274512123, 'ETH', False) == '0.12745' + assert round_coin_value(222.2, 'USDT', False, True) == '222.200' def test_shorten_date() -> None: From d610b6305de54e0cabb60350681313d00324d988 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Feb 2022 16:39:47 +0100 Subject: [PATCH 22/47] Improve /balance output by removing trailing zeros --- freqtrade/optimize/hyperopt_tools.py | 4 ++-- freqtrade/rpc/telegram.py | 13 +++++++------ tests/rpc/test_rpc_telegram.py | 6 +++--- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 61a10c32b..8c84f772a 100755 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -373,7 +373,7 @@ class HyperoptTools(): trials[f"Max Drawdown{' (Acct)' if has_account_drawdown else ''}"] = trials.apply( lambda x: "{} {}".format( - round_coin_value(x['max_drawdown_abs'], stake_currency), + round_coin_value(x['max_drawdown_abs'], stake_currency, keep_trailing_zeros=True), (f"({x['max_drawdown_account']:,.2%})" if has_account_drawdown else f"({x['max_drawdown']:,.2%})" @@ -388,7 +388,7 @@ class HyperoptTools(): trials['Profit'] = trials.apply( lambda x: '{} {}'.format( - round_coin_value(x['Total profit'], stake_currency), + round_coin_value(x['Total profit'], stake_currency, keep_trailing_zeros=True), f"({x['Profit']:,.2%})".rjust(10, ' ') ).rjust(25+len(stake_currency)) if x['Total profit'] != 0.0 else '--'.rjust(25+len(stake_currency)), diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 0a634ffae..da613fab8 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -790,12 +790,13 @@ class Telegram(RPCHandler): output = '' if self._config['dry_run']: output += "*Warning:* Simulated balances in Dry Mode.\n" - - output += ("Starting capital: " - f"`{result['starting_capital']}` {self._config['stake_currency']}" - ) - output += (f" `{result['starting_capital_fiat']}` " - f"{self._config['fiat_display_currency']}.\n" + starting_cap = round_coin_value( + result['starting_capital'], self._config['stake_currency']) + output += f"Starting capital: `{starting_cap}`" + starting_cap_fiat = round_coin_value( + result['starting_capital_fiat'], self._config['fiat_display_currency'] + ) if result['starting_capital_fiat'] > 0 else '' + output += (f" `, {starting_cap_fiat}`.\n" ) if result['starting_capital_fiat'] > 0 else '.\n' total_dust_balance = 0 diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 4d00095e4..640f9305c 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -770,7 +770,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, assert 'No closed trade' in msg_mock.call_args_list[-1][0][0] assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0] mocker.patch('freqtrade.wallets.Wallets.get_starting_balance', return_value=0.01) - assert ('∙ `-0.00000500 BTC (-0.50%) (-0.0 \N{GREEK CAPITAL LETTER SIGMA}%)`' + assert ('∙ `-0.000005 BTC (-0.50%) (-0.0 \N{GREEK CAPITAL LETTER SIGMA}%)`' in msg_mock.call_args_list[-1][0][0]) msg_mock.reset_mock() @@ -845,7 +845,7 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick assert '*XRP:*' not in result assert 'Balance:' in result assert 'Est. BTC:' in result - assert 'BTC: 12.00000000' in result + assert 'BTC: 12' in result assert "*3 Other Currencies (< 0.0001 BTC):*" in result assert 'BTC: 0.00000309' in result @@ -874,7 +874,7 @@ def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None result = msg_mock.call_args_list[0][0][0] assert msg_mock.call_count == 1 assert "*Warning:* Simulated balances in Dry Mode." in result - assert "Starting capital: `1000` BTC" in result + assert "Starting capital: `1000 BTC`" in result def test_balance_handle_too_large_response(default_conf, update, mocker) -> None: From c13eed2178819dfde379a803f13afca81869831d Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Feb 2022 19:15:55 +0100 Subject: [PATCH 23/47] use Order object to update trade --- freqtrade/freqtradebot.py | 9 ++++--- freqtrade/persistence/models.py | 47 ++++++++++++++++++++------------- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 5f2b72e1e..9166f43e9 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -16,7 +16,7 @@ from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge from freqtrade.enums import RPCMessageType, RunMode, SellType, State from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, - InvalidOrderException, PricingError) + InvalidOrderException, OperationalException, PricingError) from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.mixins import LoggingMixin @@ -1358,8 +1358,11 @@ class FreqtradeBot(LoggingMixin): return True order = self.handle_order_fee(trade, order) - - trade.update(order) + order_obj = trade.select_order_by_order_id(order['id']) + if not order_obj: + # TODO: this can't happen! + raise OperationalException("order-obj not found!") + trade.update(order_obj) trade.recalc_trade_from_orders() Trade.commit() diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index b8ea5848f..e6daa08ba 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -113,14 +113,15 @@ class Order(_DECL_BASE): trade = relationship("Trade", back_populates="orders") - ft_order_side = Column(String(25), nullable=False) - ft_pair = Column(String(25), nullable=False) + # order_side can only be 'buy', 'sell' or 'stoploss' + ft_order_side: str = Column(String(25), nullable=False) + ft_pair: str = Column(String(25), nullable=False) ft_is_open = Column(Boolean, nullable=False, default=True, index=True) order_id = Column(String(255), nullable=False, index=True) status = Column(String(255), nullable=True) symbol = Column(String(25), nullable=True) - order_type = Column(String(50), nullable=True) + order_type: str = Column(String(50), nullable=True) side = Column(String(25), nullable=True) price = Column(Float, nullable=True) average = Column(Float, nullable=True) @@ -133,9 +134,18 @@ class Order(_DECL_BASE): order_update_date = Column(DateTime, nullable=True) @property - def order_date_utc(self): + def order_date_utc(self) -> datetime: + """ Order-date with UTC timezoneinfo""" return self.order_date.replace(tzinfo=timezone.utc) + @property + def safe_price(self) -> float: + return self.average or self.price + + @property + def safe_filled(self) -> float: + return self.filled or self.amount + def __repr__(self): return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, ' @@ -452,40 +462,39 @@ class LocalTrade(): f"Trailing stoploss saved us: " f"{float(self.stop_loss) - float(self.initial_stop_loss):.8f}.") - def update(self, order: Dict) -> None: + def update(self, order: Order) -> None: """ Updates this entity with amount and actual open/close rates. :param order: order retrieved by exchange.fetch_order() :return: None """ - order_type = order['type'] # Ignore open and cancelled orders - if order['status'] == 'open' or safe_value_fallback(order, 'average', 'price') is None: + if order.status == 'open' or safe_value_fallback(order, 'average', 'price') is None: return - logger.info('Updating trade (id=%s) ...', self.id) + logger.info(f'Updating trade (id={self.id}) ...') - if order_type in ('market', 'limit') and order['side'] == 'buy': + if order.ft_order_side == 'buy': # Update open rate and actual amount - self.open_rate = float(safe_value_fallback(order, 'average', 'price')) - self.amount = float(safe_value_fallback(order, 'filled', 'amount')) + self.open_rate = order.safe_price + self.amount = order.safe_filled if self.is_open: - logger.info(f'{order_type.upper()}_BUY has been fulfilled for {self}.') + logger.info(f'{order.order_type.upper()}_BUY has been fulfilled for {self}.') self.open_order_id = None self.recalc_trade_from_orders() - elif order_type in ('market', 'limit') and order['side'] == 'sell': + elif order.ft_order_side == 'sell': if self.is_open: - logger.info(f'{order_type.upper()}_SELL has been fulfilled for {self}.') - self.close(safe_value_fallback(order, 'average', 'price')) - elif order_type in ('stop_loss_limit', 'stop-loss', 'stop-loss-limit', 'stop'): + logger.info(f'{order.order_type.upper()}_SELL has been fulfilled for {self}.') + self.close(order.safe_price) + elif order.ft_order_side == 'stoploss': self.stoploss_order_id = None self.close_rate_requested = self.stop_loss self.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value if self.is_open: - logger.info(f'{order_type.upper()} is hit for {self}.') - self.close(safe_value_fallback(order, 'average', 'price')) + logger.info(f'{order.order_type.upper()} is hit for {self}.') + self.close(order.safe_price) else: - raise ValueError(f'Unknown order type: {order_type}') + raise ValueError(f'Unknown order type: {order.order_type}') Trade.commit() def close(self, rate: float, *, show_msg: bool = True) -> None: From 1b1216fc87b49050ab81e64f41ee346cbcaee04b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Feb 2022 19:18:19 +0100 Subject: [PATCH 24/47] Rename update_trade method --- freqtrade/freqtradebot.py | 3 ++- freqtrade/persistence/models.py | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 9166f43e9..b8dfc6b08 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1362,7 +1362,8 @@ class FreqtradeBot(LoggingMixin): if not order_obj: # TODO: this can't happen! raise OperationalException("order-obj not found!") - trade.update(order_obj) + 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() diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index e6daa08ba..52aea387f 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -151,7 +151,7 @@ class Order(_DECL_BASE): return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, ' f'side={self.side}, order_type={self.order_type}, status={self.status})') - def update_from_ccxt_object(self, order): + def update_from_ccxt_object(self, order) -> 'Order': """ Update Order from ccxt response Only updates if fields are available from ccxt - @@ -178,6 +178,7 @@ class Order(_DECL_BASE): if (order.get('filled', 0.0) or 0.0) > 0: self.order_filled_date = datetime.now(timezone.utc) self.order_update_date = datetime.now(timezone.utc) + return self def to_json(self) -> Dict[str, Any]: return { @@ -462,14 +463,14 @@ class LocalTrade(): f"Trailing stoploss saved us: " f"{float(self.stop_loss) - float(self.initial_stop_loss):.8f}.") - def update(self, order: Order) -> None: + def update_trade(self, order: Order) -> None: """ Updates this entity with amount and actual open/close rates. :param order: order retrieved by exchange.fetch_order() :return: None """ # Ignore open and cancelled orders - if order.status == 'open' or safe_value_fallback(order, 'average', 'price') is None: + if order.status == 'open' or order.safe_price is None: return logger.info(f'Updating trade (id={self.id}) ...') From 508e677d70d8f4fd5825d94055c32280fecb9ffd Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Feb 2022 20:40:50 +0100 Subject: [PATCH 25/47] Fix some tests to call update_trade with order object --- freqtrade/persistence/models.py | 1 - tests/conftest.py | 2 +- tests/test_freqtradebot.py | 39 +++++++++++++++++-------- tests/test_persistence.py | 50 ++++++++++++++++++++++----------- 4 files changed, 62 insertions(+), 30 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 52aea387f..731d57262 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -16,7 +16,6 @@ from sqlalchemy.sql.schema import UniqueConstraint from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES from freqtrade.enums import SellType from freqtrade.exceptions import DependencyException, OperationalException -from freqtrade.misc import safe_value_fallback from freqtrade.persistence.migrations import check_migrate diff --git a/tests/conftest.py b/tests/conftest.py index 630223d55..043cc6fcf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2208,7 +2208,7 @@ def limit_sell_order_usdt_open(): 'id': 'mocked_limit_sell_usdt', 'type': 'limit', 'side': 'sell', - 'pair': 'mocked', + 'symbol': 'mocked', 'datetime': arrow.utcnow().isoformat(), 'timestamp': arrow.utcnow().int_timestamp, 'price': 2.20, diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 08d98b42d..50d060a39 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -227,7 +227,8 @@ def test_edge_overrides_stoploss(limit_buy_order_usdt, fee, caplog, mocker, freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) freqtrade.enter_positions() trade = Trade.query.first() - trade.update(limit_buy_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy') + trade.update_trade(oobj) ############################################# # stoploss shoud be hit @@ -292,7 +293,8 @@ def test_create_trade(default_conf_usdt, ticker_usdt, limit_buy_order_usdt, fee, assert trade.exchange == 'binance' # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy') + trade.update_trade(oobj) assert trade.open_rate == 2.0 assert trade.amount == 30.0 @@ -1803,7 +1805,8 @@ def test_handle_trade(default_conf_usdt, limit_buy_order_usdt, limit_sell_order_ assert trade time.sleep(0.01) # Race condition fix - trade.update(limit_buy_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, limit_buy_order_usdt['symbol'], 'buy') + trade.update_trade(oobj) assert trade.is_open is True freqtrade.wallets.update() @@ -1812,7 +1815,9 @@ def test_handle_trade(default_conf_usdt, limit_buy_order_usdt, limit_sell_order_ assert trade.open_order_id == limit_sell_order_usdt['id'] # Simulate fulfilled LIMIT_SELL order for trade - trade.update(limit_sell_order_usdt) + oobj = Order.parse_from_ccxt_object( + limit_sell_order_usdt, limit_sell_order_usdt['symbol'], 'sell') + trade.update_trade(oobj) assert trade.close_rate == 2.2 assert trade.close_profit == 0.09451372 @@ -1962,8 +1967,11 @@ def test_close_trade(default_conf_usdt, ticker_usdt, limit_buy_order_usdt, trade = Trade.query.first() assert trade - trade.update(limit_buy_order_usdt) - trade.update(limit_sell_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, limit_buy_order_usdt['symbol'], 'buy') + trade.update_trade(oobj) + oobj = Order.parse_from_ccxt_object( + limit_sell_order_usdt, limit_sell_order_usdt['symbol'], 'sell') + trade.update_trade(oobj) assert trade.is_open is False with pytest.raises(DependencyException, match=r'.*closed trade.*'): @@ -3103,7 +3111,8 @@ def test_sell_profit_only( freqtrade.enter_positions() trade = Trade.query.first() - trade.update(limit_buy_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, limit_buy_order_usdt['symbol'], 'buy') + trade.update_trade(oobj) freqtrade.wallets.update() patch_get_signal(freqtrade, value=(False, True, None, None)) assert freqtrade.handle_trade(trade) is handle_first @@ -3139,7 +3148,9 @@ def test_sell_not_enough_balance(default_conf_usdt, limit_buy_order_usdt, limit_ trade = Trade.query.first() amnt = trade.amount - trade.update(limit_buy_order_usdt) + + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, limit_buy_order_usdt['symbol'], 'buy') + trade.update_trade(oobj) patch_get_signal(freqtrade, value=(False, True, None, None)) mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=trade.amount * 0.985)) @@ -3247,7 +3258,8 @@ def test_ignore_roi_if_buy_signal(default_conf_usdt, limit_buy_order_usdt, freqtrade.enter_positions() trade = Trade.query.first() - trade.update(limit_buy_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, limit_buy_order_usdt['symbol'], 'buy') + trade.update_trade(oobj) freqtrade.wallets.update() patch_get_signal(freqtrade, value=(True, True, None, None)) assert freqtrade.handle_trade(trade) is False @@ -3437,7 +3449,8 @@ def test_disable_ignore_roi_if_buy_signal(default_conf_usdt, limit_buy_order_usd freqtrade.enter_positions() trade = Trade.query.first() - trade.update(limit_buy_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, limit_buy_order_usdt['symbol'], 'buy') + trade.update_trade(oobj) # Sell due to min_roi_reached patch_get_signal(freqtrade, value=(True, False, None, None)) assert freqtrade.handle_trade(trade) is True @@ -3812,7 +3825,8 @@ def test_order_book_depth_of_market( assert len(Trade.query.all()) == 1 # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy') + trade.update_trade(oobj) assert trade.open_rate == 2.0 assert whitelist == default_conf_usdt['exchange']['pair_whitelist'] @@ -3906,7 +3920,8 @@ def test_order_book_ask_strategy( assert trade time.sleep(0.01) # Race condition fix - trade.update(limit_buy_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, limit_buy_order_usdt['symbol'], 'buy') + trade.update_trade(oobj) freqtrade.wallets.update() assert trade.is_open is True diff --git a/tests/test_persistence.py b/tests/test_persistence.py index b8f7a3336..3fa47daa9 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -108,7 +108,8 @@ def test_update_limit_order(limit_buy_order_usdt, limit_sell_order_usdt, fee, ca assert trade.close_date is None trade.open_order_id = 'something' - trade.update(limit_buy_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy') + trade.update_trade(oobj) assert trade.open_order_id is None assert trade.open_rate == 2.00 assert trade.close_profit is None @@ -119,7 +120,8 @@ def test_update_limit_order(limit_buy_order_usdt, limit_sell_order_usdt, fee, ca caplog.clear() trade.open_order_id = 'something' - trade.update(limit_sell_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_sell_order_usdt, 'ADA/USDT', 'sell') + trade.update_trade(oobj) assert trade.open_order_id is None assert trade.close_rate == 2.20 assert trade.close_profit == round(0.0945137157107232, 8) @@ -146,7 +148,8 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee, ) trade.open_order_id = 'something' - trade.update(market_buy_order_usdt) + oobj = Order.parse_from_ccxt_object(market_buy_order_usdt, 'ADA/USDT', 'buy') + trade.update_trade(oobj) assert trade.open_order_id is None assert trade.open_rate == 2.0 assert trade.close_profit is None @@ -158,7 +161,8 @@ 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.update(market_sell_order_usdt) + oobj = Order.parse_from_ccxt_object(market_sell_order_usdt, 'ADA/USDT', 'sell') + trade.update_trade(oobj) assert trade.open_order_id is None assert trade.close_rate == 2.2 assert trade.close_profit == round(0.0945137157107232, 8) @@ -181,9 +185,11 @@ def test_calc_open_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt ) trade.open_order_id = 'something' - trade.update(limit_buy_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy') + trade.update_trade(oobj) assert trade._calc_open_trade_value() == 60.15 - trade.update(limit_sell_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_sell_order_usdt, 'ADA/USDT', 'sell') + trade.update_trade(oobj) assert isclose(trade.calc_close_trade_value(), 65.835) # Profit in USDT @@ -236,7 +242,8 @@ def test_calc_close_trade_price_exception(limit_buy_order_usdt, fee): ) trade.open_order_id = 'something' - trade.update(limit_buy_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy') + trade.update_trade(oobj) assert trade.calc_close_trade_value() == 0.0 @@ -257,7 +264,8 @@ def test_update_open_order(limit_buy_order_usdt): assert trade.close_date is None limit_buy_order_usdt['status'] = 'open' - trade.update(limit_buy_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy') + trade.update_trade(oobj) assert trade.open_order_id is None assert trade.close_profit is None @@ -276,8 +284,9 @@ def test_update_invalid_order(limit_buy_order_usdt): exchange='binance', ) limit_buy_order_usdt['type'] = 'invalid' + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'meep') with pytest.raises(ValueError, match=r'Unknown order type'): - trade.update(limit_buy_order_usdt) + trade.update_trade(oobj) @pytest.mark.usefixtures("init_persistence") @@ -304,7 +313,8 @@ def test_calc_open_trade_value(limit_buy_order_usdt, fee): exchange='binance', ) trade.open_order_id = 'open_trade' - trade.update(limit_buy_order_usdt) # Buy @ 2.0 + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy') + trade.update_trade(oobj) # Buy @ 2.0 # Get the open rate price with the standard fee rate assert trade._calc_open_trade_value() == 60.15 @@ -325,14 +335,16 @@ def test_calc_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt, fee exchange='binance', ) trade.open_order_id = 'close_trade' - trade.update(limit_buy_order_usdt) # Buy @ 2.0 + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy') + trade.update_trade(oobj) # Buy @ 2.0 # Get the close rate price with a custom close rate and a regular fee rate assert trade.calc_close_trade_value(rate=2.5) == 74.8125 # Get the close rate price with a custom close rate and a custom fee rate assert trade.calc_close_trade_value(rate=2.5, fee=0.003) == 74.775 # Test when we apply a Sell order, and ask price with a custom fee rate - trade.update(limit_sell_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_sell_order_usdt, 'ADA/USDT', 'sell') + trade.update_trade(oobj) assert trade.calc_close_trade_value(fee=0.005) == 65.67 @@ -409,7 +421,9 @@ def test_calc_profit(limit_buy_order_usdt, limit_sell_order_usdt, fee): exchange='binance', ) trade.open_order_id = 'something' - trade.update(limit_buy_order_usdt) # Buy @ 2.0 + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy') + + trade.update_trade(oobj) # Buy @ 2.0 # Custom closing rate and regular fee rate # Higher than open rate - 2.1 quote @@ -424,7 +438,8 @@ def test_calc_profit(limit_buy_order_usdt, limit_sell_order_usdt, fee): assert trade.calc_profit(rate=1.9, fee=0.003) == round(-3.320999999999998, 8) # Test when we apply a Sell order. Sell higher than open rate @ 2.2 - trade.update(limit_sell_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_sell_order_usdt, 'ADA/USDT', 'sell') + trade.update_trade(oobj) assert trade.calc_profit() == round(5.684999999999995, 8) # Test with a custom fee rate on the close trade @@ -443,7 +458,9 @@ def test_calc_profit_ratio(limit_buy_order_usdt, limit_sell_order_usdt, fee): exchange='binance' ) trade.open_order_id = 'something' - trade.update(limit_buy_order_usdt) # Buy @ 2.0 + + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy') + trade.update_trade(oobj) # Buy @ 2.0 # Higher than open rate - 2.1 quote assert trade.calc_profit_ratio(rate=2.1) == round(0.04476309226932673, 8) @@ -457,7 +474,8 @@ def test_calc_profit_ratio(limit_buy_order_usdt, limit_sell_order_usdt, fee): assert trade.calc_profit_ratio(rate=1.9, fee=0.003) == round(-0.05521197007481293, 8) # Test when we apply a Sell order. Sell higher than open rate @ 2.2 - trade.update(limit_sell_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_sell_order_usdt, 'ADA/USDT', 'sell') + trade.update_trade(oobj) assert trade.calc_profit_ratio() == round(0.0945137157107232, 8) # Test with a custom fee rate on the close trade From 874c161f78ccda10c76eac9c8d092e10d8c96ce8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 11 Feb 2022 07:33:46 +0100 Subject: [PATCH 26/47] Update more tests to use order_obj to update trade --- freqtrade/freqtradebot.py | 2 +- tests/conftest.py | 2 +- tests/rpc/test_rpc.py | 53 +++++++++++++++--------- tests/rpc/test_rpc_telegram.py | 74 +++++++++++++++++++++------------- tests/test_freqtradebot.py | 47 +++++++++++++++------ 5 files changed, 117 insertions(+), 61 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index b8dfc6b08..06fcb8a9a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1361,7 +1361,7 @@ class FreqtradeBot(LoggingMixin): order_obj = trade.select_order_by_order_id(order['id']) if not order_obj: # TODO: this can't happen! - raise OperationalException("order-obj not found!") + raise OperationalException(f"order-obj for {order['id']} not found!") 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() diff --git a/tests/conftest.py b/tests/conftest.py index 043cc6fcf..a7f23ea76 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1221,7 +1221,7 @@ def limit_sell_order_open(): 'id': 'mocked_limit_sell', 'type': 'limit', 'side': 'sell', - 'pair': 'mocked', + 'symbol': 'mocked', 'datetime': arrow.utcnow().isoformat(), 'timestamp': arrow.utcnow().int_timestamp, 'price': 0.00001173, diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 1c713ee86..e7b09ab74 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -11,6 +11,7 @@ from freqtrade.edge import PairInfo from freqtrade.enums import State from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError from freqtrade.persistence import Trade +from freqtrade.persistence.models import Order from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.rpc import RPC, RPCException from freqtrade.rpc.fiat_convert import CryptoToFiatConverter @@ -277,8 +278,10 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee, assert trade # Simulate buy & sell - trade.update(limit_buy_order) - trade.update(limit_sell_order) + oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') + trade.update_trade(oobj) + oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') + trade.update_trade(oobj) trade.close_date = datetime.utcnow() trade.is_open = False @@ -415,28 +418,32 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, freqtradebot.enter_positions() trade = Trade.query.first() # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) + oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'sell') + trade.update_trade(oobj) # Update the ticker with a market going up mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker_sell_up ) - trade.update(limit_sell_order) + oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') + trade.update_trade(oobj) trade.close_date = datetime.utcnow() trade.is_open = False freqtradebot.enter_positions() trade = Trade.query.first() # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) + oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') + trade.update_trade(oobj) # Update the ticker with a market going up mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker_sell_up ) - trade.update(limit_sell_order) + oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') + trade.update_trade(oobj) trade.close_date = datetime.utcnow() trade.is_open = False @@ -495,14 +502,16 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, freqtradebot.enter_positions() trade = Trade.query.first() # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) + oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') + trade.update_trade(oobj) # Update the ticker with a market going up mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker_sell_up, get_fee=fee ) - trade.update(limit_sell_order) + oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') + trade.update_trade(oobj) trade.close_date = datetime.utcnow() trade.is_open = False @@ -754,13 +763,13 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: mocker.patch( 'freqtrade.exchange.Exchange.fetch_order', side_effect=[{ - 'id': '1234', + 'id': trade.orders[0].order_id, 'status': 'open', 'type': 'limit', 'side': 'buy', 'filled': filled_amount }, { - 'id': '1234', + 'id': trade.orders[0].order_id, 'status': 'closed', 'type': 'limit', 'side': 'buy', @@ -840,10 +849,12 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee, assert trade # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) + oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') + trade.update_trade(oobj) # Simulate fulfilled LIMIT_SELL order for trade - trade.update(limit_sell_order) + oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') + trade.update_trade(oobj) trade.close_date = datetime.utcnow() trade.is_open = False @@ -874,10 +885,12 @@ def test_buy_tag_performance_handle(default_conf, ticker, limit_buy_order, fee, assert trade # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) + oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') + trade.update_trade(oobj) # Simulate fulfilled LIMIT_SELL order for trade - trade.update(limit_sell_order) + oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') + trade.update_trade(oobj) trade.close_date = datetime.utcnow() trade.is_open = False @@ -946,10 +959,12 @@ def test_sell_reason_performance_handle(default_conf, ticker, limit_buy_order, f assert trade # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) + oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') + trade.update_trade(oobj) # Simulate fulfilled LIMIT_SELL order for trade - trade.update(limit_sell_order) + oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') + trade.update_trade(oobj) trade.close_date = datetime.utcnow() trade.is_open = False @@ -1018,10 +1033,12 @@ def test_mix_tag_performance_handle(default_conf, ticker, limit_buy_order, fee, assert trade # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) + oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') + trade.update_trade(oobj) # Simulate fulfilled LIMIT_SELL order for trade - trade.update(limit_sell_order) + oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') + trade.update_trade(oobj) trade.close_date = datetime.utcnow() trade.is_open = False diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 640f9305c..44107db81 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -418,10 +418,12 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, assert trade # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) + oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') + trade.update_trade(oobj) # Simulate fulfilled LIMIT_SELL order for trade - trade.update(limit_sell_order) + oobjs = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') + trade.update_trade(oobjs) trade.close_date = datetime.utcnow() trade.is_open = False @@ -461,8 +463,8 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, trades = Trade.query.all() for trade in trades: - trade.update(limit_buy_order) - trade.update(limit_sell_order) + trade.update_trade(oobj) + trade.update_trade(oobjs) trade.close_date = datetime.utcnow() trade.is_open = False @@ -527,10 +529,12 @@ def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee, assert trade # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) + oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') + trade.update_trade(oobj) # Simulate fulfilled LIMIT_SELL order for trade - trade.update(limit_sell_order) + oobjs = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') + trade.update_trade(oobjs) trade.close_date = datetime.utcnow() trade.is_open = False @@ -574,8 +578,8 @@ def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee, trades = Trade.query.all() for trade in trades: - trade.update(limit_buy_order) - trade.update(limit_sell_order) + trade.update_trade(oobj) + trade.update_trade(oobjs) trade.close_date = datetime.utcnow() trade.is_open = False @@ -643,10 +647,12 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee, assert trade # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) + oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') + trade.update_trade(oobj) # Simulate fulfilled LIMIT_SELL order for trade - trade.update(limit_sell_order) + oobjs = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') + trade.update_trade(oobjs) trade.close_date = datetime.utcnow() trade.is_open = False @@ -690,8 +696,8 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee, trades = Trade.query.all() for trade in trades: - trade.update(limit_buy_order) - trade.update(limit_sell_order) + trade.update_trade(oobj) + trade.update_trade(oobjs) trade.close_date = datetime.utcnow() trade.is_open = False @@ -761,7 +767,9 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, trade = Trade.query.first() # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) + oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') + trade.update_trade(oobj) + context = MagicMock() # Test with invalid 2nd argument (should silently pass) context.args = ["aaa"] @@ -776,7 +784,9 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, # Update the ticker with a market going up mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', ticker_sell_up) - trade.update(limit_sell_order) + # Simulate fulfilled LIMIT_SELL order for trade + oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') + trade.update_trade(oobj) trade.close_date = datetime.now(timezone.utc) trade.is_open = False @@ -1286,10 +1296,12 @@ def test_telegram_performance_handle(default_conf, update, ticker, fee, assert trade # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) + oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') + trade.update_trade(oobj) # Simulate fulfilled LIMIT_SELL order for trade - trade.update(limit_sell_order) + oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') + trade.update_trade(oobj) trade.close_date = datetime.utcnow() trade.is_open = False @@ -1313,13 +1325,15 @@ def test_telegram_buy_tag_performance_handle(default_conf, update, ticker, fee, freqtradebot.enter_positions() trade = Trade.query.first() assert trade + trade.buy_tag = "TESTBUY" # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) + oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') + trade.update_trade(oobj) - trade.buy_tag = "TESTBUY" # Simulate fulfilled LIMIT_SELL order for trade - trade.update(limit_sell_order) + oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') + trade.update_trade(oobj) trade.close_date = datetime.utcnow() trade.is_open = False @@ -1356,13 +1370,14 @@ def test_telegram_sell_reason_performance_handle(default_conf, update, ticker, f freqtradebot.enter_positions() trade = Trade.query.first() assert trade - - # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) - trade.sell_reason = 'TESTSELL' + # Simulate fulfilled LIMIT_BUY order for trade + oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') + trade.update_trade(oobj) + # Simulate fulfilled LIMIT_SELL order for trade - trade.update(limit_sell_order) + oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') + trade.update_trade(oobj) trade.close_date = datetime.utcnow() trade.is_open = False @@ -1399,15 +1414,16 @@ def test_telegram_mix_tag_performance_handle(default_conf, update, ticker, fee, freqtradebot.enter_positions() trade = Trade.query.first() assert trade - - # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) - trade.buy_tag = "TESTBUY" trade.sell_reason = "TESTSELL" + # Simulate fulfilled LIMIT_BUY order for trade + oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') + trade.update_trade(oobj) + # Simulate fulfilled LIMIT_SELL order for trade - trade.update(limit_sell_order) + oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') + trade.update_trade(oobj) trade.close_date = datetime.utcnow() trade.is_open = False diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 50d060a39..79f297bff 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -984,11 +984,17 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, trade = Trade.query.first() trade.is_open = True trade.open_order_id = None - trade.stoploss_order_id = 100 + trade.stoploss_order_id = "100" + trade.orders.append(Order( + ft_order_side='stoploss', + order_id='100', + ft_pair=trade.pair, + ft_is_open=True, + )) assert trade stoploss_order_hit = MagicMock(return_value={ - 'id': 100, + 'id': "100", 'status': 'closed', 'type': 'stop_loss_limit', 'price': 3, @@ -1634,9 +1640,9 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_buy_order_usdt, cap mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=limit_buy_order_usdt['amount']) - + order_id = limit_buy_order_usdt['id'] trade = Trade( - open_order_id=123, + open_order_id=order_id, fee_open=0.001, fee_close=0.001, open_rate=0.01, @@ -1644,29 +1650,35 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_buy_order_usdt, cap amount=11, exchange="binance", ) + trade.orders.append(Order( + ft_order_side='buy', + price=0.01, + order_id=order_id, + + )) assert not freqtrade.update_trade_state(trade, None) assert log_has_re(r'Orderid for trade .* is empty.', caplog) caplog.clear() # Add datetime explicitly since sqlalchemy defaults apply only once written to database - freqtrade.update_trade_state(trade, '123') + freqtrade.update_trade_state(trade, order_id) # Test amount not modified by fee-logic assert not log_has_re(r'Applying fee to .*', caplog) caplog.clear() assert trade.open_order_id is None assert trade.amount == limit_buy_order_usdt['amount'] - trade.open_order_id = '123' + trade.open_order_id = order_id mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=90.81) assert trade.amount != 90.81 # test amount modified by fee-logic - freqtrade.update_trade_state(trade, '123') + freqtrade.update_trade_state(trade, order_id) assert trade.amount == 90.81 assert trade.open_order_id is None trade.is_open = True trade.open_order_id = None # Assert we call handle_trade() if trade is feasible for execution - freqtrade.update_trade_state(trade, '123') + freqtrade.update_trade_state(trade, order_id) assert log_has_re('Found open order for.*', caplog) limit_buy_order_usdt_new = deepcopy(limit_buy_order_usdt) @@ -1675,7 +1687,7 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_buy_order_usdt, cap mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', side_effect=ValueError) mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order_usdt_new) - res = freqtrade.update_trade_state(trade, '123') + res = freqtrade.update_trade_state(trade, order_id) # Cancelled empty assert res is True @@ -1687,6 +1699,8 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_buy_order_usdt, cap def test_update_trade_state_withorderdict(default_conf_usdt, trades_for_order, limit_buy_order_usdt, fee, mocker, initial_amount, has_rounding_fee, caplog): trades_for_order[0]['amount'] = initial_amount + order_id = "oid_123456" + limit_buy_order_usdt['id'] = order_id mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) # fetch_order should not be called!! mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) @@ -1702,10 +1716,18 @@ def test_update_trade_state_withorderdict(default_conf_usdt, trades_for_order, l open_date=arrow.utcnow().datetime, fee_open=fee.return_value, fee_close=fee.return_value, - open_order_id="123456", + open_order_id=order_id, is_open=True, ) - freqtrade.update_trade_state(trade, '123456', limit_buy_order_usdt) + trade.orders.append( + Order( + ft_order_side='buy', + ft_pair=trade.pair, + ft_is_open=True, + order_id=order_id, + ) + ) + freqtrade.update_trade_state(trade, order_id, limit_buy_order_usdt) assert trade.amount != amount assert trade.amount == limit_buy_order_usdt['amount'] if has_rounding_fee: @@ -3362,7 +3384,8 @@ def test_trailing_stop_loss_positive( freqtrade.enter_positions() trade = Trade.query.first() - trade.update(limit_buy_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, limit_buy_order_usdt['symbol'], 'buy') + trade.update_trade(oobj) caplog.set_level(logging.DEBUG) # stop-loss not reached assert freqtrade.handle_trade(trade) is False From e9f451406c4209ee7a8903af97aeda5b62b0b132 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 11 Feb 2022 07:43:45 +0100 Subject: [PATCH 27/47] Use correct order id --- freqtrade/freqtradebot.py | 4 ++-- tests/test_freqtradebot.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 06fcb8a9a..f2b076532 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1358,10 +1358,10 @@ class FreqtradeBot(LoggingMixin): return True order = self.handle_order_fee(trade, order) - order_obj = trade.select_order_by_order_id(order['id']) + order_obj = trade.select_order_by_order_id(order_id) if not order_obj: # TODO: this can't happen! - raise OperationalException(f"order-obj for {order['id']} not found!") + raise OperationalException(f"order-obj for {order_id} not found!") 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() diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 79f297bff..3effce2f5 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1786,7 +1786,7 @@ def test_update_trade_state_sell(default_conf_usdt, trades_for_order, limit_sell fee_open=0.0025, fee_close=0.0025, open_date=arrow.utcnow().datetime, - open_order_id="123456", + open_order_id=limit_sell_order_usdt_open['id'], is_open=True, ) order = Order.parse_from_ccxt_object(limit_sell_order_usdt_open, 'LTC/ETH', 'sell') @@ -2016,7 +2016,7 @@ def test_bot_loop_start_called_once(mocker, default_conf_usdt, caplog): def test_check_handle_timedout_buy_usercustom(default_conf_usdt, ticker_usdt, limit_buy_order_old, open_trade, fee, mocker) -> None: default_conf_usdt["unfilledtimeout"] = {"buy": 1400, "sell": 30} - + limit_buy_order_old['id'] = open_trade.open_order_id rpc_mock = patch_RPCManager(mocker) cancel_order_mock = MagicMock(return_value=limit_buy_order_old) cancel_buy_order = deepcopy(limit_buy_order_old) From db540dc99078fce9ca48652f472ca43fba8cf0ee Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Feb 2022 14:21:22 +0100 Subject: [PATCH 28/47] Orders should also store fee if in receiving currency --- freqtrade/freqtradebot.py | 10 ++++++---- freqtrade/persistence/models.py | 16 +++++++++++++--- tests/test_freqtradebot.py | 10 ++++++++-- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f2b076532..e44193f21 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1357,8 +1357,8 @@ class FreqtradeBot(LoggingMixin): # Handling of this will happen in check_handle_timedout. return True - order = self.handle_order_fee(trade, order) order_obj = trade.select_order_by_order_id(order_id) + order = self.handle_order_fee(trade, order_obj, order) if not order_obj: # TODO: this can't happen! raise OperationalException(f"order-obj for {order_id} not found!") @@ -1415,14 +1415,16 @@ class FreqtradeBot(LoggingMixin): return real_amount return amount - def handle_order_fee(self, trade: Trade, order: Dict[str, Any]) -> Dict[str, Any]: + def handle_order_fee(self, trade: Trade, order_obj: Order, order: Dict[str, Any]) -> Dict[str, Any]: # Try update amount (binance-fix) try: new_amount = self.get_real_amount(trade, order) if not isclose(safe_value_fallback(order, 'filled', 'amount'), new_amount, abs_tol=constants.MATH_CLOSE_PREC): - order['amount'] = new_amount - order.pop('filled', None) + # TODO: ?? + # order['amount'] = new_amount + order_obj.ft_fee_base = trade.amount - new_amount + # order.pop('filled', None) except DependencyException as exception: logger.warning("Could not update trade amount: %s", exception) return order diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 731d57262..0ee50d1c7 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -132,6 +132,8 @@ class Order(_DECL_BASE): order_filled_date = Column(DateTime, nullable=True) order_update_date = Column(DateTime, nullable=True) + ft_fee_base = Column(Float, nullable=True) + @property def order_date_utc(self) -> datetime: """ Order-date with UTC timezoneinfo""" @@ -143,7 +145,15 @@ class Order(_DECL_BASE): @property def safe_filled(self) -> float: - return self.filled or self.amount + return self.filled or self.amount or 0.0 + + @property + def safe_fee_base(self) -> float: + return self.ft_fee_base or 0.0 + + @property + def safe_amount_after_fee(self) -> float: + return self.safe_filled - self.safe_fee_base def __repr__(self): @@ -477,7 +487,7 @@ class LocalTrade(): if order.ft_order_side == 'buy': # Update open rate and actual amount self.open_rate = order.safe_price - self.amount = order.safe_filled + self.amount = order.safe_amount_after_fee if self.is_open: logger.info(f'{order.order_type.upper()}_BUY has been fulfilled for {self}.') self.open_order_id = None @@ -637,7 +647,7 @@ class LocalTrade(): (o.status not in NON_OPEN_EXCHANGE_STATES)): continue - tmp_amount = o.amount + tmp_amount = o.safe_amount_after_fee tmp_price = o.average or o.price if o.filled is not None: tmp_amount = o.filled diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 3effce2f5..735a95231 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1729,9 +1729,14 @@ def test_update_trade_state_withorderdict(default_conf_usdt, trades_for_order, l ) freqtrade.update_trade_state(trade, order_id, limit_buy_order_usdt) assert trade.amount != amount - assert trade.amount == limit_buy_order_usdt['amount'] + log_text = r'Applying fee on amount for .*' if has_rounding_fee: - assert log_has_re(r'Applying fee on amount for .*', caplog) + assert pytest.approx(trade.amount) == 29.992 + assert log_has_re(log_text, caplog) + else: + assert pytest.approx(trade.amount) == limit_buy_order_usdt['amount'] + assert not log_has_re(log_text, caplog) + def test_update_trade_state_exception(mocker, default_conf_usdt, @@ -2319,6 +2324,7 @@ def test_check_handle_timedout_partial_fee(default_conf_usdt, ticker_usdt, open_ limit_buy_order_old_partial_canceled, mocker) -> None: rpc_mock = patch_RPCManager(mocker) limit_buy_order_old_partial['id'] = open_trade.open_order_id + limit_buy_order_old_partial_canceled['id'] = open_trade.open_order_id cancel_order_mock = MagicMock(return_value=limit_buy_order_old_partial_canceled) mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=0)) patch_exchange(mocker) From dc7bcf5dda0c94e6cab7e7943c8475522de9c462 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Feb 2022 14:27:26 +0100 Subject: [PATCH 29/47] Update failing test --- freqtrade/freqtradebot.py | 3 ++- freqtrade/persistence/models.py | 1 - tests/test_freqtradebot.py | 1 - tests/test_integration.py | 7 ++++++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e44193f21..8007d520e 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1415,7 +1415,8 @@ class FreqtradeBot(LoggingMixin): return real_amount return amount - def handle_order_fee(self, trade: Trade, order_obj: Order, order: Dict[str, Any]) -> Dict[str, Any]: + def handle_order_fee( + self, trade: Trade, order_obj: Order, order: Dict[str, Any]) -> Dict[str, Any]: # Try update amount (binance-fix) try: new_amount = self.get_real_amount(trade, order) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 0ee50d1c7..49ce34158 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -187,7 +187,6 @@ class Order(_DECL_BASE): if (order.get('filled', 0.0) or 0.0) > 0: self.order_filled_date = datetime.now(timezone.utc) self.order_update_date = datetime.now(timezone.utc) - return self def to_json(self) -> Dict[str, Any]: return { diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 735a95231..d7b47174b 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1738,7 +1738,6 @@ def test_update_trade_state_withorderdict(default_conf_usdt, trades_for_order, l assert not log_has_re(log_text, caplog) - def test_update_trade_state_exception(mocker, default_conf_usdt, limit_buy_order_usdt, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) diff --git a/tests/test_integration.py b/tests/test_integration.py index ed38f1fec..db3b1b5fc 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -4,6 +4,7 @@ import pytest from freqtrade.enums import SellType from freqtrade.persistence import Trade +from freqtrade.persistence.models import Order from freqtrade.rpc.rpc import RPC from freqtrade.strategy.interface import SellCheckTuple from tests.conftest import get_patched_freqtradebot, patch_get_signal @@ -94,7 +95,11 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee, trades = Trade.query.all() # Make sure stoploss-order is open and trade is bought (since we mock update_trade_state) for trade in trades: - trade.stoploss_order_id = 3 + stoploss_order_closed['id'] = '3' + oobj = Order.parse_from_ccxt_object(stoploss_order_closed, trade.pair, 'stoploss') + + trade.orders.append(oobj) + trade.stoploss_order_id = '3' trade.open_order_id = None n = freqtrade.exit_positions(trades) From 6fb5b22a8e6acd41fb1a9eeb8fc2e6a4fd7d4bd8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Feb 2022 15:36:25 +0100 Subject: [PATCH 30/47] Some cleanup --- freqtrade/freqtradebot.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 8007d520e..e43f2a158 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -16,7 +16,7 @@ from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge from freqtrade.enums import RPCMessageType, RunMode, SellType, State from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, - InvalidOrderException, OperationalException, PricingError) + InvalidOrderException, PricingError) from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.mixins import LoggingMixin @@ -1358,10 +1358,8 @@ class FreqtradeBot(LoggingMixin): return True order_obj = trade.select_order_by_order_id(order_id) - order = self.handle_order_fee(trade, order_obj, order) - if not order_obj: - # TODO: this can't happen! - raise OperationalException(f"order-obj for {order_id} not found!") + 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() @@ -1415,20 +1413,15 @@ class FreqtradeBot(LoggingMixin): return real_amount return amount - def handle_order_fee( - self, trade: Trade, order_obj: Order, order: Dict[str, Any]) -> Dict[str, Any]: + def handle_order_fee(self, trade: Trade, order_obj: Order, order: Dict[str, Any]) -> None: # Try update amount (binance-fix) try: new_amount = self.get_real_amount(trade, order) if not isclose(safe_value_fallback(order, 'filled', 'amount'), new_amount, abs_tol=constants.MATH_CLOSE_PREC): - # TODO: ?? - # order['amount'] = new_amount order_obj.ft_fee_base = trade.amount - new_amount - # order.pop('filled', None) except DependencyException as exception: logger.warning("Could not update trade amount: %s", exception) - return order def get_real_amount(self, trade: Trade, order: Dict) -> float: """ From a24586cd41211b8dc99a9f70d3bc8d1c71a86d93 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Feb 2022 16:32:04 +0100 Subject: [PATCH 31/47] Update migrations for new column --- freqtrade/persistence/migrations.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 60c0eb5f9..288345e18 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -57,7 +57,7 @@ def set_sequence_ids(engine, order_id, trade_id): def migrate_trades_and_orders_table( decl_base, inspector, engine, trade_back_name: str, cols: List, - order_back_name: str): + order_back_name: str, cols_order: List): fee_open = get_column_def(cols, 'fee_open', 'fee') fee_open_cost = get_column_def(cols, 'fee_open_cost', 'null') fee_open_currency = get_column_def(cols, 'fee_open_currency', 'null') @@ -141,7 +141,7 @@ def migrate_trades_and_orders_table( from {trade_back_name} """)) - migrate_orders_table(engine, order_back_name, cols) + migrate_orders_table(engine, order_back_name, cols_order) set_sequence_ids(engine, order_id, trade_id) @@ -171,17 +171,19 @@ def drop_orders_table(engine, table_back_name: str): connection.execute(text("drop table orders")) -def migrate_orders_table(engine, table_back_name: str, cols: List): +def migrate_orders_table(engine, table_back_name: str, cols_order: List): + + ft_fee_base = get_column_def(cols_order, 'ft_fee_base', 'null') # let SQLAlchemy create the schema as required with engine.begin() as connection: connection.execute(text(f""" insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, symbol, order_type, side, price, amount, filled, average, remaining, cost, - order_date, order_filled_date, order_update_date) + order_date, order_filled_date, order_update_date, ft_fee_base) select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, symbol, order_type, side, price, amount, filled, null average, remaining, cost, - order_date, order_filled_date, order_update_date + order_date, order_filled_date, order_update_date, {ft_fee_base} from {table_back_name} """)) @@ -193,6 +195,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None: inspector = inspect(engine) cols = inspector.get_columns('trades') + cols_orders = inspector.get_columns('orders') tabs = get_table_names_for_table(inspector, 'trades') table_back_name = get_backup_name(tabs, 'trades_bak') order_tabs = get_table_names_for_table(inspector, 'orders') @@ -200,11 +203,12 @@ def check_migrate(engine, decl_base, previous_tables) -> None: # Check if migration necessary # Migrates both trades and orders table! - if not has_column(cols, 'buy_tag'): + # if not has_column(cols, 'buy_tag'): + if 'orders' not in previous_tables or not has_column(cols_orders, 'ft_fee_base'): logger.info(f"Running database migration for trades - " f"backup: {table_back_name}, {order_table_bak_name}") migrate_trades_and_orders_table( - decl_base, inspector, engine, table_back_name, cols, order_table_bak_name) + decl_base, inspector, engine, table_back_name, cols, order_table_bak_name, cols_orders) # Reread columns - the above recreated the table! inspector = inspect(engine) cols = inspector.get_columns('trades') From fddacfedaae87fcf3c0b5e4ac5bba76d5cd7d50b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Feb 2022 16:34:35 +0100 Subject: [PATCH 32/47] Remove returntype --- freqtrade/freqtradebot.py | 3 +++ freqtrade/persistence/migrations.py | 3 --- freqtrade/persistence/models.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e43f2a158..99872ff0b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1358,6 +1358,9 @@ class FreqtradeBot(LoggingMixin): return True 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.") self.handle_order_fee(trade, order_obj, order) trade.update_trade(order_obj) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 288345e18..e4ff4bc37 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -209,9 +209,6 @@ def check_migrate(engine, decl_base, previous_tables) -> None: f"backup: {table_back_name}, {order_table_bak_name}") migrate_trades_and_orders_table( decl_base, inspector, engine, table_back_name, cols, order_table_bak_name, cols_orders) - # Reread columns - the above recreated the table! - inspector = inspect(engine) - cols = inspector.get_columns('trades') if 'orders' not in previous_tables and 'trades' in previous_tables: logger.info('Moving open orders to Orders table.') diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 49ce34158..d12a345e0 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -160,7 +160,7 @@ class Order(_DECL_BASE): return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, ' f'side={self.side}, order_type={self.order_type}, status={self.status})') - def update_from_ccxt_object(self, order) -> 'Order': + def update_from_ccxt_object(self, order): """ Update Order from ccxt response Only updates if fields are available from ccxt - From 21b5f56f7d031a45b6ffb79ef4651c7cb02bbf81 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Feb 2022 03:01:39 +0000 Subject: [PATCH 33/47] Bump types-requests from 2.27.9 to 2.27.10 Bumps [types-requests](https://github.com/python/typeshed) from 2.27.9 to 2.27.10. - [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 3a2fa71e0..c52032a60 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -22,7 +22,7 @@ nbconvert==6.4.2 # mypy types types-cachetools==4.2.9 types-filelock==3.2.5 -types-requests==2.27.9 +types-requests==2.27.10 types-tabulate==0.8.5 # Extensions to datetime library From d1cded3532e96c6e76703f4e73c8055cb3b0b4f7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Feb 2022 03:01:45 +0000 Subject: [PATCH 34/47] Bump filelock from 3.4.2 to 3.6.0 Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.4.2 to 3.6.0. - [Release notes](https://github.com/tox-dev/py-filelock/releases) - [Changelog](https://github.com/tox-dev/py-filelock/blob/main/docs/changelog.rst) - [Commits](https://github.com/tox-dev/py-filelock/compare/3.4.2...3.6.0) --- updated-dependencies: - dependency-name: filelock 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 e4698918a..aeb7be035 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -5,6 +5,6 @@ scipy==1.8.0 scikit-learn==1.0.2 scikit-optimize==0.9.0 -filelock==3.4.2 +filelock==3.6.0 joblib==1.1.0 progressbar2==4.0.0 From dc8e9bab44c94c971bf8409082b54d95981298f8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Feb 2022 03:01:51 +0000 Subject: [PATCH 35/47] Bump fastapi from 0.73.0 to 0.74.0 Bumps [fastapi](https://github.com/tiangolo/fastapi) from 0.73.0 to 0.74.0. - [Release notes](https://github.com/tiangolo/fastapi/releases) - [Commits](https://github.com/tiangolo/fastapi/compare/0.73.0...0.74.0) --- updated-dependencies: - dependency-name: fastapi 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 eaf257ca8..5b4be4f37 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,7 +31,7 @@ python-rapidjson==1.5 sdnotify==0.3.2 # API Server -fastapi==0.73.0 +fastapi==0.74.0 uvicorn==0.17.4 pyjwt==2.3.0 aiofiles==0.8.0 From 317487fefc8cea707d1767466a032af40929ed72 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Feb 2022 03:01:58 +0000 Subject: [PATCH 36/47] Bump ccxt from 1.72.98 to 1.73.70 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.72.98 to 1.73.70. - [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.72.98...1.73.70) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index eaf257ca8..b396b10ac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.22.2 pandas==1.4.1 pandas-ta==0.3.14b -ccxt==1.72.98 +ccxt==1.73.70 # Pin cryptography for now due to rust build errors with piwheels cryptography==36.0.1 aiohttp==3.8.1 From d354f1f84c22addbba79a2f39290f91ab447b932 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Feb 2022 03:02:06 +0000 Subject: [PATCH 37/47] Bump mkdocs-material from 8.1.11 to 8.2.1 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.1.11 to 8.2.1. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/8.1.11...8.2.1) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index d3772ebd7..3e7fa2044 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,4 +1,4 @@ mkdocs==1.2.3 -mkdocs-material==8.1.11 +mkdocs-material==8.2.1 mdx_truly_sane_lists==1.2 pymdown-extensions==9.2 From 7b6a0f7a19bb65ecb0d77a41fb808ed865f88404 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Feb 2022 05:28:52 +0000 Subject: [PATCH 38/47] Bump uvicorn from 0.17.4 to 0.17.5 Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.17.4 to 0.17.5. - [Release notes](https://github.com/encode/uvicorn/releases) - [Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md) - [Commits](https://github.com/encode/uvicorn/compare/0.17.4...0.17.5) --- updated-dependencies: - dependency-name: uvicorn 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 5b4be4f37..7fa7db899 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,7 +32,7 @@ sdnotify==0.3.2 # API Server fastapi==0.74.0 -uvicorn==0.17.4 +uvicorn==0.17.5 pyjwt==2.3.0 aiofiles==0.8.0 psutil==5.9.0 From b9a99bd0b73a25ec59f3bf696a406fe05b2b5386 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Feb 2022 05:28:53 +0000 Subject: [PATCH 39/47] Bump python-rapidjson from 1.5 to 1.6 Bumps [python-rapidjson](https://github.com/python-rapidjson/python-rapidjson) from 1.5 to 1.6. - [Release notes](https://github.com/python-rapidjson/python-rapidjson/releases) - [Changelog](https://github.com/python-rapidjson/python-rapidjson/blob/master/CHANGES.rst) - [Commits](https://github.com/python-rapidjson/python-rapidjson/compare/v1.5...v1.6) --- updated-dependencies: - dependency-name: python-rapidjson 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 5b4be4f37..7f0c3cb6b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,7 +25,7 @@ blosc==1.10.6 py_find_1st==1.1.5 # Load ticker files 30% faster -python-rapidjson==1.5 +python-rapidjson==1.6 # Notify systemd sdnotify==0.3.2 From 02ce0dc02ef85db0485af262f81bce768c89091a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 22 Feb 2022 19:31:58 +0100 Subject: [PATCH 40/47] Set journal mode to wal for sqlite databases closes #6353 --- freqtrade/persistence/migrations.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 60c0eb5f9..5817c5a97 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -186,6 +186,13 @@ def migrate_orders_table(engine, table_back_name: str, cols: List): """)) +def set_sqlite_to_wal(engine): + if engine.name == 'sqlite' and str(engine.url) != 'sqlite://': + # Set Mode to + with engine.begin() as connection: + connection.execute(text("PRAGMA journal_mode=wal")) + + def check_migrate(engine, decl_base, previous_tables) -> None: """ Checks if migration is necessary and migrates if necessary @@ -212,3 +219,4 @@ def check_migrate(engine, decl_base, previous_tables) -> None: if 'orders' not in previous_tables and 'trades' in previous_tables: logger.info('Moving open orders to Orders table.') migrate_open_orders_to_trades(engine) + set_sqlite_to_wal(engine) From 1f9ed0beff5256bbc881d4df2989381fb36f563f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 22 Feb 2022 19:39:55 +0100 Subject: [PATCH 41/47] Add test for wal mode --- freqtrade/persistence/models.py | 3 +++ tests/test_persistence.py | 11 ++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index b8ea5848f..5eddb7b3d 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -39,6 +39,9 @@ def init_db(db_url: str, clean_open_orders: bool = False) -> None: """ kwargs = {} + if db_url == 'sqlite:///': + raise OperationalException( + f'Bad db-url {db_url}. For in-memory database, please use `sqlite://`.') if db_url == 'sqlite://': kwargs.update({ 'poolclass': StaticPool, diff --git a/tests/test_persistence.py b/tests/test_persistence.py index b8f7a3336..b2dccc75c 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -33,13 +33,18 @@ def test_init_custom_db_url(default_conf, tmpdir): init_db(default_conf['db_url'], default_conf['dry_run']) assert Path(filename).is_file() + r = Trade._session.execute(text("PRAGMA journal_mode")) + assert r.first() == ('wal',) -def test_init_invalid_db_url(default_conf): +def test_init_invalid_db_url(): # Update path to a value other than default, but still in-memory - default_conf.update({'db_url': 'unknown:///some.url'}) with pytest.raises(OperationalException, match=r'.*no valid database URL*'): - init_db(default_conf['db_url'], default_conf['dry_run']) + init_db('unknown:///some.url', True) + + with pytest.raises(OperationalException, match=r'Bad db-url.*For in-memory database, pl.*'): + init_db('sqlite:///', True) + def test_init_prod_db(default_conf, mocker): From 5a4f30d1bda54af561a9fc089b9f4a7a1ee0181a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 22 Feb 2022 20:07:41 +0100 Subject: [PATCH 42/47] Don't specially handle empty results. --- freqtrade/rpc/rpc.py | 5 ----- tests/rpc/test_rpc_telegram.py | 2 +- tests/test_persistence.py | 1 - 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 5912a0ecd..7a602978e 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -599,11 +599,6 @@ class RPC: 'est_stake': est_stake or 0, 'stake': stake_currency, }) - if total == 0.0: - if self._freqtrade.config['dry_run']: - raise RPCException('Running in Dry Run, balances are not available.') - else: - raise RPCException('All balances are zero.') value = self._fiat_converter.convert_amount( total, stake_currency, fiat_display_currency) if self._fiat_converter else 0 diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 640f9305c..353aa959f 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -861,7 +861,7 @@ def test_balance_handle_empty_response(default_conf, update, mocker) -> None: telegram._balance(update=update, context=MagicMock()) result = msg_mock.call_args_list[0][0][0] assert msg_mock.call_count == 1 - assert 'All balances are zero.' in result + assert 'Starting capital: `0 BTC' in result def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None: diff --git a/tests/test_persistence.py b/tests/test_persistence.py index b2dccc75c..f695aab8b 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -46,7 +46,6 @@ def test_init_invalid_db_url(): init_db('sqlite:///', True) - def test_init_prod_db(default_conf, mocker): default_conf.update({'dry_run': False}) default_conf.update({'db_url': constants.DEFAULT_DB_PROD_URL}) From 731eb99713b8a7803b8496a2a6c44a9d92b7df32 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Feb 2022 19:17:50 +0100 Subject: [PATCH 43/47] Update mock-trade creation to rollback first --- tests/conftest.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 630223d55..8f1e75439 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -201,6 +201,9 @@ def create_mock_trades(fee, use_db: bool = True): """ Create some fake trades ... """ + if use_db: + Trade.query.session.rollback() + def add_trade(trade): if use_db: Trade.query.session.add(trade) From 42df65d4ec20196f1903e4ec4b7f9310f04ce39b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 24 Feb 2022 06:25:21 +0100 Subject: [PATCH 44/47] Make sure backtesting is cleaned up in tests --- freqtrade/optimize/backtesting.py | 3 ++- tests/optimize/test_backtesting.py | 11 ++++++++--- tests/rpc/test_rpc_apiserver.py | 1 + 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index b423771ca..eca643732 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -128,7 +128,8 @@ class Backtesting: def __del__(self): self.cleanup() - def cleanup(self): + @staticmethod + def cleanup(): LoggingMixin.show_output = True PairLocks.use_db = True Trade.use_db = True diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index d61dffac4..a8998eb63 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -52,6 +52,13 @@ def trim_dictlist(dict_list, num): return new +@pytest.fixture(autouse=True) +def backtesting_cleanup() -> None: + yield None + + Backtesting.cleanup() + + def load_data_test(what, testdatadir): timerange = TimeRange.parse_timerange('1510694220-1510700340') data = history.load_pair_history(pair='UNITTEST/BTC', datadir=testdatadir, @@ -553,8 +560,6 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None: trade = backtesting._enter_trade(pair, row=row) assert trade is None - backtesting.cleanup() - def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None: default_conf['use_sell_signal'] = False @@ -1423,7 +1428,7 @@ def test_get_strategy_run_id(default_conf_usdt): default_conf_usdt.update({ 'strategy': 'StrategyTestV2', 'max_open_trades': float('inf') - }) + }) strategy = StrategyResolver.load_strategy(default_conf_usdt) x = get_strategy_run_id(strategy) assert isinstance(x, str) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 5b19e5e05..544321860 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1108,6 +1108,7 @@ def test_api_forcesell(botclient, mocker, ticker, fee, markets): data='{"tradeid": "1"}') assert_response(rc, 502) assert rc.json() == {"error": "Error querying /api/v1/forcesell: invalid argument"} + Trade.query.session.rollback() ftbot.enter_positions() From 3b1b66bee8a1bf3f58080a4ab74406e901834718 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Feb 2022 07:40:49 +0100 Subject: [PATCH 45/47] Prevent backtest starting when not in webserver mode #6455 --- freqtrade/rpc/api_server/api_backtest.py | 10 +++++----- freqtrade/rpc/api_server/deps.py | 7 +++++++ tests/rpc/test_rpc_apiserver.py | 5 +++++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/api_server/api_backtest.py b/freqtrade/rpc/api_server/api_backtest.py index 8b86b8005..757ed8aac 100644 --- a/freqtrade/rpc/api_server/api_backtest.py +++ b/freqtrade/rpc/api_server/api_backtest.py @@ -8,7 +8,7 @@ from freqtrade.configuration.config_validation import validate_config_consistenc from freqtrade.enums import BacktestState from freqtrade.exceptions import DependencyException from freqtrade.rpc.api_server.api_schemas import BacktestRequest, BacktestResponse -from freqtrade.rpc.api_server.deps import get_config +from freqtrade.rpc.api_server.deps import get_config, is_webserver_mode from freqtrade.rpc.api_server.webserver import ApiServer from freqtrade.rpc.rpc import RPCException @@ -22,7 +22,7 @@ router = APIRouter() @router.post('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest']) # flake8: noqa: C901 async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: BackgroundTasks, - config=Depends(get_config)): + config=Depends(get_config), ws_mode=Depends(is_webserver_mode)): """Start backtesting if not done so already""" if ApiServer._bgtask_running: raise RPCException('Bot Background task already running') @@ -121,7 +121,7 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac @router.get('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest']) -def api_get_backtest(): +def api_get_backtest(ws_mode=Depends(is_webserver_mode)): """ Get backtesting result. Returns Result after backtesting has been ran. @@ -157,7 +157,7 @@ def api_get_backtest(): @router.delete('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest']) -def api_delete_backtest(): +def api_delete_backtest(ws_mode=Depends(is_webserver_mode)): """Reset backtesting""" if ApiServer._bgtask_running: return { @@ -183,7 +183,7 @@ def api_delete_backtest(): @router.get('/backtest/abort', response_model=BacktestResponse, tags=['webserver', 'backtest']) -def api_backtest_abort(): +def api_backtest_abort(ws_mode=Depends(is_webserver_mode)): if not ApiServer._bgtask_running: return { "status": "not_running", diff --git a/freqtrade/rpc/api_server/deps.py b/freqtrade/rpc/api_server/deps.py index b428d9c6d..f5e61602e 100644 --- a/freqtrade/rpc/api_server/deps.py +++ b/freqtrade/rpc/api_server/deps.py @@ -2,6 +2,7 @@ from typing import Any, Dict, Iterator, Optional from fastapi import Depends +from freqtrade.enums import RunMode from freqtrade.persistence import Trade from freqtrade.rpc.rpc import RPC, RPCException @@ -38,3 +39,9 @@ def get_exchange(config=Depends(get_config)): ApiServer._exchange = ExchangeResolver.load_exchange( config['exchange']['name'], config) return ApiServer._exchange + + +def is_webserver_mode(config=Depends(get_config)): + if config['runmode'] != RunMode.WEBSERVER: + raise RPCException('Bot is not in the correct state') + return None diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 544321860..de7dca47b 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1350,6 +1350,11 @@ def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir): ftbot, client = botclient mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + rc = client_get(client, f"{BASE_URI}/backtest") + # Backtest prevented in default mode + assert_response(rc, 502) + + ftbot.config['runmode'] = RunMode.WEBSERVER # Backtesting not started yet rc = client_get(client, f"{BASE_URI}/backtest") assert_response(rc) From 4b7271df467e7ea4ddedd79543fc7f26f1fdea2d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Feb 2022 10:53:12 +0100 Subject: [PATCH 46/47] Improve wording, add Picture detailing what must be installed. --- docs/assets/windows_install.png | Bin 0 -> 94230 bytes docs/windows_installation.md | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 docs/assets/windows_install.png diff --git a/docs/assets/windows_install.png b/docs/assets/windows_install.png new file mode 100644 index 0000000000000000000000000000000000000000..530c3047f5016a2213cbbe2fd60ce230b36d18fd GIT binary patch literal 94230 zcmb@u1xy@W{3tp&6fN%3;_fbm7AR6&ibHXyxI@uGaf&+>cXu!DUSt<{+2Rg&`}O<& z_vPg#FE6>1>}+QC%$ak3t{bMLAdUWl_yqs}=rZr$sQ>`{8UVm5Bf~=P7-t!bLBCKO z-fKAl0LIThAD9Ft3=#mK1Z3WctGVkRf$W1`YpsesT|b!ck1<=C$u!-IngB{_AbxawI4uGCu7*FLA2ModTKCT^$QwA_6Ae&A{1n zf-DqeI2`w974HCNL?YZ;3Agge9CS7gTX`MxI%LCcC4OOw7l<(6dY0A^kb1y^$+osd z8;U~tZ)cyv%b!&(ft|n9seE5!zZhRx@i4lMg6R^lY0?S)k?i2obaU2*MV#KWJ{0u3 zaqd@-L$FMgDRDV#LB*zR@^0OElEUubNynO@_&Vc}p8z27QZlm*T+2!WIQ*GeABq4& zaq*AHh?VRY%`?Zpr1ijV1RVPXj%Tz2Aj^9ZtD~n%bV}T?F*jlj0%k&Ncq(^fu(t0Q01uB_lA{4|n z9gh>Murzcp2TMR6gh~~JC4o$3-*bO-OAas516zwJu;wfng)96r=Sql-Z_Oy+H{zF2 zV=X(It{=(RT7>rW%(4Lg?$Hs<`eIe7#Fo^%-|*o9lrpZp?pAr21A&3EzxPd&WoLWFU-%9d8i#BGdCg>z4uy(}rMjK~lwg+0+y%-k76di-cF#b9d5 z^t{Bysw2P1;qj>DPV~H)Y^-mJ=_bf3TZcyUbGsVtc=}<%6%k3DI8k#4S9E~~2;yhaY&us1OgilC?-4{Mn7UYOA%TKVJmmI<`; z6u2(YI%+D%jo^#^mc)Ij!XvR)j=IFsIRRK<4u{24H3BUf=SshQCfR@5j_eq!$S2|x zqO$wW*04FX9WIdl1GL++KSZpoK4&10{1=Kr-fvnep2eOxp%DECtXsy}K*AsRLazpc z-@q{bJ1z$G3#{6Ig9u;|ivAf5J+Z>SP(t~4kS;QAHuk@RJO<9hc5Iigl3+muAYIm7 zHZnm2e=F-o?3QgdktAjHJcEc8O-RX^Dv#_G#LdG`*AP-cBg8erw5BT9*7JwziOBWxAA!71!0(iWT^^+v?ygn7IbC-ZGH39| z%xidmJk(;$nj9W5XmrfO7125_`mOIWi+v3wiJ;p(aDN=paraHk#KO5-HyP5*r4L{K z?z@4vLr8M!4o>E8isy;AAz1WeWDd{0nTxpmySH2JVz*fv-otOot!ynNB?U8<_Bji; zu(N!|HeZ`lI~{oQZw5LTZ{=3olgl2=h}n?*KL6T@k7AaS{|LJEp(9HFcwNpdTa2W# zy_OBKTNXo1o9Hq+yvEL-Cy^?VIEz3R3W-@1vS?h6MjhN8$jLCUX}=XaeA)l7JtN6` zct6L2**R<9RAYs`qg^1o z$AUSK#FJUEO5Mpm{I^z_O345gy3FUoeh)y zhmM~l+Bgoha#h=0{t_U^dw+RLI~zl52EI*|(K$n#uZsps?Q|iksR)E5fs!X#GlT*h z<`KTd+fwSyWk$zg_je9sPT?T=RwGQH)rkKyO;0jkKCcf5G^ah6BJV-=D5@Z@mpSJ~ z_w)N&pqguPnNF;N>EqE6=JRwd+K;~`>Ms+Gr8G+68Woy*%Z({?kd$6FA` z$i{4wt~OR=zk4KqwUA;w=#&9l8i~IhtIOoB+J5TeLIXZLLsyIR2XR8zk0J2;gQ7&1 zPQMyNlr5I8M#(qN_ZIaSR)(8w6+nK4KItOMFW&c1wq;<-RUMyf5&vaU?3IUVyTfzeQY}?G zD5Yh^C)}qZWGBjfhi|<4v|4nx;PVk z%!{F6h}Zh3S@cgtL0c2|X-q0+knBe%&aVt)C7E?|u6Ox%XJIZ!p!#=`4o)EhDAnXn zRrRw_h99D{R8LWPW-iZC@xaA+vY@DQ>nRWCYBmsSP3_2+q#&(ZK6dqD`zW8F?d`da zWv%v&+3&YBbQej5Su+~Gp!cgXHU56xYG@KiW-Htuz(}DWca`&N;JI zPz3}T(~(={`#-WShL7{Qj}-O|Mfch)mSK+$Qjt%w(%^f5S}XP_3W52(3Xf2&pELie z{1w5kP_B6*xx_|1;A89z%Q9lwTcMf?MgU;QqisLDDxY3OB6H*NNdSwS*>Me?Wu&f> zXn##5!8x6i*<|gmn-@4rKiVJ(x0zo)9!S~A;6fLv7ZRywCnO`j=VhxMQjnNXA(t~# z_)#C#pY1vv;A(89nU5K2OEK7YOpS_GV-*;Ge|fm1%Tj?UrG_zW?Xi%m!Y{}|CC+&s z<~N=^T%;Txu0886yz(I5Q<`?`>$}hNSw{Ep-SkTtLWD*TlI1hHgTzKwWjo6212P(9 zO56lfg^cvSWReb|uU+6HcS)>uQEPuG1A2a2U-zB9Ax^CE8&jraVrJ9a@WYe_ASbsi zA~!xLYK2nUABOnj^4YTTUe4gPD_4exMO~9X7VBLBDz=7OYN_j`zFJw|x3P>cLqed- zh~ygkGQsnHm-5I;@VZ4w`aM2xM46@Ot3p4=$!bgi6Tf`{2P*7uhaJ?vQtm*3Z|M%$ zdD6xI#vrzg)PxbQiL4(l|}cAOiIi)*iynqocj(OVsapticyro0>omH z$9G80P`Ad`GN7SpND00S@Ld5V^nl&A{J&rI;iyab){c zsRIYxg5-k8P`mIXpv6NM`juR3%sLJL8Z5{6q=XV)VLJK1$jz+s_7T`6R%ZGQns~@g z?jedLw0@Xz#=_3sF`QrRj}Px9f&~Sx_6BqTztD{}CWA$rHu&BCXC7bs1u7vjg<@w}ax8RUr`H1}&ED-I9{r8?CZ=vRps_sh4D|===Q|=@*|b? zN8Tq4rMMx=%`ytrT8UTnKVYW$hKe7(fFJg%b2o8&W+4^0n8Pm-2l;x#X5$)Egz4!N zHL&$Tqhd15J_&HyWtAf~uZW>!c9CacC4&SE_U%Nm(YRt zgyE0bBd6bsV6ZsFegw{6%ppQas)LN|2wApT>nppV)w)t1?gwj(KQlDde($cvEcL7t zeCW`J&e2IOV`oPgR+J1EGz#9_H`W+%A(tcCoN{1XbCYf<-R1jUlq~sXX=OF(?cURg z!ERf?@HGNgGxHOCP@K?Q7^Lc7lPX?gg*7uBZzxjvTx^F4lM#FtD<>o*R8n&HO$;S5 z^VeFRbc&N?%U>>pT2xqi3#*#p5F|t4U!Rz2<8=Nrue?8j--biHO!vcdbP|PJPnR9j zJQe@_ajSAe6;+AG zYD?n(lKIz+#i{|-wqd_n#?A;Pn8o;B(}fH48hDB?E|oh-)WQH#c19QX&JBskY@__V za*E8<7w1{huN$$(=N|HdyqzHrEE}%`XR%RHj(`7zn44OLIk+3&dG&gv7oyvrD9KGO zV?d#MCLo~bZ?*OAxi&kW?OTUAvQ8`BlXE~9=6bDr2rYyd#G3_w==&-?d*t08jz4P{ z$H``7lHsW(U`RncAky})_GkBH9UW}It zqQ=6J{5 z+r0S*{b+JA?tzJqJhcIJ@``x>A$WX87lx6V>fo5I{>Y3wMMlcVC4^$0r2b&V*5gL2 zJyr)f6h!fPkqdE}Y{p{iVH;S{4ysY6)HXOD5?D!K0v4)1W2oPi?67AL>`Oiqi-U1i{0r{wZx~5sG=c zZ)oqnu3VAio78JG@txY|xK%e#*IKIZ1t*iT4B!Y5`)i1?grr{rcV435x>hyU) zFPn>qBu_9jX-!Fvc`$f&ig~bC)|;#ZN9T5g4^5F|+#TFjxFp8wveZ$aOAN&@GeN2a z{+1EVQ)~qoXRmz~QJN1N%eT37uh=g{8vIAx$6T*@!<1x~y^F>??iPFIsl`^`kkt+2 zEiJhBwp20^NofVSK7*{SlccSj(Jf+uF{%~6~V=1PQ|Cx%}=T@Jl+--?2 zPTiK}Wyb>Jte{=+-6t7x5izY6hGR_EAB9ZUQkXMP%3cW>2q+L0CRa1xN0}p@FQ8k@ zzIkhbkIm!ZfGZ=^$>E(|bFuwS^z3{M;JVVz5oo3YWUkU}frbM0ToR(yp_=-4-a6AD zoN4{Wlby7r+bu>_B5gV@{>XY*wmt))w_&k3146_Z{o1T;oT4#zA^Dy?B)-ilof9L+ z3cE#_(HgVC4j*ww^SZMZ!=yLDTMUOYuh4o4q1>T?xiGfwn}6KIrz_xFWTt$QUNke0 zeVWSot3<;BM6w$5;K?s<)C)K`L@X_z*ve{alwOVtM>2jc? zggO~(O})5Bvl&X7=r-S>*3YSPt*7iqR88|A(8c98}*cNs%GtEH=`H3Hr)g=xVMq)I#E zx2(D(y0BbqFiH(VWFqK4F7w9O1y(1I*ZX+R=?5!4V92cs3^C>xX7G4&$XZgW&7K5v zJ!+k17dsDqyn;!_izm)D7Zk)ZzpgM^Ve~;U>twQ1sX4oV=n|^^Nxc3n&+I*d$3;!* zN|M(vf0tEUc{fO)hK|3pTc`0(-q)Qmgl7RZTFT@q3(xgDJK%Pd14leckVc9w@E@mLnk~>dE z3ozW%aaEzfOw(mJXxwefQ&$X8w+rf@Kb+|&BU*ChdOYV(RS)0(+)@1lUm8_o>mmzj z{w4cY@L8V`NziDAua{lbQtVt1F1gJQT-2wAHpTrj_6LpGvD4ex#-ksig0A^D{1jvl zCsYv=ytE2Up*gmEGk5eqHN)#AE+%S!s$`y14h>mXVJIHIt9L%mQ7iIrRZ|M#IKO%I zBE|7(shhsY%ufn+nP^BaPG<8g?s_83`E*2XbC|}(+{b;5r+iZWFnXmNpqKP<>^+sd zp0~7ECOmam7Ufh$r(b=oRs+iqu=3$qN^Pt<(K3g zuL@7^4|Qxo3ycfrJ2zV<;kOp&noi!X(|K+Rl4LA3$F$T&RcsT@f7It-u=6-DHxp={ zj2R6Yih&m(vR&deOq(-P3zKtbRq;`!VPG2i3}Nbzj;;`}Gr{p_(9aUZic0@B^TI;GT<-#a$zF`!$%?n5!}t!n2*~*fMVSovNAhAV|!^ zXW-~9>R5oVY{^9%;iH+ZF7TTgCAxCzA`LzHX+FnR?jY6VW9hMxo>`8II}IaTJpD@O*Id*oEV(PUPr)d z@ghE)xw99uJyPV$aC~s$jDK(uJ+nQBsf;ZLy$DrD8~>#xw9et?l^iPYkdlbSp~FI; zPe_Lntx9f|b~jIDawQb_1)z8>FJhVhOW+{cV?=0UcAy2)-emUF;)g(C{F*coeH%y4 zh{qAt=yt)V;PKwJ{`(>XZ917j{tu20ZoQRKbO<;djxxi0?@8HES;0NQcB788*CoEG- zF*n3^lp?Do!)qO4nj+=osmRbN>`@U_7RFHomVX1q_0f|VNqqqy&-Eq#4`tt)P-^*z zxL}=gLz6Qd!m&;oW?3ARcEHNaG~Gj}_q)#Cis-!U`OX3|uqLF}@o_wjT3FvCMT+Y{ zruZ_otz%QiVkuuBZK{vQYOZ-XGY97Nj6E+<*zQ8#=US+d+`NuN2LeGz_qnDsS z4S+bB-G3dbl9?Q@QC_^oR-1=2lOr*nEjQJe)YF=x+ZRgr zmK@sgIp(<3X4}Z!@h`C>B!Q$`$ERB&4T&t=Gb!;@1BdFf%Bz|zg;AnU4(B!lpifc? z8Jw!KDB<4AUfPEH0?%mEs`BZH(LT#*xONW!&>bB>uSX5w05yp8qUoRC)x%sKl} z+pduKb#qc#v)6*j@2r|E+)>d_SSRE)8+cRUM(ywV65xlp(9;@4RP7sHuIhx^?@OD1 z5W@_>A}d^w?WsRCm!5xIIaKjR=;nGq_v?4ZAYDo7npu0)ZZnUg6-}z&*!>v;E)PX{ z(dPJ2>K}oJQNQA2eHKHvnu?ELpV^q?%oqREP8n@xBRK0AYWW(|TV!YMY}kz(}S_fI;ZMXYGnC3s~C2ktqU7^SDbc^y28 zt11gM5ewxQ65~;=Zm58Fe2ljip8enfTDPD&Fw6r|fCX4saCQH~A(4(gGUhWO&9@gR z(k?6EBdEbsxsO~ILbuvNZaVgukngHc5{psl#e4}Wg0T7iS}xEcBzwKW^&83?O5xIi zAv$BA`9l&7@0&GNli^$K6%o4lZ!7oIx-igHD6zhSVA$n^X>M$>Y%LS`*MU<$hZh*= zHj11mPk`nxm*=KXU7-qX$LBES^bNWgur}V$Ez-Ub=*ipheN)JTBVL9;O+fdJwiqMh zvZtq!gO}vF=TnR4L0znG zXwD!DO9r_pv$|}*akLx{KmT-6l(L0mv-R7*Q*jXo{*PC>IpgvuEK-C5GB_isRfG}x zvu-F+dON@XAObzVQ|q7y1V0vg-+ykNeDuHlW+pHZ+PhU*0bz4E?YKXl*?RO0TwwFS z+!LkKhXHa*?eQIa5y>L#UH>j=aM8uUdG+Ep9_uV6Tg&@jr#k5Wnb7#Z=Q0QRcl!TX z|L)I6grIc1a{{O#VIKBRdSxpkIYQwc$M5BtAeSfzS!qoc`L^w^d)|f<|K(@#bJzG+ z$-&)kJxUVDpEK?tyJ!uYY*6;>WDlo7rh+W*;%ATFq9Rk7_|wX~`U@qfgAd=r*77&U z(?-TCBpDVcRrDuMqv~wn43|?%<;U>{pkNHQw@UWOvf>PXq;JXO+~g6a`yWW0CDPN= zm~&Z|-S6`9LX$%lX#el5&=y&CCM$Hptz+eXLJ4hfwJO{wRDLdW29Ms_ITO~JH$cCw zd+Gee7tx#Vf>Jc;S^Mc&u>Zv!28le3nNQdpG8nK)M)P)9P6^rw=&m`fPKG4SIl~p& zrDu0Zy=x@Qwc+IkaVhBEkgY`LF{D`7(f&bR3>EKYs4jQjx9KGicF+3H=n%MmGCPP_ zUnSIo8FA&u@_w8{l`k_~kRLhlgl)RU8{q)fv}Mu0O&z>+dKg*t?u4NP5O>I9&wH$d zc``(e*Zi9wO!-OmYxkpQ?Kj@S7m+3-@kOPVucp^6ewb^yT}>}J&W5zze)|WBK`|V1 za$&jjc7#s|_Fu21+rtV8%dk+$Ub8PaW5JtiCZDhLe6Acw!4g&_+|e8IsBTU8K;OtY zS2%O!n_GGLa7UVco`jF;j}!!!BZoKlSyiBWa-JBhGiwf!g9W; zk>ybqR=U#z&qUb?mj!J5v_!gm8spY;0e%aF|Cu?XaISr59r|^$HBq={m8u()5jc|0ZLLV5B+jyY)JZ$QC;m7?G=_y)bbohIZQ&r@=<>f`| zZDdH(lqVgM!XIIXd#gJycyX13?wsZ3fT}~ zJLHdmTLa!ww22+OhlfQ=`tsSH3T~}!3mN`2?lrIzC2xw~%DI46MTz6ERRwGs0_M!f zT_Ie19Y;cYZZPBb=?DQ2!JGZD7`vnq=O>0@Q6j%Y+<<L&!KpA$6X57}Bse7E5`+B&K61oP++a?sX^4Bq8xe7$3?m ztu7I*Oj-_pHUZ#508TJUq_lg>>53dq4e^NVRC%?SsZ7VnX9IM6vnulVyZ+it-ip=F zANgRz*sf2a7y5H+5 zbCt?XTWYbIx?ScoR?$YB@9*ywCJh8l76diqt?Z@8z1N`JGZ-A-kv3G!U=Xq710Gw^ zP;sjP_e77A#icgcyIAMXM$#zbCfWUH$aI!wau^w`WRdQ~mQ>blsZPJIM=h=K7D;`~ z=M$r7s8hQswct0eVnUNH&>y6=lgg8ua&VU!!jt#g;SW`R z)YopN3R~#QFN(X-=duA-re!xIYFs?V*DuTKizvep@niMC6k^;V>!Sp?2_;0_sPpI4w|r1-e~I7caAQ_5>repi)%#9Yb%JdbH>rY6 zGLK=bir;_4@Jsg#Qua$-Pg5O3nkv8Qhxt?xnCq;Z{SGGl|im=CYMsY=`oCh0Cy zTsygDewIllQvai=e#&Wo55LRglXA(M(G~yJXB>a1kKC51>1gIo!yp&YX^{Xa*97gp zZ!Ia9)vJ9ySPDH(9VSjL_;KRgW`C)Jy};eMarz(fsrw17A6s-d4!TK-n@H7h#5F2u zPt0uD0bxKl^ZW#h&{tU!$Uxe!UoEo`ETtbBT-}KX4SvuVa%87S(1Z+4pK`!5$LXp+ zt46Z1`;Q^*MBP6SlET1~ab+oN*R}LfORB$qIr{7+mZbI*Ngap?o*ei*v7gcsH`6Pt z5!=&)%H&69+rrV*I6>Lv@YPCuH0dJHFZh)TwGY=1zbwvULrkZ=-s9%-!d(>|sgGE* z&+-HIKi$idqYfiGaI}WXX@9JBdaVVV`Iuc+ywcqlY}4;lUEEZ1^tdIV;kCE7HFbRE z&Y1EI?L^Cjz>)eOo*X?T%lTHiz+3Xv5=Rd4*S*cA-Ksp{$b=tm4L zh}~ZC*ywGoVkk{g8$+w=3pz}P+0gx`jnetr!!lp7%(HY>d$Gd&flwsVL#8}hqCnfw zfkSIFzFIj3^t+ntCjQZ$hcni?dN#J^R+6kn98k$q+luzQ7e@Q8?e*lm=FjrN$a0dT zpXvKr+`mScS4L^edcYtCm=a3(W)O+Z3gkdBnRKUoeb=?kE{ufbdO$?b*M#QD=rAEM zxp7qVCN^QP@n>XrmVU#Otcc@^0k)Lpv-H~)i?F^5^j}iewLFCM%>bN2WKun&6k7@%=vzU z1infMeM-N>sWq809@{tEtg1gwcE$%rKy=#p2+c>L*87V{fhXr{nBSY5=CnOiYT_f$ z$*aZR(>l#a2kyrNgV5RrfCR;e2GO}JI(@mBpWmYMy{%sDo-4_jDEU5>fV^C)LhI3S zsxA|_>okwSrGa4MA2ntaW(q4UPfZ}Nxszif#d906*S7_Up6x9=* zc1C;d7YE-qHk=iOI&>M7>uG54W(QqLm7I{f6gkkoURr1!`NtaVdSy;gdYBs_-&jbX=tqk6TSMX4 z?*v`25#nF~K_=RX@Q9Q^1AQL>Xpi`ThR<5vVPsZy{Z=lw z2{%D?Ka1tQ@4*}Mfu@U}wH0Ob1QQT9T=7;~x)Y6WA|l|$=n)ZJJ){Ie1n|3byN+dC zRlOH=DUbmhUs2kL%hcdHZ4MT|@I2PgVns_52JfnsHunPDn|DDcdFf*8Z< zYoeuCJ93`%^HvTjxsVdkm!TCiJ8UdtY>-@Vz3u(==y(H39rAwUsKt5p`(}4dfD$Ax z*QvuhSxsDyK6PB9Gh;-i(67~dS)DvolzRKKc5kw%=W%lEt&eUD3}uChb|~+fEjE08 zRf#6aBSWwmg%LwTS}I5Nq)9z#!y|XRG6I0+*s^!e_x9^*h|v;1cgX_k;X+yiy01y~ z_l+ywij#zGfY9VtVMG*`{iH3!5z%aq^lZ!zBV>@%!>87Oqp*P#2ti_^(ifC}hG%snwTB#@PDxPhw;T~t)=l8xO8H#*f45;%R<46PiOT{k{ zfi+}}1ud{e7)g1)fk5Ut(T6wGv_;>5fHTd}1Nr`H=`KSCEM!x=9{yyaJ#2}mS1U+7 zcR5)LPu_w%f#=L!?pa8kZIibt-Av#@xe3U^yxUWbRHZYCC+nkMVs0@@?TA2IN_8N+ z3UbdNQ*qjeXS)+KvaD!(2mNKo&(HoahCUb$_E8YZarnu`ddNI3Pv>YsIwxV+=wR8Pd6d>;q?jojjqTFd+)t_~ji^#;!O#P^CZd zxgrS3z=#+?5AZV)Q~P%QW#%v4<#a`rutH_%DnGV>Ge*&8n}sTH4SbT1C!m&j@qq; z9zd=V01P9(RA$dpRDiRxA@>Uyw- ztk6Hp|wngk4%1=WhO_NBvY=*##ieCmR~NCEcXjeWLCu_^LOW( zj$Ox!S8RT}$+~ve!)7OO$>BCnyc|h!`2#o_IiPqXku5Iv<~F{(>)F1I^|6-ng~0x# z^>)eMIL1E|U!xR`20`=cGh&uL&-R%?-tY59!D~s~>8XqdUemdLcCp_SZ_J#Y(_HS0 zwb~6PzbvX&$I7C17^ zGr!)cc)!j@b`dDt^_d9mHyvU0s)O){HsYTgH?@JR@o!JAGde5;Cwh$#;R*SX(gx-z zKdQyNHFDq+N~&IOT)arR_q+ZO)oDX?*WzQUKTZ;yphS=SB!^=`?sa{$d!q>q1PZnu zmEXZ|i^ILh(o?EqUyGVf^Ad-l z+c&^Lk7SJ7`tVkdhX_@kfiz?>K-;T+^RPt3Qd#CzDdvEah9eF`M=xGkILs|AC0%~I zb8+!7S3%;ovt~0B%j#-`D{o z6pj@6vTU5dp#v*GlOHbKe5ZQ`u8s8Pmj{z1e-c(s*GZ0QLMgF94+4KD6P{=xvWt*a zs`AzUZpiBkJSln8AaV4wea##?jq)k;x#y zy;7tIZSBk2_=3WcNk>w+=033#9hOjOpZt!e>oMi=Q%9pY3#h3M0r+auczyC>f63TO zAD0gRs?UsH3B-;E7tou@u&pkSUg5<1Bg3HbF0}@aaz07%InNyiziD3#ym7mv;voA^)BPM!Cx2`LNsyHn%6%QxPccH6!oXm4M&)y;(40olWwBBS!%e4d%+ zw7iFwmV93@jS}rO-I*sTd0gr$gqoYG9B+DJEHSNCsX7{LE5V3 zq^pia@i$+k-(KvK3+zvik4Kf1JZ-2h+Vk6ud!Cr+Jlw3}7<_#JIGyKPGdEzpTZ@+s za5Oh7X&2wSR8DS|E8vXHmq!-JN};Iod9|12Co#A@0{T#RVxl-6ZbGIV9BE?;sjhr* zc6zkivl=w;)K5&5{jPn}umaYQ@{^jszUB<9TGZJPp5hc!*VagaW2Sy*S5DBO3V$70 zV}IJrw6HCAvbb z8nmh==?5OkRgD8JE$bI) zm!6(5Xlo#iOL1ktUsYslvhSPl6~SYs8&rIBi@dVU6sY9{>zho~(}B}gKk~NE^9@o5 zY#2(4`qBo%Y{DK{RHtTQ-&8s?mGA_Q(S5j-!v_-`X= zHhut%gvyTL?}YCVS`O>b6_D=KPuPS`>uf;jjQ5b=Tfq(uq*i zAT(~9(DH{NeH8EHpkmcq5St}?iHsr20lVgdhj#9(z87R~VJ1rXJNg9Z=dz>BO1^IO!N0{93Rp4i9$Yf917)t=73V417v=TFlv!Bd5ci37f zw>C;$rwrcz6M`0{vpHK<3gsvy!N1#iLI6U#*c;O65n!JR-R#HE30)pLttk5`De}LY zV1mvs{-^VWs?b4S@F|xooBaQyVdi%0RP>r4108biWG^=;B>g8T_D{%us~R{b$QWzp zqu%oQKhDy|qKu2vi2XE;xhKN2CZg^&wb#NLo@)fnK>+?MQVO_|hm&V)ln|Kwn}7Oo zVkQ*qUTZ$)3*K?zzM!$AnH934$2}{P6eWS?KKj8Q8fq<5mwd|Sta32np*@4qRmyT%KXiPTC%2G$2LCVoNp5 zt$vBJ`FoLjTa4yC_ujUD4(QB%pQ`8P)J-L;ZI?UAQzAY8?sd?dF@EU%x43(+yeVka zzIpAUXFGYz-T0ldP=>;xr8Vwt%=LOkmdSb8ew)KXzPZv}u}NydS)|GeYe(tRuL<A-bZl=vc$CzDsDQT0S0 zdfH?Rsk={IhC+KtrMuVFePxABroeFExVaJ{)A2*uuVIhlK z=c0{k-RuDO*!H%mGHxDUZe$@k6spmXSr2>+ zonOoD5VS1}F=Br9@o+xMdnD%R6?yacrNh}r2DeYm%DaDs?9Z}_zte|LS=61@8Br|e ziH*?(Iz=tAyp-hl%rxb`w=*`+}SB8DFI`vtMl>kwbPit?0ktdX~PQx%@f8Cnba;a+Yx;0`?OmW-;v>} zFL)K3p-@ORz<>|EH}(19(MD>F{!vZVH*IBQMSfMmoRbI`GO5vJ9pN(t@9sNmcUF!- zuaXk*^0mv&PR!a?-)S=@cush2`+YaAPBtPuJUrZ73MG#0Us}@M^)P=7bx~L6a^nK$ z-+FhbF&39-vnJ&&WoAMuQoD!S7=uGgF2_t8#AXo!lII#D*(JihP51`NeVFb<=#Oj$*z_>2vb| zzrv0ktrr{9$l2Xp%O=0Iwe`3hQrAAqKB>zY$*nwXygaS#^0-+oppdyWbw{W_qfxs3 z)O>A$*;Oy2O z+1uH*#1r;yeumt2Gl@$oLot1Apl4uvpf@Ly56Froq;~SWUg+ON@?SP4MMG>4-um!2 zEsZ5HK5=~G=n^HENrTgaYUDT7%KJigf+z#_fd+~Fxlfq={e|9>*a#R;a6)L9FT9F= zuchPKLQ!4)GD^@h`{dHIq$H#QDl?YKMC*3!KLFrCI`U}9({+U#Ihp++ZYPmFH!%_t zQU@?sL=;CI3H#ojdjI6p;j^9Xc*jZquYMveKqWWTP+QJ9O z*v&wuEj@mHYwPH2VP}xvh{WSNGjm3A3VH^M@beQQ6iwH6I~ceIcRM8qPy&08t)UC` z@8W(;{Wg?|D>K)CthO&Gw58v8Ni1AK+qJCf;lt?T8vHd8#{U)FgZ8gp@Xzkhc|F^~SjNtRh^7V%OWAjB(Jrs3s6ecQhsnm2iSw>q>o8T!z8~uQo`~6ORI=)2A+vRe z?92^~iO09((oY>K1DBrdP}T8-Pm=z{tvizHn*6w#tdDMp&-6N~Ci>{&*m>Ki?>Gxn zpR8`Zp2-{Ie<~A-G)hwoBwxL3G6=Fx zvgGNrnT&iI`(gCi!_K+L)R)l%Z*yXtKh!Xn>Q~;Md9A8YV?(Xe`I}X#Vl~0V&KLe1 z(4qtqN2I{+ZWs)q{67dX>_=kzVY6Rzh0u7U+IkD(LKJpbY4x(Q6O_qeiaQV-Vtz zMHfGGCN`X(C3}^_GE!rida|bknH62~iM(#Equ+0_TXv<57g!ugYRPakry4e^ZTGv5 zFc>gcCH~e26+-pe$IgQL&Z$mNc45n!XnpiL3ooy*;j9P;3>cADM>yc3d zTr5tLRu;&}UIfp!miQVdf@F(JmOQ{y+>v@l;C(1*#*S>8XjP~&MoPU^#$BZ!TM(3! zQ#i}mnzPqzmjfB)pY(-$lfm%zC4Bj>7Jzc~J7_V&eEa$ZYQW{_@b0;jlT#(ARkIkX zjpApyKlo*j@NFBiTTno|r4;j*!HJh&z)8VoP!GA)|RK< zmh+9*mrzBrP-{)T|2y8dPFFK0Z!Ss{qi|E#f6UiY~h^eSDml`}CRJ zV|p>8nntuE{>b16BkK6Amhwuz-GJqum_#k2s~IPeIF_rM+W=_O9H)iYv68`flsy6j z(i^Gq#zBR{g3r;f^b%Y8?eWKANOE;`^{#dFb3d_Ib8S5~_`UFnOgpTT&J6wYxa}76 z$dX?jCgo?Q9Qxte#rA)!TGsDQ@^L=}{50QkKYxNs?EONO+ubt@sa3_`d*RNfUO|!W zxR;Q$UZP`>6(MuXl6&pEU022lYelm?Je%V+f-V6G@DM@5-yurNxn?8OkM%+kjLc zx&oD6e1Yq!B8zb7hU?U^s0c-#nVMhmofW%Ph+EO!Q{LkN+gz~`?>`nfRZtsUM&5WC z#cJvo>ru>M|>Xml)7_=dDzi& z)LvF;dni`c`nu<@c~7;K>tPOcCDql-&FATZxUBl=Bj=YT_X|c^6s~rf+Y6uq>9}JD z`Zp6B5EOf{5;rG<^le_0NOCsLwv(n$!Y(7|~icLft zRYh;*iB&6J?g{U{Cr*IvO*(~U&R?n&s_FiuTiJMWvL-V;l%5Z@CBph}QVgO(R{>B_JgN(wzd*DIwj` zU6PUl(j_3Eq)3BwYM)7{Kk#)8 zOKXbA5vN&6Uz#*}zu-gOR1g<$wOmTA1p5}-0iS*c3Mu|aTqjspOBCMQnJ2SI4R=Yd z-Aq5``lcrrc*$9g6yB~iH)-HlaUBISc6|JA6WP&Pw=InZ5y1Usmv!w3pUNXx zS19piABNqz01uIN$)D{Z#~UYO$|SMT6s8BFHZ(BJR7N6pP<&GL*cs^h2Kro?c|O5< z@3!LMeBgDi!`-phI-|gZgV7}FylQ}Mx%c92-9OVD$TnoX2F2&-Z%7!mZJ#lJgF8~2 zQtHFJ{3Gpc`{vwG=BoQ57~&n!IQ^Q)thJu0j?K9h5tcS+iMrp6ZP9&;GDt^*_`adg zB;i^@ssE2C?Gb#?;Pew^WEXStPKpWkoaUdmUfi#7B)_K+qCQF_i2YuphHHVG zlipH^0?A96RS?N(;21LE>8V?l`SW(Bf#yT$J2&c*U@f=v!~`89*2N_cjZn<+8X@SdmikFI@#lR|u&YRMRQ4phwc zoSC}~Lsj1|1?FfPzv*MKJUDW_LrJ-z<=7=cHQ%ouoAdTZ`|+R1L~uxxBi2U-ldo-j zYh{e{U7wpdPKl!|Juqf?VyFySv1F!P&V9uLp7LAgo0CQ_fh$f3>Tlh8is7@shHrYe zPB%_DE62zXZ*Cc%-)5LEsnP>8ZB{fG?``;{&W#r4xTBT;8Th>v=u`tR?zqI+W*YP+$!;i-rc1(z`{~-p{ zdJa{6CVN3sE9ulc0k$97K~Z@yqo+t0&X~%{YJp$`nB=9-GfBe^&j}T~RU-ZIx zN?-H7%3~}3bVZ+cJ2mvbyVd?N0EfN#b-ap8@eh;xnuh?wN`iH6C6QaX1)3`j@^rtX zU$pi`2xUm(o78SG6CaqhhwGu9Q2;uZ|FAA8s&tbFH}8v8Idog$?-HYn3i^thnlTmf z1LNLa$bT4nc>kSK1_D9Rl#r+47Llv>slS{N>CYqwD^rt$6odM+Jvn;hC;gtuNpfo1 zrLStGwHz5rj~C)KZY$m^UudK!KH1(x{VxcF^DaR%;b9IkWMutO+>229Cehv94C9Lq zF*DE2-KN7p3eguugo+)AOxo{ffw#i%cH_5#51xUzw)456zP>M=$UWkJO-dyrX)5g# z5nKm>?4BNsvBXojE$3M0>v9u(ex_yn0>$KiYkPn{ZD>Ue^e&x~T$ba4F9W6nI`{Xv zw=O&-gaF=Od(_pHHb+B-jU+{Y8X$zrh{!Nd+%b+beLnC|{(s2T$B%pr)_Jy!EQ)Wg z&uc`Ayk*9m1-J|i2vBwPZ!5~xO2K(KxbAd8(_7AT!dl-tQ@53tShgIcRFuEn$5Gh>yMC4BM@sryp@P$kz*S6As z;6O4AFbgMf%Slongd9DBdav+AgH9DE7GIo z4{8rTY59Y*Q(iXO+++~R3|LE7Of{zD4bAL1zI+*m{l>$`sAkBdE-W_T`v+cqr`?gy z4=j9}G$GY+N6QgRu9qcvG@&9zwe|_+Vp2I;7^W90AC!y1B%39MjG@x|F0Q%lUoVu% zyAa^2GKLy*Smy3(fWMktDxuL{$-D99DU{tmS1;(L!24XnEItOmTr!TGira>GFMD3f z-LD`4>$Pwz#XcTAiox!i52YQE3}+7epEW$ua&lUtsc&l9p-77j@}gwyFz~{xsDnGz z!evZ-Y_?llllh@fb||NN^!n2hU%v^NmwnLV{%m8)SLyaQKdI>F(Vq*AF_A z8W#WI37c23@G(LG7W6K{<$&LLgM{COP zTF8*M?SVV9Gb)hB|67mZj*gC<5>`AEa4&w<9?-*7;LTBERZfTPU4_!4Dyu&|;t1o4 zPwREKFG3a-;;Gv=cAGX)>Tf&ZqdW3h7acT7K}Z;M_PJuSSn1wJb*xttmVL$Jqe1BGG6-(sw-O3G#G~R%~BB zAHxaOmBSPMSq%SSZ)yw&?hF-=@2S?AIfCvD_kMjNyrtN#WD@~XbY+H{M8u_t&nX-O z#U58o`@05*K6vQe0uoNBv2Q+{TQlfCACX&m`8Y__gq1{Pte4=)%{BS=b_hMDjFstQ z`Pb*KNaP@9nsgm#A=65@kXo*~g>ti?mCLJlA>7G(SA7_YFh0_0!$c_jkkPlyqWF@bQknDSN z@rRB7=gNQxx|gvePwj7;Z(X8x#)2(J`{nb}=JA}^pOad}HBtvs${jOWFzOUng4b1B^1WJ_nb5zTL^vB7KGnA%qdOIm_nu^s)8rWH zhHuMph0>>zDswz)_@>{zJq3ykTnc_w?ewsl?;9l>U~`%Yf?F%OKQK6@1?gnO!neXr zVPbRT;IQ!VncZj7QpkNW+i1oX0xA#$-05oFm?7*c7wId%wfULd{7GYEL8hE0NidPN zx)v-8Zb;I{7~E1~{)3{#@$(xl-7yv2mY|xf@XEj%%sqt;ZP`L2NQiQuDRsP zq1ww}>KZ@B=b9=KFLG~9caa+X-98j=1V6Z43QciB#p-zrTN_-L z+C*S@5z2Htw$?fb3{Irot6r4R)Fe3_8b6V34sREE5R2U>VH}@nOHp2N{q=pBwW-ZG z##h#uKu#YJR4QjOv(^XH^z5FqL)k5Jt|K@1wj8%&^7rQo9i2b_s8jhg*3_h7l`W9* z!zAtK95>>w-r_t%^gHfuc+F2&c8 z(9Hb!_e}lZhV%F6SgFqD7M-awGy`m0b?1<{^v8goCt=GzXFKx5oW&(v2H0^;H{ z7cSa0`YN;Pkj|gOpHG8kxN#6?+6h`9rArf_=M_f~xWAE=Wx4As$}1k_=K2PDpf#K* z!AEhPT2^V_!?=?Zq8NRmO${=2x_ME|X=Za1TGI89A+wk*?ge0+&L?WitM0@XjtM}`dVB;^>UgWZMkcEq})eZ3v)$ZN~=#liPFpvFp zL4gc0;#%cXdh{P3e`~fP#X^G+w6c|!M_TbT*Vm)XS>12Ig9A5B66{#C4qC`Q^n7e= zET8qOHhVB`>K#AVEmMr7mqhZLoy+d+Lk@AgJ7}>JHXKw#4(VygXfHDL~xh6@<_rk4~p(jI&>I^m;mLj0q(>m2$t+DUQnMjloEBvqugum7qS(Zz8ibyp;HH7*CiUTV&7L8>Bycy{5^N z*=scoZV<@c@A(0(RIP8eMDkv^ph3%{(Ku;{!V=f9s)X{>@zF6T-W`nrV3WADbcONd z(lEy>{gg5-iw08Zhs;4`?`J*LuroY7JbHIEb+>4!oPIJrJv)1SeNngO7vP+2Z4if# zJ5>8yeXQ9C4+2_n>!~+R4@t4d;p5;p4E(;RNQ#hCHMh3ToKFI*dLsEDd3@S;tH`J* z$>3Wf4pKouQ8ufHKAG0J(_6R$A7z<~uG5T~xYATZ+{1~#}_7+xQs&>zninNxNmf{lP zPEDw);<`n{=8NO^C@H6>{~|+dhnu~&()X^;(~Bw} zgjRQS;6c)qBCJcAW}FLt4Rri?rA#WB>fqx;2I;mW8sL94hXffIOw0=p|Eah;44-F0 z5gpar7~{OtZ8@>DIEVG_Cj>ll3HbNLQn2K=(E1Cn{k1WAWB7sKmQZ{?Yk8kT<4man zoB8we>kE%|6Q2p{)1Q7Yx8st_wb49SFzu+7$XOq9H?EBZXtlW*Zi`I ztxc@HMNxX%7Ir#XDJFwh{`8OSwzBC2r)u~l_;7kyq3og#YC)Wc*L<~8|xz=IH z?3T)o!T z`K%N6{I^PdcZ)7gP81&f;4}M*<-SR-MR?^8MhB~@sHpTh_wuQR-Ke!;0Mvmfwr~IP zrT>>c7ubs8#TLAZpq}@faOX4NgPEC`&FGMC8rwiBoZUH>T(pF~cM6!EoSj|vxMZ}; z0*AQP+Z=s5q`Nz1goNADz1>aau1}*hX|K7}`uN<++nY2&rcFn?A~D2|svH*ral#qo zg+PW_RCjM41@E`sPLx+n>;~hfty{{jL>(OVoPYD&`Uz$TzreZE_ao<$d|yHwHJy1X zL6VTk!ZB3qG&5{dcUc_si{3uKkuO`dhr9Y9Gl*|^RW6lfrRG`41a-k=!dNfdnvtQS$zy z5e(Sf;#Ei93#QOA_d-oO3k!xI(cGrI_2AzSDgQ5u1w<0oNqQWlRCH8v3AmL0>L3Yi zUgTP8xLL6wE~XrGHU(c+=uQ<%T#n82(Z=D&jgOCSD2%oIH6UrF2n(FNsim5VhKXQ_ zI77BkTyMUepLR+>?n-aMpx>{%ua`1qwUgJ?JwfLp&$RNHC;^6KwwOm8xZ6cHo#|ZP zcstQdxto?O7+@=;Pcoh9inn8WcZ2PWF(?8aF;+x(|9ND9X~)T#VVrzHJKW~;IlZgz zLy+yU8AESI@bJC+IE^ev0L0r-fKr0%N)0oq>p8+mREAwchVNvYi6EVbF!%i--vz$2 z&S@OH6i)tEhcBohwb7q1N8nQUuBSmIecZAOUqGpJ;-S(azV4`)%%rNKk~*|9$JJWP zff3Yp9WGKs)g|IzPsBQFc3RugA_9^XBI7B7l9JM*l!5hxHSJCEZ?8EoW7RS5yGgW= z&RMVPAK%Iex7G!mlW^I%#3bs1iLoHQDV#)V;t^}w{$Ey(E-KUwWAah&9M<2(LM8Btvnk_2r@a-L{#;@x(T6VX$x8af<0BMFKS_65;4~e;3c#%3QpomcCyO4a=VmoIir~0#*)ntkU81b1%<8LOaL3-Z3TeORbx9yw)#3Xle~q_QnBRZ|-<_ktonuz9!7Gjh zlIsrF5PCq@4d%C z;oQmcOgDBcirIGcuY8>i{b5(k_dEa@LJmjcLbDX{?xHobDdRIjAc9NbJ0k3nFc zIsXZw__i)MfGC&FOo!w zHxBNuCSTPe!}V+Q9l^FXy*n2X5Q#k4-_Kd-+_}@|I0eniwGnMp-6;zqqs--N$6ndj ztn1+95sWn$9e)4DLoy)H|5MAMeYWxP_r5g0IWIYnk&vNkwGTt63Z(jWZbN7s78!Xr z!S;SG#8I%K5nc|>4!&O5rl#5F&MHQd`b5S_8vC+ryrQB4T$`G@dWjZWy0ZJ(u5l0d zKBrcdZEFP8k4~)0E#}?FMA6TeEG+{AX@b*|nbJ(tDjqA7$gOVQ}Lj{P5=9 z+uw{F^hJmLtpW-@p38%~^R3T*m>7Ghm?h`A{ZkX(k<_rvoE&3QQ@X;cLqpzG9Jm)_ zo}ttLnqUVx$A<-mA@AnccM7dSXFZ@6a5|(r88h)X0O&M#mQg`c2_$1q%eh< zu{5B<2I*78`yH)|GD(O24}=ASX&zc3cfxnC6?+8Dn?wzM!x?+DLr(ADM-*S-arBsP zqf!A5Dm*m0vg0ElLG2*58z1YD^DcjY_Snl}H97iasW!${Cl(5RIhLblj+XxpdYSYx z?L0iM*=liB(pID9e_DVak3BjQ=mE!*(+Jnyg&^M?wkVB=qvU zV`;#KEN6*Ct$kvakEh+TW)dl=9;C|-aIs$w)AF4jRnP;T`eka+_mA-*eY*ZiZ8Q3#iWK^PLo@z7VbFHP*V*M9an7I$T1r{n`E#I6qCh9%b-=Zxz_r^DS?@N zN=9%R?iaEE<{7wPo1@7{$fvN@5_XnUs`|>wp-B=Z#}s}K*k0(KZ}>5O@2;p3lJcoF z6%cp1W?q-13z}Xt?+Hest^l)8mai9N8#ElM(v*rIpp%WF`~6qhu4dVf);~KaYn3z? zwd}3+j&OXKx)y54_7I0eBj7cc8?hk(ybLe>{jLx&g)$m)nj=h!%!QKo;#l|};rG&U zNQJOB5^bo?A-@9iRce0unW=4P+s2FDWPCPkZ+$Ul+0#x2O4WY$S(^t3<#~GV2 zHvPX`GjzJ$6KfuUZiKtgODO=-YLe{mQ^C9B-Y)BJZ$E6!Hmk3Jzx}$+-&%(O#$IkM z9Eg(Y|H0m635vPnt>LrtM((*{jA6Ky40xq4j(QJ@Cb}pkSu6!7&~kje3wRPl4C$P* zYYp~fzUtfXL!w9jgowp+hTYv3FlYvVdx(Jb_oHviGJ{>k6F;W7TcKMfD~O@$X#Q+D z6Qt7L7909EH2|s1DT8PjmeQDDLNs1+m{Y&xAT1(%0A!WDg^0_m7ISjQC$&-z;wC`( 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 Date: Fri, 25 Feb 2022 15:07:35 +0100 Subject: [PATCH 47/47] Don't emergencysell partial sold exit closes #6457 --- freqtrade/freqtradebot.py | 15 +++++++++------ tests/test_freqtradebot.py | 23 ++++++++++++++++------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 99872ff0b..20fd833eb 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -979,10 +979,10 @@ class FreqtradeBot(LoggingMixin): or (order_obj and self.strategy.ft_check_timed_out( 'sell', trade, order_obj, datetime.now(timezone.utc)) ))): - self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT']) + canceled = self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT']) canceled_count = trade.get_exit_order_count() max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0) - if max_timeouts > 0 and canceled_count >= max_timeouts: + if canceled and max_timeouts > 0 and canceled_count >= max_timeouts: logger.warning(f'Emergencyselling trade {trade}, as the sell order ' f'timed out {max_timeouts} times.') try: @@ -1079,11 +1079,12 @@ class FreqtradeBot(LoggingMixin): reason=reason) return was_trade_fully_canceled - def handle_cancel_exit(self, trade: Trade, order: Dict, reason: str) -> str: + def handle_cancel_exit(self, trade: Trade, order: Dict, reason: str) -> bool: """ Sell cancel - cancel order and update trade - :return: Reason for cancel + :return: True if exit order was cancelled, false otherwise """ + cancelled = False # if trade is not partially completed, just cancel the order if order['remaining'] == order['amount'] or order.get('filled') == 0.0: if not self.exchange.check_order_canceled_empty(order): @@ -1094,7 +1095,7 @@ class FreqtradeBot(LoggingMixin): trade.update_order(co) except InvalidOrderException: logger.exception(f"Could not cancel sell order {trade.open_order_id}") - return 'error cancelling order' + return False logger.info('Sell order %s for %s.', reason, trade) else: reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] @@ -1108,9 +1109,11 @@ class FreqtradeBot(LoggingMixin): trade.close_date = None trade.is_open = True trade.open_order_id = None + cancelled = True else: # TODO: figure out how to handle partially complete sell orders reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] + cancelled = False self.wallets.update() self._notify_exit_cancel( @@ -1118,7 +1121,7 @@ class FreqtradeBot(LoggingMixin): order_type=self.strategy.order_types['sell'], reason=reason ) - return reason + return cancelled def _safe_exit_amount(self, pair: str, amount: float) -> float: """ diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index d7b47174b..d433998a1 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -6,7 +6,7 @@ import time from copy import deepcopy from math import isclose from typing import List -from unittest.mock import ANY, MagicMock, PropertyMock +from unittest.mock import ANY, MagicMock, PropertyMock, patch import arrow import pytest @@ -2220,9 +2220,14 @@ def test_check_handle_timedout_sell_usercustom(default_conf_usdt, ticker_usdt, l et_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit') caplog.clear() - # 2nd canceled trade ... open_trade.open_order_id = limit_sell_order_old['id'] + + # If cancelling fails - no emergency sell! + with patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_exit', return_value=False): + freqtrade.check_handle_timedout() + assert et_mock.call_count == 0 + freqtrade.check_handle_timedout() assert log_has_re('Emergencyselling trade.*', caplog) assert et_mock.call_count == 1 @@ -2564,13 +2569,17 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None: send_msg_mock.reset_mock() order['amount'] = 2 - assert freqtrade.handle_cancel_exit(trade, order, reason - ) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] + assert not freqtrade.handle_cancel_exit(trade, order, reason) # Assert cancel_order was not called (callcount remains unchanged) assert cancel_order_mock.call_count == 1 assert send_msg_mock.call_count == 1 - assert freqtrade.handle_cancel_exit(trade, order, reason - ) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] + assert (send_msg_mock.call_args_list[0][0][0]['reason'] + == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']) + + assert not freqtrade.handle_cancel_exit(trade, order, reason) + + send_msg_mock.call_args_list[0][0][0]['reason'] = CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] + # Message should not be iterated again assert trade.sell_order_status == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] assert send_msg_mock.call_count == 1 @@ -2589,7 +2598,7 @@ def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None: order = {'remaining': 1, 'amount': 1, 'status': "open"} - assert freqtrade.handle_cancel_exit(trade, order, reason) == 'error cancelling order' + assert not freqtrade.handle_cancel_exit(trade, order, reason) def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_sell_up, mocker