From 1d40162e9d1660a73d945a4bf7a30b48cc70ede4 Mon Sep 17 00:00:00 2001 From: Bohdan Kamuz Date: Sat, 15 Apr 2023 15:57:31 +0000 Subject: [PATCH 01/85] Telegram. Fixed the blacklist removal message. --- freqtrade/rpc/telegram.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 51f7fb9ee..92d2c13a5 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1413,12 +1413,12 @@ class Telegram(RPCHandler): Handler for /blacklist Shows the currently active blacklist """ - self.send_blacklist_msg(self._rpc._rpc_blacklist(context.args)) + self.send_blacklist_msg(self._rpc._rpc_blacklist(context.args), ['adding', 'to']) - def send_blacklist_msg(self, blacklist: Dict): + def send_blacklist_msg(self, blacklist: Dict, action): errmsgs = [] for pair, error in blacklist['errors'].items(): - errmsgs.append(f"Error adding `{pair}` to blacklist: `{error['error_msg']}`") + errmsgs.append(f"Error {action[0]} `{pair}` {action[1]} blacklist: `{error['error_msg']}`") if errmsgs: self._send_msg('\n'.join(errmsgs)) @@ -1434,7 +1434,7 @@ class Telegram(RPCHandler): Handler for /bl_delete Deletes pair(s) from current blacklist """ - self.send_blacklist_msg(self._rpc._rpc_blacklist_delete(context.args or [])) + self.send_blacklist_msg(self._rpc._rpc_blacklist_delete(context.args or []), ['deleting', 'from']) @authorized_only def _logs(self, update: Update, context: CallbackContext) -> None: From d34b15d6a9c4202b7b5ed2368abfa004561ef959 Mon Sep 17 00:00:00 2001 From: Bohdan Kamuz Date: Mon, 17 Apr 2023 09:40:41 +0000 Subject: [PATCH 02/85] Telegram. Fixed the blacklist removal message --- freqtrade/rpc/telegram.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 92d2c13a5..a26841e14 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1413,12 +1413,13 @@ class Telegram(RPCHandler): Handler for /blacklist Shows the currently active blacklist """ - self.send_blacklist_msg(self._rpc._rpc_blacklist(context.args), ['adding', 'to']) + self.send_blacklist_msg(self._rpc._rpc_blacklist(context.args), 1) - def send_blacklist_msg(self, blacklist: Dict, action): + def send_blacklist_msg(self, blacklist: Dict, des): errmsgs = [] + act = ['adding', 'to'] if des == 1 else ['deleting', 'from'] for pair, error in blacklist['errors'].items(): - errmsgs.append(f"Error {action[0]} `{pair}` {action[1]} blacklist: `{error['error_msg']}`") + errmsgs.append(f"Error {act[0]} `{pair}` {act[1]} blacklist: `{error['error_msg']}`") if errmsgs: self._send_msg('\n'.join(errmsgs)) @@ -1434,7 +1435,7 @@ class Telegram(RPCHandler): Handler for /bl_delete Deletes pair(s) from current blacklist """ - self.send_blacklist_msg(self._rpc._rpc_blacklist_delete(context.args or []), ['deleting', 'from']) + self.send_blacklist_msg(self._rpc._rpc_blacklist_delete(context.args or []), 0) @authorized_only def _logs(self, update: Update, context: CallbackContext) -> None: From 6f401a9e150be8f569396e129fcf698182488a1f Mon Sep 17 00:00:00 2001 From: Joe Schr <8218910+TheJoeSchr@users.noreply.github.com> Date: Tue, 18 Apr 2023 11:10:46 +0200 Subject: [PATCH 03/85] docs: use helper function `stoploss_from_absolute` in strategy callbacks "absolute" example --- docs/strategy-callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 329908527..070eb6d48 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -352,7 +352,7 @@ class AwesomeStrategy(IStrategy): # Convert absolute price to percentage relative to current_rate if stoploss_price < current_rate: - return (stoploss_price / current_rate) - 1 + return stoploss_from_absolute(stoploss_price, current_rate, is_short=trade.is_short)) # return maximum stoploss value, keeping current stoploss price unchanged return 1 From c297d999750ada0e7c01f8b10b1d6fae65a20084 Mon Sep 17 00:00:00 2001 From: Bohdan Kamuz Date: Tue, 18 Apr 2023 10:09:48 +0000 Subject: [PATCH 04/85] Telegram. Fixed the blacklist removal message --- freqtrade/rpc/telegram.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index a26841e14..e626ee598 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1413,13 +1413,12 @@ class Telegram(RPCHandler): Handler for /blacklist Shows the currently active blacklist """ - self.send_blacklist_msg(self._rpc._rpc_blacklist(context.args), 1) + self.send_blacklist_msg(self._rpc._rpc_blacklist(context.args)) - def send_blacklist_msg(self, blacklist: Dict, des): + def send_blacklist_msg(self, blacklist: Dict): errmsgs = [] - act = ['adding', 'to'] if des == 1 else ['deleting', 'from'] for pair, error in blacklist['errors'].items(): - errmsgs.append(f"Error {act[0]} `{pair}` {act[1]} blacklist: `{error['error_msg']}`") + errmsgs.append(f"Error: {error['error_msg']}") if errmsgs: self._send_msg('\n'.join(errmsgs)) @@ -1435,7 +1434,7 @@ class Telegram(RPCHandler): Handler for /bl_delete Deletes pair(s) from current blacklist """ - self.send_blacklist_msg(self._rpc._rpc_blacklist_delete(context.args or []), 0) + self.send_blacklist_msg(self._rpc._rpc_blacklist_delete(context.args or [])) @authorized_only def _logs(self, update: Update, context: CallbackContext) -> None: From f9124ef5b9c5150f2d3edf58904d8d562cf74748 Mon Sep 17 00:00:00 2001 From: Joe Schr <8218910+TheJoeSchr@users.noreply.github.com> Date: Tue, 18 Apr 2023 17:36:36 +0200 Subject: [PATCH 05/85] docs: strategy-callbacks: removes outdated `leverage` argument --- docs/strategy-callbacks.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 070eb6d48..a99b2ae29 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -318,11 +318,11 @@ class AwesomeStrategy(IStrategy): # evaluate highest to lowest, so that highest possible stop is used if current_profit > 0.40: - return stoploss_from_open(0.25, current_profit, is_short=trade.is_short, leverage=trade.leverage) + return stoploss_from_open(0.25, current_profit, is_short=trade.is_short) elif current_profit > 0.25: - return stoploss_from_open(0.15, current_profit, is_short=trade.is_short, leverage=trade.leverage) + return stoploss_from_open(0.15, current_profit, is_short=trade.is_short) elif current_profit > 0.20: - return stoploss_from_open(0.07, current_profit, is_short=trade.is_short, leverage=trade.leverage) + return stoploss_from_open(0.07, current_profit, is_short=trade.is_short) # return maximum stoploss value, keeping current stoploss price unchanged return 1 From caf524c68584995e5ef6e4d2ba165ca4279e264f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 18 Apr 2023 18:01:12 +0200 Subject: [PATCH 06/85] Don't fail on leverage tier loading error closes #8512 --- freqtrade/exchange/exchange.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 358e71c85..7e276d538 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2431,14 +2431,17 @@ class Exchange: def load_cached_leverage_tiers(self, stake_currency: str) -> Optional[Dict[str, List[Dict]]]: filename = self._config['datadir'] / "futures" / f"leverage_tiers_{stake_currency}.json" if filename.is_file(): - tiers = file_load_json(filename) - updated = tiers.get('updated') - if updated: - updated_dt = parser.parse(updated) - if updated_dt < datetime.now(timezone.utc) - timedelta(weeks=4): - logger.info("Cached leverage tiers are outdated. Will update.") - return None - return tiers['data'] + try: + tiers = file_load_json(filename) + updated = tiers.get('updated') + if updated: + updated_dt = parser.parse(updated) + if updated_dt < datetime.now(timezone.utc) - timedelta(weeks=4): + logger.info("Cached leverage tiers are outdated. Will update.") + return None + return tiers['data'] + except Exception: + logger.exception("Error loading cached leverage tiers. Refreshing.") return None def fill_leverage_tiers(self) -> None: From 94e190b954d48aec11d549c0de7aa25de172fbdc Mon Sep 17 00:00:00 2001 From: TheJoeSchr <8218910+TheJoeSchr@users.noreply.github.com> Date: Wed, 19 Apr 2023 13:22:52 +0200 Subject: [PATCH 07/85] docs: strategy-callbacks: revert `leverage=trade.leverage` removal --- docs/strategy-callbacks.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index a99b2ae29..070eb6d48 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -318,11 +318,11 @@ class AwesomeStrategy(IStrategy): # evaluate highest to lowest, so that highest possible stop is used if current_profit > 0.40: - return stoploss_from_open(0.25, current_profit, is_short=trade.is_short) + return stoploss_from_open(0.25, current_profit, is_short=trade.is_short, leverage=trade.leverage) elif current_profit > 0.25: - return stoploss_from_open(0.15, current_profit, is_short=trade.is_short) + return stoploss_from_open(0.15, current_profit, is_short=trade.is_short, leverage=trade.leverage) elif current_profit > 0.20: - return stoploss_from_open(0.07, current_profit, is_short=trade.is_short) + return stoploss_from_open(0.07, current_profit, is_short=trade.is_short, leverage=trade.leverage) # return maximum stoploss value, keeping current stoploss price unchanged return 1 From 5ed072c489ea1f54c3a3d30b1e783744ed5c5b67 Mon Sep 17 00:00:00 2001 From: Mart van de Ven Date: Wed, 19 Apr 2023 20:17:31 +0800 Subject: [PATCH 08/85] Remove extraneous code block --- docs/stoploss.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index d85902be0..8fc73be21 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -209,11 +209,6 @@ You can also keep a static stoploss until the offset is reached, and then trail If `trailing_only_offset_is_reached = True` then the trailing stoploss is only activated once the offset is reached. Until then, the stoploss remains at the configured `stoploss`. This option can be used with or without `trailing_stop_positive`, but uses `trailing_stop_positive_offset` as offset. -``` python - trailing_stop_positive_offset = 0.011 - trailing_only_offset_is_reached = True -``` - Configuration (offset is buy-price + 3%): ``` python From 46f4fd79afaed9e674b4d2ed89b60c1fed8c35e5 Mon Sep 17 00:00:00 2001 From: Mart van de Ven Date: Wed, 19 Apr 2023 20:43:41 +0800 Subject: [PATCH 09/85] Fix anchor for missing heading --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 8a1aeb40e..8c0f8d882 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -165,7 +165,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy).
*Defaults to `minutes`.*
**Datatype:** String | `unfilledtimeout.exit_timeout_count` | How many times can exit orders time out. Once this number of timeouts is reached, an emergency exit is triggered. 0 to disable and allow unlimited order cancels. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `0`.*
**Datatype:** Integer | | **Pricing** -| `entry_pricing.price_side` | Select the side of the spread the bot should look at to get the entry rate. [More information below](#buy-price-side).
*Defaults to `same`.*
**Datatype:** String (either `ask`, `bid`, `same` or `other`). +| `entry_pricing.price_side` | Select the side of the spread the bot should look at to get the entry rate. [More information below](#entry-price).
*Defaults to `same`.*
**Datatype:** String (either `ask`, `bid`, `same` or `other`). | `entry_pricing.price_last_balance` | **Required.** Interpolate the bidding price. More information [below](#entry-price-without-orderbook-enabled). | `entry_pricing.use_order_book` | Enable entering using the rates in [Order Book Entry](#entry-price-with-orderbook-enabled).
*Defaults to `True`.*
**Datatype:** Boolean | `entry_pricing.order_book_top` | Bot will use the top N rate in Order Book "price_side" to enter a trade. I.e. a value of 2 will allow the bot to pick the 2nd entry in [Order Book Entry](#entry-price-with-orderbook-enabled).
*Defaults to `1`.*
**Datatype:** Positive Integer From 66f5f76a6c6f89eb6889c8aa86f60a2399198db6 Mon Sep 17 00:00:00 2001 From: Mart van de Ven Date: Wed, 19 Apr 2023 20:43:41 +0800 Subject: [PATCH 10/85] Fix anchor for missing heading --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 8c0f8d882..783060bbd 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -138,7 +138,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `stake_currency` | **Required.** Crypto-currency used for trading.
**Datatype:** String | `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade).
**Datatype:** Positive float or `"unlimited"`. | `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade).
*Defaults to `0.99` 99%).*
**Datatype:** Positive float between `0.1` and `1.0`. -| `available_capital` | Available starting capital for the bot. Useful when running multiple bots on the same exchange account.[More information below](#configuring-amount-per-trade).
**Datatype:** Positive float. +| `available_capital` | Available starting capital for the bot. Useful when running multiple bots on the same exchange account. [More information below](#configuring-amount-per-trade).
**Datatype:** Positive float. | `amend_last_stake_amount` | Use reduced last stake amount if necessary. [More information below](#configuring-amount-per-trade).
*Defaults to `false`.*
**Datatype:** Boolean | `last_stake_amount_min_ratio` | Defines minimum stake amount that has to be left and executed. Applies only to the last stake amount when it's amended to a reduced value (i.e. if `amend_last_stake_amount` is set to `true`). [More information below](#configuring-amount-per-trade).
*Defaults to `0.5`.*
**Datatype:** Float (as ratio) | `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stoploss value when calculating min pair stake amount in order to avoid possible trade refusals.
*Defaults to `0.05` (5%).*
**Datatype:** Positive Float as ratio. From 2f9e6c990c3908168a85903839cdec6b8996faca Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 19 Apr 2023 15:26:33 +0200 Subject: [PATCH 11/85] Update docs/strategy-callbacks.md --- docs/strategy-callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 070eb6d48..a13bdfd02 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -352,7 +352,7 @@ class AwesomeStrategy(IStrategy): # Convert absolute price to percentage relative to current_rate if stoploss_price < current_rate: - return stoploss_from_absolute(stoploss_price, current_rate, is_short=trade.is_short)) + return stoploss_from_absolute(stoploss_price, current_rate, is_short=trade.is_short) # return maximum stoploss value, keeping current stoploss price unchanged return 1 From 670a584d7ec42a6e4406bb8aa5a911e4dc191576 Mon Sep 17 00:00:00 2001 From: Mart van de Ven Date: Wed, 19 Apr 2023 22:00:31 +0800 Subject: [PATCH 12/85] Fix markdown inconsistencies --- docs/configuration.md | 20 ++++++++++---------- docs/producer-consumer.md | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 783060bbd..cf3872f1c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -155,25 +155,25 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). [Strategy Override](#parameters-in-the-strategy).
*Defaults to `0.0` (no offset).*
**Datatype:** Float | `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
**Datatype:** Boolean | `fee` | Fee used during backtesting / dry-runs. Should normally not be configured, which has freqtrade fall back to the exchange default fee. Set as ratio (e.g. 0.001 = 0.1%). Fee is applied twice for each trade, once when buying, once when selling.
**Datatype:** Float (as ratio) -| `futures_funding_rate` | User-specified funding rate to be used when historical funding rates are not available from the exchange. This does not overwrite real historical rates. It is recommended that this be set to 0 unless you are testing a specific coin and you understand how the funding rate will affect freqtrade's profit calculations. [More information here](leverage.md#unavailable-funding-rates)
*Defaults to None.*
**Datatype:** Float +| `futures_funding_rate` | User-specified funding rate to be used when historical funding rates are not available from the exchange. This does not overwrite real historical rates. It is recommended that this be set to 0 unless you are testing a specific coin and you understand how the funding rate will affect freqtrade's profit calculations. [More information here](leverage.md#unavailable-funding-rates)
*Defaults to `None`.*
**Datatype:** Float | `trading_mode` | Specifies if you want to trade regularly, trade with leverage, or trade contracts whose prices are derived from matching cryptocurrency prices. [leverage documentation](leverage.md).
*Defaults to `"spot"`.*
**Datatype:** String | `margin_mode` | When trading with leverage, this determines if the collateral owned by the trader will be shared or isolated to each trading pair [leverage documentation](leverage.md).
**Datatype:** String | `liquidation_buffer` | A ratio specifying how large of a safety net to place between the liquidation price and the stoploss to prevent a position from reaching the liquidation price [leverage documentation](leverage.md).
*Defaults to `0.05`.*
**Datatype:** Float | | **Unfilled timeout** | `unfilledtimeout.entry` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled entry order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer | `unfilledtimeout.exit` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled exit order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer -| `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy).
*Defaults to `minutes`.*
**Datatype:** String +| `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy).
*Defaults to `"minutes"`.*
**Datatype:** String | `unfilledtimeout.exit_timeout_count` | How many times can exit orders time out. Once this number of timeouts is reached, an emergency exit is triggered. 0 to disable and allow unlimited order cancels. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `0`.*
**Datatype:** Integer | | **Pricing** -| `entry_pricing.price_side` | Select the side of the spread the bot should look at to get the entry rate. [More information below](#entry-price).
*Defaults to `same`.*
**Datatype:** String (either `ask`, `bid`, `same` or `other`). +| `entry_pricing.price_side` | Select the side of the spread the bot should look at to get the entry rate. [More information below](#entry-price).
*Defaults to `"same"`.*
**Datatype:** String (either `ask`, `bid`, `same` or `other`). | `entry_pricing.price_last_balance` | **Required.** Interpolate the bidding price. More information [below](#entry-price-without-orderbook-enabled). -| `entry_pricing.use_order_book` | Enable entering using the rates in [Order Book Entry](#entry-price-with-orderbook-enabled).
*Defaults to `True`.*
**Datatype:** Boolean +| `entry_pricing.use_order_book` | Enable entering using the rates in [Order Book Entry](#entry-price-with-orderbook-enabled).
*Defaults to `true`.*
**Datatype:** Boolean | `entry_pricing.order_book_top` | Bot will use the top N rate in Order Book "price_side" to enter a trade. I.e. a value of 2 will allow the bot to pick the 2nd entry in [Order Book Entry](#entry-price-with-orderbook-enabled).
*Defaults to `1`.*
**Datatype:** Positive Integer | `entry_pricing. check_depth_of_market.enabled` | Do not enter if the difference of buy orders and sell orders is met in Order Book. [Check market depth](#check-depth-of-market).
*Defaults to `false`.*
**Datatype:** Boolean | `entry_pricing. check_depth_of_market.bids_to_ask_delta` | The difference ratio of buy orders and sell orders found in Order Book. A value below 1 means sell order size is greater, while value greater than 1 means buy order size is higher. [Check market depth](#check-depth-of-market)
*Defaults to `0`.*
**Datatype:** Float (as ratio) -| `exit_pricing.price_side` | Select the side of the spread the bot should look at to get the exit rate. [More information below](#exit-price-side).
*Defaults to `same`.*
**Datatype:** String (either `ask`, `bid`, `same` or `other`). +| `exit_pricing.price_side` | Select the side of the spread the bot should look at to get the exit rate. [More information below](#exit-price-side).
*Defaults to `"same"`.*
**Datatype:** String (either `ask`, `bid`, `same` or `other`). | `exit_pricing.price_last_balance` | Interpolate the exiting price. More information [below](#exit-price-without-orderbook-enabled). -| `exit_pricing.use_order_book` | Enable exiting of open trades using [Order Book Exit](#exit-price-with-orderbook-enabled).
*Defaults to `True`.*
**Datatype:** Boolean +| `exit_pricing.use_order_book` | Enable exiting of open trades using [Order Book Exit](#exit-price-with-orderbook-enabled).
*Defaults to `true`.*
**Datatype:** Boolean | `exit_pricing.order_book_top` | Bot will use the top N rate in Order Book "price_side" to exit. I.e. a value of 2 will allow the bot to pick the 2nd ask rate in [Order Book Exit](#exit-price-with-orderbook-enabled)
*Defaults to `1`.*
**Datatype:** Positive Integer | `custom_price_max_distance_ratio` | Configure maximum distance ratio between current and custom entry or exit price.
*Defaults to `0.02` 2%).*
**Datatype:** Positive float | | **TODO** @@ -199,10 +199,10 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `exchange.ccxt_sync_config` | Additional CCXT parameters passed to the regular (sync) ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
**Datatype:** Dict | `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
**Datatype:** Dict | `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded.
*Defaults to `60` minutes.*
**Datatype:** Positive Integer -| `exchange.skip_pair_validation` | Skip pairlist validation on startup.
*Defaults to `false`
**Datatype:** Boolean -| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.
*Defaults to `false`
**Datatype:** Boolean +| `exchange.skip_pair_validation` | Skip pairlist validation on startup.
*Defaults to `false`*
**Datatype:** Boolean +| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.
*Defaults to `false`*
**Datatype:** Boolean | `exchange.unknown_fee_rate` | Fallback value to use when calculating trading fees. This can be useful for exchanges which have fees in non-tradable currencies. The value provided here will be multiplied with the "fee cost".
*Defaults to `None`
**Datatype:** float -| `exchange.log_responses` | Log relevant exchange responses. For debug mode only - use with care.
*Defaults to `false`
**Datatype:** Boolean +| `exchange.log_responses` | Log relevant exchange responses. For debug mode only - use with care.
*Defaults to `false`*
**Datatype:** Boolean | `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now.
*Defaults to `true`.*
**Datatype:** Boolean | | **Plugins** | `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation of all possible configuration options. @@ -213,7 +213,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String | `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String | `telegram.balance_dust_level` | Dust-level (in stake currency) - currencies with a balance below this will not be shown by `/balance`.
**Datatype:** float -| `telegram.reload` | Allow "reload" buttons on telegram messages.
*Defaults to `True`.
**Datatype:** boolean +| `telegram.reload` | Allow "reload" buttons on telegram messages.
*Defaults to `true`.
**Datatype:** boolean | `telegram.notification_settings.*` | Detailed notification settings. Refer to the [telegram documentation](telegram-usage.md) for details.
**Datatype:** dictionary | `telegram.allow_custom_messages` | Enable the sending of Telegram messages from strategies via the dataprovider.send_msg() function.
**Datatype:** Boolean | | **Webhook** diff --git a/docs/producer-consumer.md b/docs/producer-consumer.md index c52279f26..0bd52ac93 100644 --- a/docs/producer-consumer.md +++ b/docs/producer-consumer.md @@ -49,7 +49,7 @@ Enable subscribing to an instance by adding the `external_message_consumer` sect | `wait_timeout` | Timeout until we ping again if no message is received.
*Defaults to `300`.*
**Datatype:** Integer - in seconds. | `ping_timeout` | Ping timeout
*Defaults to `10`.*
**Datatype:** Integer - in seconds. | `sleep_time` | Sleep time before retrying to connect.
*Defaults to `10`.*
**Datatype:** Integer - in seconds. -| `remove_entry_exit_signals` | Remove signal columns from the dataframe (set them to 0) on dataframe receipt.
*Defaults to `False`.*
**Datatype:** Boolean. +| `remove_entry_exit_signals` | Remove signal columns from the dataframe (set them to 0) on dataframe receipt.
*Defaults to `false`.*
**Datatype:** Boolean. | `message_size_limit` | Size limit per message
*Defaults to `8`.*
**Datatype:** Integer - Megabytes. Instead of (or as well as) calculating indicators in `populate_indicators()` the follower instance listens on the connection to a producer instance's messages (or multiple producer instances in advanced configurations) and requests the producer's most recently analyzed dataframes for each pair in the active whitelist. From 1056ff0d1875030202675e9b035da1e42e52bfcf Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 19 Apr 2023 18:20:25 +0200 Subject: [PATCH 13/85] Update variable to better reflect it's content --- freqtrade/rpc/api_server/api_v1.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 8ea70bb69..8aa706e62 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -303,11 +303,11 @@ def get_strategy(strategy: str, config=Depends(get_config)): @router.get('/freqaimodels', response_model=FreqAIModelListResponse, tags=['freqai']) def list_freqaimodels(config=Depends(get_config)): from freqtrade.resolvers.freqaimodel_resolver import FreqaiModelResolver - strategies = FreqaiModelResolver.search_all_objects( + models = FreqaiModelResolver.search_all_objects( config, False) - strategies = sorted(strategies, key=lambda x: x['name']) + models = sorted(models, key=lambda x: x['name']) - return {'freqaimodels': [x['name'] for x in strategies]} + return {'freqaimodels': [x['name'] for x in models]} @router.get('/available_pairs', response_model=AvailablePairs, tags=['candle data']) From b545fc5590f03d84ba7aa5eaadba8b9aca896a70 Mon Sep 17 00:00:00 2001 From: Mart van de Ven Date: Thu, 20 Apr 2023 13:36:14 +0800 Subject: [PATCH 14/85] Correct user namespace from `cust_` to `custom_` --- docs/strategy-advanced.md | 2 +- docs/strategy-callbacks.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index cbb71e810..b38bd1c3f 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -15,7 +15,7 @@ If you're just getting started, please be familiar with the methods described in Storing information can be accomplished by creating a new dictionary within the strategy class. -The name of the variable can be chosen at will, but should be prefixed with `cust_` to avoid naming collisions with predefined strategy variables. +The name of the variable can be chosen at will, but should be prefixed with `custom_` to avoid naming collisions with predefined strategy variables. ```python class AwesomeStrategy(IStrategy): diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index a13bdfd02..855f2353b 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -43,7 +43,7 @@ class AwesomeStrategy(IStrategy): if self.config['runmode'].value in ('live', 'dry_run'): # Assign this to the class by using self.* # can then be used by populate_* methods - self.cust_remote_data = requests.get('https://some_remote_source.example.com') + self.custom_remote_data = requests.get('https://some_remote_source.example.com') ``` From 818da02f6ca8c81a8b476d9065dba65390611603 Mon Sep 17 00:00:00 2001 From: Mart van de Ven Date: Thu, 20 Apr 2023 14:01:01 +0800 Subject: [PATCH 15/85] Edit `Advanced Strategies` intro for clarify and brevity --- docs/strategy-advanced.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index b38bd1c3f..a93dcecdf 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -1,15 +1,15 @@ # Advanced Strategies This page explains some advanced concepts available for strategies. -If you're just getting started, please be familiar with the methods described in the [Strategy Customization](strategy-customization.md) documentation and with the [Freqtrade basics](bot-basics.md) first. +If you're just getting started, please familiarize yourself with the [Freqtrade basics](bot-basics.md) and methods described in [Strategy Customization](strategy-customization.md) first. -[Freqtrade basics](bot-basics.md) describes in which sequence each method described below is called, which can be helpful to understand which method to use for your custom needs. +The call sequence of the methods described here is covered under [bot execution logic](bot-basics.md#bot-execution-logic). Those docs are also helpful in deciding which method is most suitable for your customisation needs. !!! Note - All callback methods described below should only be implemented in a strategy if they are actually used. + Callback methods should *only* be implemented if a strategy uses them. !!! Tip - You can get a strategy template containing all below methods by running `freqtrade new-strategy --strategy MyAwesomeStrategy --template advanced` + Start off with a strategy template containing all available callback methods by running `freqtrade new-strategy --strategy MyAwesomeStrategy --template advanced` ## Storing information From ca1a616b898e4384b68d5ff7d72f23c349d763d5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Apr 2023 07:08:14 +0200 Subject: [PATCH 16/85] use Fstrings for log message --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 73b25a7a1..7a1f7151e 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1425,7 +1425,7 @@ class FreqtradeBot(LoggingMixin): corder = order reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] - logger.info('%s order %s for %s.', side, reason, trade) + logger.info(f'{side} order {reason} for {trade}.') # Using filled to determine the filled amount filled_amount = safe_value_fallback2(corder, order, 'filled', 'filled') From 4a4be27ebc01b85c7af48d5f2949345082ec7959 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Apr 2023 07:14:00 +0200 Subject: [PATCH 17/85] use orderid from order, the trade one has been reset part of #8526 --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7a1f7151e..9cc26ad77 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1507,7 +1507,7 @@ class FreqtradeBot(LoggingMixin): trade.exit_reason = None trade.open_order_id = None - self.update_trade_state(trade, trade.open_order_id, order) + self.update_trade_state(trade, order['id'], order) logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.') trade.close_rate = None From 9bc17a92323015599d86460bd408cc3ecf3758cc Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Apr 2023 06:19:57 +0000 Subject: [PATCH 18/85] Downgrade wheel to isntall gym --- .github/workflows/ci.yml | 6 +++--- Dockerfile | 2 +- build_helpers/install_windows.ps1 | 2 +- setup.sh | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 21d6f9ef4..7e4487ac8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,7 +57,7 @@ jobs: - name: Installation - *nix if: runner.os == 'Linux' run: | - python -m pip install --upgrade pip==23.0.1 wheel + python -m pip install --upgrade pip==23.0.1 wheel==0.38.4 export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH export TA_LIBRARY_PATH=${HOME}/dependencies/lib export TA_INCLUDE_PATH=${HOME}/dependencies/include @@ -163,7 +163,7 @@ jobs: rm /usr/local/bin/python3.11-config || true brew install hdf5 c-blosc - python -m pip install --upgrade pip==23.0.1 wheel + python -m pip install --upgrade pip==23.0.1 wheel==0.38.4 export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH export TA_LIBRARY_PATH=${HOME}/dependencies/lib export TA_INCLUDE_PATH=${HOME}/dependencies/include @@ -352,7 +352,7 @@ jobs: - name: Installation - *nix if: runner.os == 'Linux' run: | - python -m pip install --upgrade pip==23.0.1 wheel + python -m pip install --upgrade pip==23.0.1 wheel==0.38.4 export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH export TA_LIBRARY_PATH=${HOME}/dependencies/lib export TA_INCLUDE_PATH=${HOME}/dependencies/include diff --git a/Dockerfile b/Dockerfile index 422caecaf..ee8b3f0a8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,7 +25,7 @@ FROM base as python-deps RUN apt-get update \ && apt-get -y install build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \ && apt-get clean \ - && pip install --upgrade pip==23.0.1 + && pip install --upgrade pip==23.0.1 wheel==0.38.4 # Install TA-lib COPY build_helpers/* /tmp/ diff --git a/build_helpers/install_windows.ps1 b/build_helpers/install_windows.ps1 index cf6fbdf07..3e7df5dfc 100644 --- a/build_helpers/install_windows.ps1 +++ b/build_helpers/install_windows.ps1 @@ -1,7 +1,7 @@ # Downloads don't work automatically, since the URL is regenerated via javascript. # Downloaded from https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib -python -m pip install --upgrade pip==23.0.1 wheel +python -m pip install --upgrade pip==23.0.1 wheel==0.38.4 $pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" diff --git a/setup.sh b/setup.sh index 805e13237..d46569a53 100755 --- a/setup.sh +++ b/setup.sh @@ -50,7 +50,7 @@ function updateenv() { SYS_ARCH=$(uname -m) echo "pip install in-progress. Please wait..." # Setuptools 65.5.0 is the last version that can install gym==0.21.0 - ${PYTHON} -m pip install --upgrade pip==23.0.1 wheel setuptools==65.5.1 + ${PYTHON} -m pip install --upgrade pip==23.0.1 wheel==0.38.4 setuptools==65.5.1 REQUIREMENTS_HYPEROPT="" REQUIREMENTS_PLOT="" REQUIREMENTS_FREQAI="" From 5dccfab89c5f4788c4f0728549a07c78037faaee Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Apr 2023 11:27:04 +0200 Subject: [PATCH 19/85] Add test for start_cap_ratio --- tests/rpc/test_rpc.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index ff08a0564..5a84eaa48 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -591,6 +591,8 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): 'side': 'short', } ] + assert result['starting_capital'] == 10 + assert result['starting_capital_ratio'] == 0.0 def test_rpc_start(mocker, default_conf) -> None: From ce75a032d023130c6a39ca7626197f79fa6d3dc5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Apr 2023 10:58:49 +0200 Subject: [PATCH 20/85] Balance ratio calculation should ignore non-relevant assets closes #8532 --- freqtrade/rpc/rpc.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index ffed3c6d6..6987017b8 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -585,11 +585,13 @@ class RPC: """ Returns current account balance per crypto """ currencies: List[Dict] = [] total = 0.0 + total_bot = 0.0 try: tickers = self._freqtrade.exchange.get_tickers(cached=True) except (ExchangeError): raise RPCException('Error getting current tickers.') - + open_trades: List[Trade] = Trade.get_open_trades() + open_assets = [t.base_currency for t in open_trades] self._freqtrade.wallets.update(require_update=False) starting_capital = self._freqtrade.wallets.get_starting_balance() starting_cap_fiat = self._fiat_converter.convert_amount( @@ -618,7 +620,9 @@ class RPC: except (ExchangeError): logger.warning(f" Could not get rate for pair {coin}.") continue - total = total + est_stake + total += est_stake + if coin == stake_currency or coin in open_assets: + total_bot += est_stake currencies.append({ 'currency': coin, 'free': balance.free, @@ -651,10 +655,12 @@ class RPC: value = self._fiat_converter.convert_amount( total, stake_currency, fiat_display_currency) if self._fiat_converter else 0 + value_bot = self._fiat_converter.convert_amount( + total_bot, stake_currency, fiat_display_currency) if self._fiat_converter else 0 trade_count = len(Trade.get_trades_proxy()) - starting_capital_ratio = (total / starting_capital) - 1 if starting_capital else 0.0 - starting_cap_fiat_ratio = (value / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0 + starting_capital_ratio = (total_bot / starting_capital) - 1 if starting_capital else 0.0 + starting_cap_fiat_ratio = (value_bot / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0 return { 'currencies': currencies, From cb09ef71807488fe726bdacd823af12fc9eb193a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Apr 2023 11:08:37 +0200 Subject: [PATCH 21/85] Extract converting wallet to est_stake --- freqtrade/rpc/rpc.py | 48 ++++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 6987017b8..814f0d6a8 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -24,6 +24,7 @@ from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, MarketDirecti State, TradingMode) from freqtrade.exceptions import ExchangeError, PricingError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs +from freqtrade.exchange.types import Tickers from freqtrade.loggers import bufferHandler from freqtrade.misc import decimals_per_coin, shorten_date from freqtrade.persistence import KeyStoreKeys, KeyValueStore, Order, PairLocks, Trade @@ -581,15 +582,38 @@ class RPC: 'bot_start_date': bot_start.strftime(DATETIME_PRINT_FORMAT) if bot_start else '', } + def __balance_get_est_stake( + self, coin: str, stake_currency: str, balance: Wallet, tickers) -> float: + est_stake = 0.0 + if coin == stake_currency: + est_stake = balance.total + if self._config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: + # in Futures, "total" includes the locked stake, and therefore all positions + est_stake = balance.free + else: + try: + pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency) + rate: Optional[float] = tickers.get(pair, {}).get('last', None) + if rate: + if pair.startswith(stake_currency) and not pair.endswith(stake_currency): + rate = 1.0 / rate + est_stake = rate * balance.total + except (ExchangeError): + logger.warning(f"Could not get rate for pair {coin}.") + raise ValueError() + + return est_stake + def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict: """ Returns current account balance per crypto """ currencies: List[Dict] = [] total = 0.0 total_bot = 0.0 try: - tickers = self._freqtrade.exchange.get_tickers(cached=True) + tickers: Tickers = self._freqtrade.exchange.get_tickers(cached=True) except (ExchangeError): raise RPCException('Error getting current tickers.') + open_trades: List[Trade] = Trade.get_open_trades() open_assets = [t.base_currency for t in open_trades] self._freqtrade.wallets.update(require_update=False) @@ -601,25 +625,11 @@ class RPC: for coin, balance in self._freqtrade.wallets.get_all_balances().items(): if not balance.total: continue + try: + est_stake = self.__balance_get_est_stake(coin, stake_currency, balance, tickers) + except ValueError: + continue - est_stake: float = 0 - if coin == stake_currency: - rate = 1.0 - est_stake = balance.total - if self._config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: - # in Futures, "total" includes the locked stake, and therefore all positions - est_stake = balance.free - else: - try: - pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency) - rate = tickers.get(pair, {}).get('last') - if rate: - if pair.startswith(stake_currency) and not pair.endswith(stake_currency): - rate = 1.0 / rate - est_stake = rate * balance.total - except (ExchangeError): - logger.warning(f" Could not get rate for pair {coin}.") - continue total += est_stake if coin == stake_currency or coin in open_assets: total_bot += est_stake From 7a47500b22ae5781f643fc6584662ed0e01af505 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Apr 2023 14:57:13 +0200 Subject: [PATCH 22/85] Add "is_bot_managed" flag to API --- freqtrade/rpc/api_server/api_schemas.py | 1 + freqtrade/rpc/api_server/api_v1.py | 3 ++- freqtrade/rpc/rpc.py | 5 ++++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 53bf7558f..a9dc6243e 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -43,6 +43,7 @@ class Balance(BaseModel): leverage: float is_position: bool position: float + is_bot_managed: bool class Balances(BaseModel): diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 8aa706e62..5ee5e36c4 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -43,7 +43,8 @@ logger = logging.getLogger(__name__) # 2.23: Allow plot config request in webserver mode # 2.24: Add cancel_open_order endpoint # 2.25: Add several profit values to /status endpoint -API_VERSION = 2.25 +# 2.26: increase /balance output +API_VERSION = 2.26 # Public API, requires no auth. router_public = APIRouter() diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 814f0d6a8..a0b588bfa 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -631,7 +631,8 @@ class RPC: continue total += est_stake - if coin == stake_currency or coin in open_assets: + is_bot_managed = coin == stake_currency or coin in open_assets + if is_bot_managed: total_bot += est_stake currencies.append({ 'currency': coin, @@ -643,6 +644,7 @@ class RPC: 'side': 'long', 'leverage': 1, 'position': 0, + 'is_bot_managed': is_bot_managed, 'is_position': False, }) symbol: str @@ -660,6 +662,7 @@ class RPC: 'stake': stake_currency, 'leverage': position.leverage, 'side': position.side, + 'is_bot_managed': True, 'is_position': True }) From dbf1f0897e2bbcb2dd220d20f8d05af5cc8eb99d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Apr 2023 15:51:28 +0200 Subject: [PATCH 23/85] Add /balance full, reduce regular balance output closes #4497 --- docs/telegram-usage.md | 4 ++-- freqtrade/rpc/rpc.py | 2 ++ freqtrade/rpc/telegram.py | 23 +++++++++++++---------- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index fe990790a..e6017e271 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -191,7 +191,8 @@ official commands. You can ask at any moment for help with `/help`. | **Metrics** | | `/profit []` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default) | `/performance` | Show performance of each finished trade grouped by pair -| `/balance` | Show account balance per currency +| `/balance` | Show bot managed balance per currency +| `/balance full` | Show account balance per currency | `/daily ` | Shows profit or loss per day, over the last n days (n defaults to 7) | `/weekly ` | Shows profit or loss per week, over the last n weeks (n defaults to 8) | `/monthly ` | Shows profit or loss per month, over the last n months (n defaults to 6) @@ -202,7 +203,6 @@ official commands. You can ask at any moment for help with `/help`. | `/blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist. | `/edge` | Show validated pairs by Edge if it is enabled. - ## Telegram commands in action Below, example of Telegram message you will receive for each command. diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index a0b588bfa..addd2bd02 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -678,8 +678,10 @@ class RPC: return { 'currencies': currencies, 'total': total, + 'total_bot': total_bot, 'symbol': fiat_display_currency, 'value': value, + 'value_bot': value_bot, 'stake': stake_currency, 'starting_capital': starting_capital, 'starting_capital_ratio': starting_capital_ratio, diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index e626ee598..c4feb588a 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -905,6 +905,7 @@ class Telegram(RPCHandler): @authorized_only def _balance(self, update: Update, context: CallbackContext) -> None: """ Handler for /balance """ + full_result = context.args and 'full' in context.args result = self._rpc._rpc_balance(self._config['stake_currency'], self._config.get('fiat_display_currency', '')) @@ -928,7 +929,7 @@ class Telegram(RPCHandler): total_dust_currencies = 0 for curr in result['currencies']: curr_output = '' - if curr['est_stake'] > balance_dust_level: + if curr['est_stake'] > balance_dust_level and (full_result or curr['is_bot_managed']): if curr['is_position']: curr_output = ( f"*{curr['currency']}:*\n" @@ -965,14 +966,15 @@ class Telegram(RPCHandler): tc = result['trade_count'] > 0 stake_improve = f" `({result['starting_capital_ratio']:.2%})`" if tc else '' fiat_val = f" `({result['starting_capital_fiat_ratio']:.2%})`" if tc else '' - - output += ("\n*Estimated Value*:\n" - f"\t`{result['stake']}: " - f"{round_coin_value(result['total'], result['stake'], False)}`" - f"{stake_improve}\n" - f"\t`{result['symbol']}: " - f"{round_coin_value(result['value'], result['symbol'], False)}`" - f"{fiat_val}\n") + value = round_coin_value( + result['value' if full_result else 'value_bot'], result['symbol'], False) + total_stake = round_coin_value( + result['total' if full_result else 'total_bot'], result['stake'], False) + output += ( + f"\n*Estimated Value{' (Bot managed assets only)' if not full_result else ''}*:\n" + f"\t`{result['stake']}: {total_stake}`{stake_improve}\n" + f"\t`{result['symbol']}: {value}`{fiat_val}\n" + ) self._send_msg(output, reload_able=True, callback_path="update_balance", query=update.callback_query) @@ -1528,7 +1530,8 @@ class Telegram(RPCHandler): "------------\n" "*/show_config:* `Show running configuration` \n" "*/locks:* `Show currently locked pairs`\n" - "*/balance:* `Show account balance per currency`\n" + "*/balance:* `Show bot managed balance per currency`\n" + "*/balance total:* `Show account balance per currency`\n" "*/logs [limit]:* `Show latest logs - defaults to 10` \n" "*/count:* `Show number of active trades compared to allowed number of trades`\n" "*/edge:* `Shows validated pairs by Edge if it is enabled` \n" From c4f8ff95ddd263e2c2f49a066889598c92d98e74 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Apr 2023 16:13:27 +0200 Subject: [PATCH 24/85] Update tests --- freqtrade/rpc/api_server/api_schemas.py | 2 ++ tests/rpc/test_rpc.py | 7 ++++++- tests/rpc/test_rpc_apiserver.py | 3 +++ tests/rpc/test_rpc_telegram.py | 15 +++++++++++++-- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index a9dc6243e..20144d2c8 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -49,8 +49,10 @@ class Balance(BaseModel): class Balances(BaseModel): currencies: List[Balance] total: float + total_bot: float symbol: str value: float + value_bot: float stake: str note: str starting_capital: float diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 5a84eaa48..d675ffe4e 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -552,6 +552,7 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): 'leverage': 1.0, 'position': 0.0, 'side': 'long', + 'is_bot_managed': True, }, { 'free': 1.0, @@ -564,7 +565,7 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): 'leverage': 1.0, 'position': 0.0, 'side': 'long', - + 'is_bot_managed': False, }, { 'free': 5.0, @@ -577,6 +578,7 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): 'leverage': 1.0, 'position': 0.0, 'side': 'long', + 'is_bot_managed': False, }, { 'free': 0.0, @@ -589,8 +591,11 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): 'leverage': 5.0, 'position': 1000.0, 'side': 'short', + 'is_bot_managed': True, } ] + assert pytest.approx(result['total_bot']) == 10 + assert pytest.approx(result['total']) == 30.309096 assert result['starting_capital'] == 10 assert result['starting_capital_ratio'] == 0.0 diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 58c904838..ad7543430 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -486,7 +486,10 @@ def test_api_balance(botclient, mocker, rpc_balance, tickers): 'leverage': 1.0, 'position': 0.0, 'side': 'long', + 'is_bot_managed': True, } + assert response['total'] == 12.159513094 + assert response['total_bot'] == 12.0 assert 'starting_capital' in response assert 'starting_capital_fiat' in response assert 'starting_capital_pct' in response diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 7978a2a23..a672c62f6 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -783,19 +783,27 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick patch_get_signal(freqtradebot) telegram._balance(update=update, context=MagicMock()) + context = MagicMock() + context.args = ["full"] + telegram._balance(update=update, context=context) result = msg_mock.call_args_list[0][0][0] - assert msg_mock.call_count == 1 + result_full = msg_mock.call_args_list[1][0][0] + assert msg_mock.call_count == 2 assert '*BTC:*' in result assert '*ETH:*' not in result assert '*USDT:*' not in result assert '*EUR:*' not in result - assert '*LTC:*' in result + assert '*LTC:*' not in result + + assert '*LTC:*' in result_full assert '*XRP:*' not in result assert 'Balance:' in result assert 'Est. BTC:' in result assert 'BTC: 12' in result assert "*3 Other Currencies (< 0.0001 BTC):*" in result assert 'BTC: 0.00000309' in result + assert '*Estimated Value*:' in result_full + assert '*Estimated Value (Bot managed assets only)*:' in result def test_balance_handle_empty_response(default_conf, update, mocker) -> None: @@ -840,12 +848,15 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None 'leverage': 1.0, 'position': 0.0, 'side': 'long', + 'is_bot_managed': True, }) mocker.patch('freqtrade.rpc.rpc.RPC._rpc_balance', return_value={ 'currencies': balances, 'total': 100.0, + 'total_bot': 100.0, 'symbol': 100.0, 'value': 1000.0, + 'value_bot': 1000.0, 'starting_capital': 1000, 'starting_capital_fiat': 1000, }) From f937818b800343cf94d7f70d6368ab7b12c8677c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Apr 2023 17:13:53 +0200 Subject: [PATCH 25/85] Add "owned" fields to balance --- freqtrade/rpc/api_server/api_schemas.py | 2 ++ freqtrade/rpc/rpc.py | 24 ++++++++++++++++++------ freqtrade/rpc/telegram.py | 11 +++++++---- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 20144d2c8..dd5ca3a62 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -36,7 +36,9 @@ class Balance(BaseModel): free: float balance: float used: float + bot_owned: Optional[float] est_stake: float + est_stake_bot: Optional[float] stake: str # Starting with 2.x side: str diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index addd2bd02..06cf61dff 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -583,13 +583,16 @@ class RPC: } def __balance_get_est_stake( - self, coin: str, stake_currency: str, balance: Wallet, tickers) -> float: + self, coin: str, stake_currency: str, amount: float, + balance: Wallet, tickers) -> Tuple[float, float]: est_stake = 0.0 + est_bot_stake = 0.0 if coin == stake_currency: est_stake = balance.total if self._config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: # in Futures, "total" includes the locked stake, and therefore all positions est_stake = balance.free + est_bot_stake = est_stake else: try: pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency) @@ -598,11 +601,12 @@ class RPC: if pair.startswith(stake_currency) and not pair.endswith(stake_currency): rate = 1.0 / rate est_stake = rate * balance.total + est_bot_stake = rate * amount except (ExchangeError): logger.warning(f"Could not get rate for pair {coin}.") raise ValueError() - return est_stake + return est_stake, est_bot_stake def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict: """ Returns current account balance per crypto """ @@ -615,7 +619,7 @@ class RPC: raise RPCException('Error getting current tickers.') open_trades: List[Trade] = Trade.get_open_trades() - open_assets = [t.base_currency for t in open_trades] + open_assets: Dict[str, Trade] = {t.safe_base_currency: t for t in open_trades} self._freqtrade.wallets.update(require_update=False) starting_capital = self._freqtrade.wallets.get_starting_balance() starting_cap_fiat = self._fiat_converter.convert_amount( @@ -625,21 +629,29 @@ class RPC: for coin, balance in self._freqtrade.wallets.get_all_balances().items(): if not balance.total: continue + + trade = open_assets.get(coin, None) + is_bot_managed = coin == stake_currency or trade is not None + trade_amount = trade.amount if trade else 0 + try: - est_stake = self.__balance_get_est_stake(coin, stake_currency, balance, tickers) + est_stake, est_stake_bot = self.__balance_get_est_stake( + coin, stake_currency, trade_amount, balance, tickers) except ValueError: continue total += est_stake - is_bot_managed = coin == stake_currency or coin in open_assets + if is_bot_managed: - total_bot += est_stake + total_bot += est_stake_bot currencies.append({ 'currency': coin, 'free': balance.free, 'balance': balance.total, 'used': balance.used, + 'bot_owned': trade_amount, 'est_stake': est_stake or 0, + 'est_stake_bot': est_stake_bot if is_bot_managed else 0, 'stake': stake_currency, 'side': 'long', 'leverage': 1, diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index c4feb588a..2b997efac 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -916,8 +916,7 @@ class Telegram(RPCHandler): output = '' if self._config['dry_run']: output += "*Warning:* Simulated balances in Dry Mode.\n" - starting_cap = round_coin_value( - result['starting_capital'], self._config['stake_currency']) + starting_cap = round_coin_value(result['starting_capital'], self._config['stake_currency']) output += f"Starting capital: `{starting_cap}`" starting_cap_fiat = round_coin_value( result['starting_capital_fiat'], self._config['fiat_display_currency'] @@ -938,13 +937,17 @@ class Telegram(RPCHandler): f"\t`Est. {curr['stake']}: " f"{round_coin_value(curr['est_stake'], curr['stake'], False)}`\n") else: + est_stake = round_coin_value( + curr['est_stake' if full_result else 'est_stake_bot'], curr['stake'], False) + curr_output = ( f"*{curr['currency']}:*\n" f"\t`Available: {curr['free']:.8f}`\n" f"\t`Balance: {curr['balance']:.8f}`\n" f"\t`Pending: {curr['used']:.8f}`\n" - f"\t`Est. {curr['stake']}: " - f"{round_coin_value(curr['est_stake'], curr['stake'], False)}`\n") + f"\t`Bot Owned: {curr['bot_owned']:.8f}`\n" + f"\t`Est. {curr['stake']}: {est_stake}`\n") + elif curr['est_stake'] <= balance_dust_level: total_dust_balance += curr['est_stake'] total_dust_currencies += 1 From 741834301f24630a209663b375cdb2326913fb27 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Apr 2023 17:21:03 +0200 Subject: [PATCH 26/85] Update tests --- tests/rpc/test_rpc.py | 6 ++++++ tests/rpc/test_rpc_telegram.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index d675ffe4e..34095f4f2 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -546,7 +546,9 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): 'free': 10.0, 'balance': 12.0, 'used': 2.0, + 'bot_owned': 0, 'est_stake': 10.0, # In futures mode, "free" is used here. + 'est_stake_bot': 10, 'stake': 'BTC', 'is_position': False, 'leverage': 1.0, @@ -558,7 +560,9 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): 'free': 1.0, 'balance': 5.0, 'currency': 'ETH', + 'bot_owned': 0, 'est_stake': 0.30794, + 'est_stake_bot': 0, 'used': 4.0, 'stake': 'BTC', 'is_position': False, @@ -571,7 +575,9 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): 'free': 5.0, 'balance': 10.0, 'currency': 'USDT', + 'bot_owned': 0, 'est_stake': 0.0011562404610161968, + 'est_stake_bot': 0, 'used': 5.0, 'stake': 'BTC', 'is_position': False, diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index a672c62f6..97ac03c9f 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -842,7 +842,9 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None 'free': 1.0, 'used': 0.5, 'balance': i, + 'bot_owned': 0.5, 'est_stake': 1, + 'est_stake_bot': 1, 'stake': 'BTC', 'is_position': False, 'leverage': 1.0, From 94a6bc608ca78cb8eff7a0bc46cab3e254b72fa0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Apr 2023 17:42:09 +0200 Subject: [PATCH 27/85] Update stake-currency behavior --- freqtrade/rpc/rpc.py | 2 ++ tests/rpc/test_rpc.py | 2 +- tests/rpc/test_rpc_apiserver.py | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 06cf61dff..222912aa0 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -633,6 +633,8 @@ class RPC: trade = open_assets.get(coin, None) is_bot_managed = coin == stake_currency or trade is not None trade_amount = trade.amount if trade else 0 + if coin == stake_currency: + trade_amount = self._freqtrade.wallets.get_available_stake_amount() try: est_stake, est_stake_bot = self.__balance_get_est_stake( diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 34095f4f2..5335cebf9 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -546,7 +546,7 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): 'free': 10.0, 'balance': 12.0, 'used': 2.0, - 'bot_owned': 0, + 'bot_owned': 9.9, # available stake - reducing by reserved amount 'est_stake': 10.0, # In futures mode, "free" is used here. 'est_stake_bot': 10, 'stake': 'BTC', diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index ad7543430..a4638a07e 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -480,7 +480,9 @@ def test_api_balance(botclient, mocker, rpc_balance, tickers): 'free': 12.0, 'balance': 12.0, 'used': 0.0, + 'bot_owned': pytest.approx(11.879999), 'est_stake': 12.0, + 'est_stake_bot': 12.0, 'stake': 'BTC', 'is_position': False, 'leverage': 1.0, From c20b074880ab37d2df425eb9e5653629fa4bcdb7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Apr 2023 19:20:24 +0200 Subject: [PATCH 28/85] Add FAQ entry about permission error --- docs/faq.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/faq.md b/docs/faq.md index b52a77c6b..7b8cc2580 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -142,6 +142,13 @@ To fix this, redefine order types in the strategy to use "limit" instead of "mar The same fix should be applied in the configuration file, if order types are defined in your custom config rather than in the strategy. +### I'm trying to start the bot live, but get an API permission error + +Errors like `Invalid API-key, IP, or permissions for action` mean exactly what they actually say. +Your API key is either invalid (copy/paste error? check for leading/trailing spaces in the config), expired, or the IP you're running the bot from is not enabled in the Exchange's API console. +Usually, the permission "Spot Trading" (or the equivalent in the exchange you use) will be necessary. +Futures will usually have to be enabled specifically. + ### How do I search the bot logs for something? By default, the bot writes its log into stderr stream. This is implemented this way so that you can easily separate the bot's diagnostics messages from Backtesting, Edge and Hyperopt results, output from other various Freqtrade utility sub-commands, as well as from the output of your custom `print()`'s you may have inserted into your strategy. So if you need to search the log messages with the grep utility, you need to redirect stderr to stdout and disregard stdout. From c5dc21e80c41322a6551a3d1d53b05d474156c85 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Apr 2023 19:30:30 +0200 Subject: [PATCH 29/85] Update freqAI documentation with missing typehints --- docs/freqai-configuration.md | 9 +++++---- docs/freqai-reinforcement-learning.md | 6 ++++-- docs/strategy_migration.md | 8 ++++---- freqtrade/strategy/interface.py | 7 ++++--- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/docs/freqai-configuration.md b/docs/freqai-configuration.md index 233edf2c5..71c614b4a 100644 --- a/docs/freqai-configuration.md +++ b/docs/freqai-configuration.md @@ -52,7 +52,7 @@ The FreqAI strategy requires including the following lines of code in the standa return dataframe - def feature_engineering_expand_all(self, dataframe, period, **kwargs): + def feature_engineering_expand_all(self, dataframe: DataFrame, period, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -77,7 +77,7 @@ The FreqAI strategy requires including the following lines of code in the standa return dataframe - def feature_engineering_expand_basic(self, dataframe, **kwargs): + def feature_engineering_expand_basic(self, dataframe: DataFrame, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -101,7 +101,7 @@ The FreqAI strategy requires including the following lines of code in the standa dataframe["%-raw_price"] = dataframe["close"] return dataframe - def feature_engineering_standard(self, dataframe, **kwargs): + def feature_engineering_standard(self, dataframe: DataFrame, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This optional function will be called once with the dataframe of the base timeframe. @@ -122,7 +122,7 @@ The FreqAI strategy requires including the following lines of code in the standa dataframe["%-hour_of_day"] = (dataframe["date"].dt.hour + 1) / 25 return dataframe - def set_freqai_targets(self, dataframe, **kwargs): + def set_freqai_targets(self, dataframe: DataFrame, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* Required function to set the targets for the model. @@ -139,6 +139,7 @@ The FreqAI strategy requires including the following lines of code in the standa / dataframe["close"] - 1 ) + return dataframe ``` Notice how the `feature_engineering_*()` is where [features](freqai-feature-engineering.md#feature-engineering) are added. Meanwhile `set_freqai_targets()` adds the labels/targets. A full example strategy is available in `templates/FreqaiExampleStrategy.py`. diff --git a/docs/freqai-reinforcement-learning.md b/docs/freqai-reinforcement-learning.md index f298dbf4d..962827348 100644 --- a/docs/freqai-reinforcement-learning.md +++ b/docs/freqai-reinforcement-learning.md @@ -37,7 +37,7 @@ freqtrade trade --freqaimodel ReinforcementLearner --strategy MyRLStrategy --con where `ReinforcementLearner` will use the templated `ReinforcementLearner` from `freqai/prediction_models/ReinforcementLearner` (or a custom user defined one located in `user_data/freqaimodels`). The strategy, on the other hand, follows the same base [feature engineering](freqai-feature-engineering.md) with `feature_engineering_*` as a typical Regressor. The difference lies in the creation of the targets, Reinforcement Learning doesn't require them. However, FreqAI requires a default (neutral) value to be set in the action column: ```python - def set_freqai_targets(self, dataframe, **kwargs): + def set_freqai_targets(self, dataframe, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* Required function to set the targets for the model. @@ -53,17 +53,19 @@ where `ReinforcementLearner` will use the templated `ReinforcementLearner` from # For RL, there are no direct targets to set. This is filler (neutral) # until the agent sends an action. dataframe["&-action"] = 0 + return dataframe ``` Most of the function remains the same as for typical Regressors, however, the function below shows how the strategy must pass the raw price data to the agent so that it has access to raw OHLCV in the training environment: ```python - def feature_engineering_standard(self, dataframe, **kwargs): + def feature_engineering_standard(self, dataframe: DataFrame, **kwargs) -> DataFrame: # The following features are necessary for RL models dataframe[f"%-raw_close"] = dataframe["close"] dataframe[f"%-raw_open"] = dataframe["open"] dataframe[f"%-raw_high"] = dataframe["high"] dataframe[f"%-raw_low"] = dataframe["low"] + return dataframe ``` Finally, there is no explicit "label" to make - instead it is necessary to assign the `&-action` column which will contain the agent's actions when accessed in `populate_entry/exit_trends()`. In the present example, the neutral action to 0. This value should align with the environment used. FreqAI provides two environments, both use 0 as the neutral action. diff --git a/docs/strategy_migration.md b/docs/strategy_migration.md index 22e3d2c22..5ef7a5a4c 100644 --- a/docs/strategy_migration.md +++ b/docs/strategy_migration.md @@ -578,7 +578,7 @@ def populate_any_indicators( Features will now expand automatically. As such, the expansion loops, as well as the `{pair}` / `{timeframe}` parts will need to be removed. ``` python linenums="1" - def feature_engineering_expand_all(self, dataframe, period, **kwargs): + def feature_engineering_expand_all(self, dataframe, period, **kwargs) -> DataFrame:: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -638,7 +638,7 @@ Features will now expand automatically. As such, the expansion loops, as well as Basic features. Make sure to remove the `{pair}` part from your features. ``` python linenums="1" - def feature_engineering_expand_basic(self, dataframe, **kwargs): + def feature_engineering_expand_basic(self, dataframe: DataFrame, **kwargs) -> DataFrame:: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -673,7 +673,7 @@ Basic features. Make sure to remove the `{pair}` part from your features. ### FreqAI - feature engineering standard ``` python linenums="1" - def feature_engineering_standard(self, dataframe, **kwargs): + def feature_engineering_standard(self, dataframe: DataFrame, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This optional function will be called once with the dataframe of the base timeframe. @@ -704,7 +704,7 @@ Basic features. Make sure to remove the `{pair}` part from your features. Targets now get their own, dedicated method. ``` python linenums="1" - def set_freqai_targets(self, dataframe, **kwargs): + def set_freqai_targets(self, dataframe: DataFrame, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* Required function to set the targets for the model. diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 3bc766d91..9bc23275d 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -618,7 +618,7 @@ class IStrategy(ABC, HyperStrategyMixin): return df def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, - metadata: Dict, **kwargs): + metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -644,7 +644,8 @@ class IStrategy(ABC, HyperStrategyMixin): """ return dataframe - def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs): + def feature_engineering_expand_basic( + self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -697,7 +698,7 @@ class IStrategy(ABC, HyperStrategyMixin): """ return dataframe - def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs): + def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* Required function to set the targets for the model. From ad4996259eb7db685803cc2329177457bd50d1e8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Apr 2023 19:36:17 +0200 Subject: [PATCH 30/85] Further increase typehints in freqAI interface --- docs/freqai-configuration.md | 2 +- docs/freqai-feature-engineering.md | 8 ++++---- freqtrade/strategy/interface.py | 3 ++- freqtrade/templates/FreqaiExampleHybridStrategy.py | 10 ++++++---- freqtrade/templates/FreqaiExampleStrategy.py | 10 ++++++---- 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/docs/freqai-configuration.md b/docs/freqai-configuration.md index 71c614b4a..e7aca20be 100644 --- a/docs/freqai-configuration.md +++ b/docs/freqai-configuration.md @@ -387,7 +387,7 @@ Here we create a `PyTorchMLPRegressor` class that implements the `fit` method. T For example, if you are using a binary classifier to predict price movements as up or down, you can set the class names as follows: ```python - def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs): + def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: self.freqai.class_names = ["down", "up"] dataframe['&s-up_or_down'] = np.where(dataframe["close"].shift(-100) > dataframe["close"], 'up', 'down') diff --git a/docs/freqai-feature-engineering.md b/docs/freqai-feature-engineering.md index 05c6db523..52d5e1b2c 100644 --- a/docs/freqai-feature-engineering.md +++ b/docs/freqai-feature-engineering.md @@ -16,7 +16,7 @@ Meanwhile, high level feature engineering is handled within `"feature_parameters It is advisable to start from the template `feature_engineering_*` functions in the source provided example strategy (found in `templates/FreqaiExampleStrategy.py`) to ensure that the feature definitions are following the correct conventions. Here is an example of how to set the indicators and labels in the strategy: ```python - def feature_engineering_expand_all(self, dataframe, period, metadata, **kwargs): + def feature_engineering_expand_all(self, dataframe: DataFrame, period, metadata, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -67,7 +67,7 @@ It is advisable to start from the template `feature_engineering_*` functions in return dataframe - def feature_engineering_expand_basic(self, dataframe, metadata, **kwargs): + def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -96,7 +96,7 @@ It is advisable to start from the template `feature_engineering_*` functions in dataframe["%-raw_price"] = dataframe["close"] return dataframe - def feature_engineering_standard(self, dataframe, metadata, **kwargs): + def feature_engineering_standard(self, dataframe: DataFrame, metadata, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This optional function will be called once with the dataframe of the base timeframe. @@ -122,7 +122,7 @@ It is advisable to start from the template `feature_engineering_*` functions in dataframe["%-hour_of_day"] = (dataframe["date"].dt.hour + 1) / 25 return dataframe - def set_freqai_targets(self, dataframe, metadata, **kwargs): + def set_freqai_targets(self, dataframe: DataFrame, metadata, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* Required function to set the targets for the model. diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 9bc23275d..7adb7a154 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -674,7 +674,8 @@ class IStrategy(ABC, HyperStrategyMixin): """ return dataframe - def feature_engineering_standard(self, dataframe: DataFrame, metadata: Dict, **kwargs): + def feature_engineering_standard( + self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This optional function will be called once with the dataframe of the base timeframe. diff --git a/freqtrade/templates/FreqaiExampleHybridStrategy.py b/freqtrade/templates/FreqaiExampleHybridStrategy.py index 3f27ee4a1..03446d76e 100644 --- a/freqtrade/templates/FreqaiExampleHybridStrategy.py +++ b/freqtrade/templates/FreqaiExampleHybridStrategy.py @@ -97,7 +97,7 @@ class FreqaiExampleHybridStrategy(IStrategy): exit_short_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True) def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, - metadata: Dict, **kwargs): + metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -151,7 +151,8 @@ class FreqaiExampleHybridStrategy(IStrategy): return dataframe - def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs): + def feature_engineering_expand_basic( + self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -183,7 +184,8 @@ class FreqaiExampleHybridStrategy(IStrategy): dataframe["%-raw_price"] = dataframe["close"] return dataframe - def feature_engineering_standard(self, dataframe: DataFrame, metadata: Dict, **kwargs): + def feature_engineering_standard( + self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This optional function will be called once with the dataframe of the base timeframe. @@ -209,7 +211,7 @@ class FreqaiExampleHybridStrategy(IStrategy): dataframe["%-hour_of_day"] = dataframe["date"].dt.hour return dataframe - def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs): + def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* Required function to set the targets for the model. diff --git a/freqtrade/templates/FreqaiExampleStrategy.py b/freqtrade/templates/FreqaiExampleStrategy.py index 0093c7f7a..493ea17f3 100644 --- a/freqtrade/templates/FreqaiExampleStrategy.py +++ b/freqtrade/templates/FreqaiExampleStrategy.py @@ -48,7 +48,7 @@ class FreqaiExampleStrategy(IStrategy): [0.75, 1, 1.25, 1.5, 1.75], space="sell", default=1.25, optimize=True) def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, - metadata: Dict, **kwargs): + metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -106,7 +106,8 @@ class FreqaiExampleStrategy(IStrategy): return dataframe - def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs): + def feature_engineering_expand_basic( + self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -142,7 +143,8 @@ class FreqaiExampleStrategy(IStrategy): dataframe["%-raw_price"] = dataframe["close"] return dataframe - def feature_engineering_standard(self, dataframe: DataFrame, metadata: Dict, **kwargs): + def feature_engineering_standard( + self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This optional function will be called once with the dataframe of the base timeframe. @@ -172,7 +174,7 @@ class FreqaiExampleStrategy(IStrategy): dataframe["%-hour_of_day"] = dataframe["date"].dt.hour return dataframe - def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs): + def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* Required function to set the targets for the model. From 2dd3b341360dc3f85068bf1408c6d2414cd08ace Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Apr 2023 19:36:27 +0200 Subject: [PATCH 31/85] Fix malrendering in freqAI docs --- docs/freqai-feature-engineering.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/freqai-feature-engineering.md b/docs/freqai-feature-engineering.md index 52d5e1b2c..82b7569a5 100644 --- a/docs/freqai-feature-engineering.md +++ b/docs/freqai-feature-engineering.md @@ -181,15 +181,14 @@ You can ask for each of the defined features to be included also for informative In total, the number of features the user of the presented example strat has created is: length of `include_timeframes` * no. features in `feature_engineering_expand_*()` * length of `include_corr_pairlist` * no. `include_shifted_candles` * length of `indicator_periods_candles` $= 3 * 3 * 3 * 2 * 2 = 108$. - ### Gain finer control over `feature_engineering_*` functions with `metadata` - All `feature_engineering_*` and `set_freqai_targets()` functions are passed a `metadata` dictionary which contains information about the `pair`, `tf` (timeframe), and `period` that FreqAI is automating for feature building. As such, a user can use `metadata` inside `feature_engineering_*` functions as criteria for blocking/reserving features for certain timeframes, periods, pairs etc. +All `feature_engineering_*` and `set_freqai_targets()` functions are passed a `metadata` dictionary which contains information about the `pair`, `tf` (timeframe), and `period` that FreqAI is automating for feature building. As such, a user can use `metadata` inside `feature_engineering_*` functions as criteria for blocking/reserving features for certain timeframes, periods, pairs etc. - ```python -def feature_engineering_expand_all(self, dataframe, period, metadata, **kwargs): - if metadata["tf"] == "1h": - dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period) +```python +def feature_engineering_expand_all(self, dataframe: DataFrame, period, metadata, **kwargs) -> DataFrame: + if metadata["tf"] == "1h": + dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period) ``` This will block `ta.ROC()` from being added to any timeframes other than `"1h"`. From 185ea9c98c187ddf56817c7f6faae9110abe3972 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Apr 2023 03:56:58 +0000 Subject: [PATCH 32/85] Bump websockets from 11.0.1 to 11.0.2 Bumps [websockets](https://github.com/aaugustin/websockets) from 11.0.1 to 11.0.2. - [Release notes](https://github.com/aaugustin/websockets/releases) - [Commits](https://github.com/aaugustin/websockets/compare/11.0.1...11.0.2) --- updated-dependencies: - dependency-name: websockets 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 7db852961..2872daab6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -54,7 +54,7 @@ python-dateutil==2.8.2 schedule==1.2.0 #WS Messages -websockets==11.0.1 +websockets==11.0.2 janus==1.0.0 ast-comments==1.0.1 From d320ea052f25ab711eea08884d9745334ac1c840 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Apr 2023 03:57:22 +0000 Subject: [PATCH 33/85] Bump ccxt from 3.0.69 to 3.0.75 Bumps [ccxt](https://github.com/ccxt/ccxt) from 3.0.69 to 3.0.75. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md) - [Commits](https://github.com/ccxt/ccxt/compare/3.0.69...3.0.75) --- 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 7db852961..10a27526a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.24.2 pandas==1.5.3 pandas-ta==0.3.14b -ccxt==3.0.69 +ccxt==3.0.75 cryptography==40.0.2 aiohttp==3.8.4 SQLAlchemy==2.0.9 From a64b641fdff0bd298fc82d3c8f2c4e013e88f94b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Apr 2023 03:57:40 +0000 Subject: [PATCH 34/85] Bump ruff from 0.0.261 to 0.0.262 Bumps [ruff](https://github.com/charliermarsh/ruff) from 0.0.261 to 0.0.262. - [Release notes](https://github.com/charliermarsh/ruff/releases) - [Changelog](https://github.com/charliermarsh/ruff/blob/main/BREAKING_CHANGES.md) - [Commits](https://github.com/charliermarsh/ruff/compare/v0.0.261...v0.0.262) --- updated-dependencies: - dependency-name: ruff 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 fc0efcfe7..cd4c96eea 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ -r docs/requirements-docs.txt coveralls==3.3.1 -ruff==0.0.261 +ruff==0.0.262 mypy==1.2.0 pre-commit==3.2.2 pytest==7.3.1 From 43d0c7ff98963d4519b495452f57dcac9b5dcdc7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Apr 2023 03:59:22 +0000 Subject: [PATCH 35/85] Bump numpy from 1.24.2 to 1.24.3 Bumps [numpy](https://github.com/numpy/numpy) from 1.24.2 to 1.24.3. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v1.24.2...v1.24.3) --- updated-dependencies: - dependency-name: numpy 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 7db852961..fae87d689 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -numpy==1.24.2 +numpy==1.24.3 pandas==1.5.3 pandas-ta==0.3.14b From 15fdaecd7f2b9fc95c75650376176c3be5069fd1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Apr 2023 03:59:43 +0000 Subject: [PATCH 36/85] Bump psutil from 5.9.4 to 5.9.5 Bumps [psutil](https://github.com/giampaolo/psutil) from 5.9.4 to 5.9.5. - [Release notes](https://github.com/giampaolo/psutil/releases) - [Changelog](https://github.com/giampaolo/psutil/blob/master/HISTORY.rst) - [Commits](https://github.com/giampaolo/psutil/compare/release-5.9.4...release-5.9.5) --- updated-dependencies: - dependency-name: psutil 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 7db852961..de9ad7434 100644 --- a/requirements.txt +++ b/requirements.txt @@ -40,7 +40,7 @@ pydantic==1.10.7 uvicorn==0.21.1 pyjwt==2.6.0 aiofiles==23.1.0 -psutil==5.9.4 +psutil==5.9.5 # Support for colorized terminal output colorama==0.4.6 From 598478e48da09fa2795e75601f83d56803821810 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Apr 2023 04:00:11 +0000 Subject: [PATCH 37/85] Bump filelock from 3.11.0 to 3.12.0 Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.11.0 to 3.12.0. - [Release notes](https://github.com/tox-dev/py-filelock/releases) - [Changelog](https://github.com/tox-dev/py-filelock/blob/main/docs/changelog.rst) - [Commits](https://github.com/tox-dev/py-filelock/compare/3.11.0...3.12.0) --- updated-dependencies: - dependency-name: filelock dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index c81c17f63..87b1fd3c8 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -5,4 +5,4 @@ scipy==1.10.1 scikit-learn==1.1.3 scikit-optimize==0.9.0 -filelock==3.11.0 +filelock==3.12.0 From c513d1077fdf64d643ae7172e06350a50abdcb03 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Apr 2023 05:32:38 +0000 Subject: [PATCH 38/85] Bump sqlalchemy from 2.0.9 to 2.0.10 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 2.0.9 to 2.0.10. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/main/CHANGES.rst) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) --- updated-dependencies: - dependency-name: sqlalchemy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 49f51c315..1e3c13504 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ pandas-ta==0.3.14b ccxt==3.0.75 cryptography==40.0.2 aiohttp==3.8.4 -SQLAlchemy==2.0.9 +SQLAlchemy==2.0.10 python-telegram-bot==13.15 arrow==1.2.3 cachetools==4.2.2 From 0329c0c3f9bd1043eae628388c5dac2649df619b Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Apr 2023 07:59:21 +0200 Subject: [PATCH 39/85] pre-commit - bump sqlalchemy --- .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 89370eacc..0031300cd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: - types-requests==2.28.11.17 - types-tabulate==0.9.0.2 - types-python-dateutil==2.8.19.12 - - SQLAlchemy==2.0.9 + - SQLAlchemy==2.0.10 # stages: [push] - repo: https://github.com/pycqa/isort From f7c6828e6a956a0813a498fda092ea7091d090f0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Apr 2023 06:02:27 +0000 Subject: [PATCH 40/85] Bump mkdocs-material from 9.1.6 to 9.1.7 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.1.6 to 9.1.7. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.1.6...9.1.7) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 550c3b54c..91b0e993b 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,6 +1,6 @@ markdown==3.3.7 mkdocs==1.4.2 -mkdocs-material==9.1.6 +mkdocs-material==9.1.7 mdx_truly_sane_lists==1.3 pymdown-extensions==9.11 jinja2==3.1.2 From d1e9e7039607a1bff60a95e772244625707b30c2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Apr 2023 09:41:36 +0200 Subject: [PATCH 41/85] Improve Resample-period test --- tests/optimize/test_optimize_reports.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 9fdd0d61e..6428177c5 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -9,7 +9,7 @@ import pytest from arrow import Arrow from freqtrade.configuration import TimeRange -from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN +from freqtrade.constants import BACKTEST_BREAKDOWNS, DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN from freqtrade.data import history from freqtrade.data.btanalysis import (get_latest_backtest_filename, load_backtest_data, load_backtest_stats) @@ -470,6 +470,9 @@ def test__get_resample_from_period(): with pytest.raises(ValueError, match=r"Period noooo is not supported."): _get_resample_from_period('noooo') + for period in BACKTEST_BREAKDOWNS: + assert isinstance(_get_resample_from_period(period), str) + def test_show_sorted_pairlist(testdatadir, default_conf, capsys): filename = testdatadir / "backtest_results/backtest-result.json" From 3948890c3b9b45bfd0571bf923be1774f97cbe32 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Apr 2023 10:35:46 +0200 Subject: [PATCH 42/85] Add --breakdown to backtest-show --- freqtrade/commands/arguments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 47aa37fdf..109516f87 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -46,7 +46,7 @@ ARGS_LIST_FREQAIMODELS = ["freqaimodel_path", "print_one_column", "print_coloriz ARGS_LIST_HYPEROPTS = ["hyperopt_path", "print_one_column", "print_colorized"] -ARGS_BACKTEST_SHOW = ["exportfilename", "backtest_show_pair_list"] +ARGS_BACKTEST_SHOW = ["exportfilename", "backtest_show_pair_list", "backtest_breakdown"] ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"] From e99af87b6dc164fa1f32ad395d5bc52d69f0ec55 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Apr 2023 10:36:30 +0200 Subject: [PATCH 43/85] store periodic breakdown in backtest results This will enable the webserver to use this data. --- freqtrade/optimize/hyperopt.py | 3 ++- freqtrade/optimize/optimize_reports.py | 29 ++++++++++++++++++++------ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index ee5599e20..fe590f0d2 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -379,7 +379,8 @@ class Hyperopt: strat_stats = generate_strategy_stats( self.pairlist, self.backtesting.strategy.get_strategy_name(), - backtesting_results, min_date, max_date, market_change=self.market_change + backtesting_results, min_date, max_date, market_change=self.market_change, + is_hyperopt=True, ) results_explanation = HyperoptTools.format_results_explanation_string( strat_stats, self.config['stake_currency']) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index cbae6aede..1c5088cc1 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -7,8 +7,8 @@ from typing import Any, Dict, List, Union from pandas import DataFrame, to_datetime from tabulate import tabulate -from freqtrade.constants import (DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT, - Config, IntOrInf) +from freqtrade.constants import (BACKTEST_BREAKDOWNS, DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, + UNLIMITED_STAKE_AMOUNT, Config, IntOrInf) from freqtrade.data.metrics import (calculate_cagr, calculate_calmar, calculate_csum, calculate_expectancy, calculate_market_change, calculate_max_drawdown, calculate_sharpe, calculate_sortino) @@ -296,6 +296,7 @@ def generate_periodic_breakdown_stats(trade_list: List, period: str) -> List[Dic stats.append( { 'date': name.strftime('%d/%m/%Y'), + 'date_ts': int(name.to_pydatetime().timestamp() * 1000), 'profit_abs': profit_abs, 'wins': wins, 'draws': draws, @@ -305,6 +306,13 @@ def generate_periodic_breakdown_stats(trade_list: List, period: str) -> List[Dic return stats +def generate_all_periodic_breakdown_stats(trade_list: List) -> Dict[str, List]: + result = {} + for period in BACKTEST_BREAKDOWNS: + result[period] = generate_periodic_breakdown_stats(trade_list, period) + return result + + def generate_trading_stats(results: DataFrame) -> Dict[str, Any]: """ Generate overall trade statistics """ if len(results) == 0: @@ -381,7 +389,8 @@ def generate_strategy_stats(pairlist: List[str], strategy: str, content: Dict[str, Any], min_date: datetime, max_date: datetime, - market_change: float + market_change: float, + is_hyperopt: bool = False, ) -> Dict[str, Any]: """ :param pairlist: List of pairs to backtest @@ -416,6 +425,11 @@ def generate_strategy_stats(pairlist: List[str], daily_stats = generate_daily_stats(results) trade_stats = generate_trading_stats(results) + + periodic_breakdown = {} + if not is_hyperopt: + periodic_breakdown = {'periodic_breakdown': generate_all_periodic_breakdown_stats(results)} + best_pair = max([pair for pair in pair_results if pair['key'] != 'TOTAL'], key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None worst_pair = min([pair for pair in pair_results if pair['key'] != 'TOTAL'], @@ -434,7 +448,6 @@ def generate_strategy_stats(pairlist: List[str], 'results_per_enter_tag': enter_tag_results, 'exit_reason_summary': exit_reason_stats, 'left_open_trades': left_open_results, - # 'days_breakdown_stats': days_breakdown_stats, 'total_trades': len(results), 'trade_count_long': len(results.loc[~results['is_short']]), @@ -499,6 +512,7 @@ def generate_strategy_stats(pairlist: List[str], 'exit_profit_only': config['exit_profit_only'], 'exit_profit_offset': config['exit_profit_offset'], 'ignore_roi_if_entry_signal': config['ignore_roi_if_entry_signal'], + **periodic_breakdown, **daily_stats, **trade_stats } @@ -891,8 +905,11 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: print(table) for period in backtest_breakdown: - days_breakdown_stats = generate_periodic_breakdown_stats( - trade_list=results['trades'], period=period) + if period in results.get('periodic_breakdown', {}): + days_breakdown_stats = results['periodic_breakdown'][period] + else: + days_breakdown_stats = generate_periodic_breakdown_stats( + trade_list=results['trades'], period=period) table = text_table_periodic_breakdown(days_breakdown_stats=days_breakdown_stats, stake_currency=stake_currency, period=period) if isinstance(table, str) and len(table) > 0: From 68a8c79c08fab763605c6ea2ee4acd8e093d7608 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Apr 2023 12:03:00 +0200 Subject: [PATCH 44/85] Improve output for futures --- freqtrade/rpc/rpc.py | 4 +++- freqtrade/rpc/telegram.py | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 222912aa0..35e08cbc0 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -592,7 +592,7 @@ class RPC: if self._config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: # in Futures, "total" includes the locked stake, and therefore all positions est_stake = balance.free - est_bot_stake = est_stake + est_bot_stake = amount else: try: pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency) @@ -665,6 +665,7 @@ class RPC: position: PositionWallet for symbol, position in self._freqtrade.wallets.get_all_positions().items(): total += position.collateral + total_bot += position.collateral currencies.append({ 'currency': symbol, @@ -673,6 +674,7 @@ class RPC: 'used': 0, 'position': position.position, 'est_stake': position.collateral, + 'est_stake_bot': position.collateral, 'stake': stake_currency, 'leverage': position.leverage, 'side': position.side, diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 2b997efac..e99501cc0 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -928,7 +928,10 @@ class Telegram(RPCHandler): total_dust_currencies = 0 for curr in result['currencies']: curr_output = '' - if curr['est_stake'] > balance_dust_level and (full_result or curr['is_bot_managed']): + if ( + (curr['is_position'] or curr['est_stake'] > balance_dust_level) + and (full_result or curr['is_bot_managed']) + ): if curr['is_position']: curr_output = ( f"*{curr['currency']}:*\n" From 829724c0ec579d7dab9ef1473ecf70e5c02392ec Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Apr 2023 12:13:24 +0200 Subject: [PATCH 45/85] Fallback to "initialMargin" if collateral is not set --- freqtrade/wallets.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 8dcc92af4..6f86398f3 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -11,6 +11,7 @@ from freqtrade.constants import UNLIMITED_STAKE_AMOUNT, Config from freqtrade.enums import RunMode, TradingMode from freqtrade.exceptions import DependencyException from freqtrade.exchange import Exchange +from freqtrade.misc import safe_value_fallback from freqtrade.persistence import LocalTrade, Trade @@ -148,7 +149,7 @@ class Wallets: # Position is not open ... continue size = self._exchange._contracts_to_amount(symbol, position['contracts']) - collateral = position['collateral'] or 0.0 + collateral = safe_value_fallback(position, 'collateral', 'initialMargin', 0.0) leverage = position['leverage'] self._positions[symbol] = PositionWallet( symbol, position=size, From 8086d9053573b7c4aef6afdc1ab18e9085d1eac0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Apr 2023 12:12:45 +0200 Subject: [PATCH 46/85] Update some tests for balance updates --- tests/rpc/test_rpc.py | 9 ++++++--- tests/rpc/test_rpc_apiserver.py | 4 ++-- tests/rpc/test_rpc_telegram.py | 3 ++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 5335cebf9..bb84ff8e9 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -548,7 +548,7 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): 'used': 2.0, 'bot_owned': 9.9, # available stake - reducing by reserved amount 'est_stake': 10.0, # In futures mode, "free" is used here. - 'est_stake_bot': 10, + 'est_stake_bot': 9.9, 'stake': 'BTC', 'is_position': False, 'leverage': 1.0, @@ -591,6 +591,7 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): 'balance': 0.0, 'currency': 'ETH/USDT:USDT', 'est_stake': 20, + 'est_stake_bot': 20, 'used': 0, 'stake': 'BTC', 'is_position': True, @@ -600,10 +601,12 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): 'is_bot_managed': True, } ] - assert pytest.approx(result['total_bot']) == 10 + assert pytest.approx(result['total_bot']) == 29.9 assert pytest.approx(result['total']) == 30.309096 assert result['starting_capital'] == 10 - assert result['starting_capital_ratio'] == 0.0 + # Very high starting capital ratio, because the futures position really has the wrong unit. + # TODO: improve this test (see comment above) + assert result['starting_capital_ratio'] == pytest.approx(1.98999999) def test_rpc_start(mocker, default_conf) -> None: diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index a4638a07e..e045bf487 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -482,7 +482,7 @@ def test_api_balance(botclient, mocker, rpc_balance, tickers): 'used': 0.0, 'bot_owned': pytest.approx(11.879999), 'est_stake': 12.0, - 'est_stake_bot': 12.0, + 'est_stake_bot': pytest.approx(11.879999), 'stake': 'BTC', 'is_position': False, 'leverage': 1.0, @@ -491,7 +491,7 @@ def test_api_balance(botclient, mocker, rpc_balance, tickers): 'is_bot_managed': True, } assert response['total'] == 12.159513094 - assert response['total_bot'] == 12.0 + assert response['total_bot'] == pytest.approx(11.879999) assert 'starting_capital' in response assert 'starting_capital_fiat' in response assert 'starting_capital_pct' in response diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 97ac03c9f..9b22b73c0 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -799,7 +799,8 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick assert '*XRP:*' not in result assert 'Balance:' in result assert 'Est. BTC:' in result - assert 'BTC: 12' in result + assert 'BTC: 11' in result + assert 'BTC: 12' in result_full assert "*3 Other Currencies (< 0.0001 BTC):*" in result assert 'BTC: 0.00000309' in result assert '*Estimated Value*:' in result_full From 98db27e8f49261bf257952fb39f851d7273c3dcd Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Apr 2023 14:05:35 +0200 Subject: [PATCH 47/85] Bump develop version to 2023.5-dev --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index f8955b295..f8818c35c 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2023.4.dev' +__version__ = '2023.5.dev' if 'dev' in __version__: from pathlib import Path From 99a4a6405280f085d13d9edaf6a293a13f373497 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Apr 2023 08:23:55 +0000 Subject: [PATCH 48/85] Bump python-telegram-bot from 13.15 to 20.2 Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 13.15 to 20.2. - [Release notes](https://github.com/python-telegram-bot/python-telegram-bot/releases) - [Changelog](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/CHANGES.rst) - [Commits](https://github.com/python-telegram-bot/python-telegram-bot/compare/v13.15...v20.2) --- updated-dependencies: - dependency-name: python-telegram-bot dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9d51852fc..a07ef8897 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ ccxt==3.0.75 cryptography==40.0.2 aiohttp==3.8.4 SQLAlchemy==2.0.10 -python-telegram-bot==13.15 +python-telegram-bot==20.2 arrow==1.2.3 cachetools==4.2.2 requests==2.28.2 From da261003df117f5d221b8f32ef9bb4a99dfe71ce Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 2 Jan 2023 10:52:51 +0100 Subject: [PATCH 49/85] Fix telegram imports to match v20.0 --- freqtrade/rpc/telegram.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index e99501cc0..9fb394b1e 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -17,11 +17,12 @@ from typing import Any, Callable, Dict, List, Optional, Union import arrow from tabulate import tabulate -from telegram import (MAX_MESSAGE_LENGTH, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, - KeyboardButton, ParseMode, ReplyKeyboardMarkup, Update) +from telegram import (CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, + ReplyKeyboardMarkup, Update) +from telegram.constants import MessageLimit, ParseMode from telegram.error import BadRequest, NetworkError, TelegramError from telegram.ext import CallbackContext, CallbackQueryHandler, CommandHandler, Updater -from telegram.utils.helpers import escape_markdown +from telegram.helpers import escape_markdown from freqtrade.__init__ import __version__ from freqtrade.constants import DUST_PER_COIN, Config @@ -33,6 +34,9 @@ from freqtrade.rpc import RPC, RPCException, RPCHandler from freqtrade.rpc.rpc_types import RPCSendMsg +MAX_MESSAGE_LENGTH = MessageLimit.MAX_TEXT_LENGTH + + logger = logging.getLogger(__name__) logger.debug('Included module rpc.telegram ...') From c37b7b77e42fdc574a6a5a9d8486a55b4bab0af4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 2 Jan 2023 10:55:55 +0100 Subject: [PATCH 50/85] move telegram fixture to telegram file --- tests/conftest.py | 10 +--------- tests/rpc/test_rpc_telegram.py | 8 ++++++++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 2f2345d54..1c737b3aa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,7 @@ import json import logging import re from copy import deepcopy -from datetime import datetime, timedelta +from datetime import timedelta from pathlib import Path from typing import Optional from unittest.mock import MagicMock, Mock, PropertyMock @@ -12,7 +12,6 @@ import arrow import numpy as np import pandas as pd import pytest -from telegram import Chat, Message, Update from freqtrade import constants from freqtrade.commands import Arguments @@ -550,13 +549,6 @@ def get_default_conf_usdt(testdatadir): return configuration -@pytest.fixture -def update(): - _update = Update(0) - _update.message = Message(0, datetime.utcnow(), Chat(0, 0)) - return _update - - @pytest.fixture def fee(): return MagicMock(return_value=0.0025) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 9b22b73c0..0da24dc4a 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -43,6 +43,14 @@ def default_conf(default_conf) -> dict: return default_conf +@pytest.fixture +def update(): + message = Message(0, datetime.utcnow(), Chat(0, 0)) + _update = Update(0, message=message) + + return _update + + class DummyCls(Telegram): """ Dummy class for testing the Telegram @authorized_only decorator From 57eed50acb5fde4343705176d881c3bff9bff18f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 2 Jan 2023 11:05:12 +0100 Subject: [PATCH 51/85] Fix some test failures caused by v20 update --- tests/rpc/test_rpc_telegram.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 0da24dc4a..e70924a1d 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -159,8 +159,8 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None: patch_exchange(mocker) caplog.set_level(logging.DEBUG) chat = Chat(0xdeadbeef, 0) - update = Update(randint(1, 100)) - update.message = Message(randint(1, 100), datetime.utcnow(), chat) + message = Message(randint(1, 100), datetime.utcnow(), chat) + update = Update(randint(1, 100), message=message) default_conf['telegram']['enabled'] = False bot = FreqtradeBot(default_conf) @@ -193,9 +193,7 @@ def test_authorized_only_exception(default_conf, mocker, caplog, update) -> None def test_telegram_status(default_conf, update, mocker) -> None: - update.message.chat.id = "123" default_conf['telegram']['enabled'] = False - default_conf['telegram']['chat_id'] = "123" status_table = MagicMock() mocker.patch('freqtrade.rpc.telegram.Telegram._status_table', status_table) @@ -252,9 +250,7 @@ def test_telegram_status(default_conf, update, mocker) -> None: @pytest.mark.usefixtures("init_persistence") def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None: - update.message.chat.id = "123" default_conf['telegram']['enabled'] = False - default_conf['telegram']['chat_id'] = "123" default_conf['position_adjustment_enable'] = True mocker.patch.multiple( EXMS, @@ -305,9 +301,7 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None: @pytest.mark.usefixtures("init_persistence") def test_telegram_status_closed_trade(default_conf, update, mocker, fee) -> None: - update.message.chat.id = "123" default_conf['telegram']['enabled'] = False - default_conf['telegram']['chat_id'] = "123" default_conf['position_adjustment_enable'] = True mocker.patch.multiple( EXMS, @@ -1277,7 +1271,6 @@ def test_force_enter_handle_exception(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) - update.message.text = '/forcebuy ETH/Nonepair' telegram._force_enter(update=update, context=MagicMock(), order_side=SignalDirection.LONG) assert msg_mock.call_count == 1 From 68ac934929a48033e58c9ffaa36a7394ea8d6b8f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 2 Jan 2023 11:29:29 +0100 Subject: [PATCH 52/85] Update command list to handle frozenSets --- freqtrade/rpc/telegram.py | 2 +- tests/rpc/test_rpc_telegram.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 9fb394b1e..25a75c433 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -235,7 +235,7 @@ class Telegram(RPCHandler): ) logger.info( 'rpc.telegram is listening for following commands: %s', - [h.command for h in handles] + [[x for x in sorted(h.commands)] for h in handles] ) def cleanup(self) -> None: diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index e70924a1d..7510255e9 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -115,14 +115,14 @@ def test_telegram_init(default_conf, mocker, caplog) -> None: message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], " "['balance'], ['start'], ['stop'], " - "['forcesell', 'forceexit', 'fx'], ['forcebuy', 'forcelong'], ['forceshort'], " - "['trades'], ['delete'], ['coo', 'cancel_open_order'], ['performance'], " - "['buys', 'entries'], ['sells', 'exits'], ['mix_tags'], " + "['forceexit', 'forcesell', 'fx'], ['forcebuy', 'forcelong'], ['forceshort'], " + "['trades'], ['delete'], ['cancel_open_order', 'coo'], ['performance'], " + "['buys', 'entries'], ['exits', 'sells'], ['mix_tags'], " "['stats'], ['daily'], ['weekly'], ['monthly'], " - "['count'], ['locks'], ['unlock', 'delete_locks'], " - "['reload_config', 'reload_conf'], ['show_config', 'show_conf'], " + "['count'], ['locks'], ['delete_locks', 'unlock'], " + "['reload_conf', 'reload_config'], ['show_conf', 'show_config'], " "['stopbuy', 'stopentry'], ['whitelist'], ['blacklist'], " - "['blacklist_delete', 'bl_delete'], " + "['bl_delete', 'blacklist_delete'], " "['logs'], ['edge'], ['health'], ['help'], ['version'], ['marketdir']" "]") From 14b501a4f74a38d07e8fb88e677b6b55e4fce37a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 2 Jan 2023 13:49:25 +0100 Subject: [PATCH 53/85] Initial changes for telegram migration --- freqtrade/rpc/telegram.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 25a75c433..5a070bb2e 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -21,7 +21,7 @@ from telegram import (CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, ReplyKeyboardMarkup, Update) from telegram.constants import MessageLimit, ParseMode from telegram.error import BadRequest, NetworkError, TelegramError -from telegram.ext import CallbackContext, CallbackQueryHandler, CommandHandler, Updater +from telegram.ext import Application, CallbackContext, CallbackQueryHandler, CommandHandler, Updater from telegram.helpers import escape_markdown from freqtrade.__init__ import __version__ @@ -104,6 +104,7 @@ class Telegram(RPCHandler): super().__init__(rpc, config) self._updater: Updater + self._app: Application self._init_keyboard() self._init() @@ -162,8 +163,9 @@ class Telegram(RPCHandler): registers all known command handlers and starts polling for message updates """ - self._updater = Updater(token=self._config['telegram']['token'], workers=0, - use_context=True) + self._app = Application.builder().token(self._config['telegram']['token']).build + # self._updater = Updater(token=, workers=0, + # use_context=True) # Register command handler and start telegram message polling handles = [ @@ -222,12 +224,12 @@ class Telegram(RPCHandler): CallbackQueryHandler(self._force_enter_inline, pattern=r"\S+\/\S+"), ] for handle in handles: - self._updater.dispatcher.add_handler(handle) + self._app.add_handler(handle) for callback in callbacks: - self._updater.dispatcher.add_handler(callback) + self._app.add_handler(callback) - self._updater.start_polling( + self._app.run_polling( bootstrap_retries=-1, timeout=20, read_latency=60, # Assumed transmission latency @@ -244,7 +246,7 @@ class Telegram(RPCHandler): :return: None """ # This can take up to `timeout` from the call to `start_polling`. - self._updater.stop() + self._app.stop() def _exchange_from_msg(self, msg: Dict[str, Any]) -> str: """ From cb45689c1da939f7b28ea67edf41be2c28fb6e7e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 14 Apr 2023 18:03:35 +0200 Subject: [PATCH 54/85] Small fixes to new telegram implementation --- freqtrade/rpc/telegram.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 5a070bb2e..d5edd0871 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -163,7 +163,7 @@ class Telegram(RPCHandler): registers all known command handlers and starts polling for message updates """ - self._app = Application.builder().token(self._config['telegram']['token']).build + self._app = Application.builder().token(self._config['telegram']['token']).build() # self._updater = Updater(token=, workers=0, # use_context=True) @@ -229,16 +229,16 @@ class Telegram(RPCHandler): for callback in callbacks: self._app.add_handler(callback) - self._app.run_polling( - bootstrap_retries=-1, - timeout=20, - read_latency=60, # Assumed transmission latency - drop_pending_updates=True, - ) logger.info( 'rpc.telegram is listening for following commands: %s', [[x for x in sorted(h.commands)] for h in handles] ) + self._app.run_polling( + bootstrap_retries=-1, + timeout=20, + # read_latency=60, # Assumed transmission latency + drop_pending_updates=True, + ) def cleanup(self) -> None: """ From 5134bf8ec382240480214eea6b29c12a1f8c612c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Apr 2023 09:46:51 +0200 Subject: [PATCH 55/85] Authorized-only and /version to async --- freqtrade/rpc/telegram.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index d5edd0871..99e6a9ba7 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -58,7 +58,7 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]: :return: decorated function """ - def wrapper(self, *args, **kwargs): + async def wrapper(self, *args, **kwargs): """ Decorator logic """ update = kwargs.get('update') or args[0] @@ -80,9 +80,9 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]: chat_id ) try: - return command_handler(self, *args, **kwargs) + return await command_handler(self, *args, **kwargs) except RPCException as e: - self._send_msg(str(e)) + await self._send_msg(str(e)) except BaseException: logger.exception('Exception occurred within Telegram module') finally: @@ -1589,7 +1589,7 @@ class Telegram(RPCHandler): self._send_msg(message) @authorized_only - def _version(self, update: Update, context: CallbackContext) -> None: + async def _version(self, update: Update, context: CallbackContext) -> None: """ Handler for /version. Show version information @@ -1602,7 +1602,7 @@ class Telegram(RPCHandler): if strategy_version is not None: version_string += f', *Strategy version: * `{strategy_version}`' - self._send_msg(version_string) + await self._send_msg(version_string) @authorized_only def _show_config(self, update: Update, context: CallbackContext) -> None: @@ -1680,12 +1680,12 @@ class Telegram(RPCHandler): except TelegramError as telegram_err: logger.warning('TelegramError: %s! Giving up on that message.', telegram_err.message) - def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN, - disable_notification: bool = False, - keyboard: Optional[List[List[InlineKeyboardButton]]] = None, - callback_path: str = "", - reload_able: bool = False, - query: Optional[CallbackQuery] = None) -> None: + async def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN, + disable_notification: bool = False, + keyboard: Optional[List[List[InlineKeyboardButton]]] = None, + callback_path: str = "", + reload_able: bool = False, + query: Optional[CallbackQuery] = None) -> None: """ Send given markdown message :param msg: message @@ -1708,7 +1708,7 @@ class Telegram(RPCHandler): reply_markup = ReplyKeyboardMarkup(self._keyboard, resize_keyboard=True) try: try: - self._updater.bot.send_message( + await self._app.bot.send_message( self._config['telegram']['chat_id'], text=msg, parse_mode=parse_mode, @@ -1722,7 +1722,7 @@ class Telegram(RPCHandler): 'Telegram NetworkError: %s! Trying one more time.', network_err.message ) - self._updater.bot.send_message( + await self._app.bot.send_message( self._config['telegram']['chat_id'], text=msg, parse_mode=parse_mode, From e7e6f719e4a769cef61e47b64dbdb8acbfce082f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Apr 2023 09:52:14 +0200 Subject: [PATCH 56/85] _update_msg to async --- freqtrade/rpc/telegram.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 99e6a9ba7..f85fd5e08 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -103,7 +103,6 @@ class Telegram(RPCHandler): """ super().__init__(rpc, config) - self._updater: Updater self._app: Application self._init_keyboard() self._init() @@ -164,8 +163,6 @@ class Telegram(RPCHandler): and starts polling for message updates """ self._app = Application.builder().token(self._config['telegram']['token']).build() - # self._updater = Updater(token=, workers=0, - # use_context=True) # Register command handler and start telegram message polling handles = [ @@ -1650,8 +1647,8 @@ class Telegram(RPCHandler): f"*Current state:* `{val['state']}`" ) - def _update_msg(self, query: CallbackQuery, msg: str, callback_path: str = "", - reload_able: bool = False, parse_mode: str = ParseMode.MARKDOWN) -> None: + async def _update_msg(self, query: CallbackQuery, msg: str, callback_path: str = "", + reload_able: bool = False, parse_mode: str = ParseMode.MARKDOWN) -> None: if reload_able: reply_markup = InlineKeyboardMarkup([ [InlineKeyboardButton("Refresh", callback_data=callback_path)], @@ -1665,7 +1662,7 @@ class Telegram(RPCHandler): message_id = query.message.message_id try: - self._updater.bot.edit_message_text( + await self._app.bot.edit_message_text( chat_id=chat_id, message_id=message_id, text=msg, @@ -1695,8 +1692,8 @@ class Telegram(RPCHandler): """ reply_markup: Union[InlineKeyboardMarkup, ReplyKeyboardMarkup] if query: - self._update_msg(query=query, msg=msg, parse_mode=parse_mode, - callback_path=callback_path, reload_able=reload_able) + await self._update_msg(query=query, msg=msg, parse_mode=parse_mode, + callback_path=callback_path, reload_able=reload_able) return if reload_able and self._config['telegram'].get('reload', True): reply_markup = InlineKeyboardMarkup([ From 54732b72fd99dcf0174db1ff0292b018ae7ababb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Apr 2023 13:10:14 +0200 Subject: [PATCH 57/85] Manage startup/teardown of telegram manually --- freqtrade/rpc/telegram.py | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index f85fd5e08..8202a03c2 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -3,6 +3,7 @@ """ This module manage Telegram communication """ +import asyncio import json import logging import re @@ -13,6 +14,7 @@ from functools import partial from html import escape from itertools import chain from math import isnan +from threading import Thread from typing import Any, Callable, Dict, List, Optional, Union import arrow @@ -104,8 +106,16 @@ class Telegram(RPCHandler): super().__init__(rpc, config) self._app: Application + self._loop: asyncio.AbstractEventLoop self._init_keyboard() - self._init() + self._start_thread() + + def _start_thread(self): + """ + Creates and starts the polling thread + """ + self._thread = Thread(target=self._init, name='FTTelegram') + self._thread.start() def _init_keyboard(self) -> None: """ @@ -161,7 +171,14 @@ class Telegram(RPCHandler): Initializes this module with the given config, registers all known command handlers and starts polling for message updates + Runs in a separate thread. """ + try: + self._loop = asyncio.get_running_loop() + except RuntimeError: + self._loop = asyncio.new_event_loop() + asyncio.set_event_loop(self._loop) + self._app = Application.builder().token(self._config['telegram']['token']).build() # Register command handler and start telegram message polling @@ -230,12 +247,27 @@ class Telegram(RPCHandler): 'rpc.telegram is listening for following commands: %s', [[x for x in sorted(h.commands)] for h in handles] ) - self._app.run_polling( + self._loop.run_until_complete(self._startup_telegram()) + + async def _startup_telegram(self) -> None: + await self._app.initialize() + await self._app.start() + await self._app.updater.start_polling( bootstrap_retries=-1, timeout=20, # read_latency=60, # Assumed transmission latency drop_pending_updates=True, + # stop_signals=[], # Necessary as we don't run on the main thread ) + while True: + await asyncio.sleep(10) + if not self._app.updater.running: + break + + async def _cleanup_telegram(self) -> None: + await self._app.updater.stop() + await self._app.stop() + await self._app.shutdown() def cleanup(self) -> None: """ @@ -243,7 +275,8 @@ class Telegram(RPCHandler): :return: None """ # This can take up to `timeout` from the call to `start_polling`. - self._app.stop() + asyncio.run_coroutine_threadsafe(self._cleanup_telegram(), self._loop) + self._thread.join() def _exchange_from_msg(self, msg: Dict[str, Any]) -> str: """ From 3d0e1d142f4cd03e9305b14f0d67f2d06f9e68b2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Apr 2023 13:24:12 +0200 Subject: [PATCH 58/85] Convert endpoints to async --- freqtrade/rpc/telegram.py | 241 +++++++++++++++++++------------------- 1 file changed, 121 insertions(+), 120 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 8202a03c2..c61e36116 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -23,7 +23,7 @@ from telegram import (CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, ReplyKeyboardMarkup, Update) from telegram.constants import MessageLimit, ParseMode from telegram.error import BadRequest, NetworkError, TelegramError -from telegram.ext import Application, CallbackContext, CallbackQueryHandler, CommandHandler, Updater +from telegram.ext import Application, CallbackContext, CallbackQueryHandler, CommandHandler from telegram.helpers import escape_markdown from freqtrade.__init__ import __version__ @@ -489,7 +489,9 @@ class Telegram(RPCHandler): message = self.compose_message(deepcopy(msg), msg_type) # type: ignore if message: - self._send_msg(message, disable_notification=(noti == 'silent')) + asyncio.run_coroutine_threadsafe( + self._send_msg(message, disable_notification=(noti == 'silent')), + self._loop) def _get_sell_emoji(self, msg): """ @@ -572,7 +574,7 @@ class Telegram(RPCHandler): return lines_detail @authorized_only - def _status(self, update: Update, context: CallbackContext) -> None: + async def _status(self, update: Update, context: CallbackContext) -> None: """ Handler for /status. Returns the current TradeThread status @@ -582,12 +584,12 @@ class Telegram(RPCHandler): """ if context.args and 'table' in context.args: - self._status_table(update, context) + await self._status_table(update, context) return else: - self._status_msg(update, context) + await self._status_msg(update, context) - def _status_msg(self, update: Update, context: CallbackContext) -> None: + async def _status_msg(self, update: Update, context: CallbackContext) -> None: """ handler for `/status` and `/status `. @@ -671,9 +673,9 @@ class Telegram(RPCHandler): lines_detail = self._prepare_order_details( r['orders'], r['quote_currency'], r['is_open']) lines.extend(lines_detail if lines_detail else "") - self.__send_status_msg(lines, r) + await self.__send_status_msg(lines, r) - def __send_status_msg(self, lines: List[str], r: Dict[str, Any]) -> None: + async def __send_status_msg(self, lines: List[str], r: Dict[str, Any]) -> None: """ Send status message. """ @@ -684,13 +686,13 @@ class Telegram(RPCHandler): if (len(msg) + len(line) + 1) < MAX_MESSAGE_LENGTH: msg += line + '\n' else: - self._send_msg(msg.format(**r)) + await self._send_msg(msg.format(**r)) msg = "*Trade ID:* `{trade_id}` - continued\n" + line + '\n' - self._send_msg(msg.format(**r)) + await self._send_msg(msg.format(**r)) @authorized_only - def _status_table(self, update: Update, context: CallbackContext) -> None: + async def _status_table(self, update: Update, context: CallbackContext) -> None: """ Handler for /status table. Returns the current TradeThread status in table format @@ -723,12 +725,11 @@ class Telegram(RPCHandler): # insert separators line between Total lines = message.split("\n") message = "\n".join(lines[:-1] + [lines[1]] + [lines[-1]]) - self._send_msg(f"
{message}
", parse_mode=ParseMode.HTML, - reload_able=True, callback_path="update_status_table", - query=update.callback_query) + await self._send_msg(f"
{message}
", parse_mode=ParseMode.HTML, + reload_able=True, callback_path="update_status_table", + query=update.callback_query) - @authorized_only - def _timeunit_stats(self, update: Update, context: CallbackContext, unit: str) -> None: + async def _timeunit_stats(self, update: Update, context: CallbackContext, unit: str) -> None: """ Handler for /daily Returns a daily profit (in BTC) over the last n days. @@ -775,11 +776,11 @@ class Telegram(RPCHandler): f'{val.message} Profit over the last {timescale} {val.message2}:\n' f'
{stats_tab}
' ) - self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True, - callback_path=val.callback, query=update.callback_query) + await self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True, + callback_path=val.callback, query=update.callback_query) @authorized_only - def _daily(self, update: Update, context: CallbackContext) -> None: + async def _daily(self, update: Update, context: CallbackContext) -> None: """ Handler for /daily Returns a daily profit (in BTC) over the last n days. @@ -787,10 +788,10 @@ class Telegram(RPCHandler): :param update: message update :return: None """ - self._timeunit_stats(update, context, 'days') + await self._timeunit_stats(update, context, 'days') @authorized_only - def _weekly(self, update: Update, context: CallbackContext) -> None: + async def _weekly(self, update: Update, context: CallbackContext) -> None: """ Handler for /weekly Returns a weekly profit (in BTC) over the last n weeks. @@ -798,10 +799,10 @@ class Telegram(RPCHandler): :param update: message update :return: None """ - self._timeunit_stats(update, context, 'weeks') + await self._timeunit_stats(update, context, 'weeks') @authorized_only - def _monthly(self, update: Update, context: CallbackContext) -> None: + async def _monthly(self, update: Update, context: CallbackContext) -> None: """ Handler for /monthly Returns a monthly profit (in BTC) over the last n months. @@ -809,10 +810,10 @@ class Telegram(RPCHandler): :param update: message update :return: None """ - self._timeunit_stats(update, context, 'months') + await self._timeunit_stats(update, context, 'months') @authorized_only - def _profit(self, update: Update, context: CallbackContext) -> None: + async def _profit(self, update: Update, context: CallbackContext) -> None: """ Handler for /profit. Returns a cumulative profit statistics. @@ -886,11 +887,11 @@ class Telegram(RPCHandler): f"*Max Drawdown:* `{stats['max_drawdown']:.2%} " f"({round_coin_value(stats['max_drawdown_abs'], stake_cur)})`" ) - self._send_msg(markdown_msg, reload_able=True, callback_path="update_profit", - query=update.callback_query) + await self._send_msg(markdown_msg, reload_able=True, callback_path="update_profit", + query=update.callback_query) @authorized_only - def _stats(self, update: Update, context: CallbackContext) -> None: + async def _stats(self, update: Update, context: CallbackContext) -> None: """ Handler for /stats Show stats of recent trades @@ -921,7 +922,7 @@ class Telegram(RPCHandler): headers=['Exit Reason', 'Exits', 'Wins', 'Losses'] ) if len(exit_reasons_tabulate) > 25: - self._send_msg(f"```\n{exit_reasons_msg}```", ParseMode.MARKDOWN) + await self._send_msg(f"```\n{exit_reasons_msg}```", ParseMode.MARKDOWN) exit_reasons_msg = '' durations = stats['durations'] @@ -936,10 +937,10 @@ class Telegram(RPCHandler): ) msg = (f"""```\n{exit_reasons_msg}```\n```\n{duration_msg}```""") - self._send_msg(msg, ParseMode.MARKDOWN) + await self._send_msg(msg, ParseMode.MARKDOWN) @authorized_only - def _balance(self, update: Update, context: CallbackContext) -> None: + async def _balance(self, update: Update, context: CallbackContext) -> None: """ Handler for /balance """ full_result = context.args and 'full' in context.args result = self._rpc._rpc_balance(self._config['stake_currency'], @@ -1017,11 +1018,11 @@ class Telegram(RPCHandler): f"\t`{result['stake']}: {total_stake}`{stake_improve}\n" f"\t`{result['symbol']}: {value}`{fiat_val}\n" ) - self._send_msg(output, reload_able=True, callback_path="update_balance", - query=update.callback_query) + await self._send_msg(output, reload_able=True, callback_path="update_balance", + query=update.callback_query) @authorized_only - def _start(self, update: Update, context: CallbackContext) -> None: + async def _start(self, update: Update, context: CallbackContext) -> None: """ Handler for /start. Starts TradeThread @@ -1030,10 +1031,10 @@ class Telegram(RPCHandler): :return: None """ msg = self._rpc._rpc_start() - self._send_msg(f"Status: `{msg['status']}`") + await self._send_msg(f"Status: `{msg['status']}`") @authorized_only - def _stop(self, update: Update, context: CallbackContext) -> None: + async def _stop(self, update: Update, context: CallbackContext) -> None: """ Handler for /stop. Stops TradeThread @@ -1042,10 +1043,10 @@ class Telegram(RPCHandler): :return: None """ msg = self._rpc._rpc_stop() - self._send_msg(f"Status: `{msg['status']}`") + await self._send_msg(f"Status: `{msg['status']}`") @authorized_only - def _reload_config(self, update: Update, context: CallbackContext) -> None: + async def _reload_config(self, update: Update, context: CallbackContext) -> None: """ Handler for /reload_config. Triggers a config file reload @@ -1054,10 +1055,10 @@ class Telegram(RPCHandler): :return: None """ msg = self._rpc._rpc_reload_config() - self._send_msg(f"Status: `{msg['status']}`") + await self._send_msg(f"Status: `{msg['status']}`") @authorized_only - def _stopentry(self, update: Update, context: CallbackContext) -> None: + async def _stopentry(self, update: Update, context: CallbackContext) -> None: """ Handler for /stop_buy. Sets max_open_trades to 0 and gracefully sells all open trades @@ -1066,10 +1067,10 @@ class Telegram(RPCHandler): :return: None """ msg = self._rpc._rpc_stopentry() - self._send_msg(f"Status: `{msg['status']}`") + await self._send_msg(f"Status: `{msg['status']}`") @authorized_only - def _force_exit(self, update: Update, context: CallbackContext) -> None: + async def _force_exit(self, update: Update, context: CallbackContext) -> None: """ Handler for /forceexit . Sells the given trade at current price @@ -1100,51 +1101,51 @@ class Telegram(RPCHandler): buttons_aligned.append([InlineKeyboardButton( text='Cancel', callback_data='force_exit__cancel')]) - self._send_msg(msg="Which trade?", keyboard=buttons_aligned) + await self._send_msg(msg="Which trade?", keyboard=buttons_aligned) - def _force_exit_action(self, trade_id): + async def _force_exit_action(self, trade_id): if trade_id != 'cancel': try: self._rpc._rpc_force_exit(trade_id) except RPCException as e: - self._send_msg(str(e)) + await self._send_msg(str(e)) - def _force_exit_inline(self, update: Update, _: CallbackContext) -> None: + async def _force_exit_inline(self, update: Update, _: CallbackContext) -> None: if update.callback_query: query = update.callback_query if query.data and '__' in query.data: # Input data is "force_exit__" trade_id = query.data.split("__")[1].split(' ')[0] if trade_id == 'cancel': - query.answer() - query.edit_message_text(text="Force exit canceled.") + await query.answer() + await query.edit_message_text(text="Force exit canceled.") return trade: Optional[Trade] = Trade.get_trades(trade_filter=Trade.id == trade_id).first() query.answer() if trade: - query.edit_message_text( + await query.edit_message_text( text=f"Manually exiting Trade #{trade_id}, {trade.pair}") - self._force_exit_action(trade_id) + await self._force_exit_action(trade_id) else: - query.edit_message_text(text=f"Trade {trade_id} not found.") + await query.edit_message_text(text=f"Trade {trade_id} not found.") - def _force_enter_action(self, pair, price: Optional[float], order_side: SignalDirection): + async def _force_enter_action(self, pair, price: Optional[float], order_side: SignalDirection): if pair != 'cancel': try: - self._rpc._rpc_force_entry(pair, price, order_side=order_side) + await self._rpc._rpc_force_entry(pair, price, order_side=order_side) except RPCException as e: logger.exception("Forcebuy error!") - self._send_msg(str(e), ParseMode.HTML) + await self._send_msg(str(e), ParseMode.HTML) - def _force_enter_inline(self, update: Update, _: CallbackContext) -> None: + async def _force_enter_inline(self, update: Update, _: CallbackContext) -> None: if update.callback_query: query = update.callback_query if query.data and '_||_' in query.data: pair, side = query.data.split('_||_') order_side = SignalDirection(side) - query.answer() - query.edit_message_text(text=f"Manually entering {order_side} for {pair}") - self._force_enter_action(pair, None, order_side) + await query.answer() + await query.edit_message_text(text=f"Manually entering {order_side} for {pair}") + await self._force_enter_action(pair, None, order_side) @staticmethod def _layout_inline_keyboard( @@ -1157,7 +1158,7 @@ class Telegram(RPCHandler): return [buttons[i:i + cols] for i in range(0, len(buttons), cols)] @authorized_only - def _force_enter( + async def _force_enter( self, update: Update, context: CallbackContext, order_side: SignalDirection) -> None: """ Handler for /forcelong and `/forceshort @@ -1169,7 +1170,7 @@ class Telegram(RPCHandler): if context.args: pair = context.args[0] price = float(context.args[1]) if len(context.args) > 1 else None - self._force_enter_action(pair, price, order_side) + await self._force_enter_action(pair, price, order_side) else: whitelist = self._rpc._rpc_whitelist()['whitelist'] pair_buttons = [ @@ -1179,12 +1180,12 @@ class Telegram(RPCHandler): buttons_aligned = self._layout_inline_keyboard(pair_buttons) buttons_aligned.append([InlineKeyboardButton(text='Cancel', callback_data='cancel')]) - self._send_msg(msg="Which pair?", - keyboard=buttons_aligned, - query=update.callback_query) + await self._send_msg(msg="Which pair?", + keyboard=buttons_aligned, + query=update.callback_query) @authorized_only - def _trades(self, update: Update, context: CallbackContext) -> None: + async def _trades(self, update: Update, context: CallbackContext) -> None: """ Handler for /trades Returns last n recent trades. @@ -1213,10 +1214,10 @@ class Telegram(RPCHandler): tablefmt='simple') message = (f"{min(trades['trades_count'], nrecent)} recent trades:\n" + (f"
{trades_tab}
" if trades['trades_count'] > 0 else '')) - self._send_msg(message, parse_mode=ParseMode.HTML) + await self._send_msg(message, parse_mode=ParseMode.HTML) @authorized_only - def _delete_trade(self, update: Update, context: CallbackContext) -> None: + async def _delete_trade(self, update: Update, context: CallbackContext) -> None: """ Handler for /delete . Delete the given trade @@ -1228,13 +1229,13 @@ class Telegram(RPCHandler): raise RPCException("Trade-id not set.") trade_id = int(context.args[0]) msg = self._rpc._rpc_delete(trade_id) - self._send_msg( + await self._send_msg( f"`{msg['result_msg']}`\n" 'Please make sure to take care of this asset on the exchange manually.' ) @authorized_only - def _cancel_open_order(self, update: Update, context: CallbackContext) -> None: + async def _cancel_open_order(self, update: Update, context: CallbackContext) -> None: """ Handler for /cancel_open_order . Cancel open order for tradeid @@ -1246,10 +1247,10 @@ class Telegram(RPCHandler): raise RPCException("Trade-id not set.") trade_id = int(context.args[0]) self._rpc._rpc_cancel_open_order(trade_id) - self._send_msg('Open order canceled.') + await self._send_msg('Open order canceled.') @authorized_only - def _performance(self, update: Update, context: CallbackContext) -> None: + async def _performance(self, update: Update, context: CallbackContext) -> None: """ Handler for /performance. Shows a performance statistic from finished trades @@ -1272,12 +1273,12 @@ class Telegram(RPCHandler): else: output += stat_line - self._send_msg(output, parse_mode=ParseMode.HTML, - reload_able=True, callback_path="update_performance", - query=update.callback_query) + await self._send_msg(output, parse_mode=ParseMode.HTML, + reload_able=True, callback_path="update_performance", + query=update.callback_query) @authorized_only - def _enter_tag_performance(self, update: Update, context: CallbackContext) -> None: + async def _enter_tag_performance(self, update: Update, context: CallbackContext) -> None: """ Handler for /buys PAIR . Shows a performance statistic from finished trades @@ -1304,12 +1305,12 @@ class Telegram(RPCHandler): else: output += stat_line - self._send_msg(output, parse_mode=ParseMode.HTML, - reload_able=True, callback_path="update_enter_tag_performance", - query=update.callback_query) + await self._send_msg(output, parse_mode=ParseMode.HTML, + reload_able=True, callback_path="update_enter_tag_performance", + query=update.callback_query) @authorized_only - def _exit_reason_performance(self, update: Update, context: CallbackContext) -> None: + async def _exit_reason_performance(self, update: Update, context: CallbackContext) -> None: """ Handler for /sells. Shows a performance statistic from finished trades @@ -1331,17 +1332,17 @@ class Telegram(RPCHandler): f"({trade['count']})\n") if len(output + stat_line) >= MAX_MESSAGE_LENGTH: - self._send_msg(output, parse_mode=ParseMode.HTML) + await self._send_msg(output, parse_mode=ParseMode.HTML) output = stat_line else: output += stat_line - self._send_msg(output, parse_mode=ParseMode.HTML, - reload_able=True, callback_path="update_exit_reason_performance", - query=update.callback_query) + await self._send_msg(output, parse_mode=ParseMode.HTML, + reload_able=True, callback_path="update_exit_reason_performance", + query=update.callback_query) @authorized_only - def _mix_tag_performance(self, update: Update, context: CallbackContext) -> None: + async def _mix_tag_performance(self, update: Update, context: CallbackContext) -> None: """ Handler for /mix_tags. Shows a performance statistic from finished trades @@ -1363,17 +1364,17 @@ class Telegram(RPCHandler): f"({trade['count']})\n") if len(output + stat_line) >= MAX_MESSAGE_LENGTH: - self._send_msg(output, parse_mode=ParseMode.HTML) + await self._send_msg(output, parse_mode=ParseMode.HTML) output = stat_line else: output += stat_line - self._send_msg(output, parse_mode=ParseMode.HTML, - reload_able=True, callback_path="update_mix_tag_performance", - query=update.callback_query) + await self._send_msg(output, parse_mode=ParseMode.HTML, + reload_able=True, callback_path="update_mix_tag_performance", + query=update.callback_query) @authorized_only - def _count(self, update: Update, context: CallbackContext) -> None: + async def _count(self, update: Update, context: CallbackContext) -> None: """ Handler for /count. Returns the number of trades running @@ -1387,19 +1388,19 @@ class Telegram(RPCHandler): tablefmt='simple') message = f"
{message}
" logger.debug(message) - self._send_msg(message, parse_mode=ParseMode.HTML, - reload_able=True, callback_path="update_count", - query=update.callback_query) + await self._send_msg(message, parse_mode=ParseMode.HTML, + reload_able=True, callback_path="update_count", + query=update.callback_query) @authorized_only - def _locks(self, update: Update, context: CallbackContext) -> None: + async def _locks(self, update: Update, context: CallbackContext) -> None: """ Handler for /locks. Returns the currently active locks """ rpc_locks = self._rpc._rpc_locks() if not rpc_locks['locks']: - self._send_msg('No active locks.', parse_mode=ParseMode.HTML) + await self._send_msg('No active locks.', parse_mode=ParseMode.HTML) for locks in chunks(rpc_locks['locks'], 25): message = tabulate([[ @@ -1411,10 +1412,10 @@ class Telegram(RPCHandler): tablefmt='simple') message = f"
{escape(message)}
" logger.debug(message) - self._send_msg(message, parse_mode=ParseMode.HTML) + await self._send_msg(message, parse_mode=ParseMode.HTML) @authorized_only - def _delete_locks(self, update: Update, context: CallbackContext) -> None: + async def _delete_locks(self, update: Update, context: CallbackContext) -> None: """ Handler for /delete_locks. Returns the currently active locks @@ -1429,10 +1430,10 @@ class Telegram(RPCHandler): pair = arg self._rpc._rpc_delete_lock(lockid=lockid, pair=pair) - self._locks(update, context) + await self._locks(update, context) @authorized_only - def _whitelist(self, update: Update, context: CallbackContext) -> None: + async def _whitelist(self, update: Update, context: CallbackContext) -> None: """ Handler for /whitelist Shows the currently active whitelist @@ -1449,39 +1450,39 @@ class Telegram(RPCHandler): message += f"`{', '.join(whitelist['whitelist'])}`" logger.debug(message) - self._send_msg(message) + await self._send_msg(message) @authorized_only - def _blacklist(self, update: Update, context: CallbackContext) -> None: + async def _blacklist(self, update: Update, context: CallbackContext) -> None: """ Handler for /blacklist Shows the currently active blacklist """ - self.send_blacklist_msg(self._rpc._rpc_blacklist(context.args)) + await self.send_blacklist_msg(self._rpc._rpc_blacklist(context.args)) - def send_blacklist_msg(self, blacklist: Dict): + async def send_blacklist_msg(self, blacklist: Dict): errmsgs = [] for pair, error in blacklist['errors'].items(): errmsgs.append(f"Error: {error['error_msg']}") if errmsgs: - self._send_msg('\n'.join(errmsgs)) + await self._send_msg('\n'.join(errmsgs)) message = f"Blacklist contains {blacklist['length']} pairs\n" message += f"`{', '.join(blacklist['blacklist'])}`" logger.debug(message) - self._send_msg(message) + await self._send_msg(message) @authorized_only - def _blacklist_delete(self, update: Update, context: CallbackContext) -> None: + async def _blacklist_delete(self, update: Update, context: CallbackContext) -> None: """ Handler for /bl_delete Deletes pair(s) from current blacklist """ - self.send_blacklist_msg(self._rpc._rpc_blacklist_delete(context.args or [])) + await self.send_blacklist_msg(self._rpc._rpc_blacklist_delete(context.args or [])) @authorized_only - def _logs(self, update: Update, context: CallbackContext) -> None: + async def _logs(self, update: Update, context: CallbackContext) -> None: """ Handler for /logs Shows the latest logs @@ -1500,17 +1501,17 @@ class Telegram(RPCHandler): escape_markdown(logrec[4], version=2)) if len(msgs + msg) + 10 >= MAX_MESSAGE_LENGTH: # Send message immediately if it would become too long - self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2) + await self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2) msgs = msg + '\n' else: # Append message to messages to send msgs += msg + '\n' if msgs: - self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2) + await self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2) @authorized_only - def _edge(self, update: Update, context: CallbackContext) -> None: + async def _edge(self, update: Update, context: CallbackContext) -> None: """ Handler for /edge Shows information related to Edge @@ -1518,17 +1519,17 @@ class Telegram(RPCHandler): edge_pairs = self._rpc._rpc_edge() if not edge_pairs: message = 'Edge only validated following pairs:' - self._send_msg(message, parse_mode=ParseMode.HTML) + await self._send_msg(message, parse_mode=ParseMode.HTML) for chunk in chunks(edge_pairs, 25): edge_pairs_tab = tabulate(chunk, headers='keys', tablefmt='simple') message = (f'Edge only validated following pairs:\n' f'
{edge_pairs_tab}
') - self._send_msg(message, parse_mode=ParseMode.HTML) + await self._send_msg(message, parse_mode=ParseMode.HTML) @authorized_only - def _help(self, update: Update, context: CallbackContext) -> None: + async def _help(self, update: Update, context: CallbackContext) -> None: """ Handler for /help. Show commands of the bot @@ -1606,17 +1607,17 @@ class Telegram(RPCHandler): "*/version:* `Show version`" ) - self._send_msg(message, parse_mode=ParseMode.MARKDOWN) + await self._send_msg(message, parse_mode=ParseMode.MARKDOWN) @authorized_only - def _health(self, update: Update, context: CallbackContext) -> None: + async def _health(self, update: Update, context: CallbackContext) -> None: """ Handler for /health Shows the last process timestamp """ health = self._rpc.health() message = f"Last process: `{health['last_process_loc']}`" - self._send_msg(message) + await self._send_msg(message) @authorized_only async def _version(self, update: Update, context: CallbackContext) -> None: @@ -1635,7 +1636,7 @@ class Telegram(RPCHandler): await self._send_msg(version_string) @authorized_only - def _show_config(self, update: Update, context: CallbackContext) -> None: + async def _show_config(self, update: Update, context: CallbackContext) -> None: """ Handler for /show_config. Show config information information @@ -1664,7 +1665,7 @@ class Telegram(RPCHandler): else: pa_info = "*Position adjustment:* Off\n" - self._send_msg( + await self._send_msg( f"*Mode:* `{'Dry-run' if val['dry_run'] else 'Live'}`\n" f"*Exchange:* `{val['exchange']}`\n" f"*Market: * `{val['trading_mode']}`\n" @@ -1766,7 +1767,7 @@ class Telegram(RPCHandler): ) @authorized_only - def _changemarketdir(self, update: Update, context: CallbackContext) -> None: + async def _changemarketdir(self, update: Update, context: CallbackContext) -> None: """ Handler for /marketdir. Updates the bot's market_direction @@ -1789,14 +1790,14 @@ class Telegram(RPCHandler): if new_market_dir is not None: self._rpc._update_market_direction(new_market_dir) - self._send_msg("Successfully updated market direction" + await self._send_msg("Successfully updated market direction" f" from *{old_market_dir}* to *{new_market_dir}*.") else: raise RPCException("Invalid market direction provided. \n" "Valid market directions: *long, short, even, none*") elif context.args is not None and len(context.args) == 0: old_market_dir = self._rpc._get_market_direction() - self._send_msg(f"Currently set market direction: *{old_market_dir}*") + await self._send_msg(f"Currently set market direction: *{old_market_dir}*") else: raise RPCException("Invalid usage of command /marketdir. \n" "Usage: */marketdir [short | long | even | none]*") From b1367ac46fbaddccd7d45cd0f4389f4617431146 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Apr 2023 13:29:42 +0200 Subject: [PATCH 59/85] Update decorator typehint --- freqtrade/rpc/telegram.py | 42 ++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index c61e36116..7f3c79194 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -15,7 +15,7 @@ from html import escape from itertools import chain from math import isnan from threading import Thread -from typing import Any, Callable, Dict, List, Optional, Union +from typing import Any, Callable, Coroutine, Dict, List, Optional, Union import arrow from tabulate import tabulate @@ -53,7 +53,7 @@ class TimeunitMappings: default: int -def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]: +def authorized_only(command_handler: Callable[..., Coroutine[Any, Any, None]]): """ Decorator to check if the message comes from the correct chat_id :param command_handler: Telegram CommandHandler @@ -252,20 +252,22 @@ class Telegram(RPCHandler): async def _startup_telegram(self) -> None: await self._app.initialize() await self._app.start() - await self._app.updater.start_polling( - bootstrap_retries=-1, - timeout=20, - # read_latency=60, # Assumed transmission latency - drop_pending_updates=True, - # stop_signals=[], # Necessary as we don't run on the main thread - ) - while True: - await asyncio.sleep(10) - if not self._app.updater.running: - break + if self._app.updater: + await self._app.updater.start_polling( + bootstrap_retries=-1, + timeout=20, + # read_latency=60, # Assumed transmission latency + drop_pending_updates=True, + # stop_signals=[], # Necessary as we don't run on the main thread + ) + while True: + await asyncio.sleep(10) + if not self._app.updater.running: + break async def _cleanup_telegram(self) -> None: - await self._app.updater.stop() + if self._app.updater: + await self._app.updater.stop() await self._app.stop() await self._app.shutdown() @@ -994,7 +996,7 @@ class Telegram(RPCHandler): # Handle overflowing message length if len(output + curr_output) >= MAX_MESSAGE_LENGTH: - self._send_msg(output) + await self._send_msg(output) output = curr_output else: output += curr_output @@ -1088,7 +1090,7 @@ class Telegram(RPCHandler): statlist, _, _ = self._rpc._rpc_status_table( self._config['stake_currency'], fiat_currency) except RPCException: - self._send_msg(msg='No open trade found.') + await self._send_msg(msg='No open trade found.') return trades = [] for trade in statlist: @@ -1121,7 +1123,7 @@ class Telegram(RPCHandler): await query.edit_message_text(text="Force exit canceled.") return trade: Optional[Trade] = Trade.get_trades(trade_filter=Trade.id == trade_id).first() - query.answer() + await query.answer() if trade: await query.edit_message_text( text=f"Manually exiting Trade #{trade_id}, {trade.pair}") @@ -1132,7 +1134,7 @@ class Telegram(RPCHandler): async def _force_enter_action(self, pair, price: Optional[float], order_side: SignalDirection): if pair != 'cancel': try: - await self._rpc._rpc_force_entry(pair, price, order_side=order_side) + self._rpc._rpc_force_entry(pair, price, order_side=order_side) except RPCException as e: logger.exception("Forcebuy error!") await self._send_msg(str(e), ParseMode.HTML) @@ -1268,7 +1270,7 @@ class Telegram(RPCHandler): f"({trade['count']})\n") if len(output + stat_line) >= MAX_MESSAGE_LENGTH: - self._send_msg(output, parse_mode=ParseMode.HTML) + await self._send_msg(output, parse_mode=ParseMode.HTML) output = stat_line else: output += stat_line @@ -1300,7 +1302,7 @@ class Telegram(RPCHandler): f"({trade['count']})\n") if len(output + stat_line) >= MAX_MESSAGE_LENGTH: - self._send_msg(output, parse_mode=ParseMode.HTML) + await self._send_msg(output, parse_mode=ParseMode.HTML) output = stat_line else: output += stat_line From 914d7350faca31f597fdb189d31f0336eb590ee4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Apr 2023 13:41:00 +0200 Subject: [PATCH 60/85] Update mocks in apimanager tests --- tests/rpc/test_rpc_apiserver.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index e045bf487..8123e4689 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -283,7 +283,7 @@ def test_api__init__(default_conf, mocker): "username": "TestUser", "password": "testPass", }}) - mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) + mocker.patch('freqtrade.rpc.telegram.Telegram._init') mocker.patch('freqtrade.rpc.api_server.webserver.ApiServer.start_api', MagicMock()) apiserver = ApiServer(default_conf) apiserver.add_rpc_handler(RPC(get_patched_freqtradebot(mocker, default_conf))) @@ -341,7 +341,7 @@ def test_api_run(default_conf, mocker, caplog): "username": "TestUser", "password": "testPass", }}) - mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) + mocker.patch('freqtrade.rpc.telegram.Telegram._init') server_inst_mock = MagicMock() server_inst_mock.run_in_thread = MagicMock() @@ -419,7 +419,7 @@ def test_api_cleanup(default_conf, mocker, caplog): "username": "TestUser", "password": "testPass", }}) - mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) + mocker.patch('freqtrade.rpc.telegram.Telegram._init') server_mock = MagicMock() server_mock.cleanup = MagicMock() @@ -1877,7 +1877,7 @@ def test_api_ws_send_msg(default_conf, mocker, caplog): "password": _TEST_PASS, "ws_token": _TEST_WS_TOKEN }}) - mocker.patch('freqtrade.rpc.telegram.Updater') + mocker.patch('freqtrade.rpc.telegram.Telegram._init') mocker.patch('freqtrade.rpc.api_server.ApiServer.start_api') apiserver = ApiServer(default_conf) apiserver.add_rpc_handler(RPC(get_patched_freqtradebot(mocker, default_conf))) From fb56889b43f50bc7bb1e8ad55afb4546f935c020 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Apr 2023 14:15:38 +0200 Subject: [PATCH 61/85] Update a few tests ... --- freqtrade/rpc/telegram.py | 2 +- tests/rpc/test_rpc_telegram.py | 201 ++++++++++++++++----------------- 2 files changed, 101 insertions(+), 102 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 7f3c79194..192e2b6d7 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -177,7 +177,7 @@ class Telegram(RPCHandler): self._loop = asyncio.get_running_loop() except RuntimeError: self._loop = asyncio.new_event_loop() - asyncio.set_event_loop(self._loop) + asyncio.set_event_loop(self._loop) self._app = Application.builder().token(self._config['telegram']['token']).build() diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 7510255e9..5b7950e4c 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -8,7 +8,7 @@ from datetime import datetime, timedelta, timezone from functools import reduce from random import choice, randint from string import ascii_uppercase -from unittest.mock import ANY, MagicMock +from unittest.mock import ANY, AsyncMock, MagicMock import arrow import pytest @@ -79,12 +79,13 @@ class DummyCls(Telegram): def get_telegram_testobject(mocker, default_conf, mock=True, ftbot=None): - msg_mock = MagicMock() + msg_mock = AsyncMock() if mock: mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', _init=MagicMock(), - _send_msg=msg_mock + _send_msg=msg_mock, + _start_thread=MagicMock(), ) if not ftbot: ftbot = get_patched_freqtradebot(mocker, default_conf) @@ -95,7 +96,6 @@ def get_telegram_testobject(mocker, default_conf, mock=True, ftbot=None): def test_telegram__init__(default_conf, mocker) -> None: - mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) telegram, _, _ = get_telegram_testobject(mocker, default_conf) @@ -192,7 +192,7 @@ def test_authorized_only_exception(default_conf, mocker, caplog, update) -> None assert log_has('Exception occurred within Telegram module', caplog) -def test_telegram_status(default_conf, update, mocker) -> None: +async def test_telegram_status(default_conf, update, mocker) -> None: default_conf['telegram']['enabled'] = False status_table = MagicMock() @@ -238,18 +238,18 @@ def test_telegram_status(default_conf, update, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) - telegram._status(update=update, context=MagicMock()) + await telegram._status(update=update, context=MagicMock()) assert msg_mock.call_count == 1 context = MagicMock() # /status table context.args = ["table"] - telegram._status(update=update, context=context) + await telegram._status(update=update, context=context) assert status_table.call_count == 1 @pytest.mark.usefixtures("init_persistence") -def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None: +async def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None: default_conf['telegram']['enabled'] = False default_conf['position_adjustment_enable'] = True mocker.patch.multiple( @@ -288,7 +288,7 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None: trade.recalc_trade_from_orders() Trade.commit() - telegram._status(update=update, context=MagicMock()) + await telegram._status(update=update, context=MagicMock()) assert msg_mock.call_count == 4 msg = msg_mock.call_args_list[0][0][0] assert re.search(r'Number of Entries.*2', msg) @@ -300,8 +300,7 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None: @pytest.mark.usefixtures("init_persistence") -def test_telegram_status_closed_trade(default_conf, update, mocker, fee) -> None: - default_conf['telegram']['enabled'] = False +async def test_telegram_status_closed_trade(default_conf, update, mocker, fee) -> None: default_conf['position_adjustment_enable'] = True mocker.patch.multiple( EXMS, @@ -315,7 +314,7 @@ def test_telegram_status_closed_trade(default_conf, update, mocker, fee) -> None trade = Trade.get_trades([Trade.is_open.is_(False)]).first() context = MagicMock() context.args = [str(trade.id)] - telegram._status(update=update, context=context) + await telegram._status(update=update, context=context) assert msg_mock.call_count == 1 msg = msg_mock.call_args_list[0][0][0] assert re.search(r'Close Date:', msg) @@ -749,7 +748,7 @@ def test_telegram_profit_handle( @pytest.mark.parametrize('is_short', [True, False]) -def test_telegram_stats(default_conf, update, ticker, fee, mocker, is_short) -> None: +async def test_telegram_stats(default_conf, update, ticker, fee, mocker, is_short) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch.multiple( EXMS, @@ -759,7 +758,7 @@ def test_telegram_stats(default_conf, update, ticker, fee, mocker, is_short) -> telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) - telegram._stats(update=update, context=MagicMock()) + await telegram._stats(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'No trades yet.' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() @@ -767,7 +766,7 @@ def test_telegram_stats(default_conf, update, ticker, fee, mocker, is_short) -> # Create some test data create_mock_trades(fee, is_short=is_short) - telegram._stats(update=update, context=MagicMock()) + await telegram._stats(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'Exit Reason' in msg_mock.call_args_list[-1][0][0] assert 'ROI' in msg_mock.call_args_list[-1][0][0] @@ -775,7 +774,7 @@ def test_telegram_stats(default_conf, update, ticker, fee, mocker, is_short) -> msg_mock.reset_mock() -def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tickers) -> None: +async def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tickers) -> None: default_conf['dry_run'] = False mocker.patch(f'{EXMS}.get_balances', return_value=rpc_balance) mocker.patch(f'{EXMS}.get_tickers', tickers) @@ -784,10 +783,10 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) - telegram._balance(update=update, context=MagicMock()) + await telegram._balance(update=update, context=MagicMock()) context = MagicMock() context.args = ["full"] - telegram._balance(update=update, context=context) + await telegram._balance(update=update, context=context) result = msg_mock.call_args_list[0][0][0] result_full = msg_mock.call_args_list[1][0][0] assert msg_mock.call_count == 2 @@ -809,7 +808,7 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick assert '*Estimated Value (Bot managed assets only)*:' in result -def test_balance_handle_empty_response(default_conf, update, mocker) -> None: +async def test_balance_handle_empty_response(default_conf, update, mocker) -> None: default_conf['dry_run'] = False mocker.patch(f'{EXMS}.get_balances', return_value={}) @@ -817,26 +816,26 @@ def test_balance_handle_empty_response(default_conf, update, mocker) -> None: patch_get_signal(freqtradebot) freqtradebot.config['dry_run'] = False - telegram._balance(update=update, context=MagicMock()) + await telegram._balance(update=update, context=MagicMock()) result = msg_mock.call_args_list[0][0][0] assert msg_mock.call_count == 1 assert 'Starting capital: `0 BTC' in result -def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None: +async def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None: mocker.patch(f'{EXMS}.get_balances', return_value={}) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) - telegram._balance(update=update, context=MagicMock()) + await telegram._balance(update=update, context=MagicMock()) result = msg_mock.call_args_list[0][0][0] assert msg_mock.call_count == 1 assert "*Warning:* Simulated balances in Dry Mode." in result assert "Starting capital: `1000 BTC`" in result -def test_balance_handle_too_large_response(default_conf, update, mocker) -> None: +async def test_balance_handle_too_large_response(default_conf, update, mocker) -> None: balances = [] for i in range(100): curr = choice(ascii_uppercase) + choice(ascii_uppercase) + choice(ascii_uppercase) @@ -869,7 +868,7 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) - telegram._balance(update=update, context=MagicMock()) + await telegram._balance(update=update, context=MagicMock()) assert msg_mock.call_count > 1 # Test if wrap happens around 4000 - # and each single currency-output is around 120 characters long so we need @@ -878,78 +877,78 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None assert len(msg_mock.call_args_list[0][0][0]) > (4096 - 120) -def test_start_handle(default_conf, update, mocker) -> None: +async def test_start_handle(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.STOPPED assert freqtradebot.state == State.STOPPED - telegram._start(update=update, context=MagicMock()) + await telegram._start(update=update, context=MagicMock()) assert freqtradebot.state == State.RUNNING assert msg_mock.call_count == 1 -def test_start_handle_already_running(default_conf, update, mocker) -> None: +async def test_start_handle_already_running(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.RUNNING assert freqtradebot.state == State.RUNNING - telegram._start(update=update, context=MagicMock()) + await telegram._start(update=update, context=MagicMock()) assert freqtradebot.state == State.RUNNING assert msg_mock.call_count == 1 assert 'already running' in msg_mock.call_args_list[0][0][0] -def test_stop_handle(default_conf, update, mocker) -> None: +async def test_stop_handle(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.RUNNING assert freqtradebot.state == State.RUNNING - telegram._stop(update=update, context=MagicMock()) + await telegram._stop(update=update, context=MagicMock()) assert freqtradebot.state == State.STOPPED assert msg_mock.call_count == 1 assert 'stopping trader' in msg_mock.call_args_list[0][0][0] -def test_stop_handle_already_stopped(default_conf, update, mocker) -> None: +async def test_stop_handle_already_stopped(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.STOPPED assert freqtradebot.state == State.STOPPED - telegram._stop(update=update, context=MagicMock()) + await telegram._stop(update=update, context=MagicMock()) assert freqtradebot.state == State.STOPPED assert msg_mock.call_count == 1 assert 'already stopped' in msg_mock.call_args_list[0][0][0] -def test_stopbuy_handle(default_conf, update, mocker) -> None: +async def test_stopbuy_handle(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) assert freqtradebot.config['max_open_trades'] != 0 - telegram._stopentry(update=update, context=MagicMock()) + await telegram._stopentry(update=update, context=MagicMock()) assert freqtradebot.config['max_open_trades'] == 0 assert msg_mock.call_count == 1 assert 'No more entries will occur from now. Run /reload_config to reset.' \ in msg_mock.call_args_list[0][0][0] -def test_reload_config_handle(default_conf, update, mocker) -> None: +async def test_reload_config_handle(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.RUNNING assert freqtradebot.state == State.RUNNING - telegram._reload_config(update=update, context=MagicMock()) + await telegram._reload_config(update=update, context=MagicMock()) assert freqtradebot.state == State.RELOAD_CONFIG assert msg_mock.call_count == 1 assert 'Reloading config' in msg_mock.call_args_list[0][0][0] -def test_telegram_forceexit_handle(default_conf, update, ticker, fee, +async def test_telegram_forceexit_handle(default_conf, update, ticker, fee, ticker_sell_up, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) msg_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) @@ -980,7 +979,7 @@ def test_telegram_forceexit_handle(default_conf, update, ticker, fee, # /forceexit 1 context = MagicMock() context.args = ["1"] - telegram._force_exit(update=update, context=context) + await telegram._force_exit(update=update, context=context) assert msg_mock.call_count == 4 last_msg = msg_mock.call_args_list[-2][0][0] @@ -1016,8 +1015,8 @@ def test_telegram_forceexit_handle(default_conf, update, ticker, fee, } == last_msg -def test_telegram_force_exit_down_handle(default_conf, update, ticker, fee, - ticker_sell_down, mocker) -> None: +async def test_telegram_force_exit_down_handle(default_conf, update, ticker, fee, + ticker_sell_down, mocker) -> None: mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) msg_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) @@ -1052,7 +1051,7 @@ def test_telegram_force_exit_down_handle(default_conf, update, ticker, fee, # /forceexit 1 context = MagicMock() context.args = ["1"] - telegram._force_exit(update=update, context=context) + await telegram._force_exit(update=update, context=context) assert msg_mock.call_count == 4 @@ -1089,7 +1088,7 @@ def test_telegram_force_exit_down_handle(default_conf, update, ticker, fee, } == last_msg -def test_forceexit_all_handle(default_conf, update, ticker, fee, mocker) -> None: +async def test_forceexit_all_handle(default_conf, update, ticker, fee, mocker) -> None: patch_exchange(mocker) mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) @@ -1115,7 +1114,7 @@ def test_forceexit_all_handle(default_conf, update, ticker, fee, mocker) -> None # /forceexit all context = MagicMock() context.args = ["all"] - telegram._force_exit(update=update, context=context) + await telegram._force_exit(update=update, context=context) # Called for each trade 2 times assert msg_mock.call_count == 8 @@ -1152,7 +1151,7 @@ def test_forceexit_all_handle(default_conf, update, ticker, fee, mocker) -> None } == msg -def test_forceexit_handle_invalid(default_conf, update, mocker) -> None: +async def test_forceexit_handle_invalid(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) @@ -1164,7 +1163,7 @@ def test_forceexit_handle_invalid(default_conf, update, mocker) -> None: # /forceexit 1 context = MagicMock() context.args = ["1"] - telegram._force_exit(update=update, context=context) + await telegram._force_exit(update=update, context=context) assert msg_mock.call_count == 1 assert 'not running' in msg_mock.call_args_list[0][0][0] @@ -1174,12 +1173,12 @@ def test_forceexit_handle_invalid(default_conf, update, mocker) -> None: # /forceexit 123456 context = MagicMock() context.args = ["123456"] - telegram._force_exit(update=update, context=context) + await telegram._force_exit(update=update, context=context) assert msg_mock.call_count == 1 assert 'invalid argument' in msg_mock.call_args_list[0][0][0] -def test_force_exit_no_pair(default_conf, update, ticker, fee, mocker) -> None: +async def test_force_exit_no_pair(default_conf, update, ticker, fee, mocker) -> None: default_conf['max_open_trades'] = 4 mocker.patch.multiple( EXMS, @@ -1195,7 +1194,7 @@ def test_force_exit_no_pair(default_conf, update, ticker, fee, mocker) -> None: # /forceexit context = MagicMock() context.args = [] - telegram._force_exit(update=update, context=context) + await telegram._force_exit(update=update, context=context) # No pair assert msg_mock.call_args_list[0][1]['msg'] == 'No open trade found.' @@ -1204,7 +1203,7 @@ def test_force_exit_no_pair(default_conf, update, ticker, fee, mocker) -> None: msg_mock.reset_mock() # /forceexit - telegram._force_exit(update=update, context=context) + await telegram._force_exit(update=update, context=context) keyboard = msg_mock.call_args_list[0][1]['keyboard'] # 4 pairs + cancel assert reduce(lambda acc, x: acc + len(x), keyboard, 0) == 5 @@ -1214,7 +1213,7 @@ def test_force_exit_no_pair(default_conf, update, ticker, fee, mocker) -> None: update = MagicMock() update.callback_query = MagicMock() update.callback_query.data = keyboard[1][0].callback_data - telegram._force_exit_inline(update, None) + await telegram._force_exit_inline(update, None) assert update.callback_query.answer.call_count == 1 assert update.callback_query.edit_message_text.call_count == 1 assert femock.call_count == 1 @@ -1222,17 +1221,17 @@ def test_force_exit_no_pair(default_conf, update, ticker, fee, mocker) -> None: # Retry exiting - but cancel instead update.callback_query.reset_mock() - telegram._force_exit(update=update, context=context) + await telegram._force_exit(update=update, context=context) # Use cancel button update.callback_query.data = keyboard[-1][0].callback_data - telegram._force_exit_inline(update, None) + await telegram._force_exit_inline(update, None) query = update.callback_query assert query.answer.call_count == 1 assert query.edit_message_text.call_count == 1 assert query.edit_message_text.call_args_list[-1][1]['text'] == "Force exit canceled." -def test_force_enter_handle(default_conf, update, mocker) -> None: +async def test_force_enter_handle(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) fbuy_mock = MagicMock(return_value=None) @@ -1244,7 +1243,7 @@ def test_force_enter_handle(default_conf, update, mocker) -> None: # /forcelong ETH/BTC context = MagicMock() context.args = ["ETH/BTC"] - telegram._force_enter(update=update, context=context, order_side=SignalDirection.LONG) + await telegram._force_enter(update=update, context=context, order_side=SignalDirection.LONG) assert fbuy_mock.call_count == 1 assert fbuy_mock.call_args_list[0][0][0] == 'ETH/BTC' @@ -1257,7 +1256,7 @@ def test_force_enter_handle(default_conf, update, mocker) -> None: # /forcelong ETH/BTC 0.055 context = MagicMock() context.args = ["ETH/BTC", "0.055"] - telegram._force_enter(update=update, context=context, order_side=SignalDirection.LONG) + await telegram._force_enter(update=update, context=context, order_side=SignalDirection.LONG) assert fbuy_mock.call_count == 1 assert fbuy_mock.call_args_list[0][0][0] == 'ETH/BTC' @@ -1265,19 +1264,19 @@ def test_force_enter_handle(default_conf, update, mocker) -> None: assert fbuy_mock.call_args_list[0][0][1] == 0.055 -def test_force_enter_handle_exception(default_conf, update, mocker) -> None: +async def test_force_enter_handle_exception(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) - telegram._force_enter(update=update, context=MagicMock(), order_side=SignalDirection.LONG) + await telegram._force_enter(update=update, context=MagicMock(), order_side=SignalDirection.LONG) assert msg_mock.call_count == 1 assert msg_mock.call_args_list[0][0][0] == 'Force_entry not enabled.' -def test_force_enter_no_pair(default_conf, update, mocker) -> None: +async def test_force_enter_no_pair(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) fbuy_mock = MagicMock(return_value=None) @@ -1289,7 +1288,7 @@ def test_force_enter_no_pair(default_conf, update, mocker) -> None: context = MagicMock() context.args = [] - telegram._force_enter(update=update, context=context, order_side=SignalDirection.LONG) + await telegram._force_enter(update=update, context=context, order_side=SignalDirection.LONG) assert fbuy_mock.call_count == 0 assert msg_mock.call_count == 1 @@ -1301,11 +1300,11 @@ def test_force_enter_no_pair(default_conf, update, mocker) -> None: update = MagicMock() update.callback_query = MagicMock() update.callback_query.data = 'XRP/USDT_||_long' - telegram._force_enter_inline(update, None) + await telegram._force_enter_inline(update, None) assert fbuy_mock.call_count == 1 -def test_telegram_performance_handle(default_conf_usdt, update, ticker, fee, mocker) -> None: +async def test_telegram_performance_handle(default_conf_usdt, update, ticker, fee, mocker) -> None: mocker.patch.multiple( EXMS, @@ -1317,13 +1316,13 @@ def test_telegram_performance_handle(default_conf_usdt, update, ticker, fee, moc # Create some test data create_mock_trades_usdt(fee) - telegram._performance(update=update, context=MagicMock()) + await telegram._performance(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'Performance' in msg_mock.call_args_list[0][0][0] assert 'XRP/USDT\t2.842 USDT (10.00%) (1)' in msg_mock.call_args_list[0][0][0] -def test_telegram_entry_tag_performance_handle( +async def test_telegram_entry_tag_performance_handle( default_conf_usdt, update, ticker, fee, mocker) -> None: mocker.patch.multiple( EXMS, @@ -1336,26 +1335,26 @@ def test_telegram_entry_tag_performance_handle( create_mock_trades_usdt(fee) context = MagicMock() - telegram._enter_tag_performance(update=update, context=context) + await telegram._enter_tag_performance(update=update, context=context) assert msg_mock.call_count == 1 assert 'Entry Tag Performance' in msg_mock.call_args_list[0][0][0] assert 'TEST1\t3.987 USDT (5.00%) (1)' in msg_mock.call_args_list[0][0][0] context.args = ['XRP/USDT'] - telegram._enter_tag_performance(update=update, context=context) + await telegram._enter_tag_performance(update=update, context=context) assert msg_mock.call_count == 2 msg_mock.reset_mock() mocker.patch('freqtrade.rpc.rpc.RPC._rpc_enter_tag_performance', side_effect=RPCException('Error')) - telegram._enter_tag_performance(update=update, context=MagicMock()) + await telegram._enter_tag_performance(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert "Error" in msg_mock.call_args_list[0][0][0] -def test_telegram_exit_reason_performance_handle(default_conf_usdt, update, ticker, fee, - mocker) -> None: +async def test_telegram_exit_reason_performance_handle( + default_conf_usdt, update, ticker, fee, mocker) -> None: mocker.patch.multiple( EXMS, fetch_ticker=ticker, @@ -1367,26 +1366,26 @@ def test_telegram_exit_reason_performance_handle(default_conf_usdt, update, tick create_mock_trades_usdt(fee) context = MagicMock() - telegram._exit_reason_performance(update=update, context=context) + await telegram._exit_reason_performance(update=update, context=context) assert msg_mock.call_count == 1 assert 'Exit Reason Performance' in msg_mock.call_args_list[0][0][0] assert 'roi\t2.842 USDT (10.00%) (1)' in msg_mock.call_args_list[0][0][0] context.args = ['XRP/USDT'] - telegram._exit_reason_performance(update=update, context=context) + await telegram._exit_reason_performance(update=update, context=context) assert msg_mock.call_count == 2 msg_mock.reset_mock() mocker.patch('freqtrade.rpc.rpc.RPC._rpc_exit_reason_performance', side_effect=RPCException('Error')) - telegram._exit_reason_performance(update=update, context=MagicMock()) + await telegram._exit_reason_performance(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert "Error" in msg_mock.call_args_list[0][0][0] -def test_telegram_mix_tag_performance_handle(default_conf_usdt, update, ticker, fee, - mocker) -> None: +async def test_telegram_mix_tag_performance_handle(default_conf_usdt, update, ticker, fee, + mocker) -> None: mocker.patch.multiple( EXMS, fetch_ticker=ticker, @@ -1399,26 +1398,26 @@ def test_telegram_mix_tag_performance_handle(default_conf_usdt, update, ticker, create_mock_trades_usdt(fee) context = MagicMock() - telegram._mix_tag_performance(update=update, context=context) + await telegram._mix_tag_performance(update=update, context=context) assert msg_mock.call_count == 1 assert 'Mix Tag Performance' in msg_mock.call_args_list[0][0][0] assert ('TEST3 roi\t2.842 USDT (10.00%) (1)' in msg_mock.call_args_list[0][0][0]) context.args = ['XRP/USDT'] - telegram._mix_tag_performance(update=update, context=context) + await telegram._mix_tag_performance(update=update, context=context) assert msg_mock.call_count == 2 msg_mock.reset_mock() mocker.patch('freqtrade.rpc.rpc.RPC._rpc_mix_tag_performance', side_effect=RPCException('Error')) - telegram._mix_tag_performance(update=update, context=MagicMock()) + await telegram._mix_tag_performance(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert "Error" in msg_mock.call_args_list[0][0][0] -def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: +async def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: mocker.patch.multiple( EXMS, fetch_ticker=ticker, @@ -1428,7 +1427,7 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: patch_get_signal(freqtradebot) freqtradebot.state = State.STOPPED - telegram._count(update=update, context=MagicMock()) + await telegram._count(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'not running' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() @@ -1437,7 +1436,7 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: # Create some test data freqtradebot.enter_positions() msg_mock.reset_mock() - telegram._count(update=update, context=MagicMock()) + await telegram._count(update=update, context=MagicMock()) msg = ('
  current    max    total stake\n---------  -----  -------------\n'
            '        1      {}          {}
').format( @@ -1447,7 +1446,7 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: assert msg in msg_mock.call_args_list[0][0][0] -def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None: +async def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None: mocker.patch.multiple( EXMS, fetch_ticker=ticker, @@ -1455,7 +1454,7 @@ def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None ) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) - telegram._locks(update=update, context=MagicMock()) + await telegram._locks(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'No active locks.' in msg_mock.call_args_list[0][0][0] @@ -1464,7 +1463,7 @@ def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None PairLocks.lock_pair('ETH/BTC', arrow.utcnow().shift(minutes=4).datetime, 'randreason') PairLocks.lock_pair('XRP/BTC', arrow.utcnow().shift(minutes=20).datetime, 'deadbeef') - telegram._locks(update=update, context=MagicMock()) + await telegram._locks(update=update, context=MagicMock()) assert 'Pair' in msg_mock.call_args_list[0][0][0] assert 'Until' in msg_mock.call_args_list[0][0][0] @@ -1477,7 +1476,7 @@ def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None context = MagicMock() context.args = ['XRP/BTC'] msg_mock.reset_mock() - telegram._delete_locks(update=update, context=context) + await telegram._delete_locks(update=update, context=context) assert 'ETH/BTC' in msg_mock.call_args_list[0][0][0] assert 'randreason' in msg_mock.call_args_list[0][0][0] @@ -1485,11 +1484,11 @@ def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None assert 'deadbeef' not in msg_mock.call_args_list[0][0][0] -def test_whitelist_static(default_conf, update, mocker) -> None: +async def test_whitelist_static(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) - telegram._whitelist(update=update, context=MagicMock()) + await telegram._whitelist(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert ("Using whitelist `['StaticPairList']` with 4 pairs\n" "`ETH/BTC, LTC/BTC, XRP/BTC, NEO/BTC`" in msg_mock.call_args_list[0][0][0]) @@ -1497,33 +1496,33 @@ def test_whitelist_static(default_conf, update, mocker) -> None: context = MagicMock() context.args = ['sorted'] msg_mock.reset_mock() - telegram._whitelist(update=update, context=context) + await telegram._whitelist(update=update, context=context) assert ("Using whitelist `['StaticPairList']` with 4 pairs\n" "`ETH/BTC, LTC/BTC, NEO/BTC, XRP/BTC`" in msg_mock.call_args_list[0][0][0]) context = MagicMock() context.args = ['baseonly'] msg_mock.reset_mock() - telegram._whitelist(update=update, context=context) + await telegram._whitelist(update=update, context=context) assert ("Using whitelist `['StaticPairList']` with 4 pairs\n" "`ETH, LTC, XRP, NEO`" in msg_mock.call_args_list[0][0][0]) context = MagicMock() context.args = ['baseonly', 'sorted'] msg_mock.reset_mock() - telegram._whitelist(update=update, context=context) + await telegram._whitelist(update=update, context=context) assert ("Using whitelist `['StaticPairList']` with 4 pairs\n" "`ETH, LTC, NEO, XRP`" in msg_mock.call_args_list[0][0][0]) -def test_whitelist_dynamic(default_conf, update, mocker) -> None: +async def test_whitelist_dynamic(default_conf, update, mocker) -> None: mocker.patch(f'{EXMS}.exchange_has', return_value=True) default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 4 }] telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) - telegram._whitelist(update=update, context=MagicMock()) + await telegram._whitelist(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert ("Using whitelist `['VolumePairList']` with 4 pairs\n" "`ETH/BTC, LTC/BTC, XRP/BTC, NEO/BTC`" in msg_mock.call_args_list[0][0][0]) @@ -1531,30 +1530,30 @@ def test_whitelist_dynamic(default_conf, update, mocker) -> None: context = MagicMock() context.args = ['sorted'] msg_mock.reset_mock() - telegram._whitelist(update=update, context=context) + await telegram._whitelist(update=update, context=context) assert ("Using whitelist `['VolumePairList']` with 4 pairs\n" "`ETH/BTC, LTC/BTC, NEO/BTC, XRP/BTC`" in msg_mock.call_args_list[0][0][0]) context = MagicMock() context.args = ['baseonly'] msg_mock.reset_mock() - telegram._whitelist(update=update, context=context) + await telegram._whitelist(update=update, context=context) assert ("Using whitelist `['VolumePairList']` with 4 pairs\n" "`ETH, LTC, XRP, NEO`" in msg_mock.call_args_list[0][0][0]) context = MagicMock() context.args = ['baseonly', 'sorted'] msg_mock.reset_mock() - telegram._whitelist(update=update, context=context) + await telegram._whitelist(update=update, context=context) assert ("Using whitelist `['VolumePairList']` with 4 pairs\n" "`ETH, LTC, NEO, XRP`" in msg_mock.call_args_list[0][0][0]) -def test_blacklist_static(default_conf, update, mocker) -> None: +async def test_blacklist_static(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) - telegram._blacklist(update=update, context=MagicMock()) + await telegram._blacklist(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert ("Blacklist contains 2 pairs\n`DOGE/BTC, HOT/BTC`" in msg_mock.call_args_list[0][0][0]) @@ -1564,7 +1563,7 @@ def test_blacklist_static(default_conf, update, mocker) -> None: # /blacklist ETH/BTC context = MagicMock() context.args = ["ETH/BTC"] - telegram._blacklist(update=update, context=context) + await telegram._blacklist(update=update, context=context) assert msg_mock.call_count == 1 assert ("Blacklist contains 3 pairs\n`DOGE/BTC, HOT/BTC, ETH/BTC`" in msg_mock.call_args_list[0][0][0]) @@ -1573,7 +1572,7 @@ def test_blacklist_static(default_conf, update, mocker) -> None: msg_mock.reset_mock() context = MagicMock() context.args = ["XRP/.*"] - telegram._blacklist(update=update, context=context) + await telegram._blacklist(update=update, context=context) assert msg_mock.call_count == 1 assert ("Blacklist contains 4 pairs\n`DOGE/BTC, HOT/BTC, ETH/BTC, XRP/.*`" @@ -1582,7 +1581,7 @@ def test_blacklist_static(default_conf, update, mocker) -> None: msg_mock.reset_mock() context.args = ["DOGE/BTC"] - telegram._blacklist_delete(update=update, context=context) + await telegram._blacklist_delete(update=update, context=context) assert msg_mock.call_count == 1 assert ("Blacklist contains 3 pairs\n`HOT/BTC, ETH/BTC, XRP/.*`" in msg_mock.call_args_list[0][0][0]) @@ -2374,7 +2373,7 @@ def test__send_msg_network_error(default_conf, mocker, caplog) -> None: assert log_has('Telegram NetworkError: Oh snap! Trying one more time.', caplog) -def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: +async def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) bot = MagicMock() bot.send_message = MagicMock() @@ -2400,7 +2399,7 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: # no keyboard in config -> default keyboard freqtradebot.config['telegram']['enabled'] = True telegram = init_telegram(freqtradebot) - telegram._send_msg('test') + await telegram._send_msg('test') used_keyboard = bot.send_message.call_args[1]['reply_markup'] assert used_keyboard == default_keyboard @@ -2417,7 +2416,7 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: freqtradebot.config['telegram']['enabled'] = True freqtradebot.config['telegram']['keyboard'] = custom_keys_list telegram = init_telegram(freqtradebot) - telegram._send_msg('test') + await telegram._send_msg('test') used_keyboard = bot.send_message.call_args[1]['reply_markup'] assert used_keyboard == custom_keyboard assert log_has("using custom keyboard from config.json: " From c475c818414f03a06898cc2699165a9206d24f9f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Apr 2023 15:15:19 +0200 Subject: [PATCH 62/85] Update several tests to async behavior --- tests/rpc/test_rpc_telegram.py | 143 +++++++++++++++++---------------- 1 file changed, 72 insertions(+), 71 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 5b7950e4c..6ca028c1c 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -321,7 +321,7 @@ async def test_telegram_status_closed_trade(default_conf, update, mocker, fee) - assert re.search(r'Close Profit:', msg) -def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: +async def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: default_conf['max_open_trades'] = 3 mocker.patch.multiple( EXMS, @@ -341,13 +341,13 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: freqtradebot.state = State.STOPPED # Status is also enabled when stopped - telegram._status(update=update, context=MagicMock()) + await telegram._status(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'no active trade' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() freqtradebot.state = State.RUNNING - telegram._status(update=update, context=MagicMock()) + await telegram._status(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'no active trade' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() @@ -355,7 +355,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: # Create some test data freqtradebot.enter_positions() # Trigger status while we have a fulfilled order for the open trade - telegram._status(update=update, context=MagicMock()) + await telegram._status(update=update, context=MagicMock()) # close_rate should not be included in the message as the trade is not closed # and no line should be empty @@ -372,7 +372,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: context = MagicMock() context.args = ["2", "3"] - telegram._status(update=update, context=context) + await telegram._status(update=update, context=context) lines = msg_mock.call_args_list[0][0][0].split('\n') assert '' not in lines[:-1] @@ -387,7 +387,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: msg_mock.reset_mock() context = MagicMock() context.args = ["2"] - telegram._status(update=update, context=context) + await telegram._status(update=update, context=context) assert msg_mock.call_count == 2 @@ -399,7 +399,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: assert 'Trade ID:* `2` - continued' in msg2 -def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: +async def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: mocker.patch.multiple( EXMS, fetch_ticker=ticker, @@ -414,13 +414,13 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: freqtradebot.state = State.STOPPED # Status table is also enabled when stopped - telegram._status_table(update=update, context=MagicMock()) + await telegram._status_table(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'no active trade' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() freqtradebot.state = State.RUNNING - telegram._status_table(update=update, context=MagicMock()) + await telegram._status_table(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'no active trade' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() @@ -428,7 +428,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: # Create some test data freqtradebot.enter_positions() - telegram._status_table(update=update, context=MagicMock()) + await telegram._status_table(update=update, context=MagicMock()) text = re.sub('', '', msg_mock.call_args_list[-1][0][0]) line = text.split("\n") @@ -440,7 +440,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: assert msg_mock.call_count == 1 -def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None: +async def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None: mocker.patch( 'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=1.1 @@ -462,7 +462,7 @@ def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machi # /daily 2 context = MagicMock() context.args = ["2"] - telegram._daily(update=update, context=context) + await telegram._daily(update=update, context=context) assert msg_mock.call_count == 1 assert "Daily Profit over the last 2 days:" in msg_mock.call_args_list[0][0][0] assert 'Day ' in msg_mock.call_args_list[0][0][0] @@ -476,7 +476,7 @@ def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machi # Reset msg_mock msg_mock.reset_mock() context.args = [] - telegram._daily(update=update, context=context) + await telegram._daily(update=update, context=context) assert msg_mock.call_count == 1 assert "Daily Profit over the last 7 days:" in msg_mock.call_args_list[0][0][0] assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] @@ -493,13 +493,13 @@ def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machi # /daily 1 context = MagicMock() context.args = ["1"] - telegram._daily(update=update, context=context) + await telegram._daily(update=update, context=context) assert ' 6.83 USDT' in msg_mock.call_args_list[0][0][0] assert ' 7.51 USD' in msg_mock.call_args_list[0][0][0] assert '(2)' in msg_mock.call_args_list[0][0][0] -def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: +async def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: mocker.patch.multiple( EXMS, fetch_ticker=ticker @@ -514,7 +514,7 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: # /daily -2 context = MagicMock() context.args = ["-2"] - telegram._daily(update=update, context=context) + await telegram._daily(update=update, context=context) assert msg_mock.call_count == 1 assert 'must be an integer greater than 0' in msg_mock.call_args_list[0][0][0] @@ -524,11 +524,11 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: # /daily today context = MagicMock() context.args = ["today"] - telegram._daily(update=update, context=context) + await telegram._daily(update=update, context=context) assert 'Daily Profit over the last 7 days:' in msg_mock.call_args_list[0][0][0] -def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None: +async def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None: default_conf_usdt['max_open_trades'] = 1 mocker.patch( 'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', @@ -549,7 +549,7 @@ def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mach # /weekly 2 context = MagicMock() context.args = ["2"] - telegram._weekly(update=update, context=context) + await telegram._weekly(update=update, context=context) assert msg_mock.call_count == 1 assert "Weekly Profit over the last 2 weeks (starting from Monday):" \ in msg_mock.call_args_list[0][0][0] @@ -565,7 +565,7 @@ def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mach # Reset msg_mock msg_mock.reset_mock() context.args = [] - telegram._weekly(update=update, context=context) + await telegram._weekly(update=update, context=context) assert msg_mock.call_count == 1 assert "Weekly Profit over the last 8 weeks (starting from Monday):" \ in msg_mock.call_args_list[0][0][0] @@ -581,7 +581,7 @@ def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mach # /weekly -3 context = MagicMock() context.args = ["-3"] - telegram._weekly(update=update, context=context) + await telegram._weekly(update=update, context=context) assert msg_mock.call_count == 1 assert 'must be an integer greater than 0' in msg_mock.call_args_list[0][0][0] @@ -591,14 +591,14 @@ def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mach # /weekly this week context = MagicMock() context.args = ["this week"] - telegram._weekly(update=update, context=context) + await telegram._weekly(update=update, context=context) assert ( 'Weekly Profit over the last 8 weeks (starting from Monday):' in msg_mock.call_args_list[0][0][0] ) -def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None: +async def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None: default_conf_usdt['max_open_trades'] = 1 mocker.patch( 'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', @@ -619,7 +619,7 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac # /monthly 2 context = MagicMock() context.args = ["2"] - telegram._monthly(update=update, context=context) + await telegram._monthly(update=update, context=context) assert msg_mock.call_count == 1 assert 'Monthly Profit over the last 2 months:' in msg_mock.call_args_list[0][0][0] assert 'Month ' in msg_mock.call_args_list[0][0][0] @@ -634,7 +634,7 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac # Reset msg_mock msg_mock.reset_mock() context.args = [] - telegram._monthly(update=update, context=context) + await telegram._monthly(update=update, context=context) assert msg_mock.call_count == 1 # Default to 6 months assert 'Monthly Profit over the last 6 months:' in msg_mock.call_args_list[0][0][0] @@ -651,7 +651,7 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac # /monthly 12 context = MagicMock() context.args = ["12"] - telegram._monthly(update=update, context=context) + await telegram._monthly(update=update, context=context) assert msg_mock.call_count == 1 assert 'Monthly Profit over the last 12 months:' in msg_mock.call_args_list[0][0][0] assert ' 2.74 USDT' in msg_mock.call_args_list[0][0][0] @@ -668,7 +668,7 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac # /monthly -3 context = MagicMock() context.args = ["-3"] - telegram._monthly(update=update, context=context) + await telegram._monthly(update=update, context=context) assert msg_mock.call_count == 1 assert 'must be an integer greater than 0' in msg_mock.call_args_list[0][0][0] @@ -678,11 +678,11 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac # /monthly february context = MagicMock() context.args = ["february"] - telegram._monthly(update=update, context=context) + await telegram._monthly(update=update, context=context) assert 'Monthly Profit over the last 6 months:' in msg_mock.call_args_list[0][0][0] -def test_telegram_profit_handle( +async def test_telegram_profit_handle( default_conf_usdt, update, ticker_usdt, ticker_sell_up, fee, limit_sell_order_usdt, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=1.1) @@ -695,7 +695,7 @@ def test_telegram_profit_handle( telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt) patch_get_signal(freqtradebot) - telegram._profit(update=update, context=MagicMock()) + await telegram._profit(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'No trades yet.' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() @@ -707,7 +707,7 @@ def test_telegram_profit_handle( context = MagicMock() # Test with invalid 2nd argument (should silently pass) context.args = ["aaa"] - telegram._profit(update=update, context=context) + await telegram._profit(update=update, context=context) assert msg_mock.call_count == 1 assert 'No closed trade' in msg_mock.call_args_list[-1][0][0] assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0] @@ -730,7 +730,7 @@ def test_telegram_profit_handle( Trade.commit() context.args = [3] - telegram._profit(update=update, context=context) + await telegram._profit(update=update, context=context) assert msg_mock.call_count == 1 assert '*ROI:* Closed trades' in msg_mock.call_args_list[-1][0][0] assert ('∙ `5.685 USDT (9.45%) (0.57 \N{GREEK CAPITAL LETTER SIGMA}%)`' @@ -949,7 +949,7 @@ async def test_reload_config_handle(default_conf, update, mocker) -> None: async def test_telegram_forceexit_handle(default_conf, update, ticker, fee, - ticker_sell_up, mocker) -> None: + ticker_sell_up, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) msg_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) @@ -1587,7 +1587,7 @@ async def test_blacklist_static(default_conf, update, mocker) -> None: in msg_mock.call_args_list[0][0][0]) -def test_telegram_logs(default_conf, update, mocker) -> None: +async def test_telegram_logs(default_conf, update, mocker) -> None: mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', _init=MagicMock(), @@ -1598,13 +1598,13 @@ def test_telegram_logs(default_conf, update, mocker) -> None: context = MagicMock() context.args = [] - telegram._logs(update=update, context=context) + await telegram._logs(update=update, context=context) assert msg_mock.call_count == 1 assert "freqtrade\\.rpc\\.telegram" in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() context.args = ["1"] - telegram._logs(update=update, context=context) + await telegram._logs(update=update, context=context) assert msg_mock.call_count == 1 msg_mock.reset_mock() @@ -1612,22 +1612,22 @@ def test_telegram_logs(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.MAX_MESSAGE_LENGTH', 200) context = MagicMock() context.args = [] - telegram._logs(update=update, context=context) + await telegram._logs(update=update, context=context) # Called at least 2 times. Exact times will change with unrelated changes to setup messages # Therefore we don't test for this explicitly. assert msg_mock.call_count >= 2 -def test_edge_disabled(default_conf, update, mocker) -> None: +async def test_edge_disabled(default_conf, update, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) - telegram._edge(update=update, context=MagicMock()) + await telegram._edge(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert "Edge is not enabled." in msg_mock.call_args_list[0][0][0] -def test_edge_enabled(edge_conf, update, mocker) -> None: +async def test_edge_enabled(edge_conf, update, mocker) -> None: mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock( return_value={ 'E/F': PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60), @@ -1636,7 +1636,7 @@ def test_edge_enabled(edge_conf, update, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, edge_conf) - telegram._edge(update=update, context=MagicMock()) + await telegram._edge(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'Edge only validated following pairs:\n
' in msg_mock.call_args_list[0][0][0]
     assert 'Pair      Winrate    Expectancy    Stoploss' in msg_mock.call_args_list[0][0][0]
@@ -1645,7 +1645,7 @@ def test_edge_enabled(edge_conf, update, mocker) -> None:
 
     mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock(
         return_value={}))
-    telegram._edge(update=update, context=MagicMock())
+    await telegram._edge(update=update, context=MagicMock())
     assert msg_mock.call_count == 1
     assert 'Edge only validated following pairs:' in msg_mock.call_args_list[0][0][0]
     assert 'Winrate' not in msg_mock.call_args_list[0][0][0]
@@ -1654,20 +1654,20 @@ def test_edge_enabled(edge_conf, update, mocker) -> None:
 @pytest.mark.parametrize('is_short,regex_pattern',
                          [(True, r"just now[ ]*XRP\/BTC \(#3\)  -1.00% \("),
                           (False, r"just now[ ]*XRP\/BTC \(#3\)  1.00% \(")])
-def test_telegram_trades(mocker, update, default_conf, fee, is_short, regex_pattern):
+async def test_telegram_trades(mocker, update, default_conf, fee, is_short, regex_pattern):
 
     telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
 
     context = MagicMock()
     context.args = []
 
-    telegram._trades(update=update, context=context)
+    await telegram._trades(update=update, context=context)
     assert "0 recent trades:" in msg_mock.call_args_list[0][0][0]
     assert "
" not in msg_mock.call_args_list[0][0][0]
     msg_mock.reset_mock()
 
     context.args = ['hello']
-    telegram._trades(update=update, context=context)
+    await telegram._trades(update=update, context=context)
     assert "0 recent trades:" in msg_mock.call_args_list[0][0][0]
     assert "
" not in msg_mock.call_args_list[0][0][0]
     msg_mock.reset_mock()
@@ -1676,7 +1676,7 @@ def test_telegram_trades(mocker, update, default_conf, fee, is_short, regex_patt
 
     context = MagicMock()
     context.args = [5]
-    telegram._trades(update=update, context=context)
+    await telegram._trades(update=update, context=context)
     msg_mock.call_count == 1
     assert "2 recent trades:" in msg_mock.call_args_list[0][0][0]
     assert "Profit (" in msg_mock.call_args_list[0][0][0]
@@ -1686,13 +1686,13 @@ def test_telegram_trades(mocker, update, default_conf, fee, is_short, regex_patt
 
 
 @pytest.mark.parametrize('is_short', [True, False])
-def test_telegram_delete_trade(mocker, update, default_conf, fee, is_short):
+async def test_telegram_delete_trade(mocker, update, default_conf, fee, is_short):
 
     telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
     context = MagicMock()
     context.args = []
 
-    telegram._delete_trade(update=update, context=context)
+    await telegram._delete_trade(update=update, context=context)
     assert "Trade-id not set." in msg_mock.call_args_list[0][0][0]
 
     msg_mock.reset_mock()
@@ -1700,14 +1700,14 @@ def test_telegram_delete_trade(mocker, update, default_conf, fee, is_short):
 
     context = MagicMock()
     context.args = [1]
-    telegram._delete_trade(update=update, context=context)
+    await telegram._delete_trade(update=update, context=context)
     msg_mock.call_count == 1
     assert "Deleted trade 1." in msg_mock.call_args_list[0][0][0]
     assert "Please make sure to take care of this asset" in msg_mock.call_args_list[0][0][0]
 
 
 @pytest.mark.parametrize('is_short', [True, False])
-def test_telegram_delete_open_order(mocker, update, default_conf, fee, is_short, ticker):
+async def test_telegram_delete_open_order(mocker, update, default_conf, fee, is_short, ticker):
 
     mocker.patch.multiple(
         EXMS,
@@ -1717,7 +1717,7 @@ def test_telegram_delete_open_order(mocker, update, default_conf, fee, is_short,
     context = MagicMock()
     context.args = []
 
-    telegram._cancel_open_order(update=update, context=context)
+    await telegram._cancel_open_order(update=update, context=context)
     assert "Trade-id not set." in msg_mock.call_args_list[0][0][0]
 
     msg_mock.reset_mock()
@@ -1725,7 +1725,7 @@ def test_telegram_delete_open_order(mocker, update, default_conf, fee, is_short,
 
     context = MagicMock()
     context.args = [5]
-    telegram._cancel_open_order(update=update, context=context)
+    await telegram._cancel_open_order(update=update, context=context)
     assert "No open order for trade_id" in msg_mock.call_args_list[0][0][0]
 
     msg_mock.reset_mock()
@@ -1734,43 +1734,43 @@ def test_telegram_delete_open_order(mocker, update, default_conf, fee, is_short,
     mocker.patch(f'{EXMS}.fetch_order', return_value=trade.orders[-1].to_ccxt_object())
     context = MagicMock()
     context.args = [6]
-    telegram._cancel_open_order(update=update, context=context)
+    await telegram._cancel_open_order(update=update, context=context)
     assert msg_mock.call_count == 1
     assert "Open order canceled." in msg_mock.call_args_list[0][0][0]
 
 
-def test_help_handle(default_conf, update, mocker) -> None:
+async def test_help_handle(default_conf, update, mocker) -> None:
     telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
 
-    telegram._help(update=update, context=MagicMock())
+    await telegram._help(update=update, context=MagicMock())
     assert msg_mock.call_count == 1
     assert '*/help:* `This help message`' in msg_mock.call_args_list[0][0][0]
 
 
-def test_version_handle(default_conf, update, mocker) -> None:
+async def test_version_handle(default_conf, update, mocker) -> None:
 
     telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
 
-    telegram._version(update=update, context=MagicMock())
+    await telegram._version(update=update, context=MagicMock())
     assert msg_mock.call_count == 1
     assert f'*Version:* `{__version__}`' in msg_mock.call_args_list[0][0][0]
 
     msg_mock.reset_mock()
     freqtradebot.strategy.version = lambda: '1.1.1'
 
-    telegram._version(update=update, context=MagicMock())
+    await telegram._version(update=update, context=MagicMock())
     assert msg_mock.call_count == 1
     assert f'*Version:* `{__version__}`' in msg_mock.call_args_list[0][0][0]
     assert '*Strategy version: * `1.1.1`' in msg_mock.call_args_list[0][0][0]
 
 
-def test_show_config_handle(default_conf, update, mocker) -> None:
+async def test_show_config_handle(default_conf, update, mocker) -> None:
 
     default_conf['runmode'] = RunMode.DRY_RUN
 
     telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
 
-    telegram._show_config(update=update, context=MagicMock())
+    await telegram._show_config(update=update, context=MagicMock())
     assert msg_mock.call_count == 1
     assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0]
     assert '*Exchange:* `binance`' in msg_mock.call_args_list[0][0][0]
@@ -1779,7 +1779,7 @@ def test_show_config_handle(default_conf, update, mocker) -> None:
 
     msg_mock.reset_mock()
     freqtradebot.config['trailing_stop'] = True
-    telegram._show_config(update=update, context=MagicMock())
+    await telegram._show_config(update=update, context=MagicMock())
     assert msg_mock.call_count == 1
     assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0]
     assert '*Exchange:* `binance`' in msg_mock.call_args_list[0][0][0]
@@ -2323,7 +2323,7 @@ def test__sell_emoji(default_conf, mocker, msg, expected):
     assert telegram._get_sell_emoji(msg) == expected
 
 
-def test_telegram__send_msg(default_conf, mocker, caplog) -> None:
+async def test_telegram__send_msg(default_conf, mocker, caplog) -> None:
     mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
     bot = MagicMock()
     telegram, _, _ = get_telegram_testobject(mocker, default_conf, mock=False)
@@ -2331,33 +2331,33 @@ def test_telegram__send_msg(default_conf, mocker, caplog) -> None:
     telegram._updater.bot = bot
 
     telegram._config['telegram']['enabled'] = True
-    telegram._send_msg('test')
+    await telegram._send_msg('test')
     assert len(bot.method_calls) == 1
 
     # Test update
     query = MagicMock()
-    telegram._send_msg('test', callback_path="DeadBeef", query=query, reload_able=True)
+    await telegram._send_msg('test', callback_path="DeadBeef", query=query, reload_able=True)
     edit_message_text = telegram._updater.bot.edit_message_text
     assert edit_message_text.call_count == 1
     assert "Updated: " in edit_message_text.call_args_list[0][1]['text']
 
     telegram._updater.bot.edit_message_text = MagicMock(side_effect=BadRequest("not modified"))
-    telegram._send_msg('test', callback_path="DeadBeef", query=query)
+    await telegram._send_msg('test', callback_path="DeadBeef", query=query)
     assert telegram._updater.bot.edit_message_text.call_count == 1
     assert not log_has_re(r"TelegramError: .*", caplog)
 
     telegram._updater.bot.edit_message_text = MagicMock(side_effect=BadRequest(""))
-    telegram._send_msg('test2', callback_path="DeadBeef", query=query)
+    await telegram._send_msg('test2', callback_path="DeadBeef", query=query)
     assert telegram._updater.bot.edit_message_text.call_count == 1
     assert log_has_re(r"TelegramError: .*", caplog)
 
     telegram._updater.bot.edit_message_text = MagicMock(side_effect=TelegramError("DeadBEEF"))
-    telegram._send_msg('test3', callback_path="DeadBeef", query=query)
+    await telegram._send_msg('test3', callback_path="DeadBeef", query=query)
 
     assert log_has_re(r"TelegramError: DeadBEEF! Giving up.*", caplog)
 
 
-def test__send_msg_network_error(default_conf, mocker, caplog) -> None:
+async def test__send_msg_network_error(default_conf, mocker, caplog) -> None:
     mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
     bot = MagicMock()
     bot.send_message = MagicMock(side_effect=NetworkError('Oh snap'))
@@ -2366,7 +2366,7 @@ def test__send_msg_network_error(default_conf, mocker, caplog) -> None:
     telegram._updater.bot = bot
 
     telegram._config['telegram']['enabled'] = True
-    telegram._send_msg('test')
+    await telegram._send_msg('test')
 
     # Bot should've tried to send it twice
     assert len(bot.method_calls) == 2
@@ -2424,13 +2424,14 @@ async def test__send_msg_keyboard(default_conf, mocker, caplog) -> None:
                    "'/start', '/reload_config', '/help']]", caplog)
 
 
-def test_change_market_direction(default_conf, mocker, update) -> None:
+async def test_change_market_direction(default_conf, mocker, update) -> None:
     telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
     assert telegram._rpc._freqtrade.strategy.market_direction == MarketDirection.NONE
     context = MagicMock()
     context.args = ["long"]
-    telegram._changemarketdir(update, context)
+    await telegram._changemarketdir(update, context)
     assert telegram._rpc._freqtrade.strategy.market_direction == MarketDirection.LONG
     context = MagicMock()
     context.args = ["invalid"]
+    await telegram._changemarketdir(update, context)
     assert telegram._rpc._freqtrade.strategy.market_direction == MarketDirection.LONG

From 678c9ae67ffc2c69fc5000982ccb9ffb0ff8cf3d Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sat, 15 Apr 2023 15:18:57 +0200
Subject: [PATCH 63/85] Fix some more async telegram tests

---
 freqtrade/rpc/telegram.py      | 2 +-
 tests/rpc/test_rpc_telegram.py | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index 192e2b6d7..babe7d582 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -1083,7 +1083,7 @@ class Telegram(RPCHandler):
 
         if context.args:
             trade_id = context.args[0]
-            self._force_exit_action(trade_id)
+            await self._force_exit_action(trade_id)
         else:
             fiat_currency = self._config.get('fiat_display_currency', '')
             try:
diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py
index 6ca028c1c..f17dfa190 100644
--- a/tests/rpc/test_rpc_telegram.py
+++ b/tests/rpc/test_rpc_telegram.py
@@ -1211,7 +1211,7 @@ async def test_force_exit_no_pair(default_conf, update, ticker, fee, mocker) ->
 
     assert keyboard[1][0].callback_data == 'force_exit__2 '
     update = MagicMock()
-    update.callback_query = MagicMock()
+    update.callback_query = AsyncMock()
     update.callback_query.data = keyboard[1][0].callback_data
     await telegram._force_exit_inline(update, None)
     assert update.callback_query.answer.call_count == 1
@@ -1298,7 +1298,7 @@ async def test_force_enter_no_pair(default_conf, update, mocker) -> None:
     # One additional button - cancel
     assert reduce(lambda acc, x: acc + len(x), keyboard, 0) == 5
     update = MagicMock()
-    update.callback_query = MagicMock()
+    update.callback_query = AsyncMock()
     update.callback_query.data = 'XRP/USDT_||_long'
     await telegram._force_enter_inline(update, None)
     assert fbuy_mock.call_count == 1

From 4177afdf8be4f2de5043cbfc934a8e3ed45e28a3 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sat, 15 Apr 2023 15:43:53 +0200
Subject: [PATCH 64/85] More async test updates

---
 tests/rpc/test_rpc_telegram.py | 19 ++++++++++---------
 1 file changed, 10 insertions(+), 9 deletions(-)

diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py
index f17dfa190..39895abaf 100644
--- a/tests/rpc/test_rpc_telegram.py
+++ b/tests/rpc/test_rpc_telegram.py
@@ -64,14 +64,14 @@ class DummyCls(Telegram):
         pass
 
     @authorized_only
-    def dummy_handler(self, *args, **kwargs) -> None:
+    async def dummy_handler(self, *args, **kwargs) -> None:
         """
         Fake method that only change the state of the object
         """
         self.state['called'] = True
 
     @authorized_only
-    def dummy_exception(self, *args, **kwargs) -> None:
+    async def dummy_exception(self, *args, **kwargs) -> None:
         """
         Fake method that throw an exception
         """
@@ -91,6 +91,7 @@ def get_telegram_testobject(mocker, default_conf, mock=True, ftbot=None):
         ftbot = get_patched_freqtradebot(mocker, default_conf)
     rpc = RPC(ftbot)
     telegram = Telegram(rpc, default_conf)
+    telegram._loop = MagicMock()
 
     return telegram, ftbot, msg_mock
 
@@ -139,7 +140,7 @@ def test_cleanup(default_conf, mocker, ) -> None:
     assert telegram._updater.stop.call_count == 1
 
 
-def test_authorized_only(default_conf, mocker, caplog, update) -> None:
+async def test_authorized_only(default_conf, mocker, caplog, update) -> None:
     patch_exchange(mocker)
     caplog.set_level(logging.DEBUG)
     default_conf['telegram']['enabled'] = False
@@ -148,14 +149,14 @@ def test_authorized_only(default_conf, mocker, caplog, update) -> None:
     dummy = DummyCls(rpc, default_conf)
 
     patch_get_signal(bot)
-    dummy.dummy_handler(update=update, context=MagicMock())
+    await dummy.dummy_handler(update=update, context=MagicMock())
     assert dummy.state['called'] is True
     assert log_has('Executing handler: dummy_handler for chat_id: 0', caplog)
     assert not log_has('Rejected unauthorized message from: 0', caplog)
     assert not log_has('Exception occurred within Telegram module', caplog)
 
 
-def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None:
+async def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None:
     patch_exchange(mocker)
     caplog.set_level(logging.DEBUG)
     chat = Chat(0xdeadbeef, 0)
@@ -168,14 +169,14 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None:
     dummy = DummyCls(rpc, default_conf)
 
     patch_get_signal(bot)
-    dummy.dummy_handler(update=update, context=MagicMock())
+    await dummy.dummy_handler(update=update, context=MagicMock())
     assert dummy.state['called'] is False
     assert not log_has('Executing handler: dummy_handler for chat_id: 3735928559', caplog)
     assert log_has('Rejected unauthorized message from: 3735928559', caplog)
     assert not log_has('Exception occurred within Telegram module', caplog)
 
 
-def test_authorized_only_exception(default_conf, mocker, caplog, update) -> None:
+async def test_authorized_only_exception(default_conf, mocker, caplog, update) -> None:
     patch_exchange(mocker)
 
     default_conf['telegram']['enabled'] = False
@@ -185,7 +186,7 @@ def test_authorized_only_exception(default_conf, mocker, caplog, update) -> None
     dummy = DummyCls(rpc, default_conf)
     patch_get_signal(bot)
 
-    dummy.dummy_exception(update=update, context=MagicMock())
+    await dummy.dummy_exception(update=update, context=MagicMock())
     assert dummy.state['called'] is False
     assert not log_has('Executing handler: dummy_handler for chat_id: 0', caplog)
     assert not log_has('Rejected unauthorized message from: 0', caplog)
@@ -2086,7 +2087,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
         telegram._rpc._fiat_converter.convert_amount = old_convamount
 
 
-def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None:
+async def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None:
 
     telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
 

From 69f61ef767f6d7fe3bb493c5dd72ff0a5d1dbf95 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sat, 15 Apr 2023 15:54:25 +0200
Subject: [PATCH 65/85] Further telegram async tests

---
 tests/rpc/test_rpc_telegram.py | 31 ++++++++++++++++---------------
 1 file changed, 16 insertions(+), 15 deletions(-)

diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py
index 39895abaf..1b84df1ca 100644
--- a/tests/rpc/test_rpc_telegram.py
+++ b/tests/rpc/test_rpc_telegram.py
@@ -137,7 +137,7 @@ def test_cleanup(default_conf, mocker, ) -> None:
 
     telegram, _, _ = get_telegram_testobject(mocker, default_conf, mock=False)
     telegram.cleanup()
-    assert telegram._updater.stop.call_count == 1
+    assert telegram._app.stop.call_count == 1
 
 
 async def test_authorized_only(default_conf, mocker, caplog, update) -> None:
@@ -2327,32 +2327,33 @@ def test__sell_emoji(default_conf, mocker, msg, expected):
 async def test_telegram__send_msg(default_conf, mocker, caplog) -> None:
     mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
     bot = MagicMock()
+    bot.send_message = AsyncMock()
+    bot.edit_message_text = AsyncMock()
     telegram, _, _ = get_telegram_testobject(mocker, default_conf, mock=False)
-    telegram._updater = MagicMock()
-    telegram._updater.bot = bot
+    telegram._app = MagicMock()
+    telegram._app.bot = bot
 
-    telegram._config['telegram']['enabled'] = True
     await telegram._send_msg('test')
     assert len(bot.method_calls) == 1
 
     # Test update
     query = MagicMock()
     await telegram._send_msg('test', callback_path="DeadBeef", query=query, reload_able=True)
-    edit_message_text = telegram._updater.bot.edit_message_text
+    edit_message_text = telegram._app.bot.edit_message_text
     assert edit_message_text.call_count == 1
     assert "Updated: " in edit_message_text.call_args_list[0][1]['text']
 
-    telegram._updater.bot.edit_message_text = MagicMock(side_effect=BadRequest("not modified"))
+    telegram._app.bot.edit_message_text = AsyncMock(side_effect=BadRequest("not modified"))
     await telegram._send_msg('test', callback_path="DeadBeef", query=query)
-    assert telegram._updater.bot.edit_message_text.call_count == 1
+    assert telegram._app.bot.edit_message_text.call_count == 1
     assert not log_has_re(r"TelegramError: .*", caplog)
 
-    telegram._updater.bot.edit_message_text = MagicMock(side_effect=BadRequest(""))
+    telegram._app.bot.edit_message_text = AsyncMock(side_effect=BadRequest(""))
     await telegram._send_msg('test2', callback_path="DeadBeef", query=query)
-    assert telegram._updater.bot.edit_message_text.call_count == 1
+    assert telegram._app.bot.edit_message_text.call_count == 1
     assert log_has_re(r"TelegramError: .*", caplog)
 
-    telegram._updater.bot.edit_message_text = MagicMock(side_effect=TelegramError("DeadBEEF"))
+    telegram._app.bot.edit_message_text = AsyncMock(side_effect=TelegramError("DeadBEEF"))
     await telegram._send_msg('test3', callback_path="DeadBeef", query=query)
 
     assert log_has_re(r"TelegramError: DeadBEEF! Giving up.*", caplog)
@@ -2363,8 +2364,8 @@ async def test__send_msg_network_error(default_conf, mocker, caplog) -> None:
     bot = MagicMock()
     bot.send_message = MagicMock(side_effect=NetworkError('Oh snap'))
     telegram, _, _ = get_telegram_testobject(mocker, default_conf, mock=False)
-    telegram._updater = MagicMock()
-    telegram._updater.bot = bot
+    telegram._app = MagicMock()
+    telegram._app.bot = bot
 
     telegram._config['telegram']['enabled'] = True
     await telegram._send_msg('test')
@@ -2377,7 +2378,7 @@ async def test__send_msg_network_error(default_conf, mocker, caplog) -> None:
 async def test__send_msg_keyboard(default_conf, mocker, caplog) -> None:
     mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
     bot = MagicMock()
-    bot.send_message = MagicMock()
+    bot.send_message = AsyncMock()
     freqtradebot = get_patched_freqtradebot(mocker, default_conf)
     rpc = RPC(freqtradebot)
 
@@ -2393,8 +2394,8 @@ async def test__send_msg_keyboard(default_conf, mocker, caplog) -> None:
 
     def init_telegram(freqtradebot):
         telegram = Telegram(rpc, default_conf)
-        telegram._updater = MagicMock()
-        telegram._updater.bot = bot
+        telegram._app = MagicMock()
+        telegram._app.bot = bot
         return telegram
 
     # no keyboard in config -> default keyboard

From cf0b37057cd74de028b1caefb6e57395d84a70be Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 16 Apr 2023 16:58:12 +0200
Subject: [PATCH 66/85] update telegram "cleanup" test

---
 tests/rpc/test_rpc_telegram.py | 23 +++++++++++++++++------
 1 file changed, 17 insertions(+), 6 deletions(-)

diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py
index 1b84df1ca..b406ddc3a 100644
--- a/tests/rpc/test_rpc_telegram.py
+++ b/tests/rpc/test_rpc_telegram.py
@@ -2,6 +2,7 @@
 # pragma pylint: disable=protected-access, unused-argument, invalid-name
 # pragma pylint: disable=too-many-lines, too-many-arguments
 
+import asyncio
 import logging
 import re
 from datetime import datetime, timedelta, timezone
@@ -130,14 +131,24 @@ def test_telegram_init(default_conf, mocker, caplog) -> None:
     assert log_has(message_str, caplog)
 
 
-def test_cleanup(default_conf, mocker, ) -> None:
-    updater_mock = MagicMock()
-    updater_mock.stop = MagicMock()
-    mocker.patch('freqtrade.rpc.telegram.Updater', updater_mock)
+async def test_telegram_cleanup(default_conf, mocker, ) -> None:
+    app_mock = MagicMock()
+    app_mock.stop = AsyncMock()
+    app_mock.initialize = AsyncMock()
 
-    telegram, _, _ = get_telegram_testobject(mocker, default_conf, mock=False)
+    updater_mock = MagicMock()
+    updater_mock.stop = AsyncMock()
+    app_mock.updater = updater_mock
+    # mocker.patch('freqtrade.rpc.telegram.Application', app_mock)
+
+    telegram, _, _ = get_telegram_testobject(mocker, default_conf)
+    telegram._app = app_mock
+    telegram._loop = asyncio.get_running_loop()
+    telegram._thread = MagicMock()
     telegram.cleanup()
-    assert telegram._app.stop.call_count == 1
+    await asyncio.sleep(0.1)
+    assert app_mock.stop.call_count == 1
+    assert telegram._thread.join.call_count == 1
 
 
 async def test_authorized_only(default_conf, mocker, caplog, update) -> None:

From c9e6137ad0d472d910941ce24acbb379e2f44adc Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 16 Apr 2023 17:18:28 +0200
Subject: [PATCH 67/85] Fix test_telegram _init test

---
 freqtrade/rpc/telegram.py      |  5 ++++-
 tests/rpc/test_rpc_telegram.py | 15 +++++++++------
 2 files changed, 13 insertions(+), 7 deletions(-)

diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index babe7d582..d20d1862e 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -166,6 +166,9 @@ class Telegram(RPCHandler):
                 logger.info('using custom keyboard from '
                             f'config.json: {self._keyboard}')
 
+    def _init_telegram_app(self):
+        return Application.builder().token(self._config['telegram']['token']).build()
+
     def _init(self) -> None:
         """
         Initializes this module with the given config,
@@ -179,7 +182,7 @@ class Telegram(RPCHandler):
             self._loop = asyncio.new_event_loop()
             asyncio.set_event_loop(self._loop)
 
-        self._app = Application.builder().token(self._config['telegram']['token']).build()
+        self._app = self._init_telegram_app()
 
         # Register command handler and start telegram message polling
         handles = [
diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py
index b406ddc3a..b762c999b 100644
--- a/tests/rpc/test_rpc_telegram.py
+++ b/tests/rpc/test_rpc_telegram.py
@@ -105,15 +105,18 @@ def test_telegram__init__(default_conf, mocker) -> None:
 
 
 def test_telegram_init(default_conf, mocker, caplog) -> None:
-    start_polling = MagicMock()
-    mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling))
+    app_mock = MagicMock()
+    mocker.patch('freqtrade.rpc.telegram.Telegram._start_thread', MagicMock())
+    mocker.patch('freqtrade.rpc.telegram.Telegram._init_telegram_app', return_value=app_mock)
+    mocker.patch('freqtrade.rpc.telegram.Telegram._startup_telegram', AsyncMock())
 
-    get_telegram_testobject(mocker, default_conf, mock=False)
-    assert start_polling.call_count == 0
+    telegram, _, _ = get_telegram_testobject(mocker, default_conf, mock=False)
+    telegram._init()
+    assert app_mock.call_count == 0
 
     # number of handles registered
-    assert start_polling.dispatcher.add_handler.call_count > 0
-    assert start_polling.start_polling.call_count == 1
+    assert app_mock.add_handler.call_count > 0
+    # assert start_polling.start_polling.call_count == 1
 
     message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], "
                    "['balance'], ['start'], ['stop'], "

From 7171fd1132a9a81c210f1426833b8f383c3ae5da Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 16 Apr 2023 17:24:11 +0200
Subject: [PATCH 68/85] Test telegram startup

---
 tests/rpc/test_rpc_telegram.py | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py
index b762c999b..5fbd4f98e 100644
--- a/tests/rpc/test_rpc_telegram.py
+++ b/tests/rpc/test_rpc_telegram.py
@@ -134,6 +134,23 @@ def test_telegram_init(default_conf, mocker, caplog) -> None:
     assert log_has(message_str, caplog)
 
 
+async def test_telegram_startup(default_conf, mocker) -> None:
+    app_mock = MagicMock()
+    app_mock.initialize = AsyncMock()
+    app_mock.start = AsyncMock()
+    app_mock.updater.start_polling = AsyncMock()
+    app_mock.updater.running = False
+    sleep_mock = mocker.patch('freqtrade.rpc.telegram.asyncio.sleep',AsyncMock())
+
+    telegram, _, _ = get_telegram_testobject(mocker, default_conf)
+    telegram._app = app_mock
+    await telegram._startup_telegram()
+    assert app_mock.initialize.call_count == 1
+    assert app_mock.start.call_count == 1
+    assert app_mock.updater.start_polling.call_count == 1
+    assert sleep_mock.call_count == 1
+
+
 async def test_telegram_cleanup(default_conf, mocker, ) -> None:
     app_mock = MagicMock()
     app_mock.stop = AsyncMock()

From 5608aaca26789645acffdf3841607abcef36e06c Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 16 Apr 2023 17:45:56 +0200
Subject: [PATCH 69/85] Simplify mocking

---
 freqtrade/exchange/exchange.py | 8 ++++++--
 tests/rpc/test_rpc_telegram.py | 3 ++-
 2 files changed, 8 insertions(+), 3 deletions(-)

diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py
index 7e276d538..669a5c7d1 100644
--- a/freqtrade/exchange/exchange.py
+++ b/freqtrade/exchange/exchange.py
@@ -107,8 +107,7 @@ class Exchange:
         # Lock event loop. This is necessary to avoid race-conditions when using force* commands
         # Due to funding fee fetching.
         self._loop_lock = Lock()
-        self.loop = asyncio.new_event_loop()
-        asyncio.set_event_loop(self.loop)
+        self.loop = self._init_async_loop()
         self._config: Config = {}
 
         self._config.update(config)
@@ -212,6 +211,11 @@ class Exchange:
         if self.loop and not self.loop.is_closed():
             self.loop.close()
 
+    def _init_async_loop(self):
+        loop = asyncio.new_event_loop()
+        asyncio.set_event_loop(loop)
+        return loop
+
     def validate_config(self, config):
         # Check if timeframe is available
         self.validate_timeframes(config.get('timeframe'))
diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py
index 5fbd4f98e..adfbb4b2a 100644
--- a/tests/rpc/test_rpc_telegram.py
+++ b/tests/rpc/test_rpc_telegram.py
@@ -89,6 +89,7 @@ def get_telegram_testobject(mocker, default_conf, mock=True, ftbot=None):
             _start_thread=MagicMock(),
         )
     if not ftbot:
+        mocker.patch('freqtrade.exchange.exchange.Exchange._init_async_loop')
         ftbot = get_patched_freqtradebot(mocker, default_conf)
     rpc = RPC(ftbot)
     telegram = Telegram(rpc, default_conf)
@@ -140,7 +141,7 @@ async def test_telegram_startup(default_conf, mocker) -> None:
     app_mock.start = AsyncMock()
     app_mock.updater.start_polling = AsyncMock()
     app_mock.updater.running = False
-    sleep_mock = mocker.patch('freqtrade.rpc.telegram.asyncio.sleep',AsyncMock())
+    sleep_mock = mocker.patch('freqtrade.rpc.telegram.asyncio.sleep', AsyncMock())
 
     telegram, _, _ = get_telegram_testobject(mocker, default_conf)
     telegram._app = app_mock

From d25e82d095956a132f6ae5ec3b465204cf8fcda6 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 16 Apr 2023 17:56:12 +0200
Subject: [PATCH 70/85] Mock exchange loop

---
 tests/rpc/test_rpc_telegram.py | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py
index adfbb4b2a..4601e64c4 100644
--- a/tests/rpc/test_rpc_telegram.py
+++ b/tests/rpc/test_rpc_telegram.py
@@ -37,6 +37,11 @@ from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, create_mock_trades,
                             patch_exchange, patch_get_signal, patch_whitelist)
 
 
+@pytest.fixture(autouse=True)
+def mock_exchange_loop(mocker):
+    mocker.patch('freqtrade.exchange.exchange.Exchange._init_async_loop')
+
+
 @pytest.fixture
 def default_conf(default_conf) -> dict:
     # Telegram is enabled by default

From 516b49ff500cacd50222fa2946eae89805cbaf9b Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 16 Apr 2023 18:15:48 +0200
Subject: [PATCH 71/85] Fix bad types

---
 freqtrade/optimize/hyperopt.py | 2 +-
 freqtrade/rpc/telegram.py      | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py
index fe590f0d2..bf6bb1ea0 100644
--- a/freqtrade/optimize/hyperopt.py
+++ b/freqtrade/optimize/hyperopt.py
@@ -556,7 +556,7 @@ class Hyperopt:
         self.backtesting.exchange.close()
         self.backtesting.exchange._api = None
         self.backtesting.exchange._api_async = None
-        self.backtesting.exchange.loop = None  # type: ignore
+        self.backtesting.exchange.loop = None
         self.backtesting.exchange._loop_lock = None  # type: ignore
         self.backtesting.exchange._cache_lock = None  # type: ignore
         # self.backtesting.exchange = None  # type: ignore
diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index d20d1862e..a972dc322 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -1739,7 +1739,7 @@ class Telegram(RPCHandler):
                 [InlineKeyboardButton("Refresh", callback_data=callback_path)]])
         else:
             if keyboard is not None:
-                reply_markup = InlineKeyboardMarkup(keyboard, resize_keyboard=True)
+                reply_markup = InlineKeyboardMarkup(keyboard)
             else:
                 reply_markup = ReplyKeyboardMarkup(self._keyboard, resize_keyboard=True)
         try:

From c06759223eaa7698eefe885360926f199d3caed8 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Mon, 17 Apr 2023 06:40:18 +0200
Subject: [PATCH 72/85] Improve telegram async tests

---
 freqtrade/rpc/telegram.py      |  2 +-
 tests/rpc/test_rpc_telegram.py | 19 ++++++++++++++++++-
 2 files changed, 19 insertions(+), 2 deletions(-)

diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index a972dc322..6e509950c 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -1796,7 +1796,7 @@ class Telegram(RPCHandler):
             if new_market_dir is not None:
                 self._rpc._update_market_direction(new_market_dir)
                 await self._send_msg("Successfully updated market direction"
-                               f" from *{old_market_dir}* to *{new_market_dir}*.")
+                                     f" from *{old_market_dir}* to *{new_market_dir}*.")
             else:
                 raise RPCException("Invalid market direction provided. \n"
                                    "Valid market directions: *long, short, even, none*")
diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py
index 4601e64c4..4b4c2b028 100644
--- a/tests/rpc/test_rpc_telegram.py
+++ b/tests/rpc/test_rpc_telegram.py
@@ -5,6 +5,7 @@
 import asyncio
 import logging
 import re
+import threading
 from datetime import datetime, timedelta, timezone
 from functools import reduce
 from random import choice, randint
@@ -57,6 +58,20 @@ def update():
     return _update
 
 
+def patch_eventloop_threading(telegrambot):
+    is_init = False
+
+    def thread_fuck():
+        nonlocal is_init
+        telegrambot._loop = asyncio.new_event_loop()
+        is_init = True
+        telegrambot._loop.run_forever()
+    x = threading.Thread(target=thread_fuck, daemon=True)
+    x.start()
+    while not is_init:
+        pass
+
+
 class DummyCls(Telegram):
     """
     Dummy class for testing the Telegram @authorized_only decorator
@@ -99,6 +114,7 @@ def get_telegram_testobject(mocker, default_conf, mock=True, ftbot=None):
     rpc = RPC(ftbot)
     telegram = Telegram(rpc, default_conf)
     telegram._loop = MagicMock()
+    patch_eventloop_threading(telegram)
 
     return telegram, ftbot, msg_mock
 
@@ -2216,7 +2232,7 @@ def test_send_msg_status_notification(default_conf, mocker) -> None:
     assert msg_mock.call_args[0][0] == '*Status:* `running`'
 
 
-def test_warning_notification(default_conf, mocker) -> None:
+async def test_warning_notification(default_conf, mocker) -> None:
     telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
     telegram.send_msg({
         'type': RPCMessageType.WARNING,
@@ -2412,6 +2428,7 @@ async def test__send_msg_network_error(default_conf, mocker, caplog) -> None:
     assert log_has('Telegram NetworkError: Oh snap! Trying one more time.', caplog)
 
 
+@pytest.mark.filterwarnings("ignore:.*ChatPermissions")
 async def test__send_msg_keyboard(default_conf, mocker, caplog) -> None:
     mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
     bot = MagicMock()

From 2615b0297e5fa2c578baeafbe84497dcfa480fe7 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Wed, 19 Apr 2023 18:12:52 +0200
Subject: [PATCH 73/85] Move httpx to regular dependencies, losely-pin

---
 requirements-dev.txt | 2 --
 requirements.txt     | 2 ++
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/requirements-dev.txt b/requirements-dev.txt
index cd4c96eea..ea75bb8f2 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -18,8 +18,6 @@ pytest-random-order==1.1.0
 isort==5.12.0
 # For datetime mocking
 time-machine==2.9.0
-# fastapi testing
-httpx==0.24.0
 
 # Convert jupyter notebooks to markdown documents
 nbconvert==7.3.1
diff --git a/requirements.txt b/requirements.txt
index a07ef8897..6eecfb630 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -7,6 +7,8 @@ cryptography==40.0.2
 aiohttp==3.8.4
 SQLAlchemy==2.0.10
 python-telegram-bot==20.2
+# can't be hard-pinned due to telegram-bot pinning httpx with ~
+httpx>=0.23.3
 arrow==1.2.3
 cachetools==4.2.2
 requests==2.28.2

From b49ff3d5bcc99aa796bed83ebaf203418617b673 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sat, 22 Apr 2023 11:11:03 +0200
Subject: [PATCH 74/85] Improve type safety

---
 freqtrade/exchange/exchange.py | 2 +-
 freqtrade/optimize/hyperopt.py | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py
index 669a5c7d1..6706e94ab 100644
--- a/freqtrade/exchange/exchange.py
+++ b/freqtrade/exchange/exchange.py
@@ -211,7 +211,7 @@ class Exchange:
         if self.loop and not self.loop.is_closed():
             self.loop.close()
 
-    def _init_async_loop(self):
+    def _init_async_loop(self) -> asyncio.AbstractEventLoop:
         loop = asyncio.new_event_loop()
         asyncio.set_event_loop(loop)
         return loop
diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py
index bf6bb1ea0..fe590f0d2 100644
--- a/freqtrade/optimize/hyperopt.py
+++ b/freqtrade/optimize/hyperopt.py
@@ -556,7 +556,7 @@ class Hyperopt:
         self.backtesting.exchange.close()
         self.backtesting.exchange._api = None
         self.backtesting.exchange._api_async = None
-        self.backtesting.exchange.loop = None
+        self.backtesting.exchange.loop = None  # type: ignore
         self.backtesting.exchange._loop_lock = None  # type: ignore
         self.backtesting.exchange._cache_lock = None  # type: ignore
         # self.backtesting.exchange = None  # type: ignore

From c19d6b4e291b3112946d4aa8ab7da82d662c3de0 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 24 Apr 2023 14:01:01 +0000
Subject: [PATCH 75/85] Bump pandas from 1.5.3 to 2.0.1

Bumps [pandas](https://github.com/pandas-dev/pandas) from 1.5.3 to 2.0.1.
- [Release notes](https://github.com/pandas-dev/pandas/releases)
- [Commits](https://github.com/pandas-dev/pandas/compare/v1.5.3...v2.0.1)

---
updated-dependencies:
- dependency-name: pandas
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] 
---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index 6eecfb630..7c646aec6 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
 numpy==1.24.3
-pandas==1.5.3
+pandas==2.0.1
 pandas-ta==0.3.14b
 
 ccxt==3.0.75

From 6a271317bcc055d84dd802fbfc415e12705b4327 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Tue, 25 Apr 2023 08:53:02 +0200
Subject: [PATCH 76/85] use stop_price_param for dry stops

closes #8555
---
 freqtrade/exchange/exchange.py       | 4 ++--
 freqtrade/persistence/trade_model.py | 9 ++++++---
 2 files changed, 8 insertions(+), 5 deletions(-)

diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py
index 6706e94ab..9a303426a 100644
--- a/freqtrade/exchange/exchange.py
+++ b/freqtrade/exchange/exchange.py
@@ -867,7 +867,7 @@ class Exchange:
         }
         if stop_loss:
             dry_order["info"] = {"stopPrice": dry_order["price"]}
-            dry_order["stopPrice"] = dry_order["price"]
+            dry_order[self._ft_has['stop_price_param']] = dry_order["price"]
             # Workaround to avoid filling stoploss orders immediately
             dry_order["ft_order_type"] = "stoploss"
         orderbook: Optional[OrderBook] = None
@@ -1019,7 +1019,7 @@ class Exchange:
             from freqtrade.persistence import Order
             order = Order.order_by_id(order_id)
             if order:
-                ccxt_order = order.to_ccxt_object()
+                ccxt_order = order.to_ccxt_object(self._ft_has['stop_price_param'])
                 self._dry_run_open_orders[order_id] = ccxt_order
                 return ccxt_order
             # Gracefully handle errors with dry-run orders.
diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py
index 0572b45a6..20cfe3102 100644
--- a/freqtrade/persistence/trade_model.py
+++ b/freqtrade/persistence/trade_model.py
@@ -158,7 +158,7 @@ class Order(ModelBase):
                 self.order_filled_date = datetime.now(timezone.utc)
         self.order_update_date = datetime.now(timezone.utc)
 
-    def to_ccxt_object(self) -> Dict[str, Any]:
+    def to_ccxt_object(self, stopPriceName: str = 'stopPrice') -> Dict[str, Any]:
         order: Dict[str, Any] = {
             'id': self.order_id,
             'symbol': self.ft_pair,
@@ -170,7 +170,6 @@ class Order(ModelBase):
             'side': self.ft_order_side,
             'filled': self.filled,
             'remaining': self.remaining,
-            'stopPrice': self.stop_price,
             'datetime': self.order_date_utc.strftime('%Y-%m-%dT%H:%M:%S.%f'),
             'timestamp': int(self.order_date_utc.timestamp() * 1000),
             'status': self.status,
@@ -178,7 +177,11 @@ class Order(ModelBase):
             'info': {},
         }
         if self.ft_order_side == 'stoploss':
-            order['ft_order_type'] = 'stoploss'
+            order.update({
+                stopPriceName: self.stop_price,
+                'ft_order_type': 'stoploss',
+            })
+
         return order
 
     def to_json(self, entry_side: str, minified: bool = False) -> Dict[str, Any]:

From 1e9fa4c04109aee023c3c2140d2d8fa7af2730af Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Tue, 25 Apr 2023 09:04:02 +0200
Subject: [PATCH 77/85] Improve test to cover to_ccxt better

---
 tests/persistence/test_persistence.py | 18 +++++++++++++++---
 1 file changed, 15 insertions(+), 3 deletions(-)

diff --git a/tests/persistence/test_persistence.py b/tests/persistence/test_persistence.py
index 948973ed5..1a7d84eca 100644
--- a/tests/persistence/test_persistence.py
+++ b/tests/persistence/test_persistence.py
@@ -2481,7 +2481,7 @@ def test_select_filled_orders(fee):
 
 
 @pytest.mark.usefixtures("init_persistence")
-def test_order_to_ccxt(limit_buy_order_open):
+def test_order_to_ccxt(limit_buy_order_open, limit_sell_order_usdt_open):
 
     order = Order.parse_from_ccxt_object(limit_buy_order_open, 'mocked', 'buy')
     order.ft_trade_id = 1
@@ -2495,11 +2495,23 @@ def test_order_to_ccxt(limit_buy_order_open):
     del raw_order['fee']
     del raw_order['datetime']
     del raw_order['info']
-    assert raw_order['stopPrice'] is None
-    del raw_order['stopPrice']
+    assert raw_order.get('stopPrice') is None
+    raw_order.pop('stopPrice', None)
     del limit_buy_order_open['datetime']
     assert raw_order == limit_buy_order_open
 
+    order1 = Order.parse_from_ccxt_object(limit_sell_order_usdt_open, 'mocked', 'sell')
+    order1.ft_order_side = 'stoploss'
+    order1.stop_price = order1.price * 0.9
+    order1.ft_trade_id = 1
+    order1.session.add(order1)
+    Order.session.commit()
+
+    order_resp1 = Order.order_by_id(limit_sell_order_usdt_open['id'])
+    raw_order1 = order_resp1.to_ccxt_object()
+
+    assert raw_order1.get('stopPrice') is not None
+
 
 @pytest.mark.usefixtures("init_persistence")
 @pytest.mark.parametrize('data', [

From 59f9f4d467df2bddebfec1856b775e57dbce4671 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Tue, 25 Apr 2023 09:27:33 +0200
Subject: [PATCH 78/85] Fix exception typos due to newlines

---
 freqtrade/freqai/freqai_interface.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py
index 7eaaeab3e..039b6a175 100644
--- a/freqtrade/freqai/freqai_interface.py
+++ b/freqtrade/freqai/freqai_interface.py
@@ -489,9 +489,9 @@ class IFreqaiModel(ABC):
         if dk.training_features_list != feature_list:
             raise OperationalException(
                 "Trying to access pretrained model with `identifier` "
-                "but found different features furnished by current strategy."
-                "Change `identifier` to train from scratch, or ensure the"
-                "strategy is furnishing the same features as the pretrained"
+                "but found different features furnished by current strategy. "
+                "Change `identifier` to train from scratch, or ensure the "
+                "strategy is furnishing the same features as the pretrained "
                 "model. In case of --strategy-list, please be aware that FreqAI "
                 "requires all strategies to maintain identical "
                 "feature_engineering_* functions"

From 11c9f96d23fd17d39d211817c5c2def03fe40be3 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Tue, 25 Apr 2023 11:45:35 +0200
Subject: [PATCH 79/85] Use lock for trade entries, too

---
 freqtrade/freqtradebot.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index 9cc26ad77..76c48a444 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -490,7 +490,8 @@ class FreqtradeBot(LoggingMixin):
         # Create entity and execute trade for each pair from whitelist
         for pair in whitelist:
             try:
-                trades_created += self.create_trade(pair)
+                with self._exit_lock:
+                    trades_created += self.create_trade(pair)
             except DependencyException as exception:
                 logger.warning('Unable to create trade for %s: %s', pair, exception)
 

From e8fedb685b899c2824b40e434d8eb837fa2eb180 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Tue, 25 Apr 2023 11:52:13 +0200
Subject: [PATCH 80/85] Update missleading docstring

---
 freqtrade/freqtradebot.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index 76c48a444..89f0ac55d 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -420,7 +420,7 @@ class FreqtradeBot(LoggingMixin):
         """
         Try refinding a lost trade.
         Only used when InsufficientFunds appears on exit orders (stoploss or long sell/short buy).
-        Tries to walk the stored orders and sell them off eventually.
+        Tries to walk the stored orders and updates the trade state if necessary.
         """
         logger.info(f"Trying to refind lost order for {trade}")
         for order in trade.orders:

From 1b228e3705a5d0dd5f20f580286427fb97b7265d Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Tue, 25 Apr 2023 15:51:58 +0200
Subject: [PATCH 81/85] Improve test resiliance by removing unneeded MagicMock

---
 tests/test_freqtradebot.py | 63 ++++++++++++++++++++++++++++++++------
 1 file changed, 54 insertions(+), 9 deletions(-)

diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index 7bded0f82..ea99061b8 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -2107,6 +2107,7 @@ def test_enter_positions(mocker, default_conf_usdt, return_value, side_effect,
     assert mock_ct.call_count == len(default_conf_usdt['exchange']['pair_whitelist'])
 
 
+@pytest.mark.usefixtures("init_persistence")
 @pytest.mark.parametrize("is_short", [False, True])
 def test_exit_positions(mocker, default_conf_usdt, limit_order, is_short, caplog) -> None:
     freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
@@ -2115,12 +2116,33 @@ def test_exit_positions(mocker, default_conf_usdt, limit_order, is_short, caplog
     mocker.patch(f'{EXMS}.fetch_order', return_value=limit_order[entry_side(is_short)])
     mocker.patch(f'{EXMS}.get_trades_for_order', return_value=[])
 
-    # TODO: should not be magicmock
-    trade = MagicMock()
-    trade.is_short = is_short
-    trade.open_order_id = '123'
-    trade.open_fee = 0.001
+    order_id = '123'
+    trade = Trade(
+            open_order_id=order_id,
+            pair='ETH/USDT',
+            fee_open=0.001,
+            fee_close=0.001,
+            open_rate=0.01,
+            open_date=arrow.utcnow().datetime,
+            stake_amount=0.01,
+            amount=11,
+            exchange="binance",
+            is_short=is_short,
+            leverage=1,
+            )
+    trade.orders.append(Order(
+        ft_order_side=entry_side(is_short),
+        price=0.01,
+        ft_pair=trade.pair,
+        ft_amount=trade.amount,
+        ft_price=trade.open_rate,
+        order_id=order_id,
+
+    ))
+    Trade.session.add(trade)
+    Trade.commit()
     trades = [trade]
+    freqtrade.wallets.update()
     n = freqtrade.exit_positions(trades)
     assert n == 0
     # Test amount not modified by fee-logic
@@ -2133,17 +2155,40 @@ def test_exit_positions(mocker, default_conf_usdt, limit_order, is_short, caplog
     assert gra.call_count == 0
 
 
+@pytest.mark.usefixtures("init_persistence")
 @pytest.mark.parametrize("is_short", [False, True])
 def test_exit_positions_exception(mocker, default_conf_usdt, limit_order, caplog, is_short) -> None:
     freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
     order = limit_order[entry_side(is_short)]
     mocker.patch(f'{EXMS}.fetch_order', return_value=order)
 
-    # TODO: should not be magicmock
-    trade = MagicMock()
-    trade.is_short = is_short
+    order_id = '123'
+    trade = Trade(
+        open_order_id=order_id,
+        pair='ETH/USDT',
+        fee_open=0.001,
+        fee_close=0.001,
+        open_rate=0.01,
+        open_date=arrow.utcnow().datetime,
+        stake_amount=0.01,
+        amount=11,
+        exchange="binance",
+        is_short=is_short,
+        leverage=1,
+    )
+    trade.orders.append(Order(
+        ft_order_side=entry_side(is_short),
+        price=0.01,
+        ft_pair=trade.pair,
+        ft_amount=trade.amount,
+        ft_price=trade.open_rate,
+        order_id=order_id,
+
+    ))
     trade.open_order_id = None
-    trade.pair = 'ETH/USDT'
+    Trade.session.add(trade)
+    Trade.commit()
+    freqtrade.wallets.update()
     trades = [trade]
 
     # Test raise of DependencyException exception

From c4a0910908601cb7ae63645e41353123d0c4916f Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Tue, 25 Apr 2023 15:33:37 +0200
Subject: [PATCH 82/85] Handle special case where exit order is for more than
 the trade amount ...

---
 freqtrade/persistence/trade_model.py | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py
index 20cfe3102..cff2c37f0 100644
--- a/freqtrade/persistence/trade_model.py
+++ b/freqtrade/persistence/trade_model.py
@@ -711,7 +711,10 @@ class LocalTrade():
         if order.ft_order_side != self.entry_side:
             amount_tr = amount_to_contract_precision(self.amount, self.amount_precision,
                                                      self.precision_mode, self.contract_size)
-            if isclose(order.safe_amount_after_fee, amount_tr, abs_tol=MATH_CLOSE_PREC):
+            if (
+                isclose(order.safe_amount_after_fee, amount_tr, abs_tol=MATH_CLOSE_PREC)
+                or order.safe_amount_after_fee > amount_tr
+            ):
                 self.close(order.safe_price)
             else:
                 self.recalc_trade_from_orders()

From 1d9933412a45b892a4a3bfde30cead83bce242b6 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Thu, 27 Apr 2023 06:43:57 +0200
Subject: [PATCH 83/85] improve `/version` output formatting

---
 freqtrade/rpc/telegram.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index 6e509950c..caa8715ac 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -1636,7 +1636,7 @@ class Telegram(RPCHandler):
         strategy_version = self._rpc._freqtrade.strategy.version()
         version_string = f'*Version:* `{__version__}`'
         if strategy_version is not None:
-            version_string += f', *Strategy version: * `{strategy_version}`'
+            version_string += f'\n*Strategy version: * `{strategy_version}`'
 
         await self._send_msg(version_string)
 

From daf564b62fb012633e457fbc1448455b1cf48716 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Thu, 27 Apr 2023 18:27:09 +0200
Subject: [PATCH 84/85] Invert logic for webhook

closes #8562
---
 freqtrade/rpc/webhook.py      |  8 ++++----
 tests/rpc/test_rpc_webhook.py | 25 +++++++++++++++----------
 2 files changed, 19 insertions(+), 14 deletions(-)

diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py
index 14b881126..80690ec0c 100644
--- a/freqtrade/rpc/webhook.py
+++ b/freqtrade/rpc/webhook.py
@@ -44,8 +44,11 @@ class Webhook(RPCHandler):
 
     def _get_value_dict(self, msg: RPCSendMsg) -> Optional[Dict[str, Any]]:
         whconfig = self._config['webhook']
+        if msg['type'].value in whconfig:
+            # Explicit types should have priority
+            valuedict = whconfig.get(msg['type'].value)
         # Deprecated 2022.10 - only keep generic method.
-        if msg['type'] in [RPCMessageType.ENTRY]:
+        elif msg['type'] in [RPCMessageType.ENTRY]:
             valuedict = whconfig.get('webhookentry')
         elif msg['type'] in [RPCMessageType.ENTRY_CANCEL]:
             valuedict = whconfig.get('webhookentrycancel')
@@ -62,9 +65,6 @@ class Webhook(RPCHandler):
                              RPCMessageType.EXCEPTION,
                              RPCMessageType.WARNING):
             valuedict = whconfig.get('webhookstatus')
-        elif msg['type'].value in whconfig:
-            # Allow all types ...
-            valuedict = whconfig.get(msg['type'].value)
         elif msg['type'] in (
                 RPCMessageType.PROTECTION_TRIGGER,
                 RPCMessageType.PROTECTION_TRIGGER_GLOBAL,
diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py
index f55582107..d0a0f5b1e 100644
--- a/tests/rpc/test_rpc_webhook.py
+++ b/tests/rpc/test_rpc_webhook.py
@@ -17,6 +17,10 @@ def get_webhook_dict() -> dict:
         "enabled": True,
         "url": "https://maker.ifttt.com/trigger/freqtrade_test/with/key/c764udvJ5jfSlswVRukZZ2/",
         "webhookentry": {
+            # Intentionally broken, as "entry" should have priority.
+            "value1": "Buying {pair55555}",
+        },
+        "entry": {
             "value1": "Buying {pair}",
             "value2": "limit {limit:8f}",
             "value3": "{stake_amount:8f} {stake_currency}",
@@ -89,15 +93,15 @@ def test_send_msg_webhook(default_conf, mocker):
     webhook.send_msg(msg=msg)
     assert msg_mock.call_count == 1
     assert (msg_mock.call_args[0][0]["value1"] ==
-            default_conf["webhook"]["webhookentry"]["value1"].format(**msg))
+            default_conf["webhook"]["entry"]["value1"].format(**msg))
     assert (msg_mock.call_args[0][0]["value2"] ==
-            default_conf["webhook"]["webhookentry"]["value2"].format(**msg))
+            default_conf["webhook"]["entry"]["value2"].format(**msg))
     assert (msg_mock.call_args[0][0]["value3"] ==
-            default_conf["webhook"]["webhookentry"]["value3"].format(**msg))
+            default_conf["webhook"]["entry"]["value3"].format(**msg))
     assert (msg_mock.call_args[0][0]["value4"] ==
-            default_conf["webhook"]["webhookentry"]["value4"].format(**msg))
+            default_conf["webhook"]["entry"]["value4"].format(**msg))
     assert (msg_mock.call_args[0][0]["value5"] ==
-            default_conf["webhook"]["webhookentry"]["value5"].format(**msg))
+            default_conf["webhook"]["entry"]["value5"].format(**msg))
     # Test short
     msg_mock.reset_mock()
 
@@ -116,15 +120,15 @@ def test_send_msg_webhook(default_conf, mocker):
     webhook.send_msg(msg=msg)
     assert msg_mock.call_count == 1
     assert (msg_mock.call_args[0][0]["value1"] ==
-            default_conf["webhook"]["webhookentry"]["value1"].format(**msg))
+            default_conf["webhook"]["entry"]["value1"].format(**msg))
     assert (msg_mock.call_args[0][0]["value2"] ==
-            default_conf["webhook"]["webhookentry"]["value2"].format(**msg))
+            default_conf["webhook"]["entry"]["value2"].format(**msg))
     assert (msg_mock.call_args[0][0]["value3"] ==
-            default_conf["webhook"]["webhookentry"]["value3"].format(**msg))
+            default_conf["webhook"]["entry"]["value3"].format(**msg))
     assert (msg_mock.call_args[0][0]["value4"] ==
-            default_conf["webhook"]["webhookentry"]["value4"].format(**msg))
+            default_conf["webhook"]["entry"]["value4"].format(**msg))
     assert (msg_mock.call_args[0][0]["value5"] ==
-            default_conf["webhook"]["webhookentry"]["value5"].format(**msg))
+            default_conf["webhook"]["entry"]["value5"].format(**msg))
     # Test buy cancel
     msg_mock.reset_mock()
 
@@ -328,6 +332,7 @@ def test_send_msg_webhook(default_conf, mocker):
 
 def test_exception_send_msg(default_conf, mocker, caplog):
     default_conf["webhook"] = get_webhook_dict()
+    del default_conf["webhook"]["entry"]
     del default_conf["webhook"]["webhookentry"]
 
     webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf)

From 2a9e50a6a9baa2f1ca3216878ceb5a28c0ac024e Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Thu, 27 Apr 2023 19:43:33 +0200
Subject: [PATCH 85/85] Add test testing create-table statement creation for
 different sql dialects

closes #8561
---
 freqtrade/persistence/key_value_store.py |  2 +-
 tests/persistence/test_migrations.py     | 14 ++++++++++++++
 2 files changed, 15 insertions(+), 1 deletion(-)

diff --git a/freqtrade/persistence/key_value_store.py b/freqtrade/persistence/key_value_store.py
index 2d26acbd3..110a23d6c 100644
--- a/freqtrade/persistence/key_value_store.py
+++ b/freqtrade/persistence/key_value_store.py
@@ -36,7 +36,7 @@ class _KeyValueStoreModel(ModelBase):
 
     value_type: Mapped[ValueTypesEnum] = mapped_column(String(20), nullable=False)
 
-    string_value: Mapped[Optional[str]]
+    string_value: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
     datetime_value: Mapped[Optional[datetime]]
     float_value: Mapped[Optional[float]]
     int_value: Mapped[Optional[int]]
diff --git a/tests/persistence/test_migrations.py b/tests/persistence/test_migrations.py
index 854d39994..13b3f89bf 100644
--- a/tests/persistence/test_migrations.py
+++ b/tests/persistence/test_migrations.py
@@ -1,15 +1,18 @@
 # pragma pylint: disable=missing-docstring, C0103
 import logging
+from importlib import import_module
 from pathlib import Path
 from unittest.mock import MagicMock
 
 import pytest
 from sqlalchemy import create_engine, select, text
+from sqlalchemy.schema import CreateTable
 
 from freqtrade.constants import DEFAULT_DB_PROD_URL
 from freqtrade.enums import TradingMode
 from freqtrade.exceptions import OperationalException
 from freqtrade.persistence import Trade, init_db
+from freqtrade.persistence.base import ModelBase
 from freqtrade.persistence.migrations import get_last_sequence_ids, set_sequence_ids
 from freqtrade.persistence.models import PairLock
 from tests.conftest import log_has
@@ -411,3 +414,14 @@ def test_migrate_pairlocks(mocker, default_conf, fee, caplog):
     assert len(pairlocks) == 1
     pairlocks[0].pair == 'ETH/BTC'
     pairlocks[0].side == '*'
+
+
+@pytest.mark.parametrize('dialect', [
+    'sqlite', 'postgresql', 'mysql', 'oracle', 'mssql',
+    ])
+def test_create_table_compiles(dialect):
+
+    dialect_mod = import_module(f"sqlalchemy.dialects.{dialect}")
+    for table in ModelBase.metadata.tables.values():
+        create_sql = str(CreateTable(table).compile(dialect=dialect_mod.dialect()))
+        assert 'CREATE TABLE' in create_sql