mirror of
https://github.com/multipleof4/KalBot.git
synced 2026-03-16 21:41:02 +00:00
Fix: settle delayed live results via orphan scan
This commit is contained in:
@@ -42,14 +42,16 @@ export class LiveEngine {
|
|||||||
if (row.enabledStrategies) {
|
if (row.enabledStrategies) {
|
||||||
for (const s of row.enabledStrategies) this.enabledStrategies.add(s);
|
for (const s of row.enabledStrategies) this.enabledStrategies.add(s);
|
||||||
}
|
}
|
||||||
console.log(`[Live] Restored: PnL=$${(this.totalPnL/100).toFixed(2)}, ${this.wins}W/${this.losses}L`);
|
console.log(
|
||||||
|
`[Live] Restored: PnL=$${(this.totalPnL / 100).toFixed(2)}, ${this.wins}W/${this.losses}L`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const orders = await db.query(
|
const orders = await db.query(
|
||||||
'SELECT * FROM live_orders WHERE status = "pending" OR status = "resting"'
|
'SELECT * FROM live_orders WHERE status = "pending" OR status = "resting" OR status = "filled"'
|
||||||
);
|
);
|
||||||
for (const o of (orders[0] ||[])) {
|
for (const o of orders[0] || []) {
|
||||||
this.openOrders.set(o.orderId, o);
|
if (!o.settled) this.openOrders.set(o.orderId, o);
|
||||||
}
|
}
|
||||||
if (this.openOrders.size) {
|
if (this.openOrders.size) {
|
||||||
console.log(`[Live] Loaded ${this.openOrders.size} open order(s) from DB`);
|
console.log(`[Live] Loaded ${this.openOrders.size} open order(s) from DB`);
|
||||||
@@ -161,7 +163,7 @@ export class LiveEngine {
|
|||||||
*/
|
*/
|
||||||
async _verifyOrderFills(orderId, maxAttempts = 3) {
|
async _verifyOrderFills(orderId, maxAttempts = 3) {
|
||||||
for (let i = 0; i < maxAttempts; i++) {
|
for (let i = 0; i < maxAttempts; i++) {
|
||||||
if (i > 0) await new Promise(r => setTimeout(r, 500 * i));
|
if (i > 0) await new Promise((r) => setTimeout(r, 500 * i));
|
||||||
try {
|
try {
|
||||||
const data = await kalshiFetch('GET', `/trade-api/v2/portfolio/orders/${orderId}`);
|
const data = await kalshiFetch('GET', `/trade-api/v2/portfolio/orders/${orderId}`);
|
||||||
const order = data?.order;
|
const order = data?.order;
|
||||||
@@ -193,11 +195,15 @@ export class LiveEngine {
|
|||||||
this._resetDailyLossIfNeeded();
|
this._resetDailyLossIfNeeded();
|
||||||
|
|
||||||
if (this._dailyLoss >= this._maxDailyLossCents) {
|
if (this._dailyLoss >= this._maxDailyLossCents) {
|
||||||
console.log(`[Live] Daily loss limit ($${(this._maxDailyLossCents/100).toFixed(2)}) reached — pausing`);
|
console.log(
|
||||||
|
`[Live] Daily loss limit ($${(this._maxDailyLossCents / 100).toFixed(2)}) reached — pausing`
|
||||||
|
);
|
||||||
this.pause();
|
this.pause();
|
||||||
await notify(
|
await notify(
|
||||||
`⚠️ Daily loss limit reached ($${(this._dailyLoss / 100).toFixed(2)}). Auto-paused.`,
|
`⚠️ Daily loss limit reached ($${(this._dailyLoss / 100).toFixed(2)}). Auto-paused.`,
|
||||||
'Kalbot Safety', 'urgent', 'warning,octagonal_sign'
|
'Kalbot Safety',
|
||||||
|
'urgent',
|
||||||
|
'warning,octagonal_sign'
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -213,14 +219,18 @@ export class LiveEngine {
|
|||||||
// SAFETY: Require orderbook data before placing real money orders
|
// SAFETY: Require orderbook data before placing real money orders
|
||||||
const bestAsk = this._getBestAskFromOrderbook(signal.side, marketState.orderbook);
|
const bestAsk = this._getBestAskFromOrderbook(signal.side, marketState.orderbook);
|
||||||
if (bestAsk == null) {
|
if (bestAsk == null) {
|
||||||
console.log(`[Live] No orderbook data for ${signal.side} side — refusing to trade blind (${signal.strategy})`);
|
console.log(
|
||||||
|
`[Live] No orderbook data for ${signal.side} side — refusing to trade blind (${signal.strategy})`
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxAcceptable = signal.maxPrice || (signal.price + 3);
|
const maxAcceptable = signal.maxPrice || signal.price + 3;
|
||||||
|
|
||||||
if (bestAsk > maxAcceptable) {
|
if (bestAsk > maxAcceptable) {
|
||||||
console.log(`[Live] Best ask ${bestAsk}¢ > max ${maxAcceptable}¢ for ${signal.strategy} — skipping`);
|
console.log(
|
||||||
|
`[Live] Best ask ${bestAsk}¢ > max ${maxAcceptable}¢ for ${signal.strategy} — skipping`
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,7 +254,7 @@ export class LiveEngine {
|
|||||||
count: contracts,
|
count: contracts,
|
||||||
type: 'limit',
|
type: 'limit',
|
||||||
client_order_id: clientOrderId,
|
client_order_id: clientOrderId,
|
||||||
time_in_force: 'immediate_or_cancel',
|
time_in_force: 'immediate_or_cancel'
|
||||||
};
|
};
|
||||||
|
|
||||||
if (side === 'yes') {
|
if (side === 'yes') {
|
||||||
@@ -254,7 +264,9 @@ export class LiveEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`[Live] Placing IOC order: ${side.toUpperCase()} ${contracts}x @ ${priceCents}¢ ($${sizeDollars})[ask: ${bestAsk}¢, max: ${maxAcceptable}¢] | ${signal.reason}`);
|
console.log(
|
||||||
|
`[Live] Placing IOC order: ${side.toUpperCase()} ${contracts}x @ ${priceCents}¢ ($${sizeDollars})[ask: ${bestAsk}¢, max: ${maxAcceptable}¢] | ${signal.reason}`
|
||||||
|
);
|
||||||
|
|
||||||
const result = await kalshiFetch('POST', '/trade-api/v2/portfolio/orders', orderBody);
|
const result = await kalshiFetch('POST', '/trade-api/v2/portfolio/orders', orderBody);
|
||||||
const order = result?.order;
|
const order = result?.order;
|
||||||
@@ -277,7 +289,9 @@ export class LiveEngine {
|
|||||||
fillCount = verified.fillCount;
|
fillCount = verified.fillCount;
|
||||||
fillCost = verified.fillCost;
|
fillCost = verified.fillCost;
|
||||||
status = verified.status;
|
status = verified.status;
|
||||||
console.log(`[Live] Verified: ${fillCount} fills, $${(fillCost/100).toFixed(2)} cost, status: ${status}`);
|
console.log(
|
||||||
|
`[Live] Verified: ${fillCount} fills, $${(fillCost / 100).toFixed(2)} cost, status: ${status}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fillCount === 0) {
|
if (fillCount === 0) {
|
||||||
@@ -320,7 +334,9 @@ export class LiveEngine {
|
|||||||
console.error('[Live] DB write error:', e.message);
|
console.error('[Live] DB write error:', e.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
const msg = `💰 LIVE[${signal.strategy}] ${side.toUpperCase()} ${fillCount}x @ ${priceCents}¢ ($${(fillCost/100).toFixed(2)}) [ask:${bestAsk}¢] | ${signal.reason}`;
|
const msg = `💰 LIVE[${signal.strategy}] ${side.toUpperCase()} ${fillCount}x @ ${priceCents}¢ ($${(
|
||||||
|
fillCost / 100
|
||||||
|
).toFixed(2)}) [ask:${bestAsk}¢] | ${signal.reason}`;
|
||||||
console.log(`[Live] ${msg}`);
|
console.log(`[Live] ${msg}`);
|
||||||
await notify(msg, `Live: ${signal.strategy}`, 'high', 'money_with_wings');
|
await notify(msg, `Live: ${signal.strategy}`, 'high', 'money_with_wings');
|
||||||
|
|
||||||
@@ -330,7 +346,9 @@ export class LiveEngine {
|
|||||||
console.error(`[Live] Order failed: ${e.message}`);
|
console.error(`[Live] Order failed: ${e.message}`);
|
||||||
await notify(
|
await notify(
|
||||||
`❌ LIVE ORDER FAILED [${signal.strategy}]: ${e.message}`,
|
`❌ LIVE ORDER FAILED [${signal.strategy}]: ${e.message}`,
|
||||||
'Kalbot Error', 'urgent', 'x,warning'
|
'Kalbot Error',
|
||||||
|
'urgent',
|
||||||
|
'x,warning'
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -373,9 +391,16 @@ export class LiveEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const emoji = won ? '✅' : '❌';
|
const emoji = won ? '✅' : '❌';
|
||||||
const msg = `${emoji} LIVE [${order.strategy}] ${order.side.toUpperCase()} ${won ? 'WON' : 'LOST'} | PnL: $${(pnl/100).toFixed(2)}`;
|
const msg = `${emoji} LIVE [${order.strategy}] ${order.side.toUpperCase()} ${
|
||||||
|
won ? 'WON' : 'LOST'
|
||||||
|
} | PnL: $${(pnl / 100).toFixed(2)}`;
|
||||||
console.log(`[Live] ${msg}`);
|
console.log(`[Live] ${msg}`);
|
||||||
await notify(msg, won ? 'Live Win!' : 'Live Loss', 'high', won ? 'chart_with_upwards_trend' : 'chart_with_downwards_trend');
|
await notify(
|
||||||
|
msg,
|
||||||
|
won ? 'Live Win!' : 'Live Loss',
|
||||||
|
'high',
|
||||||
|
won ? 'chart_with_upwards_trend' : 'chart_with_downwards_trend'
|
||||||
|
);
|
||||||
|
|
||||||
settled.push(order);
|
settled.push(order);
|
||||||
this.openOrders.delete(orderId);
|
this.openOrders.delete(orderId);
|
||||||
@@ -385,6 +410,34 @@ export class LiveEngine {
|
|||||||
return settled.length ? settled : null;
|
return settled.length ? settled : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recover live orders when market rotates before result is available.
|
||||||
|
* Polls market endpoints for delayed settlement result and settles locally when it appears.
|
||||||
|
*/
|
||||||
|
async checkOrphans(getMarketFn) {
|
||||||
|
const tickers = this.getOpenTickers();
|
||||||
|
if (!tickers.length) return [];
|
||||||
|
|
||||||
|
const settled = [];
|
||||||
|
|
||||||
|
for (const ticker of tickers) {
|
||||||
|
try {
|
||||||
|
const market = await getMarketFn(ticker);
|
||||||
|
const result = String(market?.result || '').toLowerCase();
|
||||||
|
|
||||||
|
if (result === 'yes' || result === 'no') {
|
||||||
|
console.log(`[Live] Delayed settlement found for ${ticker}: ${result}`);
|
||||||
|
const done = await this.settle(ticker, result);
|
||||||
|
if (done?.length) settled.push(...done);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[Live] Orphan check failed for ${ticker}:`, e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return settled;
|
||||||
|
}
|
||||||
|
|
||||||
getOpenTickers() {
|
getOpenTickers() {
|
||||||
const tickers = new Set();
|
const tickers = new Set();
|
||||||
for (const [, order] of this.openOrders) {
|
for (const [, order] of this.openOrders) {
|
||||||
@@ -406,12 +459,13 @@ export class LiveEngine {
|
|||||||
if (!o.settled) openList.push(o);
|
if (!o.settled) openList.push(o);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
balance: this._lastBalance != null ? (this._lastBalance / 100) : null,
|
balance: this._lastBalance != null ? this._lastBalance / 100 : null,
|
||||||
portfolioValue: this._lastPortfolioValue != null ? (this._lastPortfolioValue / 100) : null,
|
portfolioValue: this._lastPortfolioValue != null ? this._lastPortfolioValue / 100 : null,
|
||||||
totalPnL: parseFloat((this.totalPnL / 100).toFixed(2)),
|
totalPnL: parseFloat((this.totalPnL / 100).toFixed(2)),
|
||||||
wins: this.wins,
|
wins: this.wins,
|
||||||
losses: this.losses,
|
losses: this.losses,
|
||||||
winRate: this.wins + this.losses > 0
|
winRate:
|
||||||
|
this.wins + this.losses > 0
|
||||||
? parseFloat(((this.wins / (this.wins + this.losses)) * 100).toFixed(1))
|
? parseFloat(((this.wins / (this.wins + this.losses)) * 100).toFixed(1))
|
||||||
: 0,
|
: 0,
|
||||||
totalTrades: this.totalTrades,
|
totalTrades: this.totalTrades,
|
||||||
|
|||||||
Reference in New Issue
Block a user