feat: Phase 4+5 — price discovery status + replay historique
Phase 4 — Price Discovery Status (la pièce maîtresse) :
- price_discovery.py (nouveau) : capture_price_snapshots() sauve les prix des tickers
liés à chaque news scorée (energy→BZ=F/NG=F, metals→GC=F/HG=F, indices→^GSPC/IWM)
- compute_absorptions() mesure combien du mouvement attendu s'est déjà produit
(status: not_yet_priced <30% / partially_priced 30-80% / fully_priced >80%)
- build_price_discovery_block() → bloc prompt avec opportunités classées
- database.py : table news_price_snapshots + save/get/purge fonctions
- auto_cycle.py : capture après ai_score_news_batch, compute avant suggestions,
block injecté dans suggestion + scoring prompts + context snapshot
- ai_analyzer.py : param price_discovery_block dans suggest + score
Phase 5 — Replay historique :
- cycle.py : POST /api/cycle/contexts/{run_id}/replay — recharge le snapshot historique
et relance suggest_patterns_from_market_context avec le contexte original
- useApi.ts : hook useReplayCycle
- SystemLogs.tsx : bouton "Rejouer ce cycle" dans onglet Contexte IA avec champ
notes, résultats inline (liste des patterns générés), section price_discovery
ouverte par défaut en rouge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -882,6 +882,12 @@ export const useCycleContextSnapshot = (runId: string | null) =>
|
||||
staleTime: 300_000,
|
||||
})
|
||||
|
||||
export const useReplayCycle = () =>
|
||||
useMutation({
|
||||
mutationFn: ({ runId, notes }: { runId: string; notes?: string }) =>
|
||||
api.post(`/cycle/contexts/${runId}/replay`, { override_notes: notes ?? null }).then(r => r.data),
|
||||
})
|
||||
|
||||
// ── IV Watchlist Management ───────────────────────────────────────────────────
|
||||
|
||||
export const useWatchlistTickers = () =>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useState, useMemo } from 'react'
|
||||
import { format } from 'date-fns'
|
||||
import { fr } from 'date-fns/locale'
|
||||
import { AlertTriangle, XCircle, Info, RefreshCw, Trash2, ChevronDown, ChevronRight, Brain } from 'lucide-react'
|
||||
import { AlertTriangle, XCircle, Info, RefreshCw, Trash2, ChevronDown, ChevronRight, Brain, Play, Loader2 } from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
import { useSystemLogs, useLogSources, useLogCycles, useClearLogs, useCycleContextSnapshots, useCycleContextSnapshot, type LogFilters } from '../hooks/useApi'
|
||||
import { useSystemLogs, useLogSources, useLogCycles, useClearLogs, useCycleContextSnapshots, useCycleContextSnapshot, useReplayCycle, type LogFilters } from '../hooks/useApi'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
|
||||
const LEVELS = ['', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
|
||||
@@ -78,14 +78,26 @@ function LogRow({ log }: { log: any }) {
|
||||
|
||||
function ContextTab() {
|
||||
const [selectedRunId, setSelectedRunId] = useState<string | null>(null)
|
||||
const [replayNotes, setReplayNotes] = useState('')
|
||||
const [replayResult, setReplayResult] = useState<any>(null)
|
||||
const { data: snapshotsData, isLoading: loadingList } = useCycleContextSnapshots(30)
|
||||
const { data: snapData, isLoading: loadingSnap } = useCycleContextSnapshot(selectedRunId)
|
||||
const { mutate: replayCycle, isPending: replaying } = useReplayCycle()
|
||||
const snapshots: any[] = snapshotsData?.snapshots ?? []
|
||||
|
||||
const fmtTs = (ts: string) => {
|
||||
try { return format(new Date(ts), 'dd/MM HH:mm:ss', { locale: fr }) } catch { return ts }
|
||||
}
|
||||
|
||||
const handleReplay = () => {
|
||||
if (!selectedRunId) return
|
||||
setReplayResult(null)
|
||||
replayCycle(
|
||||
{ runId: selectedRunId, notes: replayNotes || undefined },
|
||||
{ onSuccess: (data) => setReplayResult(data) }
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-[280px_1fr] gap-4 min-h-[500px]">
|
||||
{/* Left: list of snapshots */}
|
||||
@@ -101,7 +113,7 @@ function ContextTab() {
|
||||
{snapshots.map(s => (
|
||||
<button
|
||||
key={s.run_id}
|
||||
onClick={() => setSelectedRunId(s.run_id)}
|
||||
onClick={() => { setSelectedRunId(s.run_id); setReplayResult(null) }}
|
||||
className={clsx(
|
||||
'w-full text-left px-3 py-2.5 hover:bg-slate-800/60 transition-colors',
|
||||
selectedRunId === s.run_id && 'bg-purple-900/30 border-l-2 border-purple-500'
|
||||
@@ -115,22 +127,22 @@ function ContextTab() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right: context JSON viewer */}
|
||||
<div className="card overflow-hidden">
|
||||
{/* Right: context JSON viewer + replay */}
|
||||
<div className="card overflow-hidden flex flex-col">
|
||||
{!selectedRunId ? (
|
||||
<div className="p-8 text-center text-slate-600 text-sm">
|
||||
<div className="p-8 text-center text-slate-600 text-sm flex-1">
|
||||
<Brain className="w-8 h-8 mx-auto mb-2 opacity-30" />
|
||||
Sélectionne un cycle à gauche pour voir le contexte complet envoyé à l'IA.
|
||||
</div>
|
||||
) : loadingSnap ? (
|
||||
<div className="p-8 text-center text-slate-500 text-sm">Chargement du contexte…</div>
|
||||
<div className="p-8 text-center text-slate-500 text-sm flex-1">Chargement du contexte…</div>
|
||||
) : !snapData ? (
|
||||
<div className="p-8 text-center text-slate-500 text-sm">Erreur lors du chargement.</div>
|
||||
<div className="p-8 text-center text-slate-500 text-sm flex-1">Erreur lors du chargement.</div>
|
||||
) : (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Header strip with meta */}
|
||||
<div className="flex items-center gap-4 px-4 py-2 border-b border-slate-800 bg-slate-900/60">
|
||||
<span className="text-[10px] font-mono text-slate-400">{snapData.run_id}</span>
|
||||
<>
|
||||
{/* Header strip with meta + replay */}
|
||||
<div className="flex items-center gap-3 px-4 py-2 border-b border-slate-800 bg-slate-900/60 flex-wrap">
|
||||
<span className="text-[10px] font-mono text-slate-400">{snapData.run_id.slice(0, 24)}…</span>
|
||||
<span className="text-[10px] text-slate-500">{fmtTs(snapData.ts)}</span>
|
||||
{snapData.context?.cycle_meta && (
|
||||
<>
|
||||
@@ -140,15 +152,52 @@ function ContextTab() {
|
||||
<span className="text-[10px] text-slate-400">{snapData.context.cycle_meta.calibration_label}</span>
|
||||
</>
|
||||
)}
|
||||
<div className="flex items-center gap-2 ml-auto">
|
||||
<input
|
||||
className="bg-slate-800 border border-slate-700 rounded px-2 py-1 text-[10px] text-slate-300 w-48"
|
||||
placeholder="Notes de replay (optionnel)"
|
||||
value={replayNotes}
|
||||
onChange={e => setReplayNotes(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
onClick={handleReplay}
|
||||
disabled={replaying}
|
||||
className="flex items-center gap-1.5 bg-purple-700 hover:bg-purple-600 disabled:opacity-40 text-white px-3 py-1.5 rounded text-xs font-semibold"
|
||||
>
|
||||
{replaying
|
||||
? <><Loader2 className="w-3 h-3 animate-spin" /> Replay en cours…</>
|
||||
: <><Play className="w-3 h-3" /> Rejouer ce cycle</>}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sections accordion */}
|
||||
{/* Replay result */}
|
||||
{replayResult && (
|
||||
<div className="border-b border-slate-800 bg-green-950/20 p-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-xs font-semibold text-green-400">
|
||||
Replay terminé — {replayResult.suggestions_count} patterns générés
|
||||
</span>
|
||||
<span className="text-[10px] text-slate-500">à {fmtTs(replayResult.replayed_at)}</span>
|
||||
</div>
|
||||
<div className="space-y-1 max-h-48 overflow-y-auto">
|
||||
{(replayResult.suggestions || []).map((s: any, i: number) => (
|
||||
<div key={i} className="text-[10px] text-slate-300 bg-slate-900 rounded px-2 py-1">
|
||||
<span className="text-green-400 font-mono">{s.name}</span>
|
||||
{s.macro_fit && <span className="text-slate-500 ml-2">— {s.macro_fit?.slice(0, 80)}</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Context sections */}
|
||||
<div className="overflow-y-auto flex-1 p-3 space-y-2">
|
||||
{snapData.context && Object.entries(snapData.context).map(([key, val]) => (
|
||||
<ContextSection key={key} label={key} value={val} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -156,7 +205,7 @@ function ContextTab() {
|
||||
}
|
||||
|
||||
function ContextSection({ label, value }: { label: string; value: any }) {
|
||||
const [open, setOpen] = useState(['cycle_meta', 'news_partitioned', 'fred_releases'].includes(label))
|
||||
const [open, setOpen] = useState(['cycle_meta', 'news_partitioned', 'fred_releases', 'price_discovery'].includes(label))
|
||||
const json = JSON.stringify(value, null, 2)
|
||||
const lineCount = json.split('\n').length
|
||||
|
||||
@@ -166,6 +215,7 @@ function ContextSection({ label, value }: { label: string; value: any }) {
|
||||
geo_score: 'text-orange-400',
|
||||
news_partitioned: 'text-yellow-400',
|
||||
fred_releases: 'text-green-400',
|
||||
price_discovery: 'text-red-400',
|
||||
tech_indicators_block: 'text-cyan-400',
|
||||
iv_context_preview: 'text-pink-400',
|
||||
calendar: 'text-slate-300',
|
||||
|
||||
Reference in New Issue
Block a user