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,473 @@
1
+ import type { ParsedArgs, Subcommand } from './parse-args.js';
2
+ import { wrapSuccess, wrapError } from './json.js';
3
+ import type { CLIResponse } from './json.js';
4
+ import { handleEdge, formatEdgeHuman } from './edge.js';
5
+ import { handleAnalyze, formatAnalyzeHuman, promptAnalyzeActions } from './analyze.js';
6
+ import { formatRawReport } from '../controllers/browse.js';
7
+ import { handlePortfolio, formatPortfolioHuman } from './portfolio.js';
8
+ import { handleConfig, formatConfigHuman } from './config.js';
9
+ import { handleAlerts, formatAlertsHuman } from './alerts.js';
10
+ import { handleStatus } from './status.js';
11
+ import { handleThemes, formatThemesHuman } from './themes.js';
12
+ import { handleWatch } from './watch.js';
13
+ import { handleBacktest, formatBacktestHuman } from './backtest.js';
14
+ import { callKalshiApi } from '../tools/kalshi/api.js';
15
+ import {
16
+ formatBalance,
17
+ formatPositions,
18
+ formatOrders,
19
+ } from './formatters.js';
20
+ import type { KalshiOrder, KalshiPosition } from '../tools/kalshi/types.js';
21
+ import { buildHelp, validateTradeArgs } from './help.js';
22
+ import { fetchMarketQuote } from './helpers.js';
23
+ import { ensureIndex, forceRefreshIndex } from '../tools/kalshi/search-index.js';
24
+ import { searchEventIndex } from '../db/event-index.js';
25
+ import { scanEdges, formatEdgeScanHuman } from './search-edge.js';
26
+ import type { KalshiBalanceResponse } from './formatters.js';
27
+ import { ExitCode, exitCodeFromError } from '../utils/errors.js';
28
+ import { trackEvent } from '../utils/telemetry.js';
29
+
30
+ // ─── Alias resolution ────────────────────────────────────────────────────────
31
+ // Maps legacy CLI subcommands to canonical commands with mode/subview context
32
+
33
+ interface ResolvedCommand {
34
+ canonical: Subcommand;
35
+ mode?: string;
36
+ subview?: string;
37
+ }
38
+
39
+ function resolveAlias(subcommand: Subcommand, positionalArgs: string[]): ResolvedCommand {
40
+ switch (subcommand) {
41
+ // Legacy analysis aliases → analyze
42
+ case 'edge':
43
+ return { canonical: 'edge', mode: 'edge-only' };
44
+ // Legacy account aliases → portfolio
45
+ case 'status':
46
+ return { canonical: 'portfolio', subview: 'status' };
47
+
48
+ // themes → search themes
49
+ case 'themes':
50
+ return { canonical: 'search', subview: 'themes' };
51
+
52
+ default:
53
+ return { canonical: subcommand };
54
+ }
55
+ }
56
+
57
+ export async function dispatch(args: ParsedArgs): Promise<void> {
58
+ const { subcommand, json } = args;
59
+ const resolved = resolveAlias(subcommand, args.positionalArgs);
60
+ trackEvent('cli_command', { command: resolved.canonical, subview: resolved.subview ?? '' });
61
+
62
+ try {
63
+ // ─── reject invalid flags early (for all commands) ───────────────
64
+ if (args.parseErrors.length > 0) {
65
+ const msg = args.parseErrors.join('; ');
66
+ if (json) {
67
+ console.log(JSON.stringify(wrapError(subcommand, 'INVALID_ARGS', msg)));
68
+ process.exit(ExitCode.USER_ERROR);
69
+ } else {
70
+ console.error(msg);
71
+ process.exit(ExitCode.USER_ERROR);
72
+ }
73
+ return;
74
+ }
75
+
76
+ // ─── search ────────────────────────────────────────────────────────
77
+ if (resolved.canonical === 'search') {
78
+ const sub = resolved.subview ?? args.positionalArgs[0];
79
+ if (sub === 'themes' || resolved.subview === 'themes') {
80
+ const resp = await handleThemes(args);
81
+ if (json) {
82
+ console.log(JSON.stringify(resp));
83
+ } else {
84
+ console.log(formatThemesHuman(resp.data));
85
+ }
86
+ process.exit(resp.ok ? ExitCode.SUCCESS : ExitCode.USER_ERROR);
87
+ return;
88
+ }
89
+ if (sub === 'edge') {
90
+ const db = (await import('../db/index.js')).getDb();
91
+ const minEdgePp = (args.minEdge ?? 0.05) * 100;
92
+ const result = scanEdges(db, { minEdgePp, limit: args.limit, category: args.category });
93
+ if (json) {
94
+ console.log(JSON.stringify(wrapSuccess('search', result)));
95
+ } else {
96
+ console.log(formatEdgeScanHuman(result, minEdgePp));
97
+ }
98
+ process.exit(ExitCode.SUCCESS);
99
+ return;
100
+ }
101
+ if (!sub) {
102
+ // No query provided — show themes as a starting point
103
+ const resp = await handleThemes(args);
104
+ if (json) {
105
+ console.log(JSON.stringify(resp));
106
+ } else {
107
+ console.log(formatThemesHuman(resp.data));
108
+ }
109
+ process.exit(resp.ok ? ExitCode.SUCCESS : ExitCode.USER_ERROR);
110
+ return;
111
+ }
112
+ // General search: query the local event index
113
+ if (args.refresh) {
114
+ await forceRefreshIndex();
115
+ } else {
116
+ await ensureIndex();
117
+ }
118
+ const db = (await import('../db/index.js')).getDb();
119
+ const query = args.positionalArgs.join(' ');
120
+ const results = searchEventIndex(db, query, 30);
121
+ if (json) {
122
+ console.log(JSON.stringify(wrapSuccess('search', { events: results })));
123
+ } else {
124
+ if (results.length === 0) {
125
+ console.log(`No events found for "${query}".`);
126
+ } else {
127
+ console.log(`Found ${results.length} event(s) for "${query}":\n`);
128
+ for (const ev of results) {
129
+ const markets = ev.markets_json ? JSON.parse(ev.markets_json) : [];
130
+ const openMarkets = markets.filter((m: any) => m.status === 'open' || m.status === 'active');
131
+ console.log(` ${ev.event_ticker} ${ev.title} (${openMarkets.length} market${openMarkets.length !== 1 ? 's' : ''})`);
132
+ }
133
+ }
134
+ }
135
+ return;
136
+ }
137
+
138
+ // ─── portfolio (with subviews) ─────────────────────────────────────
139
+ if (resolved.canonical === 'portfolio') {
140
+ const subview = resolved.subview ?? args.positionalArgs[0] ?? 'overview';
141
+
142
+ if (subview === 'positions') {
143
+ const data = await callKalshiApi('GET', '/portfolio/positions');
144
+ const allPositions = (data.market_positions ?? data.positions ?? []) as KalshiPosition[];
145
+ const positions = allPositions.filter((p) => {
146
+ const pos = parseFloat(String(p.position ?? '0'));
147
+ return pos !== 0;
148
+ });
149
+ if (json) {
150
+ console.log(JSON.stringify(wrapSuccess('portfolio:positions', { positions })));
151
+ } else {
152
+ console.log(formatPositions(positions));
153
+ }
154
+ return;
155
+ }
156
+
157
+ if (subview === 'orders') {
158
+ const data = await callKalshiApi('GET', '/portfolio/orders', { params: { status: 'resting' } });
159
+ const orders = (data.orders ?? []) as KalshiOrder[];
160
+ if (json) {
161
+ console.log(JSON.stringify(wrapSuccess('portfolio:orders', { orders })));
162
+ } else {
163
+ console.log(formatOrders(orders));
164
+ }
165
+ return;
166
+ }
167
+
168
+ if (subview === 'balance') {
169
+ const data = await callKalshiApi('GET', '/portfolio/balance') as unknown as KalshiBalanceResponse;
170
+ if (json) {
171
+ console.log(JSON.stringify(wrapSuccess('portfolio:balance', data)));
172
+ } else {
173
+ console.log(formatBalance(data));
174
+ }
175
+ return;
176
+ }
177
+
178
+ if (subview === 'status') {
179
+ const output = await handleStatus();
180
+ if (json) {
181
+ console.log(JSON.stringify({ ok: true, output }));
182
+ } else {
183
+ console.log(output);
184
+ }
185
+ return;
186
+ }
187
+
188
+ // Default: full portfolio overview
189
+ const resp = await handlePortfolio(args);
190
+ if (json) {
191
+ console.log(JSON.stringify(resp));
192
+ } else {
193
+ console.log(formatPortfolioHuman(resp.data));
194
+ const warnings = (resp.meta as Record<string, unknown>)?.warnings;
195
+ if (Array.isArray(warnings) && warnings.length > 0) {
196
+ for (const w of warnings) console.error(` ⚠ ${String(w)}`);
197
+ }
198
+ }
199
+ process.exit(resp.ok ? ExitCode.SUCCESS : ExitCode.USER_ERROR);
200
+ return;
201
+ }
202
+
203
+ // ─── analyze ───────────────────────────────────────────────────────
204
+ if (resolved.canonical === 'analyze') {
205
+ const ticker = args.positionalArgs[0];
206
+ if (!ticker) {
207
+ const errResp = wrapError('analyze', 'MISSING_TICKER', 'Usage: analyze <ticker> [--refresh] [--report]');
208
+ if (json) {
209
+ console.log(JSON.stringify(errResp));
210
+ process.exit(ExitCode.USER_ERROR);
211
+ } else {
212
+ console.error('Usage: analyze <ticker> [--refresh] [--report]');
213
+ process.exit(ExitCode.USER_ERROR);
214
+ }
215
+ return;
216
+ }
217
+ const refresh = args.refresh;
218
+ const data = await handleAnalyze(ticker, refresh);
219
+ if (json) {
220
+ console.log(JSON.stringify(wrapSuccess('analyze', data)));
221
+ } else {
222
+ console.log(formatAnalyzeHuman(data));
223
+ if (args.report && data.rawReport) {
224
+ console.log('\n' + formatRawReport(data.rawReport, ticker));
225
+ }
226
+ await promptAnalyzeActions(data);
227
+ }
228
+ return;
229
+ }
230
+
231
+ // ─── watch ─────────────────────────────────────────────────────────
232
+ if (resolved.canonical === 'watch') {
233
+ // Force index rebuild before watching if --refresh is set
234
+ if (args.refresh) {
235
+ await forceRefreshIndex();
236
+ }
237
+ // Per-ticker mode if a positional arg is given and no --theme
238
+ const ticker = args.positionalArgs[0];
239
+ if (ticker && !args.theme) {
240
+ const { handleWatchTicker } = await import('./watch.js');
241
+ await handleWatchTicker(ticker.toUpperCase(), args);
242
+ return;
243
+ }
244
+ // Theme scan mode (existing behavior)
245
+ await handleWatch(args);
246
+ return;
247
+ }
248
+
249
+ // ─── backtest ──────────────────────────────────────────────────────
250
+ if (resolved.canonical === 'backtest') {
251
+ const resp = await handleBacktest(args);
252
+ if (json) {
253
+ console.log(JSON.stringify(resp));
254
+ } else if (resp.ok && resp.data) {
255
+ console.log(formatBacktestHuman(resp.data, {
256
+ minEdge: args.minEdge ?? 0.005,
257
+ }));
258
+ } else {
259
+ console.error(resp.error?.message ?? 'Backtest failed');
260
+ }
261
+ process.exit(resp.ok ? ExitCode.SUCCESS : ExitCode.USER_ERROR);
262
+ return;
263
+ }
264
+
265
+ // ─── buy / sell ────────────────────────────────────────────────────
266
+ if (subcommand === 'buy' || subcommand === 'sell') {
267
+ const [ticker, countStr, priceStr] = args.positionalArgs;
268
+ if (!ticker || !countStr) {
269
+ const usage = `Usage: ${subcommand} <ticker> <count> [price_in_cents] [--side yes|no]`;
270
+ const errResp = wrapError(subcommand, 'MISSING_ARGS', usage);
271
+ if (json) {
272
+ console.log(JSON.stringify(errResp));
273
+ process.exit(ExitCode.USER_ERROR);
274
+ } else {
275
+ console.error(usage);
276
+ process.exit(ExitCode.USER_ERROR);
277
+ }
278
+ return;
279
+ }
280
+ const validated = validateTradeArgs(countStr, priceStr);
281
+ if ('error' in validated) {
282
+ const errResp = wrapError(subcommand, 'INVALID_ARGS', validated.error);
283
+ if (json) {
284
+ console.log(JSON.stringify(errResp));
285
+ process.exit(ExitCode.USER_ERROR);
286
+ } else {
287
+ console.error(validated.error);
288
+ process.exit(ExitCode.USER_ERROR);
289
+ }
290
+ return;
291
+ }
292
+ let effectivePrice = validated.price;
293
+ // When no price given, fetch best quote to simulate a market order
294
+ // (Kalshi API requires a price field even for market-like orders)
295
+ const tradeSide = args.side ?? 'yes';
296
+ if (effectivePrice === undefined) {
297
+ const quoteResult = await fetchMarketQuote(ticker.toUpperCase(), subcommand as 'buy' | 'sell', tradeSide);
298
+ if ('error' in quoteResult) {
299
+ if (json) {
300
+ console.log(JSON.stringify(wrapError(subcommand, 'NO_QUOTE', quoteResult.error)));
301
+ process.exit(ExitCode.EXTERNAL_ERROR);
302
+ } else {
303
+ console.error(quoteResult.error);
304
+ process.exit(ExitCode.EXTERNAL_ERROR);
305
+ }
306
+ return;
307
+ }
308
+ effectivePrice = quoteResult.cents;
309
+ }
310
+ const body: Record<string, unknown> = {
311
+ ticker: ticker.toUpperCase(),
312
+ action: subcommand,
313
+ side: tradeSide,
314
+ type: 'limit',
315
+ count: validated.count,
316
+ ...(tradeSide === 'no'
317
+ ? { no_price: effectivePrice }
318
+ : { yes_price: effectivePrice }),
319
+ };
320
+ const data = await callKalshiApi('POST', '/portfolio/orders', { body });
321
+ if (json) {
322
+ console.log(JSON.stringify(wrapSuccess(subcommand, data)));
323
+ } else {
324
+ const order = data.order as Record<string, unknown> | undefined;
325
+ console.log(order ? `Order placed. ID: ${order.order_id} | Status: ${order.status}` : `Order submitted.`);
326
+ }
327
+ return;
328
+ }
329
+
330
+ // ─── cancel ────────────────────────────────────────────────────────
331
+ if (subcommand === 'cancel') {
332
+ const orderId = args.positionalArgs[0];
333
+ if (!orderId) {
334
+ const errResp = wrapError('cancel', 'MISSING_ARGS', 'Usage: cancel <order_id>');
335
+ if (json) {
336
+ console.log(JSON.stringify(errResp));
337
+ process.exit(ExitCode.USER_ERROR);
338
+ } else {
339
+ console.error('Usage: cancel <order_id>');
340
+ process.exit(ExitCode.USER_ERROR);
341
+ }
342
+ return;
343
+ }
344
+ try {
345
+ await callKalshiApi('DELETE', `/portfolio/orders/${orderId}`);
346
+ } catch (err) {
347
+ const msg = err instanceof Error ? err.message : String(err);
348
+ const hint = msg.includes('404') ? ' (order not found or already filled)' : '';
349
+ const code = exitCodeFromError(err);
350
+ if (json) {
351
+ console.log(JSON.stringify(wrapError('cancel', 'CANCEL_FAILED', msg + hint)));
352
+ process.exit(code);
353
+ } else {
354
+ console.error(`Cancel failed: ${msg}${hint}`);
355
+ process.exit(code);
356
+ }
357
+ return;
358
+ }
359
+ if (json) {
360
+ console.log(JSON.stringify(wrapSuccess('cancel', { orderId, canceled: true })));
361
+ } else {
362
+ console.log(`Order ${orderId} canceled.`);
363
+ }
364
+ return;
365
+ }
366
+
367
+ // ─── help ──────────────────────────────────────────────────────────
368
+ if (subcommand === 'help') {
369
+ const topic = args.positionalArgs[0];
370
+ const result = buildHelp('cli', topic);
371
+ if ('error' in result) {
372
+ const errResp = wrapError('help', 'UNKNOWN_TOPIC', result.error);
373
+ if (json) {
374
+ console.log(JSON.stringify(errResp));
375
+ process.exit(ExitCode.USER_ERROR);
376
+ } else {
377
+ console.error(result.error);
378
+ process.exit(ExitCode.USER_ERROR);
379
+ }
380
+ return;
381
+ }
382
+ if (json) {
383
+ console.log(JSON.stringify(wrapSuccess('help', { text: result.text })));
384
+ } else {
385
+ console.log(result.text);
386
+ }
387
+ return;
388
+ }
389
+
390
+ // ─── Legacy commands (kept for backward compat) ────────────────────
391
+
392
+ // Edge command
393
+ if (subcommand === 'edge') {
394
+ const resp = await handleEdge(args);
395
+ if (json) {
396
+ console.log(JSON.stringify(resp));
397
+ } else {
398
+ console.log(formatEdgeHuman(resp.data));
399
+ }
400
+ process.exit(resp.ok ? ExitCode.SUCCESS : ExitCode.USER_ERROR);
401
+ return;
402
+ }
403
+
404
+ // Config command
405
+ if (subcommand === 'config') {
406
+ const resp = await handleConfig(args);
407
+ if (json) {
408
+ console.log(JSON.stringify(resp));
409
+ } else if (!resp.ok) {
410
+ const errMsg = (resp as { error?: { message?: string } }).error?.message ?? 'Config error';
411
+ console.error(errMsg);
412
+ } else {
413
+ console.log(formatConfigHuman(resp.data));
414
+ }
415
+ process.exit(resp.ok ? ExitCode.SUCCESS : ExitCode.USER_ERROR);
416
+ return;
417
+ }
418
+
419
+ // Clear cache command
420
+ if (subcommand === 'clear-cache') {
421
+ const { handleClearCache } = await import('./clear-cache.js');
422
+ const result = handleClearCache();
423
+ if (json) {
424
+ console.log(JSON.stringify(wrapSuccess('clear-cache', result)));
425
+ } else {
426
+ console.log(result.message);
427
+ }
428
+ return;
429
+ }
430
+
431
+ // Alerts command
432
+ if (subcommand === 'alerts') {
433
+ const resp = await handleAlerts(args);
434
+ if (json) {
435
+ console.log(JSON.stringify(resp));
436
+ } else {
437
+ console.log(formatAlertsHuman(resp.data));
438
+ }
439
+ process.exit(resp.ok ? ExitCode.SUCCESS : ExitCode.USER_ERROR);
440
+ return;
441
+ }
442
+
443
+ // Unknown command
444
+ const resp = wrapError(subcommand, 'UNKNOWN_COMMAND', `Unknown command: ${subcommand}`);
445
+ if (json) {
446
+ console.log(JSON.stringify(resp));
447
+ process.exit(ExitCode.USER_ERROR);
448
+ } else {
449
+ console.error(`Error: unknown command "${subcommand}"`);
450
+ process.exit(ExitCode.USER_ERROR);
451
+ }
452
+ } catch (err) {
453
+ const message = err instanceof Error ? err.message : String(err);
454
+ const code = exitCodeFromError(err);
455
+ const errorCode = code === ExitCode.AUTH_ERROR
456
+ ? 'AUTH_ERROR'
457
+ : code === ExitCode.EXTERNAL_ERROR
458
+ ? 'EXTERNAL_ERROR'
459
+ : code === ExitCode.USER_ERROR
460
+ ? 'USER_ERROR'
461
+ : 'INTERNAL_ERROR';
462
+ const resp = wrapError(subcommand, errorCode, message);
463
+ trackEvent('error_occurred', { command: subcommand, error_code: errorCode });
464
+
465
+ if (json) {
466
+ console.log(JSON.stringify(resp));
467
+ process.exit(code);
468
+ } else {
469
+ console.error(`Error running "${subcommand}": ${message}`);
470
+ process.exit(code);
471
+ }
472
+ }
473
+ }
@@ -0,0 +1,62 @@
1
+ import type { ParsedArgs } from './parse-args.js';
2
+ import type { CLIResponse } from './json.js';
3
+ import type { EdgeRow } from '../db/edge.js';
4
+ import { wrapSuccess, wrapError } from './json.js';
5
+ import { getDb } from '../db/index.js';
6
+ import { getEdgeHistory, getActionableEdges, getLatestEdge } from '../db/edge.js';
7
+ import { formatEdgeTable } from './scan-formatters.js';
8
+
9
+ export async function handleEdge(args: ParsedArgs): Promise<CLIResponse<EdgeRow[]>> {
10
+ const db = getDb();
11
+ let rows: EdgeRow[];
12
+
13
+ const DEFAULT_MAX_AGE_S = 24 * 60 * 60; // 24 hours
14
+ let sinceEpoch: number;
15
+ if (args.since) {
16
+ const parsed = new Date(args.since).getTime();
17
+ if (!Number.isFinite(parsed)) {
18
+ return wrapError('edge', 'INVALID_DATE', `Invalid date format for --since: "${args.since}"`);
19
+ }
20
+ sinceEpoch = Math.floor(parsed / 1000);
21
+ } else {
22
+ sinceEpoch = Math.floor(Date.now() / 1000) - DEFAULT_MAX_AGE_S;
23
+ }
24
+
25
+ if (args.ticker) {
26
+ // History for a single ticker
27
+ rows = getEdgeHistory(db, args.ticker, sinceEpoch);
28
+ } else if (args.minConfidence) {
29
+ // Latest per ticker filtered by confidence
30
+ rows = getActionableEdges(db, args.minConfidence);
31
+ rows = rows.filter((r) => r.timestamp >= sinceEpoch);
32
+ } else if (args.theme) {
33
+ // Get tickers for theme, then latest edge per ticker
34
+ const themeRows = db.query(
35
+ `SELECT DISTINCT ticker FROM edge_history
36
+ WHERE event_ticker IN (SELECT ticker FROM events WHERE theme_id = $theme)`
37
+ ).all({ $theme: args.theme }) as { ticker: string }[];
38
+
39
+ rows = themeRows
40
+ .map((t) => getLatestEdge(db, t.ticker))
41
+ .filter((r): r is EdgeRow => r !== null);
42
+
43
+ rows = rows.filter((r) => r.timestamp >= sinceEpoch);
44
+ } else {
45
+ // Default: all latest edges
46
+ rows = getActionableEdges(db, 'low');
47
+ rows = rows.filter((r) => r.timestamp >= sinceEpoch);
48
+ }
49
+
50
+ // Sort by |edge| descending
51
+ rows.sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge));
52
+
53
+ if (args.minEdge != null) {
54
+ rows = rows.filter((r) => Math.abs(r.edge) >= args.minEdge!);
55
+ }
56
+
57
+ return wrapSuccess('edge', rows);
58
+ }
59
+
60
+ export function formatEdgeHuman(rows: EdgeRow[]): string {
61
+ return formatEdgeTable(rows);
62
+ }