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,287 @@
1
+ import { callKalshiApi } from '../tools/kalshi/api.js';
2
+ import type { KalshiOrder, KalshiPosition } from '../tools/kalshi/types.js';
3
+ import type { KalshiBalanceResponse } from './formatters.js';
4
+ import {
5
+ formatBalance,
6
+ formatPositions,
7
+ formatOrders,
8
+ formatExchangeStatus,
9
+ formatOrderConfirmation,
10
+ } from './formatters.js';
11
+ import { handleThemes, formatThemesHuman } from './themes.js';
12
+ import type { ParsedArgs, Subcommand } from './parse-args.js';
13
+
14
+ function defaultArgs(overrides: Partial<ParsedArgs>): ParsedArgs {
15
+ return {
16
+ subcommand: 'chat', positionalArgs: [], json: false,
17
+ live: false, refresh: false, report: false, dryRun: false,
18
+ verbose: false, performance: false, resolved: false,
19
+ unresolved: false, parseErrors: [],
20
+ ...overrides,
21
+ };
22
+ }
23
+ import { handleBacktest, formatBacktestHuman } from './backtest.js';
24
+ import { handleAnalyze, formatAnalyzeHuman } from './analyze.js';
25
+ import { handlePortfolio, formatPortfolioHuman } from './portfolio.js';
26
+ import { reviewPortfolio, formatReviewHuman } from './review.js';
27
+ import { buildHelp, validateTradeArgs } from './help.js';
28
+ import { fetchMarketQuote } from './helpers.js';
29
+ import { trackEvent } from '../utils/telemetry.js';
30
+
31
+ export interface CommandResult {
32
+ output: string;
33
+ /** If set, show this as a pending trade requiring approval */
34
+ pendingTrade?: {
35
+ ticker: string;
36
+ action: 'buy' | 'sell';
37
+ side: 'yes' | 'no';
38
+ count: number;
39
+ price: number | undefined;
40
+ };
41
+ /** If set, run this async function after showing `output` and append the result */
42
+ asyncFollowUp?: () => Promise<string>;
43
+ }
44
+
45
+ export async function handleSlashCommand(input: string): Promise<CommandResult | null> {
46
+ const trimmed = input.trim();
47
+ if (!trimmed.startsWith('/')) return null;
48
+
49
+ const parts = trimmed.slice(1).trim().split(/\s+/);
50
+ const command = parts[0]?.toLowerCase();
51
+ const args = parts.slice(1);
52
+ trackEvent('slash_command', { command: command ?? '' });
53
+
54
+ switch (command) {
55
+ case 'help': {
56
+ const result = buildHelp('slash', args[0]);
57
+ return { output: 'error' in result ? result.error : result.text };
58
+ }
59
+
60
+ // ─── /portfolio (with subviews) ──────────────────────────────────
61
+ case 'portfolio':
62
+ return handlePortfolioSlash(args[0]);
63
+
64
+ // Hidden aliases → /portfolio <subview>
65
+ case 'status':
66
+ return handlePortfolioSlash('status');
67
+ case 'balance':
68
+ return handlePortfolioSlash('balance');
69
+ case 'positions':
70
+ return handlePortfolioSlash('positions');
71
+ case 'orders':
72
+ return handlePortfolioSlash('orders');
73
+
74
+ // ─── Trading ─────────────────────────────────────────────────────
75
+ case 'buy':
76
+ return handleTradeCommand('buy', args);
77
+ case 'sell':
78
+ return handleTradeCommand('sell', args);
79
+ case 'cancel':
80
+ return handleCancel(args[0]);
81
+
82
+ // ─── /search themes (inline) ─────────────────────────────────────
83
+ // Note: /search <non-themes> is handled in cli.ts via browseController
84
+ case 'themes': {
85
+ const resp = await handleThemes(defaultArgs({ subcommand: 'themes' }));
86
+ return { output: formatThemesHuman(resp.data) };
87
+ }
88
+
89
+ // ─── /analyze ────────────────────────────────────────────────────
90
+ case 'analyze':
91
+ return handleAnalyzeCommand(args);
92
+
93
+ // ─── /review ─────────────────────────────────────────────────────
94
+ case 'review':
95
+ return handleReviewCommand();
96
+
97
+ // ─── /backtest ───────────────────────────────────────────────────
98
+ case 'backtest': {
99
+ // Parse backtest-specific flags from slash command args
100
+ const btArgs: Partial<ParsedArgs> = { subcommand: 'backtest' };
101
+ for (let i = 0; i < args.length; i++) {
102
+ const a = args[i];
103
+ if (a === '--resolved') btArgs.resolved = true;
104
+ else if (a === '--unresolved') btArgs.unresolved = true;
105
+ else if (a === '--category') btArgs.category = args[++i];
106
+ else if (a === '--days') { const v = Number(args[++i]); if (Number.isFinite(v) && v > 0) btArgs.days = v; }
107
+ else if (a === '--max-age') { const v = Number(args[++i]); if (Number.isFinite(v) && v > 0) btArgs.maxAge = v; }
108
+ else if (a === '--min-edge') { const v = Number(args[++i]?.replace('%', '')); if (Number.isFinite(v)) btArgs.minEdge = v / 100; }
109
+ else if (a === '--min-volume') { const v = Number(args[++i]); if (Number.isFinite(v) && v >= 0) btArgs.minVolume = v; }
110
+ else if (a === '--min-price') { const v = Number(args[++i]); if (Number.isFinite(v) && v >= 0 && v <= 100) btArgs.minPrice = v; }
111
+ else if (a === '--max-price') { const v = Number(args[++i]); if (Number.isFinite(v) && v >= 0 && v <= 100) btArgs.maxPrice = v; }
112
+ else if (a === '--export') { const v = args[++i]; if (v) btArgs.exportPath = v; }
113
+ }
114
+ const mode = btArgs.resolved ? 'resolved markets' : btArgs.unresolved ? 'open markets' : 'resolved + open markets';
115
+ const daysLabel = btArgs.days ?? 15;
116
+ return {
117
+ output: `Running ${daysLabel}-day backtest on ${mode}...`,
118
+ asyncFollowUp: async () => {
119
+ const resp = await handleBacktest(defaultArgs(btArgs));
120
+ if (!resp.ok || !resp.data) return resp.error?.message ?? 'Backtest failed';
121
+ const text = formatBacktestHuman(resp.data, { minEdge: btArgs.minEdge ?? 0.005 });
122
+ return btArgs.exportPath
123
+ ? `${text}\n\nExported per-market detail to ${btArgs.exportPath}`
124
+ : text;
125
+ },
126
+ };
127
+ }
128
+
129
+ case 'config':
130
+ // Fall through to agent — better handled by the LLM
131
+ return null;
132
+
133
+ default:
134
+ return null;
135
+ }
136
+ }
137
+
138
+ export async function executePendingTrade(trade: NonNullable<CommandResult['pendingTrade']>): Promise<string> {
139
+ let effectivePrice = trade.price;
140
+ // When no price given, fetch best quote to simulate a market order
141
+ if (effectivePrice === undefined) {
142
+ const quoteResult = await fetchMarketQuote(trade.ticker, trade.action, trade.side);
143
+ if ('error' in quoteResult) return quoteResult.error;
144
+ effectivePrice = quoteResult.cents;
145
+ }
146
+ const body: Record<string, unknown> = {
147
+ ticker: trade.ticker,
148
+ action: trade.action,
149
+ side: trade.side,
150
+ type: 'limit',
151
+ count: trade.count,
152
+ ...(trade.side === 'no'
153
+ ? { no_price: effectivePrice }
154
+ : { yes_price: effectivePrice }),
155
+ };
156
+
157
+ const data = await callKalshiApi('POST', '/portfolio/orders', { body });
158
+ const order = data.order as Record<string, unknown> | undefined;
159
+ trackEvent('trade_executed', { action: trade.action, side: trade.side, success: 'true' });
160
+ if (order) {
161
+ return `Order placed. ID: ${order.order_id} | Status: ${order.status}`;
162
+ }
163
+ return `Order submitted. Response: ${JSON.stringify(data)}`;
164
+ }
165
+
166
+ // ─── Portfolio subview handler ──────────────────────────────────────────────
167
+
168
+ async function handlePortfolioSlash(subview?: string): Promise<CommandResult> {
169
+ const view = subview?.toLowerCase() ?? 'overview';
170
+
171
+ try {
172
+ if (view === 'positions') {
173
+ const data = await callKalshiApi('GET', '/portfolio/positions');
174
+ const allPositions = (data.market_positions ?? data.positions ?? []) as KalshiPosition[];
175
+ const positions = allPositions.filter((p) => {
176
+ const pos = parseFloat(String(p.position ?? '0'));
177
+ return pos !== 0;
178
+ });
179
+ return { output: formatPositions(positions) };
180
+ }
181
+
182
+ if (view === 'orders') {
183
+ const data = await callKalshiApi('GET', '/portfolio/orders', { params: { status: 'resting' } });
184
+ const orders = (data.orders ?? []) as KalshiOrder[];
185
+ return { output: formatOrders(orders) };
186
+ }
187
+
188
+ if (view === 'balance') {
189
+ const data = await callKalshiApi('GET', '/portfolio/balance') as unknown as KalshiBalanceResponse;
190
+ return { output: formatBalance(data) };
191
+ }
192
+
193
+ if (view === 'status') {
194
+ const data = await callKalshiApi('GET', '/exchange/status');
195
+ return { output: formatExchangeStatus(data) };
196
+ }
197
+
198
+ // Default: full portfolio overview
199
+ const resp = await handlePortfolio(defaultArgs({ subcommand: 'portfolio' }));
200
+ return { output: formatPortfolioHuman(resp.data) };
201
+ } catch (err) {
202
+ return { output: `Portfolio error: ${err instanceof Error ? err.message : String(err)}` };
203
+ }
204
+ }
205
+
206
+ // ─── Analyze ────────────────────────────────────────────────────────────────
207
+
208
+ async function handleAnalyzeCommand(args: string[]): Promise<CommandResult> {
209
+ const ticker = args[0];
210
+ if (!ticker) return { output: 'Usage: /analyze <ticker> [refresh]' };
211
+ const refresh = args[1]?.toLowerCase() === 'refresh';
212
+ try {
213
+ const data = await handleAnalyze(ticker.toUpperCase(), refresh);
214
+ return { output: formatAnalyzeHuman(data) };
215
+ } catch (err) {
216
+ return { output: `Analyze failed: ${err instanceof Error ? err.message : String(err)}` };
217
+ }
218
+ }
219
+
220
+ // ─── Trade command ──────────────────────────────────────────────────────────
221
+
222
+ function parseSide(val: string | undefined): 'yes' | 'no' | null {
223
+ const v = val?.toLowerCase();
224
+ if (v === 'yes' || v === 'y') return 'yes';
225
+ if (v === 'no' || v === 'n') return 'no';
226
+ return null;
227
+ }
228
+
229
+ function handleTradeCommand(action: 'buy' | 'sell', args: string[]): CommandResult {
230
+ const [ticker, countStr, ...rest] = args;
231
+
232
+ if (!ticker || !countStr) {
233
+ return { output: `Usage: /${action} <ticker> <count> [price_in_cents] [yes|no]` };
234
+ }
235
+
236
+ // Extract side and price from remaining args: [price] [side], [side], or nothing
237
+ let side: 'yes' | 'no' = 'yes';
238
+ let priceArg: string | undefined;
239
+
240
+ if (rest.length >= 2) {
241
+ // e.g. /buy TICKER 10 50 no
242
+ priceArg = rest[0];
243
+ side = parseSide(rest[1]) ?? 'yes';
244
+ } else if (rest.length === 1) {
245
+ // Could be price or side: /buy TICKER 10 50 OR /buy TICKER 10 no
246
+ const asSide = parseSide(rest[0]);
247
+ if (asSide) {
248
+ side = asSide;
249
+ } else {
250
+ priceArg = rest[0];
251
+ }
252
+ }
253
+
254
+ const validated = validateTradeArgs(countStr, priceArg);
255
+ if ('error' in validated) {
256
+ return { output: validated.error };
257
+ }
258
+
259
+ const pendingTrade = { ticker: ticker.toUpperCase(), action, side, count: validated.count, price: validated.price };
260
+
261
+ return {
262
+ output: formatOrderConfirmation(ticker.toUpperCase(), action, side, validated.count, validated.price),
263
+ pendingTrade,
264
+ };
265
+ }
266
+
267
+ async function handleReviewCommand(): Promise<CommandResult> {
268
+ try {
269
+ const reviews = await reviewPortfolio();
270
+ return { output: formatReviewHuman(reviews) };
271
+ } catch (err) {
272
+ return { output: `Review failed: ${err instanceof Error ? err.message : String(err)}` };
273
+ }
274
+ }
275
+
276
+ async function handleCancel(orderId: string | undefined): Promise<CommandResult> {
277
+ if (!orderId) return { output: 'Usage: /cancel <order_id>' };
278
+
279
+ try {
280
+ await callKalshiApi('DELETE', `/portfolio/orders/${orderId}`);
281
+ } catch (err) {
282
+ const msg = err instanceof Error ? err.message : String(err);
283
+ const hint = msg.includes('404') ? ' (order not found or already filled)' : '';
284
+ return { output: `Cancel failed: ${msg}${hint}` };
285
+ }
286
+ return { output: `Order ${orderId} canceled.` };
287
+ }
@@ -0,0 +1,43 @@
1
+ export interface CLIResponse<T> {
2
+ ok: boolean;
3
+ command: string;
4
+ timestamp: string;
5
+ data: T;
6
+ meta?: {
7
+ scan_id?: string;
8
+ theme?: string;
9
+ events_scanned?: number;
10
+ actionable?: number;
11
+ octagon_cache_hits?: number;
12
+ octagon_fresh_reports?: number;
13
+ octagon_credits_used?: number;
14
+ bankroll?: {
15
+ cash_balance: number;
16
+ portfolio_value: number;
17
+ open_exposure: number;
18
+ available: number;
19
+ positions_count: number;
20
+ };
21
+ };
22
+ error?: { code: string; message: string };
23
+ }
24
+
25
+ export function wrapSuccess<T>(command: string, data: T, meta?: CLIResponse<T>['meta']): CLIResponse<T> {
26
+ return {
27
+ ok: true,
28
+ command,
29
+ timestamp: new Date().toISOString(),
30
+ data,
31
+ ...(meta ? { meta } : {}),
32
+ };
33
+ }
34
+
35
+ export function wrapError(command: string, code: string, message: string): CLIResponse<never> {
36
+ return {
37
+ ok: false,
38
+ command,
39
+ timestamp: new Date().toISOString(),
40
+ data: undefined as never,
41
+ error: { code, message },
42
+ };
43
+ }
@@ -0,0 +1,229 @@
1
+ const SUBCOMMANDS = [
2
+ // Core 6 commands
3
+ 'search', 'portfolio', 'analyze', 'watch',
4
+ 'buy', 'sell', 'cancel', 'help',
5
+ // Legacy aliases (kept for backward compat)
6
+ 'edge',
7
+ 'alerts', 'config', 'clear-cache', 'chat', 'init', 'status', 'themes',
8
+ // Backtest
9
+ 'backtest',
10
+ ] as const;
11
+
12
+ export type Subcommand = (typeof SUBCOMMANDS)[number];
13
+
14
+ export interface ParsedArgs {
15
+ subcommand: Subcommand;
16
+ positionalArgs: string[];
17
+ json: boolean;
18
+ theme?: string;
19
+ ticker?: string;
20
+ interval?: number;
21
+ since?: string;
22
+ minConfidence?: string;
23
+ minEdge?: number;
24
+ live: boolean;
25
+ refresh: boolean;
26
+ report: boolean;
27
+ side?: 'yes' | 'no';
28
+ dryRun: boolean;
29
+ verbose: boolean;
30
+ performance: boolean;
31
+ // Backtest-specific
32
+ resolved: boolean;
33
+ unresolved: boolean;
34
+ days?: number;
35
+ maxAge?: number;
36
+ category?: string;
37
+ limit?: number;
38
+ exportPath?: string;
39
+ minVolume?: number;
40
+ minPrice?: number;
41
+ maxPrice?: number;
42
+ parseErrors: string[];
43
+ }
44
+
45
+ export function parseArgs(argv: string[] = process.argv.slice(2)): ParsedArgs {
46
+ const positionalArgs: string[] = [];
47
+ let json = false;
48
+ let theme: string | undefined;
49
+ let ticker: string | undefined;
50
+ let interval: number | undefined;
51
+ let since: string | undefined;
52
+ let minConfidence: string | undefined;
53
+ let minEdge: number | undefined;
54
+ let live = false;
55
+ let refresh = false;
56
+ let report = false;
57
+ let side: 'yes' | 'no' | undefined;
58
+ const parseErrors: string[] = [];
59
+ let dryRun = false;
60
+ let verbose = false;
61
+ let performance = false;
62
+ let resolved = false;
63
+ let unresolved = false;
64
+ let days: number | undefined;
65
+ let category: string | undefined;
66
+ let limit: number | undefined;
67
+ let exportPath: string | undefined;
68
+ let maxAge: number | undefined;
69
+ let minVolume: number | undefined;
70
+ let minPrice: number | undefined;
71
+ let maxPrice: number | undefined;
72
+
73
+ for (let i = 0; i < argv.length; i++) {
74
+ const arg = argv[i];
75
+
76
+ if (arg === '--json') {
77
+ json = true;
78
+ } else if (arg === '--theme') {
79
+ const val = argv[++i];
80
+ if (val != null) {
81
+ theme = val;
82
+ } else {
83
+ parseErrors.push('--theme requires a value');
84
+ }
85
+ } else if (arg === '--ticker') {
86
+ const val = argv[++i];
87
+ if (val != null) {
88
+ ticker = val;
89
+ } else {
90
+ parseErrors.push('--ticker requires a value');
91
+ }
92
+ } else if (arg === '--interval') {
93
+ const raw = argv[++i];
94
+ if (raw != null) {
95
+ const numeric = Number(raw);
96
+ if (Number.isFinite(numeric) && numeric > 0) {
97
+ interval = numeric;
98
+ } else {
99
+ parseErrors.push(`Invalid --interval value: "${raw}" (expected a positive number)`);
100
+ }
101
+ } else {
102
+ parseErrors.push('--interval requires a value');
103
+ }
104
+ } else if (arg === '--since') {
105
+ const val = argv[++i];
106
+ if (val != null) {
107
+ since = val;
108
+ } else {
109
+ parseErrors.push('--since requires a value');
110
+ }
111
+ } else if (arg === '--min-confidence') {
112
+ const val = argv[++i];
113
+ if (val != null) {
114
+ minConfidence = val.toLowerCase();
115
+ } else {
116
+ parseErrors.push('--min-confidence requires a value');
117
+ }
118
+ } else if (arg === '--min-edge') {
119
+ const raw = argv[++i];
120
+ if (raw != null) {
121
+ const numeric = Number(raw.replace('%', ''));
122
+ if (Number.isFinite(numeric)) {
123
+ minEdge = numeric / 100;
124
+ } else {
125
+ parseErrors.push(`Invalid --min-edge value: "${raw}" (expected a number like 5 or 5%)`);
126
+ }
127
+ } else {
128
+ parseErrors.push('--min-edge requires a value (e.g., --min-edge 5 or --min-edge 5%)');
129
+ }
130
+ } else if (arg === '--side') {
131
+ const val = argv[++i];
132
+ if (val == null) {
133
+ parseErrors.push('--side requires a value (expected "yes" or "no")');
134
+ } else {
135
+ const lower = val.toLowerCase();
136
+ if (lower === 'yes' || lower === 'no') {
137
+ side = lower;
138
+ } else {
139
+ parseErrors.push(`Invalid --side value: "${val}" (expected "yes" or "no")`);
140
+ }
141
+ }
142
+ } else if (arg === '--live') {
143
+ live = true;
144
+ } else if (arg === '--refresh') {
145
+ refresh = true;
146
+ } else if (arg === '--report') {
147
+ report = true;
148
+ } else if (arg === '--dry-run') {
149
+ dryRun = true;
150
+ } else if (arg === '--verbose') {
151
+ verbose = true;
152
+ } else if (arg === '--performance') {
153
+ performance = true;
154
+ } else if (arg === '--resolved') {
155
+ resolved = true;
156
+ } else if (arg === '--unresolved') {
157
+ unresolved = true;
158
+ } else if (arg === '--category') {
159
+ const val = argv[++i];
160
+ if (val != null) { category = val; } else { parseErrors.push('--category requires a value'); }
161
+ } else if (arg === '--days') {
162
+ const raw = argv[++i];
163
+ if (raw != null) {
164
+ const numeric = Number(raw);
165
+ if (Number.isFinite(numeric) && numeric > 0) { days = numeric; }
166
+ else { parseErrors.push(`Invalid --days value: "${raw}" (expected a positive number)`); }
167
+ } else { parseErrors.push('--days requires a value'); }
168
+ } else if (arg === '--limit') {
169
+ const raw = argv[++i];
170
+ if (raw != null) {
171
+ const numeric = Number(raw);
172
+ if (Number.isFinite(numeric) && numeric > 0) { limit = numeric; }
173
+ else { parseErrors.push(`Invalid --limit value: "${raw}" (expected a positive number)`); }
174
+ } else { parseErrors.push('--limit requires a value'); }
175
+ } else if (arg === '--export') {
176
+ const val = argv[++i];
177
+ if (val != null) { exportPath = val; } else { parseErrors.push('--export requires a value'); }
178
+ } else if (arg === '--max-age') {
179
+ const raw = argv[++i];
180
+ if (raw != null) {
181
+ const numeric = Number(raw);
182
+ if (Number.isFinite(numeric) && numeric > 0) { maxAge = numeric; }
183
+ else { parseErrors.push(`Invalid --max-age value: "${raw}" (expected a positive number)`); }
184
+ } else { parseErrors.push('--max-age requires a value'); }
185
+ } else if (arg === '--min-volume') {
186
+ const raw = argv[++i];
187
+ if (raw != null) {
188
+ const numeric = Number(raw);
189
+ if (Number.isFinite(numeric) && numeric >= 0) { minVolume = numeric; }
190
+ else { parseErrors.push(`Invalid --min-volume value: "${raw}" (expected a non-negative number)`); }
191
+ } else { parseErrors.push('--min-volume requires a value'); }
192
+ } else if (arg === '--min-price') {
193
+ const raw = argv[++i];
194
+ if (raw != null) {
195
+ const numeric = Number(raw);
196
+ if (Number.isFinite(numeric) && numeric >= 0 && numeric <= 100) { minPrice = numeric; }
197
+ else { parseErrors.push(`Invalid --min-price value: "${raw}" (expected 0-100)`); }
198
+ } else { parseErrors.push('--min-price requires a value'); }
199
+ } else if (arg === '--max-price') {
200
+ const raw = argv[++i];
201
+ if (raw != null) {
202
+ const numeric = Number(raw);
203
+ if (Number.isFinite(numeric) && numeric >= 0 && numeric <= 100) { maxPrice = numeric; }
204
+ else { parseErrors.push(`Invalid --max-price value: "${raw}" (expected 0-100)`); }
205
+ } else { parseErrors.push('--max-price requires a value'); }
206
+ } else if (arg.startsWith('--')) {
207
+ parseErrors.push(`Unknown flag: ${arg}`);
208
+ } else {
209
+ positionalArgs.push(arg);
210
+ }
211
+ }
212
+
213
+ if (resolved && unresolved) {
214
+ parseErrors.push('Cannot use --resolved and --unresolved together');
215
+ }
216
+
217
+ const first = positionalArgs.shift();
218
+ const subcommand: Subcommand =
219
+ first && (SUBCOMMANDS as readonly string[]).includes(first)
220
+ ? (first as Subcommand)
221
+ : 'chat';
222
+
223
+ // If first arg wasn't a known subcommand, put it back as a positional
224
+ if (first && !(SUBCOMMANDS as readonly string[]).includes(first)) {
225
+ positionalArgs.unshift(first);
226
+ }
227
+
228
+ return { subcommand, positionalArgs, json, theme, ticker, interval, since, minConfidence, minEdge, side, live, refresh, report, dryRun, verbose, performance, resolved, unresolved, days, maxAge, category, limit, exportPath, minVolume, minPrice, maxPrice, parseErrors };
229
+ }