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, _trade_maturity def _sanitize(obj: Any) -> Any: """Replace NaN/Inf with None recursively for JSON compliance.""" if isinstance(obj, dict): return {k: _sanitize(v) for k, v in obj.items()} if isinstance(obj, list): return [_sanitize(v) for v in obj] if isinstance(obj, float) and (math.isnan(obj) or math.isinf(obj)): return None return obj router = APIRouter(prefix="/api/journal", tags=["journal"]) # Bearish strategies — P&L is inverted (profit when price falls) _BEARISH_KEYWORDS = {"bear", "put", "short", "sell", "vente", "baissier"} def _is_bearish(strategy: str) -> bool: s = (strategy or "").lower() return any(kw in s for kw in _BEARISH_KEYWORDS) @router.get("/macro-history") def macro_history(days: int = 15): """Macro regime snapshots for the last N days.""" return _sanitize({"history": get_macro_regime_history(days), "days": days}) @router.get("/geo-history") def geo_history(days: int = 30): """Geo alert score history for the last N days.""" return {"history": get_geo_alert_history(days), "days": days} @router.get("/trade-mtm") def trade_mtm(days: int = 30): """ Mark-to-market for all logged trade suggestions. Enriches with live prices via shared _fetch_live_prices utility. """ entries = get_trade_entry_prices(days) tickers_needed = list({(e.get("underlying") or "").upper() for e in entries if e.get("underlying")}) current_prices = _fetch_live_prices(tickers_needed, timeout=20) from datetime import date as _date result: List[Dict[str, Any]] = [] 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) days_held = None try: days_held = (_date.today() - _date.fromisoformat(e["entry_date"])).days 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)}) @router.delete("/reset") def reset_journal(): """Truncate all journal history (trades, macro, geo, cycles). Irreversible.""" reset_journal_history() return {"reset": True, "message": "Journal de bord réinitialisé"} @router.get("/summary") def journal_summary(): """Quick stats for the Journal de Bord header.""" macro = get_macro_regime_history(15) geo = get_geo_alert_history(30) trades = get_trade_entry_prices(30) # Detect regime transitions (consecutive different dominants) transitions = [] for i in range(1, len(macro)): if macro[i - 1]["dominant"] != macro[i]["dominant"]: transitions.append({ "from": macro[i]["dominant"], "to": macro[i - 1]["dominant"], "at": macro[i - 1]["timestamp"], }) return { "macro_snapshots": len(macro), "regime_transitions": transitions[:5], "current_dominant": macro[0]["dominant"] if macro else None, "geo_alerts": len(geo), "avg_geo_score": round(sum(g["geo_score"] for g in geo) / len(geo), 1) if geo else None, "max_geo_score": max((g["geo_score"] for g in geo), default=None), "trade_entries_logged": len(trades), }