Stack: FastAPI + React/TypeScript + SQLite + GPT-4o Features: Radar géopolitique, Marchés, Régime Macro, Journal de Bord MTM, Rapport IA, Super Contexte (base de raisonnement évolutive), Boucle feedback IA. Deploy: Docker + docker-compose + nginx pour openfin.open-squared.tech Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
113 lines
3.9 KiB
Python
113 lines
3.9 KiB
Python
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
|
|
|
|
|
|
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
|
|
|
|
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",
|
|
})
|
|
|
|
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),
|
|
}
|