Merge pull request #7135 from freqtrade/rpc/sendmsg
Strategy allow rpc messages
This commit is contained in:
commit
fc31c890e3
@ -734,6 +734,23 @@ if self.dp:
|
||||
!!! Warning "Warning about backtesting"
|
||||
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
|
||||
|
||||
```python
|
||||
|
@ -98,6 +98,7 @@ Example configuration showing the different settings:
|
||||
"exit_fill": "off",
|
||||
"protection_trigger": "off",
|
||||
"protection_trigger_global": "on",
|
||||
"strategy_msg": "off",
|
||||
"show_candle": "off"
|
||||
},
|
||||
"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.
|
||||
`*_fill` notifications are off by default and must be explicitly enabled.
|
||||
`protection_trigger` notifications are sent when a protection triggers and `protection_trigger_global` notifications trigger when global protections are triggered.
|
||||
`show_candle` - show candle values as part of entry/exit messages. Only possible value is "ohlc".
|
||||
`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.
|
||||
`reload` allows you to disable reload-buttons on selected messages.
|
||||
|
@ -317,6 +317,10 @@ CONF_SCHEMA = {
|
||||
'type': 'string',
|
||||
'enum': ['off', 'ohlc'],
|
||||
},
|
||||
'strategy_msg': {
|
||||
'type': 'string',
|
||||
'enum': TELEGRAM_SETTING_OPTIONS,
|
||||
},
|
||||
}
|
||||
},
|
||||
'reload': {'type': 'boolean'},
|
||||
|
@ -5,12 +5,14 @@ including ticker and orderbook data, live and historical candle (OHLCV) data
|
||||
Common Interface for bot and strategy to access data.
|
||||
"""
|
||||
import logging
|
||||
from collections import deque
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.configuration.PeriodicCache import PeriodicCache
|
||||
from freqtrade.constants import ListPairsWithTimeframes, PairWithTimeframe
|
||||
from freqtrade.data.history import load_pair_history
|
||||
from freqtrade.enums import CandleType, RunMode
|
||||
@ -33,6 +35,10 @@ class DataProvider:
|
||||
self.__cached_pairs: Dict[PairWithTimeframe, Tuple[DataFrame, datetime]] = {}
|
||||
self.__slice_index: Optional[int] = None
|
||||
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):
|
||||
"""
|
||||
@ -265,3 +271,20 @@ class DataProvider:
|
||||
if self._exchange is None:
|
||||
raise OperationalException(NO_EXCHANGE_EXCEPTION)
|
||||
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
|
||||
|
@ -17,6 +17,8 @@ class RPCMessageType(Enum):
|
||||
PROTECTION_TRIGGER = 'protection_trigger'
|
||||
PROTECTION_TRIGGER_GLOBAL = 'protection_trigger_global'
|
||||
|
||||
STRATEGY_MSG = 'strategy_msg'
|
||||
|
||||
def __repr__(self):
|
||||
return self.value
|
||||
|
||||
|
@ -214,6 +214,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
if self.trading_mode == TradingMode.FUTURES:
|
||||
self._schedule.run_pending()
|
||||
Trade.commit()
|
||||
self.rpc.process_msg_queue(self.dataprovider._msg_queue)
|
||||
self.last_process = datetime.now(timezone.utc)
|
||||
|
||||
def process_stopped(self) -> None:
|
||||
|
@ -2,6 +2,7 @@
|
||||
This module contains class to manage RPC communications (Telegram, API, ...)
|
||||
"""
|
||||
import logging
|
||||
from collections import deque
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from freqtrade.enums import RPCMessageType
|
||||
@ -77,6 +78,17 @@ class RPCManager:
|
||||
except NotImplementedError:
|
||||
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:
|
||||
if config['dry_run']:
|
||||
self.send_msg({
|
||||
|
@ -376,7 +376,8 @@ class Telegram(RPCHandler):
|
||||
|
||||
elif msg_type == RPCMessageType.STARTUP:
|
||||
message = f"{msg['status']}"
|
||||
|
||||
elif msg_type == RPCMessageType.STRATEGY_MSG:
|
||||
message = f"{msg['msg']}"
|
||||
else:
|
||||
raise NotImplementedError(f"Unknown message type: {msg_type}")
|
||||
return message
|
||||
|
@ -311,3 +311,27 @@ def test_no_exchange_mode(default_conf):
|
||||
|
||||
with pytest.raises(OperationalException, match=message):
|
||||
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
|
||||
|
@ -1,6 +1,7 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103
|
||||
import logging
|
||||
import time
|
||||
from collections import deque
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
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
|
||||
|
||||
|
||||
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:
|
||||
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||
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)
|
||||
|
@ -1994,6 +1994,16 @@ def test_startup_notification(default_conf, mocker) -> None:
|
||||
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:
|
||||
telegram, _, _ = get_telegram_testobject(mocker, default_conf)
|
||||
with pytest.raises(NotImplementedError, match=r'Unknown message type: None'):
|
||||
|
@ -68,6 +68,12 @@ def test_process_stopped(mocker, default_conf_usdt) -> None:
|
||||
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:
|
||||
mock_cleanup = mocker.patch('freqtrade.freqtradebot.cleanup_db')
|
||||
coo_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cancel_all_open_orders')
|
||||
|
Loading…
Reference in New Issue
Block a user