diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 3991432a4..d05ee5db7 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -21,6 +21,7 @@ from freqtrade.exchange import Exchange, timeframe_to_seconds from freqtrade.exchange.types import OrderBook from freqtrade.misc import append_candles_to_dataframe from freqtrade.rpc import RPCManager +from freqtrade.rpc.rpc_types import RPCAnalyzedDFMsg from freqtrade.util import PeriodicCache @@ -118,8 +119,7 @@ class DataProvider: :param new_candle: This is a new candle """ if self.__rpc: - self.__rpc.send_msg( - { + msg: RPCAnalyzedDFMsg = { 'type': RPCMessageType.ANALYZED_DF, 'data': { 'key': pair_key, @@ -127,7 +127,7 @@ class DataProvider: 'la': datetime.now(timezone.utc) } } - ) + self.__rpc.send_msg(msg) if new_candle: self.__rpc.send_msg({ 'type': RPCMessageType.NEW_CANDLE, diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 86fbaab56..42db121e9 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -30,6 +30,8 @@ from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.rpc import RPCManager from freqtrade.rpc.external_message_consumer import ExternalMessageConsumer +from freqtrade.rpc.rpc_types import (RPCBuyMsg, RPCCancelMsg, RPCProtectionMsg, RPCSellCancelMsg, + RPCSellMsg) from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.util import FtPrecise @@ -948,7 +950,6 @@ class FreqtradeBot(LoggingMixin): """ Sends rpc notification when a entry order occurred. """ - msg_type = RPCMessageType.ENTRY_FILL if fill else RPCMessageType.ENTRY open_rate = order.safe_price if open_rate is None: @@ -959,9 +960,9 @@ class FreqtradeBot(LoggingMixin): current_rate = self.exchange.get_rate( trade.pair, side='entry', is_short=trade.is_short, refresh=False) - msg = { + msg: RPCBuyMsg = { 'trade_id': trade.id, - 'type': msg_type, + 'type': RPCMessageType.ENTRY_FILL if fill else RPCMessageType.ENTRY, 'buy_tag': trade.enter_tag, 'enter_tag': trade.enter_tag, 'exchange': trade.exchange.capitalize(), @@ -973,6 +974,7 @@ class FreqtradeBot(LoggingMixin): 'order_type': order_type, 'stake_amount': trade.stake_amount, 'stake_currency': self.config['stake_currency'], + 'base_currency': self.exchange.get_pair_base_currency(trade.pair), 'fiat_currency': self.config.get('fiat_display_currency', None), 'amount': order.safe_amount_after_fee if fill else (order.amount or trade.amount), 'open_date': trade.open_date or datetime.utcnow(), @@ -991,7 +993,7 @@ class FreqtradeBot(LoggingMixin): current_rate = self.exchange.get_rate( trade.pair, side='entry', is_short=trade.is_short, refresh=False) - msg = { + msg: RPCCancelMsg = { 'trade_id': trade.id, 'type': RPCMessageType.ENTRY_CANCEL, 'buy_tag': trade.enter_tag, @@ -1003,7 +1005,9 @@ class FreqtradeBot(LoggingMixin): 'limit': trade.open_rate, 'order_type': order_type, 'stake_amount': trade.stake_amount, + 'open_rate': trade.open_rate, 'stake_currency': self.config['stake_currency'], + 'base_currency': self.exchange.get_pair_base_currency(trade.pair), 'fiat_currency': self.config.get('fiat_display_currency', None), 'amount': trade.amount, 'open_date': trade.open_date, @@ -1663,7 +1667,7 @@ class FreqtradeBot(LoggingMixin): amount = trade.amount gain = "profit" if profit_ratio > 0 else "loss" - msg = { + msg: RPCSellMsg = { 'type': (RPCMessageType.EXIT_FILL if fill else RPCMessageType.EXIT), 'trade_id': trade.id, @@ -1689,6 +1693,7 @@ class FreqtradeBot(LoggingMixin): 'close_date': trade.close_date or datetime.utcnow(), 'stake_amount': trade.stake_amount, 'stake_currency': self.config['stake_currency'], + 'base_currency': self.exchange.get_pair_base_currency(trade.pair), 'fiat_currency': self.config.get('fiat_display_currency'), 'sub_trade': sub_trade, 'cumulative_profit': trade.realized_profit, @@ -1719,7 +1724,7 @@ class FreqtradeBot(LoggingMixin): profit_ratio = trade.calc_profit_ratio(profit_rate) gain = "profit" if profit_ratio > 0 else "loss" - msg = { + msg: RPCSellCancelMsg = { 'type': RPCMessageType.EXIT_CANCEL, 'trade_id': trade.id, 'exchange': trade.exchange.capitalize(), @@ -1741,6 +1746,7 @@ class FreqtradeBot(LoggingMixin): 'open_date': trade.open_date, 'close_date': trade.close_date or datetime.now(timezone.utc), 'stake_currency': self.config['stake_currency'], + 'base_currency': self.exchange.get_pair_base_currency(trade.pair), 'fiat_currency': self.config.get('fiat_display_currency', None), 'reason': reason, 'sub_trade': sub_trade, @@ -1848,14 +1854,20 @@ class FreqtradeBot(LoggingMixin): self.strategy.lock_pair(pair, datetime.now(timezone.utc), reason='Auto lock') prot_trig = self.protections.stop_per_pair(pair, side=side) if prot_trig: - msg = {'type': RPCMessageType.PROTECTION_TRIGGER, } - msg.update(prot_trig.to_json()) + msg: RPCProtectionMsg = { + 'type': RPCMessageType.PROTECTION_TRIGGER, + 'base_currency': self.exchange.get_pair_base_currency(prot_trig.pair), + **prot_trig.to_json() # type: ignore + } self.rpc.send_msg(msg) prot_trig_glb = self.protections.global_stop(side=side) if prot_trig_glb: - msg = {'type': RPCMessageType.PROTECTION_TRIGGER_GLOBAL, } - msg.update(prot_trig_glb.to_json()) + msg = { + 'type': RPCMessageType.PROTECTION_TRIGGER_GLOBAL, + 'base_currency': self.exchange.get_pair_base_currency(prot_trig_glb.pair), + **prot_trig_glb.to_json() # type: ignore + } self.rpc.send_msg(msg) def apply_fee_conditional(self, trade: Trade, trade_base_currency: str, diff --git a/freqtrade/rpc/api_server/webserver.py b/freqtrade/rpc/api_server/webserver.py index b53662451..8030e303b 100644 --- a/freqtrade/rpc/api_server/webserver.py +++ b/freqtrade/rpc/api_server/webserver.py @@ -13,6 +13,7 @@ from freqtrade.exceptions import OperationalException from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer from freqtrade.rpc.api_server.ws.message_stream import MessageStream from freqtrade.rpc.rpc import RPC, RPCException, RPCHandler +from freqtrade.rpc.rpc_types import RPCSendMsg logger = logging.getLogger(__name__) @@ -108,7 +109,7 @@ class ApiServer(RPCHandler): cls._has_rpc = False cls._rpc = None - def send_msg(self, msg: Dict[str, Any]) -> None: + def send_msg(self, msg: RPCSendMsg) -> None: """ Publish the message to the message stream """ diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index c6a6f5cae..2b5eb107c 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -30,6 +30,7 @@ from freqtrade.persistence import Order, PairLocks, Trade from freqtrade.persistence.models import PairLock from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.rpc.fiat_convert import CryptoToFiatConverter +from freqtrade.rpc.rpc_types import RPCSendMsg from freqtrade.wallets import PositionWallet, Wallet @@ -79,7 +80,7 @@ class RPCHandler: """ Cleanup pending module resources """ @abstractmethod - def send_msg(self, msg: Dict[str, str]) -> None: + def send_msg(self, msg: RPCSendMsg) -> None: """ Sends a message to all registered rpc modules """ diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index c4d4fa2dd..1972ad6e5 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -3,11 +3,12 @@ This module contains class to manage RPC communications (Telegram, API, ...) """ import logging from collections import deque -from typing import Any, Dict, List +from typing import List from freqtrade.constants import Config from freqtrade.enums import NO_ECHO_MESSAGES, RPCMessageType from freqtrade.rpc import RPC, RPCHandler +from freqtrade.rpc.rpc_types import RPCSendMsg logger = logging.getLogger(__name__) @@ -58,7 +59,7 @@ class RPCManager: mod.cleanup() del mod - def send_msg(self, msg: Dict[str, Any]) -> None: + def send_msg(self, msg: RPCSendMsg) -> None: """ Send given message to all registered rpc modules. A message consists of one or more key value pairs of strings. @@ -69,10 +70,6 @@ class RPCManager: """ if msg.get('type') not in NO_ECHO_MESSAGES: logger.info('Sending rpc message: %s', msg) - if 'pair' in msg: - msg.update({ - 'base_currency': self._rpc._freqtrade.exchange.get_pair_base_currency(msg['pair']) - }) for mod in self.registered_modules: logger.debug('Forwarding message to rpc.%s', mod.name) try: diff --git a/freqtrade/rpc/rpc_types.py b/freqtrade/rpc/rpc_types.py new file mode 100644 index 000000000..3277a2d6e --- /dev/null +++ b/freqtrade/rpc/rpc_types.py @@ -0,0 +1,128 @@ +from datetime import datetime +from typing import Any, List, Literal, Optional, TypedDict, Union + +from freqtrade.constants import PairWithTimeframe +from freqtrade.enums import RPCMessageType + + +class RPCSendMsgBase(TypedDict): + pass + # ty1pe: Literal[RPCMessageType] + + +class RPCStatusMsg(RPCSendMsgBase): + """Used for Status, Startup and Warning messages""" + type: Literal[RPCMessageType.STATUS, RPCMessageType.STARTUP, RPCMessageType.WARNING] + status: str + + +class RPCStrategyMsg(RPCSendMsgBase): + """Used for Status, Startup and Warning messages""" + type: Literal[RPCMessageType.STRATEGY_MSG] + msg: str + + +class RPCProtectionMsg(RPCSendMsgBase): + type: Literal[RPCMessageType.PROTECTION_TRIGGER, RPCMessageType.PROTECTION_TRIGGER_GLOBAL] + id: int + pair: str + base_currency: Optional[str] + lock_time: str + lock_timestamp: int + lock_end_time: str + lock_end_timestamp: int + reason: str + side: str + active: bool + + +class RPCWhitelistMsg(RPCSendMsgBase): + type: Literal[RPCMessageType.WHITELIST] + data: List[str] + + +class __RPCBuyMsgBase(RPCSendMsgBase): + trade_id: int + buy_tag: Optional[str] + enter_tag: Optional[str] + exchange: str + pair: str + base_currency: str + leverage: Optional[float] + direction: str + limit: float + open_rate: float + order_type: Optional[str] # TODO: why optional?? + stake_amount: float + stake_currency: str + fiat_currency: Optional[str] + amount: float + open_date: datetime + current_rate: Optional[float] + sub_trade: bool + + +class RPCBuyMsg(__RPCBuyMsgBase): + type: Literal[RPCMessageType.ENTRY, RPCMessageType.ENTRY_FILL] + + +class RPCCancelMsg(__RPCBuyMsgBase): + type: Literal[RPCMessageType.ENTRY_CANCEL] + reason: str + + +class RPCSellMsg(__RPCBuyMsgBase): + type: Literal[RPCMessageType.EXIT, RPCMessageType.EXIT_FILL] + cumulative_profit: float + gain: str # Literal["profit", "loss"] + close_rate: float + profit_amount: float + profit_ratio: float + sell_reason: Optional[str] + exit_reason: Optional[str] + close_date: datetime + # current_rate: Optional[float] + order_rate: Optional[float] + + +class RPCSellCancelMsg(__RPCBuyMsgBase): + type: Literal[RPCMessageType.EXIT_CANCEL] + reason: str + gain: str # Literal["profit", "loss"] + profit_amount: float + profit_ratio: float + sell_reason: Optional[str] + exit_reason: Optional[str] + close_date: datetime + + +class _AnalyzedDFData(TypedDict): + key: PairWithTimeframe + df: Any + la: datetime + + +class RPCAnalyzedDFMsg(RPCSendMsgBase): + """New Analyzed dataframe message""" + type: Literal[RPCMessageType.ANALYZED_DF] + data: _AnalyzedDFData + + +class RPCNewCandleMsg(RPCSendMsgBase): + """New candle ping message, issued once per new candle/pair""" + type: Literal[RPCMessageType.NEW_CANDLE] + data: PairWithTimeframe + + +RPCSendMsg = Union[ + RPCStatusMsg, + RPCStrategyMsg, + RPCProtectionMsg, + RPCWhitelistMsg, + RPCBuyMsg, + RPCCancelMsg, + RPCSellMsg, + RPCSellCancelMsg, + RPCAnalyzedDFMsg, + RPCNewCandleMsg + ] diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 962c5e058..d79d8ea76 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -30,6 +30,7 @@ from freqtrade.exceptions import OperationalException from freqtrade.misc import chunks, plural, round_coin_value from freqtrade.persistence import Trade from freqtrade.rpc import RPC, RPCException, RPCHandler +from freqtrade.rpc.rpc_types import RPCSendMsg logger = logging.getLogger(__name__) @@ -429,14 +430,14 @@ class Telegram(RPCHandler): return None return message - def send_msg(self, msg: Dict[str, Any]) -> None: + def send_msg(self, msg: RPCSendMsg) -> None: """ Send a message to telegram channel """ default_noti = 'on' msg_type = msg['type'] noti = '' - if msg_type == RPCMessageType.EXIT: + if msg['type'] == RPCMessageType.EXIT: sell_noti = self._config['telegram'] \ .get('notification_settings', {}).get(str(msg_type), {}) # For backward compatibility sell still can be string @@ -453,7 +454,7 @@ class Telegram(RPCHandler): # Notification disabled return - message = self.compose_message(deepcopy(msg), msg_type) + message = self.compose_message(deepcopy(msg), msg_type) # type: ignore if message: self._send_msg(message, disable_notification=(noti == 'silent')) diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index 118ebed88..14b881126 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -10,6 +10,7 @@ from requests import RequestException, post from freqtrade.constants import Config from freqtrade.enums import RPCMessageType from freqtrade.rpc import RPC, RPCHandler +from freqtrade.rpc.rpc_types import RPCSendMsg logger = logging.getLogger(__name__) @@ -41,7 +42,7 @@ class Webhook(RPCHandler): """ pass - def _get_value_dict(self, msg: Dict[str, Any]) -> Optional[Dict[str, Any]]: + def _get_value_dict(self, msg: RPCSendMsg) -> Optional[Dict[str, Any]]: whconfig = self._config['webhook'] # Deprecated 2022.10 - only keep generic method. if msg['type'] in [RPCMessageType.ENTRY]: @@ -75,7 +76,7 @@ class Webhook(RPCHandler): return None return valuedict - def send_msg(self, msg: Dict[str, Any]) -> None: + def send_msg(self, msg: RPCSendMsg) -> None: """ Send a message to telegram channel """ try: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index ff10cd2f0..bf3cc6ab8 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3328,6 +3328,7 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_ 'profit_ratio': 0.00493809 if is_short else 0.09451372, 'stake_currency': 'USDT', 'fiat_currency': 'USD', + 'base_currency': 'ETH', 'sell_reason': ExitType.ROI.value, 'exit_reason': ExitType.ROI.value, 'open_date': ANY, @@ -3391,6 +3392,7 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd 'profit_amount': -5.65990099 if is_short else -0.00075, 'profit_ratio': -0.0945681 if is_short else -1.247e-05, 'stake_currency': 'USDT', + 'base_currency': 'ETH', 'fiat_currency': 'USD', 'sell_reason': ExitType.STOP_LOSS.value, 'exit_reason': ExitType.STOP_LOSS.value, @@ -3476,6 +3478,7 @@ def test_execute_trade_exit_custom_exit_price( 'profit_amount': pytest.approx(profit_amount), 'profit_ratio': profit_ratio, 'stake_currency': 'USDT', + 'base_currency': 'ETH', 'fiat_currency': 'USD', 'sell_reason': 'foo', 'exit_reason': 'foo', @@ -3549,6 +3552,7 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run( 'profit_ratio': -0.00501253 if is_short else -0.01493766, 'stake_currency': 'USDT', 'fiat_currency': 'USD', + 'base_currency': 'ETH', 'sell_reason': ExitType.STOP_LOSS.value, 'exit_reason': ExitType.STOP_LOSS.value, 'open_date': ANY, @@ -3813,6 +3817,7 @@ def test_execute_trade_exit_market_order( 'profit_amount': pytest.approx(profit_amount), 'profit_ratio': profit_ratio, 'stake_currency': 'USDT', + 'base_currency': 'ETH', 'fiat_currency': 'USD', 'sell_reason': ExitType.ROI.value, 'exit_reason': ExitType.ROI.value,