feat: pressure
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user