From 0d3a6a6fad81fde852e3a9fb8e1468cd49cda8a3 Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Sat, 18 Jul 2026 22:20:33 +0200 Subject: [PATCH] feat: saxo --- backend/data/geooptions.db-journal | 0 backend/routers/saxo.py | 39 ++++++++++++++++++++++++++++++ backend/services/saxo_client.py | 9 +++++++ frontend/src/hooks/useApi.ts | 8 ++++++ frontend/src/pages/Config.tsx | 38 ++++++++++++++++++++++++++++- 5 files changed, 93 insertions(+), 1 deletion(-) delete mode 100644 backend/data/geooptions.db-journal diff --git a/backend/data/geooptions.db-journal b/backend/data/geooptions.db-journal deleted file mode 100644 index e69de29..0000000 diff --git a/backend/routers/saxo.py b/backend/routers/saxo.py index 6e6c5a4..f97803b 100644 --- a/backend/routers/saxo.py +++ b/backend/routers/saxo.py @@ -39,6 +39,45 @@ def update_watchlist(req: WatchlistRequest): return {"symbols": get_watchlist()} +class ExpandWatchlistRequest(BaseModel): + keyword: str + + +@router.post("/watchlist/expand") +def expand_watchlist(req: ExpandWatchlistRequest): + """Adds every catalog entry matching a keyword (ex. 'EURUSD' -> every weekly/monthly + FuturesOption root, not just one) — avoids hand-picking each maturity's exact ticker.""" + matches = get_saxo_catalog(None, req.keyword, limit=200) + if not matches: + raise HTTPException(status_code=404, detail=f"Aucune entrée du catalogue ne correspond à '{req.keyword}' — rafraîchissez le catalogue d'abord si besoin.") + current = get_watchlist() + added = [m["symbol"] for m in matches if m["symbol"] not in current] + set_watchlist(current + added) + return {"symbols": get_watchlist(), "added": added} + + +@router.get("/debug-chain/{symbol}") +def debug_chain(symbol: str): + """Raw, unparsed options-chain snapshot for one symbol — diagnostic endpoint only. + Also written to System Logs (details field) so it can be read from the Logs page + instead of requiring direct URL/API access.""" + from services.saxo_client import debug_chain_raw, SaxoNotConnected, SaxoApiError + from services.database import log_system_event + try: + result = debug_chain_raw(symbol) + log_system_event( + level="INFO", source="saxo_debug", + message=f"Raw options chain snapshot for {symbol.upper()} (voir details)", + ticker=symbol, details=result, + ) + return result + except SaxoNotConnected as e: + raise HTTPException(status_code=401, detail=str(e)) + except SaxoApiError as e: + log_system_event(level="ERROR", source="saxo_debug", message=f"debug-chain failed for {symbol.upper()}: {e}", ticker=symbol) + raise HTTPException(status_code=502, detail=str(e)) + + def _validate_symbol(symbol: str) -> dict: from services.saxo_client import resolve_instrument, SaxoNotConnected try: diff --git a/backend/services/saxo_client.py b/backend/services/saxo_client.py index c001860..516eb93 100644 --- a/backend/services/saxo_client.py +++ b/backend/services/saxo_client.py @@ -242,6 +242,15 @@ def _snapshot_via_subscription(uic: int, asset_type: str = "StockOption") -> Dic return snapshot +def debug_chain_raw(symbol: str) -> Dict[str, Any]: + """Returns the raw (unparsed) options-chain subscription response for one symbol — + diagnostic only, used to pin down exactly where Mid/Greeks/Spot live in Saxo's real + payload instead of guessing field names blind.""" + instrument = resolve_instrument(symbol) + subscription = _snapshot_via_subscription(instrument["uic"], asset_type=instrument["asset_type"] or "StockOption") + return {"instrument": instrument, "raw": subscription} + + def snapshot_options_chain(symbol: str, target_days: int = 30) -> List[Dict[str, Any]]: """ Returns normalized rows ready for services/database.save_saxo_snapshot_rows: diff --git a/frontend/src/hooks/useApi.ts b/frontend/src/hooks/useApi.ts index e9b5dd6..e43ce5c 100644 --- a/frontend/src/hooks/useApi.ts +++ b/frontend/src/hooks/useApi.ts @@ -1701,6 +1701,14 @@ export const useUpdateSaxoWatchlist = () => { }) } +export const useExpandSaxoWatchlist = () => { + const qc = useQueryClient() + return useMutation({ + mutationFn: (keyword: string) => api.post('/saxo/watchlist/expand', { keyword }).then(r => r.data as { symbols: string[]; added: string[] }), + onSuccess: () => qc.invalidateQueries({ queryKey: ['saxo-watchlist'] }), + }) +} + export const useSnapshotSaxoNow = () => useMutation({ mutationFn: (symbol: string) => api.post(`/saxo/snapshot-now/${encodeURIComponent(symbol)}`).then(r => r.data), diff --git a/frontend/src/pages/Config.tsx b/frontend/src/pages/Config.tsx index c32051d..6298868 100644 --- a/frontend/src/pages/Config.tsx +++ b/frontend/src/pages/Config.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react' import { Link } from 'react-router-dom' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' -import { useSources, useUpdateSources, useUpdateApiKeys, useConfig, useAiStatus, useAnalysisConfig, useSaveAnalysisConfig, useCycleStatus, useUpdateCycleConfig, useTriggerCycle, useCycleStepCatalog, useRiskProfiles, useUpsertProfile, useDeleteProfile, useExitDefaults, useSaveExitDefaults, useOptionsGate, useSaveOptionsGate, useTechIndicatorsConfig, useSaveTechIndicatorsConfig, useInstrumentsWatchlist, useAddWatchlistInstrument, useRemoveWatchlistInstrument, validateTicker, useSaxoStatus, useDisconnectSaxo, useSaxoWatchlist, useUpdateSaxoWatchlist, useSnapshotSaxoNow, useValidateSaxoWatchlist, useSaxoCatalog, useSaxoCatalogSummary, useRefreshSaxoCatalog, useTestSaxoQuote, useSaxoSettings, useUpdateSaxoSettings, useSnapshotAllSaxoNow, type CycleStepDef } from '../hooks/useApi' +import { useSources, useUpdateSources, useUpdateApiKeys, useConfig, useAiStatus, useAnalysisConfig, useSaveAnalysisConfig, useCycleStatus, useUpdateCycleConfig, useTriggerCycle, useCycleStepCatalog, useRiskProfiles, useUpsertProfile, useDeleteProfile, useExitDefaults, useSaveExitDefaults, useOptionsGate, useSaveOptionsGate, useTechIndicatorsConfig, useSaveTechIndicatorsConfig, useInstrumentsWatchlist, useAddWatchlistInstrument, useRemoveWatchlistInstrument, validateTicker, useSaxoStatus, useDisconnectSaxo, useSaxoWatchlist, useUpdateSaxoWatchlist, useSnapshotSaxoNow, useValidateSaxoWatchlist, useSaxoCatalog, useSaxoCatalogSummary, useRefreshSaxoCatalog, useTestSaxoQuote, useSaxoSettings, useUpdateSaxoSettings, useSnapshotAllSaxoNow, useExpandSaxoWatchlist, type CycleStepDef } from '../hooks/useApi' import { Settings, Key, Globe, CheckCircle, XCircle, AlertCircle, Save, Eye, EyeOff, Brain, SlidersHorizontal, RefreshCw, Zap, Plus, Trash2, Pencil, X, Lock, Gauge, DollarSign, TrendingUp, ShieldAlert, DatabaseBackup, Radar, Link2, Unlink, Camera, ShieldCheck, ExternalLink } from 'lucide-react' import clsx from 'clsx' @@ -439,6 +439,9 @@ function SaxoConnectionCard() { const testQuote = useTestSaxoQuote() const [quoteSymbol, setQuoteSymbol] = useState('EURUSD') const [quoteAssetType, setQuoteAssetType] = useState('FxSpot') + const expandWatchlist = useExpandSaxoWatchlist() + const [expandKeyword, setExpandKeyword] = useState('') + const [expandMsg, setExpandMsg] = useState('') useEffect(() => { const params = new URLSearchParams(window.location.search) @@ -463,6 +466,21 @@ function SaxoConnectionCard() { const removeSymbol = (sym: string) => updateWatchlist.mutate(symbols.filter(s => s !== sym)) + const handleExpandWatchlist = async () => { + const kw = expandKeyword.trim() + if (!kw) return + setExpandMsg('') + try { + const r = await expandWatchlist.mutateAsync(kw) + setExpandMsg(r.added.length + ? `✓ ${r.added.length} ticker(s) ajouté(s) : ${r.added.join(', ')}` + : `○ Aucun nouveau ticker pour "${kw}" (déjà présents, ou rafraîchissez le catalogue)`) + setExpandKeyword('') + } catch (e: any) { + setExpandMsg(`✗ ${e?.response?.data?.detail ?? 'échec'}`) + } + } + const handleSnapshotNow = async (sym: string) => { setSnapMsg('') try { @@ -595,6 +613,24 @@ function SaxoConnectionCard() { Ajouter +
+ setExpandKeyword(e.target.value)} + onKeyDown={e => e.key === 'Enter' && handleExpandWatchlist()} + placeholder="Mot-clé sous-jacent (ex. EURUSD, Gold, Brent)" + className="bg-dark-800 border border-slate-700/40 rounded px-3 py-1.5 text-xs text-white w-72 focus:outline-none focus:border-blue-500/50" + /> + +
+ {expandMsg &&
{expandMsg}
}
{symbols.map(sym => { const check = validation?.find(v => v.symbol === sym.toUpperCase())