Add external chart adapter with fallbacks
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import os
|
||||
import unittest
|
||||
from datetime import datetime, timezone
|
||||
|
||||
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://user:pass@localhost:5432/ai_ict_test")
|
||||
|
||||
from src.services.market_state.external_chart_provider import BinanceChartReferenceProvider
|
||||
|
||||
|
||||
class ExternalChartProviderTests(unittest.TestCase):
|
||||
def test_provider_builds_live_snapshot_from_payload(self) -> None:
|
||||
provider = BinanceChartReferenceProvider(repository=_ChartSnapshotRepo())
|
||||
|
||||
snapshot = provider.fetch_snapshot(
|
||||
instrument_id=1,
|
||||
timeframe="1h",
|
||||
symbol_hint="BTC-USDT",
|
||||
now=datetime(2026, 5, 14, 4, 0, tzinfo=timezone.utc),
|
||||
payload=[
|
||||
[0, "100", "110", "90", "105", "10", 1715659200000],
|
||||
[1, "105", "115", "95", "111", "12", 1715662800000],
|
||||
],
|
||||
)
|
||||
|
||||
self.assertEqual(snapshot["status"], "active")
|
||||
self.assertEqual(snapshot["cache_state"], "fresh")
|
||||
self.assertEqual(snapshot["symbol"], "BTC-USDT")
|
||||
self.assertEqual(snapshot["interval"], "1h")
|
||||
self.assertIn("binance.com", snapshot["chart_url"])
|
||||
self.assertEqual(snapshot["reference_price"], 111.0)
|
||||
|
||||
def test_provider_uses_cached_snapshot_when_fetch_fails(self) -> None:
|
||||
repository = _ChartSnapshotRepo(
|
||||
{
|
||||
"provider_name": "binance_public_chart_reference:BTCUSDT:1h",
|
||||
"fetched_ts": "2026-05-14T02:00:00+00:00",
|
||||
"event_count": 1,
|
||||
"code": "active",
|
||||
"active_events": [
|
||||
{
|
||||
"symbol": "BTC-USDT",
|
||||
"interval": "1h",
|
||||
"chart_url": "https://www.binance.com/en/trade/BTC_USDT?type=spot",
|
||||
"reference_price": 111.0,
|
||||
"reference_ts": "2026-05-14T02:00:00+00:00",
|
||||
}
|
||||
],
|
||||
"raw_payload": {"data": []},
|
||||
}
|
||||
)
|
||||
provider = BinanceChartReferenceProvider(repository=repository, cache_max_age_seconds=21600)
|
||||
provider._fetch_payload = lambda **kwargs: (_ for _ in ()).throw(RuntimeError("api down")) # type: ignore[attr-defined]
|
||||
|
||||
snapshot = provider.fetch_snapshot(
|
||||
instrument_id=1,
|
||||
timeframe="1h",
|
||||
symbol_hint="BTC-USDT",
|
||||
now=datetime(2026, 5, 14, 2, 30, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
self.assertEqual(snapshot["status"], "active")
|
||||
self.assertEqual(snapshot["cache_state"], "cached_fallback")
|
||||
self.assertIn("api down", snapshot["warning"])
|
||||
|
||||
def test_provider_returns_empty_fallback_without_cache(self) -> None:
|
||||
provider = BinanceChartReferenceProvider(repository=_ChartSnapshotRepo())
|
||||
provider._fetch_payload = lambda **kwargs: (_ for _ in ()).throw(RuntimeError("api down")) # type: ignore[attr-defined]
|
||||
|
||||
snapshot = provider.fetch_snapshot(
|
||||
instrument_id=1,
|
||||
timeframe="1h",
|
||||
symbol_hint="BTC-USDT",
|
||||
now=datetime(2026, 5, 14, 2, 30, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
self.assertEqual(snapshot["status"], "idle")
|
||||
self.assertEqual(snapshot["cache_state"], "empty_fallback")
|
||||
self.assertIn("binance.com", snapshot["chart_url"])
|
||||
self.assertIsNone(snapshot["error"])
|
||||
|
||||
|
||||
class _ChartSnapshotRepo:
|
||||
def __init__(self, row: dict | None = None) -> None:
|
||||
self.row = row or {}
|
||||
|
||||
def fetch_instrument_by_id(self, instrument_id: int):
|
||||
return {"id": instrument_id, "symbol": "BTC-USDT", "active": True}
|
||||
|
||||
def fetch_news_provider_snapshot(self, provider_name: str):
|
||||
if self.row and provider_name == self.row.get("provider_name"):
|
||||
return dict(self.row)
|
||||
return None
|
||||
|
||||
def upsert_news_provider_snapshot(self, **kwargs):
|
||||
self.row = dict(kwargs)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -98,6 +98,7 @@ class OperatorWorkspaceServiceTests(unittest.TestCase):
|
||||
research_council_service=_ResearchCouncilSummaryService(),
|
||||
exchange_status_provider=_ExchangeStatusProvider(),
|
||||
announcements_provider=_AnnouncementsProvider(),
|
||||
external_chart_provider=_ExternalChartProvider(),
|
||||
decision_assistant_repository=_DecisionAssistantRepo(),
|
||||
)
|
||||
|
||||
@@ -589,9 +590,9 @@ class OperatorWorkspaceServiceTests(unittest.TestCase):
|
||||
self.assertIn("chart_stale_source_evidence", summary["external_context_summary"])
|
||||
self.assertGreaterEqual(summary["external_context_summary"]["chart_readonly_source_count"], 1)
|
||||
self.assertEqual(summary["external_context_summary"]["chart_stale_source_count"], 0)
|
||||
self.assertEqual(summary["external_context_summary"]["chart_adapter_mode"], "optional_reference")
|
||||
self.assertEqual(summary["external_context_summary"]["chart_adapter_mode"], "live_reference")
|
||||
self.assertEqual(summary["external_context_summary"]["chart_primary_mode"], "internal_review_packet_html")
|
||||
self.assertIn("optional", summary["external_context_summary"]["chart_context_prompt"].lower())
|
||||
self.assertIn("live", summary["external_context_summary"]["chart_context_prompt"].lower())
|
||||
self.assertIn("CPI", summary["external_context_summary"]["headline"])
|
||||
self.assertTrue(summary["no_trade_reason_items"])
|
||||
self.assertIn("watch_london_open", summary["next_actions"])
|
||||
@@ -860,6 +861,27 @@ class _AnnouncementsProvider:
|
||||
}
|
||||
|
||||
|
||||
class _ExternalChartProvider:
|
||||
def fetch_snapshot(self, **kwargs):
|
||||
return {
|
||||
"provider_name": "binance_public_chart_reference",
|
||||
"provider_key": "binance_public_chart_reference:BTCUSDT:5m",
|
||||
"status": "active",
|
||||
"freshness_code": "provider_fresh",
|
||||
"cache_state": "fresh",
|
||||
"symbol": "BTC-USDT",
|
||||
"api_symbol": "BTCUSDT",
|
||||
"interval": "5m",
|
||||
"chart_url": "https://www.binance.com/en/trade/BTC_USDT?type=spot",
|
||||
"candle_count": 2,
|
||||
"reference_price": 111.0,
|
||||
"reference_ts": "2026-05-12T02:00:00+00:00",
|
||||
"error": None,
|
||||
"warning": None,
|
||||
"raw_payload": {"data": []},
|
||||
}
|
||||
|
||||
|
||||
class _DecisionAssistantRepo:
|
||||
def fetch_preference(self, **kwargs):
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user