feat: expandable inline rows in Journal + journal/maturity params in Config
- JournalDeBord: trade rows now expand inline (full-width) instead of PostmortemPanel appearing below the whole table. Click anywhere on a row to toggle. Period selector extended to 15/30/60/90j. - Config: added Rétention Journal (30/60/90/180j) and Seuil Maturité (20/30/35/50%) controls, wired to the Appliquer button. - Backend: journal_retention_days and maturity_threshold_pct read from config table; seeded at startup with defaults 90d / 35%. get_status() now returns both values so Config page can initialise correctly. - cycle.py: CycleConfigRequest accepts and validates both new params. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -44,6 +44,8 @@ class CycleConfigRequest(BaseModel):
|
||||
similarity_threshold: Optional[float] = None
|
||||
min_ev_threshold: Optional[float] = None
|
||||
min_score_threshold: Optional[int] = None
|
||||
journal_retention_days: Optional[int] = None
|
||||
maturity_threshold_pct: Optional[int] = None
|
||||
|
||||
|
||||
@router.post("/config")
|
||||
@@ -67,6 +69,14 @@ def update_cycle_config(req: CycleConfigRequest):
|
||||
if not (0 <= req.min_score_threshold <= 100):
|
||||
raise HTTPException(400, "min_score_threshold must be between 0 and 100")
|
||||
set_config("min_score_threshold", str(req.min_score_threshold))
|
||||
if req.journal_retention_days is not None:
|
||||
if not (7 <= req.journal_retention_days <= 365):
|
||||
raise HTTPException(400, "journal_retention_days must be between 7 and 365")
|
||||
set_config("journal_retention_days", str(req.journal_retention_days))
|
||||
if req.maturity_threshold_pct is not None:
|
||||
if not (5 <= req.maturity_threshold_pct <= 75):
|
||||
raise HTTPException(400, "maturity_threshold_pct must be between 5 and 75")
|
||||
set_config("maturity_threshold_pct", str(req.maturity_threshold_pct))
|
||||
|
||||
# Restart scheduler to pick up changes
|
||||
restart_scheduler()
|
||||
|
||||
@@ -982,8 +982,11 @@ def get_status() -> Dict[str, Any]:
|
||||
sim_threshold = float(get_config("auto_cycle_similarity_threshold") or "0.30")
|
||||
min_ev = float(get_config("min_ev_threshold") or "0.0")
|
||||
min_score = int(get_config("min_score_threshold") or "0")
|
||||
retention_days = int(get_config("journal_retention_days") or "90")
|
||||
maturity_pct = int(get_config("maturity_threshold_pct") or "35")
|
||||
except Exception:
|
||||
interval_hours, enabled, sim_threshold, min_ev, min_score = 3.0, False, 0.30, 0.0, 0
|
||||
retention_days, maturity_pct = 90, 35
|
||||
|
||||
recent = get_cycle_runs(limit=1)
|
||||
last = recent[0] if recent else None
|
||||
@@ -995,6 +998,8 @@ def get_status() -> Dict[str, Any]:
|
||||
"similarity_threshold": sim_threshold,
|
||||
"min_ev_threshold": min_ev,
|
||||
"min_score_threshold": min_score,
|
||||
"journal_retention_days": retention_days,
|
||||
"maturity_threshold_pct": maturity_pct,
|
||||
"last_cycle": last,
|
||||
"scheduler_alive": bool(_cycle_thread and _cycle_thread.is_alive()),
|
||||
}
|
||||
|
||||
@@ -349,6 +349,18 @@ def init_db():
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Seed default config values if not already set
|
||||
for _key, _val in [
|
||||
("journal_retention_days", "90"),
|
||||
("maturity_threshold_pct", "35"),
|
||||
]:
|
||||
existing = c.execute("SELECT value FROM config WHERE key=?", (_key,)).fetchone()
|
||||
if not existing:
|
||||
c.execute(
|
||||
"INSERT OR IGNORE INTO config (key, value, updated_at) VALUES (?, ?, datetime('now'))",
|
||||
(_key, _val)
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
@@ -991,7 +1003,8 @@ def log_trade_entries(run_id: str, scored_patterns: List[Dict[str, Any]], quotes
|
||||
_log.info(f"[TradeLog] NEW trade: pattern='{pattern_name}' {underlying} {strategy} score={eff_score} gain={exp_move:.0f}% profile='{matched}' price={entry_price}")
|
||||
|
||||
_log.info(f"[TradeLog] Done — inserted={inserted_count} updated={updated_count} skipped_no_profile={skipped_no_profile}")
|
||||
conn.execute("DELETE FROM trade_entry_prices WHERE entry_date < date('now', '-90 days')")
|
||||
_retention = int(get_config("journal_retention_days") or "90")
|
||||
conn.execute(f"DELETE FROM trade_entry_prices WHERE entry_date < date('now', '-{_retention} days')")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
@@ -1062,18 +1075,13 @@ def get_cycle_run(run_id: str) -> Optional[Dict[str, Any]]:
|
||||
def _trade_maturity(days_held: int, horizon_days: int) -> Dict[str, Any]:
|
||||
"""
|
||||
Classify a trade's maturity based on elapsed time vs planned horizon.
|
||||
Returns status, label, weight (0-1 for lesson extraction), and color hint.
|
||||
|
||||
Thresholds (percentage of horizon elapsed):
|
||||
< 10% → trop_tot : P&L is pure noise, never evaluate
|
||||
10-35% → debut : early signal, very low weight
|
||||
35-75% → mature : reliable signal, full weight
|
||||
> 75% → fin_horizon : approaching expiry, full weight + watch flag
|
||||
Thresholds read from config (maturity_threshold_pct, default 35%).
|
||||
"""
|
||||
h = max(horizon_days or 90, 1)
|
||||
d = max(days_held or 0, 0)
|
||||
ratio = d / h
|
||||
pct = round(ratio * 100, 1)
|
||||
mature_threshold = float(get_config("maturity_threshold_pct") or "35") / 100.0
|
||||
|
||||
if ratio < 0.10:
|
||||
return {
|
||||
@@ -1081,7 +1089,7 @@ def _trade_maturity(days_held: int, horizon_days: int) -> Dict[str, Any]:
|
||||
"weight": 0.0, "color": "slate", "ratio_pct": pct,
|
||||
"readable": f"{d}j / {h}j ({pct}% écoulé — bruit statistique)",
|
||||
}
|
||||
elif ratio < 0.35:
|
||||
elif ratio < mature_threshold:
|
||||
return {
|
||||
"status": "debut", "label": "Début", "emoji": "📊",
|
||||
"weight": 0.25, "color": "yellow", "ratio_pct": pct,
|
||||
@@ -1708,6 +1716,7 @@ def get_pattern_reliability(pattern_id: str = None) -> List[Dict]:
|
||||
conn.close()
|
||||
|
||||
today = _date.today()
|
||||
mature_threshold = float(get_config("maturity_threshold_pct") or "35") / 100.0
|
||||
by_pattern: Dict[str, list] = {}
|
||||
for row in rows:
|
||||
r = dict(row)
|
||||
@@ -1718,8 +1727,7 @@ def get_pattern_reliability(pattern_id: str = None) -> List[Dict]:
|
||||
days_held = 0
|
||||
horizon = r.get("horizon_days") or 30
|
||||
ratio = days_held / horizon if horizon else 0
|
||||
# Only mature trades (≥35% of horizon elapsed)
|
||||
if ratio < 0.35:
|
||||
if ratio < mature_threshold:
|
||||
continue
|
||||
by_pattern.setdefault(r["pattern_id"], []).append(r)
|
||||
|
||||
|
||||
@@ -359,6 +359,8 @@ export default function Config() {
|
||||
const [cycleSimilarity, setCycleSimilarity] = useState(0.30)
|
||||
const [minEv, setMinEv] = useState(0.0)
|
||||
const [minScore, setMinScore] = useState(0)
|
||||
const [retentionDays, setRetentionDays] = useState(90)
|
||||
const [maturityThreshold, setMaturityThreshold] = useState(35)
|
||||
useEffect(() => {
|
||||
if (cs) {
|
||||
setCycleEnabled(cs.enabled ?? false)
|
||||
@@ -366,6 +368,8 @@ export default function Config() {
|
||||
setCycleSimilarity(cs.similarity_threshold ?? 0.30)
|
||||
setMinEv(cs.min_ev_threshold ?? 0.0)
|
||||
setMinScore(cs.min_score_threshold ?? 0)
|
||||
setRetentionDays(cs.journal_retention_days ?? 90)
|
||||
setMaturityThreshold(cs.maturity_threshold_pct ?? 35)
|
||||
}
|
||||
}, [cs])
|
||||
|
||||
@@ -676,6 +680,41 @@ export default function Config() {
|
||||
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>
|
||||
@@ -743,7 +782,7 @@ export default function Config() {
|
||||
<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 },
|
||||
{ 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}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useState, useEffect, useRef, Fragment } from 'react'
|
||||
import { BookOpen, TrendingUp, TrendingDown, Activity, AlertTriangle, RefreshCw, Zap, CheckCircle, XCircle, Brain, Trash2, Search, X, ChevronDown, ChevronUp } from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
import { useJournalSummary, useMacroHistory, useGeoHistory, useTradeMtm, useCycleHistory, useCycleStatus, useTriggerCycle, useTradePostmortem, useAnalyzePostmortem, useIvForTrade, useKellySizing, api } from '../hooks/useApi'
|
||||
@@ -443,8 +443,13 @@ function TradeMtmSection({ days }: { days: number }) {
|
||||
{trades.map((t: any) => {
|
||||
const evNet = t.ev_net ?? null
|
||||
const tradeScore = t.trade_score ?? null
|
||||
const isExpanded = selectedTradeId === t.id
|
||||
return (
|
||||
<tr key={t.id} className="hover:bg-dark-700/30 transition-colors">
|
||||
<Fragment key={t.id}>
|
||||
<tr
|
||||
className={clsx('hover:bg-dark-700/30 transition-colors cursor-pointer', isExpanded && 'bg-dark-700/40 border-b-0')}
|
||||
onClick={() => setSelectedTradeId(isExpanded ? null : t.id)}
|
||||
>
|
||||
<td className="px-3 py-2 text-slate-300 max-w-[110px] truncate">{t.pattern_name || t.pattern_id}</td>
|
||||
<td className="px-3 py-2">
|
||||
{t.matched_profile ? (
|
||||
@@ -532,21 +537,29 @@ function TradeMtmSection({ days }: { days: number }) {
|
||||
<td className="px-3 py-2 text-right">
|
||||
<PnlBadge pnl={t.pnl_pct} />
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<td className="px-2 py-2" onClick={e => e.stopPropagation()}>
|
||||
<button
|
||||
onClick={() => setSelectedTradeId(selectedTradeId === t.id ? null : t.id)}
|
||||
onClick={() => setSelectedTradeId(isExpanded ? null : t.id)}
|
||||
className={clsx(
|
||||
'p-1 rounded transition-colors',
|
||||
selectedTradeId === t.id
|
||||
isExpanded
|
||||
? 'text-blue-400 bg-blue-900/30'
|
||||
: 'text-slate-600 hover:text-blue-400 hover:bg-blue-900/20'
|
||||
)}
|
||||
title="Post-mortem IA"
|
||||
>
|
||||
<Search className="w-3.5 h-3.5" />
|
||||
{isExpanded ? <ChevronUp className="w-3.5 h-3.5" /> : <Search className="w-3.5 h-3.5" />}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{isExpanded && (
|
||||
<tr className="bg-dark-900/60">
|
||||
<td colSpan={16} className="px-0 py-0 border-b border-blue-700/20">
|
||||
<PostmortemPanel tradeId={t.id} onClose={() => setSelectedTradeId(null)} />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
@@ -554,12 +567,6 @@ function TradeMtmSection({ days }: { days: number }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedTradeId !== null && (
|
||||
<PostmortemPanel
|
||||
tradeId={selectedTradeId}
|
||||
onClose={() => setSelectedTradeId(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -848,7 +855,7 @@ export default function JournalDeBord() {
|
||||
|
||||
{/* Period selector */}
|
||||
<div className="flex gap-1 bg-dark-700 p-1 rounded text-xs">
|
||||
{[7, 15, 30].map(d => (
|
||||
{[15, 30, 60, 90].map(d => (
|
||||
<button key={d} onClick={() => setDays(d)}
|
||||
className={clsx('px-2.5 py-1 rounded transition-colors', {
|
||||
'bg-blue-600 text-white': days === d,
|
||||
|
||||
Reference in New Issue
Block a user