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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kalshi-trading-bot-cli",
3
- "version": "2.1.5",
3
+ "version": "2.1.6",
4
4
  "description": "Kalshi Trading Bot CLI - AI-powered prediction market terminal.",
5
5
  "license": "MIT",
6
6
  "author": "Octagon AI, Inc.",
@@ -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 marketProb = parseMarketProb(market);
172
- if (marketProb === null) {
173
- throw new Error(`No last traded price for ${resolvedTicker} — market may have no trades yet.`);
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
- try {
236
- kelly = await kellySize({
237
- edge: snapshot.edge,
238
- marketProb,
239
- market,
240
- multiplier: getBotSetting('risk.kelly_multiplier') as number | undefined,
241
- minEdgeThreshold: getBotSetting('risk.min_edge_threshold') as number | undefined,
242
- });
243
- } catch {
244
- kelly = {
245
- side: snapshot.edge >= 0 ? 'yes' : 'no',
246
- fraction: 0,
247
- adjustedFraction: 0,
248
- contracts: 0,
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 (existingPosition) {
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
- // When Octagon has no model scoring for this market (hasModel=false), render
409
- // the model/edge fields as "--" instead of the 0.5 placeholder — otherwise
410
- // downstream consumers (bots, dashboards) think we have a 50/50 prediction.
411
- if (data.hasModel) {
412
- lines.push(` Model Prob: ${(data.modelProb * 100).toFixed(1)}%`);
413
- lines.push(` Market Prob: ${(data.marketProb * 100).toFixed(1)}%`);
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
- // Kelly Sizing
486
+ // Position Sizing — only meaningful when there's a tradeable price.
453
487
  lines.push(' Position Sizing (Half-Kelly):');
454
- lines.push(` Side: ${data.kelly.side.toUpperCase()}`);
455
- lines.push(` Cash Balance: $${(data.kelly.cashBalance / 100).toFixed(2)}`);
456
- lines.push(` Open Exposure: $${(data.kelly.openExposure / 100).toFixed(2)}`);
457
- lines.push(` Available: $${(data.kelly.availableBankroll / 100).toFixed(2)}`);
458
- lines.push(` Contracts: ${data.kelly.contracts}`);
459
- lines.push(` Dollar Amount: $${(data.kelly.dollarAmountCents / 100).toFixed(2)}`);
460
- lines.push(` Entry Price: ${data.kelly.entryPriceCents}¢`);
461
- lines.push(` Kelly f*: ${(data.kelly.fraction * 100).toFixed(1)}%`);
462
- lines.push(` Adjusted f: ${(data.kelly.adjustedFraction * 100).toFixed(1)}%`);
463
- if (data.kelly.liquidityAdjusted) {
464
- lines.push(' Liquidity-adjusted (wide spread or low volume)');
465
- }
466
- if (data.kelly.skippedReason) {
467
- lines.push(`${data.kelly.skippedReason}`);
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