feat: cockpit

This commit is contained in:
OpenSquared
2026-07-24 10:20:05 +02:00
parent d1d9ea4855
commit 02a7cc2bd6

View File

@@ -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 """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 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 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 bars quote Bid/Ask rather than a single traded price at any point (no single
OTC FX) — CloseMid/mid-of-CloseBid+CloseAsk is used as the close price in that case. 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 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 some other asset type, etc.) as routine and fall back to another source, not surface
it as a hard error. 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 [] bars = data.get("Data") or []
if not bars: if not bars:
raise ValueError(f"No chart data returned for '{symbol}' ({asset_type})") 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 = [] out = []
for bar in bars: for bar in bars:
time_str = bar.get("Time") time_str = bar.get("Time")
close = bar.get("Close") close = _mid_field(bar, "Close", "CloseMid", "CloseBid", "CloseAsk")
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
if close is None or not time_str: if close is None or not time_str:
continue continue
out.append({ out.append({
"date": str(time_str)[:10], "date": str(time_str)[:10],
"close": float(close), "close": close,
"open": float(bar["Open"]) if bar.get("Open") is not None else None, "open": _mid_field(bar, "Open", "OpenMid", "OpenBid", "OpenAsk"),
"high": float(bar["High"]) if bar.get("High") is not None else None, "high": _mid_field(bar, "High", "HighMid", "HighBid", "HighAsk"),
"low": float(bar["Low"]) if bar.get("Low") is not None else None, "low": _mid_field(bar, "Low", "LowMid", "LowBid", "LowAsk"),
"volume": float(bar["Volume"]) if bar.get("Volume") is not None else None, "volume": float(bar["Volume"]) if bar.get("Volume") is not None else None,
}) })
if not out: if not out: