mirror of
https://github.com/multipleof4/KalBot.git
synced 2026-03-17 05:51:02 +00:00
182 lines
5.3 KiB
JavaScript
182 lines
5.3 KiB
JavaScript
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);
|
|
}
|
|
}
|
|
}
|