feat: saxo price
This commit is contained in:
@@ -11,7 +11,7 @@ quoting window carry a Call/Put payload; the rest are bare {Index, Strike}.
|
||||
import logging
|
||||
import time
|
||||
import uuid
|
||||
from datetime import date
|
||||
from datetime import date, datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import httpx
|
||||
@@ -224,6 +224,89 @@ def get_price_quote(symbol: str, asset_type: str = "FxSpot", amount: int = 10000
|
||||
}
|
||||
|
||||
|
||||
def get_price_history(symbol: str, asset_type: str = "FxSpot", days: int = 90) -> List[Dict[str, Any]]:
|
||||
"""Daily OHLC bars via Saxo's Chart API (GET /chart/v1/charts, Horizon=1440 = daily
|
||||
bars). NOT yet verified against a live account (unlike get_price_quote/
|
||||
snapshot_options_chain, which were built against confirmed real responses) — this
|
||||
follows Saxo's documented chart endpoint shape but needs a live check once deployed.
|
||||
Callers should treat any failure here (wrong field names, entitlement gap, etc.) as
|
||||
routine and fall back to another source, not surface it as a hard error.
|
||||
Returns oldest-first [{"date": "YYYY-MM-DD", "close": float}, ...].
|
||||
"""
|
||||
instrument = resolve_instrument(symbol, asset_types=asset_type)
|
||||
data = _get("/chart/v1/charts", {
|
||||
"AssetType": asset_type,
|
||||
"Uic": instrument["uic"],
|
||||
"Horizon": 1440,
|
||||
"Count": days,
|
||||
})
|
||||
bars = data.get("Data") or []
|
||||
if not bars:
|
||||
raise ValueError(f"No chart data returned for '{symbol}' ({asset_type})")
|
||||
out = []
|
||||
for bar in bars:
|
||||
close = bar.get("Close")
|
||||
time_str = bar.get("Time")
|
||||
if close is None or not time_str:
|
||||
continue
|
||||
out.append({"date": str(time_str)[:10], "close": float(close)})
|
||||
if not out:
|
||||
raise ValueError(f"Chart data for '{symbol}' had no usable Close/Time fields")
|
||||
return out
|
||||
|
||||
|
||||
def get_saxo_quote_with_volatility(symbol: str, asset_type: str, vol_window: int = 20) -> Dict[str, Any]:
|
||||
"""Saxo-sourced equivalent of services.data_fetcher.get_quote_with_volatility — same
|
||||
output shape, but priced from Saxo's own chart history instead of yfinance. Exists
|
||||
because continuous front-month futures tickers (BZ=F, CL=F, GC=F...) on yfinance
|
||||
aren't roll-adjusted, so day-over-day change/volatility can show a spurious jump on
|
||||
a contract-roll day that has nothing to do with an actual market move. Saxo's own
|
||||
CFD/spot instruments don't have that artifact. Raises on any failure — the caller
|
||||
(routers/instruments_watchlist.py) falls back to yfinance rather than surfacing this."""
|
||||
import numpy as np
|
||||
|
||||
bars = get_price_history(symbol, asset_type, days=max(vol_window + 10, 90))
|
||||
if len(bars) < vol_window + 2:
|
||||
raise ValueError(f"Not enough Saxo history for '{symbol}' to compute {vol_window}d volatility")
|
||||
|
||||
dates = [b["date"] for b in bars]
|
||||
closes = np.array([b["close"] for b in bars], dtype=float)
|
||||
|
||||
price = float(closes[-1])
|
||||
last_date = dates[-1]
|
||||
prior_idx = [i for i, d in enumerate(dates) if d < last_date]
|
||||
prev = float(closes[prior_idx[-1]]) if prior_idx else price
|
||||
change = price - prev
|
||||
change_pct = (change / prev * 100) if prev else 0
|
||||
|
||||
log_ret = np.diff(np.log(closes))
|
||||
if len(log_ret) < vol_window:
|
||||
raise ValueError(f"Not enough Saxo return points for '{symbol}' to compute {vol_window}d volatility")
|
||||
vol_series = []
|
||||
for i in range(vol_window, len(log_ret) + 1):
|
||||
window = log_ret[i - vol_window:i]
|
||||
vol_series.append((dates[i], float(np.std(window, ddof=1)) * np.sqrt(252) * 100))
|
||||
if not vol_series:
|
||||
raise ValueError(f"Volatility series empty for '{symbol}'")
|
||||
volatility_pct = vol_series[-1][1]
|
||||
|
||||
volatility_change_pct = None
|
||||
prior_vol = [v for d, v in vol_series if d < last_date]
|
||||
if prior_vol:
|
||||
volatility_change_pct = round(volatility_pct - prior_vol[-1], 2)
|
||||
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"price": round(price, 4),
|
||||
"change": round(change, 4),
|
||||
"change_pct": round(change_pct, 2),
|
||||
"volatility_pct": round(volatility_pct, 2),
|
||||
"volatility_change_pct": volatility_change_pct,
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"source": "saxo",
|
||||
}
|
||||
|
||||
|
||||
def resolve_option_root_uic(symbol: str) -> int:
|
||||
return resolve_instrument(symbol)["uic"]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user