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"
|
!!! 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
|
||||||
|
@ -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.
|
||||||
|
@ -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'},
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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({
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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'):
|
||||||
|
@ -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')
|
||||||
|
Loading…
Reference in New Issue
Block a user