feat: saxo history
This commit is contained in:
@@ -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 = () =>
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user