feat: driver-based timeline strip, regime signal metrics, drivers editor

- instruments.json: add keywords array to every driver across 20 instruments
  (Fed, BCE, BOJ, OPEC, CPI, AI, EIA, etc.) for event-to-driver matching
- instrument_service.py: add update_instrument_drivers() persisting changes to JSON
  and refreshing in-memory cache
- instruments.py: add PUT /api/instruments/{id}/drivers endpoint (DriverUpdate model)
- InstrumentDashboard:
  * RegimeCard: replace regime score bars with 6-metric signal grid
    (MA50/MA200 position, MA50 slope, MA200 slope, momentum 20j, dist MA200, ATR vol ratio)
    with colour-coded values and contextual sub-labels (Golden cross, Surextension, etc.)
  * EventTimelineStrip: rows now keyed by top-4 instrument drivers (by weight)
    instead of LT/MT/CT; events matched via case-insensitive keyword scan against
    title + description + category; fallback dashed line when no events match
  * DriversPanel: inline edit panel (toggle via Drivers button in header);
    edit label, weight, keywords (comma-separated) per driver; add/remove drivers;
    saves via PUT /api/instruments/{id}/drivers; optimistic local state update

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-24 22:48:10 +02:00
parent 418d03254d
commit aa81598278
4 changed files with 635 additions and 579 deletions

View File

@@ -42,6 +42,33 @@ def get_instrument(instrument_id: str) -> Optional[Dict]:
return _configs.get(instrument_id.upper())
def update_instrument_drivers(instrument_id: str, drivers: List[Dict]) -> None:
"""Persist updated drivers to instruments.json and refresh in-memory config."""
global _configs
if _configs is None:
_load_configs()
uid = instrument_id.upper()
if uid not in _configs:
raise ValueError(f"Instrument {uid} not found")
# Load raw JSON, update the matching instrument, save back
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
raw = json.load(f)
for inst in raw["instruments"]:
if inst["id"] == uid:
inst["drivers"] = drivers
break
with open(CONFIG_PATH, "w", encoding="utf-8") as f:
json.dump(raw, f, ensure_ascii=False, indent=2)
# Refresh in-memory cache
_configs[uid]["drivers"] = drivers
logger.info(f"[instrument_service] Updated drivers for {uid} ({len(drivers)} drivers)")
# ── DataFrame helpers ──────────────────────────────────────────────────────────
def _ohlcv_to_df(records: List[Dict]) -> pd.DataFrame: