Tiingo WebSocket streaming: re-enable the level-breach monitor
The monitor has been dead since the Oanda feed switch (Oanda's provider does not stream). New config.json scalar data_providers.streaming selects a tick-stream provider independent of the candle feed; telegram_bot resolves it via _get_streaming_dp() (separate instance, candle cache stays on the active provider) and falls back to the active provider when unset — behavior is byte-identical to before when the key is absent. TiingoDataProvider grows the two sanctioned streaming overrides: supports_streaming=True and iter_ticks() on wss://api.tiingo.com/fx (mid price only — the no-spread rule extends to the stream). Two deliberate divergences from the TD reference implementation, both because Tiingo authenticates inside the socket: server "E" messages raise TiingoDataError (a swallowed auth error would end the generator cleanly and reconnect-hammer with zero backoff), and on_connect fires on the first received message rather than after the subscribe send. Field order of quote frames was live-verified: ask is data[7], not data[6] as the docs table suggests (data[6] is askSize). make_data_provider(config, name=None) now takes an explicit provider name; keyless streaming providers disable the monitor with one warning instead of a traceback-per-minute loop. Tests 266 → 273 (WS parser, ticker round-trip, streaming config scalar, explicit-name dispatch, streaming-dp resolution, monitor auto-disable). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nsd8HcrV67GbCZXxZEn1Ma
This commit is contained in:
+14
-1
@@ -106,6 +106,7 @@ class AppConfig:
|
||||
telegram_channel_id: str = "" # "" = channel feed disabled
|
||||
alert_min_quality: str = "C"
|
||||
active_provider: str = "twelve_data"
|
||||
streaming_provider: Optional[str] = None # None = active provider
|
||||
provider_settings: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
||||
scan_pairs: List[str] = field(default_factory=lambda: [XAUUSD])
|
||||
scan_timeframes: List[Timeframe] = field(default_factory=lambda: [
|
||||
@@ -162,7 +163,8 @@ def load_app_config(path: str = CONFIG_PATH) -> AppConfig:
|
||||
app.active_provider = str(dp.get("active", app.active_provider))
|
||||
app.provider_settings = {}
|
||||
for name, block in dp.items():
|
||||
if name == "active":
|
||||
# "active" and "streaming" are reserved scalars, not blocks.
|
||||
if name in ("active", "streaming"):
|
||||
continue
|
||||
if not isinstance(block, dict):
|
||||
raise AppConfigError(
|
||||
@@ -172,6 +174,17 @@ def load_app_config(path: str = CONFIG_PATH) -> AppConfig:
|
||||
raise AppConfigError(
|
||||
f'config.json: data_providers.active "{app.active_provider}" '
|
||||
"has no matching provider block")
|
||||
streaming = dp.get("streaming")
|
||||
if streaming is not None:
|
||||
if not isinstance(streaming, str):
|
||||
raise AppConfigError(
|
||||
'config.json: data_providers.streaming must be a string '
|
||||
"(a provider name)")
|
||||
if streaming not in app.provider_settings:
|
||||
raise AppConfigError(
|
||||
f'config.json: data_providers.streaming "{streaming}" '
|
||||
"has no matching provider block")
|
||||
app.streaming_provider = streaming
|
||||
|
||||
scanning = raw.get("scanning", {})
|
||||
_check_keys(scanning, _KNOWN_SCANNING, '"scanning"')
|
||||
|
||||
Reference in New Issue
Block a user