mirror of
https://github.com/multipleof4/KalBot.git
synced 2026-03-17 05:51:02 +00:00
Fix: Add open ticker polling for delayed Kalshi results
This commit is contained in:
@@ -140,7 +140,7 @@ export class PaperEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async settle(ticker, rawResult) {
|
async settle(ticker, rawResult) {
|
||||||
// Normalize result to lowercase — Kalshi may return "Yes"/"No"/"yes"/"no"
|
// Normalize result to lowercase
|
||||||
const result = String(rawResult || '').toLowerCase();
|
const result = String(rawResult || '').toLowerCase();
|
||||||
|
|
||||||
if (result !== 'yes' && result !== 'no') {
|
if (result !== 'yes' && result !== 'no') {
|
||||||
@@ -157,13 +157,9 @@ 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) {
|
||||||
// Normalize stored side too
|
|
||||||
const side = String(pos.side || '').toLowerCase();
|
const side = String(pos.side || '').toLowerCase();
|
||||||
const won = side === result;
|
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 price = pos.price > 0 ? pos.price : 50;
|
||||||
const payout = won ? (100 / price) * pos.cost : 0;
|
const payout = won ? (100 / price) * pos.cost : 0;
|
||||||
const pnl = payout - pos.cost;
|
const pnl = payout - pos.cost;
|
||||||
@@ -180,19 +176,16 @@ export class PaperEngine {
|
|||||||
else acct.losses++;
|
else acct.losses++;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use upsert-style: try UPDATE first, if no rows matched, INSERT the settled record
|
|
||||||
const updated = await db.query(
|
const updated = await db.query(
|
||||||
`UPDATE paper_positions SET settled = true, result = $result, pnl = $pnl, settleTime = $settleTime WHERE id = $id`,
|
`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 }
|
{ id: pos.id, result, pnl: pos.pnl, settleTime: pos.settleTime }
|
||||||
);
|
);
|
||||||
// Check if UPDATE matched anything; if not, re-insert the settled position
|
|
||||||
const rows = updated[0] || [];
|
const rows = updated[0] || [];
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
await db.create('paper_positions', { ...pos });
|
await db.create('paper_positions', { ...pos });
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[Paper] Settle DB error:', e.message);
|
console.error('[Paper] Settle DB error:', e.message);
|
||||||
// Fallback: try inserting the settled record directly
|
|
||||||
try {
|
try {
|
||||||
await db.create('paper_positions', { ...pos });
|
await db.create('paper_positions', { ...pos });
|
||||||
} catch (e2) {
|
} catch (e2) {
|
||||||
@@ -215,20 +208,24 @@ export class PaperEngine {
|
|||||||
return allSettled.length > 0 ? allSettled : null;
|
return allSettled.length > 0 ? allSettled : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
getOpenTickers() {
|
||||||
* Attempt to settle any orphaned open positions by checking market status.
|
const tickers = new Set();
|
||||||
* 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 [, acct] of this.accounts) {
|
||||||
for (const ticker of acct.openPositions.keys()) {
|
for (const ticker of acct.openPositions.keys()) {
|
||||||
orphanTickers.add(ticker);
|
tickers.add(ticker);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return Array.from(tickers);
|
||||||
|
}
|
||||||
|
|
||||||
if (!orphanTickers.size) return;
|
/**
|
||||||
console.log(`[Paper] Checking ${orphanTickers.size} ticker(s) for orphaned positions...`);
|
* Check unresolved tickers periodically for delayed results
|
||||||
|
*/
|
||||||
|
async checkOrphans(getMarketFn) {
|
||||||
|
const orphanTickers = this.getOpenTickers();
|
||||||
|
if (!orphanTickers.length) return { settled: [], expired: [] };
|
||||||
|
|
||||||
|
const results = { settled: [], expired: [] };
|
||||||
|
|
||||||
for (const ticker of orphanTickers) {
|
for (const ticker of orphanTickers) {
|
||||||
try {
|
try {
|
||||||
@@ -237,22 +234,24 @@ export class PaperEngine {
|
|||||||
const result = market?.result;
|
const result = market?.result;
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
console.log(`[Paper] Orphan ticker ${ticker} has result "${result}" — settling now`);
|
console.log(`[Paper] Delayed result found for ${ticker}: "${result}"`);
|
||||||
await this.settle(ticker, result);
|
const settledPos = await this.settle(ticker, result);
|
||||||
} else if (['closed', 'settled', 'expired', 'finalized'].includes(status)) {
|
if (settledPos) results.settled.push(...settledPos);
|
||||||
// Market closed but no result yet — mark as expired loss
|
} else if (['expired', 'cancelled'].includes(status)) {
|
||||||
console.log(`[Paper] Orphan ticker ${ticker} status="${status}" with no result — force-settling as expired`);
|
// Explicitly cancelled or expired with no valid outcome
|
||||||
await this._forceExpirePositions(ticker);
|
console.log(`[Paper] Ticker ${ticker} marked as ${status} — force-settling as expired`);
|
||||||
} else {
|
const expiredPos = await this._forceExpirePositions(ticker);
|
||||||
console.log(`[Paper] Orphan ticker ${ticker} still active (status="${status}") — keeping open`);
|
if (expiredPos) results.expired.push(...expiredPos);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`[Paper] Orphan check failed for ${ticker}:`, e.message);
|
console.error(`[Paper] Orphan check failed for ${ticker}:`, e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _forceExpirePositions(ticker) {
|
async _forceExpirePositions(ticker) {
|
||||||
|
const expired = [];
|
||||||
for (const [strategyName, acct] of this.accounts) {
|
for (const [strategyName, acct] of this.accounts) {
|
||||||
const positions = acct.openPositions.get(ticker);
|
const positions = acct.openPositions.get(ticker);
|
||||||
if (!positions || !positions.length) continue;
|
if (!positions || !positions.length) continue;
|
||||||
@@ -280,25 +279,21 @@ export class PaperEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[Paper:${strategyName}] Force-expired position ${pos.id} for ${ticker} (lost $${pos.cost})`);
|
console.log(`[Paper:${strategyName}] Force-expired position ${pos.id} for ${ticker} (lost $${pos.cost})`);
|
||||||
|
expired.push(pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
acct.openPositions.delete(ticker);
|
acct.openPositions.delete(ticker);
|
||||||
await this._saveState(acct);
|
await this._saveState(acct);
|
||||||
}
|
}
|
||||||
|
return expired;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset all strategy stats and clear trade history.
|
|
||||||
* Open positions are settled as cancelled (refunded) before clearing.
|
|
||||||
*/
|
|
||||||
async resetAll() {
|
async resetAll() {
|
||||||
this._resetting = true;
|
this._resetting = true;
|
||||||
|
|
||||||
// First, cancel all open positions by refunding their cost
|
|
||||||
for (const [name, acct] of this.accounts) {
|
for (const [name, acct] of this.accounts) {
|
||||||
for (const [ticker, positions] of acct.openPositions) {
|
for (const [ticker, positions] of acct.openPositions) {
|
||||||
for (const pos of positions) {
|
for (const pos of positions) {
|
||||||
// Refund the cost back to balance before we reset it
|
|
||||||
acct.balance += pos.cost;
|
acct.balance += pos.cost;
|
||||||
pos.settled = true;
|
pos.settled = true;
|
||||||
pos.result = 'cancelled';
|
pos.result = 'cancelled';
|
||||||
@@ -310,21 +305,13 @@ export class PaperEngine {
|
|||||||
`UPDATE paper_positions SET settled = true, result = $result, pnl = $pnl, settleTime = $settleTime WHERE id = $id`,
|
`UPDATE paper_positions SET settled = true, result = $result, pnl = $pnl, settleTime = $settleTime WHERE id = $id`,
|
||||||
{ id: pos.id, result: 'cancelled', pnl: 0, settleTime: pos.settleTime }
|
{ id: pos.id, result: 'cancelled', pnl: 0, settleTime: pos.settleTime }
|
||||||
);
|
);
|
||||||
const rows = updated[0] || [];
|
} catch (e) {}
|
||||||
if (rows.length === 0) {
|
|
||||||
// Row was already gone, that's fine for a reset
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// ignore during reset
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[Paper:${name}] Cancelled open position ${pos.id} for ${ticker} (refunded $${pos.cost})`);
|
console.log(`[Paper:${name}] Cancelled open position ${pos.id} for ${ticker} (refunded $${pos.cost})`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
acct.openPositions.clear();
|
acct.openPositions.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now reset all stats
|
|
||||||
for (const [name, acct] of this.accounts) {
|
for (const [name, acct] of this.accounts) {
|
||||||
acct.balance = acct.initialBalance;
|
acct.balance = acct.initialBalance;
|
||||||
acct.totalPnL = 0;
|
acct.totalPnL = 0;
|
||||||
@@ -342,7 +329,6 @@ export class PaperEngine {
|
|||||||
console.error('[Paper] Reset DB error:', e.message);
|
console.error('[Paper] Reset DB error:', e.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save fresh state for each account
|
|
||||||
for (const [, acct] of this.accounts) {
|
for (const [, acct] of this.accounts) {
|
||||||
await this._saveState(acct);
|
await this._saveState(acct);
|
||||||
}
|
}
|
||||||
@@ -350,9 +336,6 @@ export class PaperEngine {
|
|||||||
this._resetting = false;
|
this._resetting = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get combined stats (backward compat) — aggregates all strategies.
|
|
||||||
*/
|
|
||||||
getStats() {
|
getStats() {
|
||||||
const allOpen = [];
|
const allOpen = [];
|
||||||
let totalBalance = 0;
|
let totalBalance = 0;
|
||||||
@@ -382,9 +365,6 @@ export class PaperEngine {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get per-strategy stats for the paper dashboard.
|
|
||||||
*/
|
|
||||||
getPerStrategyStats() {
|
getPerStrategyStats() {
|
||||||
const result = {};
|
const result = {};
|
||||||
for (const [name, acct] of this.accounts) {
|
for (const [name, acct] of this.accounts) {
|
||||||
|
|||||||
Reference in New Issue
Block a user