luguog commited on
Commit
b833c19
·
verified ·
1 Parent(s): f477e21

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +515 -57
main.py CHANGED
@@ -1,86 +1,544 @@
1
- from fastapi import FastAPI, Request
 
 
 
 
 
 
 
 
 
2
  from fastapi.responses import HTMLResponse, FileResponse
3
- import os, time, hmac, hashlib, requests, json
 
4
 
5
- app = FastAPI()
 
 
6
 
7
  GATE_API_KEY = os.getenv("GATE_API_KEY")
8
  GATE_API_SECRET = os.getenv("GATE_API_SECRET")
9
- GATE_API_BASE = "https://api.gate.io/api/v4"
10
-
11
- LOG_FILE = "trading_log.json"
12
- BAL_FILE = "balance_snapshots.json"
13
-
14
- @app.get("/")
15
- def home():
16
- return HTMLResponse("""
17
- <html><body>
18
- <h2>🚀 gate4: Live Gate.io GPT API</h2>
19
- <p>Endpoints:</p>
20
- <ul>
21
- <li>/balance</li>
22
- <li>/performance</li>
23
- <li>/log_trade</li>
24
- <li>/openapi.yaml</li>
25
- </ul>
26
- </body></html>
27
- """)
28
 
29
- @app.get("/openapi.yaml")
30
- def get_openapi():
31
- return FileResponse("openapi.yaml", media_type="text/yaml")
32
 
33
- @app.get("/balance")
34
- def get_balance():
35
- path = "/futures/usdt/accounts"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  method = "GET"
37
  timestamp = str(int(time.time()))
38
  body = ""
39
- query = ""
40
- sign = sign_request(method, path, query, body, timestamp)
41
 
 
42
  headers = {
43
  "KEY": GATE_API_KEY,
44
  "Timestamp": timestamp,
45
- "SIGN": sign
46
  }
 
 
 
 
 
 
 
 
 
 
 
 
47
 
 
48
  url = f"{GATE_API_BASE}{path}"
49
- res = requests.get(url, headers=headers)
50
- res.raise_for_status()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
- accounts = res.json()
53
- total = sum(float(acc.get("available", 0)) for acc in accounts)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
- log_balance(total)
56
- return {"balance": round(total, 2)}
57
 
58
  @app.get("/performance")
59
  def get_performance():
60
- if not os.path.exists(LOG_FILE):
61
- return {"summary": "No trades logged yet."}
 
 
 
 
 
62
 
63
- with open(LOG_FILE, "r") as f:
64
- trades = [json.loads(line) for line in f if line.strip()]
 
 
 
 
 
 
65
 
66
- total_pnl = sum(t.get("pnl_estimate", 0) for t in trades)
67
- summary = f"📊 Total PnL: ${total_pnl:.2f} from {len(trades)} trades."
68
  for t in trades[-5:]:
69
- summary += f"\n• {t['action']} {t['contract']} → ${t['pnl_estimate']} ({t['reason']})"
70
- return {"summary": summary}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
- @app.post("/log_trade")
 
 
 
 
 
 
 
 
 
 
 
73
  async def log_trade(request: Request):
74
- trade = await request.json()
75
- with open(LOG_FILE, "a") as f:
76
- f.write(json.dumps(trade) + "\n")
77
- return {"status": "logged", "trade": trade}
 
 
 
 
 
 
 
78
 
79
- def log_balance(balance):
80
- entry = {"timestamp": int(time.time()), "balance": balance}
81
- with open(BAL_FILE, "a") as f:
82
- f.write(json.dumps(entry) + "\n")
83
 
84
- def sign_request(method, path, query_string, body, timestamp):
85
- message = f"{method}\n{path}\n{query_string}\n{body}\n{timestamp}"
86
- return hmac.new(GATE_API_SECRET.encode(), message.encode(), hashlib.sha512).hexdigest()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ import json
4
+ import hmac
5
+ import hashlib
6
+ import threading
7
+ from typing import Any, Dict, List, Optional, Literal
8
+
9
+ import requests
10
+ from fastapi import FastAPI, HTTPException, Request
11
  from fastapi.responses import HTMLResponse, FileResponse
12
+ from fastapi.middleware.cors import CORSMiddleware
13
+ from pydantic import BaseModel, Field, ValidationError
14
 
