Merge pull request #7135 from freqtrade/rpc/sendmsg

Strategy allow rpc messages
This commit is contained in:
Matthias 2022-07-30 16:15:00 +02:00 committed by GitHub
commit fc31c890e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 123 additions and 4 deletions

View File

@ -734,6 +734,23 @@ if self.dp:
!!! Warning "Warning about backtesting" !!! Warning "Warning about backtesting"
This method will always return up-to-date values - so usage during backtesting / hyperopt will lead to wrong results. This method will always return up-to-date values - so usage during backtesting / hyperopt will lead to wrong results.
### Send Notification
The dataprovider `.send_msg()` function allows you to send custom notifications from your strategy.
Identical notifications will only be sent once per candle, unless the 2nd argument (`always_send`) is set to True.
``` python
self.dp.send_msg(f"{metadata['pair']} just got hot!")
# Force send this notification, avoid caching (Please read warning below!)
self.dp.send_msg(f"{metadata['pair']} just got hot!", always_send=True)
```
Notifications will only be sent in trading modes (Live/Dry-run) - so this method can be called without conditions for backtesting.
!!! Warning "Spamming"
You can spam yourself pretty good by setting `always_send=True` in this method. Use this with great care and only in conditions you know will not happen throughout a candle to avoid a message every 5 seconds.
### Complete Data-provider sample ### Complete Data-provider sample
```python ```python

View File

@ -98,6 +98,7 @@ Example configuration showing the different settings:
"exit_fill": "off", "exit_fill": "off",
"protection_trigger": "off", "protection_trigger": "off",
"protection_trigger_global": "on", "protection_trigger_global": "on",
"strategy_msg": "off",
"show_candle": "off" "show_candle": "off"
}, },
"reload": true, "reload": true,
@ -109,7 +110,8 @@ Example configuration showing the different settings:
`exit` notifications are sent when the order is placed, while `exit_fill` notifications are sent when the order is filled on the exchange. `exit` notifications are sent when the order is placed, while `exit_fill` notifications are sent when the order is filled on the exchange.
`*_fill` notifications are off by default and must be explicitly enabled. `*_fill` notifications are off by default and must be explicitly enabled.
`protection_trigger` notifications are sent when a protection triggers and `protection_trigger_global` notifications trigger when global protections are triggered. `protection_trigger` notifications are sent when a protection triggers and `protection_trigger_global` notifications trigger when global protections are triggered.
`show_candle` - show candle values as part of entry/exit messages. Only possible value is "ohlc". `strategy_msg` - Receive notifications from the strategy, sent via `self.dp.send_msg()` from the strategy [more details](strategy-customization.md#send-notification).
`show_candle` - show candle values as part of entry/exit messages. Only possible values are `"ohlc"` or `"off"`.
`balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown. `balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown.
`reload` allows you to disable reload-buttons on selected messages. `reload` allows you to disable reload-buttons on selected messages.

View File

@ -317,6 +317,10 @@ CONF_SCHEMA = {
'type': 'string', 'type': 'string',
'enum': ['off', 'ohlc'], 'enum': ['off', 'ohlc'],
}, },
'strategy_msg': {
'type': 'string',
'enum': TELEGRAM_SETTING_OPTIONS,
},
} }
}, },
'reload': {'type': 'boolean'}, 'reload': {'type': 'boolean'},

View File

