From e8803477dfa269f9f95932f169c95554e22b504f Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 3 May 2022 22:56:55 -0600 Subject: [PATCH 01/44] exchange/exchange add param taker_or_maker to add_dry_order_fee --- freqtrade/exchange/exchange.py | 44 ++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 08bdab265..f0ff7e514 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -785,6 +785,26 @@ class Exchange: # Dry-run methods + def taker_or_maker( + self, + order_reason: Literal['entry', 'exit', 'stoploss'], # TODO: stoploss + ): + order_type = self._config['order_types'][order_reason] + if order_type == 'market' or order_reason == 'stoploss': + return 'taker' + else: + return ( + 'maker' if ( + ( + order_reason == 'entry' and + self._config['entry_pricing']['price_side'] == 'same' + ) or ( + order_reason == 'exit' and + self._config['exit_pricing']['price_side'] == 'same' + ) + ) else 'taker' + ) + def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, rate: float, leverage: float, params: Dict = {}, stop_loss: bool = False) -> Dict[str, Any]: @@ -824,7 +844,7 @@ class Exchange: 'filled': _amount, 'cost': (dry_order['amount'] * average) / leverage }) - dry_order = self.add_dry_order_fee(pair, dry_order) + dry_order = self.add_dry_order_fee(pair, dry_order, self.taker_or_maker('entry')) dry_order = self.check_dry_limit_order_filled(dry_order) @@ -832,12 +852,17 @@ class Exchange: # Copy order and close it - so the returned order is open unless it's a market order return dry_order - def add_dry_order_fee(self, pair: str, dry_order: Dict[str, Any]) -> Dict[str, Any]: + def add_dry_order_fee( + self, + pair: str, + dry_order: Dict[str, Any], + taker_or_maker: Literal['taker', 'maker'], + ) -> Dict[str, Any]: dry_order.update({ 'fee': { 'currency': self.get_pair_quote_currency(pair), - 'cost': dry_order['cost'] * self.get_fee(pair), - 'rate': self.get_fee(pair) + 'cost': dry_order['cost'] * self.get_fee(pair, taker_or_maker=taker_or_maker), + 'rate': self.get_fee(pair, taker_or_maker=taker_or_maker) } }) return dry_order @@ -917,7 +942,16 @@ class Exchange: 'filled': order['amount'], 'remaining': 0, }) - self.add_dry_order_fee(pair, order) + enter_long = not order['is_short'] and order['side'] == 'buy' + enter_short = order['is_short'] and order['side'] == 'sell' + entry_or_exit: Literal['entry', 'exit'] = ( + 'entry' if (enter_short or enter_long) else 'exit' + ) + self.add_dry_order_fee( + pair, + order, + self.taker_or_maker(entry_or_exit) + ) return order From 5d9aee6b7e6591ddb1953159223af8ebbb9cf6a1 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 3 May 2022 23:18:13 -0600 Subject: [PATCH 02/44] test_taker_or_maker --- tests/exchange/test_exchange.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 1368bcb85..3edb5187c 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -4957,3 +4957,24 @@ def test_stoploss_contract_size(mocker, default_conf, contract_size, order_amoun assert order['cost'] == 100 assert order['filled'] == 100 assert order['remaining'] == 100 + + +@pytest.mark.parametrize('order_reason,price_side,order_type,taker_or_maker', [ + ("entry", "same", "limit", "maker"), + ("exit", "same", "limit", "maker"), + ("entry", "other", "limit", "taker"), + ("exit", "other", "limit", "taker"), + ("entry", "same", "market", "maker"), + ("exit", "same", "market", "maker"), + ("entry", "other", "market", "taker"), + ("exit", "other", "market", "taker"), + ("stoploss", "same", "limit", "taker"), + ("stoploss", "same", "market", "taker"), + ("stoploss", "other", "limit", "taker"), + ("stoploss", "other", "market", "taker"), +]) +def test_taker_or_maker(mocker, default_conf, order_reason, price_side, order_type, taker_or_maker): + default_conf[f"{order_reason}_pricing"]["price_side"] = price_side + default_conf["order_types"][order_reason] = order_type + exchange = get_patched_exchange(mocker, default_conf) + assert exchange.taker_or_maker(order_reason) == taker_or_maker From dac9931b4a53aedeb4435483dcbb956203bb5530 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 3 May 2022 23:52:01 -0600 Subject: [PATCH 03/44] test_create_dry_run_order_fees --- tests/exchange/test_exchange.py | 41 +++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 3edb5187c..ab51362a5 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1125,6 +1125,47 @@ def test_create_dry_run_order(default_conf, mocker, side, exchange_name, leverag assert order["cost"] == 1 * 200 / leverage +@pytest.mark.parametrize('side,is_short,order_reason', [ + ("buy", False, "entry"), + ("sell", False, "exit"), + ("buy", True, "exit"), + ("sell", True, "entry"), +]) +@pytest.mark.parametrize("order_type,price_side,fee", [ + ("limit", "same", 1.0), + ("limit", "other", 2.0), + ("market", "same", 2.0), + ("market", "other", 2.0), +]) +def test_create_dry_run_order_fees( + default_conf, + mocker, + side, + order_type, + is_short, + order_reason, + price_side, + fee, +): + default_conf[f"{order_reason}_pricing"]["price_side"] = "same" + default_conf["order_types"][order_reason] = order_type + mocker.patch( + 'freqtrade.exchange.Exchange.get_fee', + lambda symbol, taker_or_maker: 2.0 if taker_or_maker == 'taker' else 1.0 + ) + exchange = get_patched_exchange(mocker, default_conf) + + order = exchange.create_dry_run_order( + pair='ADA/USDT', + ordertype=order_type, + side=side, + amount=10, + rate=2.0, + ) + + assert order['ft_fee_base'] == fee + + @pytest.mark.parametrize("side,startprice,endprice", [ ("buy", 25.563, 25.566), ("sell", 25.566, 25.563) From 86ad5dd02a7ae680a1d566f7b83b55e5cf65f399 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 4 May 2022 00:08:41 -0600 Subject: [PATCH 04/44] test_exchange::test_taker_or_maker fixes --- tests/exchange/test_exchange.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index ab51362a5..a13dca4b6 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -5015,7 +5015,9 @@ def test_stoploss_contract_size(mocker, default_conf, contract_size, order_amoun ("stoploss", "other", "market", "taker"), ]) def test_taker_or_maker(mocker, default_conf, order_reason, price_side, order_type, taker_or_maker): - default_conf[f"{order_reason}_pricing"]["price_side"] = price_side + if order_reason != 'stoploss': + default_conf[f"{order_reason}_pricing"]["price_side"] = price_side + default_conf["order_types"] = {} default_conf["order_types"][order_reason] = order_type exchange = get_patched_exchange(mocker, default_conf) assert exchange.taker_or_maker(order_reason) == taker_or_maker From 10cbb5e67c32b184445f868366e4eef38afe5878 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 4 May 2022 00:10:09 -0600 Subject: [PATCH 05/44] test_exchange::test_taker_or_maker fixes --- tests/exchange/test_exchange.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index a13dca4b6..86928a2f6 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -5005,13 +5005,13 @@ def test_stoploss_contract_size(mocker, default_conf, contract_size, order_amoun ("exit", "same", "limit", "maker"), ("entry", "other", "limit", "taker"), ("exit", "other", "limit", "taker"), - ("entry", "same", "market", "maker"), - ("exit", "same", "market", "maker"), + ("stoploss", "same", "limit", "taker"), + ("stoploss", "other", "limit", "taker"), + ("entry", "same", "market", "taker"), + ("exit", "same", "market", "taker"), ("entry", "other", "market", "taker"), ("exit", "other", "market", "taker"), - ("stoploss", "same", "limit", "taker"), ("stoploss", "same", "market", "taker"), - ("stoploss", "other", "limit", "taker"), ("stoploss", "other", "market", "taker"), ]) def test_taker_or_maker(mocker, default_conf, order_reason, price_side, order_type, taker_or_maker): From 7fe8b7661d7cfe64129c78082622f8e4e9744599 Mon Sep 17 00:00:00 2001 From: Surfer Admin Date: Tue, 31 May 2022 15:46:43 -0400 Subject: [PATCH 06/44] Display the signal candle analyzed in telegram. --- freqtrade/freqtradebot.py | 20 ++++++++++++++++++++ freqtrade/rpc/telegram.py | 8 ++++++++ 2 files changed, 28 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index fba63459b..a9e15d972 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -852,6 +852,16 @@ class FreqtradeBot(LoggingMixin): 'current_rate': current_rate, } + # display the candle analyzed in telegram + analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, + self.strategy.timeframe) + analyzed_candle = analyzed_df.iloc[-1] if len(analyzed_df) > 0 else None + if analyzed_candle is not None: + candle_columns = analyzed_candle[['date', 'open', 'high', 'low', 'close']] + msg.update({ + 'analyzed_candle': candle_columns.to_json(date_unit='s', date_format='iso') + }) + # Send the message self.rpc.send_msg(msg) @@ -1540,6 +1550,16 @@ class FreqtradeBot(LoggingMixin): 'fiat_currency': self.config['fiat_display_currency'], }) + # display the candle analyzed in telegram + analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, + self.strategy.timeframe) + analyzed_candle = analyzed_df.iloc[-1] if len(analyzed_df) > 0 else None + if analyzed_candle is not None: + candle_columns = analyzed_candle[['date', 'open', 'high', 'low', 'close']] + msg.update({ + 'analyzed_candle': candle_columns.to_json(date_unit='s', date_format='iso') + }) + # Send the message self.rpc.send_msg(msg) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 4a274002e..8364578a4 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -241,6 +241,8 @@ class Telegram(RPCHandler): f" {entry_side['entered'] if is_fill else entry_side['enter']} {msg['pair']}" f" (#{msg['trade_id']})\n" ) + if msg.get('analyzed_candle'): + message += f"*Analyzed Candle:* `{msg['analyzed_candle']}`\n" message += f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag', None) else "" message += f"*Amount:* `{msg['amount']:.8f}`\n" if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0: @@ -288,6 +290,12 @@ class Telegram(RPCHandler): message = ( f"{msg['emoji']} *{msg['exchange']}:* " f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n" + ) + if not is_fill and msg.get('analyzed_candle'): + message += ( + f"*Analyzed Candle:* `{msg['analyzed_candle']}`\n" + ) + message += ( f"*{'Profit' if is_fill else 'Unrealized Profit'}:* " f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n" f"*Enter Tag:* `{msg['enter_tag']}`\n" From 405ea74f168ede4d85a4a3d9bf8f6e862ada6b75 Mon Sep 17 00:00:00 2001 From: Surfer Admin Date: Tue, 21 Jun 2022 14:06:41 -0400 Subject: [PATCH 07/44] stopPrice --- .gitignore | 5 ++ freqtrade/templates/sample_strategy.py | 27 ++------ .../templates/strategy_analysis_example.ipynb | 61 ++++++++++++++++--- 3 files changed, 60 insertions(+), 33 deletions(-) diff --git a/.gitignore b/.gitignore index 97f77f779..5042dfb9b 100644 --- a/.gitignore +++ b/.gitignore @@ -105,3 +105,8 @@ target/ !config_examples/config_ftx.example.json !config_examples/config_full.example.json !config_examples/config_kraken.example.json +doc/doc-filelist.js +.gitignore +.gitignore +doc/doc-style.css +doc/doc-script.js diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index 1b375714a..1b49f82c9 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -178,29 +178,10 @@ class SampleStrategy(IStrategy): # RSI dataframe['rsi'] = ta.RSI(dataframe) - # # Inverse Fisher transform on RSI: values [-1.0, 1.0] (https://goo.gl/2JGGoy) - # rsi = 0.1 * (dataframe['rsi'] - 50) - # dataframe['fisher_rsi'] = (np.exp(2 * rsi) - 1) / (np.exp(2 * rsi) + 1) - - # # Inverse Fisher transform on RSI normalized: values [0.0, 100.0] (https://goo.gl/2JGGoy) - # dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1) - - # # Stochastic Slow - # stoch = ta.STOCH(dataframe) - # dataframe['slowd'] = stoch['slowd'] - # dataframe['slowk'] = stoch['slowk'] - - # Stochastic Fast - stoch_fast = ta.STOCHF(dataframe) - dataframe['fastd'] = stoch_fast['fastd'] - dataframe['fastk'] = stoch_fast['fastk'] - - # # Stochastic RSI - # Please read https://github.com/freqtrade/freqtrade/issues/2961 before using this. - # STOCHRSI is NOT aligned with tradingview, which may result in non-expected results. - # stoch_rsi = ta.STOCHRSI(dataframe) - # dataframe['fastd_rsi'] = stoch_rsi['fastd'] - # dataframe['fastk_rsi'] = stoch_rsi['fastk'] + # Stochastic RSI + stoch_rsi = ta.STOCHRSI(dataframe) + dataframe['fastd_rsi'] = stoch_rsi['fastd'] + dataframe['fastk_rsi'] = stoch_rsi['fastk'] # MACD macd = ta.MACD(dataframe) diff --git a/freqtrade/templates/strategy_analysis_example.ipynb b/freqtrade/templates/strategy_analysis_example.ipynb index 93e4b83ae..33182dbf0 100644 --- a/freqtrade/templates/strategy_analysis_example.ipynb +++ b/freqtrade/templates/strategy_analysis_example.ipynb @@ -19,9 +19,27 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, - "outputs": [], + "outputs": [ + { + "ename": "OperationalException", + "evalue": "Directory `/Users/surfer/Software/MMM/develop/freqtrade/freqtrade/templates/user_data` does not exist. Please use `freqtrade create-userdir` to create a user directory", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mOperationalException\u001b[0m Traceback (most recent call last)", + "Input \u001b[0;32mIn [2]\u001b[0m, in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mfreqtrade\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mconfiguration\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m Configuration\n\u001b[1;32m 4\u001b[0m \u001b[38;5;66;03m# Customize these according to your needs.\u001b[39;00m\n\u001b[1;32m 5\u001b[0m \n\u001b[1;32m 6\u001b[0m \u001b[38;5;66;03m# Initialize empty configuration object\u001b[39;00m\n\u001b[0;32m----> 7\u001b[0m config \u001b[38;5;241m=\u001b[39m \u001b[43mConfiguration\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfrom_files\u001b[49m\u001b[43m(\u001b[49m\u001b[43m[\u001b[49m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 8\u001b[0m \u001b[38;5;66;03m# Optionally, use existing configuration file\u001b[39;00m\n\u001b[1;32m 9\u001b[0m \u001b[38;5;66;03m# config = Configuration.from_files([\"config.json\"])\u001b[39;00m\n\u001b[1;32m 10\u001b[0m \n\u001b[1;32m 11\u001b[0m \u001b[38;5;66;03m# Define some constants\u001b[39;00m\n\u001b[1;32m 12\u001b[0m config[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mtimeframe\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;241m=\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m1m\u001b[39m\u001b[38;5;124m\"\u001b[39m\n", + "File \u001b[0;32m~/Software/MMM/develop/freqtrade/freqtrade/configuration/configuration.py:60\u001b[0m, in \u001b[0;36mConfiguration.from_files\u001b[0;34m(files)\u001b[0m\n\u001b[1;32m 58\u001b[0m \u001b[38;5;66;03m# Keep this method as staticmethod, so it can be used from interactive environments\u001b[39;00m\n\u001b[1;32m 59\u001b[0m c \u001b[38;5;241m=\u001b[39m Configuration({\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mconfig\u001b[39m\u001b[38;5;124m'\u001b[39m: files}, RunMode\u001b[38;5;241m.\u001b[39mOTHER)\n\u001b[0;32m---> 60\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mc\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_config\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/Software/MMM/develop/freqtrade/freqtrade/configuration/configuration.py:42\u001b[0m, in \u001b[0;36mConfiguration.get_config\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 37\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 38\u001b[0m \u001b[38;5;124;03mReturn the config. Use this method to get the bot config\u001b[39;00m\n\u001b[1;32m 39\u001b[0m \u001b[38;5;124;03m:return: Dict: Bot config\u001b[39;00m\n\u001b[1;32m 40\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 41\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mconfig \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[0;32m---> 42\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mconfig \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mload_config\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 44\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mconfig\n", + "File \u001b[0;32m~/Software/MMM/develop/freqtrade/freqtrade/configuration/configuration.py:92\u001b[0m, in \u001b[0;36mConfiguration.load_config\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 88\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_process_common_options(config)\n\u001b[1;32m 90\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_process_trading_options(config)\n\u001b[0;32m---> 92\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_process_optimize_options\u001b[49m\u001b[43m(\u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 94\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_process_plot_options(config)\n\u001b[1;32m 96\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_process_data_options(config)\n", + "File \u001b[0;32m~/Software/MMM/develop/freqtrade/freqtrade/configuration/configuration.py:249\u001b[0m, in \u001b[0;36mConfiguration._process_optimize_options\u001b[0;34m(self, config)\u001b[0m\n\u001b[1;32m 242\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_args_to_config(config, argname\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mfee\u001b[39m\u001b[38;5;124m'\u001b[39m,\n\u001b[1;32m 243\u001b[0m logstring\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mParameter --fee detected, \u001b[39m\u001b[38;5;124m'\u001b[39m\n\u001b[1;32m 244\u001b[0m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124msetting fee to: \u001b[39m\u001b[38;5;132;01m{}\u001b[39;00m\u001b[38;5;124m ...\u001b[39m\u001b[38;5;124m'\u001b[39m)\n\u001b[1;32m 246\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_args_to_config(config, argname\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mtimerange\u001b[39m\u001b[38;5;124m'\u001b[39m,\n\u001b[1;32m 247\u001b[0m logstring\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mParameter --timerange detected: \u001b[39m\u001b[38;5;132;01m{}\u001b[39;00m\u001b[38;5;124m ...\u001b[39m\u001b[38;5;124m'\u001b[39m)\n\u001b[0;32m--> 249\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_process_datadir_options\u001b[49m\u001b[43m(\u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 251\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_args_to_config(config, argname\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mstrategy_list\u001b[39m\u001b[38;5;124m'\u001b[39m,\n\u001b[1;32m 252\u001b[0m logstring\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mUsing strategy list of \u001b[39m\u001b[38;5;132;01m{}\u001b[39;00m\u001b[38;5;124m strategies\u001b[39m\u001b[38;5;124m'\u001b[39m, logfun\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mlen\u001b[39m)\n\u001b[1;32m 254\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_args_to_config(\n\u001b[1;32m 255\u001b[0m config,\n\u001b[1;32m 256\u001b[0m argname\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mrecursive_strategy_search\u001b[39m\u001b[38;5;124m'\u001b[39m,\n\u001b[1;32m 257\u001b[0m logstring\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mRecursively searching for a strategy in the strategies folder.\u001b[39m\u001b[38;5;124m'\u001b[39m,\n\u001b[1;32m 258\u001b[0m )\n", + "File \u001b[0;32m~/Software/MMM/develop/freqtrade/freqtrade/configuration/configuration.py:180\u001b[0m, in \u001b[0;36mConfiguration._process_datadir_options\u001b[0;34m(self, config)\u001b[0m\n\u001b[1;32m 177\u001b[0m config\u001b[38;5;241m.\u001b[39mupdate({\u001b[38;5;124m'\u001b[39m\u001b[38;5;124muser_data_dir\u001b[39m\u001b[38;5;124m'\u001b[39m: \u001b[38;5;28mstr\u001b[39m(Path\u001b[38;5;241m.\u001b[39mcwd() \u001b[38;5;241m/\u001b[39m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124muser_data\u001b[39m\u001b[38;5;124m'\u001b[39m)})\n\u001b[1;32m 179\u001b[0m \u001b[38;5;66;03m# reset to user_data_dir so this contains the absolute path.\u001b[39;00m\n\u001b[0;32m--> 180\u001b[0m config[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124muser_data_dir\u001b[39m\u001b[38;5;124m'\u001b[39m] \u001b[38;5;241m=\u001b[39m \u001b[43mcreate_userdata_dir\u001b[49m\u001b[43m(\u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43muser_data_dir\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mcreate_dir\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m)\u001b[49m\n\u001b[1;32m 181\u001b[0m logger\u001b[38;5;241m.\u001b[39minfo(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mUsing user-data directory: \u001b[39m\u001b[38;5;132;01m%s\u001b[39;00m\u001b[38;5;124m ...\u001b[39m\u001b[38;5;124m'\u001b[39m, config[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124muser_data_dir\u001b[39m\u001b[38;5;124m'\u001b[39m])\n\u001b[1;32m 183\u001b[0m config\u001b[38;5;241m.\u001b[39mupdate({\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mdatadir\u001b[39m\u001b[38;5;124m'\u001b[39m: create_datadir(config, \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39margs\u001b[38;5;241m.\u001b[39mget(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mdatadir\u001b[39m\u001b[38;5;124m'\u001b[39m, \u001b[38;5;28;01mNone\u001b[39;00m))})\n", + "File \u001b[0;32m~/Software/MMM/develop/freqtrade/freqtrade/configuration/directory_operations.py:61\u001b[0m, in \u001b[0;36mcreate_userdata_dir\u001b[0;34m(directory, create_dir)\u001b[0m\n\u001b[1;32m 59\u001b[0m logger\u001b[38;5;241m.\u001b[39minfo(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mCreated user-data directory: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mfolder\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m'\u001b[39m)\n\u001b[1;32m 60\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m---> 61\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m OperationalException(\n\u001b[1;32m 62\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mDirectory `\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mfolder\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m` does not exist. \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 63\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mPlease use `freqtrade create-userdir` to create a user directory\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 65\u001b[0m \u001b[38;5;66;03m# Create required subdirectories\u001b[39;00m\n\u001b[1;32m 66\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m f \u001b[38;5;129;01min\u001b[39;00m sub_dirs:\n", + "\u001b[0;31mOperationalException\u001b[0m: Directory `/Users/surfer/Software/MMM/develop/freqtrade/freqtrade/templates/user_data` does not exist. Please use `freqtrade create-userdir` to create a user directory" + ] + } + ], "source": [ "from pathlib import Path\n", "from freqtrade.configuration import Configuration\n", @@ -34,9 +52,9 @@ "# config = Configuration.from_files([\"config.json\"])\n", "\n", "# Define some constants\n", - "config[\"timeframe\"] = \"5m\"\n", + "config[\"timeframe\"] = \"1m\"\n", "# Name of the strategy class\n", - "config[\"strategy\"] = \"SampleStrategy\"\n", + "config[\"strategy\"] = \"MMMOracle\"\n", "# Location of the data\n", "data_location = Path(config['user_data_dir'], 'data', 'binance')\n", "# Pair to analyze - Only use one pair here\n", @@ -136,9 +154,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, - "outputs": [], + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'config' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "Input \u001b[0;32mIn [3]\u001b[0m, in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mfreqtrade\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mdata\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mbtanalysis\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m load_backtest_data, load_backtest_stats\n\u001b[1;32m 3\u001b[0m \u001b[38;5;66;03m# if backtest_dir points to a directory, it'll automatically load the last backtest file.\u001b[39;00m\n\u001b[0;32m----> 4\u001b[0m backtest_dir \u001b[38;5;241m=\u001b[39m \u001b[43mconfig\u001b[49m[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124muser_data_dir\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;241m/\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mbacktest_results\u001b[39m\u001b[38;5;124m\"\u001b[39m\n", + "\u001b[0;31mNameError\u001b[0m: name 'config' is not defined" + ] + } + ], "source": [ "from freqtrade.data.btanalysis import load_backtest_data, load_backtest_stats\n", "\n", @@ -247,9 +277,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "Series([], Name: exit_reason, dtype: int64)" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "from freqtrade.data.btanalysis import load_trades_from_db\n", "\n", @@ -363,7 +404,7 @@ "metadata": { "file_extension": ".py", "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -377,7 +418,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.5" + "version": "3.9.7" }, "mimetype": "text/x-python", "name": "python", From 6ac1aa15f582d8665936c0658c09f0ba308f68d0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 11 Jul 2022 10:36:19 +0200 Subject: [PATCH 08/44] Reenable ccxt order checks --- freqtrade/exchange/exchange.py | 11 ++++------- tests/exchange/test_exchange.py | 7 +++---- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 58d5ebb07..cd13964c4 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -586,13 +586,10 @@ class Exchange: """ Checks if order-types configured in strategy/config are supported """ - # TODO: Reenable once ccxt fixes createMarketOrder assignment - as well as - # Revert the change in test_validate_ordertypes. - - # if any(v == 'market' for k, v in order_types.items()): - # if not self.exchange_has('createMarketOrder'): - # raise OperationalException( - # f'Exchange {self.name} does not support market orders.') + if any(v == 'market' for k, v in order_types.items()): + if not self.exchange_has('createMarketOrder'): + raise OperationalException( + f'Exchange {self.name} does not support market orders.') if (order_types.get("stoploss_on_exchange") and not self._ft_has.get("stoploss_on_exchange", False)): diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index f09d647ce..acd48b3fd 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1027,10 +1027,9 @@ def test_validate_ordertypes(default_conf, mocker): 'stoploss': 'market', 'stoploss_on_exchange': False } - # TODO: Revert once createMarketOrder is available again. - # with pytest.raises(OperationalException, - # match=r'Exchange .* does not support market orders.'): - # Exchange(default_conf) + with pytest.raises(OperationalException, + match=r'Exchange .* does not support market orders.'): + Exchange(default_conf) default_conf['order_types'] = { 'entry': 'limit', From 64f89af69e8e88f4c24bf0a8dace2b740821287b Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 11 Jul 2022 10:43:21 +0200 Subject: [PATCH 09/44] Add Explicit test for "has" checks --- tests/exchange/test_ccxt_compat.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 50154bcaf..74106f28b 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -153,6 +153,25 @@ class TestCCXTExchange(): assert isinstance(markets[pair], dict) assert exchange.market_is_spot(markets[pair]) + def test_has_validations(self, exchange): + + exchange, exchangename = exchange + + exchange.validate_ordertypes({ + 'entry': 'limit', + 'exit': 'limit', + 'stoploss': 'limit', + }) + + if exchangename == 'gateio': + # gateio doesn't have market orders on spot + return + exchange.validate_ordertypes({ + 'entry': 'market', + 'exit': 'market', + 'stoploss': 'market', + }) + def test_load_markets_futures(self, exchange_futures): exchange, exchangename = exchange_futures if not exchange: From ec3179156c200da0649c1604ce7a1e29561d09d9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 11 Jul 2022 11:48:24 +0200 Subject: [PATCH 10/44] Revert unwanted changes. --- .gitignore | 5 -- freqtrade/templates/sample_strategy.py | 27 ++++++-- .../templates/strategy_analysis_example.ipynb | 61 +++---------------- 3 files changed, 33 insertions(+), 60 deletions(-) diff --git a/.gitignore b/.gitignore index 5042dfb9b..97f77f779 100644 --- a/.gitignore +++ b/.gitignore @@ -105,8 +105,3 @@ target/ !config_examples/config_ftx.example.json !config_examples/config_full.example.json !config_examples/config_kraken.example.json -doc/doc-filelist.js -.gitignore -.gitignore -doc/doc-style.css -doc/doc-script.js diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index 1b49f82c9..1b375714a 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -178,10 +178,29 @@ class SampleStrategy(IStrategy): # RSI dataframe['rsi'] = ta.RSI(dataframe) - # Stochastic RSI - stoch_rsi = ta.STOCHRSI(dataframe) - dataframe['fastd_rsi'] = stoch_rsi['fastd'] - dataframe['fastk_rsi'] = stoch_rsi['fastk'] + # # Inverse Fisher transform on RSI: values [-1.0, 1.0] (https://goo.gl/2JGGoy) + # rsi = 0.1 * (dataframe['rsi'] - 50) + # dataframe['fisher_rsi'] = (np.exp(2 * rsi) - 1) / (np.exp(2 * rsi) + 1) + + # # Inverse Fisher transform on RSI normalized: values [0.0, 100.0] (https://goo.gl/2JGGoy) + # dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1) + + # # Stochastic Slow + # stoch = ta.STOCH(dataframe) + # dataframe['slowd'] = stoch['slowd'] + # dataframe['slowk'] = stoch['slowk'] + + # Stochastic Fast + stoch_fast = ta.STOCHF(dataframe) + dataframe['fastd'] = stoch_fast['fastd'] + dataframe['fastk'] = stoch_fast['fastk'] + + # # Stochastic RSI + # Please read https://github.com/freqtrade/freqtrade/issues/2961 before using this. + # STOCHRSI is NOT aligned with tradingview, which may result in non-expected results. + # stoch_rsi = ta.STOCHRSI(dataframe) + # dataframe['fastd_rsi'] = stoch_rsi['fastd'] + # dataframe['fastk_rsi'] = stoch_rsi['fastk'] # MACD macd = ta.MACD(dataframe) diff --git a/freqtrade/templates/strategy_analysis_example.ipynb b/freqtrade/templates/strategy_analysis_example.ipynb index 33182dbf0..93e4b83ae 100644 --- a/freqtrade/templates/strategy_analysis_example.ipynb +++ b/freqtrade/templates/strategy_analysis_example.ipynb @@ -19,27 +19,9 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "ename": "OperationalException", - "evalue": "Directory `/Users/surfer/Software/MMM/develop/freqtrade/freqtrade/templates/user_data` does not exist. Please use `freqtrade create-userdir` to create a user directory", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mOperationalException\u001b[0m Traceback (most recent call last)", - "Input \u001b[0;32mIn [2]\u001b[0m, in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mfreqtrade\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mconfiguration\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m Configuration\n\u001b[1;32m 4\u001b[0m \u001b[38;5;66;03m# Customize these according to your needs.\u001b[39;00m\n\u001b[1;32m 5\u001b[0m \n\u001b[1;32m 6\u001b[0m \u001b[38;5;66;03m# Initialize empty configuration object\u001b[39;00m\n\u001b[0;32m----> 7\u001b[0m config \u001b[38;5;241m=\u001b[39m \u001b[43mConfiguration\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfrom_files\u001b[49m\u001b[43m(\u001b[49m\u001b[43m[\u001b[49m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 8\u001b[0m \u001b[38;5;66;03m# Optionally, use existing configuration file\u001b[39;00m\n\u001b[1;32m 9\u001b[0m \u001b[38;5;66;03m# config = Configuration.from_files([\"config.json\"])\u001b[39;00m\n\u001b[1;32m 10\u001b[0m \n\u001b[1;32m 11\u001b[0m \u001b[38;5;66;03m# Define some constants\u001b[39;00m\n\u001b[1;32m 12\u001b[0m config[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mtimeframe\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;241m=\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m1m\u001b[39m\u001b[38;5;124m\"\u001b[39m\n", - "File \u001b[0;32m~/Software/MMM/develop/freqtrade/freqtrade/configuration/configuration.py:60\u001b[0m, in \u001b[0;36mConfiguration.from_files\u001b[0;34m(files)\u001b[0m\n\u001b[1;32m 58\u001b[0m \u001b[38;5;66;03m# Keep this method as staticmethod, so it can be used from interactive environments\u001b[39;00m\n\u001b[1;32m 59\u001b[0m c \u001b[38;5;241m=\u001b[39m Configuration({\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mconfig\u001b[39m\u001b[38;5;124m'\u001b[39m: files}, RunMode\u001b[38;5;241m.\u001b[39mOTHER)\n\u001b[0;32m---> 60\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mc\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_config\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", - "File \u001b[0;32m~/Software/MMM/develop/freqtrade/freqtrade/configuration/configuration.py:42\u001b[0m, in \u001b[0;36mConfiguration.get_config\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 37\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 38\u001b[0m \u001b[38;5;124;03mReturn the config. Use this method to get the bot config\u001b[39;00m\n\u001b[1;32m 39\u001b[0m \u001b[38;5;124;03m:return: Dict: Bot config\u001b[39;00m\n\u001b[1;32m 40\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 41\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mconfig \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[0;32m---> 42\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mconfig \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mload_config\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 44\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mconfig\n", - "File \u001b[0;32m~/Software/MMM/develop/freqtrade/freqtrade/configuration/configuration.py:92\u001b[0m, in \u001b[0;36mConfiguration.load_config\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 88\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_process_common_options(config)\n\u001b[1;32m 90\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_process_trading_options(config)\n\u001b[0;32m---> 92\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_process_optimize_options\u001b[49m\u001b[43m(\u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 94\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_process_plot_options(config)\n\u001b[1;32m 96\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_process_data_options(config)\n", - "File \u001b[0;32m~/Software/MMM/develop/freqtrade/freqtrade/configuration/configuration.py:249\u001b[0m, in \u001b[0;36mConfiguration._process_optimize_options\u001b[0;34m(self, config)\u001b[0m\n\u001b[1;32m 242\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_args_to_config(config, argname\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mfee\u001b[39m\u001b[38;5;124m'\u001b[39m,\n\u001b[1;32m 243\u001b[0m logstring\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mParameter --fee detected, \u001b[39m\u001b[38;5;124m'\u001b[39m\n\u001b[1;32m 244\u001b[0m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124msetting fee to: \u001b[39m\u001b[38;5;132;01m{}\u001b[39;00m\u001b[38;5;124m ...\u001b[39m\u001b[38;5;124m'\u001b[39m)\n\u001b[1;32m 246\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_args_to_config(config, argname\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mtimerange\u001b[39m\u001b[38;5;124m'\u001b[39m,\n\u001b[1;32m 247\u001b[0m logstring\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mParameter --timerange detected: \u001b[39m\u001b[38;5;132;01m{}\u001b[39;00m\u001b[38;5;124m ...\u001b[39m\u001b[38;5;124m'\u001b[39m)\n\u001b[0;32m--> 249\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_process_datadir_options\u001b[49m\u001b[43m(\u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 251\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_args_to_config(config, argname\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mstrategy_list\u001b[39m\u001b[38;5;124m'\u001b[39m,\n\u001b[1;32m 252\u001b[0m logstring\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mUsing strategy list of \u001b[39m\u001b[38;5;132;01m{}\u001b[39;00m\u001b[38;5;124m strategies\u001b[39m\u001b[38;5;124m'\u001b[39m, logfun\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mlen\u001b[39m)\n\u001b[1;32m 254\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_args_to_config(\n\u001b[1;32m 255\u001b[0m config,\n\u001b[1;32m 256\u001b[0m argname\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mrecursive_strategy_search\u001b[39m\u001b[38;5;124m'\u001b[39m,\n\u001b[1;32m 257\u001b[0m logstring\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mRecursively searching for a strategy in the strategies folder.\u001b[39m\u001b[38;5;124m'\u001b[39m,\n\u001b[1;32m 258\u001b[0m )\n", - "File \u001b[0;32m~/Software/MMM/develop/freqtrade/freqtrade/configuration/configuration.py:180\u001b[0m, in \u001b[0;36mConfiguration._process_datadir_options\u001b[0;34m(self, config)\u001b[0m\n\u001b[1;32m 177\u001b[0m config\u001b[38;5;241m.\u001b[39mupdate({\u001b[38;5;124m'\u001b[39m\u001b[38;5;124muser_data_dir\u001b[39m\u001b[38;5;124m'\u001b[39m: \u001b[38;5;28mstr\u001b[39m(Path\u001b[38;5;241m.\u001b[39mcwd() \u001b[38;5;241m/\u001b[39m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124muser_data\u001b[39m\u001b[38;5;124m'\u001b[39m)})\n\u001b[1;32m 179\u001b[0m \u001b[38;5;66;03m# reset to user_data_dir so this contains the absolute path.\u001b[39;00m\n\u001b[0;32m--> 180\u001b[0m config[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124muser_data_dir\u001b[39m\u001b[38;5;124m'\u001b[39m] \u001b[38;5;241m=\u001b[39m \u001b[43mcreate_userdata_dir\u001b[49m\u001b[43m(\u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43muser_data_dir\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mcreate_dir\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m)\u001b[49m\n\u001b[1;32m 181\u001b[0m logger\u001b[38;5;241m.\u001b[39minfo(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mUsing user-data directory: \u001b[39m\u001b[38;5;132;01m%s\u001b[39;00m\u001b[38;5;124m ...\u001b[39m\u001b[38;5;124m'\u001b[39m, config[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124muser_data_dir\u001b[39m\u001b[38;5;124m'\u001b[39m])\n\u001b[1;32m 183\u001b[0m config\u001b[38;5;241m.\u001b[39mupdate({\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mdatadir\u001b[39m\u001b[38;5;124m'\u001b[39m: create_datadir(config, \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39margs\u001b[38;5;241m.\u001b[39mget(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mdatadir\u001b[39m\u001b[38;5;124m'\u001b[39m, \u001b[38;5;28;01mNone\u001b[39;00m))})\n", - "File \u001b[0;32m~/Software/MMM/develop/freqtrade/freqtrade/configuration/directory_operations.py:61\u001b[0m, in \u001b[0;36mcreate_userdata_dir\u001b[0;34m(directory, create_dir)\u001b[0m\n\u001b[1;32m 59\u001b[0m logger\u001b[38;5;241m.\u001b[39minfo(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mCreated user-data directory: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mfolder\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m'\u001b[39m)\n\u001b[1;32m 60\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m---> 61\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m OperationalException(\n\u001b[1;32m 62\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mDirectory `\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mfolder\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m` does not exist. \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 63\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mPlease use `freqtrade create-userdir` to create a user directory\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 65\u001b[0m \u001b[38;5;66;03m# Create required subdirectories\u001b[39;00m\n\u001b[1;32m 66\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m f \u001b[38;5;129;01min\u001b[39;00m sub_dirs:\n", - "\u001b[0;31mOperationalException\u001b[0m: Directory `/Users/surfer/Software/MMM/develop/freqtrade/freqtrade/templates/user_data` does not exist. Please use `freqtrade create-userdir` to create a user directory" - ] - } - ], + "outputs": [], "source": [ "from pathlib import Path\n", "from freqtrade.configuration import Configuration\n", @@ -52,9 +34,9 @@ "# config = Configuration.from_files([\"config.json\"])\n", "\n", "# Define some constants\n", - "config[\"timeframe\"] = \"1m\"\n", + "config[\"timeframe\"] = \"5m\"\n", "# Name of the strategy class\n", - "config[\"strategy\"] = \"MMMOracle\"\n", + "config[\"strategy\"] = \"SampleStrategy\"\n", "# Location of the data\n", "data_location = Path(config['user_data_dir'], 'data', 'binance')\n", "# Pair to analyze - Only use one pair here\n", @@ -154,21 +136,9 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "ename": "NameError", - "evalue": "name 'config' is not defined", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", - "Input \u001b[0;32mIn [3]\u001b[0m, in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mfreqtrade\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mdata\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mbtanalysis\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m load_backtest_data, load_backtest_stats\n\u001b[1;32m 3\u001b[0m \u001b[38;5;66;03m# if backtest_dir points to a directory, it'll automatically load the last backtest file.\u001b[39;00m\n\u001b[0;32m----> 4\u001b[0m backtest_dir \u001b[38;5;241m=\u001b[39m \u001b[43mconfig\u001b[49m[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124muser_data_dir\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;241m/\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mbacktest_results\u001b[39m\u001b[38;5;124m\"\u001b[39m\n", - "\u001b[0;31mNameError\u001b[0m: name 'config' is not defined" - ] - } - ], + "outputs": [], "source": [ "from freqtrade.data.btanalysis import load_backtest_data, load_backtest_stats\n", "\n", @@ -277,20 +247,9 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Series([], Name: exit_reason, dtype: int64)" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "from freqtrade.data.btanalysis import load_trades_from_db\n", "\n", @@ -404,7 +363,7 @@ "metadata": { "file_extension": ".py", "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "Python 3", "language": "python", "name": "python3" }, @@ -418,7 +377,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.7" + "version": "3.8.5" }, "mimetype": "text/x-python", "name": "python", From 8e8f026ea77a11259e93e1691a737b0c079f7f83 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 11 Jul 2022 12:10:29 +0200 Subject: [PATCH 11/44] Telegram candle message should be configurable --- config_examples/config_full.example.json | 3 ++- docs/telegram-usage.md | 5 +++-- freqtrade/constants.py | 4 ++++ freqtrade/freqtradebot.py | 4 ++-- freqtrade/rpc/telegram.py | 23 +++++++++++++++-------- 5 files changed, 26 insertions(+), 13 deletions(-) diff --git a/config_examples/config_full.example.json b/config_examples/config_full.example.json index 6382e1baf..e2e9a16fd 100644 --- a/config_examples/config_full.example.json +++ b/config_examples/config_full.example.json @@ -155,7 +155,8 @@ "entry_cancel": "on", "exit_cancel": "on", "protection_trigger": "off", - "protection_trigger_global": "on" + "protection_trigger_global": "on", + "show_candle": "off" }, "reload": true, "balance_dust_level": 0.01 diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 2145797b4..9853e15c6 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -97,7 +97,8 @@ Example configuration showing the different settings: "entry_fill": "off", "exit_fill": "off", "protection_trigger": "off", - "protection_trigger_global": "on" + "protection_trigger_global": "on", + "show_candle": "off" }, "reload": true, "balance_dust_level": 0.01 @@ -108,7 +109,7 @@ Example configuration showing the different settings: `exit` notifications are sent when the order is placed, while `exit_fill` notifications are sent when the order is filled on the exchange. `*_fill` notifications are off by default and must be explicitly enabled. `protection_trigger` notifications are sent when a protection triggers and `protection_trigger_global` notifications trigger when global protections are triggered. - +`show_candle` - show candle values as part of entry/exit messages. Only possible value is "ohlc". `balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown. `reload` allows you to disable reload-buttons on selected messages. diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 18dbea259..ce7c0ff83 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -313,6 +313,10 @@ CONF_SCHEMA = { 'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS, }, + 'show_candle': { + 'type': 'string', + 'enum': ['off', 'ohlc'], + }, } }, 'reload': {'type': 'boolean'}, diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2d16dc9ff..fe8bcc89b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -868,7 +868,7 @@ class FreqtradeBot(LoggingMixin): if analyzed_candle is not None: candle_columns = analyzed_candle[['date', 'open', 'high', 'low', 'close']] msg.update({ - 'analyzed_candle': candle_columns.to_json(date_unit='s', date_format='iso') + 'analyzed_candle': candle_columns.to_dict() }) # Send the message @@ -1567,7 +1567,7 @@ class FreqtradeBot(LoggingMixin): if analyzed_candle is not None: candle_columns = analyzed_candle[['date', 'open', 'high', 'low', 'close']] msg.update({ - 'analyzed_candle': candle_columns.to_json(date_unit='s', date_format='iso') + 'analyzed_candle': candle_columns.to_dict() }) # Send the message diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 16a851ce1..9ade8cb5f 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -243,6 +243,19 @@ class Telegram(RPCHandler): """ return f"{msg['exchange']}{' (dry)' if self._config['dry_run'] else ''}" + def _add_analyzed_candle(self, msg) -> str: + candle_val = self._config['telegram'].get( + 'notification_settings', {}).get('show_candle', 'off') + if candle_val != 'off' and msg.get('analyzed_candle'): + if candle_val == 'ohlc': + candle_json = msg['analyzed_candle'] + return ( + f"*Candle OHLC*: `{candle_json['open']}, {candle_json['high']}, " + f"{candle_json['low']}, {candle_json['close']}`\n" + ) + + return '' + def _format_entry_msg(self, msg: Dict[str, Any]) -> str: if self._rpc._fiat_converter: msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount( @@ -259,8 +272,7 @@ class Telegram(RPCHandler): f" {entry_side['entered'] if is_fill else entry_side['enter']} {msg['pair']}" f" (#{msg['trade_id']})\n" ) - if msg.get('analyzed_candle'): - message += f"*Analyzed Candle:* `{msg['analyzed_candle']}`\n" + message += self._add_analyzed_candle(msg) message += f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag') else "" message += f"*Amount:* `{msg['amount']:.8f}`\n" if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0: @@ -308,12 +320,7 @@ class Telegram(RPCHandler): message = ( f"{msg['emoji']} *{self._exchange_from_msg(msg)}:* " f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n" - ) - if not is_fill and msg.get('analyzed_candle'): - message += ( - f"*Analyzed Candle:* `{msg['analyzed_candle']}`\n" - ) - message += ( + f"{self._add_analyzed_candle(msg)}" f"*{'Profit' if is_fill else 'Unrealized Profit'}:* " f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n" f"*Enter Tag:* `{msg['enter_tag']}`\n" From 9a3a2f9013003182b9b5fe014e9d6679138e9f2b Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 11 Jul 2022 13:55:32 +0200 Subject: [PATCH 12/44] Simplify adding candle to message --- freqtrade/freqtradebot.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index fe8bcc89b..bde9ea8aa 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -861,8 +861,15 @@ class FreqtradeBot(LoggingMixin): 'current_rate': current_rate, } + self._analyzed_candle_to_msg(trade.pair, msg) + + # Send the message + self.rpc.send_msg(msg) + + def _analyzed_candle_to_msg(self, pair: str, msg: Dict): + """Msg dict will be enhanced with analyzed_candle if possible.""" # display the candle analyzed in telegram - analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, + analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(pair, self.strategy.timeframe) analyzed_candle = analyzed_df.iloc[-1] if len(analyzed_df) > 0 else None if analyzed_candle is not None: @@ -871,9 +878,6 @@ class FreqtradeBot(LoggingMixin): 'analyzed_candle': candle_columns.to_dict() }) - # Send the message - self.rpc.send_msg(msg) - def _notify_enter_cancel(self, trade: Trade, order_type: str, reason: str) -> None: """ Sends rpc notification when a entry order cancel occurred. @@ -1560,15 +1564,7 @@ class FreqtradeBot(LoggingMixin): 'fiat_currency': self.config['fiat_display_currency'], }) - # display the candle analyzed in telegram - analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, - self.strategy.timeframe) - analyzed_candle = analyzed_df.iloc[-1] if len(analyzed_df) > 0 else None - if analyzed_candle is not None: - candle_columns = analyzed_candle[['date', 'open', 'high', 'low', 'close']] - msg.update({ - 'analyzed_candle': candle_columns.to_dict() - }) + self._analyzed_candle_to_msg(trade.pair, msg) # Send the message self.rpc.send_msg(msg) From f9d3775d4c36f1c45a137f5919ebef12f6703c1e Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 11 Jul 2022 14:09:39 +0200 Subject: [PATCH 13/44] Move "candle" logic for message to telegram this avoids calling this method unless necessary --- freqtrade/freqtradebot.py | 16 ---------------- freqtrade/rpc/telegram.py | 21 ++++++++++++--------- 2 files changed, 12 insertions(+), 25 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index bde9ea8aa..469bfda7e 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -861,23 +861,9 @@ class FreqtradeBot(LoggingMixin): 'current_rate': current_rate, } - self._analyzed_candle_to_msg(trade.pair, msg) - # Send the message self.rpc.send_msg(msg) - def _analyzed_candle_to_msg(self, pair: str, msg: Dict): - """Msg dict will be enhanced with analyzed_candle if possible.""" - # display the candle analyzed in telegram - analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(pair, - self.strategy.timeframe) - analyzed_candle = analyzed_df.iloc[-1] if len(analyzed_df) > 0 else None - if analyzed_candle is not None: - candle_columns = analyzed_candle[['date', 'open', 'high', 'low', 'close']] - msg.update({ - 'analyzed_candle': candle_columns.to_dict() - }) - def _notify_enter_cancel(self, trade: Trade, order_type: str, reason: str) -> None: """ Sends rpc notification when a entry order cancel occurred. @@ -1564,8 +1550,6 @@ class FreqtradeBot(LoggingMixin): 'fiat_currency': self.config['fiat_display_currency'], }) - self._analyzed_candle_to_msg(trade.pair, msg) - # Send the message self.rpc.send_msg(msg) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 9ade8cb5f..2aff1d210 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -243,16 +243,19 @@ class Telegram(RPCHandler): """ return f"{msg['exchange']}{' (dry)' if self._config['dry_run'] else ''}" - def _add_analyzed_candle(self, msg) -> str: + def _add_analyzed_candle(self, pair: str) -> str: candle_val = self._config['telegram'].get( 'notification_settings', {}).get('show_candle', 'off') - if candle_val != 'off' and msg.get('analyzed_candle'): + if candle_val != 'off': if candle_val == 'ohlc': - candle_json = msg['analyzed_candle'] - return ( - f"*Candle OHLC*: `{candle_json['open']}, {candle_json['high']}, " - f"{candle_json['low']}, {candle_json['close']}`\n" - ) + analyzed_df, _ = self._rpc._freqtrade.dataprovider.get_analyzed_dataframe( + pair, self._config['timeframe']) + candle = analyzed_df.iloc[-1].squeeze() if len(analyzed_df) > 0 else None + if candle is not None: + return ( + f"*Candle OHLC*: `{candle['open']}, {candle['high']}, " + f"{candle['low']}, {candle['close']}`\n" + ) return '' @@ -272,7 +275,7 @@ class Telegram(RPCHandler): f" {entry_side['entered'] if is_fill else entry_side['enter']} {msg['pair']}" f" (#{msg['trade_id']})\n" ) - message += self._add_analyzed_candle(msg) + message += self._add_analyzed_candle(msg['pair']) message += f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag') else "" message += f"*Amount:* `{msg['amount']:.8f}`\n" if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0: @@ -320,7 +323,7 @@ class Telegram(RPCHandler): message = ( f"{msg['emoji']} *{self._exchange_from_msg(msg)}:* " f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n" - f"{self._add_analyzed_candle(msg)}" + f"{self._add_analyzed_candle(msg['pair'])}" f"*{'Profit' if is_fill else 'Unrealized Profit'}:* " f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n" f"*Enter Tag:* `{msg['enter_tag']}`\n" From bf992fd9dfce74e916a961164436a8349df25aa1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 11 Jul 2022 14:09:44 +0200 Subject: [PATCH 14/44] Add test for newly added functionality --- tests/rpc/test_rpc_telegram.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index e36d98083..91ee92fd7 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -12,6 +12,7 @@ from unittest.mock import ANY, MagicMock import arrow import pytest +from pandas import DataFrame from telegram import Chat, Message, ReplyKeyboardMarkup, Update from telegram.error import BadRequest, NetworkError, TelegramError @@ -1655,8 +1656,17 @@ def test_show_config_handle(default_conf, update, mocker) -> None: (RPCMessageType.ENTRY, 'Long', 'long_signal_01', 1.0), (RPCMessageType.ENTRY, 'Long', 'long_signal_01', 5.0), (RPCMessageType.ENTRY, 'Short', 'short_signal_01', 2.0)]) -def test_send_msg_buy_notification(default_conf, mocker, caplog, message_type, - enter, enter_signal, leverage) -> None: +def test_send_msg_enter_notification(default_conf, mocker, caplog, message_type, + enter, enter_signal, leverage) -> None: + default_conf['telegram']['notification_settings']['show_candle'] = 'ohlc' + df = DataFrame({ + 'open': [1.1], + 'high': [2.2], + 'low': [1.0], + 'close': [1.5], + }) + mocker.patch('freqtrade.data.dataprovider.DataProvider.get_analyzed_dataframe', + return_value=(df, 1)) msg = { 'type': message_type, @@ -1674,6 +1684,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog, message_type, 'fiat_currency': 'USD', 'current_rate': 1.099e-05, 'amount': 1333.3333333333335, + 'analyzed_candle': {'open': 1.1, 'high': 2.2, 'low': 1.0, 'close': 1.5}, 'open_date': arrow.utcnow().shift(hours=-1) } telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) @@ -1683,6 +1694,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog, message_type, assert msg_mock.call_args[0][0] == ( f'\N{LARGE BLUE CIRCLE} *Binance (dry):* {enter} ETH/BTC (#1)\n' + '*Candle OHLC*: `1.1, 2.2, 1.0, 1.5`\n' f'*Enter Tag:* `{enter_signal}`\n' '*Amount:* `1333.33333333`\n' f'{leverage_text}' @@ -1710,7 +1722,8 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog, message_type, @pytest.mark.parametrize('message_type,enter_signal', [ (RPCMessageType.ENTRY_CANCEL, 'long_signal_01'), (RPCMessageType.ENTRY_CANCEL, 'short_signal_01')]) -def test_send_msg_buy_cancel_notification(default_conf, mocker, message_type, enter_signal) -> None: +def test_send_msg_enter_cancel_notification( + default_conf, mocker, message_type, enter_signal) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) From b9ba94d644c4408059093e0cb0ada7fee1a4ac46 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 11 Jul 2022 16:07:58 +0200 Subject: [PATCH 15/44] Bump ccxt to 1.90.47 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 115ae734e..2bb3b4b75 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.23.1 pandas==1.4.3 pandas-ta==0.3.14b -ccxt==1.90.41 +ccxt==1.90.47 # Pin cryptography for now due to rust build errors with piwheels cryptography==37.0.4 aiohttp==3.8.1 From 5c164efdb6845313eee57d849aff04f3b93eb93d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 11 Jul 2022 16:09:12 +0200 Subject: [PATCH 16/44] Also check for createLimitOrder as optionals --- freqtrade/exchange/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index 0046ba458..5765dc459 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -73,7 +73,7 @@ EXCHANGE_HAS_REQUIRED = [ EXCHANGE_HAS_OPTIONAL = [ # Private 'fetchMyTrades', # Trades for order - fee detection - # 'createLimitOrder', 'createMarketOrder', # Either OR for orders + 'createLimitOrder', 'createMarketOrder', # Either OR for orders # 'setLeverage', # Margin/Futures trading # 'setMarginMode', # Margin/Futures trading # 'fetchFundingHistory', # Futures trading From cdc58058d72aca1bf11cfd108f01f79226056277 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 14 Jul 2022 19:40:26 +0200 Subject: [PATCH 17/44] Add candletype to notebook example closes #7084, closes #7073 --- docs/strategy_analysis_example.md | 4 +++- freqtrade/templates/strategy_analysis_example.ipynb | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/strategy_analysis_example.md b/docs/strategy_analysis_example.md index ae0c6a6a3..fbfce37d1 100644 --- a/docs/strategy_analysis_example.md +++ b/docs/strategy_analysis_example.md @@ -31,11 +31,13 @@ pair = "BTC/USDT" ```python # Load data using values set above from freqtrade.data.history import load_pair_history +from freqtrade.enums import CandleType candles = load_pair_history(datadir=data_location, timeframe=config["timeframe"], pair=pair, data_format = "hdf5", + candle_type=CandleType.SPOT, ) # Confirm success @@ -93,7 +95,7 @@ from freqtrade.data.btanalysis import load_backtest_data, load_backtest_stats # if backtest_dir points to a directory, it'll automatically load the last backtest file. backtest_dir = config["user_data_dir"] / "backtest_results" -# backtest_dir can also point to a specific file +# backtest_dir can also point to a specific file # backtest_dir = config["user_data_dir"] / "backtest_results/backtest-result-2020-07-01_20-04-22.json" ``` diff --git a/freqtrade/templates/strategy_analysis_example.ipynb b/freqtrade/templates/strategy_analysis_example.ipynb index 93e4b83ae..a7430c225 100644 --- a/freqtrade/templates/strategy_analysis_example.ipynb +++ b/freqtrade/templates/strategy_analysis_example.ipynb @@ -51,11 +51,13 @@ "source": [ "# Load data using values set above\n", "from freqtrade.data.history import load_pair_history\n", + "from freqtrade.enums import CandleType\n", "\n", "candles = load_pair_history(datadir=data_location,\n", " timeframe=config[\"timeframe\"],\n", " pair=pair,\n", " data_format = \"hdf5\",\n", + " candle_type=CandleType.SPOT,\n", " )\n", "\n", "# Confirm success\n", From b657a4df2375aeebc9ad4f1e4c4eff995f7fd90a Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 15 Jul 2022 18:55:30 +0200 Subject: [PATCH 18/44] Improve hyperopt docs part of #7088 --- docs/hyperopt.md | 24 ++++++++++++++++++++++-- docs/strategy-advanced.md | 2 ++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index f44a68037..55fe8f008 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -335,6 +335,7 @@ There are four parameter types each suited for different purposes. ## Optimizing an indicator parameter Assuming you have a simple strategy in mind - a EMA cross strategy (2 Moving averages crossing) - and you'd like to find the ideal parameters for this strategy. +By default, we assume a stoploss of 5% - and a take-profit (`minimal_roi`) of 10% - which means freqtrade will sell the trade once 10% profit has been reached. ``` python from pandas import DataFrame @@ -349,6 +350,9 @@ import freqtrade.vendor.qtpylib.indicators as qtpylib class MyAwesomeStrategy(IStrategy): stoploss = -0.05 timeframe = '15m' + minimal_roi = { + "0": 0.10 + }, # Define the parameter spaces buy_ema_short = IntParameter(3, 50, default=5) buy_ema_long = IntParameter(15, 200, default=50) @@ -383,7 +387,7 @@ class MyAwesomeStrategy(IStrategy): return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - conditions = [] + conditions = [] conditions.append(qtpylib.crossed_above( dataframe[f'ema_long_{self.buy_ema_long.value}'], dataframe[f'ema_short_{self.buy_ema_short.value}'] )) @@ -404,7 +408,7 @@ Using `self.buy_ema_short.range` will return a range object containing all entri In this case (`IntParameter(3, 50, default=5)`), the loop would run for all numbers between 3 and 50 (`[3, 4, 5, ... 49, 50]`). By using this in a loop, hyperopt will generate 48 new columns (`['buy_ema_3', 'buy_ema_4', ... , 'buy_ema_50']`). -Hyperopt itself will then use the selected value to create the buy and sell signals +Hyperopt itself will then use the selected value to create the buy and sell signals. While this strategy is most likely too simple to provide consistent profit, it should serve as an example how optimize indicator parameters. @@ -868,6 +872,22 @@ To combat these, you have multiple options: * reduce the number of parallel processes (`-j `) * Increase the memory of your machine +## The objective has been evaluated at this point before. + +If you see `The objective has been evaluated at this point before.` - then this is a sign that your space has been exhausted, or is close to that. +Basically all points in your space have been hit (or a local minima has been hit) - and hyperopt does no longer find points in the multi-dimensional space it did not try yet. +Freqtrade tries to counter the "local minima" problem by using new, randomized points in this case. + +Example: + +``` python +buy_ema_short = IntParameter(5, 20, default=10, space="buy", optimize=True) +# This is the only parameter in the buy space +``` + +The `buy_ema_short` space has 15 possible values (`5, 6, ... 19, 20`). If you now run hyperopt for the buy space, hyperopt will only have 15 values to try before running out of options. +Your epochs should therefore be aligned to the possible values - or you should be ready to interrupt a run if you norice a lot of `The objective has been evaluated at this point before.` warnings. + ## Show details of Hyperopt results After you run Hyperopt for the desired amount of epochs, you can later list all results for analysis, select only best or profitable once, and show the details for any of the epochs previously evaluated. This can be done with the `hyperopt-list` and `hyperopt-show` sub-commands. The usage of these sub-commands is described in the [Utils](utils.md#list-hyperopt-results) chapter. diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 374c675a2..a3115bfb2 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -224,3 +224,5 @@ for val in self.buy_ema_short.range: # Append columns to existing dataframe merged_frame = pd.concat(frames, axis=1) ``` + +Freqtrade does however also counter this by running `dataframe.copy()` on the dataframe right after the `populate_indicators()` method - so performance implications of this should be low to non-existant. From fada432f49611da259b720c58e0dfd8259936276 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 15 Jul 2022 19:48:12 +0200 Subject: [PATCH 19/44] Pin markdown docs dependency --- docs/requirements-docs.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 66e83a397..22d92c65d 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,4 @@ +markdown==3.3.7 mkdocs==1.3.0 mkdocs-material==8.3.9 mdx_truly_sane_lists==1.2 From 1c7f60103db564fb5abf0ac0db83600e713c4586 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 15 Jul 2022 20:26:24 +0200 Subject: [PATCH 20/44] Don't use master for publish CI action --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4fe1ad853..7b077be04 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -351,7 +351,7 @@ jobs: python setup.py sdist bdist_wheel - name: Publish to PyPI (Test) - uses: pypa/gh-action-pypi-publish@master + uses: pypa/gh-action-pypi-publish@v1.5.0 if: (github.event_name == 'release') with: user: __token__ @@ -359,7 +359,7 @@ jobs: repository_url: https://test.pypi.org/legacy/ - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@master + uses: pypa/gh-action-pypi-publish@v1.5.0 if: (github.event_name == 'release') with: user: __token__ From 29efe75a6fc307a7224017765134199bed7dc11e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Jul 2022 11:15:14 +0200 Subject: [PATCH 21/44] Update hyperoptable strategy to use V3 interface --- tests/rpc/test_rpc_apiserver.py | 1 + .../strategy/strats/hyperoptable_strategy.py | 4 +- .../strats/hyperoptable_strategy_v2.py | 54 +++++++++++++++++++ tests/strategy/test_interface.py | 2 +- tests/strategy/test_strategy_loading.py | 6 +-- 5 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 tests/strategy/strats/hyperoptable_strategy_v2.py diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index c0de54c6d..57c08f48e 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1398,6 +1398,7 @@ def test_api_strategies(botclient): assert rc.json() == {'strategies': [ 'HyperoptableStrategy', + 'HyperoptableStrategyV2', 'InformativeDecoratorTest', 'StrategyTestV2', 'StrategyTestV3', diff --git a/tests/strategy/strats/hyperoptable_strategy.py b/tests/strategy/strats/hyperoptable_strategy.py index 876b31b14..9850a5675 100644 --- a/tests/strategy/strats/hyperoptable_strategy.py +++ b/tests/strategy/strats/hyperoptable_strategy.py @@ -1,13 +1,13 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement from pandas import DataFrame -from strategy_test_v2 import StrategyTestV2 +from strategy_test_v3 import StrategyTestV3 import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.strategy import BooleanParameter, DecimalParameter, IntParameter, RealParameter -class HyperoptableStrategy(StrategyTestV2): +class HyperoptableStrategy(StrategyTestV3): """ Default Strategy provided by freqtrade bot. Please do not modify this strategy, it's intended for internal use only. diff --git a/tests/strategy/strats/hyperoptable_strategy_v2.py b/tests/strategy/strats/hyperoptable_strategy_v2.py new file mode 100644 index 000000000..94a15b456 --- /dev/null +++ b/tests/strategy/strats/hyperoptable_strategy_v2.py @@ -0,0 +1,54 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement + +from strategy_test_v2 import StrategyTestV2 + +from freqtrade.strategy import BooleanParameter, DecimalParameter, IntParameter, RealParameter + + +class HyperoptableStrategyV2(StrategyTestV2): + """ + Default Strategy provided by freqtrade bot. + Please do not modify this strategy, it's intended for internal use only. + Please look at the SampleStrategy in the user_data/strategy directory + or strategy repository https://github.com/freqtrade/freqtrade-strategies + for samples and inspiration. + """ + + buy_params = { + 'buy_rsi': 35, + # Intentionally not specified, so "default" is tested + # 'buy_plusdi': 0.4 + } + + sell_params = { + 'sell_rsi': 74, + 'sell_minusdi': 0.4 + } + + buy_plusdi = RealParameter(low=0, high=1, default=0.5, space='buy') + sell_rsi = IntParameter(low=50, high=100, default=70, space='sell') + sell_minusdi = DecimalParameter(low=0, high=1, default=0.5001, decimals=3, space='sell', + load=False) + protection_enabled = BooleanParameter(default=True) + protection_cooldown_lookback = IntParameter([0, 50], default=30) + + @property + def protections(self): + prot = [] + if self.protection_enabled.value: + prot.append({ + "method": "CooldownPeriod", + "stop_duration_candles": self.protection_cooldown_lookback.value + }) + return prot + + bot_loop_started = False + + def bot_loop_start(self): + self.bot_loop_started = True + + def bot_start(self, **kwargs) -> None: + """ + Parameters can also be defined here ... + """ + self.buy_rsi = IntParameter([0, 50], default=30, space='buy') diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index dca87e724..f6996a7a2 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -916,7 +916,7 @@ def test_hyperopt_parameters(): def test_auto_hyperopt_interface(default_conf): - default_conf.update({'strategy': 'HyperoptableStrategy'}) + default_conf.update({'strategy': 'HyperoptableStrategyV2'}) PairLocks.timeframe = default_conf['timeframe'] strategy = StrategyResolver.load_strategy(default_conf) strategy.ft_bot_start() diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index 666ae2b05..bdfcf3211 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -34,7 +34,7 @@ def test_search_all_strategies_no_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=False) assert isinstance(strategies, list) - assert len(strategies) == 6 + assert len(strategies) == 7 assert isinstance(strategies[0], dict) @@ -42,10 +42,10 @@ def test_search_all_strategies_with_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=True) assert isinstance(strategies, list) - assert len(strategies) == 7 + assert len(strategies) == 8 # with enum_failed=True search_all_objects() shall find 2 good strategies # and 1 which fails to load - assert len([x for x in strategies if x['class'] is not None]) == 6 + assert len([x for x in strategies if x['class'] is not None]) == 7 assert len([x for x in strategies if x['class'] is None]) == 1 From 2e642593e5e737ab87c3097574a4cb8def66f448 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Jul 2022 11:23:37 +0200 Subject: [PATCH 22/44] Update formatting of hyperopt_conf fixture --- tests/optimize/conftest.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/optimize/conftest.py b/tests/optimize/conftest.py index 8a9e0cbf0..9eb3a88cc 100644 --- a/tests/optimize/conftest.py +++ b/tests/optimize/conftest.py @@ -18,11 +18,11 @@ def hyperopt_conf(default_conf): 'runmode': RunMode.HYPEROPT, 'strategy': 'HyperoptableStrategy', 'hyperopt_loss': 'ShortTradeDurHyperOptLoss', - 'hyperopt_path': str(Path(__file__).parent / 'hyperopts'), - 'epochs': 1, - 'timerange': None, - 'spaces': ['default'], - 'hyperopt_jobs': 1, + 'hyperopt_path': str(Path(__file__).parent / 'hyperopts'), + 'epochs': 1, + 'timerange': None, + 'spaces': ['default'], + 'hyperopt_jobs': 1, 'hyperopt_min_trades': 1, }) return hyperconf From e53e53087491081b4b0f7e6f2f6c3afcd5ddefec Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Jul 2022 11:32:43 +0200 Subject: [PATCH 23/44] Add test showing broken inheritance hyperopt --- tests/optimize/test_hyperopt.py | 36 ++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 1ad8b33cf..fb5593ed2 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -855,7 +855,7 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None: 'strategy': 'HyperoptableStrategy', 'user_data_dir': Path(tmpdir), 'hyperopt_random_state': 42, - 'spaces': ['all'] + 'spaces': ['all'], }) hyperopt = Hyperopt(hyperopt_conf) hyperopt.backtesting.exchange.get_max_leverage = MagicMock(return_value=1.0) @@ -883,6 +883,40 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None: hyperopt.get_optimizer([], 2) +def test_in_strategy_auto_hyperopt_with_parallel(mocker, hyperopt_conf, tmpdir, fee) -> None: + # patch_exchange(mocker) + # TODO: This reaches out to the exchange! + mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + (Path(tmpdir) / 'hyperopt_results').mkdir(parents=True) + # No hyperopt needed + hyperopt_conf.update({ + 'strategy': 'HyperoptableStrategy', + 'user_data_dir': Path(tmpdir), + 'hyperopt_random_state': 42, + 'spaces': ['all'], + # Enforce parallelity + 'epochs': 2, + 'hyperopt_jobs': 2, + 'fee': fee.return_value, + }) + hyperopt = Hyperopt(hyperopt_conf) + hyperopt.backtesting.exchange.get_max_leverage = lambda *x, **xx: 1.0 + assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) + assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter) + assert hyperopt.backtesting.strategy.bot_loop_started is True + + assert hyperopt.backtesting.strategy.buy_rsi.in_space is True + assert hyperopt.backtesting.strategy.buy_rsi.value == 35 + assert hyperopt.backtesting.strategy.sell_rsi.value == 74 + assert hyperopt.backtesting.strategy.protection_cooldown_lookback.value == 30 + buy_rsi_range = hyperopt.backtesting.strategy.buy_rsi.range + assert isinstance(buy_rsi_range, range) + # Range from 0 - 50 (inclusive) + assert len(list(buy_rsi_range)) == 51 + + hyperopt.start() + + def test_SKDecimal(): space = SKDecimal(1, 2, decimals=2) assert 1.5 in space From 40e2da10f35f7b2769df9278d050f912d2650a39 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Jul 2022 11:05:58 +0200 Subject: [PATCH 24/44] Add hypeorpt cloudpickle magic closes #7078 --- freqtrade/optimize/hyperopt.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 7c7493590..566412f29 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -6,6 +6,7 @@ This module contains the hyperopt logic import logging import random +import sys import warnings from datetime import datetime, timezone from math import ceil @@ -17,6 +18,7 @@ import rapidjson from colorama import Fore, Style from colorama import init as colorama_init from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects +from joblib.externals import cloudpickle from pandas import DataFrame from freqtrade.constants import DATETIME_PRINT_FORMAT, FTHYPT_FILEVERSION, LAST_BT_RESULT_FN @@ -87,6 +89,7 @@ class Hyperopt: self.backtesting._set_strategy(self.backtesting.strategylist[0]) self.custom_hyperopt.strategy = self.backtesting.strategy + self.hyperopt_pickle_magic(self.backtesting.strategy.__class__.__bases__) self.custom_hyperoptloss: IHyperOptLoss = HyperOptLossResolver.load_hyperoptloss( self.config) self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function @@ -137,6 +140,17 @@ class Hyperopt: logger.info(f"Removing `{p}`.") p.unlink() + def hyperopt_pickle_magic(self, bases) -> None: + """ + Hyperopt magic to allow strategy inheritance across files. + For this to properly work, we need to register the module of the imported class + to pickle as value. + """ + for modules in bases: + if modules.__name__ != 'IStrategy': + cloudpickle.register_pickle_by_value(sys.modules[modules.__module__]) + self.hyperopt_pickle_magic(modules.__bases__) + def _get_params_dict(self, dimensions: List[Dimension], raw_params: List[Any]) -> Dict: # Ensure the number of dimensions match From 7c4dd4c48c7257f433669522ff1bd14be9d8133a Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 15 Jul 2022 09:54:53 +0200 Subject: [PATCH 25/44] Support fee cost as string closes #7056 --- freqtrade/exchange/exchange.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index cd13964c4..f6b85436d 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1646,7 +1646,7 @@ class Exchange: fee_curr = fee.get('currency') if fee_curr is None: return None - fee_cost = fee['cost'] + fee_cost = float(fee['cost']) if self._ft_has['fee_cost_in_contracts']: # Convert cost via "contracts" conversion fee_cost = self._contracts_to_amount(symbol, fee['cost']) @@ -1654,7 +1654,7 @@ class Exchange: # Calculate fee based on order details if fee_curr == self.get_pair_base_currency(symbol): # Base currency - divide by amount - return round(fee['cost'] / amount, 8) + return round(fee_cost / amount, 8) elif fee_curr == self.get_pair_quote_currency(symbol): # Quote currency - divide by cost return round(fee_cost / cost, 8) if cost else None @@ -1685,7 +1685,7 @@ class Exchange: :param amount: Amount of the order :return: Tuple with cost, currency, rate of the given fee dict """ - return (fee['cost'], + return (float(fee['cost']), fee['currency'], self.calculate_fee_rate( fee, From 7b8a5585dd6b2cb7cbfc550c30d2353af3869616 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 15 Jul 2022 14:35:49 +0200 Subject: [PATCH 26/44] Fetch 2ndary stoploss order once the order triggered. --- freqtrade/exchange/gateio.py | 14 +++++++++++++- tests/exchange/test_gateio.py | 19 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index b9de212de..f46508022 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -97,11 +97,23 @@ class Gateio(Exchange): return trades def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: - return self.fetch_order( + order = self.fetch_order( order_id=order_id, pair=pair, params={'stop': True} ) + if self.trading_mode == TradingMode.FUTURES: + if order['status'] == 'closed': + # Places a real order - which we need to fetch explicitly. + new_orderid = order.get('info', {}).get('trade_id') + if new_orderid: + order1 = self.fetch_order(order_id=new_orderid, pair=pair, params=params) + order1['id_stop'] = order1['id'] + order1['id'] = order_id + order1['stopPrice'] = order.get('stopPrice') + + return order1 + return order def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: return self.cancel_order( diff --git a/tests/exchange/test_gateio.py b/tests/exchange/test_gateio.py index cbd4776fb..dabdbba65 100644 --- a/tests/exchange/test_gateio.py +++ b/tests/exchange/test_gateio.py @@ -53,6 +53,25 @@ def test_fetch_stoploss_order_gateio(default_conf, mocker): assert fetch_order_mock.call_args_list[0][1]['pair'] == 'ETH/BTC' assert fetch_order_mock.call_args_list[0][1]['params'] == {'stop': True} + default_conf['trading_mode'] = 'futures' + default_conf['margin_mode'] = 'isolated' + + exchange = get_patched_exchange(mocker, default_conf, id='gateio') + + exchange.fetch_order = MagicMock(return_value={ + 'status': 'closed', + 'id': '1234', + 'stopPrice': 5.62, + 'info': { + 'trade_id': '222555' + } + }) + + exchange.fetch_stoploss_order('1234', 'ETH/BTC') + assert exchange.fetch_order.call_count == 2 + assert exchange.fetch_order.call_args_list[0][1]['order_id'] == '1234' + assert exchange.fetch_order.call_args_list[1][1]['order_id'] == '222555' + def test_cancel_stoploss_order_gateio(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, id='gateio') From 415780a4fe1c37785ebe26c398762f467ed5b570 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Jul 2022 11:58:38 +0200 Subject: [PATCH 27/44] gateio order cost is not in contracts closes #7081 --- freqtrade/exchange/exchange.py | 3 ++- freqtrade/exchange/gateio.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index f6b85436d..7ac45bcc9 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -79,6 +79,7 @@ class Exchange: "ccxt_futures_name": "swap", "fee_cost_in_contracts": False, # Fee cost needs contract conversion "needs_trading_fees": False, # use fetch_trading_fees to cache fees + "order_props_in_contracts": ['amount', 'cost', 'filled', 'remaining'], } _ft_has: Dict = {} _ft_has_futures: Dict = {} @@ -423,7 +424,7 @@ class Exchange: if 'symbol' in order and order['symbol'] is not None: contract_size = self._get_contract_size(order['symbol']) if contract_size != 1: - for prop in ['amount', 'cost', 'filled', 'remaining']: + for prop in self._ft_has.get('order_props_in_contracts', []): if prop in order and order[prop] is not None: order[prop] = order[prop] * contract_size return order diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index f46508022..c049ce4cc 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -34,6 +34,7 @@ class Gateio(Exchange): _ft_has_futures: Dict = { "needs_trading_fees": True, "fee_cost_in_contracts": False, # Set explicitly to false for clarity + "order_props_in_contracts": ['amount', 'filled', 'remaining'], } _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ From ed64e4299bb6b728513f3063a56e6fceda777e90 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Jul 2022 12:52:35 +0200 Subject: [PATCH 28/44] Stoploss orders should also be eligible to update closed fees --- freqtrade/exchange/gateio.py | 8 +++++++- freqtrade/freqtradebot.py | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index c049ce4cc..6df3425d2 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -1,12 +1,13 @@ """ Gate.io exchange subclass """ import logging from datetime import datetime -from typing import Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple from freqtrade.constants import BuySell from freqtrade.enums import MarginMode, TradingMode from freqtrade.exceptions import OperationalException from freqtrade.exchange import Exchange +from freqtrade.misc import safe_value_fallback2 logger = logging.getLogger(__name__) @@ -97,6 +98,11 @@ class Gateio(Exchange): } return trades + def get_order_id_conditional(self, order: Dict[str, Any]) -> str: + if self.trading_mode == TradingMode.FUTURES: + return safe_value_fallback2(order, order, 'id_stop', 'id') + return order['id'] + def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: order = self.fetch_order( order_id=order_id, diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 469bfda7e..2007f9b4e 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -332,6 +332,8 @@ class FreqtradeBot(LoggingMixin): if not trade.is_open and not trade.fee_updated(trade.exit_side): # Get sell fee order = trade.select_order(trade.exit_side, False) + if not order: + order = trade.select_order('stoploss', False) if order: logger.info( f"Updating {trade.exit_side}-fee on trade {trade}" From d03dfb393497fe63b042ebd22d932a0bcabcbfec Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Jul 2022 13:12:50 +0200 Subject: [PATCH 29/44] Oder cost is real cost (including leverage) --- freqtrade/exchange/exchange.py | 2 +- tests/exchange/test_exchange.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 7ac45bcc9..43fbbe28b 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -822,7 +822,7 @@ class Exchange: 'price': rate, 'average': rate, 'amount': _amount, - 'cost': _amount * rate / leverage, + 'cost': _amount * rate, 'type': ordertype, 'side': side, 'filled': 0, diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index acd48b3fd..bf4f44440 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1135,7 +1135,7 @@ def test_create_dry_run_order(default_conf, mocker, side, exchange_name, leverag assert order["symbol"] == "ETH/BTC" assert order["amount"] == 1 assert order["leverage"] == leverage - assert order["cost"] == 1 * 200 / leverage + assert order["cost"] == 1 * 200 @pytest.mark.parametrize("side,startprice,endprice", [ From 357000c478a078a22d09345887ee9955d41b9abe Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Jul 2022 13:27:13 +0200 Subject: [PATCH 30/44] Extract exchange validation to separate method --- freqtrade/exchange/exchange.py | 28 +++++++++++++++------------- tests/conftest.py | 5 +---- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index cd13964c4..505d02e8c 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -175,23 +175,11 @@ class Exchange: logger.info(f'Using Exchange "{self.name}"') if validate: - # Check if timeframe is available - self.validate_timeframes(config.get('timeframe')) - # Initial markets load self._load_markets() - - # Check if all pairs are available - self.validate_stakecurrency(config['stake_currency']) - if not exchange_config.get('skip_pair_validation'): - self.validate_pairs(config['exchange']['pair_whitelist']) - self.validate_ordertypes(config.get('order_types', {})) - self.validate_order_time_in_force(config.get('order_time_in_force', {})) + self.validate_config(config) self.required_candle_call_count = self.validate_required_startup_candles( config.get('startup_candle_count', 0), config.get('timeframe', '')) - self.validate_trading_mode_and_margin_mode(self.trading_mode, self.margin_mode) - self.validate_pricing(config['exit_pricing']) - self.validate_pricing(config['entry_pricing']) # Converts the interval provided in minutes in config to seconds self.markets_refresh_interval: int = exchange_config.get( @@ -214,6 +202,20 @@ class Exchange: logger.info("Closing async ccxt session.") self.loop.run_until_complete(self._api_async.close()) + def validate_config(self, config): + # Check if timeframe is available + self.validate_timeframes(config.get('timeframe')) + + # Check if all pairs are available + self.validate_stakecurrency(config['stake_currency']) + if not config['exchange'].get('skip_pair_validation'): + self.validate_pairs(config['exchange']['pair_whitelist']) + self.validate_ordertypes(config.get('order_types', {})) + self.validate_order_time_in_force(config.get('order_time_in_force', {})) + self.validate_trading_mode_and_margin_mode(self.trading_mode, self.margin_mode) + self.validate_pricing(config['exit_pricing']) + self.validate_pricing(config['entry_pricing']) + def _init_ccxt(self, exchange_config: Dict[str, Any], ccxt_module: CcxtModuleType = ccxt, ccxt_kwargs: Dict = {}) -> ccxt.Exchange: """ diff --git a/tests/conftest.py b/tests/conftest.py index 0cf32545f..3158e9ede 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -112,11 +112,8 @@ def patch_exchange( mock_supported_modes=True ) -> None: mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock(return_value={})) - mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) + mocker.patch('freqtrade.exchange.Exchange.validate_config', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange.validate_ordertypes', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange.validate_pricing') mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id)) mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title())) mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2)) From bf07d8fe879ca592db74b1bf61bb59cd8441bee4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Jul 2022 13:57:04 +0200 Subject: [PATCH 31/44] Update test to properly patch/mock exchange --- tests/optimize/test_hyperopt.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index fb5593ed2..0f615b7a3 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -1,7 +1,7 @@ # pragma pylint: disable=missing-docstring,W0212,C0103 from datetime import datetime, timedelta from pathlib import Path -from unittest.mock import ANY, MagicMock +from unittest.mock import ANY, MagicMock, PropertyMock import pandas as pd import pytest @@ -18,8 +18,8 @@ from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.optimize.optimize_reports import generate_strategy_stats from freqtrade.optimize.space import SKDecimal from freqtrade.strategy import IntParameter -from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, log_has, log_has_re, patch_exchange, - patched_configuration_load_config_file) +from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, get_markets, log_has, log_has_re, + patch_exchange, patched_configuration_load_config_file) def generate_result_metrics(): @@ -884,9 +884,11 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None: def test_in_strategy_auto_hyperopt_with_parallel(mocker, hyperopt_conf, tmpdir, fee) -> None: - # patch_exchange(mocker) - # TODO: This reaches out to the exchange! + mocker.patch('freqtrade.exchange.Exchange.validate_config', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch('freqtrade.exchange.Exchange._load_markets') + mocker.patch('freqtrade.exchange.Exchange.markets', + PropertyMock(return_value=get_markets())) (Path(tmpdir) / 'hyperopt_results').mkdir(parents=True) # No hyperopt needed hyperopt_conf.update({ @@ -901,6 +903,9 @@ def test_in_strategy_auto_hyperopt_with_parallel(mocker, hyperopt_conf, tmpdir, }) hyperopt = Hyperopt(hyperopt_conf) hyperopt.backtesting.exchange.get_max_leverage = lambda *x, **xx: 1.0 + hyperopt.backtesting.exchange.get_min_pair_stake_amount = lambda *x, **xx: 1.0 + hyperopt.backtesting.exchange.get_max_pair_stake_amount = lambda *x, **xx: 100.0 + assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter) assert hyperopt.backtesting.strategy.bot_loop_started is True From 8b2535a8da3eabbbb53a70eeebb6348afaf0edd6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Jul 2022 15:42:17 +0200 Subject: [PATCH 32/44] Update Typing for fees --- freqtrade/constants.py | 1 + freqtrade/exchange/exchange.py | 11 ++++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index ce7c0ff83..6d74ceafd 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -542,3 +542,4 @@ TradeList = List[List] LongShort = Literal['long', 'short'] EntryExit = Literal['entry', 'exit'] BuySell = Literal['buy', 'sell'] +MakerTaker = Literal['maker', 'taker'] diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index db7a8ce41..2deb2b70d 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -20,7 +20,7 @@ from ccxt import ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, Precise, decimal_to_ from pandas import DataFrame from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BuySell, - EntryExit, ListPairsWithTimeframes, PairWithTimeframe) + EntryExit, ListPairsWithTimeframes, MakerTaker, PairWithTimeframe) from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, @@ -870,7 +870,8 @@ class Exchange: 'filled': _amount, 'cost': (dry_order['amount'] * average) / leverage }) - dry_order = self.add_dry_order_fee(pair, dry_order, self.taker_or_maker('entry')) + # market orders will always incurr taker fees + dry_order = self.add_dry_order_fee(pair, dry_order, 'taker') dry_order = self.check_dry_limit_order_filled(dry_order) @@ -882,7 +883,7 @@ class Exchange: self, pair: str, dry_order: Dict[str, Any], - taker_or_maker: Literal['taker', 'maker'], + taker_or_maker: MakerTaker, ) -> Dict[str, Any]: dry_order.update({ 'fee': { @@ -970,7 +971,7 @@ class Exchange: }) enter_long = not order['is_short'] and order['side'] == 'buy' enter_short = order['is_short'] and order['side'] == 'sell' - entry_or_exit: Literal['entry', 'exit'] = ( + entry_or_exit: EntryExit = ( 'entry' if (enter_short or enter_long) else 'exit' ) self.add_dry_order_fee( @@ -1635,7 +1636,7 @@ class Exchange: @retrier def get_fee(self, symbol: str, type: str = '', side: str = '', amount: float = 1, - price: float = 1, taker_or_maker: str = 'maker') -> float: + price: float = 1, taker_or_maker: MakerTaker = 'maker') -> float: try: if self._config['dry_run'] and self._config.get('fee', None) is not None: return self._config['fee'] From 4172f92bfc492a140103d79027fc326348183cc0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Jul 2022 17:03:45 +0200 Subject: [PATCH 33/44] simplify dry-run taker/maker selection --- freqtrade/exchange/exchange.py | 33 ++++--------------------- tests/exchange/test_exchange.py | 43 ++++++++++++--------------------- 2 files changed, 20 insertions(+), 56 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 2deb2b70d..efb33ee74 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -811,26 +811,6 @@ class Exchange: # Dry-run methods - def taker_or_maker( - self, - order_reason: Literal['entry', 'exit', 'stoploss'], # TODO: stoploss - ): - order_type = self._config['order_types'][order_reason] - if order_type == 'market' or order_reason == 'stoploss': - return 'taker' - else: - return ( - 'maker' if ( - ( - order_reason == 'entry' and - self._config['entry_pricing']['price_side'] == 'same' - ) or ( - order_reason == 'exit' and - self._config['exit_pricing']['price_side'] == 'same' - ) - ) else 'taker' - ) - def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, rate: float, leverage: float, params: Dict = {}, stop_loss: bool = False) -> Dict[str, Any]: @@ -873,7 +853,7 @@ class Exchange: # market orders will always incurr taker fees dry_order = self.add_dry_order_fee(pair, dry_order, 'taker') - dry_order = self.check_dry_limit_order_filled(dry_order) + dry_order = self.check_dry_limit_order_filled(dry_order, immediate=True) self._dry_run_open_orders[dry_order["id"]] = dry_order # Copy order and close it - so the returned order is open unless it's a market order @@ -955,7 +935,8 @@ class Exchange: pass return False - def check_dry_limit_order_filled(self, order: Dict[str, Any]) -> Dict[str, Any]: + def check_dry_limit_order_filled( + self, order: Dict[str, Any], immediate: bool = False) -> Dict[str, Any]: """ Check dry-run limit order fill and update fee (if it filled). """ @@ -969,15 +950,11 @@ class Exchange: 'filled': order['amount'], 'remaining': 0, }) - enter_long = not order['is_short'] and order['side'] == 'buy' - enter_short = order['is_short'] and order['side'] == 'sell' - entry_or_exit: EntryExit = ( - 'entry' if (enter_short or enter_long) else 'exit' - ) + self.add_dry_order_fee( pair, order, - self.taker_or_maker(entry_or_exit) + 'taker' if immediate else 'maker', ) return order diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 0c4df8f5a..ff8b4b40c 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1160,23 +1160,33 @@ def test_create_dry_run_order_fees( price_side, fee, ): - default_conf[f"{order_reason}_pricing"]["price_side"] = "same" - default_conf["order_types"][order_reason] = order_type mocker.patch( 'freqtrade.exchange.Exchange.get_fee', - lambda symbol, taker_or_maker: 2.0 if taker_or_maker == 'taker' else 1.0 + side_effect=lambda symbol, taker_or_maker: 2.0 if taker_or_maker == 'taker' else 1.0 ) + mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled', + return_value=price_side == 'other') exchange = get_patched_exchange(mocker, default_conf) order = exchange.create_dry_run_order( - pair='ADA/USDT', + pair='LTC/USDT', ordertype=order_type, side=side, amount=10, rate=2.0, + leverage=1.0 ) + if price_side == 'other' or order_type == 'market': + assert order['fee']['rate'] == fee + return + else: + assert order['fee'] is None - assert order['ft_fee_base'] == fee + mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled', + return_value=price_side != 'other') + + order1 = exchange.fetch_dry_run_order(order['id']) + assert order1['fee']['rate'] == fee @pytest.mark.parametrize("side,startprice,endprice", [ @@ -5101,26 +5111,3 @@ def test_stoploss_contract_size(mocker, default_conf, contract_size, order_amoun assert order['cost'] == 100 assert order['filled'] == 100 assert order['remaining'] == 100 - - -@pytest.mark.parametrize('order_reason,price_side,order_type,taker_or_maker', [ - ("entry", "same", "limit", "maker"), - ("exit", "same", "limit", "maker"), - ("entry", "other", "limit", "taker"), - ("exit", "other", "limit", "taker"), - ("stoploss", "same", "limit", "taker"), - ("stoploss", "other", "limit", "taker"), - ("entry", "same", "market", "taker"), - ("exit", "same", "market", "taker"), - ("entry", "other", "market", "taker"), - ("exit", "other", "market", "taker"), - ("stoploss", "same", "market", "taker"), - ("stoploss", "other", "market", "taker"), -]) -def test_taker_or_maker(mocker, default_conf, order_reason, price_side, order_type, taker_or_maker): - if order_reason != 'stoploss': - default_conf[f"{order_reason}_pricing"]["price_side"] = price_side - default_conf["order_types"] = {} - default_conf["order_types"][order_reason] = order_type - exchange = get_patched_exchange(mocker, default_conf) - assert exchange.taker_or_maker(order_reason) == taker_or_maker From 423af371c0ec8822f03cd9f10528e2c0d58e3512 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Jul 2022 17:59:05 +0200 Subject: [PATCH 34/44] Simplify calculation by calling "get_fee" only once --- freqtrade/exchange/exchange.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index efb33ee74..a430cdac5 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -865,11 +865,12 @@ class Exchange: dry_order: Dict[str, Any], taker_or_maker: MakerTaker, ) -> Dict[str, Any]: + fee = self.get_fee(pair, taker_or_maker=taker_or_maker) dry_order.update({ 'fee': { 'currency': self.get_pair_quote_currency(pair), - 'cost': dry_order['cost'] * self.get_fee(pair, taker_or_maker=taker_or_maker), - 'rate': self.get_fee(pair, taker_or_maker=taker_or_maker) + 'cost': dry_order['cost'] * fee, + 'rate': fee } }) return dry_order From 9347677c6061648517767dce5916dcf0394c71e6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Jul 2022 19:33:26 +0200 Subject: [PATCH 35/44] Uppdate pricecontours test to not recreate backtesting every loop in hopes to fix random failure --- tests/optimize/test_backtesting.py | 61 ++++++++++++++++++------------ 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 6912184aa..0b964c54a 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -90,28 +90,6 @@ def load_data_test(what, testdatadir): fill_missing=True)} -def simple_backtest(config, contour, mocker, testdatadir) -> None: - patch_exchange(mocker) - config['timeframe'] = '1m' - backtesting = Backtesting(config) - backtesting._set_strategy(backtesting.strategylist[0]) - - data = load_data_test(contour, testdatadir) - processed = backtesting.strategy.advise_all_indicators(data) - min_date, max_date = get_timerange(processed) - assert isinstance(processed, dict) - results = backtesting.backtest( - processed=processed, - start_date=min_date, - end_date=max_date, - max_open_trades=1, - position_stacking=False, - enable_protections=config.get('enable_protections', False), - ) - # results :: - return results - - # FIX: fixturize this? def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC'): data = history.load_data(datadir=datadir, timeframe='1m', pairs=[pair]) @@ -942,6 +920,7 @@ def test_backtest_dataprovider_analyzed_df(default_conf, fee, mocker, testdatadi def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatadir) -> None: # While this test IS a copy of test_backtest_pricecontours, it's needed to ensure # results do not carry-over to the next run, which is not given by using parametrize. + patch_exchange(mocker) default_conf['protections'] = [ { "method": "CooldownPeriod", @@ -949,6 +928,7 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad }] default_conf['enable_protections'] = True + default_conf['timeframe'] = '1m' mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) @@ -959,12 +939,27 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad ['sine', 9], ['raise', 10], ] + backtesting = Backtesting(default_conf) + backtesting._set_strategy(backtesting.strategylist[0]) + # While entry-signals are unrealistic, running backtesting # over and over again should not cause different results for [contour, numres] in tests: # Debug output for random test failure print(f"{contour}, {numres}") - assert len(simple_backtest(default_conf, contour, mocker, testdatadir)['results']) == numres + data = load_data_test(contour, testdatadir) + processed = backtesting.strategy.advise_all_indicators(data) + min_date, max_date = get_timerange(processed) + assert isinstance(processed, dict) + results = backtesting.backtest( + processed=processed, + start_date=min_date, + end_date=max_date, + max_open_trades=1, + position_stacking=False, + enable_protections=default_conf.get('enable_protections', False), + ) + assert len(results['results']) == numres @pytest.mark.parametrize('protections,contour,expected', [ @@ -990,7 +985,25 @@ def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir, mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) # While entry-signals are unrealistic, running backtesting # over and over again should not cause different results - assert len(simple_backtest(default_conf, contour, mocker, testdatadir)['results']) == expected + + patch_exchange(mocker) + default_conf['timeframe'] = '1m' + backtesting = Backtesting(default_conf) + backtesting._set_strategy(backtesting.strategylist[0]) + + data = load_data_test(contour, testdatadir) + processed = backtesting.strategy.advise_all_indicators(data) + min_date, max_date = get_timerange(processed) + assert isinstance(processed, dict) + results = backtesting.backtest( + processed=processed, + start_date=min_date, + end_date=max_date, + max_open_trades=1, + position_stacking=False, + enable_protections=default_conf.get('enable_protections', False), + ) + assert len(results['results']) == expected def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir): From 05a5ae4fcfac1423a67b80411535a0b2ba07da27 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Jul 2022 22:28:46 +0200 Subject: [PATCH 36/44] Update plotting to use entry/exit terminology --- freqtrade/plot/plotting.py | 22 +++++++++++----------- tests/test_plotting.py | 36 ++++++++++++++++++------------------ 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index a64281156..f8e95300a 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -255,18 +255,18 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: """ # Trades can be empty if trades is not None and len(trades) > 0: - # Create description for sell summarizing the trade + # Create description for exit summarizing the trade trades['desc'] = trades.apply( lambda row: f"{row['profit_ratio']:.2%}, " + (f"{row['enter_tag']}, " if row['enter_tag'] is not None else "") + f"{row['exit_reason']}, " + f"{row['trade_duration']} min", axis=1) - trade_buys = go.Scatter( + trade_entries = go.Scatter( x=trades["open_date"], y=trades["open_rate"], mode='markers', - name='Trade buy', + name='Trade entry', text=trades["desc"], marker=dict( symbol='circle-open', @@ -277,12 +277,12 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: ) ) - trade_sells = go.Scatter( + trade_exits = go.Scatter( x=trades.loc[trades['profit_ratio'] > 0, "close_date"], y=trades.loc[trades['profit_ratio'] > 0, "close_rate"], text=trades.loc[trades['profit_ratio'] > 0, "desc"], mode='markers', - name='Sell - Profit', + name='Exit - Profit', marker=dict( symbol='square-open', size=11, @@ -290,12 +290,12 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: color='green' ) ) - trade_sells_loss = go.Scatter( + trade_exits_loss = go.Scatter( x=trades.loc[trades['profit_ratio'] <= 0, "close_date"], y=trades.loc[trades['profit_ratio'] <= 0, "close_rate"], text=trades.loc[trades['profit_ratio'] <= 0, "desc"], mode='markers', - name='Sell - Loss', + name='Exit - Loss', marker=dict( symbol='square-open', size=11, @@ -303,9 +303,9 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: color='red' ) ) - fig.add_trace(trade_buys, 1, 1) - fig.add_trace(trade_sells, 1, 1) - fig.add_trace(trade_sells_loss, 1, 1) + fig.add_trace(trade_entries, 1, 1) + fig.add_trace(trade_exits, 1, 1) + fig.add_trace(trade_exits_loss, 1, 1) else: logger.warning("No trades found.") return fig @@ -444,7 +444,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra Generate the graph from the data generated by Backtesting or from DB Volume will always be ploted in row2, so Row 1 and 3 are to our disposal for custom indicators :param pair: Pair to Display on the graph - :param data: OHLCV DataFrame containing indicators and buy/sell signals + :param data: OHLCV DataFrame containing indicators and entry/exit signals :param trades: All trades created :param indicators1: List containing Main plot indicators :param indicators2: List containing Sub plot indicators diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 9ee7a75c6..52e96e477 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -72,7 +72,7 @@ def test_add_indicators(default_conf, testdatadir, caplog): strategy = StrategyResolver.load_strategy(default_conf) - # Generate buy/sell signals and indicators + # Generate entry/exit signals and indicators data = strategy.analyze_ticker(data, {'pair': pair}) fig = generate_empty_figure() @@ -113,7 +113,7 @@ def test_add_areas(default_conf, testdatadir, caplog): ind_plain = {"macd": {"fill_to": "macdhist"}} strategy = StrategyResolver.load_strategy(default_conf) - # Generate buy/sell signals and indicators + # Generate entry/exit signals and indicators data = strategy.analyze_ticker(data, {'pair': pair}) fig = generate_empty_figure() @@ -165,24 +165,24 @@ def test_plot_trades(testdatadir, caplog): fig = plot_trades(fig, trades) figure = fig1.layout.figure - # Check buys - color, should be in first graph, ... - trade_buy = find_trace_in_fig_data(figure.data, 'Trade buy') - assert isinstance(trade_buy, go.Scatter) - assert trade_buy.yaxis == 'y' - assert len(trades) == len(trade_buy.x) - assert trade_buy.marker.color == 'cyan' - assert trade_buy.marker.symbol == 'circle-open' - assert trade_buy.text[0] == '3.99%, buy_tag, roi, 15 min' + # Check entry - color, should be in first graph, ... + trade_entries = find_trace_in_fig_data(figure.data, 'Trade entry') + assert isinstance(trade_entries, go.Scatter) + assert trade_entries.yaxis == 'y' + assert len(trades) == len(trade_entries.x) + assert trade_entries.marker.color == 'cyan' + assert trade_entries.marker.symbol == 'circle-open' + assert trade_entries.text[0] == '3.99%, buy_tag, roi, 15 min' - trade_sell = find_trace_in_fig_data(figure.data, 'Sell - Profit') - assert isinstance(trade_sell, go.Scatter) - assert trade_sell.yaxis == 'y' - assert len(trades.loc[trades['profit_ratio'] > 0]) == len(trade_sell.x) - assert trade_sell.marker.color == 'green' - assert trade_sell.marker.symbol == 'square-open' - assert trade_sell.text[0] == '3.99%, buy_tag, roi, 15 min' + trade_exit = find_trace_in_fig_data(figure.data, 'Exit - Profit') + assert isinstance(trade_exit, go.Scatter) + assert trade_exit.yaxis == 'y' + assert len(trades.loc[trades['profit_ratio'] > 0]) == len(trade_exit.x) + assert trade_exit.marker.color == 'green' + assert trade_exit.marker.symbol == 'square-open' + assert trade_exit.text[0] == '3.99%, buy_tag, roi, 15 min' - trade_sell_loss = find_trace_in_fig_data(figure.data, 'Sell - Loss') + trade_sell_loss = find_trace_in_fig_data(figure.data, 'Exit - Loss') assert isinstance(trade_sell_loss, go.Scatter) assert trade_sell_loss.yaxis == 'y' assert len(trades.loc[trades['profit_ratio'] <= 0]) == len(trade_sell_loss.x) From 46be1b8778c9729f16651c2b8f00ac78dd98de98 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 17 Jul 2022 07:21:42 +0200 Subject: [PATCH 37/44] Version bump ccxt to 1.90.88 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2bb3b4b75..49121a9da 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.23.1 pandas==1.4.3 pandas-ta==0.3.14b -ccxt==1.90.47 +ccxt==1.90.88 # Pin cryptography for now due to rust build errors with piwheels cryptography==37.0.4 aiohttp==3.8.1 From 2c6fb617a6574ac9342fdbaa937938e581de525a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Jul 2022 03:01:23 +0000 Subject: [PATCH 38/44] Bump jsonschema from 4.6.2 to 4.7.2 Bumps [jsonschema](https://github.com/python-jsonschema/jsonschema) from 4.6.2 to 4.7.2. - [Release notes](https://github.com/python-jsonschema/jsonschema/releases) - [Changelog](https://github.com/python-jsonschema/jsonschema/blob/main/CHANGELOG.rst) - [Commits](https://github.com/python-jsonschema/jsonschema/compare/v4.6.2...v4.7.2) --- updated-dependencies: - dependency-name: jsonschema dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 49121a9da..78a327988 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ arrow==1.2.2 cachetools==4.2.2 requests==2.28.1 urllib3==1.26.10 -jsonschema==4.6.2 +jsonschema==4.7.2 TA-Lib==0.4.24 technical==1.3.0 tabulate==0.8.10 From 5f820ab0a64a0ebe0152c74901cb890d8a8a0cd9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Jul 2022 03:01:26 +0000 Subject: [PATCH 39/44] Bump pytest-asyncio from 0.18.3 to 0.19.0 Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.18.3 to 0.19.0. - [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases) - [Changelog](https://github.com/pytest-dev/pytest-asyncio/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.18.3...v0.19.0) --- updated-dependencies: - dependency-name: pytest-asyncio dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index f2f77c2ba..ac7f4f619 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,7 +10,7 @@ flake8-tidy-imports==4.8.0 mypy==0.961 pre-commit==2.20.0 pytest==7.1.2 -pytest-asyncio==0.18.3 +pytest-asyncio==0.19.0 pytest-cov==3.0.0 pytest-mock==3.8.2 pytest-random-order==1.0.4 From cb63d5e3dfe9dc9b8275098ba8b7cf6dc446ac2f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Jul 2022 03:01:31 +0000 Subject: [PATCH 40/44] Bump fastapi from 0.78.0 to 0.79.0 Bumps [fastapi](https://github.com/tiangolo/fastapi) from 0.78.0 to 0.79.0. - [Release notes](https://github.com/tiangolo/fastapi/releases) - [Commits](https://github.com/tiangolo/fastapi/compare/0.78.0...0.79.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 49121a9da..18c656bfb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,7 +34,7 @@ orjson==3.7.7 sdnotify==0.3.2 # API Server -fastapi==0.78.0 +fastapi==0.79.0 uvicorn==0.18.2 pyjwt==2.4.0 aiofiles==0.8.0 From f07ad7aa875a3c0c53de3c70b73330a90676d6c4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Jul 2022 03:01:40 +0000 Subject: [PATCH 41/44] Bump ccxt from 1.90.88 to 1.90.89 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.90.88 to 1.90.89. - [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.90.88...1.90.89) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 49121a9da..9c9973ee8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.23.1 pandas==1.4.3 pandas-ta==0.3.14b -ccxt==1.90.88 +ccxt==1.90.89 # Pin cryptography for now due to rust build errors with piwheels cryptography==37.0.4 aiohttp==3.8.1 From ea523136fc29cdf62a14c59e85edfe089ac03b1c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Jul 2022 03:01:49 +0000 Subject: [PATCH 42/44] Bump types-requests from 2.28.0 to 2.28.1 Bumps [types-requests](https://github.com/python/typeshed) from 2.28.0 to 2.28.1. - [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 f2f77c2ba..435cb9b1c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -24,6 +24,6 @@ nbconvert==6.5.0 # mypy types types-cachetools==5.2.1 types-filelock==3.2.7 -types-requests==2.28.0 +types-requests==2.28.1 types-tabulate==0.8.11 types-python-dateutil==2.8.18 From 0daa9d3e57bfa09f98c17d14258dd9fad32803dd Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 18 Jul 2022 07:56:41 +0200 Subject: [PATCH 43/44] Bump types-requests in pre-commit --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 59e7f6894..a23181c37 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: additional_dependencies: - types-cachetools==5.2.1 - types-filelock==3.2.7 - - types-requests==2.28.0 + - types-requests==2.28.1 - types-tabulate==0.8.11 - types-python-dateutil==2.8.18 # stages: [push] From 75e190ff1ddfecfd1796d99dbde4a060a18e96b7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 18 Jul 2022 20:15:07 +0200 Subject: [PATCH 44/44] Update sell-test without filled buy order --- tests/test_freqtradebot.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 4963e2b0a..e431e7ac3 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2060,8 +2060,9 @@ def test_update_trade_state_orderexception(mocker, default_conf_usdt, caplog) -> @pytest.mark.parametrize("is_short", [False, True]) def test_update_trade_state_sell( - default_conf_usdt, trades_for_order, limit_order_open, limit_order, is_short, mocker, + default_conf_usdt, trades_for_order, limit_order_open, limit_order, is_short, mocker ): + buy_order = limit_order[entry_side(is_short)] open_order = limit_order_open[exit_side(is_short)] l_order = limit_order[exit_side(is_short)] mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) @@ -2088,6 +2089,9 @@ def test_update_trade_state_sell( leverage=1, is_short=is_short, ) + order = Order.parse_from_ccxt_object(buy_order, 'LTC/ETH', entry_side(is_short)) + trade.orders.append(order) + order = Order.parse_from_ccxt_object(open_order, 'LTC/ETH', exit_side(is_short)) trade.orders.append(order) assert order.status == 'open' @@ -2787,6 +2791,7 @@ def test_manage_open_orders_partial( rpc_mock = patch_RPCManager(mocker) open_trade.is_short = is_short open_trade.leverage = leverage + open_trade.orders[0].ft_order_side = 'sell' if is_short else 'buy' limit_buy_order_old_partial['id'] = open_trade.open_order_id limit_buy_order_old_partial['side'] = 'sell' if is_short else 'buy' limit_buy_canceled = deepcopy(limit_buy_order_old_partial) @@ -2872,6 +2877,7 @@ def test_manage_open_orders_partial_except( limit_buy_order_old_partial_canceled, mocker ) -> None: open_trade.is_short = is_short + open_trade.orders[0].ft_order_side = 'sell' if is_short else 'buy' rpc_mock = patch_RPCManager(mocker) limit_buy_order_old_partial_canceled['id'] = open_trade.open_order_id limit_buy_order_old_partial['id'] = open_trade.open_order_id @@ -3626,7 +3632,7 @@ def test_execute_trade_exit_market_order( 'freqtrade.exchange.Exchange', fetch_ticker=ticker_usdt, get_fee=fee, - _is_dry_limit_order_filled=MagicMock(return_value=False), + _is_dry_limit_order_filled=MagicMock(return_value=True), ) patch_whitelist(mocker, default_conf_usdt) freqtrade = FreqtradeBot(default_conf_usdt) @@ -3642,7 +3648,8 @@ def test_execute_trade_exit_market_order( # Increase the price and sell it mocker.patch.multiple( 'freqtrade.exchange.Exchange', - fetch_ticker=ticker_usdt_sell_up + fetch_ticker=ticker_usdt_sell_up, + _is_dry_limit_order_filled=MagicMock(return_value=False), ) freqtrade.config['order_types']['exit'] = 'market' @@ -3655,7 +3662,7 @@ def test_execute_trade_exit_market_order( assert not trade.is_open assert trade.close_profit == profit_ratio - assert rpc_mock.call_count == 3 + assert rpc_mock.call_count == 4 last_msg = rpc_mock.call_args_list[-2][0][0] assert { 'type': RPCMessageType.EXIT,