15
+ # -----------------------------------------------------------------------------
16
+ # Config
17
+ # -----------------------------------------------------------------------------
18
 
19
  GATE_API_KEY = os.getenv("GATE_API_KEY")
20
  GATE_API_SECRET = os.getenv("GATE_API_SECRET")
21
+ GATE_API_BASE = os.getenv("GATE_API_BASE", "https://api.gate.io/api/v4")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
+ LOG_FILE = os.getenv("TRADE_LOG_FILE", "trading_log.jsonl")
24
+ BAL_FILE = os.getenv("BALANCE_SNAP_FILE", "balance_snapshots.jsonl")
 
25
 
26
+ LLM_ENDPOINT = os.getenv("LLM_ENDPOINT") # HF Inference endpoint or similar
27
+ LLM_API_KEY = os.getenv("LLM_API_KEY") # optional bearer token
28
+
29
+ if not GATE_API_KEY or not GATE_API_SECRET:
30
+ raise RuntimeError("GATE_API_KEY and GATE_API_SECRET must be set")
31
+
32
+ # -----------------------------------------------------------------------------
33
+ # FastAPI app
34
+ # -----------------------------------------------------------------------------
35
+
36
+ app = FastAPI(title="gate4-alpha-api", version="0.2.0")
37
+
38
+ app.add_middleware(
39
+ CORSMiddleware,
40
+ allow_origins=["*"], # restrict in real deployment
41
+ allow_credentials=True,
42
+ allow_methods=["*"],
43
+ allow_headers=["*"],
44
+ )
45
+
46
+ _log_lock = threading.Lock()
47
+ _bal_lock = threading.Lock()
48
+
49
+ # -----------------------------------------------------------------------------
50
+ # Models
51
+ # -----------------------------------------------------------------------------
52
+
53
+ class TradeLog(BaseModel):
54
+ timestamp: int = Field(default_factory=lambda: int(time.time()))
55
+ action: Literal["long", "short", "flat", "close"]
56
+ contract: str
57
+ size: float
58
+ entry_price: float
59
+ exit_price: Optional[float] = None
60
+ pnl_realized: float = 0.0
61
+ pnl_estimate: float = 0.0
62
+ reason: Optional[str] = None
63
+ meta: Dict[str, Any] = Field(default_factory=dict)
64
+
65
+
66
+ class BalanceSnapshot(BaseModel):
67
+ timestamp: int
68
+ balance: float
69
+
70
+
71
+ class KPIResponse(BaseModel):
72
+ total_pnl: float
73
+ realized_pnl: float
74
+ trade_count: int
75
+ win_rate: float
76
+ max_drawdown_pct: float
77
+ avg_pnl_per_trade: float
78
+ equity_curve: List[BalanceSnapshot]
79
+
80
+
81
+ class AlphaRequest(BaseModel):
82
+ contract: str
83
+ context: Optional[str] = None
84
+ # Optional overrides for KPI input if caller wants to inject their data
85
+ kpis_override: Optional[Dict[str, float]] = None
86
+
87
+
88
+ class AlphaDecision(BaseModel):
89
+ action: Literal["long", "short", "flat"]
90
+ confidence: float = Field(ge=0.0, le=1.0)
91
+ size_factor: float = Field(ge=0.0, le=1.0)
92
+ spread_bps: float
93
+ kpis: Dict[str, float]
94
+ comment: str
95
+ raw_model_output: Optional[Any] = None
96
+
97
+
98
+ # -----------------------------------------------------------------------------
99
+ # Gate.io helpers
100
+ # -----------------------------------------------------------------------------
101
+
102
+ def sign_request(method: str, path: str, query_string: str, body: str, timestamp: str) -> str:
103
+ message = f"{method}\n{path}\n{query_string}\n{body}\n{timestamp}"
104
+ return hmac.new(
105
+ GATE_API_SECRET.encode(),
106
+ message.encode(),
107
+ hashlib.sha512,
108
+ ).hexdigest()
109
+
110
+
111
+ def gate_private_get(path: str, query: str = "") -> Any:
112
  method = "GET"
113
  timestamp = str(int(time.time()))
114
  body = ""
 
 
115
 
