refactor: move trade ideas to Journal tab, tabify Config, trim Dashboard

- Extract TradeCard/TradeRow/IBKRTicket + TradeIdeasTab self-contained
  component to frontend/src/components/TradeIdeas.tsx
- Dashboard: remove trade ideas section, MtM section, all related hooks/
  state/computations; replace Signaux Géo mini-card with Trades du cycle;
  add News géo link on Risque Géopolitique card
- JournalDeBord: add Idées de trade tab (left of Ouverts) using TradeIdeasTab
- Config: reorganize into 5 tabs — IA & Analyse, Auto-Cycle & Logging,
  Sources, Journal & Sortie, Profils de risque; expose min_score_threshold
  as a prominent slider in the Auto-Cycle tab

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-19 19:36:11 +02:00
parent 08651551db
commit f2b020ee4b
4 changed files with 1420 additions and 1528 deletions

View File

@@ -0,0 +1,921 @@
import { useState, useMemo, useEffect, Fragment } from 'react'
import {
useAllPatterns, useLastScores, useScorePatterns, useAiStatus,
usePortfolioPositions, useTradeMtm, useRiskProfiles, useMacroRegime, useAddPosition,
} from '../hooks/useApi'
import {
Target, Brain, Plus, RefreshCw, ChevronDown, ChevronUp, CheckCircle2,
LayoutGrid, List, Terminal,
} from 'lucide-react'
import clsx from 'clsx'
import { format } from 'date-fns'
import { fr } from 'date-fns/locale'
// ── Scoring utilities (re-exported for Dashboard use) ─────────────────────────
export const scoreColor = (s: number) => {
if (s >= 70) return 'text-emerald-400'
if (s >= 50) return 'text-yellow-400'
return 'text-slate-400'
}
export const scoreBg = (s: number) => {
if (s >= 70) return 'bg-emerald-500'
if (s >= 50) return 'bg-yellow-500'
return 'bg-slate-600'
}
// ── Constants ─────────────────────────────────────────────────────────────────
const BUCKET_ICONS: Record<string, string> = {
actualites: '📰', calendrier: '📅', prix: '📈', rr: '⚖️',
geo: '🌍', eco: '🌐', flux: '📡',
banques: '🏦', macro_cal: '📋',
taux: '📉', energie: '⛽', forex_sig: '💱', actions: '📊', vix: '⚡',
asymetrie: '⚖️', timing_rr: '⏱️',
}
export const CATEGORIES = [
{ key: 'all', label: 'Tous' },
{ key: 'energy', label: '⛽ Énergie' },
{ key: 'metals', label: '🥇 Métaux' },
{ key: 'agriculture', label: '🌾 Agri' },
{ key: 'indices', label: '📊 Indices' },
{ key: 'equities', label: '📈 Actions' },
{ key: 'forex', label: '💱 Forex' },
]
export interface TradeItem {
trade: any
patternName: string
patternId: string
assetClass: string
score: number | null
scoreInfo: any | null
scoreDelta: number | null
rankRationale: string | null
expectedMovePct: number
}
const BIAS_DISPLAY: Record<string, { label: string; color: string }> = {
'bullish+': { label: '★★ Compatible', color: '#10b981' },
'bullish': { label: '★ Compatible', color: '#34d399' },
'neutral': { label: '→ Neutre', color: '#64748b' },
'bearish': { label: '✗ Défavorable', color: '#f97316' },
'bearish+': { label: '✗✗ Contra', color: '#ef4444' },
'defensive':{ label: '⚠ Défensif', color: '#f59e0b' },
}
// ── IBKR helpers ──────────────────────────────────────────────────────────────
function computeStrikeDollars(price: number, guidance: string, strategy: string): number {
const g = guidance.toUpperCase()
const match = g.match(/(\d+)\s*%\s*OTM/)
if (!match) return price
const pct = parseInt(match[1]) / 100
const isBearish = /put|bear/i.test(strategy)
const raw = isBearish ? price * (1 - pct) : price * (1 + pct)
const inc = price > 1000 ? 10 : price > 200 ? 5 : price > 50 ? 1 : 0.5
return Math.round(raw / inc) * inc
}
function estimateExpiryDate(horizonDays: number): Date {
const d = new Date()
d.setDate(d.getDate() + horizonDays)
const dow = d.getDay()
if (dow !== 5) d.setDate(d.getDate() + ((5 - dow + 7) % 7))
return d
}
function buildLegs(strategy: string, strike: number | null, price: number | null) {
const s = strategy.toLowerCase()
const atm = price ? (() => {
const inc = price > 1000 ? 10 : price > 200 ? 5 : price > 50 ? 1 : 0.5
return Math.round(price / inc) * inc
})() : null
if (s.includes('bull call spread')) return [
{ action: 'BUY', type: 'CALL', strike: atm },
{ action: 'SELL', type: 'CALL', strike },
]
if (s.includes('bear put spread')) return [
{ action: 'BUY', type: 'PUT', strike: atm },
{ action: 'SELL', type: 'PUT', strike },
]
if (s.includes('straddle')) return [
{ action: 'BUY', type: 'CALL', strike: atm ?? strike },
{ action: 'BUY', type: 'PUT', strike: atm ?? strike },
]
if (s.includes('strangle')) return [
{ action: 'BUY', type: 'CALL', strike: price ? Math.round(price * 1.05) : strike },
{ action: 'BUY', type: 'PUT', strike: price ? Math.round(price * 0.95) : strike },
]
if (s.includes('call')) return [{ action: 'BUY', type: 'CALL', strike }]
if (s.includes('put')) return [{ action: 'BUY', type: 'PUT', strike }]
return [{ action: 'BUY', type: strategy.toUpperCase(), strike }]
}
// ── Sub-components ────────────────────────────────────────────────────────────
function BucketBar({ score, max }: { score: number; max: number }) {
const pct = max > 0 ? (score / max) * 100 : 0
const color = pct >= 75 ? 'bg-emerald-500' : pct >= 50 ? 'bg-yellow-500' : 'bg-red-500/70'
return (
<div className="w-12 bg-dark-600 rounded-full h-1 shrink-0">
<div className={clsx('h-1 rounded-full transition-all', color)} style={{ width: `${Math.min(pct, 100)}%` }} />
</div>
)
}
function ScorePillars({ buckets }: { buckets: any[] }) {
if (!buckets?.length) return null
return (
<div className="grid grid-cols-4 gap-1 mb-2">
{buckets.map((b: any) => {
const pct = b.max > 0 ? Math.round((b.score / b.max) * 100) : 0
const color = pct >= 70 ? 'bg-emerald-500' : pct >= 45 ? 'bg-yellow-500' : 'bg-red-500/70'
return (
<div key={b.id} className="flex flex-col items-center gap-0.5">
<div className="w-full bg-slate-700/60 rounded-full h-1">
<div className={clsx('h-1 rounded-full', color)} style={{ width: `${pct}%` }} />
</div>
<span className="text-[9px] text-slate-600 truncate w-full text-center leading-none">
{BUCKET_ICONS[b.id]} {b.score}/{b.max}
</span>
</div>
)
})}
</div>
)
}
function ScoreJustification({ buckets, keyCatalyst }: { buckets: any[]; keyCatalyst?: string }) {
return (
<div className="space-y-2 text-xs">
{keyCatalyst && (
<div className="flex gap-1.5 bg-yellow-400/5 border border-yellow-400/20 rounded px-2 py-1.5">
<span className="text-yellow-400 shrink-0">🔑</span>
<p className="text-yellow-200/80 leading-snug">{keyCatalyst}</p>
</div>
)}
{buckets.map((b: any) => {
const pct = b.max > 0 ? Math.round((b.score / b.max) * 100) : 0
const sc = pct >= 70 ? 'text-emerald-400' : pct >= 45 ? 'text-yellow-400' : 'text-red-400'
return (
<div key={b.id} className="bg-dark-700/60 rounded p-2 space-y-1.5">
<div className="flex items-center gap-1.5">
<span className="text-base leading-none">{BUCKET_ICONS[b.id] ?? '•'}</span>
<span className="text-slate-300 font-medium flex-1">{b.label}</span>
<BucketBar score={b.score} max={b.max} />
<span className={clsx('font-mono text-right shrink-0 font-bold', sc)}>{b.score}/{b.max}</span>
</div>
{b.comment && (
<p className="text-slate-400 leading-snug pl-1 border-l-2 border-slate-600">{b.comment}</p>
)}
{b.subs?.length > 0 && (
<div className="pl-2 space-y-1.5 border-l border-slate-700/50 ml-1">
{b.subs.map((sub: any) => {
const sp = sub.max > 0 ? Math.round((sub.score / sub.max) * 100) : 0
const ssc = sp >= 70 ? 'text-emerald-400' : sp >= 45 ? 'text-yellow-400' : 'text-slate-500'
return (
<div key={sub.id} className="space-y-0.5">
<div className="flex items-center gap-1">
<span className="text-[11px]">{BUCKET_ICONS[sub.id] ?? ''}</span>
<span className="text-slate-500 flex-1 truncate">{sub.label}</span>
<span className={clsx('font-mono text-[10px] shrink-0', ssc)}>{sub.score}/{sub.max}</span>
</div>
{sub.comment && (
<p className="text-slate-600 leading-snug text-[10px] pl-3">{sub.comment}</p>
)}
</div>
)
})}
</div>
)}
</div>
)
})}
</div>
)
}
function IBKRTicket({ strategy, underlying, strikeGuidance, expiryDays, entryPrice, maxLoss, targetGain, timing, invalidation }: {
strategy: string; underlying: string; strikeGuidance?: string | null
expiryDays?: number | null; entryPrice?: number | null
maxLoss?: number | null; targetGain?: number | null
timing?: string | null; invalidation?: string | null
}) {
const strike = entryPrice && strikeGuidance
? computeStrikeDollars(entryPrice, strikeGuidance, strategy)
: (entryPrice ?? null)
const expiryDate = expiryDays ? estimateExpiryDate(expiryDays) : null
const legs = buildLegs(strategy, strike, entryPrice ?? null)
const fmtStrike = (s: number | null | undefined) =>
s == null ? '—' : s >= 100 ? `$${s.toFixed(0)}` : `$${s.toFixed(2)}`
return (
<div className="bg-blue-950/20 border border-blue-700/30 rounded p-3 space-y-2.5">
<div className="text-[10px] font-bold text-blue-300 uppercase tracking-wide flex items-center gap-1.5">
<Terminal className="w-3 h-3" /> Ticket IBKR
{!entryPrice && <span className="ml-auto text-slate-600 font-normal normal-case">Prix non disponible vérifier dans IBKR</span>}
</div>
<div className="grid grid-cols-3 gap-3 text-[10px]">
<div>
<div className="text-slate-600 mb-0.5">Sous-jacent</div>
<div className="text-white font-mono font-bold text-sm">{underlying}</div>
<div className="text-slate-600 text-[9px]">Security Type: OPT</div>
</div>
<div>
<div className="text-slate-600 mb-0.5">Stratégie</div>
<div className="text-slate-200 font-semibold">{strategy}</div>
{entryPrice && <div className="text-slate-600 text-[9px]">Sous-jacent: ${entryPrice.toFixed(2)}</div>}
</div>
<div>
<div className="text-slate-600 mb-0.5">Expiration</div>
<div className="text-slate-200 font-mono">
{expiryDate ? format(expiryDate, 'd MMM yyyy', { locale: fr }) : expiryDays ? `~${expiryDays}j` : '—'}
</div>
<div className="text-slate-600 text-[9px]">{expiryDays ? `${expiryDays}j DTE` : ''}</div>
</div>
</div>
<div className="space-y-1">
{legs.map((leg, i) => (
<div key={i} className="flex items-center gap-2 bg-slate-900/70 rounded px-2.5 py-1.5 font-mono text-[10px]">
<span className={clsx('w-8 font-bold', leg.action === 'BUY' ? 'text-emerald-400' : 'text-red-400')}>{leg.action}</span>
<span className="text-slate-400">1 contrat</span>
<span className={clsx('font-bold w-10 text-center', leg.type === 'CALL' ? 'text-blue-300' : leg.type === 'PUT' ? 'text-orange-300' : 'text-slate-300')}>{leg.type}</span>
<span className="text-slate-600">Strike</span>
<span className="text-yellow-300 font-bold">{fmtStrike(leg.strike)}</span>
{leg.strike === null && strikeGuidance && <span className="text-slate-500">({strikeGuidance})</span>}
</div>
))}
</div>
<div className="grid grid-cols-3 gap-3 text-[10px]">
<div>
<div className="text-slate-600 mb-0.5">Type d'ordre</div>
<div className="text-slate-300 font-semibold">LIMIT</div>
<div className="text-slate-600 text-[9px]">Prix = prime dans IBKR</div>
</div>
<div>
<div className="text-slate-600 mb-0.5">Budget max</div>
<div className="text-slate-300 font-mono">{maxLoss ? `${Math.abs(maxLoss).toFixed(0)}€` : '~1 000'}</div>
<div className="text-slate-600 text-[9px]">Perte max estimée</div>
</div>
<div>
<div className="text-slate-600 mb-0.5">Cible</div>
<div className="text-emerald-400 font-mono">{targetGain ? `+${targetGain.toFixed(0)}€` : ''}</div>
</div>
</div>
{timing && (
<div className="flex gap-1.5 text-[10px] bg-yellow-900/10 border border-yellow-700/20 rounded px-2 py-1.5">
<span className="text-yellow-400 shrink-0">⏱</span>
<span className="text-yellow-200/70 leading-snug">{timing}</span>
</div>
)}
{invalidation && (
<div className="flex gap-1.5 text-[10px] bg-red-900/10 border border-red-700/20 rounded px-2 py-1.5">
<span className="text-red-400 shrink-0">✗</span>
<span className="text-red-200/70 leading-snug">{invalidation}</span>
</div>
)}
</div>
)
}
export function TradeCard({ item, onAdd, macroInfo, addedInfo, profiles }: {
item: TradeItem
onAdd: (item: TradeItem) => void
macroInfo?: { dominant: string; label: string; color: string; emoji: string; assetBias: Record<string, string> } | null
addedInfo?: { entry_date: string; expiry_days?: number; entry_price?: number; strike_guidance?: string } | null
profiles?: any[]
}) {
const [expanded, setExpanded] = useState(false)
const { trade, patternName, assetClass, score, scoreInfo, scoreDelta, rankRationale, expectedMovePct } = item
const effectiveScore = score !== null ? Math.max(0, Math.min(100, score + (scoreDelta ?? 0))) : null
const gainPct = expectedMovePct ?? 0
const evNet = effectiveScore !== null && gainPct > 0
? (effectiveScore / 100) * (gainPct / 100) - (1 - effectiveScore / 100)
: null
const matchedProfile = useMemo(() => {
if (effectiveScore === null || !profiles?.length) return undefined
return profiles.find(prof =>
prof.enabled && effectiveScore >= prof.min_score && gainPct >= prof.min_gain_pct
) ?? null
}, [effectiveScore, gainPct, profiles])
const breakdown = scoreInfo?.score_breakdown ?? {}
const rationale = scoreInfo?.summary ?? trade.rationale ?? scoreInfo?.key_catalyst ?? ''
const maxLoss = trade.max_loss_eur ?? scoreInfo?.recommended_trade?.max_loss_eur
const target = maxLoss != null && gainPct > 0
? Math.round(Math.abs(maxLoss) * gainPct / 100)
: (trade.target_gain_eur ?? scoreInfo?.recommended_trade?.target_gain_eur)
const timing = trade.timing_note ?? scoreInfo?.recommended_trade?.timing_note
return (
<div className={clsx('card transition-all', {
'border-emerald-700/50': effectiveScore !== null && effectiveScore >= 70,
'border-yellow-700/30': effectiveScore !== null && effectiveScore >= 50 && effectiveScore < 70,
'border-slate-700/20': effectiveScore === null,
})}>
<div className="text-xs text-slate-600 line-clamp-1 mb-1 font-mono">{patternName}</div>
<div className="flex items-start justify-between mb-1.5">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1 flex-wrap">
<span className="badge badge-blue text-xs">{assetClass}</span>
{trade.underlying && <span className="text-sm text-white font-semibold font-mono">{trade.underlying}</span>}
{trade.isRecommended && effectiveScore !== null && (
<span className="text-xs text-yellow-400 bg-yellow-400/10 border border-yellow-400/30 rounded px-1">★ IA</span>
)}
</div>
{trade.strategy && <span className="badge badge-green text-xs mt-0.5">{trade.strategy}</span>}
</div>
{effectiveScore !== null ? (
<div className="ml-2 shrink-0 text-center min-w-[48px]">
<div className={clsx('text-2xl font-bold leading-none', scoreColor(effectiveScore))}>{effectiveScore}</div>
<div className="text-xs text-slate-600 flex items-center justify-center gap-0.5">
<span>/100</span>
{scoreDelta !== null && scoreDelta !== 0 && (
<span className={clsx('text-[10px] font-mono font-bold', scoreDelta > 0 ? 'text-emerald-400' : 'text-red-400')}>
{scoreDelta > 0 ? '+' : ''}{scoreDelta}
</span>
)}
</div>
{scoreInfo?.score_trend != null && (
<div className={clsx('text-[10px] font-mono font-bold mt-0.5', scoreInfo.score_trend > 0 ? 'text-emerald-400' : scoreInfo.score_trend < 0 ? 'text-red-400' : 'text-slate-600')}>
{scoreInfo.score_trend > 0 ? '+' : scoreInfo.score_trend < 0 ? '' : ''}{scoreInfo.score_trend !== 0 ? Math.abs(scoreInfo.score_trend) : ''}
</div>
)}
</div>
) : (
<span className="ml-2 shrink-0 text-xs text-slate-500 bg-dark-600 border border-slate-700/40 rounded px-1.5 py-0.5 whitespace-nowrap">à scorer</span>
)}
</div>
{effectiveScore !== null && (
<>
<div className="bg-dark-600 rounded-full h-1.5 mb-1.5">
<div className={clsx('h-1.5 rounded-full', scoreBg(effectiveScore))} style={{ width: `${effectiveScore}%` }} />
</div>
{scoreInfo?.buckets?.length > 0 && <ScorePillars buckets={scoreInfo.buckets} />}
</>
)}
{effectiveScore !== null && (
<div className="flex items-center gap-1.5 mb-2 text-[10px] flex-wrap">
<span className="text-slate-600">Gain:{' '}<span className={gainPct > 0 ? 'text-slate-300' : 'text-orange-400/80'}>{gainPct > 0 ? `${gainPct}%` : '?'}</span></span>
{evNet !== null ? (
<><span className="text-slate-700">·</span>
<span className={clsx('font-mono font-semibold', evNet >= 0 ? 'text-emerald-400' : 'text-orange-400')}>EV {evNet >= 0 ? '+' : ''}{(evNet * 100).toFixed(0)}%</span></>
) : gainPct === 0 ? (
<><span className="text-slate-700">·</span><span className="text-orange-400/70">EV ?</span></>
) : null}
{matchedProfile !== undefined && (
<><span className="text-slate-700">·</span>
{matchedProfile
? <span className="font-semibold" style={{ color: matchedProfile.color }}>● {matchedProfile.name}</span>
: <span className="text-red-400/80">✗ {gainPct === 0 ? 'Gain non défini' : 'Aucun profil'}</span>
}</>
)}
</div>
)}
{rankRationale && <div className="text-[10px] text-slate-600 italic mb-1.5 line-clamp-1">{rankRationale}</div>}
{macroInfo && macroInfo.dominant !== 'incertain' && (() => {
const bias = macroInfo.assetBias[assetClass] ?? 'neutral'
const bd = BIAS_DISPLAY[bias] ?? BIAS_DISPLAY['neutral']
return (
<div className="flex items-center gap-1.5 mb-2 text-[10px]">
<span style={{ color: macroInfo.color }}>{macroInfo.emoji} {macroInfo.label}</span>
<span className="text-slate-700">·</span>
<span style={{ color: bd.color }}>{bd.label}</span>
</div>
)
})()}
{rationale && <p className="text-xs text-slate-400 line-clamp-2 mb-2">{rationale}</p>}
{timing && <div className="text-xs text-yellow-400/80 mb-2">⏱ {timing}</div>}
{(maxLoss != null || target != null) && (
<div className="flex gap-2 text-xs mb-2">
{maxLoss != null && <span className="text-red-400">Max -{Math.abs(maxLoss)}€</span>}
{target != null && <span className="text-emerald-400">Cible +{target}€</span>}
{scoreInfo?.confidence && <span className="text-slate-600 ml-auto">conf. {scoreInfo.confidence}%</span>}
</div>
)}
{effectiveScore !== null && (scoreInfo?.buckets?.length > 0 || Object.keys(breakdown).length > 0) && (
<>
<button onClick={() => setExpanded(!expanded)}
className="flex items-center gap-1 text-xs text-slate-500 hover:text-slate-300 mb-1.5 transition-colors">
{expanded ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
{expanded ? 'Masquer la justification' : ' Justification du score'}
</button>
{expanded && (
<div className="mb-2">
{scoreInfo?.buckets?.length > 0 ? (
<ScoreJustification buckets={scoreInfo.buckets} keyCatalyst={scoreInfo.key_catalyst} />
) : (
<div className="space-y-1 bg-dark-700/50 rounded p-2">
{Object.entries(breakdown).map(([k, v]: [string, any]) => (
<div key={k} className="flex items-center justify-between text-xs">
<span className="text-slate-500 capitalize">{k.replace(/_/g, ' ')}</span>
<div className="flex items-center gap-1">
<div className="w-14 bg-dark-600 rounded-full h-1">
<div className="bg-blue-500 h-1 rounded-full" style={{ width: `${(v / 25) * 100}%` }} />
</div>
<span className="text-slate-300 w-5 text-right text-xs">{v}/25</span>
</div>
</div>
))}
</div>
)}
</div>
)}
</>
)}
{addedInfo && (
<div className="flex items-center gap-1.5 mb-2 text-[10px] text-emerald-400 bg-emerald-900/20 border border-emerald-700/30 rounded px-2 py-1">
<CheckCircle2 className="w-3 h-3 shrink-0" />
<span>Ajouté le {format(new Date(addedInfo.entry_date), "d MMM yyyy", { locale: fr })}</span>
</div>
)}
<button onClick={() => onAdd(item)}
className={clsx('w-full flex items-center justify-center gap-1 text-xs rounded py-1 transition-all border',
addedInfo
? 'text-slate-500 border-slate-700/30 hover:text-slate-300 hover:border-slate-600'
: 'text-blue-400 hover:text-white hover:bg-blue-600 border-blue-500/30 hover:border-blue-500'
)}>
<Plus className="w-3 h-3" />
{addedInfo ? 'Ajouter à nouveau' : 'Ajouter au portefeuille'}
</button>
</div>
)
}
export function TradeRow({ item, onAdd, macroInfo, addedInfo, profiles, rank }: {
item: TradeItem; onAdd: (item: TradeItem) => void
macroInfo?: { dominant: string; label: string; color: string; emoji: string; assetBias: Record<string, string> } | null
addedInfo?: { entry_date: string; expiry_days?: number; entry_price?: number; strike_guidance?: string } | null
profiles?: any[]; rank: number
}) {
const [expanded, setExpanded] = useState(false)
const { trade, patternName, assetClass, score, scoreInfo, scoreDelta, rankRationale, expectedMovePct } = item
const effectiveScore = score !== null ? Math.max(0, Math.min(100, score + (scoreDelta ?? 0))) : null
const gainPct = expectedMovePct ?? 0
const evNet = effectiveScore !== null && gainPct > 0
? (effectiveScore / 100) * (gainPct / 100) - (1 - effectiveScore / 100)
: null
const matchedProfile = useMemo(() => {
if (effectiveScore === null || !profiles?.length) return undefined
return profiles.find(p => p.enabled && effectiveScore >= p.min_score && gainPct >= p.min_gain_pct) ?? null
}, [effectiveScore, gainPct, profiles])
const bias = macroInfo?.assetBias[assetClass] ?? 'neutral'
const bd = BIAS_DISPLAY[bias] ?? BIAS_DISPLAY['neutral']
const maxLoss = trade.max_loss_eur ?? scoreInfo?.recommended_trade?.max_loss_eur
const target = maxLoss != null && gainPct > 0
? Math.round(Math.abs(maxLoss) * gainPct / 100)
: (trade.target_gain_eur ?? scoreInfo?.recommended_trade?.target_gain_eur)
const daysHeld = addedInfo?.entry_date
? Math.floor((Date.now() - new Date(addedInfo.entry_date).getTime()) / 86400000)
: null
const horizon = addedInfo?.expiry_days ?? null
const maturityPct = daysHeld !== null && horizon ? Math.min(100, Math.round(daysHeld / horizon * 100)) : null
const maturityColor = maturityPct === null ? 'text-slate-700'
: maturityPct >= 75 ? 'text-orange-400'
: maturityPct >= 35 ? 'text-emerald-400'
: maturityPct >= 10 ? 'text-yellow-400'
: 'text-slate-500'
return (
<Fragment>
<tr
className={clsx(
'border-b border-slate-800/60 cursor-pointer transition-colors text-xs',
expanded ? 'bg-dark-700/50' : 'hover:bg-dark-700/30',
effectiveScore !== null && effectiveScore >= 70 && 'border-l-2 border-l-emerald-600/50',
effectiveScore !== null && effectiveScore >= 50 && effectiveScore < 70 && 'border-l-2 border-l-yellow-600/30',
)}
onClick={() => setExpanded(v => !v)}
>
<td className="pl-3 py-2 text-slate-700 font-mono text-[10px] w-6">{rank}</td>
<td className="px-2 py-2 max-w-[180px]">
<div className="text-slate-400 truncate text-[10px] leading-tight">{patternName}</div>
<div className="flex items-center gap-1 mt-0.5">
<span className="badge badge-blue text-[10px] py-0">{assetClass}</span>
{trade.underlying && <span className="font-mono text-white text-[11px] font-semibold">{trade.underlying}</span>}
{trade.isRecommended && <span className="text-yellow-400 text-[10px]">★ IA</span>}
</div>
</td>
<td className="px-2 py-2">
{trade.strategy
? <span className={clsx('badge text-[10px] py-0', trade.direction === 'bearish' ? 'badge-red' : 'badge-green')}>{trade.strategy}</span>
: <span className="text-slate-700 text-[10px]">—</span>}
</td>
<td className="px-2 py-2 text-right">
{effectiveScore !== null ? (
<div className="flex flex-col items-end gap-0.5">
<span className={clsx('text-base font-bold leading-none', scoreColor(effectiveScore))}>{effectiveScore}</span>
{scoreDelta !== null && scoreDelta !== 0 && (
<span className={clsx('text-[10px] font-mono', scoreDelta > 0 ? 'text-emerald-400' : 'text-red-400')}>
{scoreDelta > 0 ? '+' : ''}{scoreDelta}
</span>
)}
</div>
) : <span className="text-slate-600 text-[10px]">à scorer</span>}
</td>
<td className="px-2 py-2 w-20">
{effectiveScore !== null && (
<div className="bg-dark-600 rounded-full h-1.5 w-full">
<div className={clsx('h-1.5 rounded-full', scoreBg(effectiveScore))} style={{ width: `${effectiveScore}%` }} />
</div>
)}
</td>
<td className="px-2 py-2 text-right font-mono">
{evNet !== null
? <span className={clsx('text-[11px] font-semibold', evNet >= 0 ? 'text-emerald-400' : 'text-orange-400')}>EV {evNet >= 0 ? '+' : ''}{(evNet * 100).toFixed(0)}%</span>
: <span className="text-slate-700 text-[10px]">—</span>}
</td>
<td className="px-2 py-2 text-right font-mono text-slate-400 text-[11px]">{gainPct > 0 ? `${gainPct}%` : ''}</td>
<td className="px-2 py-2 text-right text-[10px]">
{maxLoss != null && <div className="text-red-400">-{Math.abs(maxLoss)}€</div>}
{target != null && <div className="text-emerald-400">+{target}€</div>}
</td>
<td className="px-2 py-2 text-[10px]">
{matchedProfile !== undefined && (
matchedProfile
? <span className="font-semibold" style={{ color: matchedProfile.color }}>● {matchedProfile.name}</span>
: <span className="text-red-400/70">✗ Aucun</span>
)}
</td>
<td className="px-2 py-2 text-[10px]" style={{ color: bd.color }}>{bd.label}</td>
<td className="px-2 py-2 text-right text-[10px] whitespace-nowrap">
{addedInfo?.entry_date
? <span className="text-slate-500">{format(new Date(addedInfo.entry_date), "d MMM", { locale: fr })}</span>
: <span className="text-slate-800">—</span>}
</td>
<td className="px-2 py-2 text-right text-[10px]">
{daysHeld !== null ? (
<div className="flex flex-col items-end gap-0.5">
<span className={clsx('font-mono font-semibold', maturityColor)}>{daysHeld}j{horizon ? `/${horizon}j` : ''}</span>
{maturityPct !== null && (
<div className="w-12 h-1 bg-slate-800 rounded-full overflow-hidden">
<div className={clsx('h-full rounded-full', maturityPct >= 75 ? 'bg-orange-500' : maturityPct >= 35 ? 'bg-emerald-500' : 'bg-yellow-500')}
style={{ width: `${maturityPct}%` }} />
</div>
)}
</div>
) : <span className="text-slate-800">—</span>}
</td>
<td className="pr-3 py-2 text-slate-600 text-right">
{expanded ? <ChevronUp className="w-3.5 h-3.5 inline" /> : <ChevronDown className="w-3.5 h-3.5 inline" />}
</td>
</tr>
{expanded && (
<tr className="bg-dark-900/60">
<td colSpan={13} className="px-0 py-0 border-b border-blue-700/20">
<div className="p-4 space-y-3 text-xs">
<div className="grid grid-cols-3 gap-4">
<div className="col-span-2">
{scoreInfo?.key_catalyst && (
<div className="flex gap-1.5 bg-yellow-400/5 border border-yellow-400/20 rounded px-2 py-1.5 mb-3">
<span className="text-yellow-400 shrink-0">🔑</span>
<p className="text-yellow-200/80 leading-snug">{scoreInfo.key_catalyst}</p>
</div>
)}
{rankRationale && <p className="text-slate-500 italic mb-2">{rankRationale}</p>}
{scoreInfo?.summary && <p className="text-slate-400 mb-3">{scoreInfo.summary}</p>}
{scoreInfo?.buckets?.length > 0 && (
<ScoreJustification buckets={scoreInfo.buckets} keyCatalyst={undefined} />
)}
</div>
<div className="space-y-3">
{scoreInfo?.contra_signals?.length > 0 && (
<div className="bg-orange-900/10 border border-orange-700/20 rounded p-2 space-y-1">
<div className="text-orange-400 font-semibold uppercase text-[9px] tracking-wide">Contra signals</div>
{scoreInfo.contra_signals.slice(0, 3).map((cs: any, i: number) => (
<div key={i} className="text-orange-300/70 text-[10px] leading-snug">{cs.title ?? cs}</div>
))}
</div>
)}
{macroInfo && macroInfo.dominant !== 'incertain' && (
<div className="text-[11px]">
<span style={{ color: macroInfo.color }}>{macroInfo.emoji} {macroInfo.label}</span>
<span className="text-slate-600 mx-1">·</span>
<span style={{ color: bd.color }}>{bd.label}</span>
</div>
)}
{addedInfo && (
<div className="flex items-center gap-1.5 text-[10px] text-emerald-400 bg-emerald-900/20 border border-emerald-700/30 rounded px-2 py-1">
<CheckCircle2 className="w-3 h-3 shrink-0" />
Ajouté le {format(new Date(addedInfo.entry_date), "d MMM yyyy", { locale: fr })}
</div>
)}
<button
onClick={e => { e.stopPropagation(); onAdd(item) }}
className={clsx('w-full flex items-center justify-center gap-1 text-xs rounded py-1.5 transition-all border',
addedInfo
? 'text-slate-500 border-slate-700/30 hover:text-slate-300 hover:border-slate-600'
: 'text-blue-400 hover:text-white hover:bg-blue-600 border-blue-500/30 hover:border-blue-500'
)}>
<Plus className="w-3 h-3" />
{addedInfo ? 'Ajouter à nouveau' : 'Ajouter au portefeuille'}
</button>
</div>
</div>
{trade.strategy && trade.underlying && (
<IBKRTicket
strategy={trade.strategy}
underlying={trade.underlying}
strikeGuidance={trade.strike_guidance ?? scoreInfo?.recommended_trade?.strike_guidance ?? addedInfo?.strike_guidance}
expiryDays={scoreInfo?.recommended_trade?.expiry_days ?? trade.expiry_days ?? trade.horizon_days ?? addedInfo?.expiry_days}
entryPrice={addedInfo?.entry_price ?? null}
maxLoss={trade.max_loss_eur ?? scoreInfo?.recommended_trade?.max_loss_eur}
targetGain={trade.target_gain_eur ?? scoreInfo?.recommended_trade?.target_gain_eur}
timing={scoreInfo?.recommended_trade?.timing_note}
invalidation={scoreInfo?.recommended_trade?.invalidation}
/>
)}
</div>
</td>
</tr>
)}
</Fragment>
)
}
// ── Self-contained Trade Ideas Tab ────────────────────────────────────────────
export function TradeIdeasTab() {
const { data: allPatternsData } = useAllPatterns()
const { data: lastScoresData } = useLastScores()
const { data: aiStatus } = useAiStatus()
const { data: positions, refetch: refetchPositions } = usePortfolioPositions('open')
const { data: tradeMtmData } = useTradeMtm(30)
const { data: riskProfilesData } = useRiskProfiles()
const { data: macroData } = useMacroRegime()
const { mutate: scorePatterns, isPending: scoring } = useScorePatterns()
const { mutate: addPos } = useAddPosition()
const riskProfiles: any[] = (riskProfilesData as any)?.profiles ?? []
const allPatterns: any[] = allPatternsData ?? []
const scoredAt: string | null = lastScoresData?.scored_at ?? null
const [categoryFilter, setCategoryFilter] = useState('all')
const [topN, setTopN] = useState(10)
const [viewMode, setViewMode] = useState<'cards' | 'grid'>('grid')
const [toast, setToast] = useState<{ title: string; sub: string } | null>(null)
useEffect(() => {
if (!toast) return
const t = setTimeout(() => setToast(null), 3500)
return () => clearTimeout(t)
}, [toast])
const scoreMap = useMemo(() => {
const map: Record<string, any> = {}
for (const sp of (lastScoresData?.scored_patterns ?? [])) {
if (sp.pattern_id) map[sp.pattern_id] = sp
}
return map
}, [lastScoresData])
const macroInfo = useMemo(() => {
if (!macroData?.scenarios) return null
const sc = macroData.scenarios
const dom = sc.dominant ?? 'incertain'
const m = sc.meta?.[dom] ?? { label: dom, color: '#94a3b8', emoji: '?' }
return { dominant: dom, label: m.label, color: m.color, emoji: m.emoji, assetBias: sc.asset_bias?.[dom] ?? {} }
}, [macroData])
const addedMap = useMemo(() => {
const map: Record<string, { entry_date: string; expiry_days?: number }> = {}
const upsert = (key: string, entry_date: string, expiry_days?: number) => {
if (!map[key] || entry_date > map[key].entry_date) map[key] = { entry_date, expiry_days }
}
for (const pos of (positions as any[] ?? [])) {
const strategy = (pos.strategy ?? '').toLowerCase()
const trigger = (pos.geo_trigger ?? '').toLowerCase()
const underly = (pos.underlying ?? '').toLowerCase()
const expiry_days = pos.expiry_days ?? undefined
if (trigger) upsert(`trigger:${trigger}:${strategy}`, pos.entry_date ?? '', expiry_days)
if (underly) upsert(`ticker:${underly}:${strategy}`, pos.entry_date ?? '', expiry_days)
}
return map
}, [positions])
const mtmMap = useMemo(() => {
const map: Record<string, { entry_date: string; expiry_days?: number; entry_price?: number; strike_guidance?: string }> = {}
for (const t of ((tradeMtmData as any)?.trades ?? [])) {
const pid = t.pattern_id as string
if (!pid) continue
const entry_date = (t.entry_date as string) ?? ''
if (!map[pid] || entry_date > map[pid].entry_date)
map[pid] = { entry_date, expiry_days: t.expiry_days_at_entry ?? t.horizon_days, entry_price: t.entry_price, strike_guidance: t.strike_guidance }
}
return map
}, [tradeMtmData])
const { topScored, allUnscored } = useMemo(() => {
const filtered = allPatterns.filter(p => categoryFilter === 'all' || p.asset_class === categoryFilter)
const scored: TradeItem[] = []
const unscored: TradeItem[] = []
for (const p of filtered) {
const sp = scoreMap[p.id]
if (sp) {
const recUnderlying = sp.recommended_trade?.underlying
const trades: any[] = p.suggested_trades ?? []
const tradesOrFallback = trades.length > 0 ? trades : [sp.recommended_trade ?? {}]
const rankings: any[] = sp.trade_rankings ?? []
for (const t of tradesOrFallback) {
const ranking = rankings.find((r: any) =>
r.underlying === t.underlying && (!r.strategy || !t.strategy || r.strategy === t.strategy)
)
scored.push({
trade: { ...t, isRecommended: recUnderlying && t.underlying === recUnderlying },
patternName: p.name, patternId: p.id,
assetClass: t.asset_class ?? p.asset_class,
score: sp.score, scoreInfo: sp,
scoreDelta: ranking?.score_delta ?? null,
rankRationale: ranking?.rationale ?? null,
expectedMovePct: t.expected_move_pct ?? p.expected_move_pct ?? 0,
})
}
} else {
const trades: any[] = p.suggested_trades ?? []
if (trades.length === 0) {
unscored.push({ trade: {}, patternName: p.name, patternId: p.id, assetClass: p.asset_class, score: null, scoreInfo: null, scoreDelta: null, rankRationale: null, expectedMovePct: p.expected_move_pct ?? 0 })
} else {
for (const t of trades) {
unscored.push({ trade: t, patternName: p.name, patternId: p.id, assetClass: t.asset_class ?? p.asset_class, score: null, scoreInfo: null, scoreDelta: null, rankRationale: null, expectedMovePct: t.expected_move_pct ?? p.expected_move_pct ?? 0 })
}
}
}
}
const effScore = (item: TradeItem) => Math.max(0, Math.min(100, (item.score ?? 0) + (item.scoreDelta ?? 0)))
scored.sort((a, b) => effScore(b) - effScore(a))
const sliced = topN === 0 ? scored : scored.slice(0, topN)
return { topScored: sliced, allUnscored: unscored }
}, [allPatterns, scoreMap, categoryFilter, topN])
const handleAdd = (item: TradeItem) => {
const t = item.trade
const sp = item.scoreInfo
addPos({
title: `${t.strategy ?? ''} ${t.underlying ?? ''} — ${item.patternName}`.trim(),
underlying: t.underlying ?? item.patternName,
strategy: t.strategy ?? '',
asset_class: item.assetClass,
expiry_days: t.expiry_days ?? sp?.recommended_trade?.expiry_days ?? 90,
capital_invested: Math.abs(t.max_loss_eur ?? sp?.recommended_trade?.max_loss_eur ?? 1000),
geo_trigger: item.patternName,
rationale: t.rationale ?? sp?.key_catalyst ?? '',
legs: [{ option_type: (t.strategy ?? '').toLowerCase().includes('put') ? 'put' : 'call', quantity: 1, position: 'long' }],
}, {
onSuccess: () => {
refetchPositions()
setToast({ title: 'Ajouté au portefeuille', sub: `${t.strategy ?? ''} ${t.underlying ?? ''} · ${item.patternName}`.trim() })
},
})
}
const getAddedInfo = (item: TradeItem) => {
const strategy = (item.trade.strategy ?? '').toLowerCase()
const trigger = (item.patternName ?? '').toLowerCase()
const underly = (item.trade.underlying ?? '').toLowerCase()
return addedMap[`trigger:${trigger}:${strategy}`] ?? addedMap[`ticker:${underly}:${strategy}`] ?? mtmMap[item.patternId] ?? null
}
return (
<div className="space-y-4">
{/* Toolbar */}
<div className="flex items-center justify-between gap-3 flex-wrap">
<div className="flex items-center gap-2 flex-wrap">
<h2 className="section-title flex items-center gap-1 mb-0">
<Target className="w-3 h-3" /> Idées de trade
</h2>
{scoredAt && (
<span className="text-xs text-slate-600">— scoré le {format(new Date(scoredAt), "d MMM à HH:mm", { locale: fr })}</span>
)}
{topScored.length > 0 && (
<span className="text-xs text-emerald-500">{topScored.length} scoré{topScored.length > 1 ? 's' : ''} · {allUnscored.length} à scorer</span>
)}
</div>
<div className="flex items-center gap-2 flex-wrap">
{/* Category filter */}
<div className="flex items-center gap-0.5 bg-dark-700 rounded p-0.5">
{CATEGORIES.map(c => (
<button key={c.key} onClick={() => setCategoryFilter(c.key)}
className={clsx('px-2 py-1 rounded text-xs transition-colors', {
'bg-blue-600 text-white': categoryFilter === c.key,
'text-slate-400 hover:text-slate-200': categoryFilter !== c.key,
})}>
{c.label}
</button>
))}
</div>
{/* Top N */}
<div className="flex items-center gap-0.5 bg-dark-700 rounded p-0.5">
{[5, 10, 20, 0].map(n => (
<button key={n} onClick={() => setTopN(n)}
className={clsx('px-2 py-1 rounded text-xs transition-colors', {
'bg-slate-600 text-white': topN === n,
'text-slate-400 hover:text-slate-200': topN !== n,
})}>
{n === 0 ? 'Tous' : `Top ${n}`}
</button>
))}
</div>
{/* View toggle */}
<div className="flex items-center gap-0.5 bg-dark-700 rounded p-0.5">
<button onClick={() => setViewMode('cards')}
className={clsx('p-1.5 rounded transition-colors', viewMode === 'cards' ? 'bg-slate-600 text-white' : 'text-slate-500 hover:text-slate-300')}
title="Vue cartes"><LayoutGrid className="w-3.5 h-3.5" /></button>
<button onClick={() => setViewMode('grid')}
className={clsx('p-1.5 rounded transition-colors', viewMode === 'grid' ? 'bg-slate-600 text-white' : 'text-slate-500 hover:text-slate-300')}
title="Vue grille"><List className="w-3.5 h-3.5" /></button>
</div>
{/* Score button */}
{aiStatus?.enabled ? (
<button onClick={() => scorePatterns({})}
disabled={scoring}
className="flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white px-3 py-1.5 rounded text-xs font-semibold transition-all">
{scoring ? <><RefreshCw className="w-3 h-3 animate-spin" /> Scoring GPT-4o...</> : <><Brain className="w-3 h-3" /> Scorer les patterns</>}
</button>
) : (
<span className="text-xs text-slate-600 border border-slate-700/30 rounded px-2 py-1">Clé OpenAI requise</span>
)}
</div>
</div>
{/* Scored trades */}
{topScored.length > 0 && (
viewMode === 'cards' ? (
<div className="grid grid-cols-5 gap-3 mb-5">
{topScored.map((item, i) => (
<TradeCard key={`${item.patternId}-scored-${i}`} item={item} onAdd={handleAdd} macroInfo={macroInfo} addedInfo={getAddedInfo(item)} profiles={riskProfiles} />
))}
</div>
) : (
<div className="overflow-x-auto rounded-lg border border-slate-700/40 mb-5">
<table className="w-full text-xs">
<thead>
<tr className="border-b border-slate-700/60 text-slate-600 text-[10px] uppercase tracking-wide">
<th className="pl-3 py-2 text-left w-6">#</th>
<th className="px-2 py-2 text-left">Pattern / Ticker</th>
<th className="px-2 py-2 text-left">Stratégie</th>
<th className="px-2 py-2 text-right">Score</th>
<th className="px-2 py-2 w-20"></th>
<th className="px-2 py-2 text-right">EV</th>
<th className="px-2 py-2 text-right">Gain</th>
<th className="px-2 py-2 text-right">Max/Cible</th>
<th className="px-2 py-2">Profil</th>
<th className="px-2 py-2">Macro</th>
<th className="px-2 py-2 text-right">Entrée</th>
<th className="px-2 py-2 text-right">Durée</th>
<th className="pr-3 py-2 w-6"></th>
</tr>
</thead>
<tbody>
{topScored.map((item, i) => (
<TradeRow key={`${item.patternId}-scored-${i}`} item={item} onAdd={handleAdd} macroInfo={macroInfo} addedInfo={getAddedInfo(item)} profiles={riskProfiles} rank={i + 1} />
))}
</tbody>
</table>
</div>
)
)}
{/* Unscored trades */}
{allUnscored.length > 0 && (
<>
{topScored.length > 0 && (
<div className="flex items-center gap-2 text-xs text-slate-600 mb-3">
<span className="border-b border-slate-700/30 flex-1"></span>
{allUnscored.length} trade{allUnscored.length > 1 ? 's' : ''} à scorer
<span className="border-b border-slate-700/30 flex-1"></span>
</div>
)}
{viewMode === 'cards' ? (
<div className="grid grid-cols-5 gap-3">
{allUnscored.map((item, i) => (
<TradeCard key={`${item.patternId}-unscored-${i}`} item={item} onAdd={handleAdd} macroInfo={macroInfo} addedInfo={getAddedInfo(item)} profiles={riskProfiles} />
))}
</div>
) : (
<div className="overflow-x-auto rounded-lg border border-slate-700/40">
<table className="w-full text-xs">
<tbody>
{allUnscored.map((item, i) => (
<TradeRow key={`${item.patternId}-unscored-${i}`} item={item} onAdd={handleAdd} macroInfo={macroInfo} addedInfo={getAddedInfo(item)} profiles={riskProfiles} rank={topScored.length + i + 1} />
))}
</tbody>
</table>
</div>
)}
</>
)}
{allPatterns.length === 0 && (
<div className="card text-center py-8 text-slate-500 text-sm">
Démarrer le backend patterns en cours de chargement
</div>
)}
{/* Toast */}
{toast && (
<div className="fixed bottom-6 right-6 z-50 flex items-start gap-3 bg-emerald-950 border border-emerald-600/50 text-emerald-200 rounded-xl px-4 py-3 shadow-2xl min-w-[260px]">
<CheckCircle2 className="w-5 h-5 text-emerald-400 shrink-0 mt-0.5" />
<div>
<div className="text-sm font-semibold text-emerald-300">{toast.title}</div>
<div className="text-xs text-emerald-500 mt-0.5 line-clamp-2">{toast.sub}</div>
</div>
</div>
)}
</div>
)
}

View File

@@ -348,6 +348,7 @@ export default function Config() {
const [fredKey, setFredKey] = useState('')
const [showKeys, setShowKeys] = useState(false)
const [savedMsg, setSavedMsg] = useState('')
const [configTab, setConfigTab] = useState<'ia' | 'cycle' | 'sources' | 'journal' | 'profiles'>('ia')
// Analysis config local state
const [analysisTopN, setAnalysisTopN] = useState(10)
@@ -428,24 +429,48 @@ export default function Config() {
})
}
const CONFIG_TABS = [
{ key: 'ia', label: 'IA & Analyse', icon: Brain },
{ key: 'cycle', label: 'Auto-Cycle & Logging', icon: Zap },
{ key: 'sources', label: 'Sources', icon: Globe },
{ key: 'journal', label: 'Journal & Sortie', icon: Lock },
{ key: 'profiles', label: 'Profils de risque', icon: SlidersHorizontal },
] as const
return (
<div className="p-6 space-y-6">
<div>
<h1 className="text-xl font-bold text-white flex items-center gap-2">
<Settings className="w-5 h-5 text-blue-400" /> Configuration
</h1>
<p className="text-xs text-slate-500 mt-0.5">Clés API, sources d'information, paramètres IA</p>
<div className="p-6 space-y-5">
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold text-white flex items-center gap-2">
<Settings className="w-5 h-5 text-blue-400" /> Configuration
</h1>
<p className="text-xs text-slate-500 mt-0.5">Clés API, sources, paramètres IA &amp; cycle</p>
</div>
{savedMsg && (
<div className="flex items-center gap-2 text-emerald-400 text-sm bg-emerald-900/10 border border-emerald-700/30 rounded px-3 py-1.5">
<CheckCircle className="w-4 h-4" /> {savedMsg}
</div>
)}
</div>
{savedMsg && (
<div className="card border-emerald-700/40 bg-emerald-900/10 text-emerald-400 text-sm flex items-center gap-2">
<CheckCircle className="w-4 h-4" /> {savedMsg}
</div>
)}
{/* Tab navigation */}
<div className="flex gap-1 bg-dark-700 p-1 rounded w-fit">
{CONFIG_TABS.map(({ key, label, icon: Icon }) => (
<button key={key} onClick={() => setConfigTab(key as any)}
className={clsx('flex items-center gap-1.5 px-3 py-1.5 rounded text-sm transition-colors', {
'bg-blue-600 text-white': configTab === key,
'text-slate-400 hover:text-slate-200': configTab !== key,
})}>
<Icon className="w-3.5 h-3.5" />
{label}
</button>
))}
</div>
<div className="grid grid-cols-3 gap-6">
{/* Left col: API Keys + AI status */}
<div className="col-span-1 space-y-4">
{/* ── IA & Analyse ── */}
{configTab === 'ia' && (
<div className="grid grid-cols-2 gap-6">
<div className="space-y-4">
{/* AI Status */}
<div className={clsx('card', aiStatus?.enabled ? 'border-emerald-700/40' : 'border-slate-700/40')}>
<div className="section-title flex items-center gap-1"><Key className="w-3 h-3" /> Statut IA</div>
@@ -468,30 +493,328 @@ export default function Config() {
</div>
</div>
{/* API Keys */}
{/* OpenAI API Key */}
<div className="card">
<div className="flex items-center justify-between mb-3">
<div className="section-title mb-0 flex items-center gap-1"><Key className="w-3 h-3" /> Clés API</div>
<div className="section-title mb-0 flex items-center gap-1"><Key className="w-3 h-3" /> Clé OpenAI</div>
<button onClick={() => setShowKeys(!showKeys)} className="text-slate-500 hover:text-slate-300">
{showKeys ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
</button>
</div>
<div className="space-y-3">
<div>
<div className="flex items-center justify-between mb-1">
<label className="text-xs text-slate-500">OpenAI API Key</label>
<span className={clsx('text-xs', aiStatus?.enabled ? 'text-emerald-400' : 'text-slate-600')}>
{aiStatus?.enabled ? '✓ Actif' : '○ Inactif'}
</span>
</div>
<input
type={showKeys ? 'text' : 'password'}
value={openaiKey}
onChange={e => setOpenaiKey(e.target.value)}
placeholder="sk-proj-..."
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-xs text-white focus:outline-none focus:border-blue-500 font-mono"
/>
</div>
<button
onClick={saveKeys}
disabled={savingKeys || !openaiKey}
className="w-full bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white rounded py-1.5 text-xs font-semibold flex items-center justify-center gap-1.5"
>
<Save className="w-3.5 h-3.5" />
{savingKeys ? 'Sauvegarde...' : 'Sauvegarder la clé'}
</button>
</div>
</div>
</div>
{/* Right: Analysis params */}
<div className="card">
<h2 className="text-base font-bold text-white flex items-center gap-2 mb-4">
<SlidersHorizontal className="w-4 h-4 text-blue-400" /> Paramètres d'analyse IA
</h2>
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<label className="text-xs text-slate-500 mb-1 block">Top N résultats par défaut</label>
<div className="flex gap-1">
{[5, 10, 15, 20].map(n => (
<button key={n} onClick={() => setAnalysisTopN(n)}
className={clsx('flex-1 py-1.5 rounded text-sm transition-colors', {
'bg-blue-600 text-white': analysisTopN === n,
'bg-dark-700 text-slate-400 hover:text-slate-200': analysisTopN !== n,
})}>
Top {n}
</button>
))}
</div>
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Catégorie par défaut</label>
<select value={analysisCategoryDefault} onChange={e => setAnalysisCategoryDefault(e.target.value)}
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500">
<option value="all">Toutes les catégories</option>
<option value="energy">Énergie</option>
<option value="metals">Métaux</option>
<option value="agriculture">Agriculture</option>
<option value="indices">Indices</option>
<option value="equities">Actions</option>
<option value="forex">Forex</option>
</select>
</div>
</div>
<div className="mb-4">
<div className="flex items-center justify-between mb-1">
<label className="text-xs text-slate-500">Template d'analyse (prompt GPT-4o)</label>
<button onClick={() => setAnalysisTemplate('')} className="text-xs text-slate-600 hover:text-slate-400">
Par défaut
</button>
</div>
<textarea
value={analysisTemplate}
onChange={e => setAnalysisTemplate(e.target.value)}
rows={8}
placeholder="Laisser vide pour utiliser le template par défaut..."
className="w-full bg-dark-700 border border-slate-700 rounded px-3 py-2 text-sm text-white font-mono focus:outline-none focus:border-blue-500 resize-y"
/>
<div className="text-xs text-slate-600 mt-1">
Contexte marché (prix, IV, variation 1j) et news filtrées sont injectés automatiquement.
</div>
</div>
<button
onClick={() => saveAnalysis(
{ top_n: analysisTopN, category_filter: analysisCategoryDefault, template: analysisTemplate || undefined },
{ onSuccess: () => { setSavedMsg('Paramètres d\'analyse sauvegardés'); setTimeout(() => setSavedMsg(''), 2000) } }
)}
disabled={savingAnalysis}
className="flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
<Save className="w-4 h-4" />
{savingAnalysis ? 'Sauvegarde...' : 'Sauvegarder'}
</button>
</div>
</div>
)}
{/* ── Auto-Cycle & Logging ── */}
{configTab === 'cycle' && (
<div className="space-y-5">
{/* Score min threshold — prominent */}
<div className="card border-blue-700/40 bg-blue-900/5">
<h2 className="text-base font-bold text-white flex items-center gap-2 mb-1">
<SlidersHorizontal className="w-4 h-4 text-blue-400" /> Score minimum pour le cycle
</h2>
<p className="text-xs text-slate-500 mb-4">
Pré-filtre du cycle les patterns avec un score inférieur ne sont pas soumis à l'analyse.
Les profils de risque déterminent ensuite quels trades sont loggés.
</p>
<div className="grid grid-cols-2 gap-6">
<div>
<label className="text-xs text-slate-500 mb-2 block">
Score minimum pre-cycle : <span className="text-blue-300 font-mono font-bold text-lg">{minScore}</span>
<span className="text-slate-600 ml-1">/100</span>
</label>
<input type="range" min="0" max="80" step="5"
value={minScore}
onChange={e => setMinScore(parseInt(e.target.value))}
className="w-full accent-blue-500" />
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
<span>0 (tous)</span><span>80 (très sélectif)</span>
</div>
<div className="text-xs text-slate-500 mt-2">
{minScore === 0
? 'Tous les patterns analysés par le cycle'
: `Seuls les patterns avec score ≥ ${minScore} sont soumis au cycle`}
</div>
</div>
<div>
<label className="text-xs text-slate-500 mb-2 block">
EV minimum : <span className="text-blue-300 font-mono font-bold">{minEv >= 0 ? '+' : ''}{(minEv * 100).toFixed(0)}%</span>
<span className="text-slate-600 ml-1">net attendu</span>
</label>
<input type="range" min="-0.5" max="0.5" step="0.05"
value={minEv}
onChange={e => setMinEv(parseFloat(e.target.value))}
className="w-full accent-blue-500" />
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
<span>-50%</span><span>+50%</span>
</div>
<div className="text-xs text-slate-500 mt-2">
{minEv === 0
? 'Toute EV nette acceptée'
: minEv > 0
? `EV nette ≥ +${(minEv * 100).toFixed(0)}% requise`
: `Filtre désactivé (EV < ${(minEv * 100).toFixed(0)}%)`}
</div>
</div>
</div>
</div>
{/* Auto-Cycle settings */}
<div className="card">
<div className="flex items-center justify-between mb-4">
<h2 className="text-base font-bold text-white flex items-center gap-2">
<Zap className="w-4 h-4 text-blue-400" /> Auto-Cycle Intelligence
</h2>
{cs && (
<div className="flex items-center gap-2 text-xs">
{cs.running ? (
<span className="flex items-center gap-1 text-blue-400 bg-blue-900/30 border border-blue-700/30 px-2 py-0.5 rounded animate-pulse">
<RefreshCw className="w-3 h-3 animate-spin" /> En cours...
</span>
) : cs.scheduler_alive ? (
<span className="text-emerald-400 bg-emerald-900/30 border border-emerald-700/30 px-2 py-0.5 rounded">
● Scheduler actif
</span>
) : (
<span className="text-slate-600 bg-dark-700 border border-slate-700/30 px-2 py-0.5 rounded">
○ Scheduler inactif
</span>
)}
</div>
)}
</div>
<p className="text-xs text-slate-500 mb-4">
Toutes les N heures : suggère de nouveaux patterns, filtre les doublons, score tout, log les prix et génère un commentaire IA.
</p>
<div className="grid grid-cols-3 gap-4 mb-4">
<div>
<label className="text-xs text-slate-500 mb-2 block">Activer l'auto-cycle</label>
<button
onClick={() => setCycleEnabled(!cycleEnabled)}
className={clsx('w-full py-2 rounded border text-sm font-semibold transition-all', {
'bg-blue-600 border-blue-500 text-white': cycleEnabled,
'bg-dark-700 border-slate-700 text-slate-400 hover:border-slate-500': !cycleEnabled,
})}>
{cycleEnabled ? '✓ Activé' : '○ Désactivé'}
</button>
</div>
<div>
<label className="text-xs text-slate-500 mb-2 block">Intervalle (heures)</label>
<div className="flex gap-1">
{[1, 3, 6, 12].map(h => (
<button key={h} onClick={() => setCycleHours(h)}
className={clsx('flex-1 py-2 rounded text-sm transition-colors', {
'bg-blue-600 text-white': cycleHours === h,
'bg-dark-700 text-slate-400 hover:text-slate-200': cycleHours !== h,
})}>
{h}h
</button>
))}
</div>
</div>
<div>
<label className="text-xs text-slate-500 mb-2 block">
Similarité max ({Math.round(cycleSimilarity * 100)}%)
</label>
<input type="range" min="0.1" max="0.8" step="0.05"
value={cycleSimilarity}
onChange={e => setCycleSimilarity(parseFloat(e.target.value))}
className="w-full accent-blue-500" />
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
<span>10% (strict)</span><span>80% (permissif)</span>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<label className="text-xs text-slate-500 mb-2 block">
Rétention Journal ({retentionDays}j)
</label>
<div className="flex gap-1">
{[30, 60, 90, 180].map(d => (
<button key={d} onClick={() => setRetentionDays(d)}
className={clsx('flex-1 py-2 rounded text-sm transition-colors', {
'bg-blue-600 text-white': retentionDays === d,
'bg-dark-700 text-slate-400 hover:text-slate-200': retentionDays !== d,
})}>
{d}j
</button>
))}
</div>
</div>
<div>
<label className="text-xs text-slate-500 mb-2 block">
Seuil maturité ({maturityThreshold}%)
</label>
<div className="flex gap-1">
{[20, 30, 35, 50].map(p => (
<button key={p} onClick={() => setMaturityThreshold(p)}
className={clsx('flex-1 py-2 rounded text-sm transition-colors', {
'bg-blue-600 text-white': maturityThreshold === p,
'bg-dark-700 text-slate-400 hover:text-slate-200': maturityThreshold !== p,
})}>
{p}%
</button>
))}
</div>
</div>
</div>
{cs?.last_cycle && (
<div className="card bg-dark-700/50 mb-2 text-xs space-y-2">
<div className="flex items-center gap-4 flex-wrap">
<span className="text-slate-500">Dernier cycle :</span>
<span className="text-slate-300">{cs.last_cycle.started_at?.slice(0, 16)} UTC</span>
<span className={clsx('badge', cs.last_cycle.status === 'completed' ? 'badge-green' : 'badge-red')}>
{cs.last_cycle.status}
</span>
<span className="text-slate-500">+{cs.last_cycle.patterns_added} patterns</span>
<span className="text-slate-500">{cs.last_cycle.patterns_scored} scorés</span>
<span className="text-slate-500">Géo: {cs.last_cycle.geo_score}</span>
<span className="text-slate-500 capitalize">{cs.last_cycle.dominant_regime}</span>
</div>
</div>
)}
{cs?.enabled && cs?.next_run_at && !cs?.running && (
<div className="card bg-dark-700/50 mb-4 text-xs">
<NextRunCountdown nextRunAt={cs.next_run_at} />
</div>
)}
<div className="flex items-center gap-3">
<button
onClick={() => updateCycleConfig(
{ enabled: cycleEnabled, interval_hours: cycleHours, similarity_threshold: cycleSimilarity, min_ev_threshold: minEv, min_score_threshold: minScore, journal_retention_days: retentionDays, maturity_threshold_pct: maturityThreshold },
{ onSuccess: () => { refetchCycle(); setSavedMsg('Auto-cycle configuré'); setTimeout(() => setSavedMsg(''), 2000) } }
)}
disabled={savingCycle}
className="flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
<Save className="w-4 h-4" />
{savingCycle ? 'Sauvegarde...' : 'Appliquer'}
</button>
<button
onClick={() => triggerCycle(undefined, { onSuccess: () => { refetchCycle(); setSavedMsg('Cycle lancé !'); setTimeout(() => setSavedMsg(''), 3000) } })}
disabled={triggeringCycle || !aiStatus?.enabled}
className="flex items-center gap-1.5 border border-blue-500/50 text-blue-400 hover:bg-blue-900/20 disabled:opacity-40 px-4 py-2 rounded text-sm font-semibold">
<RefreshCw className={clsx('w-4 h-4', triggeringCycle && 'animate-spin')} />
Lancer maintenant
</button>
</div>
</div>
</div>
)}
{/* ── Sources ── */}
{configTab === 'sources' && (
<div className="space-y-5">
{/* Data API Keys */}
<div className="card">
<div className="flex items-center justify-between mb-3">
<div className="section-title mb-0 flex items-center gap-1"><Key className="w-3 h-3" /> Clés API données</div>
<button onClick={() => setShowKeys(!showKeys)} className="text-slate-500 hover:text-slate-300">
{showKeys ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
</button>
</div>
<div className="grid grid-cols-3 gap-4">
{[
{ label: 'OpenAI API Key', value: openaiKey, setter: setOpenaiKey, placeholder: 'sk-proj-...', status: aiStatus?.enabled },
{ label: 'NewsAPI Key', value: newsapiKey, setter: setNewsapiKey, placeholder: 'Obtenir sur newsapi.org', status: false },
{ label: 'EIA API Key', value: eiaKey, setter: setEiaKey, placeholder: 'Obtenir sur eia.gov', status: false },
{ label: 'FRED API Key', value: fredKey, setter: setFredKey, placeholder: 'Obtenir sur fred.stlouisfed.org', status: false },
].map(({ label, value, setter, placeholder, status }) => (
{ label: 'NewsAPI Key', value: newsapiKey, setter: setNewsapiKey, placeholder: 'Obtenir sur newsapi.org' },
{ label: 'EIA API Key', value: eiaKey, setter: setEiaKey, placeholder: 'Obtenir sur eia.gov' },
{ label: 'FRED API Key', value: fredKey, setter: setFredKey, placeholder: 'Obtenir sur fred.stlouisfed.org' },
].map(({ label, value, setter, placeholder }) => (
<div key={label}>
<div className="flex items-center justify-between mb-1">
<label className="text-xs text-slate-500">{label}</label>
{status !== undefined && (
<span className={clsx('text-xs', status ? 'text-emerald-400' : 'text-slate-600')}>
{status ? ' Actif' : ' Inactif'}
</span>
)}
</div>
<label className="text-xs text-slate-500 mb-1 block">{label}</label>
<input
type={showKeys ? 'text' : 'password'}
value={value}
@@ -501,20 +824,17 @@ export default function Config() {
/>
</div>
))}
<button
onClick={saveKeys}
disabled={savingKeys || (!openaiKey && !newsapiKey && !eiaKey && !fredKey)}
className="w-full bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white rounded py-1.5 text-xs font-semibold flex items-center justify-center gap-1.5"
>
<Save className="w-3.5 h-3.5" />
{savingKeys ? 'Sauvegarde...' : 'Sauvegarder les clés'}
</button>
</div>
<button
onClick={saveKeys}
disabled={savingKeys || (!newsapiKey && !eiaKey && !fredKey)}
className="mt-3 flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
<Save className="w-4 h-4" />
{savingKeys ? 'Sauvegarde...' : 'Sauvegarder les clés'}
</button>
</div>
</div>
{/* Right col: Sources */}
<div className="col-span-2 space-y-4">
{/* RSS / data sources */}
{isLoading ? (
<div className="card animate-pulse h-40 bg-dark-700" />
) : (
@@ -531,17 +851,9 @@ export default function Config() {
const requiresKey = src.requires_key
const doc = SOURCE_DOCS[key]
const keySet = requiresKey ? !!config?.[requiresKey + '_set'] : true
return (
<div
key={key}
className={clsx(
'flex items-start justify-between p-3 rounded border transition-all',
enabled
? 'border-blue-500/40 bg-blue-900/5'
: 'border-slate-700/30 bg-dark-700/30'
)}
>
<div key={key} className={clsx('flex items-start justify-between p-3 rounded border transition-all',
enabled ? 'border-blue-500/40 bg-blue-900/5' : 'border-slate-700/30 bg-dark-700/30')}>
<div className="flex-1 min-w-0 mr-3">
<div className="flex items-center gap-2">
<span className="text-xs text-white font-semibold">{src.name || key}</span>
@@ -553,27 +865,16 @@ export default function Config() {
})}>
{doc?.cost}
</span>
{requiresKey && !keySet && (
<span className="badge badge-orange text-xs">Clé requise</span>
)}
{requiresKey && !keySet && <span className="badge badge-orange text-xs">Clé requise</span>}
</div>
<div className="text-xs text-slate-500 mt-0.5">{doc?.description}</div>
</div>
<button
onClick={() => toggleSource(key)}
disabled={requiresKey && !keySet}
className={clsx(
'shrink-0 w-10 h-5 rounded-full border transition-all relative',
enabled
? 'bg-blue-600 border-blue-500'
: 'bg-dark-600 border-slate-600',
requiresKey && !keySet && 'opacity-40 cursor-not-allowed'
)}
>
<div className={clsx(
'absolute top-0.5 w-4 h-4 rounded-full bg-white transition-all',
enabled ? 'left-5' : 'left-0.5'
)} />
<button onClick={() => toggleSource(key)} disabled={requiresKey && !keySet}
className={clsx('shrink-0 w-10 h-5 rounded-full border transition-all relative',
enabled ? 'bg-blue-600 border-blue-500' : 'bg-dark-600 border-slate-600',
requiresKey && !keySet && 'opacity-40 cursor-not-allowed')}>
<div className={clsx('absolute top-0.5 w-4 h-4 rounded-full bg-white transition-all',
enabled ? 'left-5' : 'left-0.5')} />
</button>
</div>
)
@@ -581,13 +882,9 @@ export default function Config() {
</div>
</div>
))}
<div className="flex justify-end">
<button
onClick={saveSources}
disabled={savingSources || !localSources}
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold"
>
<button onClick={saveSources} disabled={savingSources || !localSources}
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
<Save className="w-4 h-4" />
{savingSources ? 'Sauvegarde...' : 'Appliquer les changements'}
</button>
@@ -595,154 +892,79 @@ export default function Config() {
</>
)}
</div>
</div>
)}
{/* ── Paramètres d'analyse IA */}
<div className="card">
<h2 className="text-base font-bold text-white flex items-center gap-2 mb-4">
<SlidersHorizontal className="w-4 h-4 text-blue-400" /> Paramètres d'analyse IA
</h2>
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<label className="text-xs text-slate-500 mb-1 block">Nombre de résultats par défaut (Top N)</label>
<div className="flex gap-1">
{[5, 10, 15, 20].map(n => (
<button key={n} onClick={() => setAnalysisTopN(n)}
className={clsx('flex-1 py-1.5 rounded text-sm transition-colors', {
'bg-blue-600 text-white': analysisTopN === n,
'bg-dark-700 text-slate-400 hover:text-slate-200': analysisTopN !== n,
})}>
Top {n}
</button>
))}
{/* ── Journal & Sortie ── */}
{configTab === 'journal' && (
<div className="card">
<h2 className="text-base font-bold text-white flex items-center gap-2 mb-4">
<Lock className="w-4 h-4 text-amber-400" /> Paramètres de sortie des trades
</h2>
<p className="text-xs text-slate-500 mb-4">
Valeurs par défaut pour les objectifs et stop-loss. Chaque trade peut avoir ses propres seuils (Journal Ouverts).
</p>
<div className="grid grid-cols-2 gap-6 mb-4">
<div>
<label className="text-xs text-slate-500 mb-2 block">
Objectif de gain : <span className="text-emerald-400 font-mono font-bold">+{exitTarget}%</span>
<span className="text-slate-600 ml-1">(P&L sous-jacent)</span>
</label>
<input type="range" min="5" max="150" step="5"
value={exitTarget}
onChange={e => setExitTarget(parseFloat(e.target.value))}
className="w-full accent-emerald-500" />
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
<span>+5%</span><span>+150%</span>
</div>
</div>
<div>
<label className="text-xs text-slate-500 mb-2 block">
Stop-loss : <span className="text-red-400 font-mono font-bold">{exitStop}%</span>
<span className="text-slate-600 ml-1">(P&L sous-jacent)</span>
</label>
<input type="range" min="-90" max="-5" step="5"
value={exitStop}
onChange={e => setExitStop(parseFloat(e.target.value))}
className="w-full accent-red-500" />
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
<span>-90%</span><span>-5%</span>
</div>
</div>
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Catégorie par défaut</label>
<select value={analysisCategoryDefault} onChange={e => setAnalysisCategoryDefault(e.target.value)}
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500">
<option value="all">Toutes les catégories</option>
<option value="energy">Énergie</option>
<option value="metals">Métaux</option>
<option value="agriculture">Agriculture</option>
<option value="indices">Indices</option>
<option value="equities">Actions</option>
<option value="forex">Forex</option>
</select>
</div>
</div>
<div className="mb-4">
<div className="flex items-center justify-between mb-1">
<label className="text-xs text-slate-500">Template d'analyse (prompt envoyé à GPT-4o)</label>
<button onClick={() => setAnalysisTemplate('')}
className="text-xs text-slate-600 hover:text-slate-400">
Remettre par défaut
</button>
</div>
<textarea
value={analysisTemplate}
onChange={e => setAnalysisTemplate(e.target.value)}
rows={10}
placeholder="Laisser vide pour utiliser le template par défaut..."
className="w-full bg-dark-700 border border-slate-700 rounded px-3 py-2 text-sm text-white font-mono focus:outline-none focus:border-blue-500 resize-y"
/>
<div className="text-xs text-slate-600 mt-1">
Variables disponibles dans le template : le contexte marché (prix, IV, variation 1j) et les news filtrées par keywords sont toujours injectées automatiquement.
</div>
</div>
<div className="flex items-center gap-3">
<button
onClick={() => saveAnalysis(
{ top_n: analysisTopN, category_filter: analysisCategoryDefault, template: analysisTemplate || undefined },
{ onSuccess: () => { setSavedMsg('Paramètres d\'analyse sauvegardés'); setTimeout(() => setSavedMsg(''), 2000) } }
)}
disabled={savingAnalysis}
className="flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
<Save className="w-4 h-4" />
{savingAnalysis ? 'Sauvegarde...' : 'Sauvegarder'}
</button>
{savedMsg && <span className="text-xs text-emerald-400">{savedMsg}</span>}
</div>
</div>
{/* ── Fermeture de trades (Exit Defaults) ── */}
<div className="card">
<h2 className="text-base font-bold text-white flex items-center gap-2 mb-4">
<Lock className="w-4 h-4 text-amber-400" /> Paramètres de sortie des trades
</h2>
<p className="text-xs text-slate-500 mb-4">
Valeurs par défaut pour les objectifs et stop-loss. Chaque trade peut avoir ses propres seuils (via la colonne Cible/Stop dans Journal Ouverts).
</p>
<div className="grid grid-cols-2 gap-6 mb-4">
<div>
<label className="text-xs text-slate-500 mb-2 block">
Objectif de gain : <span className="text-emerald-400 font-mono font-bold">+{exitTarget}%</span>
<span className="text-slate-600 ml-1">(P&L du sous-jacent)</span>
</label>
<input type="range" min="5" max="150" step="5"
value={exitTarget}
onChange={e => setExitTarget(parseFloat(e.target.value))}
className="w-full accent-emerald-500" />
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
<span>+5%</span><span>+150%</span>
<div className="grid grid-cols-2 gap-6 mb-4">
<div>
<label className="text-xs text-slate-500 mb-2 block">Mode signal IA de retournement</label>
<div className="flex gap-2">
{[
{ value: 'badge_only', label: '🔔 Badge uniquement' },
{ value: 'auto', label: '⚡ Auto (futur)' },
].map(opt => (
<button key={opt.value} onClick={() => setExitSignalMode(opt.value)}
className={clsx('flex-1 py-2 rounded text-xs border transition-all', {
'bg-blue-600 border-blue-500 text-white': exitSignalMode === opt.value,
'bg-dark-700 border-slate-700 text-slate-400 hover:text-slate-200': exitSignalMode !== opt.value,
})}>
{opt.label}
</button>
))}
</div>
<div className="text-[10px] text-slate-600 mt-1">
Badge only : alerte visible, vous confirmez manuellement. Auto : fermeture proposée.
</div>
</div>
<div>
<label className="text-xs text-slate-500 mb-2 block">
Seuil retournement signal : <span className="text-blue-400 font-mono font-bold">{exitSignalThreshold}pts</span>
</label>
<input type="range" min="10" max="60" step="5"
value={exitSignalThreshold}
onChange={e => setExitSignalThreshold(parseFloat(e.target.value))}
className="w-full accent-blue-500" />
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
<span>10pts (sensible)</span><span>60pts (tolérant)</span>
</div>
</div>
</div>
<div>
<label className="text-xs text-slate-500 mb-2 block">
Stop-loss : <span className="text-red-400 font-mono font-bold">{exitStop}%</span>
<span className="text-slate-600 ml-1">(P&L du sous-jacent)</span>
</label>
<input type="range" min="-90" max="-5" step="5"
value={exitStop}
onChange={e => setExitStop(parseFloat(e.target.value))}
className="w-full accent-red-500" />
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
<span>-90%</span><span>-5%</span>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-6 mb-4">
<div>
<label className="text-xs text-slate-500 mb-2 block">Mode signal IA de retournement</label>
<div className="flex gap-2">
{[
{ value: 'badge_only', label: '🔔 Badge uniquement' },
{ value: 'auto', label: '⚡ Auto (futur)' },
].map(opt => (
<button key={opt.value} onClick={() => setExitSignalMode(opt.value)}
className={clsx('flex-1 py-2 rounded text-xs border transition-all', {
'bg-blue-600 border-blue-500 text-white': exitSignalMode === opt.value,
'bg-dark-700 border-slate-700 text-slate-400 hover:text-slate-200': exitSignalMode !== opt.value,
})}>
{opt.label}
</button>
))}
</div>
<div className="text-[10px] text-slate-600 mt-1">
Badge only : alerte visible, vous confirmez manuellement. Auto : fermeture proposée par le système.
</div>
</div>
<div>
<label className="text-xs text-slate-500 mb-2 block">
Seuil de retournement signal : <span className="text-blue-400 font-mono font-bold">{exitSignalThreshold}pts</span>
<span className="text-slate-600 ml-1">(chute du score)</span>
</label>
<input type="range" min="10" max="60" step="5"
value={exitSignalThreshold}
onChange={e => setExitSignalThreshold(parseFloat(e.target.value))}
className="w-full accent-blue-500" />
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
<span>10pts (sensible)</span><span>60pts (tolérant)</span>
</div>
</div>
</div>
<div className="flex items-center gap-3">
<button
onClick={() => saveExitDefaults(
{ target_pct: exitTarget, stop_loss_pct: exitStop,
@@ -754,159 +976,12 @@ export default function Config() {
<Save className="w-4 h-4" />
{savingExitDefaults ? 'Sauvegarde...' : 'Sauvegarder les seuils'}
</button>
{savedMsg && <span className="text-xs text-emerald-400">{savedMsg}</span>}
</div>
</div>
)}
{/* ── Auto-Cycle ── */}
<div className="card">
<div className="flex items-center justify-between mb-4">
<h2 className="text-base font-bold text-white flex items-center gap-2">
<Zap className="w-4 h-4 text-blue-400" /> Auto-Cycle Intelligence
</h2>
{cs && (
<div className="flex items-center gap-2 text-xs">
{cs.running ? (
<span className="flex items-center gap-1 text-blue-400 bg-blue-900/30 border border-blue-700/30 px-2 py-0.5 rounded animate-pulse">
<RefreshCw className="w-3 h-3 animate-spin" /> En cours...
</span>
) : cs.scheduler_alive ? (
<span className="text-emerald-400 bg-emerald-900/30 border border-emerald-700/30 px-2 py-0.5 rounded">
Scheduler actif
</span>
) : (
<span className="text-slate-600 bg-dark-700 border border-slate-700/30 px-2 py-0.5 rounded">
Scheduler inactif
</span>
)}
</div>
)}
</div>
{/* ── Profils de risque ── */}
{configTab === 'profiles' && <RiskProfilesCard />}
<p className="text-xs text-slate-500 mb-4">
Toutes les N heures : suggère de nouveaux patterns, filtre les doublons, score tout, log les prix et génère un commentaire IA sur les performances.
</p>
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<label className="text-xs text-slate-500 mb-2 block">
Rétention Journal ({retentionDays}j trades effacés après N jours)
</label>
<div className="flex gap-1">
{[30, 60, 90, 180].map(d => (
<button key={d} onClick={() => setRetentionDays(d)}
className={clsx('flex-1 py-2 rounded text-sm transition-colors', {
'bg-blue-600 text-white': retentionDays === d,
'bg-dark-700 text-slate-400 hover:text-slate-200': retentionDays !== d,
})}>
{d}j
</button>
))}
</div>
</div>
<div>
<label className="text-xs text-slate-500 mb-2 block">
Seuil de maturité ({maturityThreshold}% trades en-dessous exclus des stats)
</label>
<div className="flex gap-1">
{[20, 30, 35, 50].map(p => (
<button key={p} onClick={() => setMaturityThreshold(p)}
className={clsx('flex-1 py-2 rounded text-sm transition-colors', {
'bg-blue-600 text-white': maturityThreshold === p,
'bg-dark-700 text-slate-400 hover:text-slate-200': maturityThreshold !== p,
})}>
{p}%
</button>
))}
</div>
</div>
</div>
<div className="grid grid-cols-3 gap-4 mb-4">
<div>
<label className="text-xs text-slate-500 mb-2 block">Activer l'auto-cycle</label>
<button
onClick={() => setCycleEnabled(!cycleEnabled)}
className={clsx('w-full py-2 rounded border text-sm font-semibold transition-all', {
'bg-blue-600 border-blue-500 text-white': cycleEnabled,
'bg-dark-700 border-slate-700 text-slate-400 hover:border-slate-500': !cycleEnabled,
})}>
{cycleEnabled ? ' Activé' : ' Désactivé'}
</button>
</div>
<div>
<label className="text-xs text-slate-500 mb-2 block">Intervalle (heures)</label>
<div className="flex gap-1">
{[1, 3, 6, 12].map(h => (
<button key={h} onClick={() => setCycleHours(h)}
className={clsx('flex-1 py-2 rounded text-sm transition-colors', {
'bg-blue-600 text-white': cycleHours === h,
'bg-dark-700 text-slate-400 hover:text-slate-200': cycleHours !== h,
})}>
{h}h
</button>
))}
</div>
</div>
<div>
<label className="text-xs text-slate-500 mb-2 block">
Similarité max ({Math.round(cycleSimilarity * 100)}% — nouveaux patterns en-dessous)
</label>
<input type="range" min="0.1" max="0.8" step="0.05"
value={cycleSimilarity}
onChange={e => setCycleSimilarity(parseFloat(e.target.value))}
className="w-full accent-blue-500" />
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
<span>10% (strict)</span><span>80% (permissif)</span>
</div>
</div>
</div>
{/* Risk Profiles */}
<RiskProfilesCard />
{cs?.last_cycle && (
<div className="card bg-dark-700/50 mb-2 text-xs space-y-2">
<div className="flex items-center gap-4 flex-wrap">
<span className="text-slate-500">Dernier cycle :</span>
<span className="text-slate-300">{cs.last_cycle.started_at?.slice(0, 16)} UTC</span>
<span className={clsx('badge', cs.last_cycle.status === 'completed' ? 'badge-green' : 'badge-red')}>
{cs.last_cycle.status}
</span>
<span className="text-slate-500">+{cs.last_cycle.patterns_added} patterns</span>
<span className="text-slate-500">{cs.last_cycle.patterns_scored} scorés</span>
<span className="text-slate-500">Géo: {cs.last_cycle.geo_score}</span>
<span className="text-slate-500 capitalize">{cs.last_cycle.dominant_regime}</span>
</div>
</div>
)}
{cs?.enabled && cs?.next_run_at && !cs?.running && (
<div className="card bg-dark-700/50 mb-4 text-xs">
<NextRunCountdown nextRunAt={cs.next_run_at} />
</div>
)}
<div className="flex items-center gap-3">
<button
onClick={() => updateCycleConfig(
{ enabled: cycleEnabled, interval_hours: cycleHours, similarity_threshold: cycleSimilarity, min_ev_threshold: minEv, min_score_threshold: minScore, journal_retention_days: retentionDays, maturity_threshold_pct: maturityThreshold },
{ onSuccess: () => { refetchCycle(); setSavedMsg('Auto-cycle configuré'); setTimeout(() => setSavedMsg(''), 2000) } }
)}
disabled={savingCycle}
className="flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
<Save className="w-4 h-4" />
{savingCycle ? 'Sauvegarde...' : 'Appliquer'}
</button>
<button
onClick={() => triggerCycle(undefined, { onSuccess: () => { refetchCycle(); setSavedMsg('Cycle lancé !'); setTimeout(() => setSavedMsg(''), 3000) } })}
disabled={triggeringCycle || !aiStatus?.enabled}
className="flex items-center gap-1.5 border border-blue-500/50 text-blue-400 hover:bg-blue-900/20 disabled:opacity-40 px-4 py-2 rounded text-sm font-semibold">
<RefreshCw className={clsx('w-4 h-4', triggeringCycle && 'animate-spin')} />
Lancer maintenant
</button>
{savedMsg && <span className="text-xs text-emerald-400">{savedMsg}</span>}
</div>
</div>
</div>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
import { useState, useEffect, useRef, Fragment } from 'react'
import { BookOpen, TrendingUp, TrendingDown, Activity, AlertTriangle, RefreshCw, Zap, CheckCircle, XCircle, Brain, Trash2, Search, X, ChevronDown, ChevronUp, Terminal, Lock, ShieldAlert, PieChart } from 'lucide-react'
import { BookOpen, TrendingUp, TrendingDown, Activity, AlertTriangle, RefreshCw, Zap, CheckCircle, XCircle, Brain, Trash2, Search, X, ChevronDown, ChevronUp, Terminal, Lock, ShieldAlert, PieChart, Target } from 'lucide-react'
import { TradeIdeasTab } from '../components/TradeIdeas'
import clsx from 'clsx'
import { useJournalSummary, useMacroHistory, useGeoHistory, useTradeMtm, useClosedTrades, useCloseTrade, useUpdateExitParams, useExitDefaults, useCycleHistory, useCycleStatus, useTriggerCycle, useTradePostmortem, useAnalyzePostmortem, useIvForTrade, useKellySizing, useSimPortfolioRisk, useSkippedTrades, api } from '../hooks/useApi'
import { useQueryClient } from '@tanstack/react-query'
@@ -1441,6 +1442,7 @@ function SimPortfolioRiskPanel() {
const TABS = [
{ key: 'cycles', label: 'Cycles IA', icon: Zap },
{ key: 'macro', label: 'Régimes Macro', icon: Activity },
{ key: 'ideas', label: 'Idées de trade', icon: Target },
{ key: 'mtm', label: 'Ouverts', icon: TrendingUp },
{ key: 'closed', label: 'Fermés', icon: Lock },
{ key: 'geo', label: 'Alertes Géo', icon: AlertTriangle },
@@ -1541,7 +1543,7 @@ function SkippedTradesSection({ days }: { days: number }) {
}
export default function JournalDeBord() {
const [tab, setTab] = useState<'cycles' | 'macro' | 'mtm' | 'closed' | 'geo' | 'skipped'>('cycles')
const [tab, setTab] = useState<'cycles' | 'macro' | 'ideas' | 'mtm' | 'closed' | 'geo' | 'skipped'>('cycles')
const [days, setDays] = useState(15)
const [confirmReset, setConfirmReset] = useState(false)
const [resetting, setResetting] = useState(false)
@@ -1696,6 +1698,7 @@ export default function JournalDeBord() {
{/* Content */}
{tab === 'cycles' && <CyclesSection />}
{tab === 'macro' && <MacroHistorySection days={days} />}
{tab === 'ideas' && <TradeIdeasTab />}
{tab === 'mtm' && <TradeMtmSection days={days} />}
{tab === 'closed' && <ClosedTradesSection days={days} />}
{tab === 'geo' && <GeoHistorySection days={days} />}