feat: trade lifecycle management — close, archive, target/stop alerts

- DB: 9 new columns on trade_entry_prices (status, closed_at, close_reason,
  close_note, pnl_realized, close_price, target_pct, stop_loss_pct, signal_threshold)
  via ALTER TABLE migration; close_trade(), get_closed_trades(),
  update_trade_exit_params() helpers; exit_defaults config key
- Backend: PATCH /trades/{id}/close, PATCH /trades/{id}/exit-params,
  GET/PUT /exit-defaults, GET /closed-trades with win-rate/avg-PnL stats;
  trade-mtm now computes alert_type (target_reached|stop_loss) per trade
- Journal: new "Fermés" tab with closed trades table + stats banner (win rate,
  avg PnL, total PnL, best trade); open trades show Cible/Stop progress bar +
  🎯/🛑 alert badges + 1-click close modal (price, reason, note)
- Config: new "Paramètres de sortie" panel — target_pct, stop_loss_pct,
  signal_reversal_mode, signal_reversal_threshold with live sliders

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-19 14:17:29 +02:00
parent 0ba73cae0b
commit ee69f3cbd9
5 changed files with 673 additions and 13 deletions

View File

@@ -209,6 +209,15 @@ def init_db():
("capital_invested", "REAL"),
("strike_guidance", "TEXT"),
("expiry_days_at_entry", "INTEGER"),
("status", "TEXT DEFAULT 'open'"),
("closed_at", "TEXT"),
("close_reason", "TEXT"),
("close_note", "TEXT"),
("pnl_realized", "REAL"),
("close_price", "REAL"),
("target_pct", "REAL"),
("stop_loss_pct", "REAL"),
("signal_threshold", "REAL"),
]:
try:
c.execute(f"ALTER TABLE trade_entry_prices ADD COLUMN {col} {definition}")
@@ -216,6 +225,7 @@ def init_db():
pass
try:
c.execute("CREATE INDEX IF NOT EXISTS idx_tep_date ON trade_entry_prices(entry_date DESC)")
c.execute("CREATE INDEX IF NOT EXISTS idx_tep_status ON trade_entry_prices(status, closed_at DESC)")
except Exception:
pass
@@ -273,6 +283,12 @@ def init_db():
"auto_cycle_similarity_threshold": "0.30",
"min_ev_threshold": "0.0",
"min_score_threshold": "0",
"exit_defaults": json.dumps({
"target_pct": 30.0,
"stop_loss_pct": -50.0,
"signal_reversal_mode": "badge_only",
"signal_reversal_threshold": 25,
}),
}
for k, v in defaults.items():
c.execute("INSERT OR IGNORE INTO config (key, value) VALUES (?, ?)", (k, v))
@@ -1160,7 +1176,8 @@ def get_trade_entry_prices(days: int = 30) -> List[Dict[str, Any]]:
conn = get_conn()
rows = conn.execute(
"""SELECT * FROM trade_entry_prices
WHERE entry_date >= date('now', ?)
WHERE (status IS NULL OR status = 'open')
AND entry_date >= date('now', ?)
ORDER BY entry_date DESC, score_at_entry DESC""",
(f"-{days} days",)
).fetchall()
@@ -1168,6 +1185,57 @@ def get_trade_entry_prices(days: int = 30) -> List[Dict[str, Any]]:
return [dict(r) for r in rows]
def get_closed_trades(days: int = 180) -> List[Dict[str, Any]]:
conn = get_conn()
rows = conn.execute(
"""SELECT * FROM trade_entry_prices
WHERE status = 'closed'
AND closed_at >= date('now', ?)
ORDER BY closed_at DESC""",
(f"-{days} days",)
).fetchall()
conn.close()
return [dict(r) for r in rows]
def close_trade(trade_id: int, close_price: float, pnl_realized: float,
close_reason: str, close_note: str = "") -> bool:
conn = get_conn()
cur = conn.execute(
"""UPDATE trade_entry_prices
SET status='closed', closed_at=datetime('now'), close_price=?,
pnl_realized=?, close_reason=?, close_note=?
WHERE id=? AND (status IS NULL OR status='open')""",
(close_price, pnl_realized, close_reason, close_note, trade_id)
)
conn.commit()
conn.close()
return cur.rowcount > 0
def update_trade_exit_params(trade_id: int, target_pct: float = None,
stop_loss_pct: float = None,
signal_threshold: float = None) -> bool:
updates: List[str] = []
vals: List[Any] = []
if target_pct is not None:
updates.append("target_pct=?"); vals.append(target_pct)
if stop_loss_pct is not None:
updates.append("stop_loss_pct=?"); vals.append(stop_loss_pct)
if signal_threshold is not None:
updates.append("signal_threshold=?"); vals.append(signal_threshold)
if not updates:
return False
conn = get_conn()
cur = conn.execute(
f"UPDATE trade_entry_prices SET {', '.join(updates)} WHERE id=?",
vals + [trade_id]
)
conn.commit()
conn.close()
return cur.rowcount > 0
def get_trade_entry_by_id(trade_id: int) -> Optional[Dict[str, Any]]:
conn = get_conn()
row = conn.execute("SELECT * FROM trade_entry_prices WHERE id=?", (trade_id,)).fetchone()