116
+ sign = sign_request(method, path, query, body, timestamp)
117
  headers = {
118
  "KEY": GATE_API_KEY,
119
  "Timestamp": timestamp,
120
+ "SIGN": sign,
121
  }
122
+ url = f"{GATE_API_BASE}{path}"
123
+ if query:
124
+ url = f"{url}?{query}"
125
+
126
+ try:
127
+ res = requests.get(url, headers=headers, timeout=10)
128
+ res.raise_for_status()
129
+ except requests.RequestException as e:
130
+ raise HTTPException(status_code=502, detail=f"Gate.io request failed: {e}")
131
+
132
+ return res.json()
133
+
134
 
135
+ def gate_public_get(path: str, query: str = "") -> Any:
136
  url = f"{GATE_API_BASE}{path}"
137
+ if query:
138
+ url = f"{url}?{query}"
139
+ try:
140
+ res = requests.get(url, timeout=10)
141
+ res.raise_for_status()
142
+ except requests.RequestException as e:
143
+ raise HTTPException(status_code=502, detail=f"Gate.io public request failed: {e}")
144
+ return res.json()
145
+
146
+
147
+ def get_futures_account_total_balance() -> float:
148
+ path = "/futures/usdt/accounts"
149
+ accounts = gate_private_get(path)
150
+ # Gate futures account endpoint returns a list of account objects
151
+ total = 0.0
152
+ for acc in accounts:
153
+ try:
154
+ total += float(acc.get("available", 0.0))
155
+ except (TypeError, ValueError):
156
+ continue
157
+ return total
158
+
159
+
160
+ def get_contract_spread_bps(contract: str) -> float:
161
+ """
162
+ Uses futures ticker to derive bid/ask spread in basis points.
163
+ """
164
+ path = "/futures/usdt/tickers"
165
+ query = f"contract={contract}"
166
+ tickers = gate_public_get(path, query=query)
167
+
168
+ if not tickers:
169
+ raise HTTPException(status_code=404, detail=f"No ticker data for {contract}")
170
+
171
+ t = tickers[0]
172
+ try:
173
+ bid = float(t.get("bid", 0.0))
174
+ ask = float(t.get("ask", 0.0))
175
+ except (TypeError, ValueError):
176
+ raise HTTPException(status_code=502, detail="Malformed ticker from Gate.io")
177
+
178
+ if bid <= 0 or ask <= 0 or ask <= bid:
179
+ return 0.0
180
+
181
+ mid = 0.5 * (bid + ask)
182
+ spread_bps = (ask - bid) / mid * 1e4
183
+ return spread_bps
184
+
185
+
186
+ # -----------------------------------------------------------------------------
187
+ # File-backed state
188
+ # -----------------------------------------------------------------------------
189
+
190
+ def _safe_read_lines(path: str) -> List[str]:
191
+ if not os.path.exists(path):
192
+ return []
193
+ with open(path, "r") as f:
194
+ return [line for line in f if line.strip()]
195
+
196
+
197
+ def load_trades() -> List[TradeLog]:
198
+ lines = _safe_read_lines(LOG_FILE)
199
+ out: List[TradeLog] = []
200
+ for line in lines:
201
+ try:
202
+ raw = json.loads(line)
203
+ out.append(TradeLog(**raw))
204
+ except (json.JSONDecodeError, ValidationError):
205
+ # skip malformed line
206
+ continue
207
+ return out
208
+
209
+
210
+ def load_balances() -> List[BalanceSnapshot]:
211
+ lines = _safe_read_lines(BAL_FILE)
212
+ out: List[BalanceSnapshot] = []
213
+ for line in lines:
214
+ try:
215
+ raw = json.loads(line)
216
+ out.append(BalanceSnapshot(**raw))
217
+ except (json.JSONDecodeError, ValidationError):
218
+ continue
219
+ return out
220
+
221
+
222
+ def append_trade(trade: TradeLog) -> None:
223
+ with _log_lock, open(LOG_FILE, "a") as f:
224
+ f.write(trade.model_json() + "\n")
225
+
226
 
