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 fastapi import APIRouter
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
import math
|
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:
|
def _sanitize(obj: Any) -> Any:
|
||||||
@@ -66,12 +66,16 @@ def trade_mtm(days: int = 30):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
horizon = e.get("horizon_days") or 90
|
||||||
|
maturity = _trade_maturity(days_held or 0, horizon)
|
||||||
|
|
||||||
result.append({
|
result.append({
|
||||||
**e,
|
**e,
|
||||||
"current_price": current_price,
|
"current_price": current_price,
|
||||||
"pnl_pct": pnl_pct,
|
"pnl_pct": pnl_pct,
|
||||||
"days_held": days_held,
|
"days_held": days_held,
|
||||||
"direction": "bearish" if _is_bearish(e.get("strategy", "")) else "bullish",
|
"direction": "bearish" if _is_bearish(e.get("strategy", "")) else "bullish",
|
||||||
|
"maturity": maturity,
|
||||||
})
|
})
|
||||||
|
|
||||||
return _sanitize({"trades": result, "days": days, "tickers_fetched": len(current_prices)})
|
return _sanitize({"trades": result, "days": days, "tickers_fetched": len(current_prices)})
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from services.database import (
|
|||||||
get_trade_entry_by_id,
|
get_trade_entry_by_id,
|
||||||
list_ai_reports,
|
list_ai_reports,
|
||||||
save_ai_report,
|
save_ai_report,
|
||||||
|
_trade_maturity,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -234,36 +235,72 @@ def generate_portfolio_report(days: int = 90):
|
|||||||
|
|
||||||
from services.ai_analyzer import _chat
|
from services.ai_analyzer import _chat
|
||||||
|
|
||||||
data = get_mtm_trades_with_traces(days=days, limit_movers=5)
|
from datetime import date as _date
|
||||||
winners = data["winners"]
|
|
||||||
losers = data["losers"]
|
|
||||||
|
|
||||||
winners_block = _trade_summary_block("TOP GAINS", winners)
|
data = get_mtm_trades_with_traces(days=days, limit_movers=10)
|
||||||
losers_block = _trade_summary_block("TOP PERTES", losers)
|
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"
|
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.
|
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 ═══
|
═══ STATISTIQUES GLOBALES ═══
|
||||||
Période : {days} derniers jours
|
Période : {days}j | Total trades: {len(all_trades)} | Pricés: {data['priced_count']}
|
||||||
Trades total: {data['total_trades']} | Pricés: {data['priced_count']} | P&L moyen: {avg_str}
|
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>",
|
"headline": "<1 phrase résumant la performance des trades matures>",
|
||||||
"regime_assessment": "<le régime macro a-t-il bien servi nos thèses ? convergence ou divergence ?>",
|
"regime_assessment": "<le régime macro a-t-il bien servi nos thèses matures ?>",
|
||||||
"winners_analysis": "<pourquoi ces trades ont marché — pattern commun, catalyseur, régime ? 3-4 phrases>",
|
"winners_analysis": "<pourquoi les trades matures gagnants ont marché — 3-4 phrases>",
|
||||||
"losers_analysis": "<pourquoi ces trades ont déçu — mauvaise thèse, mauvais timing, contra-signal manqué ? 3-4 phrases>",
|
"losers_analysis": "<pourquoi les trades matures perdants ont déçu — 3-4 phrases>",
|
||||||
"key_lessons": ["<leçon 1>", "<leçon 2>", "<leçon 3>"],
|
"key_lessons": ["<leçon 1 des matures>", "<leçon 2>", "<leçon 3>"],
|
||||||
"blind_spots": "<ce que notre système de scoring n'a pas bien capturé cette période>",
|
"blind_spots": "<ce que le scoring n'a pas bien capturé sur les trades matures>",
|
||||||
"next_cycle_priorities": "<3 priorités concrètes pour améliorer les prochains cycles : patterns à surveiller, ajustements de scoring, régimes à anticiper>",
|
"next_cycle_priorities": "<3 priorités concrètes pour les prochains cycles>",
|
||||||
"risk_watch": "<1-2 risques macro-géopolitiques à surveiller de près qui pourraient impacter nos positions actuelles>"
|
"risk_watch": "<1-2 risques à surveiller>",
|
||||||
|
"timing_note": "<observation sur les trades immatures — signaux à surveiller sans jugement>"
|
||||||
}}"""
|
}}"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -272,7 +309,7 @@ Génère un rapport JSON structuré :
|
|||||||
prompt,
|
prompt,
|
||||||
model="gpt-4o",
|
model="gpt-4o",
|
||||||
json_mode=True,
|
json_mode=True,
|
||||||
max_tokens=1200,
|
max_tokens=1400,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[PortfolioReport] GPT-4o call failed: {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")
|
raise HTTPException(503, "GPT-4o n'a pas retourné de réponse")
|
||||||
|
|
||||||
stats = {
|
stats = {
|
||||||
"total_trades": data["total_trades"],
|
"total_trades": len(all_trades),
|
||||||
"priced_count": data["priced_count"],
|
"priced_count": data["priced_count"],
|
||||||
"avg_pnl_pct": avg_pnl,
|
"avg_pnl_pct": avg_pnl,
|
||||||
|
"mature_count": len(matures),
|
||||||
|
"early_count": len(trop_tot) + len(en_cours),
|
||||||
}
|
}
|
||||||
|
|
||||||
report_id = save_ai_report(
|
report_id = save_ai_report(
|
||||||
|
|||||||
@@ -503,60 +503,106 @@ def _auto_portfolio_snapshot(ai_key: str) -> None:
|
|||||||
os.environ["OPENAI_API_KEY"] = ai_key
|
os.environ["OPENAI_API_KEY"] = ai_key
|
||||||
|
|
||||||
from services.database import (
|
from services.database import (
|
||||||
get_mtm_trades_with_traces, save_ai_report, get_latest_portfolio_lessons,
|
get_mtm_trades_with_traces, save_ai_report, _trade_maturity,
|
||||||
)
|
)
|
||||||
|
from datetime import date as _date
|
||||||
|
|
||||||
data = get_mtm_trades_with_traces(days=30, limit_movers=5)
|
data = get_mtm_trades_with_traces(days=90, limit_movers=10)
|
||||||
winners = data.get("winners", [])
|
all_trades = data.get("all_trades", [])
|
||||||
losers = data.get("losers", [])
|
|
||||||
priced = data.get("priced_count", 0)
|
priced = data.get("priced_count", 0)
|
||||||
|
|
||||||
# Need at least 3 priced trades with actual movement to make analysis meaningful
|
# Classify each trade by maturity
|
||||||
meaningful = [
|
def _with_maturity(t: Dict) -> Dict:
|
||||||
t for t in (winners + losers)
|
try:
|
||||||
if t.get("pnl_pct") is not None and abs(t.get("pnl_pct", 0)) > 0.05
|
dh = (_date.today() - _date.fromisoformat(t["entry_date"])).days
|
||||||
]
|
except Exception:
|
||||||
if len(meaningful) < 3:
|
dh = 0
|
||||||
|
mat = _trade_maturity(dh, t.get("horizon_days") or 90)
|
||||||
|
return {**t, "days_held": dh, "maturity": mat}
|
||||||
|
|
||||||
|
enriched = [_with_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")]
|
||||||
|
|
||||||
|
# Only learn lessons from mature trades (weight > 0.9)
|
||||||
|
meaningful_mature = [t for t in matures if abs(t.get("pnl_pct", 0)) > 0.1]
|
||||||
|
|
||||||
|
if len(meaningful_mature) < 2:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[AutoSnapshot] Skipping GPT-4o report: only {len(meaningful)} trades "
|
f"[AutoSnapshot] Skipping GPT-4o report: only {len(meaningful_mature)} mature trades "
|
||||||
f"with meaningful P&L movement (need ≥ 3)"
|
f"with meaningful P&L (need ≥ 2). "
|
||||||
|
f"Immatures={len(trop_tot)}, en cours={len(en_cours)}, matures={len(matures)}"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
avg_pnl = data.get("avg_pnl_pct")
|
# Sort matures by P&L for the 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
|
||||||
|
|
||||||
stats = {
|
stats = {
|
||||||
"total_trades": data["total_trades"],
|
"total_trades": len(all_trades),
|
||||||
"priced_count": priced,
|
"priced_count": priced,
|
||||||
"avg_pnl_pct": avg_pnl,
|
"avg_pnl_pct": avg_pnl,
|
||||||
|
"mature_count": len(matures),
|
||||||
|
"early_count": len(trop_tot) + len(en_cours),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Build prompt (reuse same logic as reasoning.py generate endpoint)
|
# Build maturity context blocks for the prompt
|
||||||
from routers.reasoning import _trade_summary_block, _bucket_summary, _rankings_summary
|
def _trade_line(t: Dict) -> str:
|
||||||
|
mat = t["maturity"]
|
||||||
|
return (
|
||||||
|
f" {t.get('underlying','?')} {t.get('strategy','?')} "
|
||||||
|
f"P&L={t.get('pnl_pct',0):+.1f}% "
|
||||||
|
f"[{mat['readable']}] "
|
||||||
|
f"score_entrée={t.get('score_at_entry','?')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
mature_wins_block = "\n".join(_trade_line(t) for t in winners) or " Aucun"
|
||||||
|
mature_losses_block = "\n".join(_trade_line(t) for t in losers) or " Aucun"
|
||||||
|
early_block = "\n".join(
|
||||||
|
f" {t.get('underlying','?')} {t.get('strategy','?')} "
|
||||||
|
f"P&L={t.get('pnl_pct',0):+.1f}% [{t['maturity']['readable']}]"
|
||||||
|
for t in (trop_tot + en_cours)[:6]
|
||||||
|
) or " Aucun"
|
||||||
|
|
||||||
|
avg_str = f"{avg_pnl:+.1f}%" if avg_pnl is not None else "N/A"
|
||||||
from services.ai_analyzer import _chat
|
from services.ai_analyzer import _chat
|
||||||
|
|
||||||
winners_block = _trade_summary_block("TOP GAINS", winners)
|
prompt = f"""Tu es un stratège macro-géopolitique senior. Rapport post-cycle automatique.
|
||||||
losers_block = _trade_summary_block("TOP PERTES", losers)
|
|
||||||
avg_str = f"{avg_pnl:+.1f}%" if avg_pnl is not None else "N/A"
|
|
||||||
|
|
||||||
prompt = f"""Tu es un stratège macro-géopolitique senior. Rapport synthétique post-cycle automatique.
|
⚠️ RÈGLE FONDAMENTALE DE TIMING :
|
||||||
|
Les options ont des horizons de 30-90 jours. Un trade de 3 jours ne dit RIEN sur sa performance finale.
|
||||||
|
Tu dois UNIQUEMENT tirer des leçons des trades MATURES (≥35% de l'horizon écoulé).
|
||||||
|
Les trades immatures sont listés pour information seulement — n'en tire AUCUNE conclusion de performance.
|
||||||
|
|
||||||
═══ STATISTIQUES GLOBALES ═══
|
═══ STATISTIQUES GLOBALES ═══
|
||||||
Période : 30 derniers jours | Trades total: {data['total_trades']} | Pricés: {priced} | P&L moyen: {avg_str}
|
Période analysée : 90 jours | Trades total: {len(all_trades)} | Pricés: {priced}
|
||||||
|
Trades MATURES (signal fiable): {len(matures)} | P&L moyen matures: {avg_str}
|
||||||
|
Trades IMMATURES (trop tôt pour juger): {len(trop_tot) + len(en_cours)}
|
||||||
|
|
||||||
═══ {winners_block}
|
═══ MATURES — TOP GAINS (signal fiable) ═══
|
||||||
|
{mature_wins_block}
|
||||||
|
|
||||||
═══ {losers_block}
|
═══ MATURES — TOP PERTES (signal fiable) ═══
|
||||||
|
{mature_losses_block}
|
||||||
|
|
||||||
Génère un rapport JSON :
|
═══ EN COURS — NE PAS ÉVALUER (trop tôt) ═══
|
||||||
|
{early_block}
|
||||||
|
|
||||||
|
Génère un rapport JSON basé UNIQUEMENT sur les trades matures :
|
||||||
{{
|
{{
|
||||||
"headline": "<1 phrase résumant la performance>",
|
"headline": "<1 phrase résumant la performance des trades matures>",
|
||||||
"regime_assessment": "<alignement régime macro avec nos thèses ?>",
|
"regime_assessment": "<alignement régime macro avec nos thèses matures ?>",
|
||||||
"winners_analysis": "<pourquoi ces trades ont marché — 2-3 phrases>",
|
"winners_analysis": "<pourquoi les trades matures gagnants ont marché — 2-3 phrases>",
|
||||||
"losers_analysis": "<pourquoi ces trades ont déçu — 2-3 phrases>",
|
"losers_analysis": "<pourquoi les trades matures perdants ont déçu — 2-3 phrases>",
|
||||||
"key_lessons": ["<leçon 1>", "<leçon 2>", "<leçon 3>"],
|
"key_lessons": ["<leçon 1 tirée des matures>", "<leçon 2>", "<leçon 3>"],
|
||||||
"blind_spots": "<ce que le scoring n'a pas bien capturé>",
|
"blind_spots": "<ce que le scoring n'a pas bien capturé sur les trades matures>",
|
||||||
"next_cycle_priorities": "<3 priorités pour le prochain cycle>",
|
"next_cycle_priorities": "<3 priorités pour le prochain cycle>",
|
||||||
"risk_watch": "<1-2 risques à surveiller>"
|
"risk_watch": "<1-2 risques à surveiller>",
|
||||||
|
"timing_note": "<observation sur les trades immatures — à surveiller mais pas à juger>"
|
||||||
}}"""
|
}}"""
|
||||||
|
|
||||||
result = _chat(
|
result = _chat(
|
||||||
|
|||||||
@@ -889,7 +889,13 @@ def log_trade_entries(run_id: str, scored_patterns: List[Dict[str, Any]], quotes
|
|||||||
|
|
||||||
ticker_key = underlying.upper()
|
ticker_key = underlying.upper()
|
||||||
entry_price = price_map.get(ticker_key)
|
entry_price = price_map.get(ticker_key)
|
||||||
horizon = int(trade.get("horizon_days") or sp.get("horizon_days") or 30)
|
horizon = int(
|
||||||
|
trade.get("horizon_days") or
|
||||||
|
sp.get("horizon_days") or
|
||||||
|
sp.get("recommended_trade", {}).get("expiry_days") or
|
||||||
|
_orig.get("horizon_days") or
|
||||||
|
90
|
||||||
|
)
|
||||||
|
|
||||||
existing_row = conn.execute(
|
existing_row = conn.execute(
|
||||||
"SELECT id FROM trade_entry_prices WHERE pattern_id=? AND underlying=? AND strategy=?",
|
"SELECT id FROM trade_entry_prices WHERE pattern_id=? AND underlying=? AND strategy=?",
|
||||||
@@ -975,6 +981,48 @@ def get_cycle_run(run_id: str) -> Optional[Dict[str, Any]]:
|
|||||||
return dict(row) if row else None
|
return dict(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
h = max(horizon_days or 90, 1)
|
||||||
|
d = max(days_held or 0, 0)
|
||||||
|
ratio = d / h
|
||||||
|
pct = round(ratio * 100, 1)
|
||||||
|
|
||||||
|
if ratio < 0.10:
|
||||||
|
return {
|
||||||
|
"status": "trop_tot", "label": "Trop tôt", "emoji": "🕐",
|
||||||
|
"weight": 0.0, "color": "slate", "ratio_pct": pct,
|
||||||
|
"readable": f"{d}j / {h}j ({pct}% écoulé — bruit statistique)",
|
||||||
|
}
|
||||||
|
elif ratio < 0.35:
|
||||||
|
return {
|
||||||
|
"status": "debut", "label": "Début", "emoji": "📊",
|
||||||
|
"weight": 0.25, "color": "yellow", "ratio_pct": pct,
|
||||||
|
"readable": f"{d}j / {h}j ({pct}% écoulé — signal précoce)",
|
||||||
|
}
|
||||||
|
elif ratio < 0.75:
|
||||||
|
return {
|
||||||
|
"status": "mature", "label": "Signal fiable", "emoji": "✅",
|
||||||
|
"weight": 1.0, "color": "emerald", "ratio_pct": pct,
|
||||||
|
"readable": f"{d}j / {h}j ({pct}% écoulé — signal fiable)",
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"status": "fin_horizon", "label": "Fin d'horizon", "emoji": "⏰",
|
||||||
|
"weight": 1.0, "color": "orange", "ratio_pct": pct,
|
||||||
|
"readable": f"{d}j / {h}j ({pct}% écoulé — surveiller de près)",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_trade_entry_prices(days: int = 30) -> List[Dict[str, Any]]:
|
def get_trade_entry_prices(days: int = 30) -> List[Dict[str, Any]]:
|
||||||
conn = get_conn()
|
conn = get_conn()
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
|
|||||||
@@ -394,7 +394,7 @@ function TradeMtmSection({ days }: { days: number }) {
|
|||||||
<th className="text-right px-3 py-2 font-medium">Date</th>
|
<th className="text-right px-3 py-2 font-medium">Date</th>
|
||||||
<th className="text-right px-3 py-2 font-medium">Prix entrée</th>
|
<th className="text-right px-3 py-2 font-medium">Prix entrée</th>
|
||||||
<th className="text-right px-3 py-2 font-medium">Prix actuel</th>
|
<th className="text-right px-3 py-2 font-medium">Prix actuel</th>
|
||||||
<th className="text-right px-3 py-2 font-medium">J</th>
|
<th className="text-right px-3 py-2 font-medium">Maturité</th>
|
||||||
<th className="text-right px-3 py-2 font-medium">P&L th.</th>
|
<th className="text-right px-3 py-2 font-medium">P&L th.</th>
|
||||||
<th className="px-3 py-2 font-medium w-8"></th>
|
<th className="px-3 py-2 font-medium w-8"></th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -451,8 +451,37 @@ function TradeMtmSection({ days }: { days: number }) {
|
|||||||
<td className="px-3 py-2 text-right font-mono text-slate-300 text-[11px]">
|
<td className="px-3 py-2 text-right font-mono text-slate-300 text-[11px]">
|
||||||
{t.current_price != null ? t.current_price.toFixed(2) : '—'}
|
{t.current_price != null ? t.current_price.toFixed(2) : '—'}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2 text-right text-slate-600 text-[11px]">
|
<td className="px-3 py-2 text-right">
|
||||||
{t.days_held != null ? t.days_held : '—'}
|
{t.maturity ? (
|
||||||
|
<div className="flex flex-col items-end gap-0.5">
|
||||||
|
<span className={clsx('text-[10px] font-semibold',
|
||||||
|
t.maturity.status === 'trop_tot' ? 'text-slate-500' :
|
||||||
|
t.maturity.status === 'debut' ? 'text-yellow-400' :
|
||||||
|
t.maturity.status === 'mature' ? 'text-emerald-400' :
|
||||||
|
'text-orange-400'
|
||||||
|
)}>
|
||||||
|
{t.maturity.emoji} {t.maturity.label}
|
||||||
|
</span>
|
||||||
|
<span className="text-[9px] text-slate-600 font-mono">
|
||||||
|
{t.days_held ?? 0}j / {t.horizon_days ?? 90}j ({t.maturity.ratio_pct}%)
|
||||||
|
</span>
|
||||||
|
<div className="w-16 h-1 bg-slate-700 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className={clsx('h-full rounded-full',
|
||||||
|
t.maturity.status === 'trop_tot' ? 'bg-slate-600' :
|
||||||
|
t.maturity.status === 'debut' ? 'bg-yellow-500' :
|
||||||
|
t.maturity.status === 'mature' ? 'bg-emerald-500' :
|
||||||
|
'bg-orange-500'
|
||||||
|
)}
|
||||||
|
style={{ width: `${Math.min(t.maturity.ratio_pct, 100)}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="text-slate-700 text-[11px]">
|
||||||
|
{t.days_held != null ? `${t.days_held}j` : '—'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2 text-right">
|
<td className="px-3 py-2 text-right">
|
||||||
<PnlBadge pnl={t.pnl_pct} />
|
<PnlBadge pnl={t.pnl_pct} />
|
||||||
|
|||||||
Reference in New Issue
Block a user