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.
- package/README.md +205 -9
- 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 +88 -0
- package/src/commands/basket.ts +646 -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 +238 -7
- package/src/commands/editorial-themes.ts +494 -0
- package/src/commands/events.ts +140 -0
- package/src/commands/help.ts +299 -14
- package/src/commands/index.ts +137 -6
- package/src/commands/parse-args.ts +204 -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/commands/status.ts +2 -2
- 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/schema.ts +18 -0
- package/src/gateway/commands/handler.ts +6 -2
- package/src/model/llm.ts +5 -5
- package/src/scan/octagon-events-api.ts +55 -0
- package/src/scan/octagon-kalshi-api.ts +564 -0
- package/src/utils/env.ts +1 -1
- package/src/utils/model.ts +1 -1
package/src/db/event-index.ts
CHANGED
|
@@ -17,8 +17,18 @@ export interface IndexedEvent {
|
|
|
17
17
|
* Search the local event index using keyword matching.
|
|
18
18
|
* All keywords must match against title, event_ticker, series_ticker, or category.
|
|
19
19
|
* Returns up to `limit` results.
|
|
20
|
+
*
|
|
21
|
+
* By default, only events with at least one active (open/active status, not past
|
|
22
|
+
* close_time) market are returned, and expired markets are stripped from each
|
|
23
|
+
* event's `markets_json`. Pass `{ includeExpired: true }` to disable both filters.
|
|
20
24
|
*/
|
|
21
|
-
export function searchEventIndex(
|
|
25
|
+
export function searchEventIndex(
|
|
26
|
+
db: Database,
|
|
27
|
+
query: string,
|
|
28
|
+
limit = 50,
|
|
29
|
+
options: { includeExpired?: boolean } = {},
|
|
30
|
+
): IndexedEvent[] {
|
|
31
|
+
const { includeExpired = false } = options;
|
|
22
32
|
const keywords = query
|
|
23
33
|
.toLowerCase()
|
|
24
34
|
.split(/\s+/)
|
|
@@ -30,11 +40,21 @@ export function searchEventIndex(db: Database, query: string, limit = 50): Index
|
|
|
30
40
|
const conditions = keywords.map((_, i) => `(search_text LIKE $kw${i})`);
|
|
31
41
|
const whereClause = conditions.join(' AND ');
|
|
32
42
|
|
|
33
|
-
const
|
|
43
|
+
const now = new Date().toISOString();
|
|
44
|
+
const params: Record<string, string | number> = { $limit: limit, $now: now };
|
|
34
45
|
keywords.forEach((kw, i) => {
|
|
35
46
|
params[`$kw${i}`] = `%${kw}%`;
|
|
36
47
|
});
|
|
37
48
|
|
|
49
|
+
// Require at least one active market unless caller opts in to expired events.
|
|
50
|
+
const activeMarketsClause = includeExpired
|
|
51
|
+
? ''
|
|
52
|
+
: `AND EXISTS (
|
|
53
|
+
SELECT 1 FROM json_each(markets_json)
|
|
54
|
+
WHERE json_extract(value, '$.status') IN ('open','active')
|
|
55
|
+
AND (json_extract(value, '$.close_time') IS NULL OR json_extract(value, '$.close_time') > $now)
|
|
56
|
+
)`;
|
|
57
|
+
|
|
38
58
|
// Use a CTE to compute search_text, filter expired markets, and rank by open-market volume descending
|
|
39
59
|
const fullSql = `
|
|
40
60
|
WITH indexed AS (
|
|
@@ -46,6 +66,7 @@ export function searchEventIndex(db: Database, query: string, limit = 50): Index
|
|
|
46
66
|
SELECT event_ticker, series_ticker, title, category, strike_date, sub_title, tags, markets_json, indexed_at
|
|
47
67
|
FROM indexed
|
|
48
68
|
WHERE ${whereClause}
|
|
69
|
+
${activeMarketsClause}
|
|
49
70
|
)
|
|
50
71
|
SELECT *
|
|
51
72
|
FROM matched
|
|
@@ -62,7 +83,62 @@ export function searchEventIndex(db: Database, query: string, limit = 50): Index
|
|
|
62
83
|
LIMIT $limit
|
|
63
84
|
`;
|
|
64
85
|
|
|
65
|
-
|
|
86
|
+
const rows = db.query(fullSql).all(params) as IndexedEvent[];
|
|
87
|
+
if (includeExpired) return rows;
|
|
88
|
+
|
|
89
|
+
// Strip expired markets from each event's markets_json so callers never see them.
|
|
90
|
+
return rows.map((r) => ({
|
|
91
|
+
...r,
|
|
92
|
+
markets_json: filterActiveMarketsJson(r.markets_json, now),
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Predicate shared by every call site that filters expired markets.
|
|
98
|
+
* "Active" means status in ('open','active') AND (no close_time or close_time > now).
|
|
99
|
+
*/
|
|
100
|
+
function isActiveMarketRecord(record: Record<string, unknown>, nowIso: string): boolean {
|
|
101
|
+
const status = record.status;
|
|
102
|
+
if (status !== 'open' && status !== 'active') return false;
|
|
103
|
+
const closeTime = record.close_time;
|
|
104
|
+
if (closeTime != null && typeof closeTime === 'string' && closeTime <= nowIso) return false;
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Parse markets_json from the index into an array of object records.
|
|
110
|
+
* Returns [] on parse failure or non-array payloads, and drops any non-object entries.
|
|
111
|
+
*/
|
|
112
|
+
function parseMarketsJsonSafe(markets_json: string | null): Array<Record<string, unknown>> {
|
|
113
|
+
if (!markets_json) return [];
|
|
114
|
+
let parsed: unknown;
|
|
115
|
+
try {
|
|
116
|
+
parsed = JSON.parse(markets_json);
|
|
117
|
+
} catch {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
if (!Array.isArray(parsed)) return [];
|
|
121
|
+
return parsed.filter((m): m is Record<string, unknown> => typeof m === 'object' && m !== null);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Parse markets_json, drop markets that aren't currently active, and re-serialize.
|
|
126
|
+
* Returns the original string on parse failure (so callers don't lose data they could re-parse).
|
|
127
|
+
*/
|
|
128
|
+
function filterActiveMarketsJson(markets_json: string | null, nowIso: string): string | null {
|
|
129
|
+
if (!markets_json) return markets_json;
|
|
130
|
+
let parsed: unknown;
|
|
131
|
+
try {
|
|
132
|
+
parsed = JSON.parse(markets_json);
|
|
133
|
+
} catch {
|
|
134
|
+
return markets_json;
|
|
135
|
+
}
|
|
136
|
+
if (!Array.isArray(parsed)) return markets_json;
|
|
137
|
+
const active = parsed.filter(
|
|
138
|
+
(m): m is Record<string, unknown> =>
|
|
139
|
+
typeof m === 'object' && m !== null && isActiveMarketRecord(m as Record<string, unknown>, nowIso),
|
|
140
|
+
);
|
|
141
|
+
return JSON.stringify(active);
|
|
66
142
|
}
|
|
67
143
|
|
|
68
144
|
/**
|
|
@@ -235,10 +311,20 @@ export function setLastRefresh(db: Database, timestamp: number): void {
|
|
|
235
311
|
/**
|
|
236
312
|
* Reconstruct KalshiEvent[] from the local index for given event tickers.
|
|
237
313
|
* Parses markets_json back into nested market objects.
|
|
314
|
+
*
|
|
315
|
+
* By default, expired markets (status not open/active, or past close_time) are
|
|
316
|
+
* stripped from each event. Pass `{ includeExpired: true }` to keep them.
|
|
238
317
|
*/
|
|
239
|
-
export function getEventsFromIndex(
|
|
318
|
+
export function getEventsFromIndex(
|
|
319
|
+
db: Database,
|
|
320
|
+
eventTickers: string[],
|
|
321
|
+
options: { includeExpired?: boolean } = {},
|
|
322
|
+
): KalshiEvent[] {
|
|
240
323
|
if (eventTickers.length === 0) return [];
|
|
241
324
|
|
|
325
|
+
const { includeExpired = false } = options;
|
|
326
|
+
const nowIso = new Date().toISOString();
|
|
327
|
+
|
|
242
328
|
const placeholders = eventTickers.map(() => '?').join(',');
|
|
243
329
|
const rows = db
|
|
244
330
|
.query(
|
|
@@ -248,25 +334,22 @@ export function getEventsFromIndex(db: Database, eventTickers: string[]): Kalshi
|
|
|
248
334
|
)
|
|
249
335
|
.all(...eventTickers) as IndexedEvent[];
|
|
250
336
|
|
|
251
|
-
return rows
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
markets,
|
|
268
|
-
} as KalshiEvent;
|
|
269
|
-
});
|
|
337
|
+
return rows.map((r) => {
|
|
338
|
+
let markets = parseMarketsJsonSafe(r.markets_json);
|
|
339
|
+
if (!includeExpired) {
|
|
340
|
+
markets = markets.filter((m) => isActiveMarketRecord(m, nowIso));
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
event_ticker: r.event_ticker,
|
|
344
|
+
series_ticker: r.series_ticker ?? '',
|
|
345
|
+
title: r.title,
|
|
346
|
+
category: r.category ?? '',
|
|
347
|
+
sub_title: r.sub_title ?? '',
|
|
348
|
+
strike_date: r.strike_date ?? '',
|
|
349
|
+
mutually_exclusive: false,
|
|
350
|
+
markets: markets as unknown as KalshiMarket[],
|
|
351
|
+
} as KalshiEvent;
|
|
352
|
+
});
|
|
270
353
|
}
|
|
271
354
|
|
|
272
355
|
/**
|
|
@@ -284,14 +367,9 @@ export function getTopEventsByVolume(db: Database, limit: number): KalshiEvent[]
|
|
|
284
367
|
|
|
285
368
|
const events: Array<{ event: KalshiEvent; totalVolume: number }> = [];
|
|
286
369
|
for (const r of rows) {
|
|
287
|
-
|
|
288
|
-
try {
|
|
289
|
-
markets = r.markets_json ? JSON.parse(r.markets_json) : [];
|
|
290
|
-
} catch {
|
|
291
|
-
// Corrupted markets_json — treat as no markets
|
|
292
|
-
}
|
|
370
|
+
const markets = parseMarketsJsonSafe(r.markets_json);
|
|
293
371
|
const totalVolume = markets.reduce(
|
|
294
|
-
(sum
|
|
372
|
+
(sum, m) => sum + (parseFloat(String(m.volume ?? '')) || parseFloat(String(m.volume_fp ?? '')) || 0),
|
|
295
373
|
0,
|
|
296
374
|
);
|
|
297
375
|
events.push({
|
|
@@ -303,7 +381,7 @@ export function getTopEventsByVolume(db: Database, limit: number): KalshiEvent[]
|
|
|
303
381
|
sub_title: r.sub_title ?? '',
|
|
304
382
|
strike_date: r.strike_date ?? '',
|
|
305
383
|
mutually_exclusive: false,
|
|
306
|
-
markets,
|
|
384
|
+
markets: markets as unknown as KalshiMarket[],
|
|
307
385
|
} as KalshiEvent,
|
|
308
386
|
totalVolume,
|
|
309
387
|
});
|
package/src/db/schema.ts
CHANGED
|
@@ -181,6 +181,24 @@ export function migrate(db: Database): void {
|
|
|
181
181
|
|
|
182
182
|
CREATE INDEX IF NOT EXISTS idx_history_event
|
|
183
183
|
ON octagon_history(event_ticker, captured_at);
|
|
184
|
+
|
|
185
|
+
CREATE TABLE IF NOT EXISTS editorial_themes (
|
|
186
|
+
name TEXT PRIMARY KEY,
|
|
187
|
+
description TEXT,
|
|
188
|
+
search_volume INTEGER,
|
|
189
|
+
created_at INTEGER NOT NULL,
|
|
190
|
+
updated_at INTEGER NOT NULL
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
CREATE TABLE IF NOT EXISTS editorial_theme_series (
|
|
194
|
+
theme_name TEXT NOT NULL,
|
|
195
|
+
series_ticker TEXT NOT NULL,
|
|
196
|
+
PRIMARY KEY (theme_name, series_ticker),
|
|
197
|
+
FOREIGN KEY (theme_name) REFERENCES editorial_themes(name) ON DELETE CASCADE
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
CREATE INDEX IF NOT EXISTS idx_ets_series
|
|
201
|
+
ON editorial_theme_series(series_ticker);
|
|
184
202
|
`);
|
|
185
203
|
|
|
186
204
|
// Schema migrations for columns added after initial release
|
|
@@ -23,8 +23,12 @@ function makeArgs(overrides: Partial<ParsedArgs>): ParsedArgs {
|
|
|
23
23
|
performance: false,
|
|
24
24
|
resolved: false,
|
|
25
25
|
unresolved: false,
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
behavioral: false,
|
|
27
|
+
ranked: false,
|
|
28
|
+
showCluster: false,
|
|
29
|
+
activeOnly: false,
|
|
30
|
+
cells: false,
|
|
31
|
+
autoProbs: false,
|
|
28
32
|
parseErrors: [],
|
|
29
33
|
...overrides,
|
|
30
34
|
};
|
package/src/model/llm.ts
CHANGED
|
@@ -9,11 +9,11 @@ import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
|
9
9
|
import { StructuredToolInterface } from '@langchain/core/tools';
|
|
10
10
|
import { Runnable } from '@langchain/core/runnables';
|
|
11
11
|
import { z } from 'zod';
|
|
12
|
-
import { DEFAULT_SYSTEM_PROMPT } from '
|
|
13
|
-
import type { TokenUsage } from '
|
|
14
|
-
import { logger } from '
|
|
15
|
-
import { classifyError, isNonRetryableError } from '
|
|
16
|
-
import { resolveProvider, getProviderById } from '
|
|
12
|
+
import { DEFAULT_SYSTEM_PROMPT } from '../agent/prompts.js';
|
|
13
|
+
import type { TokenUsage } from '../agent/types.js';
|
|
14
|
+
import { logger } from '../utils/index.js';
|
|
15
|
+
import { classifyError, isNonRetryableError } from '../utils/errors.js';
|
|
16
|
+
import { resolveProvider, getProviderById } from '../providers.js';
|
|
17
17
|
|
|
18
18
|
export const DEFAULT_PROVIDER = 'openai';
|
|
19
19
|
export const DEFAULT_MODEL = process.env.DEFAULT_MODEL ?? 'gpt-5.4';
|
|
@@ -48,6 +48,61 @@ const EVENTS_API_BASE = 'https://api.octagonai.co/v1';
|
|
|
48
48
|
const PAGE_LIMIT = 200;
|
|
49
49
|
const TIMEOUT_MS = 60_000;
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Fetch a single page of events with optional filters. Useful for CLI commands
|
|
53
|
+
* that don't need the full universe — e.g. `events list --limit 50`.
|
|
54
|
+
*/
|
|
55
|
+
export async function fetchOctagonEventsPage(opts?: {
|
|
56
|
+
limit?: number;
|
|
57
|
+
cursor?: string | null;
|
|
58
|
+
hasHistory?: boolean;
|
|
59
|
+
}): Promise<{ data: OctagonEventEntry[]; next_cursor: string | null; has_more: boolean }> {
|
|
60
|
+
const apiKey = process.env.OCTAGON_API_KEY;
|
|
61
|
+
if (!apiKey) throw new Error('OCTAGON_API_KEY not set');
|
|
62
|
+
|
|
63
|
+
const params = new URLSearchParams({ limit: String(opts?.limit ?? PAGE_LIMIT) });
|
|
64
|
+
if (opts?.hasHistory) params.set('has_history', 'true');
|
|
65
|
+
if (opts?.cursor) params.set('cursor', opts.cursor);
|
|
66
|
+
|
|
67
|
+
const controller = new AbortController();
|
|
68
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
69
|
+
let resp: Response;
|
|
70
|
+
try {
|
|
71
|
+
resp = await fetch(`${EVENTS_API_BASE}/prediction-markets/events?${params}`, {
|
|
72
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
73
|
+
signal: controller.signal,
|
|
74
|
+
});
|
|
75
|
+
} finally {
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
}
|
|
78
|
+
if (!resp.ok) {
|
|
79
|
+
const body = await resp.text().catch(() => '');
|
|
80
|
+
throw new Error(`Octagon events API ${resp.status}: ${body.slice(0, 200)}`);
|
|
81
|
+
}
|
|
82
|
+
const page = (await resp.json()) as { data?: OctagonEventEntry[]; next_cursor?: string | null; has_more?: boolean };
|
|
83
|
+
return {
|
|
84
|
+
data: Array.isArray(page.data) ? page.data : [],
|
|
85
|
+
next_cursor: page.next_cursor ?? null,
|
|
86
|
+
has_more: !!page.has_more,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Look up a single event by ticker. Scans pages until found (universe is small).
|
|
92
|
+
* Returns null if not found.
|
|
93
|
+
*/
|
|
94
|
+
export async function fetchOctagonEventByTicker(eventTicker: string): Promise<OctagonEventEntry | null> {
|
|
95
|
+
let cursor: string | null = null;
|
|
96
|
+
do {
|
|
97
|
+
const page: { data: OctagonEventEntry[]; next_cursor: string | null; has_more: boolean } =
|
|
98
|
+
await fetchOctagonEventsPage({ cursor });
|
|
99
|
+
const hit = page.data.find((e) => e.event_ticker === eventTicker);
|
|
100
|
+
if (hit) return hit;
|
|
101
|
+
cursor = page.has_more ? page.next_cursor : null;
|
|
102
|
+
} while (cursor);
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
51
106
|
/**
|
|
52
107
|
* Fetch all events from the Octagon Prediction Markets Events API,
|
|
53
108
|
* paginating through all pages.
|