feat: saxo history

This commit is contained in:
OpenSquared
2026-07-19 08:49:57 +02:00
parent d4fb15ca9a
commit e7247d4c4c
5 changed files with 131 additions and 33 deletions

View File

@@ -1752,14 +1752,25 @@ export const useDedupeSaxoHistory = () => {
})
}
export const useSaxoHistory = (symbol?: string, dateFrom?: string, dateTo?: string) =>
export const useSaxoHistory = (symbol?: string, asOf?: string) =>
useQuery<SaxoSnapshotRow[]>({
queryKey: ['saxo-history', symbol, dateFrom, dateTo],
queryKey: ['saxo-history', symbol, asOf],
queryFn: () => api.get('/saxo/history', {
params: { ...(symbol ? { symbol } : {}), ...(dateFrom ? { date_from: dateFrom } : {}), ...(dateTo ? { date_to: dateTo } : {}) },
params: { ...(symbol ? { symbol } : {}), ...(asOf ? { as_of: asOf } : {}) },
}).then(r => r.data),
})
export const useDeleteSaxoHistory = () => {
const qc = useQueryClient()
return useMutation({
mutationFn: (symbol: string) => api.delete(`/saxo/history/${encodeURIComponent(symbol)}`).then(r => r.data as { symbol: string; rows_deleted: number }),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['saxo-history'] })
qc.invalidateQueries({ queryKey: ['saxo-symbols'] })
},
})
}
export type SaxoSymbolSummary = { symbol: string; rows_count: number; first_date: string; last_date: string }
export const useSaxoSymbols = () =>

View File

@@ -1,7 +1,7 @@
import { useMemo, useState } from 'react'
import { History, RefreshCw, Trash2 } from 'lucide-react'
import { History, RefreshCw, Trash2, X } from 'lucide-react'
import clsx from 'clsx'
import { useSaxoSymbols, useSaxoHistory, useDedupeSaxoHistory, type SaxoSnapshotRow } from '../hooks/useApi'
import { useSaxoSymbols, useSaxoHistory, useDedupeSaxoHistory, useDeleteSaxoHistory, type SaxoSnapshotRow } from '../hooks/useApi'
function fmt(v: number | null | undefined, digits = 2) {
return v == null ? '—' : v.toFixed(digits)
@@ -16,34 +16,48 @@ function fmtPrice(v: number | null | undefined) {
return v.toFixed(digits)
}
function daysAgo(n: number) {
const d = new Date()
d.setDate(d.getDate() - n)
return d.toISOString().slice(0, 10)
// <input type="datetime-local"> 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<string>('')
const [dateFrom, setDateFrom] = useState(daysAgo(30))
const [dateTo, setDateTo] = useState('')
const [asOfLocal, setAsOfLocal] = useState<string>('')
const asOf = asOfLocal ? toSqliteUtc(asOfLocal) : undefined
const { data: rows = [], isLoading, isFetching, refetch } = useSaxoHistory(symbol || undefined, asOf)
const { data: rows = [], isLoading, isFetching, refetch } = useSaxoHistory(symbol || undefined, dateFrom || undefined, dateTo || undefined)
const dedupe = useDedupeSaxoHistory()
const [dedupeMsg, setDedupeMsg] = useState<string | null>(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: { date: string; rows: SaxoSnapshotRow[] }[] = []
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.date === r.snapshot_date) last.rows.push(r)
else groups.push({ date: r.snapshot_date, rows: [r] })
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])
@@ -56,7 +70,7 @@ export default function SaxoHistory() {
<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.
Une ligne par contrat (ticker/maturité/strike/type) la capture la plus récente, ou celle en vigueur à la date/heure choisie ci-dessous.
</p>
</div>
<div className="flex items-center gap-2">
@@ -94,16 +108,27 @@ export default function SaxoHistory() {
</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" />
<label className="stat-label block mb-1">Voir au (vide = actuel)</label>
<div className="flex items-center gap-1.5">
<input type="datetime-local" value={asOfLocal} onChange={e => setAsOfLocal(e.target.value)}
className="bg-dark-700 border border-slate-700/50 rounded px-2 py-1.5 text-sm text-white" />
{asOfLocal && (
<button onClick={() => setAsOfLocal('')} title="Revenir à l'actuel" className="text-slate-500 hover:text-slate-300">
<X className="w-4 h-4" />
</button>
)}
</div>
</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>
<button
onClick={handleDelete}
disabled={!symbol || deleteHistory.isPending}
title={symbol ? `Effacer tout l'historique de ${symbol}` : 'Choisissez un symbole précis pour effacer son historique'}
className="flex items-center gap-1.5 text-xs border border-red-700/50 text-red-400 hover:text-red-300 hover:border-red-500 px-3 py-1.5 rounded transition-all disabled:opacity-30 disabled:cursor-not-allowed"
>
<Trash2 className="w-3.5 h-3.5" />
{deleteHistory.isPending ? 'Suppression...' : `Effacer l'historique${symbol ? ` de ${symbol}` : ''}`}
</button>
<div className="text-xs text-slate-500 ml-auto">{rows.length} contrat(s)</div>
</div>
{symbolSummaries.length > 0 && (
@@ -143,13 +168,14 @@ export default function SaxoHistory() {
<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>
<th className="py-1 pr-3">Capturé le</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 key={`h-${group.key}`} className="border-t border-slate-700/40">
<td colSpan={14} className="py-1.5 pr-3 text-slate-300 font-semibold bg-dark-700/30">{group.symbol} {group.expiry}</td>
</tr>
{group.rows.map(r => (
<tr key={r.id} className="border-t border-slate-800/60 hover:bg-dark-700/30">
@@ -174,6 +200,7 @@ export default function SaxoHistory() {
<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>
<td className="py-1 pr-3 text-slate-500">{r.created_at}</td>
</tr>
))}
</>