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,564 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed wrappers over Octagon's Kalshi search/clusters/correlation/basket API.
|
|
3
|
+
* Endpoints live under https://api.octagonai.co/v1/prediction-markets/kalshi/*.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors the pattern in octagon-events-api.ts:
|
|
6
|
+
* - Fetch + Authorization: Bearer ${OCTAGON_API_KEY}
|
|
7
|
+
* - 60s AbortController timeout per request
|
|
8
|
+
* - Non-2xx → Error with status + body excerpt
|
|
9
|
+
*
|
|
10
|
+
* All endpoints are stateless from the CLI's perspective — no SQLite caching.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const KALSHI_API_BASE = 'https://api.octagonai.co/v1/prediction-markets/kalshi';
|
|
14
|
+
const TIMEOUT_MS = 60_000;
|
|
15
|
+
|
|
16
|
+
function buildQuery(params?: object): string {
|
|
17
|
+
if (!params) return '';
|
|
18
|
+
const usp = new URLSearchParams();
|
|
19
|
+
for (const [k, v] of Object.entries(params)) {
|
|
20
|
+
if (v === undefined || v === null || v === '') continue;
|
|
21
|
+
usp.set(k, String(v));
|
|
22
|
+
}
|
|
23
|
+
const s = usp.toString();
|
|
24
|
+
return s ? `?${s}` : '';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function kalshiApi<T>(
|
|
28
|
+
method: 'GET' | 'POST',
|
|
29
|
+
path: string,
|
|
30
|
+
opts?: {
|
|
31
|
+
params?: object;
|
|
32
|
+
body?: unknown;
|
|
33
|
+
},
|
|
34
|
+
): Promise<T> {
|
|
35
|
+
const apiKey = process.env.OCTAGON_API_KEY;
|
|
36
|
+
if (!apiKey) {
|
|
37
|
+
throw new Error('OCTAGON_API_KEY not set. Get one at https://app.octagonai.co');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const url = `${KALSHI_API_BASE}${path}${method === 'GET' ? buildQuery(opts?.params) : ''}`;
|
|
41
|
+
const controller = new AbortController();
|
|
42
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
43
|
+
|
|
44
|
+
let resp: Response;
|
|
45
|
+
try {
|
|
46
|
+
resp = await fetch(url, {
|
|
47
|
+
method,
|
|
48
|
+
headers: {
|
|
49
|
+
Authorization: `Bearer ${apiKey}`,
|
|
50
|
+
...(method === 'POST' ? { 'Content-Type': 'application/json' } : {}),
|
|
51
|
+
},
|
|
52
|
+
...(method === 'POST' && opts?.body !== undefined ? { body: JSON.stringify(opts.body) } : {}),
|
|
53
|
+
signal: controller.signal,
|
|
54
|
+
});
|
|
55
|
+
} finally {
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!resp.ok) {
|
|
60
|
+
const body = await resp.text().catch(() => '');
|
|
61
|
+
let detail = body.slice(0, 300);
|
|
62
|
+
try {
|
|
63
|
+
const parsed = JSON.parse(body) as { detail?: unknown };
|
|
64
|
+
if (typeof parsed.detail === 'string') detail = parsed.detail;
|
|
65
|
+
} catch {
|
|
66
|
+
// body wasn't JSON — fall through with text excerpt
|
|
67
|
+
}
|
|
68
|
+
throw new Error(`Octagon Kalshi API ${resp.status} (${method} ${path}): ${detail}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (await resp.json()) as T;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── Response shapes ────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
export interface KalshiMarketRow {
|
|
77
|
+
market_ticker: string;
|
|
78
|
+
event_ticker: string;
|
|
79
|
+
series_ticker?: string | null;
|
|
80
|
+
title: string;
|
|
81
|
+
subtitle?: string | null;
|
|
82
|
+
yes_subtitle?: string | null;
|
|
83
|
+
no_subtitle?: string | null;
|
|
84
|
+
status: string;
|
|
85
|
+
close_time: string | null;
|
|
86
|
+
last_price?: number | null;
|
|
87
|
+
yes_bid?: number | null;
|
|
88
|
+
yes_ask?: number | null;
|
|
89
|
+
no_bid?: number | null;
|
|
90
|
+
no_ask?: number | null;
|
|
91
|
+
volume?: number | null;
|
|
92
|
+
volume_24h?: number | null;
|
|
93
|
+
liquidity?: number | null;
|
|
94
|
+
open_interest?: number | null;
|
|
95
|
+
category?: string | null;
|
|
96
|
+
event_name?: string | null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface PagedResult<T> {
|
|
100
|
+
data: T[];
|
|
101
|
+
next_cursor: string | null;
|
|
102
|
+
has_more: boolean;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface SimilarMarketRow extends KalshiMarketRow {
|
|
106
|
+
distance: number;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface ClusterRow {
|
|
110
|
+
cluster_id: number;
|
|
111
|
+
label: string;
|
|
112
|
+
description: string;
|
|
113
|
+
size: number;
|
|
114
|
+
sample_titles: string[];
|
|
115
|
+
created_at: string;
|
|
116
|
+
mean_daily_return?: number;
|
|
117
|
+
daily_volatility?: number;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface ClusterMembership {
|
|
121
|
+
market_ticker: string;
|
|
122
|
+
thematic: {
|
|
123
|
+
cluster_id: number;
|
|
124
|
+
label: string;
|
|
125
|
+
description: string;
|
|
126
|
+
size: number;
|
|
127
|
+
} | null;
|
|
128
|
+
behavioral: {
|
|
129
|
+
cluster_id: number;
|
|
130
|
+
label: string;
|
|
131
|
+
size: number;
|
|
132
|
+
mean_daily_return?: number;
|
|
133
|
+
daily_volatility?: number;
|
|
134
|
+
} | null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface ClusterPeersResponse {
|
|
138
|
+
market_ticker: string;
|
|
139
|
+
kind: 'thematic' | 'behavioral';
|
|
140
|
+
cluster: {
|
|
141
|
+
cluster_id: number;
|
|
142
|
+
label: string;
|
|
143
|
+
description: string;
|
|
144
|
+
size: number;
|
|
145
|
+
};
|
|
146
|
+
data: SimilarMarketRow[];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface CorrelationResponse {
|
|
150
|
+
tickers: string[];
|
|
151
|
+
matrix: (number | null)[][];
|
|
152
|
+
ranked_pairs: { ticker_a: string; ticker_b: string; correlation: number }[];
|
|
153
|
+
window_days: number;
|
|
154
|
+
interval: string;
|
|
155
|
+
missing: string[];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface BasketCandle {
|
|
159
|
+
time: number;
|
|
160
|
+
open: number;
|
|
161
|
+
high: number;
|
|
162
|
+
low: number;
|
|
163
|
+
close: number;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface BasketCandlesResponse {
|
|
167
|
+
timeframe: string;
|
|
168
|
+
interval_source: string;
|
|
169
|
+
candles: BasketCandle[];
|
|
170
|
+
tickers: string[];
|
|
171
|
+
missing: string[];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface BasketSummary {
|
|
175
|
+
total_return: number;
|
|
176
|
+
annualized_return: number;
|
|
177
|
+
sharpe: number | null;
|
|
178
|
+
max_drawdown: number;
|
|
179
|
+
win_rate: number;
|
|
180
|
+
first_nav: number;
|
|
181
|
+
final_nav: number;
|
|
182
|
+
observation_count: number;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface BasketBacktestResponse extends BasketCandlesResponse {
|
|
186
|
+
summary: BasketSummary;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface BasketSizeLeg {
|
|
190
|
+
market_ticker: string;
|
|
191
|
+
side: 'yes' | 'no';
|
|
192
|
+
model_probability: number | null;
|
|
193
|
+
price: number | null;
|
|
194
|
+
edge_pp: number | null;
|
|
195
|
+
kelly_fraction: number | null;
|
|
196
|
+
weight: number | null;
|
|
197
|
+
notional_usd: number | null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface BasketSizeResponse {
|
|
201
|
+
bankroll_usd: number;
|
|
202
|
+
kelly_multiplier: number;
|
|
203
|
+
total_notional: number;
|
|
204
|
+
legs: BasketSizeLeg[];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface BasketBuildLeg {
|
|
208
|
+
market_ticker: string;
|
|
209
|
+
title: string;
|
|
210
|
+
category: string | null;
|
|
211
|
+
cluster_id: number | null;
|
|
212
|
+
cluster_label: string | null;
|
|
213
|
+
volume_24h: number | null;
|
|
214
|
+
price: number | null;
|
|
215
|
+
side: 'yes' | 'no';
|
|
216
|
+
model_probability: number | null;
|
|
217
|
+
kelly_fraction: number | null;
|
|
218
|
+
weight: number | null;
|
|
219
|
+
notional_usd: number | null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export interface BasketBuildResponse {
|
|
223
|
+
legs: BasketBuildLeg[];
|
|
224
|
+
realized_max_pairwise_correlation: number | null;
|
|
225
|
+
cluster_breakdown: Record<string, number>;
|
|
226
|
+
dropped: { market_ticker: string; reason: string }[];
|
|
227
|
+
universe_size: number;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export interface RankedClusterRow {
|
|
231
|
+
cluster_id: number;
|
|
232
|
+
label: string;
|
|
233
|
+
description: string;
|
|
234
|
+
size: number;
|
|
235
|
+
basket_tickers: string[];
|
|
236
|
+
summary: BasketSummary;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export interface RankedClustersResponse {
|
|
240
|
+
timeframe: string;
|
|
241
|
+
kind: 'thematic' | 'behavioral';
|
|
242
|
+
top_n_per_cluster: number;
|
|
243
|
+
min_return: number;
|
|
244
|
+
data: RankedClusterRow[];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export interface MarketsWithEdgeRow {
|
|
248
|
+
event_ticker: string;
|
|
249
|
+
market_ticker?: string | null;
|
|
250
|
+
title: string;
|
|
251
|
+
series_category: string | null;
|
|
252
|
+
model_probability: number; // 0-100 (live API returns percentage, not fraction)
|
|
253
|
+
market_probability: number; // 0-100
|
|
254
|
+
edge_pp: number; // already in percentage points
|
|
255
|
+
expected_return: number;
|
|
256
|
+
confidence_score: number;
|
|
257
|
+
total_volume: number;
|
|
258
|
+
total_open_interest: number;
|
|
259
|
+
captured_at?: string;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export interface MarketsWithEdgeResponse {
|
|
263
|
+
run_id: string;
|
|
264
|
+
captured_at: string | null;
|
|
265
|
+
sort_by: string;
|
|
266
|
+
data: MarketsWithEdgeRow[];
|
|
267
|
+
next_cursor: string | null;
|
|
268
|
+
has_more: boolean;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ─── Group A — Primitives ───────────────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
export interface SearchMarketsParams {
|
|
274
|
+
q?: string;
|
|
275
|
+
category?: string;
|
|
276
|
+
series_ticker?: string;
|
|
277
|
+
series_prefix?: string;
|
|
278
|
+
event_ticker?: string;
|
|
279
|
+
close_before?: string;
|
|
280
|
+
min_volume_24h?: number;
|
|
281
|
+
sort_by?: 'volume_24h' | 'close_time' | 'last_price';
|
|
282
|
+
limit?: number;
|
|
283
|
+
cursor?: string;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function searchKalshiMarkets(params: SearchMarketsParams): Promise<PagedResult<KalshiMarketRow>> {
|
|
287
|
+
return kalshiApi<PagedResult<KalshiMarketRow>>('GET', '/markets', { params });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export interface SimilarParams {
|
|
291
|
+
anchor_ticker?: string;
|
|
292
|
+
q?: string;
|
|
293
|
+
top_k?: number;
|
|
294
|
+
category?: string;
|
|
295
|
+
min_volume_24h?: number;
|
|
296
|
+
close_before?: string;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export interface SimilarResponse {
|
|
300
|
+
anchor_ticker: string | null;
|
|
301
|
+
anchor_query: string | null;
|
|
302
|
+
data: SimilarMarketRow[];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function findSimilarMarkets(params: SimilarParams): Promise<SimilarResponse> {
|
|
306
|
+
return kalshiApi<SimilarResponse>('GET', '/markets/similar', { params });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export interface ListClustersParams {
|
|
310
|
+
limit?: number;
|
|
311
|
+
sample_titles?: number;
|
|
312
|
+
label_contains?: string;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function listClusters(params: ListClustersParams = {}): Promise<{ data: ClusterRow[] }> {
|
|
316
|
+
return kalshiApi<{ data: ClusterRow[] }>('GET', '/clusters', { params });
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function listBehavioralClusters(params: ListClustersParams = {}): Promise<{ data: ClusterRow[] }> {
|
|
320
|
+
return kalshiApi<{ data: ClusterRow[] }>('GET', '/behavioral-clusters', { params });
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function getClusterMarkets(clusterId: number, params: { limit?: number; cursor?: string } = {}): Promise<PagedResult<SimilarMarketRow>> {
|
|
324
|
+
return kalshiApi<PagedResult<SimilarMarketRow>>('GET', `/clusters/${clusterId}/markets`, { params });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function getBehavioralClusterMarkets(clusterId: number, params: { limit?: number; cursor?: string } = {}): Promise<PagedResult<SimilarMarketRow>> {
|
|
328
|
+
return kalshiApi<PagedResult<SimilarMarketRow>>('GET', `/behavioral-clusters/${clusterId}/markets`, { params });
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function getMarketClusterMembership(marketTicker: string): Promise<ClusterMembership> {
|
|
332
|
+
return kalshiApi<ClusterMembership>('GET', `/markets/${encodeURIComponent(marketTicker)}/clusters`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function getClusterPeers(marketTicker: string, params: { kind?: 'thematic' | 'behavioral'; limit?: number } = {}): Promise<ClusterPeersResponse> {
|
|
336
|
+
return kalshiApi<ClusterPeersResponse>('GET', `/markets/${encodeURIComponent(marketTicker)}/cluster-peers`, { params });
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export interface CorrelationsBody {
|
|
340
|
+
market_tickers: string[];
|
|
341
|
+
sides?: ('yes' | 'no')[];
|
|
342
|
+
include_cell_detail?: boolean;
|
|
343
|
+
window_days?: number;
|
|
344
|
+
interval?: '1h' | '1d';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export interface CorrelationCellDetail {
|
|
348
|
+
ticker_a: string;
|
|
349
|
+
ticker_b: string;
|
|
350
|
+
correlation: number | null;
|
|
351
|
+
overlap_count: number;
|
|
352
|
+
reason: 'ok' | 'insufficient_overlap' | 'zero_variance';
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export interface CorrelationResponseWithSides extends CorrelationResponse {
|
|
356
|
+
sides?: ('yes' | 'no')[];
|
|
357
|
+
cells_detail?: CorrelationCellDetail[] | null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function getCorrelations(body: CorrelationsBody): Promise<CorrelationResponseWithSides> {
|
|
361
|
+
return kalshiApi<CorrelationResponseWithSides>('POST', '/markets/correlations', { body });
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export interface BasketCandlesBody {
|
|
365
|
+
market_tickers: string[];
|
|
366
|
+
weights?: number[];
|
|
367
|
+
timeframe?: '1w' | '1m' | '3m' | '6m' | '1y';
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export function getBasketCandles(body: BasketCandlesBody): Promise<BasketCandlesResponse> {
|
|
371
|
+
return kalshiApi<BasketCandlesResponse>('POST', '/baskets/candles', { body });
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ─── Group B — Composites ───────────────────────────────────────────────────
|
|
375
|
+
|
|
376
|
+
export interface BasketSizeBody {
|
|
377
|
+
bankroll_usd: number;
|
|
378
|
+
kelly_multiplier: number;
|
|
379
|
+
legs: { market_ticker: string; side: 'yes' | 'no'; model_probability: number }[];
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function getBasketSize(body: BasketSizeBody): Promise<BasketSizeResponse> {
|
|
383
|
+
return kalshiApi<BasketSizeResponse>('POST', '/baskets/size', { body });
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export interface BasketBuildUniverse {
|
|
387
|
+
q?: string;
|
|
388
|
+
anchor_ticker?: string;
|
|
389
|
+
market_tickers?: string[]; // explicit candidate pool (1–200); takes precedence over q/anchor
|
|
390
|
+
category?: string;
|
|
391
|
+
series_ticker?: string;
|
|
392
|
+
min_volume_24h?: number;
|
|
393
|
+
close_before?: string;
|
|
394
|
+
label_contains_any?: string[];
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export interface BasketBuildSizing {
|
|
398
|
+
strategy: 'equal' | 'kelly';
|
|
399
|
+
bankroll_usd?: number;
|
|
400
|
+
kelly_multiplier?: number;
|
|
401
|
+
leg_probabilities?: Record<string, number>;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export interface BasketBuildBody {
|
|
405
|
+
universe: BasketBuildUniverse;
|
|
406
|
+
n: number;
|
|
407
|
+
max_per_cluster?: number;
|
|
408
|
+
max_pairwise_correlation?: number;
|
|
409
|
+
candidate_pool_size?: number;
|
|
410
|
+
correlation_window_days?: number;
|
|
411
|
+
sizing: BasketBuildSizing;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export function buildBasket(body: BasketBuildBody): Promise<BasketBuildResponse> {
|
|
415
|
+
return kalshiApi<BasketBuildResponse>('POST', '/baskets/build', { body });
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export function backtestBasket(body: BasketCandlesBody): Promise<BasketBacktestResponse> {
|
|
419
|
+
return kalshiApi<BasketBacktestResponse>('POST', '/baskets/backtest', { body });
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export interface RankedClustersParams {
|
|
423
|
+
timeframe?: '1w' | '1m' | '3m' | '6m' | '1y';
|
|
424
|
+
min_return?: number;
|
|
425
|
+
top_n_per_cluster?: number;
|
|
426
|
+
kind?: 'thematic' | 'behavioral';
|
|
427
|
+
max_clusters?: number;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function getClustersRankedByReturn(params: RankedClustersParams = {}): Promise<RankedClustersResponse> {
|
|
431
|
+
return kalshiApi<RankedClustersResponse>('GET', '/clusters/ranked-by-return', { params });
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export interface MarketsWithEdgeParams {
|
|
435
|
+
run_id?: string;
|
|
436
|
+
category?: string;
|
|
437
|
+
edge_pp_min?: number;
|
|
438
|
+
edge_pp_max?: number;
|
|
439
|
+
expected_return_min?: number;
|
|
440
|
+
total_volume_min?: number;
|
|
441
|
+
model_probability_min?: number;
|
|
442
|
+
sort_by?: 'edge_pp' | 'expected_return' | 'total_volume' | 'model_probability';
|
|
443
|
+
limit?: number;
|
|
444
|
+
cursor?: string;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export function getMarketsWithEdge(params: MarketsWithEdgeParams = {}): Promise<MarketsWithEdgeResponse> {
|
|
448
|
+
return kalshiApi<MarketsWithEdgeResponse>('GET', '/markets-with-edge', { params });
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ─── Endpoints added in subsequent sessions ─────────────────────────────────
|
|
452
|
+
|
|
453
|
+
export interface PerTickerEdgeRow {
|
|
454
|
+
input_ticker: string;
|
|
455
|
+
market_ticker: string | null;
|
|
456
|
+
event_ticker: string | null;
|
|
457
|
+
title: string | null;
|
|
458
|
+
series_category: string | null;
|
|
459
|
+
model_probability: number | null; // 0-1 fraction per the new endpoint doc
|
|
460
|
+
market_probability: number | null;
|
|
461
|
+
edge_pp: number | null;
|
|
462
|
+
expected_return: number | null;
|
|
463
|
+
confidence_score: number | null;
|
|
464
|
+
total_volume: number | null;
|
|
465
|
+
total_open_interest: number | null;
|
|
466
|
+
status: 'scored' | 'unscored';
|
|
467
|
+
captured_at: string | null;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export interface PerTickerEdgeResponse {
|
|
471
|
+
run_id: string;
|
|
472
|
+
captured_at: string;
|
|
473
|
+
data: PerTickerEdgeRow[];
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export function getMarketsEdge(body: { tickers: string[]; run_id?: string }): Promise<PerTickerEdgeResponse> {
|
|
477
|
+
return kalshiApi<PerTickerEdgeResponse>('POST', '/markets/edge', { body });
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export function getEventMarkets(
|
|
481
|
+
eventTicker: string,
|
|
482
|
+
params: { limit?: number; cursor?: string; min_volume_24h?: number } = {},
|
|
483
|
+
): Promise<{ event_ticker: string; data: KalshiMarketRow[]; next_cursor?: string | null; has_more?: boolean }> {
|
|
484
|
+
return kalshiApi('GET', `/events/${encodeURIComponent(eventTicker)}/markets`, { params });
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export interface SeriesRollupRow {
|
|
488
|
+
series_ticker: string;
|
|
489
|
+
series_title: string | null;
|
|
490
|
+
market_count: number;
|
|
491
|
+
active_count: number;
|
|
492
|
+
total_volume_24h: number;
|
|
493
|
+
dominant_category: string | null;
|
|
494
|
+
categories: string[];
|
|
495
|
+
last_seen_at: string;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export interface SeriesListParams {
|
|
499
|
+
series_prefix?: string;
|
|
500
|
+
category?: string;
|
|
501
|
+
min_volume_24h?: number;
|
|
502
|
+
sort_by?: 'total_volume_24h' | 'market_count' | 'active_count';
|
|
503
|
+
limit?: number;
|
|
504
|
+
cursor?: string;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export function listKalshiSeries(params: SeriesListParams = {}): Promise<PagedResult<SeriesRollupRow>> {
|
|
508
|
+
return kalshiApi<PagedResult<SeriesRollupRow>>('GET', '/series', { params });
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export interface SeriesEventRow {
|
|
512
|
+
event_ticker: string;
|
|
513
|
+
series_ticker: string;
|
|
514
|
+
title: string;
|
|
515
|
+
sub_title?: string | null;
|
|
516
|
+
category?: string | null;
|
|
517
|
+
mutually_exclusive?: boolean | null;
|
|
518
|
+
available_on_brokers?: boolean | null;
|
|
519
|
+
last_updated_ts?: string | null;
|
|
520
|
+
kalshi_url?: string | null;
|
|
521
|
+
kalshi_image_url?: string | null;
|
|
522
|
+
has_report?: boolean;
|
|
523
|
+
close_time?: string | null;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export function getSeriesEvents(
|
|
527
|
+
seriesTicker: string,
|
|
528
|
+
params: { limit?: number; cursor?: string; q?: string } = {},
|
|
529
|
+
): Promise<{ series_ticker: string; data: SeriesEventRow[]; next_cursor?: string | null; has_more?: boolean }> {
|
|
530
|
+
return kalshiApi('GET', `/series/${encodeURIComponent(seriesTicker)}/events`, { params });
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
export interface ValidateBasketLeg {
|
|
534
|
+
market_ticker: string;
|
|
535
|
+
side: 'yes' | 'no';
|
|
536
|
+
stake_usd: number;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export interface ValidateBasketBody {
|
|
540
|
+
legs: ValidateBasketLeg[];
|
|
541
|
+
bankroll_usd?: number;
|
|
542
|
+
correlation_window_days?: number;
|
|
543
|
+
correlation_interval?: '1h' | '1d';
|
|
544
|
+
max_pairwise_correlation?: number;
|
|
545
|
+
calendar_clash_window_days?: number;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export interface BasketValidateResponse {
|
|
549
|
+
total_stake_usd: number;
|
|
550
|
+
bankroll_usd: number | null;
|
|
551
|
+
max_leg_pct: number;
|
|
552
|
+
cluster_breakdown_thematic: Record<string, string[]>;
|
|
553
|
+
cluster_breakdown_behavioral: Record<string, string[]>;
|
|
554
|
+
unassigned_market_tickers: string[];
|
|
555
|
+
max_pairwise_correlation: number | null;
|
|
556
|
+
pairwise_correlations: { ticker_a: string; ticker_b: string; correlation: number }[];
|
|
557
|
+
calendar_clashes: { window_start: string; window_end: string; market_tickers: string[] }[];
|
|
558
|
+
duplicate_underliers: { event_ticker: string; market_tickers: string[] }[];
|
|
559
|
+
warnings: string[];
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
export function validateBasket(body: ValidateBasketBody): Promise<BasketValidateResponse> {
|
|
563
|
+
return kalshiApi<BasketValidateResponse>('POST', '/baskets/validate', { body });
|
|
564
|
+
}
|
|
@@ -49,7 +49,7 @@ function classifyConfidence(absEdge: number): string {
|
|
|
49
49
|
* Convert an Octagon event entry to local DB records and persist.
|
|
50
50
|
* Returns true if a new record was inserted, false if skipped.
|
|
51
51
|
*/
|
|
52
|
-
function persistEvent(db: Database, event: OctagonEventEntry): boolean {
|
|
52
|
+
export function persistEvent(db: Database, event: OctagonEventEntry): boolean {
|
|
53
53
|
const capturedDate = new Date(event.captured_at);
|
|
54
54
|
const closeDate = new Date(event.close_time);
|
|
55
55
|
if (isNaN(capturedDate.getTime()) || isNaN(closeDate.getTime())) return false;
|
|
@@ -102,7 +102,8 @@ function persistEvent(db: Database, event: OctagonEventEntry): boolean {
|
|
|
102
102
|
|
|
103
103
|
db.prepare(
|
|
104
104
|
`UPDATE octagon_reports SET has_history = $hh, mutually_exclusive = $me, series_category = $sc,
|
|
105
|
-
confidence_score = $cs, outcome_probabilities_json = $opj, close_time = $ct
|
|
105
|
+
confidence_score = $cs, outcome_probabilities_json = $opj, close_time = $ct,
|
|
106
|
+
analysis_last_updated = $alu
|
|
106
107
|
WHERE report_id = $rid`,
|
|
107
108
|
).run({
|
|
108
109
|
$rid: reportId,
|
|
@@ -112,34 +113,54 @@ function persistEvent(db: Database, event: OctagonEventEntry): boolean {
|
|
|
112
113
|
$cs: event.confidence_score ?? null,
|
|
113
114
|
$opj: event.outcome_probabilities ? JSON.stringify(event.outcome_probabilities) : null,
|
|
114
115
|
$ct: event.close_time ?? null,
|
|
116
|
+
$alu: event.analysis_last_updated ?? null,
|
|
115
117
|
});
|
|
116
118
|
})();
|
|
117
119
|
|
|
118
|
-
// Also persist to edge_history
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
120
|
+
// Also persist to edge_history. Two cases:
|
|
121
|
+
// - Multi-outcome events (e.g. KXFEDCHAIRNOM-29) ship outcome_probabilities
|
|
122
|
+
// with per-market model/market probs. Write one edge_history row per
|
|
123
|
+
// outcome with the correct ticker — otherwise downstream consumers see
|
|
124
|
+
// the event-level placeholder (typically ~0.5) on every contract.
|
|
125
|
+
// - Single-outcome / no-outcome events: fall back to event-level probs on
|
|
126
|
+
// a single row keyed by the event ticker, as before.
|
|
127
|
+
const outcomes = Array.isArray(event.outcome_probabilities) ? event.outcome_probabilities : [];
|
|
128
|
+
const perOutcomeRows = outcomes.length > 0
|
|
129
|
+
? outcomes
|
|
130
|
+
.filter((o) => o && o.market_ticker && typeof o.model_probability === 'number' && typeof o.market_probability === 'number')
|
|
131
|
+
.map((o) => ({
|
|
132
|
+
ticker: o.market_ticker,
|
|
133
|
+
model_prob: o.model_probability / 100,
|
|
134
|
+
market_prob: o.market_probability / 100,
|
|
135
|
+
}))
|
|
136
|
+
: [{ ticker: event.event_ticker, model_prob: modelProb, market_prob: marketProb }];
|
|
137
|
+
|
|
138
|
+
for (const row of perOutcomeRows) {
|
|
139
|
+
const rowEdge = row.model_prob - row.market_prob;
|
|
140
|
+
try {
|
|
141
|
+
insertEdge(db, {
|
|
142
|
+
ticker: row.ticker,
|
|
143
|
+
event_ticker: event.event_ticker,
|
|
144
|
+
timestamp: capturedAt,
|
|
145
|
+
model_prob: row.model_prob,
|
|
146
|
+
market_prob: row.market_prob,
|
|
147
|
+
edge: rowEdge,
|
|
148
|
+
octagon_report_id: reportId,
|
|
149
|
+
drivers_json: null,
|
|
150
|
+
sources_json: null,
|
|
151
|
+
catalysts_json: null,
|
|
152
|
+
cache_hit: 1,
|
|
153
|
+
cache_miss: 0,
|
|
154
|
+
confidence: classifyConfidence(Math.abs(rowEdge)),
|
|
155
|
+
});
|
|
156
|
+
} catch (err) {
|
|
157
|
+
// Only swallow UNIQUE constraint violations (duplicate ticker+timestamp)
|
|
158
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
159
|
+
if (/UNIQUE constraint failed/i.test(msg)) {
|
|
160
|
+
// Expected — edge already exists for this ticker+timestamp
|
|
161
|
+
} else {
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
143
164
|
}
|
|
144
165
|
}
|
|
145
166
|
|