feat: chatbot
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
"""
|
||||
Free-form, read-only chat with GPT-4o about the current cockpit state.
|
||||
No function-calling — this endpoint can never trigger an action.
|
||||
Free-form chat with GPT-4o about the current cockpit state.
|
||||
The only tool the model can call (propose_trade) just writes a pending row to
|
||||
ai_trade_proposals — it never touches the real portfolio. Confirm/reject below
|
||||
are the only way a proposal turns into (or is discarded from) a real position.
|
||||
"""
|
||||
from typing import List, Optional
|
||||
|
||||
@@ -58,3 +60,50 @@ def clear_session(body: ClearBody):
|
||||
clear_chat_session(body.session_id)
|
||||
clear_context_cache(body.session_id)
|
||||
return {"cleared": body.session_id}
|
||||
|
||||
|
||||
@router.get("/trade-proposals")
|
||||
def list_trade_proposals(status: str = "pending"):
|
||||
from services.database import get_ai_trade_proposals
|
||||
return {"proposals": get_ai_trade_proposals(status=status)}
|
||||
|
||||
|
||||
@router.post("/trade-proposals/{proposal_id}/confirm")
|
||||
def confirm_trade_proposal(proposal_id: str):
|
||||
"""Promotes a pending AI proposal into a real open position, reusing the
|
||||
same enrichment (live price, Black-Scholes leg pricing) as a manual add."""
|
||||
from services.database import get_ai_trade_proposal, resolve_ai_trade_proposal
|
||||
from routers.portfolio import add_pos, AddPositionRequest
|
||||
|
||||
proposal = get_ai_trade_proposal(proposal_id)
|
||||
if not proposal:
|
||||
raise HTTPException(404, "Proposition introuvable.")
|
||||
if proposal["status"] != "pending":
|
||||
raise HTTPException(400, f"Proposition deja {proposal['status']}.")
|
||||
|
||||
req = AddPositionRequest(
|
||||
title=proposal["title"],
|
||||
underlying=proposal["underlying"],
|
||||
strategy=proposal["strategy"],
|
||||
asset_class=proposal.get("asset_class") or "indices",
|
||||
expiry_days=proposal.get("expiry_days") or 90,
|
||||
legs=proposal.get("legs") or [],
|
||||
capital_invested=proposal["capital_invested"],
|
||||
geo_trigger=proposal.get("geo_trigger") or "",
|
||||
rationale=proposal.get("rationale") or "",
|
||||
)
|
||||
result = add_pos(req)
|
||||
resolve_ai_trade_proposal(proposal_id, "confirmed", portfolio_id=result["id"])
|
||||
return {"status": "confirmed", "portfolio_id": result["id"]}
|
||||
|
||||
|
||||
@router.post("/trade-proposals/{proposal_id}/reject")
|
||||
def reject_trade_proposal(proposal_id: str):
|
||||
from services.database import get_ai_trade_proposal, resolve_ai_trade_proposal
|
||||
proposal = get_ai_trade_proposal(proposal_id)
|
||||
if not proposal:
|
||||
raise HTTPException(404, "Proposition introuvable.")
|
||||
if proposal["status"] != "pending":
|
||||
raise HTTPException(400, f"Proposition deja {proposal['status']}.")
|
||||
resolve_ai_trade_proposal(proposal_id, "rejected")
|
||||
return {"status": "rejected"}
|
||||
|
||||
@@ -89,6 +89,84 @@ SIGNAL_CATALOG: List[Dict[str, Any]] = [
|
||||
"signal": {"type": "int", "label": "Signal", "default": 9, "min": 3, "max": 20},
|
||||
},
|
||||
},
|
||||
# ── Wavelets — décomposition en bandes de fréquence sur la watchlist ────
|
||||
{
|
||||
"id": "wavelet_engine",
|
||||
"label": "Ondelettes — moteur",
|
||||
"description": "Paramètres partagés du calcul (désactive tous les signaux ondelettes si décoché)",
|
||||
"desk_type": "technical",
|
||||
"params": {
|
||||
"num_levels": {"type": "int", "label": "Nb bandes", "default": 4, "min": 2, "max": 6},
|
||||
"wavelet": {"type": "select", "label": "Famille", "default": "gmw", "options": ["gmw", "morlet", "bump"]},
|
||||
"method": {"type": "select", "label": "Méthode", "default": "cwt", "options": ["cwt", "ssq"]},
|
||||
"lookback_days": {"type": "int", "label": "Lookback (j)", "default": 120, "min": 60, "max": 250},
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "wavelet_extremum",
|
||||
"label": "Ondelettes — extremum",
|
||||
"description": "Pic ou creux confirmé sur une bande",
|
||||
"desk_type": "technical",
|
||||
"params": {},
|
||||
},
|
||||
{
|
||||
"id": "wavelet_level_threshold",
|
||||
"label": "Ondelettes — seuil de niveau",
|
||||
"description": "La bande dépasse un seuil de z-score causal (sur/sous-achetée)",
|
||||
"desk_type": "technical",
|
||||
"params": {
|
||||
"threshold_k": {"type": "float", "label": "Seuil (écarts-type)", "default": 2.0, "min": 1.0, "max": 4.0},
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "wavelet_trend_flatten",
|
||||
"label": "Ondelettes — tendance puis tassement",
|
||||
"description": "Forte pente suivie d'un aplatissement — signal de fin de mouvement",
|
||||
"desk_type": "technical",
|
||||
"params": {
|
||||
"trend_days": {"type": "int", "label": "Jours tendance", "default": 10, "min": 3, "max": 30},
|
||||
"flatten_days": {"type": "int", "label": "Jours tassement", "default": 5, "min": 2, "max": 15},
|
||||
"trend_threshold_k": {"type": "float", "label": "Seuil tendance", "default": 1.0, "min": 0.3, "max": 3.0},
|
||||
"flatten_threshold_k": {"type": "float", "label": "Seuil tassement", "default": 0.3, "min": 0.1, "max": 1.5},
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "wavelet_acceleration",
|
||||
"label": "Ondelettes — déceleration/accélération",
|
||||
"description": "Accélération soutenue en sens inverse de la pente — signal de retournement",
|
||||
"desk_type": "technical",
|
||||
"params": {
|
||||
"accel_days": {"type": "int", "label": "Jours consécutifs", "default": 3, "min": 1, "max": 10},
|
||||
"accel_threshold_k":{"type": "float", "label": "Seuil (écarts-type)", "default": 1.5, "min": 0.5, "max": 4.0},
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "wavelet_band_cross",
|
||||
"label": "Ondelettes — croisement de bandes",
|
||||
"description": "Une bande croise une bande secondaire",
|
||||
"desk_type": "technical",
|
||||
"params": {
|
||||
"secondary_band": {"type": "int", "label": "Index bande secondaire", "default": 1, "min": 0, "max": 5},
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "wavelet_ridge_shift",
|
||||
"label": "Ondelettes — bascule de ridge",
|
||||
"description": "Le cycle dominant (ridge SSQ) dévie de sa moyenne — nécessite méthode = ssq",
|
||||
"desk_type": "technical",
|
||||
"params": {
|
||||
"threshold_k": {"type": "float", "label": "Seuil (écarts-type)", "default": 2.0, "min": 1.0, "max": 4.0},
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "wavelet_energy_threshold",
|
||||
"label": "Ondelettes — seuil d'énergie",
|
||||
"description": "L'énergie d'une bande dépasse un seuil — nécessite méthode = ssq",
|
||||
"desk_type": "technical",
|
||||
"params": {
|
||||
"threshold_k": {"type": "float", "label": "Seuil (écarts-type)", "default": 2.0, "min": 1.0, "max": 4.0},
|
||||
},
|
||||
},
|
||||
# ── Sentiment signals ───────────────────────────────────────────────────
|
||||
{
|
||||
"id": "vix_level",
|
||||
|
||||
Reference in New Issue
Block a user