feat: saxo
This commit is contained in:
@@ -6169,6 +6169,15 @@ def get_saxo_catalog(asset_type: Optional[str] = None, q: Optional[str] = None,
|
|||||||
return [dict(r) for r in rows]
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
def get_saxo_catalog_by_symbol(symbol: str) -> Optional[Dict[str, Any]]:
|
||||||
|
conn = get_conn()
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT * FROM saxo_instrument_catalog WHERE symbol = ? COLLATE NOCASE LIMIT 1", (symbol,)
|
||||||
|
).fetchone()
|
||||||
|
conn.close()
|
||||||
|
return dict(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
def get_saxo_catalog_summary() -> List[Dict[str, Any]]:
|
def get_saxo_catalog_summary() -> List[Dict[str, Any]]:
|
||||||
conn = get_conn()
|
conn = get_conn()
|
||||||
rows = conn.execute("""
|
rows = conn.execute("""
|
||||||
|
|||||||
@@ -117,6 +117,20 @@ def resolve_instrument(symbol: str, asset_types: str = _OPTION_ASSET_TYPES) -> D
|
|||||||
if cache_key in _root_uic_cache:
|
if cache_key in _root_uic_cache:
|
||||||
return _root_uic_cache[cache_key]
|
return _root_uic_cache[cache_key]
|
||||||
|
|
||||||
|
# Fast path: exchange-suffixed catalog symbols (e.g. "EUU:XCME", "OG:xcme") are Saxo
|
||||||
|
# identifiers, not natural-language search terms — Saxo's free-text Keywords search
|
||||||
|
# isn't reliable for them. If we already cached this exact symbol via a catalog
|
||||||
|
# refresh, use its known Uic directly instead of re-searching live.
|
||||||
|
from services.database import get_saxo_catalog_by_symbol
|
||||||
|
cached_entry = get_saxo_catalog_by_symbol(symbol)
|
||||||
|
if cached_entry:
|
||||||
|
result = {
|
||||||
|
"uic": cached_entry["uic"], "symbol": cached_entry["symbol"],
|
||||||
|
"description": cached_entry["description"], "asset_type": cached_entry["asset_type"],
|
||||||
|
}
|
||||||
|
_root_uic_cache[cache_key] = result
|
||||||
|
return result
|
||||||
|
|
||||||
data = _get("/ref/v1/instruments", {"Keywords": symbol, "AssetTypes": asset_types})
|
data = _get("/ref/v1/instruments", {"Keywords": symbol, "AssetTypes": asset_types})
|
||||||
items = data.get("Data") or data.get("data") or []
|
items = data.get("Data") or data.get("data") or []
|
||||||
if not items:
|
if not items:
|
||||||
|
|||||||
@@ -584,7 +584,7 @@ export default function StrategyBuilder() {
|
|||||||
...(saxoCatalog ?? []).map(c => c.symbol),
|
...(saxoCatalog ?? []).map(c => c.symbol),
|
||||||
])).sort()
|
])).sort()
|
||||||
|
|
||||||
const { data: chain, isLoading: chainLoading, isError: chainError, refetch: refetchChain, isFetching } =
|
const { data: chain, isLoading: chainLoading, isError: chainError, error: chainErrorObj, refetch: refetchChain, isFetching } =
|
||||||
useOptionChainSlice(symbol, horizonDays, 3)
|
useOptionChainSlice(symbol, horizonDays, 3)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -680,7 +680,8 @@ export default function StrategyBuilder() {
|
|||||||
|
|
||||||
{chainError && (
|
{chainError && (
|
||||||
<div className="px-4 py-3 rounded border border-red-700/40 bg-red-900/10 text-xs text-red-300 flex items-center gap-2">
|
<div className="px-4 py-3 rounded border border-red-700/40 bg-red-900/10 text-xs text-red-300 flex items-center gap-2">
|
||||||
<AlertTriangle className="w-4 h-4" /> Chaîne d'options indisponible pour {symbol}. Essayez un autre symbole (ETF/action optionable).
|
<AlertTriangle className="w-4 h-4" />
|
||||||
|
{(chainErrorObj as any)?.response?.data?.detail ?? `Chaîne d'options indisponible pour ${symbol}. Essayez un autre symbole.`}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user