feat: cockpit
This commit is contained in:
@@ -28,6 +28,28 @@ _configs: Optional[Dict[str, Any]] = None
|
||||
_narrative_cache: Dict[Tuple[str, str], str] = {}
|
||||
|
||||
|
||||
# "Quick add from Watchlist" — instruments_watchlist's asset_class vocabulary (see
|
||||
# routers/instruments_watchlist.py's _QUOTE_TYPE_TO_ASSET_CLASS) mapped onto instruments.json's
|
||||
# own category vocabulary (CATEGORY_ORDER in frontend/src/pages/InstrumentDashboard.tsx).
|
||||
# Approximate on purpose — this only decides which group heading a quick-added instrument
|
||||
# is filed under in the picker, it doesn't affect chart/Saxo link/drivers.
|
||||
_ASSET_CLASS_TO_CATEGORY = {
|
||||
"forex": "fx", "energy": "energy", "indices": "equity_index",
|
||||
"etfs": "stock", "equities": "stock", "unknown": "stock",
|
||||
}
|
||||
|
||||
# Generic chart/driver defaults for a quick-added instrument — instruments.json entries are
|
||||
# hand-curated (custom drivers, ai_context, related_assets...); a quick add has none of that
|
||||
# yet, just enough to render a chart and price. The user can flesh it out later via the
|
||||
# existing Drivers editor (update_instrument_drivers), same as any other instrument.
|
||||
_QUICK_ADD_DEFAULTS = {
|
||||
"chart": {"ma_periods": [20, 50, 200], "bollinger_period": 20, "bollinger_std": 2, "show_volume": True},
|
||||
"drivers": [], "regime_labels": [], "event_keywords": [],
|
||||
"related_assets": [], "correlation_instruments": [], "ai_context": "",
|
||||
"currency": "USD", "description": "",
|
||||
}
|
||||
|
||||
|
||||
def _load_configs() -> None:
|
||||
"""instruments.json is the static, git-tracked catalog (baked into the Docker image —
|
||||
reset to its git contents on every deploy rebuild). Saxo link + drivers are
|
||||
@@ -40,7 +62,8 @@ def _load_configs() -> None:
|
||||
_configs = {inst["id"]: inst for inst in data["instruments"]}
|
||||
|
||||
from services.database import get_instrument_overrides
|
||||
for uid, override in get_instrument_overrides().items():
|
||||
overrides = get_instrument_overrides()
|
||||
for uid, override in overrides.items():
|
||||
if uid not in _configs:
|
||||
continue
|
||||
# saxo_quote_symbol: the override row is the sole source of truth once it exists —
|
||||
@@ -52,6 +75,22 @@ def _load_configs() -> None:
|
||||
if override.get("drivers") is not None:
|
||||
_configs[uid]["drivers"] = override["drivers"]
|
||||
|
||||
# "Quick add from Watchlist" — an override row with `name` set but no matching
|
||||
# instruments.json entry stands in for a whole catalog entry (see
|
||||
# set_instrument_override_quick_add / quick_add_instrument_from_watchlist below).
|
||||
for uid, override in overrides.items():
|
||||
if uid in _configs or not override.get("name"):
|
||||
continue
|
||||
_configs[uid] = {
|
||||
"id": uid,
|
||||
"name": override["name"],
|
||||
"yf_ticker": override.get("yf_ticker") or uid,
|
||||
"category": override.get("category") or "stock",
|
||||
"saxo_quote_symbol": override.get("saxo_quote_symbol"),
|
||||
"drivers": override.get("drivers") or [],
|
||||
**{k: v for k, v in _QUICK_ADD_DEFAULTS.items() if k not in ("drivers",)},
|
||||
}
|
||||
|
||||
logger.info(f"[instrument_service] Loaded {len(_configs)} instrument configs")
|
||||
|
||||
|
||||
@@ -106,6 +145,37 @@ def update_instrument_saxo_link(instrument_id: str, saxo_symbol: Optional[str])
|
||||
logger.info(f"[instrument_service] Updated Saxo link for {uid}: {saxo_symbol}")
|
||||
|
||||
|
||||
def quick_add_instrument_from_watchlist(ticker: str) -> Dict[str, Any]:
|
||||
"""Bring a Cockpit watchlist instrument (services.database.instruments_watchlist) into
|
||||
Instrument Analysis without hand-authoring an instruments.json entry — used by the
|
||||
picker's "Depuis la Watchlist" section for tickers that don't already have a catalog
|
||||
entry. Its saxo_quote_symbol (already linked in Config -> Instruments Watchlist) is
|
||||
copied over automatically — no need to re-link it a second time in Instrument Analysis.
|
||||
Idempotent: if instrument_id already resolves (either a real catalog entry or an
|
||||
earlier quick add), returns it unchanged rather than overwriting name/category."""
|
||||
global _configs
|
||||
if _configs is None:
|
||||
_load_configs()
|
||||
|
||||
uid = ticker.strip().upper()
|
||||
if uid in _configs:
|
||||
return {"id": uid, "created": False}
|
||||
|
||||
from services.database import get_instruments_watchlist, set_instrument_override_quick_add
|
||||
row = next((r for r in get_instruments_watchlist() if r["ticker"].upper() == uid), None)
|
||||
if row is None:
|
||||
raise ValueError(f"'{uid}' n'est pas dans la Watchlist du Cockpit")
|
||||
|
||||
category = _ASSET_CLASS_TO_CATEGORY.get(row.get("asset_class") or "unknown", "stock")
|
||||
set_instrument_override_quick_add(
|
||||
uid, name=row.get("name") or uid, yf_ticker=uid, category=category,
|
||||
saxo_quote_symbol=row.get("saxo_quote_symbol"),
|
||||
)
|
||||
_load_configs()
|
||||
logger.info(f"[instrument_service] Quick-added {uid} from Watchlist (category={category})")
|
||||
return {"id": uid, "created": True}
|
||||
|
||||
|
||||
# ── DataFrame helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
def _ohlcv_to_df(records: List[Dict]) -> pd.DataFrame:
|
||||
|
||||
Reference in New Issue
Block a user