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>
89 lines
2.8 KiB
Python
89 lines
2.8 KiB
Python
from fastapi import APIRouter, HTTPException
|
|
from pydantic import BaseModel
|
|
from typing import Optional
|
|
from services.database import get_risk_profiles, upsert_risk_profile, delete_risk_profile, _compute_trade_score
|
|
|
|
router = APIRouter(prefix="/api/profiles", tags=["profiles"])
|
|
|
|
|
|
class RiskProfileRequest(BaseModel):
|
|
id: Optional[int] = None
|
|
name: str
|
|
min_score: int
|
|
min_gain_pct: float
|
|
color: Optional[str] = "#3b82f6"
|
|
enabled: Optional[bool] = True
|
|
sort_order: Optional[int] = 0
|
|
|
|
|
|
@router.get("")
|
|
def list_profiles():
|
|
"""List all risk profiles ordered by sort_order."""
|
|
profiles = get_risk_profiles()
|
|
# Annotate each profile with the EV breakeven info
|
|
result = []
|
|
for p in profiles:
|
|
# At the exact frontier: score = min_score, gain = min_gain_pct
|
|
_, ev_net, trade_score = _compute_trade_score(p["min_score"], p["min_gain_pct"])
|
|
result.append({
|
|
**p,
|
|
"ev_net_at_frontier": round(ev_net, 3),
|
|
"trade_score_at_frontier": trade_score,
|
|
})
|
|
return {"profiles": result}
|
|
|
|
|
|
@router.post("")
|
|
def create_profile(req: RiskProfileRequest):
|
|
"""Create a new risk profile."""
|
|
if not (0 <= req.min_score <= 100):
|
|
raise HTTPException(400, "min_score must be between 0 and 100")
|
|
if req.min_gain_pct < 0:
|
|
raise HTTPException(400, "min_gain_pct must be >= 0")
|
|
pid = upsert_risk_profile(req.model_dump())
|
|
profiles = get_risk_profiles()
|
|
return {"id": pid, "profiles": profiles}
|
|
|
|
|
|
@router.put("/{profile_id}")
|
|
def update_profile(profile_id: int, req: RiskProfileRequest):
|
|
"""Update an existing risk profile."""
|
|
if not (0 <= req.min_score <= 100):
|
|
raise HTTPException(400, "min_score must be between 0 and 100")
|
|
data = req.model_dump()
|
|
data["id"] = profile_id
|
|
upsert_risk_profile(data)
|
|
return {"profiles": get_risk_profiles()}
|
|
|
|
|
|
@router.delete("/{profile_id}")
|
|
def remove_profile(profile_id: int):
|
|
"""Delete a risk profile."""
|
|
profiles = get_risk_profiles()
|
|
if len([p for p in profiles if p["enabled"]]) <= 1:
|
|
# Allow deletion but warn
|
|
pass
|
|
delete_risk_profile(profile_id)
|
|
return {"profiles": get_risk_profiles()}
|
|
|
|
|
|
@router.get("/preview")
|
|
def preview_score(score: int = 50, gain_pct: float = 100.0):
|
|
"""
|
|
Preview the trade metrics for a given (score, gain_pct) pair.
|
|
Useful for the Config UI slider simulation.
|
|
"""
|
|
ev_gross, ev_net, trade_score = _compute_trade_score(score, gain_pct)
|
|
profiles = get_risk_profiles(enabled_only=True)
|
|
from services.database import _matches_profile
|
|
matched = _matches_profile(score, gain_pct, profiles)
|
|
return {
|
|
"score": score,
|
|
"gain_pct": gain_pct,
|
|
"ev_gross": ev_gross,
|
|
"ev_net": ev_net,
|
|
"trade_score": trade_score,
|
|
"matched_profile": matched,
|
|
"accepted": matched is not None,
|
|
}
|