feat: saxo history
This commit is contained in:
BIN
backend/data/geooptions.db-journal
Normal file
BIN
backend/data/geooptions.db-journal
Normal file
Binary file not shown.
@@ -5,7 +5,7 @@ from pydantic import BaseModel
|
|||||||
|
|
||||||
from services import saxo_auth
|
from services import saxo_auth
|
||||||
from services.saxo_scheduler import get_watchlist, set_watchlist
|
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"])
|
router = APIRouter(prefix="/api/saxo", tags=["saxo"])
|
||||||
|
|
||||||
@@ -36,6 +36,35 @@ def update_watchlist(req: WatchlistRequest):
|
|||||||
return {"symbols": get_watchlist()}
|
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}")
|
@router.post("/snapshot-now/{symbol}")
|
||||||
def snapshot_now(symbol: str):
|
def snapshot_now(symbol: str):
|
||||||
from services.saxo_client import snapshot_options_chain, SaxoNotConnected
|
from services.saxo_client import snapshot_options_chain, SaxoNotConnected
|
||||||
|
|||||||
@@ -6116,3 +6116,13 @@ def get_saxo_snapshots(symbol: Optional[str] = None, date_from: Optional[str] =
|
|||||||
rows = conn.execute(query, params).fetchall()
|
rows = conn.execute(query, params).fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
return [dict(r) for r in rows]
|
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]
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import Markets from './pages/Markets'
|
|||||||
import MacroRegime from './pages/MacroRegime'
|
import MacroRegime from './pages/MacroRegime'
|
||||||
import OptionsLab from './pages/OptionsLab'
|
import OptionsLab from './pages/OptionsLab'
|
||||||
import StrategyBuilder from './pages/StrategyBuilder'
|
import StrategyBuilder from './pages/StrategyBuilder'
|
||||||
|
import SaxoHistory from './pages/SaxoHistory'
|
||||||
import WaveletsSimulation from './pages/WaveletsSimulation'
|
import WaveletsSimulation from './pages/WaveletsSimulation'
|
||||||
import Backtest from './pages/Backtest'
|
import Backtest from './pages/Backtest'
|
||||||
import CalendarPage from './pages/CalendarPage'
|
import CalendarPage from './pages/CalendarPage'
|
||||||
@@ -52,6 +53,7 @@ const KEEP_ALIVE_DEFS: { path: string; component: ComponentType }[] = [
|
|||||||
{ path: '/macro', component: MacroRegime },
|
{ path: '/macro', component: MacroRegime },
|
||||||
{ path: '/options', component: OptionsLab },
|
{ path: '/options', component: OptionsLab },
|
||||||
{ path: '/strategy-builder', component: StrategyBuilder },
|
{ path: '/strategy-builder', component: StrategyBuilder },
|
||||||
|
{ path: '/saxo-history', component: SaxoHistory },
|
||||||
{ path: '/wavelets-simulation', component: WaveletsSimulation },
|
{ path: '/wavelets-simulation', component: WaveletsSimulation },
|
||||||
{ path: '/patterns', component: PatternExplorer },
|
{ path: '/patterns', component: PatternExplorer },
|
||||||
{ path: '/pattern-lab', component: PatternLab },
|
{ path: '/pattern-lab', component: PatternLab },
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { NavLink } from 'react-router-dom'
|
import { NavLink } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
LayoutDashboard, Globe, BarChart2, FlaskConical,
|
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'
|
} from 'lucide-react'
|
||||||
import { useGeoRiskScore, useAiStatus, usePortfolioSummary } from '../../hooks/useApi'
|
import { useGeoRiskScore, useAiStatus, usePortfolioSummary } from '../../hooks/useApi'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
@@ -13,6 +13,7 @@ const nav = [
|
|||||||
{ to: '/macro', icon: Activity, label: 'Macro Regime' },
|
{ to: '/macro', icon: Activity, label: 'Macro Regime' },
|
||||||
{ to: '/options', icon: TrendingUp, label: 'Options Lab' },
|
{ to: '/options', icon: TrendingUp, label: 'Options Lab' },
|
||||||
{ to: '/strategy-builder', icon: Layers, label: 'Strategy Builder' },
|
{ 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: '/wavelets-simulation', icon: Waves, label: 'Wavelets Simulation' },
|
||||||
{ to: '/patterns', icon: Zap, label: 'Patterns' },
|
{ to: '/patterns', icon: Zap, label: 'Patterns' },
|
||||||
{ to: '/pattern-lab', icon: FlaskConical, label: 'Pattern Lab' },
|
{ to: '/pattern-lab', icon: FlaskConical, label: 'Pattern Lab' },
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {
|
|||||||
LayoutDashboard, Globe, BarChart2, Activity, TrendingUp, Zap, FlaskConical,
|
LayoutDashboard, Globe, BarChart2, Activity, TrendingUp, Zap, FlaskConical,
|
||||||
DollarSign, BookOpen, FileBarChart, Brain, Microscope, ShieldAlert, Gauge,
|
DollarSign, BookOpen, FileBarChart, Brain, Microscope, ShieldAlert, Gauge,
|
||||||
GitCompare, History, Calendar, Sliders, TrendingUp as MacroSeriesIcon,
|
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'
|
} from 'lucide-react'
|
||||||
import type { LucideIcon } 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: '/macro', label: 'Macro Regime', icon: Activity },
|
||||||
{ path: '/options', label: 'Options Lab', icon: TrendingUp },
|
{ path: '/options', label: 'Options Lab', icon: TrendingUp },
|
||||||
{ path: '/strategy-builder', label: 'Strategy Builder', icon: Layers },
|
{ 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: '/wavelets-simulation', label: 'Wavelets Sim.', icon: Waves },
|
||||||
{ path: '/patterns', label: 'Patterns', icon: Zap },
|
{ path: '/patterns', label: 'Patterns', icon: Zap },
|
||||||
{ path: '/pattern-lab', label: 'Pattern Lab', icon: FlaskConical },
|
{ path: '/pattern-lab', label: 'Pattern Lab', icon: FlaskConical },
|
||||||
|
|||||||
@@ -1714,9 +1714,27 @@ export type SaxoSnapshotRow = {
|
|||||||
created_at: string
|
created_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSaxoHistory = (symbol?: string) =>
|
export const useSaxoHistory = (symbol?: string, dateFrom?: string, dateTo?: string) =>
|
||||||
useQuery<SaxoSnapshotRow[]>({
|
useQuery<SaxoSnapshotRow[]>({
|
||||||
queryKey: ['saxo-history', symbol],
|
queryKey: ['saxo-history', symbol, dateFrom, dateTo],
|
||||||
queryFn: () => api.get('/saxo/history', { params: symbol ? { symbol } : {} }).then(r => r.data),
|
queryFn: () => api.get('/saxo/history', {
|
||||||
enabled: !!symbol,
|
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<SaxoSymbolSummary[]>({
|
||||||
|
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<SaxoValidation[]>({
|
||||||
|
queryKey: ['saxo-validate'],
|
||||||
|
queryFn: () => api.get('/saxo/validate').then(r => r.data),
|
||||||
|
enabled: false,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
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 { 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 } from 'lucide-react'
|
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'
|
import clsx from 'clsx'
|
||||||
|
|
||||||
const API = ''
|
const API = ''
|
||||||
@@ -426,6 +427,7 @@ function SaxoConnectionCard() {
|
|||||||
const disconnect = useDisconnectSaxo()
|
const disconnect = useDisconnectSaxo()
|
||||||
const updateWatchlist = useUpdateSaxoWatchlist()
|
const updateWatchlist = useUpdateSaxoWatchlist()
|
||||||
const snapshotNow = useSnapshotSaxoNow()
|
const snapshotNow = useSnapshotSaxoNow()
|
||||||
|
const { data: validation, refetch: runValidation, isFetching: validating } = useValidateSaxoWatchlist()
|
||||||
const [input, setInput] = useState('')
|
const [input, setInput] = useState('')
|
||||||
const [snapMsg, setSnapMsg] = useState('')
|
const [snapMsg, setSnapMsg] = useState('')
|
||||||
|
|
||||||
@@ -508,8 +510,23 @@ function SaxoConnectionCard() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-xs text-slate-500 mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
Watchlist snapshotée périodiquement (historique Date/Spot/Expiry/Strike/Bid/Ask/Mid/VolatilityPct/Greeks) :
|
<div className="text-xs text-slate-500">
|
||||||
|
Watchlist snapshotée périodiquement (historique Date/Spot/Expiry/Strike/Bid/Ask/Mid/VolatilityPct/Greeks) :
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
|
<button
|
||||||
|
onClick={() => runValidation()}
|
||||||
|
disabled={!status?.connected || validating}
|
||||||
|
title="Vérifie que chaque symbole résout bien vers un vrai instrument Saxo"
|
||||||
|
className="flex items-center gap-1 text-xs text-slate-400 hover:text-slate-200 border border-slate-700/50 px-2 py-1 rounded disabled:opacity-40"
|
||||||
|
>
|
||||||
|
<ShieldCheck className={clsx('w-3.5 h-3.5', validating && 'animate-pulse')} /> Vérifier l'orthographe
|
||||||
|
</button>
|
||||||
|
<Link to="/saxo-history" className="flex items-center gap-1 text-xs text-blue-400 hover:text-blue-300">
|
||||||
|
<ExternalLink className="w-3.5 h-3.5" /> Historique
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<input
|
<input
|
||||||
@@ -525,15 +542,22 @@ function SaxoConnectionCard() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="flex flex-wrap gap-1.5">
|
||||||
{symbols.map(sym => (
|
{symbols.map(sym => {
|
||||||
<span key={sym} className="badge-blue flex items-center gap-1.5">
|
const check = validation?.find(v => v.symbol === sym.toUpperCase())
|
||||||
{sym}
|
return (
|
||||||
<button onClick={() => handleSnapshotNow(sym)} title="Snapshot maintenant" className="hover:text-white">
|
<span key={sym} className={clsx('badge flex items-center gap-1.5',
|
||||||
<Camera className="w-3 h-3" />
|
check ? (check.valid ? 'badge-green' : 'badge-red') : 'badge-blue')}
|
||||||
</button>
|
title={check && !check.valid ? check.error ?? 'Symbole non résolu par Saxo' : undefined}
|
||||||
<button onClick={() => removeSymbol(sym)}><X className="w-3 h-3" /></button>
|
>
|
||||||
</span>
|
{sym}
|
||||||
))}
|
{check && (check.valid ? <CheckCircle className="w-3 h-3" /> : <XCircle className="w-3 h-3" />)}
|
||||||
|
<button onClick={() => handleSnapshotNow(sym)} title="Snapshot maintenant" className="hover:text-white">
|
||||||
|
<Camera className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
<button onClick={() => removeSymbol(sym)}><X className="w-3 h-3" /></button>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
{snapMsg && <div className="text-[10px] text-slate-500 mt-2">{snapMsg}</div>}
|
{snapMsg && <div className="text-[10px] text-slate-500 mt-2">{snapMsg}</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
153
frontend/src/pages/SaxoHistory.tsx
Normal file
153
frontend/src/pages/SaxoHistory.tsx
Normal file
@@ -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<string>('')
|
||||||
|
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 (
|
||||||
|
<div className="p-6 space-y-5">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-xl font-bold text-white flex items-center gap-2">
|
||||||
|
<History className="w-5 h-5 text-blue-400" /> Saxo — Historique des snapshots
|
||||||
|
</h1>
|
||||||
|
<p className="text-xs text-slate-500 mt-0.5">
|
||||||
|
Accumulé depuis la connexion Saxo — pas d'historique passé disponible via leur API, seulement ce qu'on capture nous-mêmes.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => refetch()}
|
||||||
|
disabled={isFetching}
|
||||||
|
className="flex items-center gap-1.5 text-xs border border-slate-600 text-slate-400 hover:text-slate-200 hover:border-slate-500 px-3 py-1.5 rounded transition-all disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<RefreshCw className={clsx('w-3.5 h-3.5', isFetching && 'animate-spin')} />
|
||||||
|
{isFetching ? 'Chargement...' : 'Rafraîchir'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card flex flex-wrap items-end gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="stat-label block mb-1">Symbole</label>
|
||||||
|
<select
|
||||||
|
value={symbol}
|
||||||
|
onChange={e => setSymbol(e.target.value)}
|
||||||
|
className="bg-dark-700 border border-slate-700/50 rounded px-2 py-1.5 text-sm text-white w-40"
|
||||||
|
>
|
||||||
|
<option value="">Tous</option>
|
||||||
|
{symbolSummaries.map(s => <option key={s.symbol} value={s.symbol}>{s.symbol}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="stat-label block mb-1">Du</label>
|
||||||
|
<input type="date" value={dateFrom} onChange={e => setDateFrom(e.target.value)}
|
||||||
|
className="bg-dark-700 border border-slate-700/50 rounded px-2 py-1.5 text-sm text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="stat-label block mb-1">Au</label>
|
||||||
|
<input type="date" value={dateTo} onChange={e => setDateTo(e.target.value)}
|
||||||
|
className="bg-dark-700 border border-slate-700/50 rounded px-2 py-1.5 text-sm text-white" />
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-slate-500 ml-auto">{rows.length} lignes</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{symbolSummaries.length > 0 && (
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||||
|
{symbolSummaries.map(s => (
|
||||||
|
<button
|
||||||
|
key={s.symbol}
|
||||||
|
onClick={() => setSymbol(s.symbol === symbol ? '' : s.symbol)}
|
||||||
|
className={clsx('card-sm text-left transition-colors', s.symbol === symbol && 'border-blue-500/60')}
|
||||||
|
>
|
||||||
|
<div className="text-sm font-bold text-white">{s.symbol}</div>
|
||||||
|
<div className="text-xs text-slate-500">{s.rows_count} lignes · {s.first_date} → {s.last_date}</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="card-sm text-xs text-slate-500">Chargement…</div>
|
||||||
|
) : rows.length === 0 ? (
|
||||||
|
<div className="card-sm text-xs text-slate-500">Aucun snapshot enregistré pour ces filtres.</div>
|
||||||
|
) : (
|
||||||
|
<div className="card overflow-x-auto">
|
||||||
|
<table className="w-full text-xs">
|
||||||
|
<thead>
|
||||||
|
<tr className="text-slate-500 text-left sticky top-0 bg-dark-800">
|
||||||
|
<th className="py-1 pr-3">Symbole</th>
|
||||||
|
<th className="py-1 pr-3">Expiry</th>
|
||||||
|
<th className="py-1 pr-3 text-right">Strike</th>
|
||||||
|
<th className="py-1 pr-3">Type</th>
|
||||||
|
<th className="py-1 pr-3 text-right">Spot</th>
|
||||||
|
<th className="py-1 pr-3 text-right">Bid</th>
|
||||||
|
<th className="py-1 pr-3 text-right">Ask</th>
|
||||||
|
<th className="py-1 pr-3 text-right">Mid</th>
|
||||||
|
<th className="py-1 pr-3 text-right">IV%</th>
|
||||||
|
<th className="py-1 pr-3 text-right">Delta</th>
|
||||||
|
<th className="py-1 pr-3 text-right">Gamma</th>
|
||||||
|
<th className="py-1 pr-3 text-right">Theta</th>
|
||||||
|
<th className="py-1 pr-3 text-right">Vega</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{grouped.map(group => (
|
||||||
|
<>
|
||||||
|
<tr key={`h-${group.date}`} className="border-t border-slate-700/40">
|
||||||
|
<td colSpan={13} className="py-1.5 pr-3 text-slate-300 font-semibold bg-dark-700/30">{group.date}</td>
|
||||||
|
</tr>
|
||||||
|
{group.rows.map(r => (
|
||||||
|
<tr key={r.id} className="border-t border-slate-800/60 hover:bg-dark-700/30">
|
||||||
|
<td className="py-1 pr-3 text-slate-300">{r.symbol}</td>
|
||||||
|
<td className="py-1 pr-3 text-slate-400">{r.expiry_date}</td>
|
||||||
|
<td className="py-1 pr-3 text-right text-slate-300">{r.strike}</td>
|
||||||
|
<td className="py-1 pr-3">
|
||||||
|
<span className={clsx('badge', r.option_type === 'call' ? 'badge-green' : 'badge-red')}>{r.option_type}</span>
|
||||||
|
</td>
|
||||||
|
<td className="py-1 pr-3 text-right text-slate-400">{fmt(r.spot)}</td>
|
||||||
|
<td className="py-1 pr-3 text-right text-slate-300">{fmt(r.bid)}</td>
|
||||||
|
<td className="py-1 pr-3 text-right text-slate-300">{fmt(r.ask)}</td>
|
||||||
|
<td className="py-1 pr-3 text-right text-white font-semibold">{fmt(r.mid)}</td>
|
||||||
|
{/* Saxo's field is already named "...Pct" (MidVolatilityPct) — assumed pre-scaled, not a 0-1 fraction */}
|
||||||
|
<td className="py-1 pr-3 text-right text-amber-400">{r.volatility_pct != null ? `${r.volatility_pct.toFixed(1)}%` : '—'}</td>
|
||||||
|
<td className="py-1 pr-3 text-right text-slate-400">{fmt(r.delta, 4)}</td>
|
||||||
|
<td className="py-1 pr-3 text-right text-slate-400">{fmt(r.gamma, 4)}</td>
|
||||||
|
<td className="py-1 pr-3 text-right text-slate-400">{fmt(r.theta, 4)}</td>
|
||||||
|
<td className="py-1 pr-3 text-right text-slate-400">{fmt(r.vega, 4)}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user