feat: saxo connector

This commit is contained in:
OpenSquared
2026-07-18 19:44:32 +02:00
parent 3cd453906e
commit dec5da37b1
6 changed files with 174 additions and 7 deletions

View File

@@ -24,7 +24,11 @@ from services.saxo_auth import SAXO_API_BASE_URL, get_valid_access_token
logger = logging.getLogger(__name__)
_OPTION_ASSET_TYPES = "StockOption,StockIndexOption,FuturesOption"
_OPTION_ASSET_TYPES = "StockOption,StockIndexOption,FuturesOption,FxVanillaOption"
# Bounded, stable catalogs worth fully caching in our own DB (StockOption/StockIndexOption
# are far too large to bulk-fetch — those stay resolved on demand via Keywords search).
CATALOG_ASSET_TYPES = ["FuturesOption", "FxVanillaOption"]
# symbol -> resolved instrument details, cheap in-process cache (roots don't change within a session)
_root_uic_cache: Dict[str, Dict[str, Any]] = {}
@@ -67,6 +71,25 @@ def _get(path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
return resp.json()
def list_instruments(asset_types: str, keywords: Optional[str] = None, max_pages: int = 20) -> List[Dict[str, Any]]:
"""Paginated dump of /ref/v1/instruments for a given AssetTypes filter — used to build
our own local catalog for bounded, stable asset classes (FuturesOption/FxVanillaOption)."""
all_items: List[Dict[str, Any]] = []
top = 1000
skip = 0
for _ in range(max_pages):
params: Dict[str, Any] = {"AssetTypes": asset_types, "$top": top, "$skip": skip}
if keywords:
params["Keywords"] = keywords
data = _get("/ref/v1/instruments", params)
items = data.get("Data") or data.get("data") or []
all_items.extend(items)
if len(items) < top:
break
skip += top
return all_items
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."""
@@ -103,7 +126,7 @@ def resolve_instrument(symbol: str) -> Dict[str, Any]:
raise ValueError(f"Aucun option root Saxo trouvé pour '{symbol}' (réponse: {list(data.keys())})")
item = items[0]
uic = _first(item, "Uic", "uic", "Identifier")
uic = _first(item, "Identifier", "Uic", "uic")
if uic is None:
raise ValueError(f"Champ Uic introuvable dans la réponse instruments pour '{symbol}': {item}")