feat: cockpit command center + skipped trades journal
Dashboard: insert 2 rows of 4 mini-cards between top row and trade ideas - Row 1: PnL Simulé, Risque Simulé, Dernier Cycle, Régime Macro - Row 2: Super Contexte, Signaux Géo, Meilleur Pattern, Patterns Actifs - All cards link to underlying pages via react-router Link Journal: add 'Non loggés' tab exposing trades suggested by cycle but skipped because no risk profile was matched - New skipped_trades table (auto-created on backend restart) - log_trade_entries() persists each skip with score/gain/asset_class - GET /api/journal/skipped-trades + useSkippedTrades hook Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@ from services.database import (
|
||||
get_macro_regime_history, get_geo_alert_history, get_trade_entry_prices,
|
||||
get_closed_trades, close_trade, update_trade_exit_params,
|
||||
get_trade_entry_by_id, get_config, set_config, reset_journal_history,
|
||||
_fetch_live_prices, _trade_maturity,
|
||||
_fetch_live_prices, _trade_maturity, get_skipped_trades,
|
||||
)
|
||||
import json
|
||||
|
||||
@@ -253,6 +253,13 @@ def trade_check(body: TradeCheckRequest):
|
||||
return check_new_trade(body.underlying, body.strategy, body.asset_class)
|
||||
|
||||
|
||||
@router.get("/skipped-trades")
|
||||
def skipped_trades_endpoint(days: int = 30):
|
||||
"""Trades suggested by cycle that didn't pass any risk profile threshold."""
|
||||
trades = get_skipped_trades(days)
|
||||
return _sanitize({"trades": trades, "days": days, "count": len(trades)})
|
||||
|
||||
|
||||
@router.delete("/reset")
|
||||
def reset_journal():
|
||||
"""Truncate all journal history (trades, macro, geo, cycles). Irreversible."""
|
||||
|
||||
@@ -379,6 +379,25 @@ def init_db():
|
||||
details TEXT
|
||||
)""")
|
||||
|
||||
c.execute("""CREATE TABLE IF NOT EXISTS skipped_trades (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
run_id TEXT,
|
||||
pattern_id TEXT,
|
||||
pattern_name TEXT,
|
||||
underlying TEXT,
|
||||
strategy TEXT,
|
||||
score INTEGER DEFAULT 0,
|
||||
expected_move_pct REAL,
|
||||
skip_reason TEXT DEFAULT 'no_profile',
|
||||
skip_detail TEXT,
|
||||
asset_class TEXT,
|
||||
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%S', 'now'))
|
||||
)""")
|
||||
try:
|
||||
c.execute("CREATE INDEX IF NOT EXISTS idx_skipped_date ON skipped_trades(created_at DESC)")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
c.execute("CREATE INDEX IF NOT EXISTS idx_kb_category ON knowledge_base(category, status)")
|
||||
c.execute("CREATE INDEX IF NOT EXISTS idx_rs_version ON reasoning_state(version DESC)")
|
||||
@@ -1014,6 +1033,17 @@ def log_trade_entries(run_id: str, scored_patterns: List[Dict[str, Any]], quotes
|
||||
if matched is None:
|
||||
skipped_no_profile += 1
|
||||
_log.debug(f"[TradeLog] SKIP {underlying} score={eff_score} gain={exp_move:.0f}% — no profile match")
|
||||
_trade_ac = trade.get("asset_class") or sp.get("asset_class") or _orig.get("asset_class") or ""
|
||||
try:
|
||||
log_skipped_trade(
|
||||
run_id=run_id, pattern_id=pid, pattern_name=pattern_name,
|
||||
underlying=underlying, strategy=strategy, score=eff_score,
|
||||
expected_move_pct=exp_move,
|
||||
skip_detail=f"profiles checked: {len(profiles)}, best: score>={eff_score} gain>={exp_move:.0f}%",
|
||||
asset_class=_trade_ac,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
continue
|
||||
|
||||
ev_gross, ev_net, trade_score = _compute_trade_score(eff_score, exp_move)
|
||||
@@ -1205,6 +1235,35 @@ def get_closed_trades(days: int = 180) -> List[Dict[str, Any]]:
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
def get_skipped_trades(days: int = 30) -> List[Dict[str, Any]]:
|
||||
conn = get_conn()
|
||||
rows = conn.execute(
|
||||
"""SELECT * FROM skipped_trades
|
||||
WHERE created_at >= date('now', ?)
|
||||
ORDER BY created_at DESC, score DESC""",
|
||||
(f"-{days} days",)
|
||||
).fetchall()
|
||||
conn.close()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
def log_skipped_trade(run_id: str, pattern_id: str, pattern_name: str,
|
||||
underlying: str, strategy: str, score: int,
|
||||
expected_move_pct: float, skip_reason: str = "no_profile",
|
||||
skip_detail: str = "", asset_class: str = "") -> None:
|
||||
conn = get_conn()
|
||||
conn.execute(
|
||||
"""INSERT INTO skipped_trades
|
||||
(run_id, pattern_id, pattern_name, underlying, strategy, score,
|
||||
expected_move_pct, skip_reason, skip_detail, asset_class)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
(run_id, pattern_id, pattern_name, underlying, strategy, score,
|
||||
expected_move_pct, skip_reason, skip_detail, asset_class)
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
def close_trade(trade_id: int, close_price: float, pnl_realized: float,
|
||||
close_reason: str, close_note: str = "") -> bool:
|
||||
conn = get_conn()
|
||||
|
||||
@@ -472,6 +472,13 @@ export const useTradeCheck = () =>
|
||||
api.post('/journal/trade-check', body).then(r => r.data),
|
||||
})
|
||||
|
||||
export const useSkippedTrades = (days = 30) =>
|
||||
useQuery({
|
||||
queryKey: ['journal-skipped', days],
|
||||
queryFn: () => api.get(`/journal/skipped-trades?days=${days}`).then(r => r.data),
|
||||
staleTime: 60_000,
|
||||
})
|
||||
|
||||
// ── Risk Profiles ─────────────────────────────────────────────────────────────
|
||||
|
||||
export const useRiskProfiles = () =>
|
||||
|
||||
@@ -4,8 +4,10 @@ import {
|
||||
useCalendar, useAiStatus, usePortfolioSummary, useAddPosition,
|
||||
useScorePatterns, useLastScores, useAllPatterns, useMacroRegime,
|
||||
usePortfolioPositions, useTradeMtm, useRiskProfiles, useRiskDashboard,
|
||||
useSimPortfolioRisk, useCycleStatus, useKnowledgeState,
|
||||
} from '../hooks/useApi'
|
||||
import { Target, Clock, Brain, Globe, Plus, RefreshCw, ChevronDown, ChevronUp, CheckCircle2, ShieldAlert, LayoutGrid, List, Terminal } from 'lucide-react'
|
||||
import { Target, Clock, Brain, Globe, Plus, RefreshCw, ChevronDown, ChevronUp, CheckCircle2, ShieldAlert, LayoutGrid, List, Terminal, ArrowUpRight } from 'lucide-react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import clsx from 'clsx'
|
||||
import type { Quote } from '../types'
|
||||
import { format } from 'date-fns'
|
||||
@@ -780,6 +782,9 @@ export default function Dashboard() {
|
||||
const { data: tradeMtmData } = useTradeMtm(30)
|
||||
const { data: riskProfilesData } = useRiskProfiles()
|
||||
const { data: riskDashboard } = useRiskDashboard()
|
||||
const { data: simRisk } = useSimPortfolioRisk()
|
||||
const { data: cycleStatusData } = useCycleStatus()
|
||||
const { data: knowledgeState } = useKnowledgeState()
|
||||
const { mutate: scorePatterns, isPending: scoring } = useScorePatterns()
|
||||
const { mutate: addPos } = useAddPosition()
|
||||
|
||||
@@ -1101,6 +1106,212 @@ export default function Dashboard() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Command Center: Résumé Opérationnel ── */}
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
|
||||
{/* PnL Simulé */}
|
||||
{(() => {
|
||||
const trades: any[] = (tradeMtmData as any)?.trades ?? []
|
||||
const withPnl = trades.filter((t: any) => t.pnl_pct != null)
|
||||
const avgPnl = withPnl.length
|
||||
? withPnl.reduce((s: number, t: any) => s + t.pnl_pct, 0) / withPnl.length
|
||||
: null
|
||||
const winners = withPnl.filter((t: any) => t.pnl_pct > 0).length
|
||||
const losers = withPnl.filter((t: any) => t.pnl_pct < 0).length
|
||||
return (
|
||||
<Link to="/journal" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="section-title mb-0 text-[10px]">📊 PnL Simulé</span>
|
||||
<ArrowUpRight className="w-3 h-3 text-slate-600" />
|
||||
</div>
|
||||
<div className={clsx('text-2xl font-bold font-mono mt-1',
|
||||
avgPnl === null ? 'text-slate-500' : avgPnl >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||
{avgPnl !== null ? `${avgPnl >= 0 ? '+' : ''}${avgPnl.toFixed(1)}%` : '—'}
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-500 mt-1">
|
||||
{trades.length} trades · <span className="text-emerald-500">{winners}✓</span>{' '}
|
||||
<span className="text-red-400">{losers}✗</span>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* Risque Simulé */}
|
||||
{(() => {
|
||||
const risk = simRisk as any
|
||||
const alertCount: number = risk?.alerts?.length ?? 0
|
||||
const conflictCount: number = risk?.conflicts?.length ?? 0
|
||||
const openCount: number = risk?.open_count ?? 0
|
||||
return (
|
||||
<Link to="/risk" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="section-title mb-0 text-[10px]">🛡️ Risque Simulé</span>
|
||||
<ArrowUpRight className="w-3 h-3 text-slate-600" />
|
||||
</div>
|
||||
<div className={clsx('text-2xl font-bold mt-1', alertCount > 0 ? 'text-red-400' : 'text-emerald-400')}>
|
||||
{alertCount > 0 ? `${alertCount} alerte${alertCount > 1 ? 's' : ''}` : 'OK'}
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-500 mt-1">
|
||||
{openCount} positions
|
||||
{conflictCount > 0 && <span className="text-red-400 ml-1">· {conflictCount} conflit{conflictCount > 1 ? 's' : ''}</span>}
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* Dernier Cycle */}
|
||||
{(() => {
|
||||
const last = (cycleStatusData as any)?.last_cycle
|
||||
const ts = last?.ts
|
||||
? new Date(last.ts.endsWith('Z') ? last.ts : last.ts + 'Z')
|
||||
: null
|
||||
const elapsed = ts ? Math.round((Date.now() - ts.getTime()) / 60_000) : null
|
||||
const elapsedStr = elapsed === null ? '—'
|
||||
: elapsed < 60 ? `${elapsed}min`
|
||||
: elapsed < 1440 ? `${Math.round(elapsed / 60)}h`
|
||||
: `${Math.round(elapsed / 1440)}j`
|
||||
return (
|
||||
<Link to="/journal" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="section-title mb-0 text-[10px]">🔄 Dernier Cycle</span>
|
||||
<ArrowUpRight className="w-3 h-3 text-slate-600" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-blue-400 mt-1">{elapsedStr}</div>
|
||||
<div className="text-[10px] text-slate-500 mt-1">
|
||||
{last
|
||||
? `${last.patterns_added ?? 0} loggés · géo ${last.geo_score ?? '—'}`
|
||||
: 'Aucun cycle enregistré'}
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* Régime Macro */}
|
||||
{(() => {
|
||||
const dom = macroInfo?.dominant
|
||||
const colorClass = dom === 'growth' ? 'text-emerald-400'
|
||||
: dom === 'stagflation' || dom === 'recession' ? 'text-red-400'
|
||||
: dom === 'deflation' ? 'text-blue-300'
|
||||
: 'text-slate-400'
|
||||
return (
|
||||
<Link to="/macro" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="section-title mb-0 text-[10px]">🌐 Régime Macro</span>
|
||||
<ArrowUpRight className="w-3 h-3 text-slate-600" />
|
||||
</div>
|
||||
<div className={clsx('text-lg font-bold mt-1', colorClass)}>
|
||||
{macroInfo ? `${macroInfo.emoji} ${macroInfo.label}` : '—'}
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-500 mt-1 line-clamp-1">
|
||||
{macroInfo
|
||||
? Object.entries(macroInfo.assetBias).slice(0, 2).map(([k, v]) => `${k}→${v}`).join(' · ')
|
||||
: 'Chargement...'}
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* ── Command Center: Intelligence & Contexte ── */}
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
|
||||
{/* Super Contexte IA */}
|
||||
{(() => {
|
||||
const state = (knowledgeState as any)?.state
|
||||
const synthesis = state?.synthesis
|
||||
const insights = (synthesis?.regime_insights?.length ?? 0)
|
||||
+ (synthesis?.pattern_insights?.length ?? 0)
|
||||
+ (synthesis?.recurring_mistakes?.length ?? 0)
|
||||
const priority = synthesis?.strategic_priorities?.[0]
|
||||
const excerpt = typeof priority === 'string' ? priority
|
||||
: typeof state?.narrative === 'string' ? state.narrative.slice(0, 90)
|
||||
: null
|
||||
return (
|
||||
<Link to="/super-contexte" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="section-title mb-0 text-[10px]">🧠 Super Contexte</span>
|
||||
<ArrowUpRight className="w-3 h-3 text-slate-600" />
|
||||
</div>
|
||||
<div className="text-sm font-bold text-purple-400 mt-1">
|
||||
{state ? `${insights} insights actifs` : 'Aucune synthèse'}
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-500 mt-1 line-clamp-2">
|
||||
{excerpt ?? 'Lancer une synthèse dans Super Contexte'}
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* Signaux Géo */}
|
||||
{(() => {
|
||||
const topRisks: Array<[string, number]> = riskScore?.top_risks ?? []
|
||||
return (
|
||||
<Link to="/geo" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="section-title mb-0 text-[10px]">🌍 Signaux Géo</span>
|
||||
<ArrowUpRight className="w-3 h-3 text-slate-600" />
|
||||
</div>
|
||||
<div className={clsx('text-2xl font-bold font-mono mt-1', gauge ? gauge.color : 'text-slate-500')}>
|
||||
{riskScore?.score ?? '—'}
|
||||
</div>
|
||||
<div className="space-y-0.5 mt-1">
|
||||
{topRisks.slice(0, 2).map(([cat, val]) => (
|
||||
<div key={cat} className="flex justify-between text-[10px]">
|
||||
<span className="text-slate-500 capitalize truncate">{cat.replace(/_/g, ' ')}</span>
|
||||
<span className="text-slate-400 shrink-0 ml-1">{Math.round(val * 100)}%</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* Meilleur Pattern scoré */}
|
||||
{(() => {
|
||||
const best = [...allPatterns]
|
||||
.map(p => ({ p, sp: scoreMap[p.id] }))
|
||||
.filter(x => x.sp?.score != null)
|
||||
.sort((a, b) => (b.sp.score ?? 0) - (a.sp.score ?? 0))[0]
|
||||
const score = best?.sp?.score ?? null
|
||||
const name = best?.p?.name ?? null
|
||||
const ticker = best?.sp?.recommended_trade?.underlying ?? null
|
||||
return (
|
||||
<Link to="/patterns" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="section-title mb-0 text-[10px]">⭐ Meilleur Pattern</span>
|
||||
<ArrowUpRight className="w-3 h-3 text-slate-600" />
|
||||
</div>
|
||||
<div className={clsx('text-2xl font-bold font-mono mt-1', score !== null ? scoreColor(score) : 'text-slate-500')}>
|
||||
{score ?? '—'}
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-500 mt-1 line-clamp-1">
|
||||
{name ?? 'Aucun scoré'}{ticker ? ` · ${ticker}` : ''}
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* Patterns Actifs */}
|
||||
{(() => {
|
||||
const total = allPatterns.length
|
||||
const scored = allPatterns.filter(p => scoreMap[p.id]).length
|
||||
const unscored = total - scored
|
||||
return (
|
||||
<Link to="/patterns" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="section-title mb-0 text-[10px]">📋 Patterns Actifs</span>
|
||||
<ArrowUpRight className="w-3 h-3 text-slate-600" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-slate-200 mt-1">{total}</div>
|
||||
<div className="text-[10px] text-slate-500 mt-1">
|
||||
<span className="text-emerald-500">{scored} scorés</span>
|
||||
{unscored > 0 && <span className="text-slate-600"> · {unscored} à scorer</span>}
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* ── Trade ideas scorées par IA ── */}
|
||||
<div>
|
||||
{/* Toolbar */}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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 clsx from 'clsx'
|
||||
import { useJournalSummary, useMacroHistory, useGeoHistory, useTradeMtm, useClosedTrades, useCloseTrade, useUpdateExitParams, useExitDefaults, useCycleHistory, useCycleStatus, useTriggerCycle, useTradePostmortem, useAnalyzePostmortem, useIvForTrade, useKellySizing, useSimPortfolioRisk, api } from '../hooks/useApi'
|
||||
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'
|
||||
import { format } from 'date-fns'
|
||||
import { fr } from 'date-fns/locale'
|
||||
@@ -1444,10 +1444,104 @@ const TABS = [
|
||||
{ key: 'mtm', label: 'Ouverts', icon: TrendingUp },
|
||||
{ key: 'closed', label: 'Fermés', icon: Lock },
|
||||
{ key: 'geo', label: 'Alertes Géo', icon: AlertTriangle },
|
||||
{ key: 'skipped', label: 'Non loggés', icon: XCircle },
|
||||
] as const
|
||||
|
||||
function SkippedTradesSection({ days }: { days: number }) {
|
||||
const { data, isLoading } = useSkippedTrades(days)
|
||||
const trades: any[] = (data as any)?.trades ?? []
|
||||
|
||||
const ASSET_CLASS_COLORS: Record<string, string> = {
|
||||
energy: 'bg-orange-900/30 text-orange-300 border-orange-700/30',
|
||||
metals: 'bg-yellow-900/30 text-yellow-300 border-yellow-700/30',
|
||||
agriculture: 'bg-green-900/30 text-green-300 border-green-700/30',
|
||||
indices: 'bg-blue-900/30 text-blue-300 border-blue-700/30',
|
||||
forex: 'bg-purple-900/30 text-purple-300 border-purple-700/30',
|
||||
rates: 'bg-slate-800/60 text-slate-300 border-slate-600/30',
|
||||
equities: 'bg-cyan-900/30 text-cyan-300 border-cyan-700/30',
|
||||
}
|
||||
|
||||
if (isLoading) return <div className="card animate-pulse h-24" />
|
||||
|
||||
if (trades.length === 0) {
|
||||
return (
|
||||
<div className="card text-center py-8 text-slate-500 text-sm">
|
||||
<XCircle className="w-6 h-6 mx-auto mb-2 text-slate-600" />
|
||||
Aucune suggestion non loggée sur {days}j — tous les trades scorés passent au moins un profil de risque.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const byReason = trades.reduce((acc: Record<string, number>, t: any) => {
|
||||
acc[t.skip_reason] = (acc[t.skip_reason] ?? 0) + 1
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="card">
|
||||
<div className="section-title flex items-center gap-1 mb-3">
|
||||
<XCircle className="w-3 h-3 text-amber-400" />
|
||||
Trades suggérés non loggés — {trades.length} sur {days}j
|
||||
<span className="ml-2 text-slate-600 font-normal text-xs">
|
||||
(score ou gain insuffisant pour tout profil actif)
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-3 mb-4 flex-wrap">
|
||||
{Object.entries(byReason).map(([reason, count]) => (
|
||||
<div key={reason} className="badge badge-yellow text-[10px]">
|
||||
{reason}: {count}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="overflow-x-auto rounded-lg border border-slate-700/40">
|
||||
<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">Pattern</th>
|
||||
<th className="px-2 py-2 text-left">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 text-right">Gain attendu</th>
|
||||
<th className="px-2 py-2 text-left">Classe</th>
|
||||
<th className="pr-3 py-2 text-left">Raison du skip</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800/40">
|
||||
{trades.map((t: any) => (
|
||||
<tr key={t.id} className="hover:bg-dark-700/30">
|
||||
<td className="pl-3 py-2 text-slate-300 max-w-[140px] truncate">{t.pattern_name || '—'}</td>
|
||||
<td className="px-2 py-2 font-mono text-slate-400">{t.underlying}</td>
|
||||
<td className="px-2 py-2 text-slate-400">{t.strategy || '—'}</td>
|
||||
<td className={clsx('px-2 py-2 text-right font-bold font-mono',
|
||||
t.score >= 50 ? 'text-emerald-400' : t.score >= 25 ? 'text-yellow-400' : 'text-slate-600')}>
|
||||
{t.score ?? '—'}
|
||||
</td>
|
||||
<td className="px-2 py-2 text-right text-slate-400">
|
||||
{t.expected_move_pct != null ? `${t.expected_move_pct.toFixed(0)}%` : '—'}
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
{t.asset_class ? (
|
||||
<span className={clsx('badge text-[10px] border', ASSET_CLASS_COLORS[t.asset_class] ?? 'badge-blue')}>
|
||||
{t.asset_class}
|
||||
</span>
|
||||
) : <span className="text-slate-600">—</span>}
|
||||
</td>
|
||||
<td className="pr-3 py-2 text-slate-600 max-w-[200px] truncate" title={t.skip_detail}>
|
||||
{t.skip_detail || t.skip_reason}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function JournalDeBord() {
|
||||
const [tab, setTab] = useState<'cycles' | 'macro' | 'mtm' | 'closed' | 'geo'>('cycles')
|
||||
const [tab, setTab] = useState<'cycles' | 'macro' | 'mtm' | 'closed' | 'geo' | 'skipped'>('cycles')
|
||||
const [days, setDays] = useState(15)
|
||||
const [confirmReset, setConfirmReset] = useState(false)
|
||||
const [resetting, setResetting] = useState(false)
|
||||
@@ -1605,6 +1699,7 @@ export default function JournalDeBord() {
|
||||
{tab === 'mtm' && <TradeMtmSection days={days} />}
|
||||
{tab === 'closed' && <ClosedTradesSection days={days} />}
|
||||
{tab === 'geo' && <GeoHistorySection days={days} />}
|
||||
{tab === 'skipped' && <SkippedTradesSection days={days} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user