feat: saxo history

This commit is contained in:
OpenSquared
2026-07-18 18:44:00 +02:00
parent ff242bd2a6
commit ca57f7d8d6
2 changed files with 39 additions and 5 deletions

View File

@@ -67,13 +67,13 @@ def symbols_with_history():
@router.post("/snapshot-now/{symbol}")
def snapshot_now(symbol: str):
from services.saxo_client import snapshot_options_chain, SaxoNotConnected
from services.saxo_client import snapshot_options_chain, SaxoNotConnected, SaxoApiError
from services.database import save_saxo_snapshot_rows
try:
rows = snapshot_options_chain(symbol)
except SaxoNotConnected as e:
raise HTTPException(status_code=401, detail=str(e))
except ValueError as e:
except (ValueError, SaxoApiError) as e:
raise HTTPException(status_code=502, detail=str(e))
save_saxo_snapshot_rows(rows)
return {"symbol": symbol.upper(), "rows_saved": len(rows)}

View File

@@ -34,6 +34,26 @@ class SaxoNotConnected(Exception):
pass
class SaxoApiError(Exception):
"""Wraps an httpx.HTTPStatusError with Saxo's actual JSON error body (ErrorCode/Message),
since the bare status code is useless for debugging a 400 on a subscription body."""
def __init__(self, resp: httpx.Response):
detail = resp.text
try:
detail = resp.json()
except Exception:
pass
super().__init__(f"Saxo API {resp.status_code} on {resp.request.method} {resp.request.url}: {detail}")
def _raise_for_status(resp: httpx.Response):
if resp.is_error:
raise SaxoApiError(resp)
_account_key_cache: Optional[str] = None
def _headers() -> Dict[str, str]:
token = get_valid_access_token()
if not token:
@@ -43,10 +63,24 @@ def _headers() -> Dict[str, str]:
def _get(path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
resp = httpx.get(f"{SAXO_API_BASE_URL}{path}", headers=_headers(), params=params or {}, timeout=15)
resp.raise_for_status()
_raise_for_status(resp)
return resp.json()
def get_default_account_key() -> str:
"""Most /trade/v1 subscriptions (including the options chain) require AccountKey in
Arguments — /port/v1/accounts/me is the standard way to get the user's default account."""
global _account_key_cache
if _account_key_cache:
return _account_key_cache
data = _get("/port/v1/accounts/me")
account_key = _first(data, "AccountKey", "accountKey")
if not account_key:
raise ValueError(f"Champ AccountKey introuvable dans /port/v1/accounts/me: {data}")
_account_key_cache = account_key
return account_key
def _first(d: Dict[str, Any], *keys: str) -> Any:
for k in keys:
if k in d:
@@ -113,7 +147,7 @@ def _snapshot_via_subscription(uic: int, asset_type: str = "StockOption") -> Dic
body = {
"ContextId": context_id,
"ReferenceId": reference_id,
"Arguments": {"Uic": uic, "AssetType": asset_type},
"Arguments": {"Uic": uic, "AssetType": asset_type, "AccountKey": get_default_account_key()},
"RefreshRate": 5000,
}
resp = httpx.post(
@@ -121,7 +155,7 @@ def _snapshot_via_subscription(uic: int, asset_type: str = "StockOption") -> Dic
headers={**token_headers, "Content-Type": "application/json"},
json=body, timeout=15,
)
resp.raise_for_status()
_raise_for_status(resp)
snapshot = resp.json()
try: