Files
OpenFin/backend/routers/instruments.py
OpenSquared aa81598278 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>
2026-06-24 22:48:10 +02:00

88 lines
2.8 KiB
Python

"""
Instrument Dashboard Router.
Exposes per-instrument snapshot (price, indicators, regime, trend, events) and AI narrative.
"""
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel
from typing import List, Dict, Any, Optional
from services.instrument_service import (
get_all_instruments,
get_instrument,
get_snapshot,
get_narrative,
update_instrument_drivers,
)
class DriverUpdate(BaseModel):
drivers: List[Dict[str, Any]]
router = APIRouter(prefix="/api/instruments", tags=["instruments"])
@router.get("", response_model=List[Dict[str, Any]])
def list_instruments() -> List[Dict[str, Any]]:
"""
Return all instrument configurations (no price data).
"""
return get_all_instruments()
@router.get("/{instrument_id}/snapshot")
async def instrument_snapshot(
instrument_id: str,
period: str = Query(default="1y", description="yfinance period string (e.g. 1y, 6mo, 3mo)"),
interval: str = Query(default="1d", description="yfinance interval string (e.g. 1d, 1wk)"),
) -> Dict[str, Any]:
"""
Full snapshot for a single instrument: price data, indicators, regime, trend, events.
"""
config = get_instrument(instrument_id)
if not config:
raise HTTPException(status_code=404, detail=f"Instrument '{instrument_id}' not found")
snapshot = await get_snapshot(instrument_id, period=period, interval=interval)
if "error" in snapshot:
raise HTTPException(status_code=500, detail=snapshot["error"])
return snapshot
@router.post("/{instrument_id}/narrative")
async def generate_narrative(
instrument_id: str,
force: bool = Query(default=False, description="Force regeneration, bypass cache"),
) -> Dict[str, Any]:
"""
Generate (or return cached) a French AI narrative for the instrument.
"""
config = get_instrument(instrument_id)
if not config:
raise HTTPException(status_code=404, detail=f"Instrument '{instrument_id}' not found")
narrative = await get_narrative(instrument_id, force=force)
return {
"instrument_id": instrument_id.upper(),
"instrument_name": config.get("name", instrument_id),
"narrative": narrative,
}
@router.put("/{instrument_id}/drivers")
def update_drivers(instrument_id: str, body: DriverUpdate) -> Dict[str, Any]:
"""
Persist updated drivers (label, weight, keywords) for an instrument to instruments.json.
"""
config = get_instrument(instrument_id)
if not config:
raise HTTPException(status_code=404, detail=f"Instrument '{instrument_id}' not found")
try:
update_instrument_drivers(instrument_id, body.drivers)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
return {"ok": True, "instrument_id": instrument_id.upper(), "drivers_count": len(body.drivers)}