Spaces:
No application file
No application file
| """Virtual paper-trading engine. Uses live Deriv prices but never executes.""" | |
| from __future__ import annotations | |
| from typing import Any, Optional | |
| from datetime import datetime, timezone | |
| import uuid | |
| from . import supabase_client as sb | |
| from .config import get_settings | |
| from .schemas import PaperTradeRequest | |
| async def get_or_create_portfolio(user_id: Optional[str], mode: str = "demo") -> dict: | |
| rows = sb.select("portfolio", eq={"mode": mode, "user_id": user_id} if user_id | |
| else {"mode": mode}, limit=1) | |
| if rows: | |
| return rows[0] | |
| s = get_settings() | |
| created = sb.insert("portfolio", { | |
| "user_id": user_id, "mode": mode, | |
| "balance": s.DEMO_STARTING_BALANCE, | |
| "equity": s.DEMO_STARTING_BALANCE, | |
| }) | |
| return created or { | |
| "balance": s.DEMO_STARTING_BALANCE, | |
| "equity": s.DEMO_STARTING_BALANCE, | |
| "open_positions": 0, "realized_pnl": 0, "unrealized_pnl": 0, | |
| } | |
| async def open_paper_trade(req: PaperTradeRequest, current_price: float, | |
| user_id: Optional[str] = None) -> dict[str, Any]: | |
| portfolio = await get_or_create_portfolio(user_id, "demo") | |
| entry = req.entry or current_price | |
| row = sb.insert("trade_history", { | |
| "id": str(uuid.uuid4()), | |
| "user_id": user_id, | |
| "prediction_id": req.prediction_id, | |
| "mode": "demo", | |
| "symbol": req.symbol, | |
| "side": req.side, | |
| "entry_price": entry, | |
| "size": req.size, | |
| "stop_loss": req.sl, | |
| "take_profit": req.tp, | |
| "status": "open", | |
| "reason_opened": "AI signal accepted", | |
| "opened_at": datetime.now(timezone.utc).isoformat(), | |
| }) | |
| if portfolio.get("id"): | |
| sb.update("portfolio", portfolio["id"], { | |
| "open_positions": (portfolio.get("open_positions") or 0) + 1, | |
| }) | |
| return row or {} | |
| async def close_paper_trade(trade_id: str, exit_price: float, | |
| reason: str = "manual close") -> dict[str, Any]: | |
| rows = sb.select("trade_history", eq={"id": trade_id}, limit=1) | |
| if not rows: | |
| return {"ok": False, "message": "Trade not found"} | |
| t = rows[0] | |
| sign = 1 if t["side"] == "BUY" else -1 | |
| pnl = sign * (exit_price - float(t["entry_price"])) * float(t["size"]) | |
| sb.update("trade_history", trade_id, { | |
| "exit_price": exit_price, | |
| "pnl": pnl, | |
| "status": "closed", | |
| "reason_closed": reason, | |
| "closed_at": datetime.now(timezone.utc).isoformat(), | |
| }) | |
| pf = await get_or_create_portfolio(t.get("user_id"), "demo") | |
| if pf.get("id"): | |
| sb.update("portfolio", pf["id"], { | |
| "realized_pnl": float(pf.get("realized_pnl") or 0) + pnl, | |
| "balance": float(pf.get("balance") or 0) + pnl, | |
| "equity": float(pf.get("equity") or 0) + pnl, | |
| "open_positions": max(0, (pf.get("open_positions") or 1) - 1), | |
| }) | |
| return {"ok": True, "pnl": pnl} | |