kalshi-trading-bot-cli 2.1.0

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.
Files changed (198) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +360 -0
  3. package/assets/kalshi-flow-light.png +0 -0
  4. package/assets/screenshot.png +0 -0
  5. package/env.example +43 -0
  6. package/kalshi-flow-light.png +0 -0
  7. package/package.json +66 -0
  8. package/src/agent/agent.ts +249 -0
  9. package/src/agent/channels.ts +53 -0
  10. package/src/agent/index.ts +29 -0
  11. package/src/agent/prompts.ts +171 -0
  12. package/src/agent/run-context.ts +23 -0
  13. package/src/agent/scratchpad.ts +465 -0
  14. package/src/agent/token-counter.ts +33 -0
  15. package/src/agent/tool-executor.ts +166 -0
  16. package/src/agent/types.ts +221 -0
  17. package/src/audit/index.ts +25 -0
  18. package/src/audit/reader.ts +43 -0
  19. package/src/audit/trail.ts +29 -0
  20. package/src/audit/types.ts +133 -0
  21. package/src/backtest/discovery.ts +170 -0
  22. package/src/backtest/fetcher.ts +247 -0
  23. package/src/backtest/metrics.ts +165 -0
  24. package/src/backtest/renderer.ts +196 -0
  25. package/src/backtest/types.ts +45 -0
  26. package/src/cli.ts +943 -0
  27. package/src/commands/alerts.ts +48 -0
  28. package/src/commands/analyze.ts +662 -0
  29. package/src/commands/backtest.ts +276 -0
  30. package/src/commands/clear-cache.ts +24 -0
  31. package/src/commands/config.ts +107 -0
  32. package/src/commands/dispatch.ts +473 -0
  33. package/src/commands/edge.ts +62 -0
  34. package/src/commands/formatters.ts +339 -0
  35. package/src/commands/help.ts +263 -0
  36. package/src/commands/helpers.ts +48 -0
  37. package/src/commands/index.ts +287 -0
  38. package/src/commands/json.ts +43 -0
  39. package/src/commands/parse-args.ts +229 -0
  40. package/src/commands/portfolio.ts +236 -0
  41. package/src/commands/review.ts +176 -0
  42. package/src/commands/scan-formatters.ts +98 -0
  43. package/src/commands/scan.ts +38 -0
  44. package/src/commands/search-edge.ts +139 -0
  45. package/src/commands/status.ts +70 -0
  46. package/src/commands/themes.ts +117 -0
  47. package/src/commands/watch.ts +295 -0
  48. package/src/components/answer-box.ts +57 -0
  49. package/src/components/approval-prompt.ts +34 -0
  50. package/src/components/browse-list.ts +134 -0
  51. package/src/components/chat-log.ts +291 -0
  52. package/src/components/custom-editor.ts +18 -0
  53. package/src/components/debug-panel.ts +52 -0
  54. package/src/components/index.ts +17 -0
  55. package/src/components/intro.ts +92 -0
  56. package/src/components/select-list.ts +155 -0
  57. package/src/components/tool-event.ts +127 -0
  58. package/src/components/user-query.ts +18 -0
  59. package/src/components/working-indicator.ts +87 -0
  60. package/src/controllers/agent-runner.ts +283 -0
  61. package/src/controllers/browse.ts +1013 -0
  62. package/src/controllers/index.ts +7 -0
  63. package/src/controllers/input-history.ts +76 -0
  64. package/src/controllers/model-selection.ts +244 -0
  65. package/src/db/alerts.ts +77 -0
  66. package/src/db/edge.ts +105 -0
  67. package/src/db/event-index.ts +323 -0
  68. package/src/db/events.ts +41 -0
  69. package/src/db/index.ts +60 -0
  70. package/src/db/octagon-cache.ts +118 -0
  71. package/src/db/positions.ts +71 -0
  72. package/src/db/risk.ts +51 -0
  73. package/src/db/schema.ts +227 -0
  74. package/src/db/themes.ts +34 -0
  75. package/src/db/trades.ts +50 -0
  76. package/src/eval/brier.ts +90 -0
  77. package/src/eval/index.ts +4 -0
  78. package/src/eval/performance.ts +87 -0
  79. package/src/gateway/access-control.ts +253 -0
  80. package/src/gateway/agent-runner.ts +75 -0
  81. package/src/gateway/alerts/formatter.ts +90 -0
  82. package/src/gateway/alerts/index.ts +4 -0
  83. package/src/gateway/alerts/router.ts +32 -0
  84. package/src/gateway/alerts/terminal.ts +16 -0
  85. package/src/gateway/alerts/types.ts +13 -0
  86. package/src/gateway/channels/index.ts +9 -0
  87. package/src/gateway/channels/manager.ts +153 -0
  88. package/src/gateway/channels/types.ts +48 -0
  89. package/src/gateway/channels/whatsapp/README.md +234 -0
  90. package/src/gateway/channels/whatsapp/auth-store.ts +140 -0
  91. package/src/gateway/channels/whatsapp/dedupe.ts +60 -0
  92. package/src/gateway/channels/whatsapp/error.ts +122 -0
  93. package/src/gateway/channels/whatsapp/inbound.ts +326 -0
  94. package/src/gateway/channels/whatsapp/index.ts +5 -0
  95. package/src/gateway/channels/whatsapp/lid.ts +56 -0
  96. package/src/gateway/channels/whatsapp/logger.ts +25 -0
  97. package/src/gateway/channels/whatsapp/login.ts +94 -0
  98. package/src/gateway/channels/whatsapp/outbound.ts +119 -0
  99. package/src/gateway/channels/whatsapp/plugin.ts +54 -0
  100. package/src/gateway/channels/whatsapp/reconnect.ts +40 -0
  101. package/src/gateway/channels/whatsapp/runtime.ts +122 -0
  102. package/src/gateway/channels/whatsapp/session.ts +89 -0
  103. package/src/gateway/channels/whatsapp/types.ts +32 -0
  104. package/src/gateway/commands/handler.ts +64 -0
  105. package/src/gateway/commands/index.ts +7 -0
  106. package/src/gateway/commands/parser.ts +29 -0
  107. package/src/gateway/commands/wa-formatters.ts +92 -0
  108. package/src/gateway/config.ts +244 -0
  109. package/src/gateway/extension-points.ts +17 -0
  110. package/src/gateway/gateway.ts +301 -0
  111. package/src/gateway/group/history-buffer.ts +75 -0
  112. package/src/gateway/group/index.ts +8 -0
  113. package/src/gateway/group/member-tracker.ts +60 -0
  114. package/src/gateway/group/mention-detection.ts +42 -0
  115. package/src/gateway/heartbeat/index.ts +8 -0
  116. package/src/gateway/heartbeat/prompt.ts +73 -0
  117. package/src/gateway/heartbeat/runner.ts +200 -0
  118. package/src/gateway/heartbeat/suppression.ts +74 -0
  119. package/src/gateway/index.ts +138 -0
  120. package/src/gateway/routing/resolve-route.ts +119 -0
  121. package/src/gateway/sessions/store.ts +65 -0
  122. package/src/gateway/types.ts +11 -0
  123. package/src/gateway/utils.ts +82 -0
  124. package/src/index.tsx +30 -0
  125. package/src/model/llm.ts +247 -0
  126. package/src/providers.ts +94 -0
  127. package/src/risk/circuit-breaker.ts +113 -0
  128. package/src/risk/correlation.ts +40 -0
  129. package/src/risk/gate.ts +125 -0
  130. package/src/risk/index.ts +10 -0
  131. package/src/risk/kelly.ts +230 -0
  132. package/src/scan/alerter.ts +64 -0
  133. package/src/scan/edge-computer.ts +164 -0
  134. package/src/scan/invoker.ts +199 -0
  135. package/src/scan/loop.ts +184 -0
  136. package/src/scan/octagon-client.ts +627 -0
  137. package/src/scan/octagon-events-api.ts +105 -0
  138. package/src/scan/octagon-prefetch.ts +172 -0
  139. package/src/scan/theme-resolver.ts +179 -0
  140. package/src/scan/types.ts +62 -0
  141. package/src/scan/watchdog.ts +126 -0
  142. package/src/setup/wizard.ts +659 -0
  143. package/src/theme.ts +67 -0
  144. package/src/tools/fetch/cache.ts +95 -0
  145. package/src/tools/fetch/external-content.ts +200 -0
  146. package/src/tools/fetch/index.ts +1 -0
  147. package/src/tools/fetch/web-fetch-utils.ts +122 -0
  148. package/src/tools/fetch/web-fetch.ts +419 -0
  149. package/src/tools/index.ts +10 -0
  150. package/src/tools/kalshi/api.ts +251 -0
  151. package/src/tools/kalshi/dlq.ts +35 -0
  152. package/src/tools/kalshi/events.ts +84 -0
  153. package/src/tools/kalshi/exchange.ts +24 -0
  154. package/src/tools/kalshi/historical.ts +89 -0
  155. package/src/tools/kalshi/index.ts +11 -0
  156. package/src/tools/kalshi/kalshi-search.ts +437 -0
  157. package/src/tools/kalshi/kalshi-trade.ts +102 -0
  158. package/src/tools/kalshi/markets.ts +76 -0
  159. package/src/tools/kalshi/portfolio.ts +100 -0
  160. package/src/tools/kalshi/search-index.ts +198 -0
  161. package/src/tools/kalshi/series.ts +16 -0
  162. package/src/tools/kalshi/trading.ts +115 -0
  163. package/src/tools/kalshi/types.ts +199 -0
  164. package/src/tools/registry.ts +160 -0
  165. package/src/tools/search/index.ts +25 -0
  166. package/src/tools/search/tavily.ts +35 -0
  167. package/src/tools/types.ts +53 -0
  168. package/src/tools/v2/edge-query.ts +135 -0
  169. package/src/tools/v2/octagon-report.ts +112 -0
  170. package/src/tools/v2/portfolio-query.ts +79 -0
  171. package/src/tools/v2/portfolio-review.ts +59 -0
  172. package/src/tools/v2/risk-status.ts +94 -0
  173. package/src/tools/v2/scan.ts +78 -0
  174. package/src/types/qrcode-terminal.d.ts +7 -0
  175. package/src/types/whiskeysockets-baileys.d.ts +41 -0
  176. package/src/types.ts +22 -0
  177. package/src/utils/ai-message.ts +26 -0
  178. package/src/utils/bot-config.ts +219 -0
  179. package/src/utils/cache.ts +195 -0
  180. package/src/utils/config.ts +113 -0
  181. package/src/utils/env.ts +111 -0
  182. package/src/utils/errors.ts +313 -0
  183. package/src/utils/history-context.ts +32 -0
  184. package/src/utils/in-memory-chat-history.ts +268 -0
  185. package/src/utils/index.ts +28 -0
  186. package/src/utils/input-key-handlers.ts +64 -0
  187. package/src/utils/logger.ts +67 -0
  188. package/src/utils/long-term-chat-history.ts +138 -0
  189. package/src/utils/markdown-table.ts +227 -0
  190. package/src/utils/model.ts +70 -0
  191. package/src/utils/ollama.ts +37 -0
  192. package/src/utils/paths.ts +12 -0
  193. package/src/utils/progress-channel.ts +84 -0
  194. package/src/utils/telemetry.ts +103 -0
  195. package/src/utils/text-navigation.ts +81 -0
  196. package/src/utils/thinking-verbs.ts +18 -0
  197. package/src/utils/tokens.ts +36 -0
  198. package/src/utils/tool-description.ts +61 -0
