kalshi-trading-bot-cli 2.1.5 → 2.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/commands/analyze.ts +90 -52
package/package.json
CHANGED
package/src/commands/analyze.ts
CHANGED
|
@@ -55,6 +55,17 @@ export interface AnalyzeData {
|
|
|
55
55
|
* not as the 0.5 placeholder.
|
|
56
56
|
*/
|
|
57
57
|
hasModel: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* True when the Kalshi market has a `last_price` (it has actually traded).
|
|
60
|
+
* When false, market_prob/edge/Kelly cannot be computed; the formatter
|
|
61
|
+
* renders "--" for those fields and notes that the report was generated
|
|
62
|
+
* without a tradeable price reference.
|
|
63
|
+
*
|
|
64
|
+
* The Octagon report itself still loads — only the trading-side math is
|
|
65
|
+
* skipped. This is the common case for newly-listed event-level markets
|
|
66
|
+
* (e.g. World Cup quarterfinal contracts before the bracket is set).
|
|
67
|
+
*/
|
|
68
|
+
hasMarketPrice: boolean;
|
|
58
69
|
reportAge: string | null;
|
|
59
70
|
reportId: string;
|
|
60
71
|
rawReport: string;
|
|
@@ -168,10 +179,16 @@ export async function handleAnalyze(
|
|
|
168
179
|
const market = await resolveMarket(ticker);
|
|
169
180
|
const resolvedTicker = market.ticker;
|
|
170
181
|
const eventTicker = market.event_ticker;
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
182
|
+
const rawMarketProb = parseMarketProb(market);
|
|
183
|
+
const hasMarketPrice = rawMarketProb !== null;
|
|
184
|
+
|
|
185
|
+
// Many event-level Kalshi tickers exist before any contract has traded
|
|
186
|
+
// (World Cup brackets, FOMC date ladders, IPO timing — there's no
|
|
187
|
+
// last_price until someone takes a side). The Octagon report path doesn't
|
|
188
|
+
// need market_prob — only the edge / Kelly / risk-gate math does. So we
|
|
189
|
+
// keep going with a neutral fallback and mark hasMarketPrice = false so
|
|
190
|
+
// the formatter renders "--" for the trading-side fields.
|
|
191
|
+
const marketProb = hasMarketPrice ? rawMarketProb! : 0.5;
|
|
175
192
|
|
|
176
193
|
const invoker = createOctagonInvoker();
|
|
177
194
|
const octagonClient = new OctagonClient(invoker, db, auditTrail);
|
|
@@ -230,30 +247,37 @@ export async function handleAnalyze(
|
|
|
230
247
|
confidence: snapshot.confidence,
|
|
231
248
|
});
|
|
232
249
|
|
|
233
|
-
// Kelly sizing — wrapped in try/catch for demo mode (portfolio endpoints may 401)
|
|
250
|
+
// Kelly sizing — wrapped in try/catch for demo mode (portfolio endpoints may 401).
|
|
251
|
+
// Skip Kelly entirely when there's no last_price: any sizing computed from
|
|
252
|
+
// a 50% market_prob fallback would be meaningless.
|
|
253
|
+
const emptyKelly: KellyResult = {
|
|
254
|
+
side: snapshot.edge >= 0 ? 'yes' : 'no',
|
|
255
|
+
fraction: 0,
|
|
256
|
+
adjustedFraction: 0,
|
|
257
|
+
contracts: 0,
|
|
258
|
+
dollarAmountCents: 0,
|
|
259
|
+
entryPriceCents: 0,
|
|
260
|
+
availableBankroll: 0,
|
|
261
|
+
openExposure: 0,
|
|
262
|
+
cashBalance: 0,
|
|
263
|
+
portfolioValue: 0,
|
|
264
|
+
liquidityAdjusted: false,
|
|
265
|
+
};
|
|
234
266
|
let kelly: KellyResult;
|
|
235
|
-
|
|
236
|
-
kelly =
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
dollarAmountCents: 0,
|
|
250
|
-
entryPriceCents: 0,
|
|
251
|
-
availableBankroll: 0,
|
|
252
|
-
openExposure: 0,
|
|
253
|
-
cashBalance: 0,
|
|
254
|
-
portfolioValue: 0,
|
|
255
|
-
liquidityAdjusted: false,
|
|
256
|
-
};
|
|
267
|
+
if (!hasMarketPrice) {
|
|
268
|
+
kelly = emptyKelly;
|
|
269
|
+
} else {
|
|
270
|
+
try {
|
|
271
|
+
kelly = await kellySize({
|
|
272
|
+
edge: snapshot.edge,
|
|
273
|
+
marketProb,
|
|
274
|
+
market,
|
|
275
|
+
multiplier: getBotSetting('risk.kelly_multiplier') as number | undefined,
|
|
276
|
+
minEdgeThreshold: getBotSetting('risk.min_edge_threshold') as number | undefined,
|
|
277
|
+
});
|
|
278
|
+
} catch {
|
|
279
|
+
kelly = { ...emptyKelly };
|
|
280
|
+
}
|
|
257
281
|
}
|
|
258
282
|
|
|
259
283
|
// Risk gate
|
|
@@ -292,7 +316,10 @@ export async function handleAnalyze(
|
|
|
292
316
|
const entryPrice = (snapshot.edge > 0 ? yesAsk : noAsk);
|
|
293
317
|
|
|
294
318
|
let signal: string;
|
|
295
|
-
if (
|
|
319
|
+
if (!hasMarketPrice) {
|
|
320
|
+
// No tradeable price — no actionable signal. Render explicitly.
|
|
321
|
+
signal = 'no signal (market has no last traded price)';
|
|
322
|
+
} else if (existingPosition) {
|
|
296
323
|
const holdDir = existingPosition.direction.toUpperCase();
|
|
297
324
|
const edgeReversed =
|
|
298
325
|
(existingPosition.direction === 'yes' && snapshot.edge < -0.03) ||
|
|
@@ -362,6 +389,7 @@ export async function handleAnalyze(
|
|
|
362
389
|
modelRunAt,
|
|
363
390
|
staleUpstream,
|
|
364
391
|
hasModel,
|
|
392
|
+
hasMarketPrice,
|
|
365
393
|
modelProb: snapshot.modelProb,
|
|
366
394
|
marketProb,
|
|
367
395
|
edge: snapshot.edge,
|
|
@@ -404,19 +432,25 @@ export function formatAnalyzeHuman(data: AnalyzeData): string {
|
|
|
404
432
|
}
|
|
405
433
|
lines.push('');
|
|
406
434
|
|
|
407
|
-
// Edge & Probabilities.
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
435
|
+
// Edge & Probabilities. Two independent reasons a field may be unavailable:
|
|
436
|
+
// hasModel=false → Octagon has no model scoring → Model Prob shows "--"
|
|
437
|
+
// hasMarketPrice=false → Kalshi market has no last_price → Market Prob shows "--"
|
|
438
|
+
// Edge needs both. Either being false means edge/confidence/mispricing
|
|
439
|
+
// render "--" — we never show a number derived from a placeholder.
|
|
440
|
+
const modelStr = data.hasModel
|
|
441
|
+
? `${(data.modelProb * 100).toFixed(1)}%`
|
|
442
|
+
: `-- (no Octagon model coverage for this market)`;
|
|
443
|
+
const marketStr = data.hasMarketPrice
|
|
444
|
+
? `${(data.marketProb * 100).toFixed(1)}%`
|
|
445
|
+
: `-- (no last traded price — market hasn't traded yet)`;
|
|
446
|
+
const canComputeEdge = data.hasModel && data.hasMarketPrice;
|
|
447
|
+
lines.push(` Model Prob: ${modelStr}`);
|
|
448
|
+
lines.push(` Market Prob: ${marketStr}`);
|
|
449
|
+
if (canComputeEdge) {
|
|
414
450
|
lines.push(` Edge: ${data.edgePp} (${(data.edge * 100).toFixed(1)}%)`);
|
|
415
451
|
lines.push(` Confidence: ${data.confidence}`);
|
|
416
452
|
lines.push(` Mispricing: ${data.mispricingSignal}`);
|
|
417
453
|
} else {
|
|
418
|
-
lines.push(` Model Prob: -- (no Octagon model coverage for this market)`);
|
|
419
|
-
lines.push(` Market Prob: ${(data.marketProb * 100).toFixed(1)}%`);
|
|
420
454
|
lines.push(` Edge: --`);
|
|
421
455
|
lines.push(` Confidence: --`);
|
|
422
456
|
lines.push(` Mispricing: --`);
|
|
@@ -449,22 +483,26 @@ export function formatAnalyzeHuman(data: AnalyzeData): string {
|
|
|
449
483
|
lines.push('');
|
|
450
484
|
}
|
|
451
485
|
|
|
452
|
-
//
|
|
486
|
+
// Position Sizing — only meaningful when there's a tradeable price.
|
|
453
487
|
lines.push(' Position Sizing (Half-Kelly):');
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
lines.push(
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
488
|
+
if (!data.hasMarketPrice) {
|
|
489
|
+
lines.push(' ⚠ Skipped — market has no last traded price; no sizing reference available.');
|
|
490
|
+
} else {
|
|
491
|
+
lines.push(` Side: ${data.kelly.side.toUpperCase()}`);
|
|
492
|
+
lines.push(` Cash Balance: $${(data.kelly.cashBalance / 100).toFixed(2)}`);
|
|
493
|
+
lines.push(` Open Exposure: $${(data.kelly.openExposure / 100).toFixed(2)}`);
|
|
494
|
+
lines.push(` Available: $${(data.kelly.availableBankroll / 100).toFixed(2)}`);
|
|
495
|
+
lines.push(` Contracts: ${data.kelly.contracts}`);
|
|
496
|
+
lines.push(` Dollar Amount: $${(data.kelly.dollarAmountCents / 100).toFixed(2)}`);
|
|
497
|
+
lines.push(` Entry Price: ${data.kelly.entryPriceCents}¢`);
|
|
498
|
+
lines.push(` Kelly f*: ${(data.kelly.fraction * 100).toFixed(1)}%`);
|
|
499
|
+
lines.push(` Adjusted f: ${(data.kelly.adjustedFraction * 100).toFixed(1)}%`);
|
|
500
|
+
if (data.kelly.liquidityAdjusted) {
|
|
501
|
+
lines.push(' ⚠ Liquidity-adjusted (wide spread or low volume)');
|
|
502
|
+
}
|
|
503
|
+
if (data.kelly.skippedReason) {
|
|
504
|
+
lines.push(` ⚠ ${data.kelly.skippedReason}`);
|
|
505
|
+
}
|
|
468
506
|
}
|
|
469
507
|
lines.push('');
|
|
470
508
|
|