@ -5,12 +5,14 @@ including ticker and orderbook data, live and historical candle (OHLCV) data
Common Interface for bot and strategy to access data. Common Interface for bot and strategy to access data.
""" """
import logging import logging
from collections import deque
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
from pandas import DataFrame from pandas import DataFrame
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.configuration.PeriodicCache import PeriodicCache
from freqtrade.constants import ListPairsWithTimeframes, PairWithTimeframe from freqtrade.constants import ListPairsWithTimeframes, PairWithTimeframe
from freqtrade.data.history import load_pair_history from freqtrade.data.history import load_pair_history
from freqtrade.enums import CandleType, RunMode from freqtrade.enums import CandleType, RunMode
@ -33,6 +35,10 @@ class DataProvider:
self.__cached_pairs: Dict[PairWithTimeframe, Tuple[DataFrame, datetime]] = {} self.__cached_pairs: Dict[PairWithTimeframe, Tuple[DataFrame, datetime]] = {}
self.__slice_index: Optional[int] = None self.__slice_index: Optional[int] = None
self.__cached_pairs_backtesting: Dict[PairWithTimeframe, DataFrame] = {} self.__cached_pairs_backtesting: Dict[PairWithTimeframe, DataFrame] = {}
self._msg_queue: deque = deque()
self.__msg_cache = PeriodicCache(
maxsize=1000, ttl=timeframe_to_seconds(self._config.get('timeframe', '1h')))
def _set_dataframe_max_index(self, limit_index: int): def _set_dataframe_max_index(self, limit_index: int):
""" """
@ -265,3 +271,20 @@ class DataProvider:
if self._exchange is None: if self._exchange is None:
raise OperationalException(NO_EXCHANGE_EXCEPTION) raise OperationalException(NO_EXCHANGE_EXCEPTION)
return self._exchange.fetch_l2_order_book(pair, maximum) return self._exchange.fetch_l2_order_book(pair, maximum)
def send_msg(self, message: str, *, always_send: bool = False) -> None:
"""
Send custom RPC Notifications from your bot.
Will not send any bot in modes other than Dry-run or Live.
:param message: Message to be sent. Must be below 4096.
:param always_send: If False, will send the message only once per candle, and surpress
identical messages.
Careful as this can end up spaming your chat.
Defaults to False
"""
if self.runmode not in (RunMode.DRY_RUN, RunMode.LIVE):
return
if always_send or message not in self.__msg_cache:
self._msg_queue.append(message)
self.__msg_cache[message] = True

View File

@ -17,6 +17,8 @@ class RPCMessageType(Enum):
PROTECTION_TRIGGER = 'protection_trigger' PROTECTION_TRIGGER = 'protection_trigger'
PROTECTION_TRIGGER_GLOBAL = 'protection_trigger_global' PROTECTION_TRIGGER_GLOBAL = 'protection_trigger_global'
STRATEGY_MSG = 'strategy_msg'
def __repr__(self): def __repr__(self):
return self.value return self.value

View File

@ -214,6 +214,7 @@ class FreqtradeBot(LoggingMixin):
if self.trading_mode == TradingMode.FUTURES: if self.trading_mode == TradingMode.FUTURES:
self._schedule.run_pending() self._schedule.run_pending()
Trade.commit() Trade.commit()
self.rpc.process_msg_queue(self.dataprovider._msg_queue)
self.last_process = datetime.now(timezone.utc) self.last_process = datetime.now(timezone.utc)
def process_stopped(self) -> None: def process_stopped(self) -> None:

View File

