feat: VaR/PnL schedulers + snapshots DB + page sur bouton
Backend: - Tables var_snapshots + pnl_snapshots dans SQLite (contexte macro + prix tickers) - var_service.py : save_var_snapshot, save_pnl_snapshot + fonctions get_* - var_scheduler.py : threads APScheduler pour VaR (défaut 6h) et PnL (défaut 1h) - router var.py : /run-now (POST compute+save), /latest, /snapshots, /pnl/run-now, /pnl/latest, /scheduler/status, /scheduler/config - main.py : démarrage des deux schedulers au startup Frontend: - VaRAnalysis.tsx : plus d'auto-fetch ; charge le dernier snapshot DB au mount ; bouton "Calculer" → POST /run-now ; erreur backend = message clair ; historique de snapshots sélectionnables - Config.tsx : section "Schedulers VaR & PnL" dans l'onglet cycle avec toggle enable/disable, intervalle, et boutons "Snapshot maintenant" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,20 +1,107 @@
|
||||
from fastapi import APIRouter, Query
|
||||
from services.var_service import compute_var
|
||||
from fastapi import APIRouter, Query, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from services.var_service import (
|
||||
compute_var, save_var_snapshot,
|
||||
get_var_snapshots, get_var_snapshot, get_latest_var_snapshot,
|
||||
get_pnl_snapshots, get_latest_pnl_snapshot, save_pnl_snapshot,
|
||||
)
|
||||
from services.var_scheduler import (
|
||||
get_scheduler_status, restart_var_scheduler, restart_pnl_scheduler,
|
||||
)
|
||||
from services.database import set_config
|
||||
|
||||
router = APIRouter(prefix="/api/var", tags=["var"])
|
||||
|
||||
|
||||
@router.get("/compute")
|
||||
def var_compute(
|
||||
@router.get("/latest")
|
||||
def var_latest():
|
||||
"""Get most recent saved VaR snapshot (no recompute)."""
|
||||
snap = get_latest_var_snapshot()
|
||||
return {"snapshot": snap}
|
||||
|
||||
|
||||
@router.get("/snapshots")
|
||||
def var_snapshots(limit: int = Query(default=20, ge=1, le=100)):
|
||||
"""List recent VaR snapshot summaries."""
|
||||
return {"snapshots": get_var_snapshots(limit)}
|
||||
|
||||
|
||||
@router.get("/snapshots/{snapshot_id}")
|
||||
def var_snapshot_detail(snapshot_id: int):
|
||||
snap = get_var_snapshot(snapshot_id)
|
||||
if not snap:
|
||||
raise HTTPException(404, "Snapshot not found")
|
||||
return snap
|
||||
|
||||
|
||||
@router.post("/run-now")
|
||||
def var_run_now(
|
||||
confidence: float = Query(default=0.95, ge=0.90, le=0.99),
|
||||
horizon_days: int = Query(default=1, ge=1, le=30),
|
||||
lookback_days: int = Query(default=252, ge=60, le=504),
|
||||
default_iv: float = Query(default=0.20, ge=0.05, le=0.80),
|
||||
):
|
||||
"""Compute portfolio VaR using Black-Scholes delta approach."""
|
||||
return compute_var(
|
||||
"""Compute VaR now and save to DB. Returns the full result."""
|
||||
result = compute_var(
|
||||
confidence=confidence,
|
||||
horizon_days=horizon_days,
|
||||
lookback_days=lookback_days,
|
||||
default_iv=default_iv,
|
||||
)
|
||||
if "error" in result:
|
||||
raise HTTPException(400, result["error"])
|
||||
snapshot_id = save_var_snapshot(
|
||||
result, confidence, horizon_days, lookback_days, default_iv
|
||||
)
|
||||
return {**result, "snapshot_id": snapshot_id}
|
||||
|
||||
|
||||
# ─── PnL endpoints ───────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/pnl/latest")
|
||||
def pnl_latest():
|
||||
return {"snapshot": get_latest_pnl_snapshot()}
|
||||
|
||||
|
||||
@router.get("/pnl/snapshots")
|
||||
def pnl_snapshots(limit: int = Query(default=48, ge=1, le=200)):
|
||||
return {"snapshots": get_pnl_snapshots(limit)}
|
||||
|
||||
|
||||
@router.post("/pnl/run-now")
|
||||
def pnl_run_now():
|
||||
try:
|
||||
row_id = save_pnl_snapshot()
|
||||
except Exception as e:
|
||||
raise HTTPException(500, str(e))
|
||||
return {"snapshot_id": row_id, "snapshot": get_latest_pnl_snapshot()}
|
||||
|
||||
|
||||
# ─── Scheduler status / config ───────────────────────────────────────────────
|
||||
|
||||
@router.get("/scheduler/status")
|
||||
def scheduler_status():
|
||||
return get_scheduler_status()
|
||||
|
||||
|
||||
class SchedulerConfig(BaseModel):
|
||||
var_enabled: bool | None = None
|
||||
var_hours: float | None = None
|
||||
pnl_enabled: bool | None = None
|
||||
pnl_hours: float | None = None
|
||||
|
||||
|
||||
@router.post("/scheduler/config")
|
||||
def scheduler_config(cfg: SchedulerConfig):
|
||||
if cfg.var_enabled is not None:
|
||||
set_config("var_scheduler_enabled", "true" if cfg.var_enabled else "false")
|
||||
if cfg.var_hours is not None:
|
||||
set_config("var_scheduler_hours", str(cfg.var_hours))
|
||||
if cfg.pnl_enabled is not None:
|
||||
set_config("pnl_scheduler_enabled", "true" if cfg.pnl_enabled else "false")
|
||||
if cfg.pnl_hours is not None:
|
||||
set_config("pnl_scheduler_hours", str(cfg.pnl_hours))
|
||||
# Restart scheduler threads to pick up new config
|
||||
restart_var_scheduler()
|
||||
restart_pnl_scheduler()
|
||||
return {"ok": True, "status": get_scheduler_status()}
|
||||
|
||||
Reference in New Issue
Block a user