import { useMemo, useState } from 'react' import { History, RefreshCw, Trash2, X } from 'lucide-react' import clsx from 'clsx' import { useSaxoSymbols, useSaxoHistory, useDedupeSaxoHistory, useDeleteSaxoHistory, type SaxoSnapshotRow } from '../hooks/useApi' function fmt(v: number | null | undefined, digits = 2) { return v == null ? '—' : v.toFixed(digits) } // FX option premiums are a small fraction of the underlying (e.g. 0.0050 on a 1.15 // EURUSD) — 2 decimals rounds every real value to "0.00", indistinguishable from an // actually-empty quote. Scale decimals to the premium's own magnitude instead. function fmtPrice(v: number | null | undefined) { if (v == null) return '—' const digits = v !== 0 && Math.abs(v) < 1 ? 5 : 2 return v.toFixed(digits) } // has no timezone suffix, so the browser parses it as local // time — reformat to the same "YYYY-MM-DD HH:MM:SS" UTC shape SQLite's datetime('now') // writes into created_at, so a plain string comparison on the backend lines up correctly. function toSqliteUtc(datetimeLocal: string): string { return new Date(datetimeLocal).toISOString().slice(0, 19).replace('T', ' ') } export default function SaxoHistory() { const { data: symbolSummaries = [] } = useSaxoSymbols() const [symbol, setSymbol] = useState('') const [asOfLocal, setAsOfLocal] = useState('') const asOf = asOfLocal ? toSqliteUtc(asOfLocal) : undefined const { data: rows = [], isLoading, isFetching, refetch } = useSaxoHistory(symbol || undefined, asOf) const dedupe = useDedupeSaxoHistory() const [dedupeMsg, setDedupeMsg] = useState(null) const handleDedupe = () => { dedupe.mutate(undefined, { onSuccess: (data) => setDedupeMsg(`${data.rows_deleted} ligne(s) redondante(s) supprimée(s).`), }) } const deleteHistory = useDeleteSaxoHistory() const handleDelete = () => { if (!symbol) return if (!window.confirm(`Effacer tout l'historique de snapshots de "${symbol}" ? Cette action est irréversible (les données ne seront plus disponibles pour les backtests).`)) return deleteHistory.mutate(symbol, { onSuccess: (data) => { setDedupeMsg(`${data.rows_deleted} ligne(s) supprimée(s) pour ${data.symbol}.`) setSymbol('') }, }) } const grouped = useMemo(() => { const groups: { key: string; symbol: string; expiry: string; rows: SaxoSnapshotRow[] }[] = [] for (const r of rows) { const key = `${r.symbol}||${r.expiry_date}` const last = groups[groups.length - 1] if (last && last.key === key) last.rows.push(r) else groups.push({ key, symbol: r.symbol, expiry: r.expiry_date, rows: [r] }) } return groups }, [rows]) return (

Saxo — Historique des snapshots

Une ligne par contrat (ticker/maturité/strike/type) — la capture la plus récente, ou celle en vigueur à la date/heure choisie ci-dessous.

{dedupeMsg &&
{dedupeMsg}
}
setAsOfLocal(e.target.value)} className="bg-dark-700 border border-slate-700/50 rounded px-2 py-1.5 text-sm text-white" /> {asOfLocal && ( )}
{rows.length} contrat(s)
{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 */} ))} ))}
Symbole Expiry Strike Type Spot Bid Ask Mid IV% Delta Gamma Theta Vega Capturé le
{group.symbol} — {group.expiry}
{r.symbol} {r.expiry_date} {r.strike} {r.option_type} {fmt(r.spot)} {fmtPrice(r.bid)} {fmtPrice(r.ask)} {fmtPrice(r.mid)} {!!r.is_synthetic && ( * )} {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)} {r.created_at}
)}
) }