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