mirror of
https://github.com/multipleof4/KalBot.git
synced 2026-03-16 21:41:02 +00:00
Fix: Normalize result case, settle orphans, add reset
This commit is contained in:
@@ -76,6 +76,10 @@ export class PaperEngine {
|
|||||||
list.push(pos);
|
list.push(pos);
|
||||||
acct.openPositions.set(pos.ticker, list);
|
acct.openPositions.set(pos.ticker, list);
|
||||||
}
|
}
|
||||||
|
const totalOpen = positions[0].length;
|
||||||
|
if (totalOpen > 0) {
|
||||||
|
console.log(`[Paper] Loaded ${totalOpen} open position(s) from DB`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[Paper] Init error (fresh start):', e.message);
|
console.error('[Paper] Init error (fresh start):', e.message);
|
||||||
@@ -95,7 +99,7 @@ export class PaperEngine {
|
|||||||
id: `pt_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
id: `pt_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
||||||
strategy: signal.strategy,
|
strategy: signal.strategy,
|
||||||
ticker: signal.ticker,
|
ticker: signal.ticker,
|
||||||
side: signal.side,
|
side: signal.side.toLowerCase(),
|
||||||
price: signal.price,
|
price: signal.price,
|
||||||
size: signal.size,
|
size: signal.size,
|
||||||
cost,
|
cost,
|
||||||
@@ -132,7 +136,15 @@ export class PaperEngine {
|
|||||||
return trade;
|
return trade;
|
||||||
}
|
}
|
||||||
|
|
||||||
async settle(ticker, result) {
|
async settle(ticker, rawResult) {
|
||||||
|
// Normalize result to lowercase — Kalshi may return "Yes"/"No"/"yes"/"no"
|
||||||
|
const result = String(rawResult || '').toLowerCase();
|
||||||
|
|
||||||
|
if (result !== 'yes' && result !== 'no') {
|
||||||
|
console.warn(`[Paper] Unknown settlement result "${rawResult}" for ${ticker}, skipping.`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const allSettled = [];
|
const allSettled = [];
|
||||||
|
|
||||||
for (const [strategyName, acct] of this.accounts) {
|
for (const [strategyName, acct] of this.accounts) {
|
||||||
@@ -142,8 +154,15 @@ export class PaperEngine {
|
|||||||
console.log(`[Paper:${strategyName}] Settling ${positions.length} position(s) for ${ticker}, result: ${result}`);
|
console.log(`[Paper:${strategyName}] Settling ${positions.length} position(s) for ${ticker}, result: ${result}`);
|
||||||
|
|
||||||
for (const pos of positions) {
|
for (const pos of positions) {
|
||||||
const won = pos.side === result;
|
// Normalize stored side too
|
||||||
const payout = won ? (100 / pos.price) * pos.cost : 0;
|
const side = String(pos.side || '').toLowerCase();
|
||||||
|
const won = side === result;
|
||||||
|
|
||||||
|
// Payout: if won, each contract pays 100¢. Cost is in dollars, price in cents.
|
||||||
|
// Contracts bought = cost / (price / 100). Payout = contracts * $1.
|
||||||
|
// Simplified: payout = cost * (100 / price)
|
||||||
|
const price = pos.price > 0 ? pos.price : 50;
|
||||||
|
const payout = won ? (100 / price) * pos.cost : 0;
|
||||||
const pnl = payout - pos.cost;
|
const pnl = payout - pos.cost;
|
||||||
|
|
||||||
pos.settled = true;
|
pos.settled = true;
|
||||||
@@ -167,7 +186,7 @@ export class PaperEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const emoji = won ? '✅' : '❌';
|
const emoji = won ? '✅' : '❌';
|
||||||
const msg = `${emoji} [${strategyName}] ${pos.side.toUpperCase()} ${won ? 'WON' : 'LOST'} | PnL: $${pnl.toFixed(2)} | Bal: $${acct.balance.toFixed(2)}`;
|
const msg = `${emoji} [${strategyName}] ${side.toUpperCase()} ${won ? 'WON' : 'LOST'} | PnL: $${pnl.toFixed(2)} | Bal: $${acct.balance.toFixed(2)}`;
|
||||||
console.log(`[Paper] ${msg}`);
|
console.log(`[Paper] ${msg}`);
|
||||||
await notify(msg, won ? `${strategyName} Win!` : `${strategyName} Loss`);
|
await notify(msg, won ? `${strategyName} Win!` : `${strategyName} Loss`);
|
||||||
|
|
||||||
@@ -181,6 +200,101 @@ export class PaperEngine {
|
|||||||
return allSettled.length > 0 ? allSettled : null;
|
return allSettled.length > 0 ? allSettled : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to settle any orphaned open positions by checking market status.
|
||||||
|
* Called on startup to clean up positions from previous runs.
|
||||||
|
*/
|
||||||
|
async settleOrphans(getMarketFn) {
|
||||||
|
const orphanTickers = new Set();
|
||||||
|
for (const [, acct] of this.accounts) {
|
||||||
|
for (const ticker of acct.openPositions.keys()) {
|
||||||
|
orphanTickers.add(ticker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!orphanTickers.size) return;
|
||||||
|
console.log(`[Paper] Checking ${orphanTickers.size} ticker(s) for orphaned positions...`);
|
||||||
|
|
||||||
|
for (const ticker of orphanTickers) {
|
||||||
|
try {
|
||||||
|
const market = await getMarketFn(ticker);
|
||||||
|
const status = String(market?.status || '').toLowerCase();
|
||||||
|
const result = market?.result;
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
console.log(`[Paper] Orphan ticker ${ticker} has result "${result}" — settling now`);
|
||||||
|
await this.settle(ticker, result);
|
||||||
|
} else if (['closed', 'settled', 'expired', 'finalized'].includes(status)) {
|
||||||
|
// Market closed but no result yet — mark as expired loss
|
||||||
|
console.log(`[Paper] Orphan ticker ${ticker} status="${status}" with no result — force-settling as expired`);
|
||||||
|
await this._forceExpirePositions(ticker);
|
||||||
|
} else {
|
||||||
|
console.log(`[Paper] Orphan ticker ${ticker} still active (status="${status}") — keeping open`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[Paper] Orphan check failed for ${ticker}:`, e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _forceExpirePositions(ticker) {
|
||||||
|
for (const [strategyName, acct] of this.accounts) {
|
||||||
|
const positions = acct.openPositions.get(ticker);
|
||||||
|
if (!positions || !positions.length) continue;
|
||||||
|
|
||||||
|
for (const pos of positions) {
|
||||||
|
pos.settled = true;
|
||||||
|
pos.result = 'expired';
|
||||||
|
pos.pnl = parseFloat((-pos.cost).toFixed(2));
|
||||||
|
pos.settleTime = Date.now();
|
||||||
|
|
||||||
|
acct.totalPnL -= pos.cost;
|
||||||
|
acct.losses++;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.query(
|
||||||
|
`UPDATE paper_positions SET settled = true, result = $result, pnl = $pnl, settleTime = $settleTime WHERE id = $id`,
|
||||||
|
{ id: pos.id, result: 'expired', pnl: pos.pnl, settleTime: pos.settleTime }
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[Paper] Force-expire DB error:', e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[Paper:${strategyName}] Force-expired position ${pos.id} for ${ticker} (lost $${pos.cost})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
acct.openPositions.delete(ticker);
|
||||||
|
await this._saveState(acct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset all strategy stats and clear trade history.
|
||||||
|
*/
|
||||||
|
async resetAll() {
|
||||||
|
for (const [name, acct] of this.accounts) {
|
||||||
|
acct.balance = acct.initialBalance;
|
||||||
|
acct.totalPnL = 0;
|
||||||
|
acct.wins = 0;
|
||||||
|
acct.losses = 0;
|
||||||
|
acct.openPositions.clear();
|
||||||
|
console.log(`[Paper:${name}] Reset to $${acct.initialBalance}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.query('DELETE paper_positions');
|
||||||
|
await db.query('DELETE paper_strategy_state');
|
||||||
|
console.log('[Paper] Cleared all DB records');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[Paper] Reset DB error:', e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save fresh state for each account
|
||||||
|
for (const [, acct] of this.accounts) {
|
||||||
|
await this._saveState(acct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get combined stats (backward compat) — aggregates all strategies.
|
* Get combined stats (backward compat) — aggregates all strategies.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user