227
+ def append_balance_snapshot(balance: float) -> BalanceSnapshot:
228
+ snap = BalanceSnapshot(timestamp=int(time.time()), balance=balance)
229
+ with _bal_lock, open(BAL_FILE, "a") as f:
230
+ f.write(snap.model_json() + "\n")
231
+ return snap
232
+
233
+
234
+ # -----------------------------------------------------------------------------
235
+ # KPI logic
236
+ # -----------------------------------------------------------------------------
237
+
238
+ def compute_kpis(trades: List[TradeLog], balances: List[BalanceSnapshot]) -> KPIResponse:
239
+ realized_pnls = [t.pnl_realized for t in trades]
240
+ est_pnls = [t.pnl_estimate for t in trades]
241
+
242
+ realized_pnl = float(sum(realized_pnls))
243
+ total_pnl = float(realized_pnl + sum(est_pnls))
244
+
245
+ trade_count = len(trades)
246
+ wins = sum(1 for t in trades if t.pnl_realized > 0)
247
+ win_rate = float(wins / trade_count) if trade_count > 0 else 0.0
248
+ avg_pnl_per_trade = float(realized_pnl / trade_count) if trade_count > 0 else 0.0
249
+
250
+ equity_curve = balances
251
+ max_drawdown_pct = 0.0
252
+ if equity_curve:
253
+ peak = equity_curve[0].balance
254
+ for point in equity_curve:
255
+ if point.balance > peak:
256
+ peak = point.balance
257
+ if peak > 0:
258
+ dd = (point.balance - peak) / peak * 100.0
259
+ if dd < max_drawdown_pct:
260
+ max_drawdown_pct = dd
261
+
262
+ return KPIResponse(
263
+ total_pnl=total_pnl,
264
+ realized_pnl=realized_pnl,
265
+ trade_count=trade_count,
266
+ win_rate=win_rate,
267
+ max_drawdown_pct=max_drawdown_pct,
268
+ avg_pnl_per_trade=avg_pnl_per_trade,
269
+ equity_curve=equity_curve,
270
+ )
271
+
272
+
273
+ def kpis_to_feature_dict(kpis: KPIResponse) -> Dict[str, float]:
274
+ return {
275
+ "total_pnl": kpis.total_pnl,
276
+ "realized_pnl": kpis.realized_pnl,
277
+ "trade_count": float(kpis.trade_count),
278
+ "win_rate": kpis.win_rate,
279
+ "max_drawdown_pct": kpis.max_drawdown_pct,
280
+ "avg_pnl_per_trade": kpis.avg_pnl_per_trade,
281
+ }
282
+
283
+
284
+ # -----------------------------------------------------------------------------
285
+ # LLM integration
286
+ # -----------------------------------------------------------------------------
287
+
288
+ def _build_alpha_prompt(req: AlphaRequest, spread_bps: float, kpis: Dict[str, float]) -> str:
289
+ kpi_json = json.dumps(kpis, sort_keys=True)
290
+ ctx = req.context or ""
291
+ return (
292
+ "You are a deterministic trading policy engine.\n"
293
+ "Given KPIs and spread metrics, choose ONE action: long, short, or flat.\n"
294
+ "Respond ONLY with a compact JSON object:\n"
295
+ '{ "action": "...", "confidence": 0.0-1.0, "size_factor": 0.0-1.0, "comment": "..." }\n\n'
296
+ f"Contract: {req.contract}\n"
297
+ f"Spread_bps: {spread_bps:.4f}\n"
298
+ f"KPIs: {kpi_json}\n"
299
+ f"Context: {ctx}\n"
300
+ "Constraints:\n"
301
+ "- If max_drawdown_pct < -25 or win_rate < 0.4 ⇒ prefer flat.\n"
302
+ "- If trade_count < 10 ⇒ confidence <= 0.4.\n"
303
+ "- size_factor must be <= 0.3 if spread_bps > 10.\n"
304
+ )
305
+
306
+
307
+ def call_llm_for_alpha(prompt: str) -> Dict[str, Any]:
308
+ if not LLM_ENDPOINT:
309
+ raise HTTPException(status_code=500, detail="LLM_ENDPOINT not configured")
310
+
311
+ payload = {
312
+ "inputs": prompt,
313
+ # HF / generic text generation params; adjust per endpoint
314
+ "parameters": {
315
+ "max_new_tokens": 256,
316
+ "temperature": 0.1,
317
+ "top_p": 0.9,
318
+ "return_full_text": False,
319
+ },
320
+ }
321
+ headers = {"Content-Type": "application/json"}
322
+ if LLM_API_KEY:
323
+ headers["Authorization"] = f"Bearer {LLM_API_KEY}"
324
+
325
+ try:
326
+ res = requests.post(LLM_ENDPOINT, headers=headers, json=payload, timeout=20)
327
+ res.raise_for_status()
328
+ except requests.RequestException as e:
329
+ raise HTTPException(status_code=502, detail=f"LLM request failed: {e}")
330
+
331
+ try:
332
+ data = res.json()
333
+ except json.JSONDecodeError:
334
+ raise HTTPException(status_code=502, detail="LLM returned non-JSON payload")
335
+
336
+ # HF endpoints often return [{"generated_text": "..."}]
337
+ if isinstance(data, list) and data and isinstance(data[0], dict) and "generated_text" in data[0]:
338
+ text = data[0]["generated_text"]
339
+ elif isinstance(data, dict) and "generated_text" in data:
340
+ text = data["generated_text"]
341
+ else:
342
+ # assume raw text
343
+ text = str(data)
344
+
345
+ # extract JSON from text
346
+ text = text.strip()
347
+ # best-effort: find first '{' ... last '}'
348
+ start = text.find("{")
349
+ end = text.rfind("}")
350
+ if start == -1 or end == -1 or end <= start:
351
+ raise HTTPException(status_code=502, detail="LLM output missing JSON object")
352
+
353
+ snippet = text[start : end + 1]
354
+ try:
355
+ parsed = json.loads(snippet)
356
+ except json.JSONDecodeError as e:
357
+ raise HTTPException(status_code=502, detail=f"LLM JSON parse failed: {e}")
358
+
359
+ return parsed
360
+
361
+
362
+ def build_alpha_decision(
363
+ req: AlphaRequest,
364
+ spread_bps: float,
365
+ kpi_features: Dict[str, float],
366
+ raw_model_out: Dict[str, Any],
367
+ ) -> AlphaDecision:
368
+ action = raw_model_out.get("action", "").lower().strip()
369
+ if action not in ("long", "short", "flat"):
370
+ action = "flat"
371
+
372
+ confidence = float(raw_model_out.get("confidence", 0.0))
373
+ size_factor = float(raw_model_out.get("size_factor", 0.0))
374
+ comment = str(raw_model_out.get("comment", "")).strip()[:240]
375
+
376
+ # Hard safety clamps
377
+ if confidence < 0.0:
378
+ confidence = 0.0
379
+ if confidence > 1.0:
380
+ confidence = 1.0
381
+ if size_factor < 0.0:
382
+ size_factor = 0.0
383
+ if size_factor > 1.0:
384
+ size_factor = 1.0
385
+
386
+ # Deterministic risk gating based on KPIs
387
+ if kpi_features.get("max_drawdown_pct", 0.0) < -30.0:
388
+ # lock system to flat on deep drawdown
389
+ action = "flat"
390
+ confidence = min(confidence, 0.3)
391
+ size_factor = 0.0
392
+ comment = (comment + " [forced_flat_due_to_drawdown]").strip()
393
+
394
+ if kpi_features.get("trade_count", 0.0) < 10:
395
+ confidence = min(confidence, 0.4)
396
+
397
+ if spread_bps > 10.0 and size_factor > 0.3:
398
+ size_factor = 0.3
399
+
400
+ return AlphaDecision(
401
+ action=action,
402
+ confidence=confidence,
403
+ size_factor=size_factor,
404
+ spread_bps=spread_bps,
405
+ kpis=kpi_features,
406
+ comment=comment,
407
+ raw_model_output=raw_model_out,
408
+ )
409
+
410
+
411
+ # -----------------------------------------------------------------------------
412
+ # Routes
413
+ # -----------------------------------------------------------------------------
414
+
415
+ @app.get("/", response_class=HTMLResponse)
416
+ def home() -> str:
417
+ return """
418
+ <html>
419
+ <body>
420
+ <h2>gate4-alpha-api</h2>
421
+ <p>Endpoints:</p>
422
+ <ul>
423
+ <li>GET /balance</li>
424
+ <li>GET /performance</li>
425
+ <li>GET /kpis</li>
426
+ <li>POST /log_trade</li>
427
+ <li>POST /alpha/entry</li>
428
+ <li>GET /openapi.yaml</li>
429
+ </ul>
430
+ </body>
431
+ </html>
432
+ """
433
+
434
+
435
+ @app.get("/openapi.yaml")
436
+ def get_openapi():
437
+ """
438
+ Serve static OpenAPI file if you generate one externally.
439
+ """
440
+ if not os.path.exists("openapi.yaml"):
441
+ raise HTTPException(status_code=404, detail="openapi.yaml not found")
442
+ return FileResponse("openapi.yaml", media_type="text/yaml")
443
+
444
+
445
+ @app.get("/balance")
446
+ def get_balance():
447
+ """
448
+ Snapshot Gate.io USDT futures account and persist balance curve.
449
+ """
450
+ total = get_futures_account_total_balance()
451
+ snap = append_balance_snapshot(total)
452
+ return {
453
+ "timestamp": snap.timestamp,
454
+ "balance": round(snap.balance, 6),
455
+ }
456
 
 
 
