import { db } from '../db.js'; import { notify } from '../notify.js'; /** * Paper Trading Engine. * Executes virtual trades, tracks PnL, stores in SurrealDB. */ export class PaperEngine { constructor(initialBalance = 1000) { this.balance = initialBalance; this.openPositions = new Map(); // ticker -> [positions] this.tradeHistory = []; this.totalPnL = 0; this.wins = 0; this.losses = 0; } async init() { // Load state from SurrealDB try { const state = await db.query('SELECT * FROM paper_state ORDER BY timestamp DESC LIMIT 1'); const saved = state[0]?.[0]; if (saved) { this.balance = saved.balance; this.totalPnL = saved.totalPnL; this.wins = saved.wins; this.losses = saved.losses; console.log(`[Paper] Restored state: $${this.balance.toFixed(2)} balance, ${this.wins}W/${this.losses}L`); } // Load open positions const positions = await db.query('SELECT * FROM paper_positions WHERE settled = false'); if (positions[0]) { for (const pos of positions[0]) { const list = this.openPositions.get(pos.ticker) || []; list.push(pos); this.openPositions.set(pos.ticker, list); } console.log(`[Paper] Restored ${this.openPositions.size} open position(s)`); } } catch (e) { console.error('[Paper] Init error (fresh start):', e.message); } } /** * Execute a paper trade from a strategy signal. */ async executeTrade(signal, marketState) { const cost = signal.size; // Each contract costs signal.price cents, but we simplify: $1 per contract unit if (this.balance < cost) { console.log(`[Paper] Insufficient balance ($${this.balance.toFixed(2)}) for $${cost} trade`); return null; } const trade = { id: `pt_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, strategy: signal.strategy, ticker: signal.ticker, side: signal.side, price: signal.price, // Entry price in cents size: signal.size, cost, reason: signal.reason, entryTime: Date.now(), settled: false, result: null, pnl: null, marketState: { yesPct: marketState.yesPct, noPct: marketState.noPct, yesOdds: marketState.yesOdds, noOdds: marketState.noOdds } }; this.balance -= cost; const list = this.openPositions.get(trade.ticker) || []; list.push(trade); this.openPositions.set(trade.ticker, list); // Store in SurrealDB try { await db.create('paper_positions', trade); await this._saveState(); } catch (e) { console.error('[Paper] DB write error:', e.message); } const msg = `๐Ÿ“ PAPER ${trade.side.toUpperCase()} @ ${trade.price}ยข ($${cost}) | ${trade.strategy} | ${trade.reason}`; console.log(`[Paper] ${msg}`); await notify(msg, 'Paper Trade'); return trade; } /** * Settle all positions for a ticker when the market resolves. */ async settle(ticker, result) { const positions = this.openPositions.get(ticker); if (!positions || positions.length === 0) return; console.log(`[Paper] Settling ${positions.length} position(s) for ${ticker}, result: ${result}`); for (const pos of positions) { const won = pos.side === result; // Payout: if won, pay out at $1 per contract (100ยข), minus cost // If lost, lose the cost const payout = won ? (100 / pos.price) * pos.cost : 0; const pnl = payout - pos.cost; pos.settled = true; pos.result = result; pos.pnl = parseFloat(pnl.toFixed(2)); pos.settleTime = Date.now(); this.balance += payout; this.totalPnL += pnl; if (won) this.wins++; else this.losses++; // Update in SurrealDB try { await db.query(`UPDATE paper_positions SET settled = true, result = $result, pnl = $pnl, settleTime = $settleTime WHERE id = $id`, { id: pos.id, result, pnl: pos.pnl, settleTime: pos.settleTime }); } catch (e) { console.error('[Paper] Settle DB error:', e.message); } const emoji = won ? 'โœ…' : 'โŒ'; const msg = `${emoji} ${pos.strategy} ${pos.side.toUpperCase()} ${won ? 'WON' : 'LOST'} | PnL: $${pnl.toFixed(2)} | Balance: $${this.balance.toFixed(2)}`; console.log(`[Paper] ${msg}`); await notify(msg, won ? 'Paper Win!' : 'Paper Loss'); } this.openPositions.delete(ticker); await this._saveState(); return positions; } getStats() { const openPositionsList = []; for (const [ticker, positions] of this.openPositions) { openPositionsList.push(...positions); } return { balance: parseFloat(this.balance.toFixed(2)), totalPnL: parseFloat(this.totalPnL.toFixed(2)), wins: this.wins, losses: this.losses, winRate: this.wins + this.losses > 0 ? parseFloat(((this.wins / (this.wins + this.losses)) * 100).toFixed(1)) : 0, openPositions: openPositionsList, totalTrades: this.wins + this.losses }; } async _saveState() { try { await db.create('paper_state', { balance: this.balance, totalPnL: this.totalPnL, wins: this.wins, losses: this.losses, timestamp: Date.now() }); } catch (e) { console.error('[Paper] State save error:', e.message); } } }