@@ -0,0 +1,221 @@
1
+ // ============================================================================
2
+ // Channel Profiles
3
+ // ============================================================================
4
+
5
+ /**
6
+ * Per-channel formatting profile that controls how the agent responds.
7
+ * Add new entries to CHANNEL_PROFILES in prompts.ts when adding channels.
8
+ */
9
+ export interface ChannelProfile {
10
+ /** Human-readable label used in the system prompt preamble (e.g., "CLI", "WhatsApp") */
11
+ label: string;
12
+ /** One-liner describing the output surface, injected after the date line */
13
+ preamble: string;
14
+ /** Bullet points for the ## Behavior section */
15
+ behavior: string[];
16
+ /** Bullet points for the ## Response Format section */
17
+ responseFormat: string[];
18
+ /** Full tables instruction block, or null to omit the section entirely */
19
+ tables: string | null;
20
+ }
21
+
22
+ // ============================================================================
23
+ // Approval
24
+ // ============================================================================
25
+
26
+ /**
27
+ * User's response to a tool approval prompt.
28
+ * - 'allow-once': approve this single invocation
29
+ * - 'allow-session': approve all invocations of this tool for the rest of the session
30
+ * - 'deny': reject and immediately end the agent's turn
31
+ */
32
+ export type ApprovalDecision = 'allow-once' | 'allow-session' | 'deny';
33
+
34
+ /**
35
+ * Agent configuration
36
+ */
37
+ export interface AgentConfig {
38
+ /** Model to use for LLM calls (e.g., 'gpt-5.4', 'claude-sonnet-4-20250514') */
39
+ model?: string;
40
+ /** Model provider (e.g., 'openai', 'anthropic', 'google', 'ollama') */
41
+ modelProvider?: string;
42
+ /** Maximum agent loop iterations (default: 10) */
43
+ maxIterations?: number;
44
+ /** AbortSignal for cancelling agent execution */
45
+ signal?: AbortSignal;
46
+ /** Delivery channel (e.g., 'cli') — affects response formatting */
47
+ channel?: string;
48
+ /** Group chat context (e.g., WhatsApp groups) */
49
+ groupContext?: import('./prompts.js').GroupContext;
50
+ /** Called when a tool needs explicit user approval to proceed */
51
+ requestToolApproval?: (request: { tool: string; args: Record<string, unknown> }) => Promise<ApprovalDecision>;
52
+ /** Shared set of tool names that have been session-approved (persists across queries) */
53
+ sessionApprovedTools?: Set<string>;
54
+ }
55
+
56
+ /**
57
+ * Message in conversation history
58
+ */
59
+ export interface Message {
60
+ role: 'user' | 'assistant' | 'tool';
61
+ content: string;
62
+ }
63
+
64
+ // ============================================================================
65
+ // Agent Events (for real-time streaming UI)
66
+ // ============================================================================
67
+
68
+ /**
69
+ * Agent is processing/thinking
70
+ */
71
+ export interface ThinkingEvent {
72
+ type: 'thinking';
73
+ message: string;
74
+ }
75
+
76
+ /**
77
+ * Tool execution started
78
+ */
79
+ export interface ToolStartEvent {
80
+ type: 'tool_start';
81
+ tool: string;
82
+ args: Record<string, unknown>;
83
+ }
84
+
85
+ /**
86
+ * Tool execution completed successfully
87
+ */
88
+ export interface ToolEndEvent {
89
+ type: 'tool_end';
90
+ tool: string;
91
+ args: Record<string, unknown>;
92
+ result: string;
93
+ duration: number;
94
+ }
95
+
96
+ /**
97
+ * Tool execution failed
98
+ */
99
+ export interface ToolErrorEvent {
100
+ type: 'tool_error';
101
+ tool: string;
102
+ error: string;
103
+ }
104
+
105
+ /**
106
+ * Mid-execution progress update from a subagent tool
107
+ */
108
+ export interface ToolProgressEvent {
109
+ type: 'tool_progress';
110
+ tool: string;
111
+ message: string;
112
+ }
113
+
114
+ /**
115
+ * Tool call warning due to approaching/exceeding suggested limits
116
+ */
117
+ export interface ToolLimitEvent {
118
+ type: 'tool_limit';
119
+ tool: string;
120
+ /** Warning message about tool usage limits */
121
+ warning?: string;
122
+ /** Whether the tool call was blocked (always false - we only warn, never block) */
123
+ blocked: boolean;
124
+ }
125
+
126
+ /**
127
+ * Tool approval decision event for sensitive tools.
128
+ */
129
+ export interface ToolApprovalEvent {
130
+ type: 'tool_approval';
131
+ tool: string;
132
+ args: Record<string, unknown>;
133
+ approved: ApprovalDecision;
134
+ }
135
+
136
+ /**
137
+ * Tool execution was denied by user approval flow.
138
+ */
139
+ export interface ToolDeniedEvent {
140
+ type: 'tool_denied';
141
+ tool: string;
142
+ args: Record<string, unknown>;
143
+ }
144
+
145
+ /**
146
+ * Context was cleared due to exceeding token threshold (Anthropic-style)
147
+ */
148
+ export interface ContextClearedEvent {
149
+ type: 'context_cleared';
150
+ /** Number of tool results that were cleared from context */
151
+ clearedCount: number;
152
+ /** Number of most recent tool results that were kept */
153
+ keptCount: number;
154
+ }
155
+
156
+ /**
157
+ * Session-start memory context was loaded into the system prompt.
158
+ */
159
+ export interface MemoryRecalledEvent {
160
+ type: 'memory_recalled';
161
+ filesLoaded: string[];
162
+ tokenCount: number;
163
+ }
164
+
165
+ /**
166
+ * Pre-compaction memory flush lifecycle event.
167
+ */
168
+ export interface MemoryFlushEvent {
169
+ type: 'memory_flush';
170
+ phase: 'start' | 'end';
171
+ filesWritten?: string[];
172
+ }
173
+
174
+ /**
175
+ * Token usage statistics
176
+ */
177
+ export interface TokenUsage {
178
+ inputTokens: number;
179
+ outputTokens: number;
180
+ totalTokens: number;
181
+ }
182
+
183
+ /**
184
+ * Agent completed with final result
185
+ */
186
+ export interface DoneEvent {
187
+ type: 'done';
188
+ answer: string;
189
+ toolCalls: Array<{ tool: string; args: Record<string, unknown>; result: string }>;
190
+ iterations: number;
191
+ totalTime: number;
192
+ tokenUsage?: TokenUsage;
193
+ tokensPerSecond?: number;
194
+ }
195
+
196
+ /**
197
+ * Union type for all agent events
198
+ */
199
+ export type AgentEvent =
200
+ | ThinkingEvent
201
+ | ToolStartEvent
202
+ | ToolProgressEvent
203
+ | ToolEndEvent
204
+ | ToolErrorEvent
205
+ | ToolApprovalEvent
206
+ | ToolDeniedEvent
207
+ | ToolLimitEvent
208
+ | ContextClearedEvent
209
+ | DoneEvent;
210
+
211
+ /**
212
+ * Aggregated event used by the CLI history renderer.
213
+ * Combines lifecycle events (tool_start/tool_end/tool_error) into a single display row.
214
+ */
215
+ export interface DisplayEvent {
216
+ id: string;
217
+ event: AgentEvent;
218
+ completed?: boolean;
219
+ endEvent?: AgentEvent;
220
+ progressMessage?: string;
221
+ }
@@ -0,0 +1,25 @@
1
+ export { AuditTrail } from './trail.js';
2
+ export { readAuditLog } from './reader.js';
3
+ export type { ReadAuditLogOpts } from './reader.js';
4
+ export type {
5
+ AuditEvent,
6
+ AuditEventType,
7
+ AuditBase,
8
+ DistributiveOmit,
9
+ ScanStartEvent,
10
+ ScanCompleteEvent,
11
+ OctagonCallEvent,
12
+ EdgeDetectedEvent,
13
+ RecommendationEvent,
14
+ TradeExecutedEvent,
15
+ AlertSentEvent,
16
+ WatchdogCheckEvent,
17
+ ApiRetryEvent,
18
+ DlqEntryEvent,
19
+ ConfigChangeEvent,
20
+ ConfigSetEvent,
21
+ } from './types.js';
22
+
23
+ import { AuditTrail } from './trail.js';
24
+
25
+ export const auditTrail = new AuditTrail();
@@ -0,0 +1,43 @@
1
+ import { readFileSync, existsSync } from 'fs';
2
+ import type { AuditEvent } from './types.js';
3
+ import { appPath } from '../utils/paths.js';
4
+
5
+ const DEFAULT_PATH = appPath('audit.jsonl');
6
+
7
+ export interface ReadAuditLogOpts {
8
+ filePath?: string;
9
+ since?: Date;
10
+ type?: string;
11
+ ticker?: string;
12
+ limit?: number;
13
+ }
14
+
15
+ export function readAuditLog(opts: ReadAuditLogOpts = {}): AuditEvent[] {
16
+ const filePath = opts.filePath ?? DEFAULT_PATH;
17
+
18
+ if (!existsSync(filePath)) return [];
19
+
20
+ const content = readFileSync(filePath, 'utf-8');
21
+ const lines = content.split('\n').filter((line) => line.length > 0);
22
+
23
+ let events: AuditEvent[] = lines.map((line) => JSON.parse(line));
24
+
25
+ if (opts.since) {
26
+ const sinceIso = opts.since.toISOString();
27
+ events = events.filter((e) => e.ts >= sinceIso);
28
+ }
29
+
30
+ if (opts.type) {
31
+ events = events.filter((e) => e.type === opts.type);
32
+ }
33
+
34
+ if (opts.ticker) {
35
+ events = events.filter((e) => 'ticker' in e && (e as any).ticker === opts.ticker);
36
+ }
37
+
38
+ if (opts.limit !== undefined) {
39
+ events = events.slice(0, opts.limit);
40
+ }
41
+
42
+ return events;
43
+ }
@@ -0,0 +1,29 @@
1
+ import { appendFileSync, mkdirSync } from 'fs';
2
+ import { dirname } from 'path';
3
+ import type { AuditEvent, DistributiveOmit } from './types.js';
4
+ import { appPath } from '../utils/paths.js';
5
+
6
+ const DEFAULT_PATH = appPath('audit.jsonl');
7
+
8
+ export class AuditTrail {
9
+ private filePath: string;
10
+ private dirCreated = false;
11
+
12
+ constructor(filePath?: string) {
13
+ this.filePath = filePath ?? DEFAULT_PATH;
14
+ }
15
+
16
+ log(event: DistributiveOmit<AuditEvent, 'ts'>): void {
17
+ if (!this.dirCreated) {
18
+ mkdirSync(dirname(this.filePath), { recursive: true });
19
+ this.dirCreated = true;
20
+ }
21
+
22
+ const record = { ts: new Date().toISOString(), ...event };
23
+ appendFileSync(this.filePath, JSON.stringify(record) + '\n');
24
+ }
25
+
26
+ close(): void {
27
+ // No-op for sync I/O — included for interface symmetry
28
+ }
29
+ }
@@ -0,0 +1,133 @@
1
+ import type { ConfidenceLevel } from '../scan/types.js';
2
+
3
+ export interface AuditBase {
4
+ ts: string; // ISO 8601 UTC
5
+ type: string;
6
+ }
7
+
8
+ export interface ScanStartEvent extends AuditBase {
9
+ type: 'SCAN_START';
10
+ theme: string;
11
+ events_count: number;
12
+ }
13
+
14
+ export interface ScanCompleteEvent extends AuditBase {
15
+ type: 'SCAN_COMPLETE';
16
+ scan_id: string;
17
+ theme: string;
18
+ events_scanned: number;
19
+ edges_found: number;
20
+ duration_ms: number;
21
+ }
22
+
23
+ export interface OctagonCallEvent extends AuditBase {
24
+ type: 'OCTAGON_CALL';
25
+ ticker: string;
26
+ variant: string;
27
+ cache_hit: boolean;
28
+ credits_used: number;
29
+ }
30
+
31
+ export interface EdgeDetectedEvent extends AuditBase {
32
+ type: 'EDGE_DETECTED';
33
+ ticker: string;
34
+ model_prob: number;
35
+ market_prob: number;
36
+ edge: number;
37
+ confidence: ConfidenceLevel;
38
+ drivers: string[];
39
+ }
40
+
41
+ export interface RecommendationEvent extends AuditBase {
42
+ type: 'RECOMMENDATION';
43
+ ticker: string;
44
+ action: string;
45
+ size: number;
46
+ kelly: number;
47
+ risk_gate: string;
48
+ }
49
+
50
+ export interface TradeExecutedEvent extends AuditBase {
51
+ type: 'TRADE_EXECUTED';
52
+ ticker: string;
53
+ order_id: string;
54
+ fill_price: number;
55
+ size: number;
56
+ }
57
+
58
+ export interface AlertSentEvent extends AuditBase {
59
+ type: 'ALERT_SENT';
60
+ alert_id: string;
61
+ channels: string[];
62
+ }
63
+
64
+ export interface WatchdogCheckEvent extends AuditBase {
65
+ type: 'WATCHDOG_CHECK';
66
+ ticker: string;
67
+ entry_edge: number;
68
+ current_edge: number;
69
+ status: string;
70
+ }
71
+
72
+ export interface ApiRetryEvent extends AuditBase {
73
+ type: 'API_RETRY';
74
+ method: string;
75
+ path: string;
76
+ attempt: number;
77
+ max_retries: number;
78
+ status_code: number;
79
+ delay_ms: number;
80
+ }
81
+
82
+ export interface DlqEntryEvent extends AuditBase {
83
+ type: 'DLQ_ENTRY';
84
+ method: string;
85
+ path: string;
86
+ error: string;
87
+ attempts: number;
88
+ }
89
+
90
+ export interface ConfigChangeEvent extends AuditBase {
91
+ type: 'CONFIG_CHANGE';
92
+ category: string;
93
+ avg_brier: number;
94
+ trigger: string;
95
+ recommendation: string;
96
+ }
97
+
98
+ export interface ConfigSetEvent extends AuditBase {
99
+ type: 'CONFIG_SET';
100
+ key: string;
101
+ old_value: string; // JSON.stringify'd
102
+ new_value: string; // JSON.stringify'd
103
+ }
104
+
105
+ export interface OctagonErrorEvent extends AuditBase {
106
+ type: 'OCTAGON_ERROR';
107
+ ticker: string;
108
+ event_ticker: string;
109
+ error: string;
110
+ }
111
+
112
+ export type AuditEvent =
113
+ | ScanStartEvent
114
+ | ScanCompleteEvent
115
+ | OctagonCallEvent
116
+ | EdgeDetectedEvent
117
+ | RecommendationEvent
118
+ | TradeExecutedEvent
119
+ | AlertSentEvent
120
+ | WatchdogCheckEvent
121
+ | ApiRetryEvent
122
+ | DlqEntryEvent
123
+ | ConfigChangeEvent
124
+ | ConfigSetEvent
125
+ | OctagonErrorEvent;
126
+
127
+ export type AuditEventType = AuditEvent['type'];
128
+
129
+ /**
130
+ * Distributive Omit that preserves discriminated union narrowing.
131
+ * Standard Omit<Union, K> collapses the union; this distributes over each member.
132
+ */
133
+ export type DistributiveOmit<T, K extends keyof T> = T extends unknown ? Omit<T, K> : never;
@@ -0,0 +1,170 @@
1
+ import type { Database } from 'bun:sqlite';
2
+ import { callKalshiApi } from '../tools/kalshi/api.js';
3
+ import type { KalshiMarket } from '../tools/kalshi/types.js';
4
+
5
+ const CONCURRENCY = 10;
6
+
7
+ export interface SettledMarket {
8
+ ticker: string;
9
+ event_ticker: string;
10
+ result: 'yes' | 'no';
11
+ close_time: string;
12
+ series_category: string;
13
+ last_price: number; // last traded price (0-1)
14
+ volume: number; // lifetime trading volume (used for tradeability gate)
15
+ }
16
+
17
+ export interface OpenMarket {
18
+ ticker: string;
19
+ event_ticker: string;
20
+ market_prob: number; // current trading price (0-1)
21
+ close_time: string;
22
+ series_category: string;
23
+ volume: number; // lifetime trading volume (tradeability gate)
24
+ volume_24h: number; // 24-hour volume (liquidity-now gate)
25
+ }
26
+
27
+ /** Parse market price from Kalshi response (handles both cents and dollars formats). */
28
+ function parsePrice(m: KalshiMarket): number {
29
+ const dollars = parseFloat(m.last_price_dollars ?? '');
30
+ if (Number.isFinite(dollars)) return dollars;
31
+ return typeof m.last_price === 'number' ? m.last_price / 100 : 0;
32
+ }
33
+
34
+ /** Parse lifetime volume (prefers volume_fp string from new API). */
35
+ function parseVolume(m: KalshiMarket): number {
36
+ const fp = parseFloat(m.volume_fp ?? '');
37
+ if (Number.isFinite(fp)) return fp;
38
+ return typeof m.volume === 'number' ? m.volume : 0;
39
+ }
40
+
41
+ /** Parse 24h volume (prefers volume_24h_fp string from new API). */
42
+ function parseVolume24h(m: KalshiMarket): number {
43
+ const fp = parseFloat(m.volume_24h_fp ?? '');
44
+ if (Number.isFinite(fp)) return fp;
45
+ return typeof m.volume_24h === 'number' ? m.volume_24h : 0;
46
+ }
47
+
48
+ /** Fetch event markets from Kalshi, returning empty array on error. */
49
+ async function fetchEventMarkets(eventTicker: string): Promise<KalshiMarket[]> {
50
+ try {
51
+ const response = await callKalshiApi('GET', `/events/${eventTicker}`, {
52
+ params: { with_nested_markets: true },
53
+ });
54
+ if (!response || typeof response !== 'object') return [];
55
+ const obj = response as Record<string, unknown>;
56
+ const event = (obj.event ?? obj) as Record<string, unknown>;
57
+ const markets = event.markets;
58
+ return Array.isArray(markets) ? markets as KalshiMarket[] : [];
59
+ } catch {
60
+ return [];
61
+ }
62
+ }
63
+
64
+ /** Build the event discovery query with optional category filter and extra WHERE clauses. */
65
+ function buildEventQuery(
66
+ extraWhere: string,
67
+ category?: string,
68
+ ): { query: string; params: Record<string, string> } {
69
+ let query = `SELECT event_ticker, MAX(series_category) as category
70
+ FROM octagon_reports r WHERE variant_used = 'events-api'${extraWhere}`;
71
+ const params: Record<string, string> = {};
72
+ if (category) {
73
+ query += ' AND LOWER(series_category) LIKE $cat';
74
+ params.$cat = `%${category.toLowerCase()}%`;
75
+ }
76
+ query += ' GROUP BY event_ticker';
77
+ return { query, params };
78
+ }
79
+
80
+ /** Process items in parallel batches of `concurrency`. */
81
+ export async function parallelMap<T, R>(
82
+ items: T[],
83
+ fn: (item: T) => Promise<R>,
84
+ concurrency: number,
85
+ ): Promise<R[]> {
86
+ const results: R[] = [];
87
+ for (let i = 0; i < items.length; i += concurrency) {
88
+ const batch = items.slice(i, i + concurrency);
89
+ const batchResults = await Promise.all(batch.map(fn));
90
+ results.push(...batchResults);
91
+ }
92
+ return results;
93
+ }
94
+
95
+ /**
96
+ * Discover settled Kalshi markets that have Octagon coverage with history.
97
+ *
98
+ * No close_time filter: the backtest's prediction-age window is enforced
99
+ * downstream via `selectSnapshotByDate`. Filtering on close_time here would
100
+ * wrongly exclude events that closed before the lookback window but whose
101
+ * predictions were still made within the prediction-age window (they'd still
102
+ * have outcomes to score against).
103
+ */
104
+ export async function discoverSettledMarkets(
105
+ db: Database,
106
+ opts?: { category?: string },
107
+ ): Promise<SettledMarket[]> {
108
+ const { query, params } = buildEventQuery('', opts?.category);
109
+ const events = db.query(query).all(params) as Array<{ event_ticker: string; category: string | null }>;
110
+
111
+ const batchResults = await parallelMap(events, async ({ event_ticker, category: cat }) => {
112
+ const markets = await fetchEventMarkets(event_ticker);
113
+ const settled: SettledMarket[] = [];
114
+
115
+ for (const m of markets) {
116
+ const result = (m.result ?? '').toLowerCase();
117
+ if (result !== 'yes' && result !== 'no') continue;
118
+
119
+ settled.push({
120
+ ticker: m.ticker,
121
+ event_ticker,
122
+ result: result as 'yes' | 'no',
123
+ close_time: m.close_time ?? '',
124
+ series_category: cat ?? '',
125
+ last_price: parsePrice(m),
126
+ volume: parseVolume(m),
127
+ });
128
+ }
129
+ return settled;
130
+ }, CONCURRENCY);
131
+
132
+ return batchResults.flat();
133
+ }
134
+
135
+ /**
136
+ * Discover open Kalshi markets that have Octagon coverage.
137
+ */
138
+ export async function discoverOpenMarkets(
139
+ db: Database,
140
+ opts?: { category?: string },
141
+ ): Promise<OpenMarket[]> {
142
+ const { query: q2, params: p2 } = buildEventQuery('', opts?.category);
143
+ const events2 = db.query(q2).all(p2) as Array<{ event_ticker: string; category: string | null }>;
144
+
145
+ const batchResults = await parallelMap(events2, async ({ event_ticker, category: cat }) => {
146
+ const markets = await fetchEventMarkets(event_ticker);
147
+ const open: OpenMarket[] = [];
148
+
149
+ for (const m of markets) {
150
+ const status = (m.status ?? '').toLowerCase();
151
+ if (status !== 'open' && status !== 'active') continue;
152
+
153
+ const marketProb = parsePrice(m);
154
+ if (marketProb <= 0) continue;
155
+
156
+ open.push({
157
+ ticker: m.ticker,
158
+ event_ticker,
159
+ market_prob: marketProb,
160
+ close_time: m.close_time ?? '',
161
+ series_category: cat ?? '',
162
+ volume: parseVolume(m),
163
+ volume_24h: parseVolume24h(m),
164
+ });
165
+ }
166
+ return open;
167
+ }, CONCURRENCY);
168
+
169
+ return batchResults.flat();
170
+ }