feat: time-aware trade maturity classification
- Add _trade_maturity() helper: classifies trades by % of horizon elapsed (trop_tot <10%, debut 10-35%, mature 35-75%, fin_horizon >75%) - Fix horizon_days fallback chain in log_trade_entries (default 30→90) - journal.py: enrich each MTM trade with maturity dict + horizon_days - reasoning.py: portfolio report segments trades by maturity; GPT-4o draws lessons only from matures (≥35% elapsed), never from trop_tot - auto_cycle.py: 90d window, maturity-aware prompt with timing rules - JournalDeBord.tsx: maturity badge with emoji, label, progress bar and day counter (Xj / Yj Z%) replacing plain days_held column Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
from fastapi import APIRouter
|
||||
from typing import Any, Dict, List
|
||||
import math
|
||||
from services.database import get_macro_regime_history, get_geo_alert_history, get_trade_entry_prices, reset_journal_history, _fetch_live_prices
|
||||
from services.database import get_macro_regime_history, get_geo_alert_history, get_trade_entry_prices, reset_journal_history, _fetch_live_prices, _trade_maturity
|
||||
|
||||
|
||||
def _sanitize(obj: Any) -> Any:
|
||||
@@ -66,12 +66,16 @@ def trade_mtm(days: int = 30):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
horizon = e.get("horizon_days") or 90
|
||||
maturity = _trade_maturity(days_held or 0, horizon)
|
||||
|
||||
result.append({
|
||||
**e,
|
||||
"current_price": current_price,
|
||||
"pnl_pct": pnl_pct,
|
||||
"days_held": days_held,
|
||||
"direction": "bearish" if _is_bearish(e.get("strategy", "")) else "bullish",
|
||||
"maturity": maturity,
|
||||
})
|
||||
|
||||
return _sanitize({"trades": result, "days": days, "tickers_fetched": len(current_prices)})
|
||||
|
||||
@@ -21,6 +21,7 @@ from services.database import (
|
||||
get_trade_entry_by_id,
|
||||
list_ai_reports,
|
||||
save_ai_report,
|
||||
_trade_maturity,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -234,36 +235,72 @@ def generate_portfolio_report(days: int = 90):
|
||||
|
||||
from services.ai_analyzer import _chat
|
||||
|
||||
data = get_mtm_trades_with_traces(days=days, limit_movers=5)
|
||||
winners = data["winners"]
|
||||
losers = data["losers"]
|
||||
from datetime import date as _date
|
||||
|
||||
winners_block = _trade_summary_block("TOP GAINS", winners)
|
||||
losers_block = _trade_summary_block("TOP PERTES", losers)
|
||||
data = get_mtm_trades_with_traces(days=days, limit_movers=10)
|
||||
all_trades = data.get("all_trades", [])
|
||||
|
||||
avg_pnl = data.get("avg_pnl_pct")
|
||||
# Classify every trade by maturity
|
||||
def _enrich_maturity(t: dict) -> dict:
|
||||
try:
|
||||
dh = (_date.today() - _date.fromisoformat(t["entry_date"])).days
|
||||
except Exception:
|
||||
dh = 0
|
||||
return {**t, "days_held": dh, "maturity": _trade_maturity(dh, t.get("horizon_days") or 90)}
|
||||
|
||||
enriched = [_enrich_maturity(t) for t in all_trades if t.get("pnl_pct") is not None]
|
||||
trop_tot = [t for t in enriched if t["maturity"]["status"] == "trop_tot"]
|
||||
en_cours = [t for t in enriched if t["maturity"]["status"] == "debut"]
|
||||
matures = [t for t in enriched if t["maturity"]["status"] in ("mature", "fin_horizon")]
|
||||
|
||||
# Sort by P&L for report
|
||||
winners = sorted(matures, key=lambda t: t.get("pnl_pct", 0), reverse=True)[:5]
|
||||
losers = sorted(matures, key=lambda t: t.get("pnl_pct", 0))[:5]
|
||||
avg_pnl = (sum(t.get("pnl_pct", 0) for t in matures) / len(matures)) if matures else None
|
||||
avg_str = f"{avg_pnl:+.1f}%" if avg_pnl is not None else "N/A"
|
||||
|
||||
def _tline(t: dict) -> str:
|
||||
mat = t["maturity"]
|
||||
return (f" {t.get('underlying','?')} {t.get('strategy','?')} "
|
||||
f"P&L={t.get('pnl_pct',0):+.1f}% [{mat['readable']}] "
|
||||
f"score={t.get('score_at_entry','?')}")
|
||||
|
||||
mature_wins = "\n".join(_tline(t) for t in winners) or " Aucun"
|
||||
mature_loss = "\n".join(_tline(t) for t in losers) or " Aucun"
|
||||
early_lines = "\n".join(_tline(t) for t in (trop_tot + en_cours)[:8]) or " Aucun"
|
||||
|
||||
prompt = f"""Tu es un stratège macro-géopolitique senior. Génère un rapport synthétique sur notre portefeuille options.
|
||||
|
||||
⚠️ RÈGLE FONDAMENTALE DE TIMING :
|
||||
Nos trades sont des options de 30 à 90 jours. Un trade vieux de 3 jours n'apporte AUCUNE information sur sa performance finale.
|
||||
Tu dois tirer des leçons UNIQUEMENT des trades MATURES (≥35% de l'horizon écoulé).
|
||||
Les trades IMMATURES (< 35%) sont listés pour transparence — n'en tire aucune conclusion de performance.
|
||||
|
||||
═══ STATISTIQUES GLOBALES ═══
|
||||
Période : {days} derniers jours
|
||||
Trades total: {data['total_trades']} | Pricés: {data['priced_count']} | P&L moyen: {avg_str}
|
||||
Période : {days}j | Total trades: {len(all_trades)} | Pricés: {data['priced_count']}
|
||||
MATURES (signal fiable): {len(matures)} — P&L moyen: {avg_str}
|
||||
IMMATURES (trop tôt): {len(trop_tot) + len(en_cours)}
|
||||
|
||||
═══ {winners_block}
|
||||
═══ MATURES — TOP GAINS (signal fiable — tire des leçons ici) ═══
|
||||
{mature_wins}
|
||||
|
||||
═══ {losers_block}
|
||||
═══ MATURES — TOP PERTES (signal fiable — tire des leçons ici) ═══
|
||||
{mature_loss}
|
||||
|
||||
Génère un rapport JSON structuré :
|
||||
═══ EN COURS / IMMATURES (ne pas juger la performance) ═══
|
||||
{early_lines}
|
||||
|
||||
Génère un rapport JSON structuré basé UNIQUEMENT sur les trades matures :
|
||||
{{
|
||||
"headline": "<1 phrase résumant la performance de la période>",
|
||||
"regime_assessment": "<le régime macro a-t-il bien servi nos thèses ? convergence ou divergence ?>",
|
||||
"winners_analysis": "<pourquoi ces trades ont marché — pattern commun, catalyseur, régime ? 3-4 phrases>",
|
||||
"losers_analysis": "<pourquoi ces trades ont déçu — mauvaise thèse, mauvais timing, contra-signal manqué ? 3-4 phrases>",
|
||||
"key_lessons": ["<leçon 1>", "<leçon 2>", "<leçon 3>"],
|
||||
"blind_spots": "<ce que notre système de scoring n'a pas bien capturé cette période>",
|
||||
"next_cycle_priorities": "<3 priorités concrètes pour améliorer les prochains cycles : patterns à surveiller, ajustements de scoring, régimes à anticiper>",
|
||||
"risk_watch": "<1-2 risques macro-géopolitiques à surveiller de près qui pourraient impacter nos positions actuelles>"
|
||||
"headline": "<1 phrase résumant la performance des trades matures>",
|
||||
"regime_assessment": "<le régime macro a-t-il bien servi nos thèses matures ?>",
|
||||
"winners_analysis": "<pourquoi les trades matures gagnants ont marché — 3-4 phrases>",
|
||||
"losers_analysis": "<pourquoi les trades matures perdants ont déçu — 3-4 phrases>",
|
||||
"key_lessons": ["<leçon 1 des matures>", "<leçon 2>", "<leçon 3>"],
|
||||
"blind_spots": "<ce que le scoring n'a pas bien capturé sur les trades matures>",
|
||||
"next_cycle_priorities": "<3 priorités concrètes pour les prochains cycles>",
|
||||
"risk_watch": "<1-2 risques à surveiller>",
|
||||
"timing_note": "<observation sur les trades immatures — signaux à surveiller sans jugement>"
|
||||
}}"""
|
||||
|
||||
try:
|
||||
@@ -272,7 +309,7 @@ Génère un rapport JSON structuré :
|
||||
prompt,
|
||||
model="gpt-4o",
|
||||
json_mode=True,
|
||||
max_tokens=1200,
|
||||
max_tokens=1400,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"[PortfolioReport] GPT-4o call failed: {e}")
|
||||
@@ -282,9 +319,11 @@ Génère un rapport JSON structuré :
|
||||
raise HTTPException(503, "GPT-4o n'a pas retourné de réponse")
|
||||
|
||||
stats = {
|
||||
"total_trades": data["total_trades"],
|
||||
"total_trades": len(all_trades),
|
||||
"priced_count": data["priced_count"],
|
||||
"avg_pnl_pct": avg_pnl,
|
||||
"mature_count": len(matures),
|
||||
"early_count": len(trop_tot) + len(en_cours),
|
||||
}
|
||||
|
||||
report_id = save_ai_report(
|
||||
|
||||
Reference in New Issue
Block a user