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
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: