kalshi-trading-bot-cli 2.1.2 → 2.1.4

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.
@@ -7,6 +7,10 @@ const SUBCOMMANDS = [
7
7
  'alerts', 'config', 'clear-cache', 'chat', 'init', 'status', 'themes',
8
8
  // Backtest
9
9
  'backtest',
10
+ // Octagon Kalshi search/clusters/basket (new)
11
+ 'similar', 'clusters', 'peers', 'correlate', 'basket',
12
+ // Octagon events + Kalshi series rollup + editorial themes registry
13
+ 'events', 'series', 'catalysts',
10
14
  ] as const;
11
15
 
12
16
  export type Subcommand = (typeof SUBCOMMANDS)[number];
@@ -39,6 +43,34 @@ export interface ParsedArgs {
39
43
  minVolume?: number;
40
44
  minPrice?: number;
41
45
  maxPrice?: number;
46
+ // Octagon Kalshi search/clusters/basket flags
47
+ topK?: number;
48
+ behavioral: boolean;
49
+ ranked: boolean;
50
+ labelContains?: string;
51
+ closeBefore?: string;
52
+ windowDays?: number;
53
+ correlationInterval?: '1h' | '1d';
54
+ timeframe?: '1w' | '1m' | '3m' | '6m' | '1y';
55
+ weights?: number[];
56
+ bankroll?: number;
57
+ kellyMultiplier?: number;
58
+ n?: number;
59
+ maxPerCluster?: number;
60
+ maxCorrelation?: number;
61
+ minReturn?: number;
62
+ seriesTicker?: string;
63
+ seriesPrefix?: string;
64
+ sortBy?: string;
65
+ probabilities?: string;
66
+ tickers?: string;
67
+ query?: string;
68
+ showCluster: boolean;
69
+ aggregateBy?: 'series';
70
+ activeOnly: boolean;
71
+ sides?: string; // comma-separated yes/no per ticker for correlate
72
+ cells: boolean; // include_cell_detail for correlate
73
+ autoProbs: boolean; // basket size: auto-fetch leg probabilities via markets/edge
42
74
  parseErrors: string[];
43
75
  }
44
76
 
@@ -69,6 +101,34 @@ export function parseArgs(argv: string[] = process.argv.slice(2)): ParsedArgs {
69
101
  let minVolume: number | undefined;
70
102
  let minPrice: number | undefined;
71
103
  let maxPrice: number | undefined;
104
+ // Octagon Kalshi flags
105
+ let topK: number | undefined;
106
+ let behavioral = false;
107
+ let ranked = false;
108
+ let labelContains: string | undefined;
109
+ let closeBefore: string | undefined;
110
+ let windowDays: number | undefined;
111
+ let correlationInterval: '1h' | '1d' | undefined;
112
+ let timeframe: '1w' | '1m' | '3m' | '6m' | '1y' | undefined;
113
+ let weights: number[] | undefined;
114
+ let bankroll: number | undefined;
115
+ let kellyMultiplier: number | undefined;
116
+ let n: number | undefined;
117
+ let maxPerCluster: number | undefined;
118
+ let maxCorrelation: number | undefined;
119
+ let minReturn: number | undefined;
120
+ let seriesTicker: string | undefined;
121
+ let seriesPrefix: string | undefined;
122
+ let sortBy: string | undefined;
123
+ let probabilities: string | undefined;
124
+ let tickers: string | undefined;
125
+ let query: string | undefined;
126
+ let showCluster = false;
127
+ let aggregateBy: 'series' | undefined;
128
+ let activeOnly = false;
129
+ let sides: string | undefined;
130
+ let cells = false;
131
+ let autoProbs = false;
72
132
 
73
133
  for (let i = 0; i < argv.length; i++) {
74
134
  const arg = argv[i];
@@ -203,6 +263,140 @@ export function parseArgs(argv: string[] = process.argv.slice(2)): ParsedArgs {
203
263
  if (Number.isFinite(numeric) && numeric >= 0 && numeric <= 100) { maxPrice = numeric; }
204
264
  else { parseErrors.push(`Invalid --max-price value: "${raw}" (expected 0-100)`); }
205
265
  } else { parseErrors.push('--max-price requires a value'); }
266
+ } else if (arg === '--top-k') {
267
+ const raw = argv[++i];
268
+ if (raw != null) {
269
+ const numeric = Number(raw);
270
+ if (Number.isFinite(numeric) && Number.isInteger(numeric) && numeric > 0) { topK = numeric; }
271
+ else { parseErrors.push(`Invalid --top-k value: "${raw}" (expected a positive integer)`); }
272
+ } else { parseErrors.push('--top-k requires a value'); }
273
+ } else if (arg === '--behavioral') {
274
+ behavioral = true;
275
+ } else if (arg === '--ranked') {
276
+ ranked = true;
277
+ } else if (arg === '--show-cluster') {
278
+ showCluster = true;
279
+ } else if (arg === '--label') {
280
+ const val = argv[++i];
281
+ if (val != null) { labelContains = val; } else { parseErrors.push('--label requires a value'); }
282
+ } else if (arg === '--close-before') {
283
+ const val = argv[++i];
284
+ if (val != null) { closeBefore = val; } else { parseErrors.push('--close-before requires a value'); }
285
+ } else if (arg === '--window-days') {
286
+ const raw = argv[++i];
287
+ if (raw != null) {
288
+ const numeric = Number(raw);
289
+ if (Number.isFinite(numeric) && Number.isInteger(numeric) && numeric > 0) { windowDays = numeric; }
290
+ else { parseErrors.push(`Invalid --window-days value: "${raw}" (expected a positive integer)`); }
291
+ } else { parseErrors.push('--window-days requires a value'); }
292
+ } else if (arg === '--correlation-interval') {
293
+ const val = argv[++i];
294
+ if (val === '1h' || val === '1d') { correlationInterval = val; }
295
+ else { parseErrors.push(`Invalid --correlation-interval value: "${val}" (expected "1h" or "1d")`); }
296
+ } else if (arg === '--timeframe') {
297
+ const val = argv[++i];
298
+ if (val === '1w' || val === '1m' || val === '3m' || val === '6m' || val === '1y') {
299
+ timeframe = val;
300
+ } else {
301
+ parseErrors.push(`Invalid --timeframe value: "${val}" (expected one of 1w, 1m, 3m, 6m, 1y)`);
302
+ }
303
+ } else if (arg === '--weights') {
304
+ const val = argv[++i];
305
+ if (val != null) {
306
+ const parts = val.split(',').map((s) => Number(s.trim()));
307
+ if (parts.every((p) => Number.isFinite(p))) { weights = parts; }
308
+ else { parseErrors.push(`Invalid --weights value: "${val}" (expected comma-separated numbers)`); }
309
+ } else { parseErrors.push('--weights requires a value (e.g., --weights 0.4,0.4,0.2)'); }
310
+ } else if (arg === '--bankroll') {
311
+ const raw = argv[++i];
312
+ if (raw != null) {
313
+ const numeric = Number(raw);
314
+ if (Number.isFinite(numeric) && numeric > 0) { bankroll = numeric; }
315
+ else { parseErrors.push(`Invalid --bankroll value: "${raw}" (expected a positive number)`); }
316
+ } else { parseErrors.push('--bankroll requires a value'); }
317
+ } else if (arg === '--kelly') {
318
+ const raw = argv[++i];
319
+ if (raw != null) {
320
+ const numeric = Number(raw);
321
+ if (Number.isFinite(numeric) && numeric >= 0 && numeric <= 1) { kellyMultiplier = numeric; }
322
+ else { parseErrors.push(`Invalid --kelly value: "${raw}" (expected 0-1)`); }
323
+ } else { parseErrors.push('--kelly requires a value (e.g., --kelly 0.25)'); }
324
+ } else if (arg === '-n' || arg === '--n') {
325
+ const raw = argv[++i];
326
+ if (raw != null) {
327
+ const numeric = Number(raw);
328
+ if (Number.isFinite(numeric) && Number.isInteger(numeric) && numeric > 0) { n = numeric; }
329
+ else { parseErrors.push(`Invalid -n value: "${raw}" (expected a positive integer)`); }
330
+ } else { parseErrors.push('-n requires a value'); }
331
+ } else if (arg === '--max-per-cluster') {
332
+ const raw = argv[++i];
333
+ if (raw != null) {
334
+ const numeric = Number(raw);
335
+ if (Number.isFinite(numeric) && Number.isInteger(numeric) && numeric > 0) { maxPerCluster = numeric; }
336
+ else { parseErrors.push(`Invalid --max-per-cluster value: "${raw}" (expected a positive integer)`); }
337
+ } else { parseErrors.push('--max-per-cluster requires a value'); }
338
+ } else if (arg === '--max-corr') {
339
+ const raw = argv[++i];
340
+ if (raw != null) {
341
+ const numeric = Number(raw);
342
+ if (Number.isFinite(numeric) && numeric >= -1 && numeric <= 1) { maxCorrelation = numeric; }
343
+ else { parseErrors.push(`Invalid --max-corr value: "${raw}" (expected -1 to 1)`); }
344
+ } else { parseErrors.push('--max-corr requires a value'); }
345
+ } else if (arg === '--min-return') {
346
+ const raw = argv[++i];
347
+ if (raw != null) {
348
+ const numeric = Number(raw);
349
+ if (Number.isFinite(numeric)) { minReturn = numeric; }
350
+ else { parseErrors.push(`Invalid --min-return value: "${raw}" (expected a number, e.g. 0.2 for 20%)`); }
351
+ } else { parseErrors.push('--min-return requires a value'); }
352
+ } else if (arg === '--series') {
353
+ const val = argv[++i];
354
+ if (val != null) { seriesTicker = val; } else { parseErrors.push('--series requires a value'); }
355
+ } else if (arg === '--series-prefix') {
356
+ const val = argv[++i];
357
+ if (val != null) { seriesPrefix = val.toUpperCase(); } else { parseErrors.push('--series-prefix requires a value'); }
358
+ } else if (arg === '--sort-by') {
359
+ const val = argv[++i];
360
+ if (val == null) {
361
+ parseErrors.push('--sort-by requires a value');
362
+ } else {
363
+ // Union across both consumers (search edge + search). Each consumer
364
+ // additionally filters to its own subset; this guards against typos at
365
+ // the CLI surface so invalid values don't silently fall through to defaults.
366
+ const VALID_SORT_BY = new Set([
367
+ // search edge (markets-with-edge)
368
+ 'edge_pp', 'expected_return', 'total_volume', 'model_probability',
369
+ // search (markets)
370
+ 'volume_24h', 'close_time', 'last_price',
371
+ ]);
372
+ if (VALID_SORT_BY.has(val)) {
373
+ sortBy = val;
374
+ } else {
375
+ parseErrors.push(`Invalid --sort-by value: "${val}" (expected one of ${Array.from(VALID_SORT_BY).join(', ')})`);
376
+ }
377
+ }
378
+ } else if (arg === '--probs') {
379
+ const val = argv[++i];
380
+ if (val != null) { probabilities = val; } else { parseErrors.push('--probs requires a value (e.g., --probs KXBTC-...:0.62,KXETH-...:0.58)'); }
381
+ } else if (arg === '--tickers') {
382
+ const val = argv[++i];
383
+ if (val != null) { tickers = val; } else { parseErrors.push('--tickers requires a value (comma-separated list)'); }
384
+ } else if (arg === '-q' || arg === '--query') {
385
+ const val = argv[++i];
386
+ if (val != null) { query = val; } else { parseErrors.push(`${arg} requires a value`); }
387
+ } else if (arg === '--aggregate-by') {
388
+ const val = argv[++i];
389
+ if (val === 'series') { aggregateBy = 'series'; }
390
+ else { parseErrors.push(`Invalid --aggregate-by value: "${val}" (expected "series")`); }
391
+ } else if (arg === '--active-only') {
392
+ activeOnly = true;
393
+ } else if (arg === '--sides') {
394
+ const val = argv[++i];
395
+ if (val != null) { sides = val; } else { parseErrors.push('--sides requires a value (e.g., --sides yes,no,yes)'); }
396
+ } else if (arg === '--cells') {
397
+ cells = true;
398
+ } else if (arg === '--auto-probs') {
399
+ autoProbs = true;
206
400
  } else if (arg.startsWith('--')) {
207
401
  parseErrors.push(`Unknown flag: ${arg}`);
208
402
  } else {
@@ -225,5 +419,14 @@ export function parseArgs(argv: string[] = process.argv.slice(2)): ParsedArgs {
225
419
  positionalArgs.unshift(first);
226
420
  }
227
421
 
228
- return { subcommand, positionalArgs, json, theme, ticker, interval, since, minConfidence, minEdge, side, live, refresh, report, dryRun, verbose, performance, resolved, unresolved, days, maxAge, category, limit, exportPath, minVolume, minPrice, maxPrice, parseErrors };
422
+ return {
423
+ subcommand, positionalArgs, json, theme, ticker, interval, since, minConfidence, minEdge, side,
424
+ live, refresh, report, dryRun, verbose, performance, resolved, unresolved, days, maxAge, category,
425
+ limit, exportPath, minVolume, minPrice, maxPrice,
426
+ topK, behavioral, ranked, labelContains, closeBefore, windowDays, correlationInterval, timeframe,
427
+ weights, bankroll, kellyMultiplier, n, maxPerCluster, maxCorrelation, minReturn, seriesTicker,
428
+ sortBy, probabilities, tickers, query, showCluster, aggregateBy, activeOnly,
429
+ seriesPrefix, sides, cells, autoProbs,
430
+ parseErrors,
431
+ };
229
432
  }
@@ -0,0 +1,87 @@
1
+ import { wrapSuccess, wrapError } from './json.js';
2
+ import type { CLIResponse } from './json.js';
3
+ import type { ParsedArgs } from './parse-args.js';
4
+ import {
5
+ getClusterPeers,
6
+ getMarketClusterMembership,
7
+ type ClusterPeersResponse,
8
+ type ClusterMembership,
9
+ } from '../scan/octagon-kalshi-api.js';
10
+ import { formatTable } from './scan-formatters.js';
11
+
12
+ function truncate(s: string, max: number): string {
13
+ return s.length > max ? s.slice(0, max - 1) + '…' : s;
14
+ }
15
+
16
+ export type PeersResult =
17
+ | { kind: 'peers'; data: ClusterPeersResponse }
18
+ | { kind: 'membership'; data: ClusterMembership };
19
+
20
+ export async function handlePeers(args: ParsedArgs): Promise<CLIResponse<PeersResult>> {
21
+ const ticker = args.positionalArgs[0]?.toUpperCase() ?? args.ticker?.toUpperCase();
22
+ if (!ticker) {
23
+ return wrapError('peers', 'MISSING_TICKER', 'Usage: peers <ticker> [--behavioral] [--limit N] [--show-cluster]');
24
+ }
25
+ try {
26
+ if (args.showCluster) {
27
+ const data = await getMarketClusterMembership(ticker);
28
+ return wrapSuccess('peers', { kind: 'membership', data });
29
+ }
30
+ const data = await getClusterPeers(ticker, {
31
+ kind: args.behavioral ? 'behavioral' : 'thematic',
32
+ limit: args.limit,
33
+ });
34
+ return wrapSuccess('peers', { kind: 'peers', data });
35
+ } catch (err) {
36
+ const message = err instanceof Error ? err.message : String(err);
37
+ return wrapError('peers', 'OCTAGON_ERROR', message);
38
+ }
39
+ }
40
+
41
+ export function formatPeersHuman(result: PeersResult): string {
42
+ if (result.kind === 'membership') return formatMembership(result.data);
43
+ return formatPeers(result.data);
44
+ }
45
+
46
+ function formatPeers(data: ClusterPeersResponse): string {
47
+ const lines: string[] = [];
48
+ const c = data.cluster;
49
+ lines.push(`Peers for ${data.market_ticker} (${data.kind} cluster ${c.cluster_id}: "${c.label}", size ${c.size})`);
50
+ if (c.description) lines.push(` ${c.description}`);
51
+ lines.push('');
52
+
53
+ if (data.data.length === 0) {
54
+ lines.push('No peer markets in this cluster.');
55
+ return lines.join('\n');
56
+ }
57
+
58
+ const rows: string[][] = data.data.map((m) => [
59
+ m.market_ticker,
60
+ truncate(m.title, 45),
61
+ m.distance != null ? m.distance.toFixed(3) : '-',
62
+ m.category ?? '-',
63
+ ]);
64
+
65
+ lines.push(formatTable(['Ticker', 'Title', 'Distance', 'Category'], rows));
66
+ return lines.join('\n');
67
+ }
68
+
69
+ function formatMembership(data: ClusterMembership): string {
70
+ const lines: string[] = [];
71
+ lines.push(`Cluster membership for ${data.market_ticker}`);
72
+ lines.push('');
73
+ if (data.thematic) {
74
+ lines.push(` Thematic cluster ${data.thematic.cluster_id}: "${data.thematic.label}" (size ${data.thematic.size})`);
75
+ if (data.thematic.description) lines.push(` ${data.thematic.description}`);
76
+ } else {
77
+ lines.push(' Thematic (not assigned in current run)');
78
+ }
79
+ if (data.behavioral) {
80
+ const meanRet = data.behavioral.mean_daily_return != null ? ` · mean ${(data.behavioral.mean_daily_return * 100).toFixed(2)}%/day` : '';
81
+ const vol = data.behavioral.daily_volatility != null ? ` · vol ${(data.behavioral.daily_volatility * 100).toFixed(2)}%/day` : '';
82
+ lines.push(` Behavioral cluster ${data.behavioral.cluster_id}: "${data.behavioral.label}" (size ${data.behavioral.size})${meanRet}${vol}`);
83
+ } else {
84
+ lines.push(' Behavioral (not assigned in current run)');
85
+ }
86
+ return lines.join('\n');
87
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Octagon-powered search formatters that back the extended /search and
3
+ * /search edge code paths. Used by dispatch.ts and index.ts when
4
+ * OCTAGON_API_KEY is set; the legacy local-SQLite paths remain as fallback.
5
+ */
6
+ import { formatTable } from './scan-formatters.js';
7
+ import type { KalshiMarketRow, PagedResult, MarketsWithEdgeResponse } from '../scan/octagon-kalshi-api.js';
8
+
9
+ function truncate(s: string, max: number): string {
10
+ return s.length > max ? s.slice(0, max - 1) + '…' : s;
11
+ }
12
+
13
+ function fmtMoney(v: number | null | undefined): string {
14
+ if (v === null || v === undefined) return '-';
15
+ return `$${v.toFixed(2)}`;
16
+ }
17
+
18
+ function fmtVol(v: number | null | undefined): string {
19
+ if (v === null || v === undefined) return '-';
20
+ if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M`;
21
+ if (v >= 1_000) return `${(v / 1_000).toFixed(1)}k`;
22
+ return v.toFixed(0);
23
+ }
24
+
25
+ function fmtCloseDate(iso: string | null): string {
26
+ if (!iso) return '-';
27
+ return iso.slice(0, 10);
28
+ }
29
+
30
+ export function formatMarketSearchHuman(query: string, page: PagedResult<KalshiMarketRow>): string {
31
+ const lines: string[] = [];
32
+ const more = page.has_more ? ' (more available)' : '';
33
+ lines.push(`Markets matching "${query}" — ${page.data.length} shown${more}`);
34
+ lines.push('');
35
+
36
+ if (page.data.length === 0) {
37
+ lines.push('No markets found.');
38
+ return lines.join('\n');
39
+ }
40
+
41
+ const rows: string[][] = page.data.map((m) => [
42
+ m.market_ticker,
43
+ truncate(m.title, 40),
44
+ fmtMoney(m.last_price ?? m.yes_ask),
45
+ fmtVol(m.volume_24h),
46
+ m.category ?? '-',
47
+ fmtCloseDate(m.close_time),
48
+ ]);
49
+ lines.push(formatTable(['Ticker', 'Title', 'Last', '24h Vol', 'Category', 'Closes'], rows));
50
+ return lines.join('\n');
51
+ }
52
+
53
+ export function formatMarketsWithEdgeHuman(data: MarketsWithEdgeResponse, minEdgePp: number): string {
54
+ const lines: string[] = [];
55
+ // Guard against invalid date strings — new Date('garbage').toISOString() throws RangeError.
56
+ let captured = 'unknown';
57
+ if (data.captured_at) {
58
+ const d = new Date(data.captured_at);
59
+ if (!Number.isNaN(d.getTime())) {
60
+ captured = d.toISOString().slice(0, 16).replace('T', ' ');
61
+ }
62
+ }
63
+ lines.push(`Octagon Edge Scanner (server-side) — run ${data.run_id.slice(0, 8)}, captured ${captured} UTC, sort by ${data.sort_by}`);
64
+ lines.push('════════════════════════════════════════════════════════');
65
+ lines.push('');
66
+
67
+ if (data.data.length === 0) {
68
+ lines.push(` No events with |edge| ≥ ${minEdgePp}pp found.`);
69
+ return lines.join('\n');
70
+ }
71
+
72
+ const rows: string[][] = data.data.map((r, i) => [
73
+ String(i + 1),
74
+ r.market_ticker || r.event_ticker,
75
+ truncate(r.title, 35),
76
+ `${r.model_probability.toFixed(1)}%`,
77
+ `${r.market_probability.toFixed(1)}%`,
78
+ `${r.edge_pp >= 0 ? '+' : ''}${r.edge_pp.toFixed(1)}pp`,
79
+ `${(r.expected_return * 100).toFixed(1)}%`,
80
+ fmtVol(r.total_volume),
81
+ r.series_category ?? '-',
82
+ ]);
83
+ lines.push(formatTable(
84
+ ['#', 'Ticker', 'Title', 'Model', 'Market', 'Edge', 'Exp Ret', 'Volume', 'Category'],
85
+ rows,
86
+ ));
87
+ lines.push('');
88
+ lines.push(`${data.data.length} event(s) returned${data.has_more ? ' (more available)' : ''}.`);
89
+ return lines.join('\n');
90
+ }