From 02a7cc2bd6507474003c2faa7648f6046902c8f1 Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Fri, 24 Jul 2026 10:20:05 +0200 Subject: [PATCH] feat: cockpit --- backend/services/saxo_client.py | 36 +++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/backend/services/saxo_client.py b/backend/services/saxo_client.py index 76e26f5..b401e45 100644 --- a/backend/services/saxo_client.py +++ b/backend/services/saxo_client.py @@ -244,8 +244,12 @@ def get_price_history(symbol: str, asset_type: str = "FxSpot", days: int = 90) - """Daily OHLC bars via Saxo's Chart API (GET /chart/v3/charts, Horizon=1440 = daily bars). v1 confirmed dead (plain HTML 404, not a Saxo JSON error); v3 confirmed live 2026-07-23 (returns real bars — ContractFutures symbols worked immediately). FX Spot - bars quote Bid/Ask rather than a single traded Close (no single last-traded price for - OTC FX) — CloseMid/mid-of-CloseBid+CloseAsk is used as the close price in that case. + bars quote Bid/Ask rather than a single traded price at any point (no single + last-traded price for OTC FX) — every OHLC field falls back to its *Mid field, then + to a mid-of-Bid+Ask, e.g. CloseMid/mid-of-CloseBid+CloseAsk for close (same pattern + for Open/High/Low — without this, FX Spot bars carry open/high/low: None and the + candlestick chart has nothing to draw, even though the line chart, which only needs + close, looks fine). Callers should treat any failure here (entitlement gap, still-wrong field names for some other asset type, etc.) as routine and fall back to another source, not surface it as a hard error. @@ -263,22 +267,32 @@ def get_price_history(symbol: str, asset_type: str = "FxSpot", days: int = 90) - bars = data.get("Data") or [] if not bars: raise ValueError(f"No chart data returned for '{symbol}' ({asset_type})") + def _mid_field(bar: Dict[str, Any], plain: str, mid: str, bid: str, ask: str) -> Optional[float]: + """FX Spot bars have no single traded price at any point in the bar (OTC quote-only + market) — Saxo gives OpenBid/OpenAsk, HighBid/HighAsk, LowBid/LowAsk, CloseBid/CloseAsk + instead of a plain Open/High/Low/Close. Mirror the same bid/ask-mid fallback used for + Close across all four OHLC fields, or every FX bar renders with null open/high/low and + the candlestick chart has nothing to draw (line chart still works since it only needs + close).""" + val = bar.get(plain) + if val is None: + val = bar.get(mid) + if val is None and bar.get(bid) is not None and bar.get(ask) is not None: + val = (bar[bid] + bar[ask]) / 2 + return float(val) if val is not None else None + out = [] for bar in bars: time_str = bar.get("Time") - close = bar.get("Close") - if close is None: - close = bar.get("CloseMid") - if close is None and bar.get("CloseBid") is not None and bar.get("CloseAsk") is not None: - close = (bar["CloseBid"] + bar["CloseAsk"]) / 2 + close = _mid_field(bar, "Close", "CloseMid", "CloseBid", "CloseAsk") if close is None or not time_str: continue out.append({ "date": str(time_str)[:10], - "close": float(close), - "open": float(bar["Open"]) if bar.get("Open") is not None else None, - "high": float(bar["High"]) if bar.get("High") is not None else None, - "low": float(bar["Low"]) if bar.get("Low") is not None else None, + "close": close, + "open": _mid_field(bar, "Open", "OpenMid", "OpenBid", "OpenAsk"), + "high": _mid_field(bar, "High", "HighMid", "HighBid", "HighAsk"), + "low": _mid_field(bar, "Low", "LowMid", "LowBid", "LowAsk"), "volume": float(bar["Volume"]) if bar.get("Volume") is not None else None, }) if not out: