From ff242bd2a6442fdbad6983bccaf279590c867c13 Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Sat, 18 Jul 2026 18:26:07 +0200 Subject: [PATCH] feat: saxo history --- backend/data/geooptions.db-journal | Bin 0 -> 33344 bytes backend/routers/saxo.py | 31 ++++- backend/services/database.py | 10 ++ frontend/src/App.tsx | 2 + frontend/src/components/layout/Sidebar.tsx | 3 +- frontend/src/config/keepAliveMeta.ts | 3 +- frontend/src/hooks/useApi.ts | 26 +++- frontend/src/pages/Config.tsx | 50 +++++-- frontend/src/pages/SaxoHistory.tsx | 153 +++++++++++++++++++++ 9 files changed, 258 insertions(+), 20 deletions(-) create mode 100644 backend/data/geooptions.db-journal create mode 100644 frontend/src/pages/SaxoHistory.tsx diff --git a/backend/data/geooptions.db-journal b/backend/data/geooptions.db-journal new file mode 100644 index 0000000000000000000000000000000000000000..feff101913cce8b634479764e9f0995deda55d7b GIT binary patch literal 33344 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 literal 0 HcmV?d00001 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)}
+
+ )} +
+ ) +}