feat: cockpit
This commit is contained in:
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user