feat: saxo connector

This commit is contained in:
OpenSquared
2026-07-18 20:27:44 +02:00
parent dec5da37b1
commit d359480302
4 changed files with 110 additions and 7 deletions

View File

@@ -86,6 +86,19 @@ def snapshot_now(symbol: str):
return {"symbol": symbol.upper(), "rows_saved": len(rows)}
@router.get("/quote/{symbol}")
def quote(symbol: str, asset_type: str = Query("FxSpot")):
"""Basic (non-options) price test — isolates whether NoDataAccess is options-specific
or a broader account restriction, independently of the options-chain subscription."""
from services.saxo_client import get_price_quote, SaxoNotConnected, SaxoApiError
try:
return get_price_quote(symbol, asset_type=asset_type)
except SaxoNotConnected as e:
raise HTTPException(status_code=401, detail=str(e))
except (ValueError, SaxoApiError) as e:
raise HTTPException(status_code=502, detail=str(e))
@router.get("/history")
def history(
symbol: Optional[str] = Query(None),

View File

@@ -114,16 +114,17 @@ def _first(d: Dict[str, Any], *keys: str) -> Any:
return None
def resolve_instrument(symbol: str) -> Dict[str, Any]:
def resolve_instrument(symbol: str, asset_types: str = _OPTION_ASSET_TYPES) -> Dict[str, Any]:
"""Full matched instrument (Uic + Symbol/Description/AssetType) — surfaced by /validate so
a wrong-ticker guess is visibly distinguishable from an account-entitlement error."""
if symbol in _root_uic_cache:
return _root_uic_cache[symbol]
cache_key = f"{symbol}|{asset_types}"
if cache_key in _root_uic_cache:
return _root_uic_cache[cache_key]
data = _get("/ref/v1/instruments", {"Keywords": symbol, "AssetTypes": _OPTION_ASSET_TYPES})
data = _get("/ref/v1/instruments", {"Keywords": symbol, "AssetTypes": asset_types})
items = data.get("Data") or data.get("data") or []
if not items:
raise ValueError(f"Aucun option root Saxo trouvé pour '{symbol}' (réponse: {list(data.keys())})")
raise ValueError(f"Aucun instrument Saxo trouvé pour '{symbol}' (AssetTypes={asset_types})")
item = items[0]
uic = _first(item, "Identifier", "Uic", "uic")
@@ -136,10 +137,43 @@ def resolve_instrument(symbol: str) -> Dict[str, Any]:
"description": _first(item, "Description", "description"),
"asset_type": _first(item, "AssetType", "assetType"),
}
_root_uic_cache[symbol] = result
_root_uic_cache[cache_key] = result
return result
def get_price_quote(symbol: str, asset_type: str = "FxSpot", amount: int = 100000) -> Dict[str, Any]:
"""
Basic (non-options) price lookup via /trade/v1/infoprices/list — confirmed working shape
(AccountKey, Uics, AssetType, Amount, FieldGroups) straight from a real Saxo Explorer
response. Used to test plain market-data access independently of the options chain
(which returns NoDataAccess even though basic FxSpot/Stock prices work fine).
"""
instrument = resolve_instrument(symbol, asset_types=asset_type)
data = _get("/trade/v1/infoprices/list", {
"AccountKey": get_default_account_key(),
"Uics": instrument["uic"],
"AssetType": asset_type,
"Amount": amount,
"FieldGroups": "DisplayAndFormat,Quote",
})
items = data.get("Data") or []
if not items:
raise ValueError(f"Aucune cotation renvoyée pour '{symbol}' ({asset_type})")
row = items[0]
quote = row.get("Quote", {})
display = row.get("DisplayAndFormat", {})
return {
"symbol": instrument["symbol"],
"description": display.get("Description"),
"asset_type": asset_type,
"uic": instrument["uic"],
"bid": quote.get("Bid"),
"ask": quote.get("Ask"),
"last_updated": row.get("LastUpdated"),
"price_source": row.get("PriceSource"),
}
def resolve_option_root_uic(symbol: str) -> int:
return resolve_instrument(symbol)["uic"]