diff --git a/backend/data/geooptions.db-journal b/backend/data/geooptions.db-journal new file mode 100644 index 0000000..feff101 Binary files /dev/null and b/backend/data/geooptions.db-journal differ diff --git a/backend/routers/saxo.py b/backend/routers/saxo.py index 65ab817..aedbbf2 100644 --- a/backend/routers/saxo.py +++ b/backend/routers/saxo.py @@ -5,7 +5,7 @@ from pydantic import BaseModel from services import saxo_auth from services.saxo_scheduler import get_watchlist, set_watchlist -from services.database import get_saxo_snapshots +from services.database import get_saxo_snapshots, get_saxo_snapshot_symbols router = APIRouter(prefix="/api/saxo", tags=["saxo"]) @@ -36,6 +36,35 @@ def update_watchlist(req: WatchlistRequest): return {"symbols": get_watchlist()} +def _validate_symbol(symbol: str) -> dict: + from services.saxo_client import resolve_option_root_uic, SaxoNotConnected + try: + uic = resolve_option_root_uic(symbol) + return {"symbol": symbol.upper(), "valid": True, "uic": uic, "error": None} + except SaxoNotConnected as e: + return {"symbol": symbol.upper(), "valid": False, "uic": None, "error": str(e)} + except Exception as e: + return {"symbol": symbol.upper(), "valid": False, "uic": None, "error": str(e)} + + +@router.get("/validate") +def validate_watchlist(): + """Checks each watchlist symbol actually resolves to a real Saxo option-root instrument.""" + return [_validate_symbol(s) for s in get_watchlist()] + + +@router.get("/validate/{symbol}") +def validate_symbol(symbol: str): + return _validate_symbol(symbol) + + +@router.get("/symbols") +def symbols_with_history(): + """Distinct symbols that already have recorded snapshot history (may differ from the + current watchlist — includes ad-hoc 'snapshot now' calls and previously-tracked symbols).""" + return get_saxo_snapshot_symbols() + + @router.post("/snapshot-now/{symbol}") def snapshot_now(symbol: str): from services.saxo_client import snapshot_options_chain, SaxoNotConnected diff --git a/backend/services/database.py b/backend/services/database.py index a222c87..4b07539 100644 --- a/backend/services/database.py +++ b/backend/services/database.py @@ -6116,3 +6116,13 @@ def get_saxo_snapshots(symbol: Optional[str] = None, date_from: Optional[str] = rows = conn.execute(query, params).fetchall() conn.close() return [dict(r) for r in rows] + + +def get_saxo_snapshot_symbols() -> List[Dict[str, Any]]: + conn = get_conn() + rows = conn.execute(""" + SELECT symbol, COUNT(*) AS rows_count, MIN(snapshot_date) AS first_date, MAX(snapshot_date) AS last_date + FROM saxo_option_snapshots GROUP BY symbol ORDER BY symbol + """).fetchall() + conn.close() + return [dict(r) for r in rows] diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 23a5692..bc7daf1 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -10,6 +10,7 @@ import Markets from './pages/Markets' import MacroRegime from './pages/MacroRegime' import OptionsLab from './pages/OptionsLab' import StrategyBuilder from './pages/StrategyBuilder' +import SaxoHistory from './pages/SaxoHistory' import WaveletsSimulation from './pages/WaveletsSimulation' import Backtest from './pages/Backtest' import CalendarPage from './pages/CalendarPage' @@ -52,6 +53,7 @@ const KEEP_ALIVE_DEFS: { path: string; component: ComponentType }[] = [ { path: '/macro', component: MacroRegime }, { path: '/options', component: OptionsLab }, { path: '/strategy-builder', component: StrategyBuilder }, + { path: '/saxo-history', component: SaxoHistory }, { path: '/wavelets-simulation', component: WaveletsSimulation }, { path: '/patterns', component: PatternExplorer }, { path: '/pattern-lab', component: PatternLab }, diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index 56f936e..80da574 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -1,7 +1,7 @@ import { NavLink } from 'react-router-dom' import { LayoutDashboard, Globe, BarChart2, FlaskConical, - History, Calendar, TrendingUp, Zap, DollarSign, Settings, BrainCircuit, Activity, BookOpen, FileBarChart, Brain, ShieldAlert, Microscope, ScrollText, Gauge, GitCompare, Building2, Users, ScanEye, CandlestickChart, PlayCircle, Radio, Bot, Sliders, Waves, Layers + History, Calendar, TrendingUp, Zap, DollarSign, Settings, BrainCircuit, Activity, BookOpen, FileBarChart, Brain, ShieldAlert, Microscope, ScrollText, Gauge, GitCompare, Building2, Users, ScanEye, CandlestickChart, PlayCircle, Radio, Bot, Sliders, Waves, Layers, History as HistoryIcon } from 'lucide-react' import { useGeoRiskScore, useAiStatus, usePortfolioSummary } from '../../hooks/useApi' import clsx from 'clsx' @@ -13,6 +13,7 @@ const nav = [ { to: '/macro', icon: Activity, label: 'Macro Regime' }, { to: '/options', icon: TrendingUp, label: 'Options Lab' }, { to: '/strategy-builder', icon: Layers, label: 'Strategy Builder' }, + { to: '/saxo-history', icon: HistoryIcon, label: 'Saxo History' }, { to: '/wavelets-simulation', icon: Waves, label: 'Wavelets Simulation' }, { to: '/patterns', icon: Zap, label: 'Patterns' }, { to: '/pattern-lab', icon: FlaskConical, label: 'Pattern Lab' }, diff --git a/frontend/src/config/keepAliveMeta.ts b/frontend/src/config/keepAliveMeta.ts index 617814c..6ccf2e6 100644 --- a/frontend/src/config/keepAliveMeta.ts +++ b/frontend/src/config/keepAliveMeta.ts @@ -2,7 +2,7 @@ import { LayoutDashboard, Globe, BarChart2, Activity, TrendingUp, Zap, FlaskConical, DollarSign, BookOpen, FileBarChart, Brain, Microscope, ShieldAlert, Gauge, GitCompare, History, Calendar, Sliders, TrendingUp as MacroSeriesIcon, - Building2, Users, ScanEye, Radio, Bot, PlayCircle, ScrollText, Settings, Waves, Layers, + Building2, Users, ScanEye, Radio, Bot, PlayCircle, ScrollText, Settings, Waves, Layers, History as HistoryIcon, } from 'lucide-react' import type { LucideIcon } from 'lucide-react' @@ -19,6 +19,7 @@ export const KEEP_ALIVE_META: KeepAliveMeta[] = [ { path: '/macro', label: 'Macro Regime', icon: Activity }, { path: '/options', label: 'Options Lab', icon: TrendingUp }, { path: '/strategy-builder', label: 'Strategy Builder', icon: Layers }, + { path: '/saxo-history', label: 'Saxo History', icon: HistoryIcon }, { path: '/wavelets-simulation', label: 'Wavelets Sim.', icon: Waves }, { path: '/patterns', label: 'Patterns', icon: Zap }, { path: '/pattern-lab', label: 'Pattern Lab', icon: FlaskConical }, diff --git a/frontend/src/hooks/useApi.ts b/frontend/src/hooks/useApi.ts index e5344b4..a98d5be 100644 --- a/frontend/src/hooks/useApi.ts +++ b/frontend/src/hooks/useApi.ts @@ -1714,9 +1714,27 @@ export type SaxoSnapshotRow = { created_at: string } -export const useSaxoHistory = (symbol?: string) => +export const useSaxoHistory = (symbol?: string, dateFrom?: string, dateTo?: string) => useQuery({ - queryKey: ['saxo-history', symbol], - queryFn: () => api.get('/saxo/history', { params: symbol ? { symbol } : {} }).then(r => r.data), - enabled: !!symbol, + queryKey: ['saxo-history', symbol, dateFrom, dateTo], + queryFn: () => api.get('/saxo/history', { + params: { ...(symbol ? { symbol } : {}), ...(dateFrom ? { date_from: dateFrom } : {}), ...(dateTo ? { date_to: dateTo } : {}) }, + }).then(r => r.data), + }) + +export type SaxoSymbolSummary = { symbol: string; rows_count: number; first_date: string; last_date: string } + +export const useSaxoSymbols = () => + useQuery({ + queryKey: ['saxo-symbols'], + queryFn: () => api.get('/saxo/symbols').then(r => r.data), + }) + +export type SaxoValidation = { symbol: string; valid: boolean; uic: number | null; error: string | null } + +export const useValidateSaxoWatchlist = () => + useQuery({ + queryKey: ['saxo-validate'], + queryFn: () => api.get('/saxo/validate').then(r => r.data), + enabled: false, }) diff --git a/frontend/src/pages/Config.tsx b/frontend/src/pages/Config.tsx index 7b02f0b..34e2c95 100644 --- a/frontend/src/pages/Config.tsx +++ b/frontend/src/pages/Config.tsx @@ -1,7 +1,8 @@ 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, 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 } from 'lucide-react' +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, 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' const API = '' @@ -426,6 +427,7 @@ function SaxoConnectionCard() { const disconnect = useDisconnectSaxo() const updateWatchlist = useUpdateSaxoWatchlist() const snapshotNow = useSnapshotSaxoNow() + const { data: validation, refetch: runValidation, isFetching: validating } = useValidateSaxoWatchlist() const [input, setInput] = useState('') const [snapMsg, setSnapMsg] = useState('') @@ -508,8 +510,23 @@ function SaxoConnectionCard() { )} -
- Watchlist snapshotée périodiquement (historique Date/Spot/Expiry/Strike/Bid/Ask/Mid/VolatilityPct/Greeks) : +
+
+ Watchlist snapshotée périodiquement (historique Date/Spot/Expiry/Strike/Bid/Ask/Mid/VolatilityPct/Greeks) : +
+
+ + + Historique + +
- {symbols.map(sym => ( - - {sym} - - - - ))} + {symbols.map(sym => { + const check = validation?.find(v => v.symbol === sym.toUpperCase()) + return ( + + {sym} + {check && (check.valid ? : )} + + + + ) + })}
{snapMsg &&
{snapMsg}
}
diff --git a/frontend/src/pages/SaxoHistory.tsx b/frontend/src/pages/SaxoHistory.tsx new file mode 100644 index 0000000..f5d751f --- /dev/null +++ b/frontend/src/pages/SaxoHistory.tsx @@ -0,0 +1,153 @@ +import { useMemo, useState } from 'react' +import { History, RefreshCw } from 'lucide-react' +import clsx from 'clsx' +import { useSaxoSymbols, useSaxoHistory, type SaxoSnapshotRow } from '../hooks/useApi' + +function fmt(v: number | null | undefined, digits = 2) { + return v == null ? '—' : v.toFixed(digits) +} + +function daysAgo(n: number) { + const d = new Date() + d.setDate(d.getDate() - n) + return d.toISOString().slice(0, 10) +} + +export default function SaxoHistory() { + const { data: symbolSummaries = [] } = useSaxoSymbols() + const [symbol, setSymbol] = useState('') + const [dateFrom, setDateFrom] = useState(daysAgo(30)) + const [dateTo, setDateTo] = useState('') + + const { data: rows = [], isLoading, isFetching, refetch } = useSaxoHistory(symbol || undefined, dateFrom || undefined, dateTo || undefined) + + const grouped = useMemo(() => { + const groups: { date: string; rows: SaxoSnapshotRow[] }[] = [] + for (const r of rows) { + const last = groups[groups.length - 1] + if (last && last.date === r.snapshot_date) last.rows.push(r) + else groups.push({ date: r.snapshot_date, rows: [r] }) + } + return groups + }, [rows]) + + return ( +
+
+
+

+ Saxo — Historique des snapshots +

+

+ Accumulé depuis la connexion Saxo — pas d'historique passé disponible via leur API, seulement ce qu'on capture nous-mêmes. +

+
+ +
+ +
+
+ + +
+
+ + setDateFrom(e.target.value)} + className="bg-dark-700 border border-slate-700/50 rounded px-2 py-1.5 text-sm text-white" /> +
+
+ + setDateTo(e.target.value)} + className="bg-dark-700 border border-slate-700/50 rounded px-2 py-1.5 text-sm text-white" /> +
+
{rows.length} lignes
+
+ + {symbolSummaries.length > 0 && ( +
+ {symbolSummaries.map(s => ( + + ))} +
+ )} + + {isLoading ? ( +
Chargement…
+ ) : rows.length === 0 ? ( +
Aucun snapshot enregistré pour ces filtres.
+ ) : ( +
+ + + + + + + + + + + + + + + + + + + + {grouped.map(group => ( + <> + + + + {group.rows.map(r => ( + + + + + + + + + + {/* Saxo's field is already named "...Pct" (MidVolatilityPct) — assumed pre-scaled, not a 0-1 fraction */} + + + + + + + ))} + + ))} + +
SymboleExpiryStrikeTypeSpotBidAskMidIV%DeltaGammaThetaVega
{group.date}
{r.symbol}{r.expiry_date}{r.strike} + {r.option_type} + {fmt(r.spot)}{fmt(r.bid)}{fmt(r.ask)}{fmt(r.mid)}{r.volatility_pct != null ? `${r.volatility_pct.toFixed(1)}%` : '—'}{fmt(r.delta, 4)}{fmt(r.gamma, 4)}{fmt(r.theta, 4)}{fmt(r.vega, 4)}
+
+ )} +
+ ) +}