File size: 9,030 Bytes
13fb76c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
"""

AI Trading Backend — FastAPI entry point.



Endpoints:

  GET  /health

  GET  /models

  GET  /portfolio?mode=demo

  GET  /history?mode=demo&limit=50

  GET  /performance?mode=demo

  GET  /confidence?symbol=R_10

  POST /predict

  POST /reason

  POST /paper-trade

  POST /trade           (live, requires DERIV_API_TOKEN)

  POST /feedback

  POST /retrain-request

"""
from __future__ import annotations
import logging
from typing import Optional
from fastapi import FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware

from . import supabase_client as sb
from .config import get_settings
from .deriv_client import DerivClient, DerivError
from .paper_trader import (open_paper_trade, close_paper_trade,
                           get_or_create_portfolio)
from .prediction_engine import ensemble, heuristic_forecast
from .qwen_reasoner import reason as qwen_reason
from .risk import validate_trade
from .schemas import (PredictRequest, ReasonRequest, PaperTradeRequest,
                      TradeRequest, TradeResponse, FeedbackRequest,
                      RetrainRequest, StrategySignal)
from .strategy_ob_fvg import generate_signal

log = logging.getLogger("uvicorn.error")
settings = get_settings()

app = FastAPI(title="AI Trading Backend", version="0.1.0")
app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.CORS_ORIGINS,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


# -------------------------- system ------------------------------------------

@app.get("/health")
async def health():
    return {
        "ok": True,
        "qwen_configured": bool(settings.HF_TOKEN),
        "supabase_configured": bool(sb.sb()),
        "deriv_live_enabled": bool(settings.DERIV_API_TOKEN),
        "model": settings.QWEN_MODEL,
    }


@app.get("/models")
async def models():
    return {
        "reasoner": settings.QWEN_MODEL,
        "forecasters": ["heuristic-momentum-v1", "lstm-stub", "xgboost-stub",
                        "ensemble-v1"],
        "strategies": ["ob_fvg"],
    }


# -------------------------- portfolio / history ------------------------------

@app.get("/portfolio")
async def portfolio(mode: str = "demo"):
    return await get_or_create_portfolio(None, mode)


@app.get("/history")
async def history(mode: str = "demo", limit: int = 50):
    return sb.select("trade_history", eq={"mode": mode},
                     order="opened_at", desc=True, limit=limit)


@app.get("/performance")
async def performance(mode: str = "demo"):
    trades = sb.select("trade_history", eq={"mode": mode, "status": "closed"},
                       order="closed_at", desc=True, limit=500)
    if not trades:
        return {"total_trades": 0, "win_rate": 0, "total_pnl": 0,
                "profit_factor": 0}
    wins = [t for t in trades if (t.get("pnl") or 0) > 0]
    losses = [t for t in trades if (t.get("pnl") or 0) < 0]
    gross_win = sum(t["pnl"] for t in wins) or 0.0
    gross_loss = abs(sum(t["pnl"] for t in losses)) or 1e-9
    return {
        "total_trades": len(trades),
        "winning_trades": len(wins),
        "losing_trades": len(losses),
        "win_rate": len(wins) / len(trades),
        "total_pnl": sum(t["pnl"] or 0 for t in trades),
        "profit_factor": gross_win / gross_loss,
    }


@app.get("/confidence")
async def confidence(symbol: str = "R_10"):
    rows = sb.select("predictions", eq={"symbol": symbol},
                     order="created_at", desc=True, limit=20)
    if not rows:
        return {"symbol": symbol, "avg_confidence": 0, "samples": 0}
    avg = sum(float(r.get("confidence") or 0) for r in rows) / len(rows)
    return {"symbol": symbol, "avg_confidence": avg, "samples": len(rows)}


# -------------------------- core AI loop ------------------------------------