@ -2,6 +2,7 @@
This module contains class to manage RPC communications (Telegram, API, ...) This module contains class to manage RPC communications (Telegram, API, ...)
""" """
import logging import logging
from collections import deque
from typing import Any, Dict, List from typing import Any, Dict, List
from freqtrade.enums import RPCMessageType from freqtrade.enums import RPCMessageType
@ -77,6 +78,17 @@ class RPCManager:
except NotImplementedError: except NotImplementedError:
logger.error(f"Message type '{msg['type']}' not implemented by handler {mod.name}.") logger.error(f"Message type '{msg['type']}' not implemented by handler {mod.name}.")
def process_msg_queue(self, queue: deque) -> None:
"""
Process all messages in the queue.
"""
while queue:
msg = queue.popleft()
self.send_msg({
'type': RPCMessageType.STRATEGY_MSG,
'msg': msg,
})
def startup_messages(self, config: Dict[str, Any], pairlist, protections) -> None: def startup_messages(self, config: Dict[str, Any], pairlist, protections) -> None:
if config['dry_run']: if config['dry_run']:
self.send_msg({ self.send_msg({

View File

@ -376,7 +376,8 @@ class Telegram(RPCHandler):
elif msg_type == RPCMessageType.STARTUP: elif msg_type == RPCMessageType.STARTUP:
message = f"{msg['status']}" message = f"{msg['status']}"
elif msg_type == RPCMessageType.STRATEGY_MSG:
message = f"{msg['msg']}"
else: else:
raise NotImplementedError(f"Unknown message type: {msg_type}") raise NotImplementedError(f"Unknown message type: {msg_type}")
return message return message

View File

@ -311,3 +311,27 @@ def test_no_exchange_mode(default_conf):
with pytest.raises(OperationalException, match=message): with pytest.raises(OperationalException, match=message):
dp.available_pairs() dp.available_pairs()
def test_dp_send_msg(default_conf):
default_conf["runmode"] = RunMode.DRY_RUN
default_conf["timeframe"] = '1h'
dp = DataProvider(default_conf, None)
msg = 'Test message'
dp.send_msg(msg)
assert msg in dp._msg_queue
dp._msg_queue.pop()
assert msg not in dp._msg_queue
# Message is not resent due to caching
dp.send_msg(msg)
assert msg not in dp._msg_queue
dp.send_msg(msg, always_send=True)
assert msg in dp._msg_queue
default_conf["runmode"] = RunMode.BACKTEST
dp = DataProvider(default_conf, None)
dp.send_msg(msg, always_send=True)
assert msg not in dp._msg_queue

View File

@ -1,6 +1,7 @@
# pragma pylint: disable=missing-docstring, C0103 # pragma pylint: disable=missing-docstring, C0103
import logging import logging
import time import time
from collections import deque
from unittest.mock import MagicMock from unittest.mock import MagicMock
from freqtrade.enums import RPCMessageType from freqtrade.enums import RPCMessageType
@ -81,9 +82,25 @@ def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None:
assert telegram_mock.call_count == 0 assert telegram_mock.call_count == 0
def test_process_msg_queue(mocker, default_conf, caplog) -> None:
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg')
mocker.patch('freqtrade.rpc.telegram.Telegram._init')
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
rpc_manager = RPCManager(freqtradebot)
queue = deque()
queue.append('Test message')
queue.append('Test message 2')
rpc_manager.process_msg_queue(queue)
assert log_has("Sending rpc message: {'type': strategy_msg, 'msg': 'Test message'}", caplog)
assert log_has("Sending rpc message: {'type': strategy_msg, 'msg': 'Test message 2'}", caplog)
assert telegram_mock.call_count == 2
def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None: def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None:
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg')
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init')
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
rpc_manager = RPCManager(freqtradebot) rpc_manager = RPCManager(freqtradebot)

View File

@ -1994,6 +1994,16 @@ def test_startup_notification(default_conf, mocker) -> None:
assert msg_mock.call_args[0][0] == '*Custom:* `Hello World`' assert msg_mock.call_args[0][0] == '*Custom:* `Hello World`'
def test_send_msg_strategy_msg_notification(default_conf, mocker) -> None:
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
telegram.send_msg({
'type': RPCMessageType.STRATEGY_MSG,
'msg': 'hello world, Test msg'
})
assert msg_mock.call_args[0][0] == 'hello world, Test msg'
def test_send_msg_unknown_type(default_conf, mocker) -> None: def test_send_msg_unknown_type(default_conf, mocker) -> None:
telegram, _, _ = get_telegram_testobject(mocker, default_conf) telegram, _, _ = get_telegram_testobject(mocker, default_conf)
with pytest.raises(NotImplementedError, match=r'Unknown message type: None'): with pytest.raises(NotImplementedError, match=r'Unknown message type: None'):

View File

@ -68,6 +68,12 @@ def test_process_stopped(mocker, default_conf_usdt) -> None:
assert coo_mock.call_count == 1 assert coo_mock.call_count == 1
def test_process_calls_sendmsg(mocker, default_conf_usdt) -> None:
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
freqtrade.process()
assert freqtrade.rpc.process_msg_queue.call_count == 1
def test_bot_cleanup(mocker, default_conf_usdt, caplog) -> None: def test_bot_cleanup(mocker, default_conf_usdt, caplog) -> None:
mock_cleanup = mocker.patch('freqtrade.freqtradebot.cleanup_db') mock_cleanup = mocker.patch('freqtrade.freqtradebot.cleanup_db')
coo_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cancel_all_open_orders') coo_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cancel_all_open_orders')