457
 
458
  @app.get("/performance")
459
  def get_performance():
460
+ """
461
+ Human-readable PnL summary + last few trades.
462
+ """
463
+ trades = load_trades()
464
+ balances = load_balances()
465
+ if not trades and not balances:
466
+ return {"summary": "No trades or balances logged yet."}
467
 
468
+ kpis = compute_kpis(trades, balances)
469
+ summary = (
470
+ f"Total PnL: {kpis.total_pnl:.2f}, "
471
+ f"Realized: {kpis.realized_pnl:.2f}, "
472
+ f"Trades: {kpis.trade_count}, "
473
+ f"Win rate: {kpis.win_rate:.2%}, "
474
+ f"Max DD: {kpis.max_drawdown_pct:.2f}%"
475
+ )
476
 
477
+ tail = []
 
478
  for t in trades[-5:]:
479
+ tail.append(
480
+ {
481
+ "ts": t.timestamp,
482
+ "action": t.action,
483
+ "contract": t.contract,
484
+ "pnl_realized": t.pnl_realized,
485
+ "pnl_estimate": t.pnl_estimate,
486
+ "reason": t.reason,
487
+ }
488
+ )
489
+
490
+ return {
491
+ "summary": summary,
492
+ "last_trades": tail,
493
+ "kpis": kpis.model_dump(),
494
+ }
495
 