@app.post("/predict", response_model=StrategySignal)
async def predict(req: PredictRequest):
    """Run OB+FVG strategy on latest Deriv candles and emit a signal."""
    granularity = _granularity_seconds(req.timeframe)
    client = DerivClient()
    try:
        candles = await client.candles(req.symbol, granularity=granularity,
                                       count=req.lookback)
    except DerivError as e:
        raise HTTPException(502, f"Deriv error: {e}")

    signal = generate_signal(req.symbol, req.timeframe, candles)
    forecast = ensemble(candles)

    # persist
    pred = sb.insert("predictions", {
        "symbol": req.symbol,
        "timeframe": req.timeframe,
        "decision": signal.decision,
        "confidence": round(signal.confidence, 3),
        "risk_score": round(1 - signal.confidence, 3),
        "success_probability": round(signal.confidence, 3),
        "reasoning": signal.rationale,
        "trade_plan": {"entry": signal.entry, "sl": signal.sl, "tp": signal.tp},
        "indicators": signal.indicators,
        "market_state": {"forecast": forecast},
        "suggested_entry": signal.entry,
        "suggested_sl": signal.sl,
        "suggested_tp": signal.tp,
        "model_version": "ob_fvg-v1",
    })
    sb.insert("live_signals", {
        "symbol": req.symbol,
        "decision": signal.decision,
        "confidence": round(signal.confidence, 3),
        "price": signal.price,
        "ob_zone": signal.ob.model_dump() if signal.ob else None,
        "fvg_zone": signal.fvg.model_dump() if signal.fvg else None,
        "reasoning": signal.rationale,
    })
    return signal


@app.post("/reason")
async def reason_endpoint(req: ReasonRequest):
    return await qwen_reason(req.symbol, req.timeframe,
                             req.indicators, req.prediction, req.market_state)


# -------------------------- trading ------------------------------------------

@app.post("/paper-trade", response_model=TradeResponse)
async def paper_trade(req: PaperTradeRequest):
    client = DerivClient()
    tick = await client.tick(req.symbol)
    price = float(tick.get("quote") or 0)
    if not price:
        raise HTTPException(502, "Could not fetch current price")

    pf = await get_or_create_portfolio(None, "demo")
    check = validate_trade(
        balance=float(pf.get("balance") or 0),
        open_positions=int(pf.get("open_positions") or 0),
        today_pnl=float(pf.get("realized_pnl") or 0),
        trade_size=req.size,
        confidence=0.7,  # passed when called from /predict; user override OK
        max_daily_loss=settings.MAX_DAILY_LOSS_DEFAULT,
        max_open_trades=settings.MAX_OPEN_TRADES_DEFAULT,
        risk_percent=2.0,
    )
    if not check.ok:
        return TradeResponse(ok=False, message=check.reason or "Rejected")

    row = await open_paper_trade(req, current_price=price)
    return TradeResponse(ok=True, trade_id=row.get("id"),
                         message="Paper trade opened")


@app.post("/trade", response_model=TradeResponse)
async def live_trade(req: TradeRequest):
    if not settings.DERIV_API_TOKEN:
        raise HTTPException(403,
            "Live trading disabled: set DERIV_API_TOKEN in Space secrets.")
    client = DerivClient(token=settings.DERIV_API_TOKEN)
    contract_type = "CALL" if req.side == "BUY" else "PUT"
    try:
        buy = await client.buy_contract(
            symbol=req.symbol, contract_type=contract_type,
            amount=req.size, duration=5, duration_unit="m",
        )
    except DerivError as e:
        raise HTTPException(502, f"Deriv: {e}")
    row = sb.insert("trade_history", {
        "mode": "live",
        "symbol": req.symbol,
        "side": req.side,
        "entry_price": float(buy.get("buy_price") or 0),
        "size": req.size,
        "stop_loss": req.sl,
        "take_profit": req.tp,
        "status": "open",
        "deriv_contract_id": str(buy.get("contract_id") or ""),
        "prediction_id": req.prediction_id,
    })
    return TradeResponse(ok=True, trade_id=(row or {}).get("id"),
                         contract_id=str(buy.get("contract_id") or ""),
                         message="Live contract bought")


# -------------------------- feedback / retrain -------------------------------

@app.post("/feedback")
async def feedback(req: FeedbackRequest):
    sb.insert("feedback", req.model_dump())
    return {"ok": True}


@app.post("/retrain-request")
async def retrain(req: RetrainRequest):
    sb.insert("logs", {
        "level": "info", "source": "retrain",
        "message": f"Retrain requested for {req.model_name}",
        "meta": req.model_dump(),
    })
    return {"ok": True, "queued": True}


# -------------------------- helpers ------------------------------------------

def _granularity_seconds(tf: str) -> int:
    table = {"1m": 60, "5m": 300, "15m": 900, "30m": 1800,
             "1h": 3600, "4h": 14400, "1d": 86400}
    return table.get(tf, 60)