kalshi-trading-bot-cli 2.1.3 → 2.1.5
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/README.md +214 -5
- package/data/themes_seo.json +159 -0
- package/env.example +5 -2
- package/package.json +1 -1
- package/src/backtest/renderer.ts +0 -2
- package/src/cli.ts +89 -0
- package/src/commands/analyze-batch.ts +101 -0
- package/src/commands/analyze.ts +99 -17
- package/src/commands/basket.ts +653 -0
- package/src/commands/catalysts.ts +121 -0
- package/src/commands/clusters.ts +153 -0
- package/src/commands/correlate.ts +112 -0
- package/src/commands/dispatch.ts +306 -7
- package/src/commands/editorial-themes.ts +494 -0
- package/src/commands/events.ts +140 -0
- package/src/commands/help.ts +343 -19
- package/src/commands/index.ts +137 -6
- package/src/commands/parse-args.ts +213 -1
- package/src/commands/peers.ts +87 -0
- package/src/commands/search-remote.ts +90 -0
- package/src/commands/series.ts +386 -0
- package/src/commands/similar.ts +97 -0
- package/src/components/intro.ts +9 -0
- package/src/db/editorial-themes.ts +111 -0
- package/src/db/event-index.ts +109 -31
- package/src/db/octagon-cache.ts +2 -1
- package/src/db/schema.ts +23 -0
- package/src/gateway/commands/handler.ts +6 -2
- package/src/scan/octagon-events-api.ts +55 -0
- package/src/scan/octagon-kalshi-api.ts +564 -0
- package/src/scan/octagon-prefetch.ts +48 -27
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Batch analyze — fan out across multiple tickers in a single Octagon call.
|
|
3
|
+
*
|
|
4
|
+
* The full per-ticker `analyze` does deep work (resolve market, run risk gate,
|
|
5
|
+
* Kelly-size, etc.) and is slow because it makes a fresh Octagon call per
|
|
6
|
+
* ticker. When the user just wants a model-edge readout across N tickers,
|
|
7
|
+
* `POST /kalshi/markets/edge` returns them all in one shot — orders of
|
|
8
|
+
* magnitude faster.
|
|
9
|
+
*
|
|
10
|
+
* This is a complement to `analyze <ticker>`, not a replacement. Use it when
|
|
11
|
+
* you need:
|
|
12
|
+
* - quick edge ladder across 5-100 tickers
|
|
13
|
+
* - JSON output for downstream scripting
|
|
14
|
+
* - refresh without paying per-ticker Octagon round-trips
|
|
15
|
+
*/
|
|
16
|
+
import { wrapSuccess, wrapError } from './json.js';
|
|
17
|
+
import type { CLIResponse } from './json.js';
|
|
18
|
+
import { getMarketsEdge, type PerTickerEdgeRow } from '../scan/octagon-kalshi-api.js';
|
|
19
|
+
import { formatTable } from './scan-formatters.js';
|
|
20
|
+
|
|
21
|
+
function truncate(s: string, max: number): string {
|
|
22
|
+
return s.length > max ? s.slice(0, max - 1) + '…' : s;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function fmtVol(v: number | null | undefined): string {
|
|
26
|
+
if (v === null || v === undefined) return '-';
|
|
27
|
+
if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M`;
|
|
28
|
+
if (v >= 1_000) return `${(v / 1_000).toFixed(1)}k`;
|
|
29
|
+
return v.toFixed(0);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface AnalyzeBatchResult {
|
|
33
|
+
run_id: string;
|
|
34
|
+
captured_at: string;
|
|
35
|
+
data: PerTickerEdgeRow[];
|
|
36
|
+
scored: number;
|
|
37
|
+
unscored: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function handleAnalyzeBatch(tickers: string[]): Promise<CLIResponse<AnalyzeBatchResult>> {
|
|
41
|
+
const cleaned = Array.from(new Set(
|
|
42
|
+
tickers.map((t) => t.trim().toUpperCase()).filter(Boolean),
|
|
43
|
+
));
|
|
44
|
+
if (cleaned.length === 0) {
|
|
45
|
+
return wrapError('analyze', 'NO_TICKERS', 'analyze (batch): supply 1+ tickers');
|
|
46
|
+
}
|
|
47
|
+
if (cleaned.length > 100) {
|
|
48
|
+
return wrapError('analyze', 'TOO_MANY_TICKERS', 'analyze (batch): at most 100 tickers per call');
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const resp = await getMarketsEdge({ tickers: cleaned });
|
|
52
|
+
const scored = resp.data.filter((r) => r.status === 'scored').length;
|
|
53
|
+
return wrapSuccess('analyze', {
|
|
54
|
+
run_id: resp.run_id,
|
|
55
|
+
captured_at: resp.captured_at,
|
|
56
|
+
data: resp.data,
|
|
57
|
+
scored,
|
|
58
|
+
unscored: resp.data.length - scored,
|
|
59
|
+
});
|
|
60
|
+
} catch (err) {
|
|
61
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
62
|
+
return wrapError('analyze', 'OCTAGON_ERROR', message);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function formatAnalyzeBatchHuman(data: AnalyzeBatchResult): string {
|
|
67
|
+
const lines: string[] = [];
|
|
68
|
+
lines.push(`Batch analyze — ${data.scored}/${data.data.length} scored in run ${data.run_id.slice(0, 8)} (${data.captured_at.slice(0, 16).replace('T', ' ')})`);
|
|
69
|
+
lines.push('');
|
|
70
|
+
if (data.data.length === 0) {
|
|
71
|
+
lines.push('No tickers returned.');
|
|
72
|
+
return lines.join('\n');
|
|
73
|
+
}
|
|
74
|
+
const rows: string[][] = data.data.map((r) => {
|
|
75
|
+
const status = r.status === 'scored' ? '✓' : '—';
|
|
76
|
+
const modelStr = r.model_probability == null ? '-' : `${(r.model_probability * 100).toFixed(1)}%`;
|
|
77
|
+
const marketStr = r.market_probability == null ? '-' : `${(r.market_probability * 100).toFixed(1)}%`;
|
|
78
|
+
const edgeStr = r.edge_pp == null ? '-' : `${r.edge_pp > 0 ? '+' : ''}${r.edge_pp.toFixed(1)}pp`;
|
|
79
|
+
const erStr = r.expected_return == null ? '-' : `${(r.expected_return * 100).toFixed(1)}%`;
|
|
80
|
+
return [
|
|
81
|
+
r.input_ticker,
|
|
82
|
+
status,
|
|
83
|
+
truncate(r.title ?? '', 32),
|
|
84
|
+
r.series_category ?? '-',
|
|
85
|
+
modelStr,
|
|
86
|
+
marketStr,
|
|
87
|
+
edgeStr,
|
|
88
|
+
erStr,
|
|
89
|
+
fmtVol(r.total_volume),
|
|
90
|
+
];
|
|
91
|
+
});
|
|
92
|
+
lines.push(formatTable(
|
|
93
|
+
['Ticker', '?', 'Title', 'Category', 'Model', 'Market', 'Edge', 'Exp Ret', 'Volume'],
|
|
94
|
+
rows,
|
|
95
|
+
));
|
|
96
|
+
if (data.unscored > 0) {
|
|
97
|
+
lines.push('');
|
|
98
|
+
lines.push(`${data.unscored} ticker(s) not scored in this Octagon run — try \`kalshi analyze <ticker> --refresh\` for a one-off deep call.`);
|
|
99
|
+
}
|
|
100
|
+
return lines.join('\n');
|
|
101
|
+
}
|
package/src/commands/analyze.ts
CHANGED
|
@@ -25,7 +25,16 @@ export interface AnalyzeData {
|
|
|
25
25
|
eventTicker: string;
|
|
26
26
|
title: string;
|
|
27
27
|
expirationTime: string | null;
|
|
28
|
-
|
|
28
|
+
/** Local timestamp when we last pulled the report from Octagon. */
|
|
29
|
+
refreshedAt: string | null;
|
|
30
|
+
/** Upstream Octagon model-run timestamp (Octagon's `analysis_last_updated`). */
|
|
31
|
+
modelRunAt: string | null;
|
|
32
|
+
/**
|
|
33
|
+
* True when --refresh just ran but the upstream `analysis_last_updated`
|
|
34
|
+
* is unchanged from before the refresh. Tells the user we bumped the
|
|
35
|
+
* cache time but didn't get a newer underlying report from Octagon.
|
|
36
|
+
*/
|
|
37
|
+
staleUpstream: boolean;
|
|
29
38
|
modelProb: number;
|
|
30
39
|
marketProb: number;
|
|
31
40
|
edge: number;
|
|
@@ -40,6 +49,12 @@ export interface AnalyzeData {
|
|
|
40
49
|
riskGate: RiskGateResult;
|
|
41
50
|
liquidityGrade: string;
|
|
42
51
|
fromCache: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* True when Octagon has no model scoring for this market in the cached report.
|
|
54
|
+
* When true, model probability + edge fields should be rendered as "--",
|
|
55
|
+
* not as the 0.5 placeholder.
|
|
56
|
+
*/
|
|
57
|
+
hasModel: boolean;
|
|
43
58
|
reportAge: string | null;
|
|
44
59
|
reportId: string;
|
|
45
60
|
rawReport: string;
|
|
@@ -162,6 +177,15 @@ export async function handleAnalyze(
|
|
|
162
177
|
const octagonClient = new OctagonClient(invoker, db, auditTrail);
|
|
163
178
|
const edgeComputer = new EdgeComputer(db, auditTrail);
|
|
164
179
|
|
|
180
|
+
// Capture the upstream Octagon `analysis_last_updated` BEFORE the refresh
|
|
181
|
+
// so we can detect when --refresh re-fetches the same stale upstream
|
|
182
|
+
// report (cache fetch time bumped, but Octagon's underlying model run
|
|
183
|
+
// didn't move). This catches "stale upstream" cases where the user thinks
|
|
184
|
+
// they got fresh analysis but actually got the same body Octagon last
|
|
185
|
+
// generated weeks ago.
|
|
186
|
+
const preRefreshReport = refresh ? getLatestReport(db, resolvedTicker) : null;
|
|
187
|
+
const preRefreshAnalysis = preRefreshReport?.analysis_last_updated ?? null;
|
|
188
|
+
|
|
165
189
|
// Use cache by default; only refresh when explicitly requested
|
|
166
190
|
// Try prefetch first to avoid an individual Octagon API call
|
|
167
191
|
let variant: 'cache' | 'refresh' = refresh ? 'refresh' : 'cache';
|
|
@@ -302,17 +326,42 @@ export async function handleAnalyze(
|
|
|
302
326
|
risk_gate: gate.passed ? 'PASSED' : 'FAILED',
|
|
303
327
|
});
|
|
304
328
|
|
|
305
|
-
//
|
|
306
|
-
|
|
329
|
+
// Two distinct timestamps:
|
|
330
|
+
// refreshedAt = our local fetched_at (when WE pulled this from Octagon).
|
|
331
|
+
// This is the "Refreshed" date — what bumps when --refresh runs.
|
|
332
|
+
// modelRunAt = Octagon's analysis_last_updated (when their model last
|
|
333
|
+
// scored this event). Independent of our cache.
|
|
334
|
+
const refreshedAt = latestDbReport
|
|
307
335
|
? new Date(latestDbReport.fetched_at * 1000).toISOString().replace('T', ' ').slice(0, 16) + ' UTC'
|
|
308
336
|
: null;
|
|
337
|
+
const modelRunAt = latestDbReport?.analysis_last_updated
|
|
338
|
+
? latestDbReport.analysis_last_updated.replace('T', ' ').slice(0, 16) + ' UTC'
|
|
339
|
+
: null;
|
|
340
|
+
|
|
341
|
+
// hasModel = Octagon returned a real probability for this market. A
|
|
342
|
+
// cache-miss report keeps modelProb at the 0.5 placeholder; we must NOT
|
|
343
|
+
// render that as if it were a real prediction.
|
|
344
|
+
const hasModel = !report.cacheMiss && Number.isFinite(snapshot.modelProb)
|
|
345
|
+
&& !(snapshot.modelProb === 0.5 && report.drivers.length === 0 && report.catalysts.length === 0);
|
|
346
|
+
|
|
347
|
+
// staleUpstream = user asked for --refresh but Octagon's upstream model run
|
|
348
|
+
// timestamp didn't move. Cache fetch time bumped, but the underlying report
|
|
349
|
+
// body is the same one Octagon previously generated. The user wanted fresh
|
|
350
|
+
// analysis; they got an unchanged stale one.
|
|
351
|
+
const staleUpstream = refresh
|
|
352
|
+
&& preRefreshAnalysis != null
|
|
353
|
+
&& latestDbReport?.analysis_last_updated != null
|
|
354
|
+
&& preRefreshAnalysis === latestDbReport.analysis_last_updated;
|
|
309
355
|
|
|
310
356
|
return {
|
|
311
357
|
ticker: resolvedTicker,
|
|
312
358
|
eventTicker,
|
|
313
359
|
title: market.title || market.subtitle || resolvedTicker,
|
|
314
360
|
expirationTime: market.expiration_time || market.expected_expiration_time || market.close_time || null,
|
|
315
|
-
|
|
361
|
+
refreshedAt,
|
|
362
|
+
modelRunAt,
|
|
363
|
+
staleUpstream,
|
|
364
|
+
hasModel,
|
|
316
365
|
modelProb: snapshot.modelProb,
|
|
317
366
|
marketProb,
|
|
318
367
|
edge: snapshot.edge,
|
|
@@ -355,12 +404,23 @@ export function formatAnalyzeHuman(data: AnalyzeData): string {
|
|
|
355
404
|
}
|
|
356
405
|
lines.push('');
|
|
357
406
|
|
|
358
|
-
// Edge & Probabilities
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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)}%`);
|
|
414
|
+
lines.push(` Edge: ${data.edgePp} (${(data.edge * 100).toFixed(1)}%)`);
|
|
415
|
+
lines.push(` Confidence: ${data.confidence}`);
|
|
416
|
+
lines.push(` Mispricing: ${data.mispricingSignal}`);
|
|
417
|
+
} else {
|
|
418
|
+
lines.push(` Model Prob: -- (no Octagon model coverage for this market)`);
|
|
419
|
+
lines.push(` Market Prob: ${(data.marketProb * 100).toFixed(1)}%`);
|
|
420
|
+
lines.push(` Edge: --`);
|
|
421
|
+
lines.push(` Confidence: --`);
|
|
422
|
+
lines.push(` Mispricing: --`);
|
|
423
|
+
}
|
|
364
424
|
lines.push('');
|
|
365
425
|
|
|
366
426
|
// Price Drivers
|
|
@@ -428,15 +488,37 @@ export function formatAnalyzeHuman(data: AnalyzeData): string {
|
|
|
428
488
|
}
|
|
429
489
|
}
|
|
430
490
|
|
|
431
|
-
//
|
|
491
|
+
// Two distinct timestamps — labeled with the wording users actually use:
|
|
492
|
+
//
|
|
493
|
+
// Cache refreshed at = when the bot last fetched/re-read the Octagon
|
|
494
|
+
// payload. This is what bumps on --refresh.
|
|
495
|
+
// Report body updated at = the upstream Octagon `analysis_last_updated`
|
|
496
|
+
// (matches the "Updated: …" date embedded in the
|
|
497
|
+
// report body text). Doesn't change unless
|
|
498
|
+
// Octagon re-runs their analysis upstream.
|
|
499
|
+
//
|
|
500
|
+
// If you're a bot/agent reading this output: use **Report body updated at**
|
|
501
|
+
// to decide whether the underlying analysis is fresh. The Cache refreshed
|
|
502
|
+
// at time only tells you when we last re-pulled the same body — it can be
|
|
503
|
+
// recent while the report itself is weeks old.
|
|
432
504
|
lines.push('');
|
|
433
|
-
if (data.
|
|
434
|
-
|
|
505
|
+
if (data.refreshedAt) {
|
|
506
|
+
const ageSuffix = data.reportAge ? ` (${data.reportAge})` : '';
|
|
507
|
+
lines.push(` Cache refreshed at: ${data.refreshedAt}${ageSuffix}`);
|
|
508
|
+
lines.push(` ↳ when the bot last fetched the Octagon payload; bumps on --refresh`);
|
|
509
|
+
}
|
|
510
|
+
if (data.modelRunAt) {
|
|
511
|
+
lines.push(` Report body updated at: ${data.modelRunAt}`);
|
|
512
|
+
lines.push(` ↳ when Octagon last ran the model upstream (the "Updated:" date inside the report)`);
|
|
513
|
+
}
|
|
514
|
+
if (data.staleUpstream) {
|
|
515
|
+
lines.push('');
|
|
516
|
+
lines.push(` ⚠ --refresh pulled the same Octagon report body. The cache fetch time bumped,`);
|
|
517
|
+
lines.push(` but Octagon's upstream analysis hasn't been re-run since ${data.modelRunAt ?? 'an earlier date'}.`);
|
|
518
|
+
lines.push(` Treat this as a stale upstream report — no newer analysis is available.`);
|
|
435
519
|
}
|
|
436
|
-
if (data.fromCache
|
|
437
|
-
lines.push(` Data: cached
|
|
438
|
-
} else if (data.fromCache) {
|
|
439
|
-
lines.push(` Data: cached. Run \`analyze ${data.ticker} --refresh\` for latest (costs 3 credits).`);
|
|
520
|
+
if (data.fromCache) {
|
|
521
|
+
lines.push(` Data: cached. Run \`analyze ${data.ticker} --refresh\` for the latest report (costs 3 credits).`);
|
|
440
522
|
} else {
|
|
441
523
|
lines.push(' Data: freshly generated.');
|
|
442
524
|
}
|