From abee090881340bb541e3756752f64f96530e6bad Mon Sep 17 00:00:00 2001
From: OpenSquared
Date: Thu, 18 Jun 2026 10:08:16 +0200
Subject: [PATCH] feat: expandable inline rows in Journal + journal/maturity
params in Config
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 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
---
backend/routers/cycle.py | 10 +++++++
backend/services/auto_cycle.py | 5 ++++
backend/services/database.py | 30 ++++++++++++--------
frontend/src/pages/Config.tsx | 41 +++++++++++++++++++++++++++-
frontend/src/pages/JournalDeBord.tsx | 33 +++++++++++++---------
5 files changed, 94 insertions(+), 25 deletions(-)
diff --git a/backend/routers/cycle.py b/backend/routers/cycle.py
index 6299c69..2aefce2 100644
--- a/backend/routers/cycle.py
+++ b/backend/routers/cycle.py
@@ -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()
diff --git a/backend/services/auto_cycle.py b/backend/services/auto_cycle.py
index b5951ec..8ccee10 100644
--- a/backend/services/auto_cycle.py
+++ b/backend/services/auto_cycle.py
@@ -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()),
}
diff --git a/backend/services/database.py b/backend/services/database.py
index dea1dd0..66a07e2 100644
--- a/backend/services/database.py
+++ b/backend/services/database.py
@@ -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)
diff --git a/frontend/src/pages/Config.tsx b/frontend/src/pages/Config.tsx
index ab46311..9b7d832 100644
--- a/frontend/src/pages/Config.tsx
+++ b/frontend/src/pages/Config.tsx
@@ -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.
+
+
+
+
+ {[30, 60, 90, 180].map(d => (
+
+ ))}
+
+
+
+
+
+ {[20, 30, 35, 50].map(p => (
+
+ ))}
+
+
+
+
@@ -743,7 +782,7 @@ export default function Config() {