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
|
"""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:
|
||||||
|
|||||||
Reference in New Issue
Block a user