feat: P&L côte à côte (Ouvert|Réalisé) + filtres journal + suppression trades fermés
- Dashboard: P&L card séparé en deux colonnes (Ouvertes/Réalisées) pour Simulé et Portfolio
- Dashboard: closed trades P&L locked from pnl_realized, ne fluctue plus après fermeture
- Journal Ouvert: filtres ticker/stratégie + classe d'actif + direction (haussier/baissier)
- Journal Fermés: mêmes filtres + filtre P&L (gagnants/perdants) + bouton supprimer par ligne
- Journal Non loggés: filtres ticker + classe d'actif + raison de skip
- Backend: DELETE /api/journal/trades/{id} + delete_trade() dans database.py
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, get_skipped_trades,
|
||||
_fetch_live_prices, _trade_maturity, get_skipped_trades, delete_trade,
|
||||
)
|
||||
import json
|
||||
|
||||
@@ -61,11 +61,35 @@ def trade_mtm(days: int = 30):
|
||||
for e in entries:
|
||||
ticker = (e.get("underlying") or "").upper()
|
||||
entry_price = e.get("entry_price")
|
||||
current_price = current_prices.get(ticker)
|
||||
pnl_pct = None
|
||||
if entry_price and current_price and entry_price > 0:
|
||||
raw_pnl = (current_price - entry_price) / entry_price * 100
|
||||
pnl_pct = round(-raw_pnl if _is_bearish(e.get("strategy", "")) else raw_pnl, 2)
|
||||
is_closed = e.get("status") == "closed"
|
||||
|
||||
# For closed trades: pnl_pct is fixed from pnl_realized (locked in).
|
||||
# For open trades: compute live from current market price.
|
||||
if is_closed:
|
||||
pnl_realized = e.get("pnl_realized")
|
||||
capital = e.get("capital_invested") or entry_price or 0
|
||||
if pnl_realized is not None and capital and capital > 0:
|
||||
pnl_pct = round(pnl_realized / capital * 100, 2)
|
||||
elif e.get("close_price") and entry_price and entry_price > 0:
|
||||
raw = (e["close_price"] - entry_price) / entry_price * 100
|
||||
pnl_pct = round(-raw if _is_bearish(e.get("strategy", "")) else raw, 2)
|
||||
else:
|
||||
pnl_pct = None
|
||||
current_price = e.get("close_price")
|
||||
price_warning = None if pnl_pct is not None else "no_close_price"
|
||||
else:
|
||||
current_price = current_prices.get(ticker)
|
||||
pnl_pct = None
|
||||
if entry_price and current_price and entry_price > 0:
|
||||
raw_pnl = (current_price - entry_price) / entry_price * 100
|
||||
pnl_pct = round(-raw_pnl if _is_bearish(e.get("strategy", "")) else raw_pnl, 2)
|
||||
price_warning = None
|
||||
if entry_price is None and current_price is None:
|
||||
price_warning = "no_price_data"
|
||||
elif entry_price is None:
|
||||
price_warning = "no_entry_price"
|
||||
elif current_price is None:
|
||||
price_warning = "no_live_price"
|
||||
|
||||
days_held = None
|
||||
try:
|
||||
@@ -74,27 +98,19 @@ def trade_mtm(days: int = 30):
|
||||
pass
|
||||
|
||||
horizon = e.get("horizon_days") or 90
|
||||
maturity = _trade_maturity(days_held or 0, horizon)
|
||||
maturity = "closed" if is_closed else _trade_maturity(days_held or 0, horizon)
|
||||
|
||||
defaults = _get_exit_defaults()
|
||||
target = e.get("target_pct") if e.get("target_pct") is not None else defaults.get("target_pct", 30.0)
|
||||
stop = e.get("stop_loss_pct") if e.get("stop_loss_pct") is not None else defaults.get("stop_loss_pct", -50.0)
|
||||
|
||||
alert_type = None
|
||||
if pnl_pct is not None:
|
||||
if not is_closed and pnl_pct is not None:
|
||||
if pnl_pct >= target:
|
||||
alert_type = "target_reached"
|
||||
elif pnl_pct <= stop:
|
||||
alert_type = "stop_loss"
|
||||
|
||||
price_warning = None
|
||||
if entry_price is None and current_price is None:
|
||||
price_warning = "no_price_data"
|
||||
elif entry_price is None:
|
||||
price_warning = "no_entry_price"
|
||||
elif current_price is None:
|
||||
price_warning = "no_live_price"
|
||||
|
||||
result.append({
|
||||
**e,
|
||||
"current_price": current_price,
|
||||
@@ -260,6 +276,14 @@ def skipped_trades_endpoint(days: int = 30):
|
||||
return _sanitize({"trades": trades, "days": days, "count": len(trades)})
|
||||
|
||||
|
||||
@router.delete("/trades/{trade_id}")
|
||||
def delete_trade_endpoint(trade_id: int):
|
||||
ok = delete_trade(trade_id)
|
||||
if not ok:
|
||||
raise HTTPException(404, "Trade non trouvé")
|
||||
return {"deleted": True, "trade_id": trade_id}
|
||||
|
||||
|
||||
@router.delete("/reset")
|
||||
def reset_journal():
|
||||
"""Truncate all journal history (trades, macro, geo, cycles). Irreversible."""
|
||||
|
||||
@@ -1327,6 +1327,14 @@ def get_closed_trades(days: int = 180) -> List[Dict[str, Any]]:
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
def delete_trade(trade_id: int) -> bool:
|
||||
conn = get_conn()
|
||||
cur = conn.execute("DELETE FROM trade_entry_prices WHERE id = ?", (trade_id,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return cur.rowcount > 0
|
||||
|
||||
|
||||
def get_skipped_trades(days: int = 30) -> List[Dict[str, Any]]:
|
||||
conn = get_conn()
|
||||
rows = conn.execute(
|
||||
|
||||
Reference in New Issue
Block a user