496
+
497
+ @app.get("/kpis", response_model=KPIResponse)
498
+ def get_kpis():
499
+ """
500
+ Machine-consumable KPI surface for external systems.
501
+ """
502
+ trades = load_trades()
503
+ balances = load_balances()
504
+ return compute_kpis(trades, balances)
505
+
506
+
507
+ @app.post("/log_trade", response_model=TradeLog)
508
  async def log_trade(request: Request):
509
+ """
510
+ Append a trade event into the trading log.
511
+ Request body must match TradeLog schema (fields can be omitted if defaulted).
512
+ """
513
+ payload = await request.json()
514
+ try:
515
+ trade = TradeLog(**payload)
516
+ except ValidationError as e:
517
+ raise HTTPException(status_code=422, detail=e.errors())
518
+ append_trade(trade)
519
+ return trade
520
 
 
 
 
 
521
 
522
+ @app.post("/alpha/entry", response_model=AlphaDecision)
523
+ async def alpha_entry(req: AlphaRequest):
524
+ """
525
+ LLM-based entry decision:
526
+ - Derives KPIs from logs unless overridden.
527
+ - Computes current spread in bps for the requested contract.
528
+ - Calls LLM policy layer and enforces deterministic risk clamps.
529
+ """
530
+ trades = load_trades()
531
+ balances = load_balances()
532
+ base_kpis = compute_kpis(trades, balances)
533
+ base_features = kpis_to_feature_dict(base_kpis)
534
+
535
+ if req.kpis_override:
536
+ features = {**base_features, **req.kpis_override}
537
+ else:
538
+ features = base_features
539
+
540
+ spread_bps = get_contract_spread_bps(req.contract)
541
+ prompt = _build_alpha_prompt(req, spread_bps, features)
542
+ raw_model_out = call_llm_for_alpha(prompt)
543
+ decision = build_alpha_decision(req, spread_bps, features, raw_model_out)
544
+ return decision