feat: sexo history
This commit is contained in:
@@ -37,10 +37,14 @@ def update_watchlist(req: WatchlistRequest):
|
|||||||
|
|
||||||
|
|
||||||
def _validate_symbol(symbol: str) -> dict:
|
def _validate_symbol(symbol: str) -> dict:
|
||||||
from services.saxo_client import resolve_option_root_uic, SaxoNotConnected
|
from services.saxo_client import resolve_instrument, SaxoNotConnected
|
||||||
try:
|
try:
|
||||||
uic = resolve_option_root_uic(symbol)
|
info = resolve_instrument(symbol)
|
||||||
return {"symbol": symbol.upper(), "valid": True, "uic": uic, "error": None}
|
return {
|
||||||
|
"symbol": symbol.upper(), "valid": True, "error": None,
|
||||||
|
"uic": info["uic"], "matched_symbol": info["symbol"],
|
||||||
|
"description": info["description"], "asset_type": info["asset_type"],
|
||||||
|
}
|
||||||
except SaxoNotConnected as e:
|
except SaxoNotConnected as e:
|
||||||
return {"symbol": symbol.upper(), "valid": False, "uic": None, "error": str(e)}
|
return {"symbol": symbol.upper(), "valid": False, "uic": None, "error": str(e)}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
_OPTION_ASSET_TYPES = "StockOption,StockIndexOption,FuturesOption,ContractFutures"
|
_OPTION_ASSET_TYPES = "StockOption,StockIndexOption,FuturesOption,ContractFutures"
|
||||||
|
|
||||||
# symbol -> option root Uic, cheap in-process cache (roots don't change within a session)
|
# symbol -> resolved instrument details, cheap in-process cache (roots don't change within a session)
|
||||||
_root_uic_cache: Dict[str, int] = {}
|
_root_uic_cache: Dict[str, Dict[str, Any]] = {}
|
||||||
|
|
||||||
|
|
||||||
class SaxoNotConnected(Exception):
|
class SaxoNotConnected(Exception):
|
||||||
@@ -91,7 +91,9 @@ def _first(d: Dict[str, Any], *keys: str) -> Any:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def resolve_option_root_uic(symbol: str) -> int:
|
def resolve_instrument(symbol: str) -> 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:
|
if symbol in _root_uic_cache:
|
||||||
return _root_uic_cache[symbol]
|
return _root_uic_cache[symbol]
|
||||||
|
|
||||||
@@ -100,12 +102,23 @@ def resolve_option_root_uic(symbol: str) -> int:
|
|||||||
if not items:
|
if not items:
|
||||||
raise ValueError(f"Aucun option root Saxo trouvé pour '{symbol}' (réponse: {list(data.keys())})")
|
raise ValueError(f"Aucun option root Saxo trouvé pour '{symbol}' (réponse: {list(data.keys())})")
|
||||||
|
|
||||||
uic = _first(items[0], "Uic", "uic", "Identifier")
|
item = items[0]
|
||||||
|
uic = _first(item, "Uic", "uic", "Identifier")
|
||||||
if uic is None:
|
if uic is None:
|
||||||
raise ValueError(f"Champ Uic introuvable dans la réponse instruments pour '{symbol}': {items[0]}")
|
raise ValueError(f"Champ Uic introuvable dans la réponse instruments pour '{symbol}': {item}")
|
||||||
|
|
||||||
_root_uic_cache[symbol] = int(uic)
|
result = {
|
||||||
return int(uic)
|
"uic": int(uic),
|
||||||
|
"symbol": _first(item, "Symbol", "symbol"),
|
||||||
|
"description": _first(item, "Description", "description"),
|
||||||
|
"asset_type": _first(item, "AssetType", "assetType"),
|
||||||
|
}
|
||||||
|
_root_uic_cache[symbol] = result
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_option_root_uic(symbol: str) -> int:
|
||||||
|
return resolve_instrument(symbol)["uic"]
|
||||||
|
|
||||||
|
|
||||||
def get_option_space(root_uic: int) -> Dict[str, Any]:
|
def get_option_space(root_uic: int) -> Dict[str, Any]:
|
||||||
|
|||||||
@@ -1730,7 +1730,10 @@ export const useSaxoSymbols = () =>
|
|||||||
queryFn: () => api.get('/saxo/symbols').then(r => r.data),
|
queryFn: () => api.get('/saxo/symbols').then(r => r.data),
|
||||||
})
|
})
|
||||||
|
|
||||||
export type SaxoValidation = { symbol: string; valid: boolean; uic: number | null; error: string | null }
|
export type SaxoValidation = {
|
||||||
|
symbol: string; valid: boolean; uic: number | null; error: string | null
|
||||||
|
matched_symbol?: string; description?: string; asset_type?: string
|
||||||
|
}
|
||||||
|
|
||||||
export const useValidateSaxoWatchlist = () =>
|
export const useValidateSaxoWatchlist = () =>
|
||||||
useQuery<SaxoValidation[]>({
|
useQuery<SaxoValidation[]>({
|
||||||
|
|||||||
@@ -547,7 +547,9 @@ function SaxoConnectionCard() {
|
|||||||
return (
|
return (
|
||||||
<span key={sym} className={clsx('badge flex items-center gap-1.5',
|
<span key={sym} className={clsx('badge flex items-center gap-1.5',
|
||||||
check ? (check.valid ? 'badge-green' : 'badge-red') : 'badge-blue')}
|
check ? (check.valid ? 'badge-green' : 'badge-red') : 'badge-blue')}
|
||||||
title={check && !check.valid ? check.error ?? 'Symbole non résolu par Saxo' : undefined}
|
title={check ? (check.valid
|
||||||
|
? `Résolu: ${check.matched_symbol ?? '?'} — ${check.description ?? ''} (${check.asset_type ?? '?'}, Uic ${check.uic})`
|
||||||
|
: check.error ?? 'Symbole non résolu par Saxo') : undefined}
|
||||||
>
|
>
|
||||||
{sym}
|
{sym}
|
||||||
{check && (check.valid ? <CheckCircle className="w-3 h-3" /> : <XCircle className="w-3 h-3" />)}
|
{check && (check.valid ? <CheckCircle className="w-3 h-3" /> : <XCircle className="w-3 h-3" />)}
|
||||||
|
|||||||
Reference in New Issue
Block a user