feat: cockpit

This commit is contained in:
OpenSquared
2026-07-24 12:23:14 +02:00
parent 35a00f43c3
commit a2e770aac9
4 changed files with 87 additions and 115 deletions

View File

@@ -152,7 +152,11 @@ def quick_add_instrument_from_watchlist(ticker: str) -> Dict[str, Any]:
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."""
earlier quick add), returns it unchanged rather than overwriting name/category. Also
tries the yfinance "=X" FX suffix (EURUSD -> EURUSD=X) before creating anything, so a
watchlist ticker that already has a curated catalog counterpart reuses it instead of
fragmenting into a second, duplicate id — this check has to live server-side (not just
in the frontend picker) since _configs is the only always-fresh source of truth."""
global _configs
if _configs is None:
_load_configs()
@@ -160,6 +164,8 @@ def quick_add_instrument_from_watchlist(ticker: str) -> Dict[str, Any]:
uid = ticker.strip().upper()
if uid in _configs:
return {"id": uid, "created": False}
if f"{uid}=X" in _configs:
return {"id": f"{uid}=X", "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)
@@ -764,25 +770,32 @@ _PERIOD_TO_DAYS = {
}
def _fetch_ohlcv(config: Dict[str, Any], instrument_id: str, period: str, interval: str) -> Tuple[List[Dict], str]:
def _fetch_ohlcv(config: Dict[str, Any], instrument_id: str, period: str, interval: str) -> Tuple[List[Dict], str, Optional[str]]:
"""Fetch OHLCV records for the snapshot — Saxo-first when the instrument has a
saxo_quote_symbol linked, yfinance otherwise (or as a silent fallback on any Saxo
failure). Returns (records, source)."""
failure). Returns (records, source, error) — error is the Saxo failure reason, kept
even when the yfinance fallback also comes up empty (a quick-added instrument's
yf_ticker is just its Cockpit ticker, e.g. "BRENT" — that's never a real yfinance
symbol, so surfacing *why Saxo failed* is the only actionable diagnostic in that case,
rather than a bare empty chart with no explanation)."""
saxo_symbol = config.get("saxo_quote_symbol")
saxo_error = None
if saxo_symbol:
from services.database import get_saxo_catalog_by_symbol
entry = get_saxo_catalog_by_symbol(saxo_symbol)
asset_type = entry["asset_type"] if entry else "FxSpot"
try:
from services.database import get_saxo_catalog_by_symbol
from services.saxo_client import get_price_history
entry = get_saxo_catalog_by_symbol(saxo_symbol)
asset_type = entry["asset_type"] if entry else "FxSpot"
days = _PERIOD_TO_DAYS.get(period, 365)
bars = get_price_history(saxo_symbol, asset_type, days=days)
records = [{"date": b["date"], "open": b.get("open"), "high": b.get("high"),
"low": b.get("low"), "close": b.get("close"), "volume": b.get("volume")}
for b in bars]
return records, "saxo"
return records, "saxo", None
except Exception as e:
logger.warning(f"[instrument_service] Saxo fetch failed for {instrument_id} ({saxo_symbol}), falling back to yfinance: {e}")
catalog_note = " — pas dans le catalogue Saxo local, asset_type par défaut" if not entry else ""
saxo_error = f"Saxo ({saxo_symbol}, asset_type={asset_type}{catalog_note}): {e}"
logger.warning(f"[instrument_service] Saxo fetch failed for {instrument_id} ({saxo_symbol}, asset_type={asset_type}): {e}")
yf_ticker = config.get("yf_ticker", instrument_id)
try:
@@ -791,7 +804,9 @@ def _fetch_ohlcv(config: Dict[str, Any], instrument_id: str, period: str, interv
except Exception as e:
logger.error(f"[instrument_service] Data fetch failed for {instrument_id}: {e}")
records = []
return records, "yfinance"
if not records and saxo_error:
return records, "yfinance", saxo_error
return records, "yfinance", None
async def get_snapshot(
@@ -807,7 +822,7 @@ async def get_snapshot(
if not config:
return {"error": f"Unknown instrument: {instrument_id}"}
records, source = _fetch_ohlcv(config, instrument_id, period, interval)
records, source, source_error = _fetch_ohlcv(config, instrument_id, period, interval)
df = _ohlcv_to_df(records)
# Compute everything
@@ -885,6 +900,7 @@ async def get_snapshot(
"change_abs": change_abs,
"period": period,
"source": source,
"source_error": source_error if not price_data else None,
}