feat: pressure

This commit is contained in:
OpenSquared
2026-07-02 18:07:21 +02:00
parent 01709e5edf
commit ae5865a156
3 changed files with 303 additions and 6 deletions

View File

@@ -1213,6 +1213,117 @@ const PERIODS = [
{ key: '5y', label: '5Y' },
]
// ── Types factor-state ────────────────────────────────────────────────────────
interface FactorContrib {
event_name: string; event_date: string; template_name: string
pips_full: number; days_elapsed: number; absorption_days: number
decay_pct: number; pips_current: number
}
interface FactorCategory {
label: string; pips: number; contributions: FactorContrib[]
}
interface FactorState {
instrument: string; at_date: string; net_pips: number
direction: 'bullish' | 'bearish' | 'neutral'
categories: FactorCategory[]; n_events: number
}
// ── PressureCockpit ───────────────────────────────────────────────────────────
function PressureCockpit({ instrumentId, refreshKey }: { instrumentId: string; refreshKey: number }) {
const [state, setState] = useState<FactorState | null>(null)
const [loading, setLoading] = useState(false)
const [expanded, setExpanded] = useState<string | null>(null)
useEffect(() => {
if (!instrumentId) return
setLoading(true)
api.get(`/instruments/${instrumentId}/factor-state`)
.then(r => setState(r.data))
.catch(() => setState(null))
.finally(() => setLoading(false))
}, [instrumentId, refreshKey])
if (loading) return (
<div className="h-16 flex items-center justify-center text-xs text-slate-600 italic">
Calcul pression en cours
</div>
)
if (!state) return null
const { net_pips, direction, categories } = state
const maxAbs = Math.max(...categories.map(c => Math.abs(c.pips)), 1)
const netCls = direction === 'bullish' ? 'text-emerald-400' : direction === 'bearish' ? 'text-red-400' : 'text-slate-400'
const netLabel = direction === 'bullish' ? '▲ HAUSSIER' : direction === 'bearish' ? '▼ BAISSIER' : '◼ NEUTRE'
return (
<div className="space-y-2">
{/* NET */}
<div className="flex items-center gap-3 px-1">
<span className="text-xs text-slate-500 uppercase tracking-wide">Pression nette</span>
<span className={clsx('font-mono font-bold text-base', netCls)}>
{net_pips >= 0 ? '+' : ''}{net_pips} pips
</span>
<span className={clsx('text-[10px] font-semibold px-1.5 py-0.5 rounded border', netCls,
direction === 'bullish' ? 'border-emerald-700/40 bg-emerald-900/20'
: direction === 'bearish' ? 'border-red-700/40 bg-red-900/20'
: 'border-slate-700/40 bg-slate-800/20')}>
{netLabel}
</span>
<span className="ml-auto text-[10px] text-slate-600">{state.n_events} event{state.n_events > 1 ? 's' : ''} actifs</span>
</div>
{/* Barres par catégorie */}
{categories.length === 0 ? (
<p className="text-xs text-slate-600 italic px-1">Aucun event actif avec prédiction pour cet instrument.</p>
) : (
<div className="space-y-1.5">
{categories.map(cat => {
const barW = Math.round(Math.abs(cat.pips) / maxAbs * 100)
const isPos = cat.pips >= 0
const isOpen = expanded === cat.label
return (
<div key={cat.label}>
<button
className="w-full flex items-center gap-2 text-xs hover:bg-slate-800/30 rounded px-1 py-0.5 transition-colors"
onClick={() => setExpanded(isOpen ? null : cat.label)}
>
<span className="w-32 text-left text-slate-400 shrink-0 truncate">{cat.label}</span>
{/* Barre */}
<div className="flex-1 h-3 bg-slate-800/60 rounded-full overflow-hidden">
<div
className={clsx('h-full rounded-full transition-all', isPos ? 'bg-emerald-600/70' : 'bg-red-600/70')}
style={{ width: `${barW}%` }}
/>
</div>
<span className={clsx('font-mono font-semibold w-16 text-right shrink-0', isPos ? 'text-emerald-400' : 'text-red-400')}>
{isPos ? '+' : ''}{cat.pips}
</span>
<span className="text-slate-600 text-[10px] shrink-0">{isOpen ? '▲' : '▼'}</span>
</button>
{/* Détail events */}
{isOpen && (
<div className="ml-2 mt-1 space-y-1 border-l border-slate-700/40 pl-3">
{cat.contributions.map((c, i) => (
<div key={i} className="flex items-center gap-2 text-[10px] text-slate-500">
<span className="flex-1 truncate">{c.event_name}</span>
<span className="text-slate-600 shrink-0">{c.event_date} · {c.decay_pct}% actif ({c.days_elapsed}j)</span>
<span className={clsx('font-mono shrink-0 w-12 text-right', c.pips_current >= 0 ? 'text-emerald-500' : 'text-red-500')}>
{c.pips_current >= 0 ? '+' : ''}{c.pips_current}
</span>
</div>
))}
</div>
)}
</div>
)
})}
</div>
)}
</div>
)
}
export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { instrumentIdProp?: string; isVisible?: boolean } = {}) {
const { id: paramId = localStorage.getItem('last_instrument') || 'EURUSD=X' } = useParams<{ id: string }>()
const navigate = useNavigate()
@@ -1600,6 +1711,14 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i
: null
return (
<div className="space-y-3">
{/* Pression nette actuelle */}
<div className="rounded-xl border border-slate-700/40 bg-dark-800/60 p-4">
<div className="flex items-center gap-2 mb-3">
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wide">Pression Nette</span>
</div>
<PressureCockpit instrumentId={instrumentId} refreshKey={chartReady} />
</div>
{/* Frise des graphes causaux (inclut l'event) */}
<div className="rounded-xl border border-slate-700/40 bg-dark-800/60 p-4">
<div className="flex items-center gap-2 mb-3">