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,339 @@
1
+ import type { KalshiMarket, KalshiPosition, KalshiOrder } from '../tools/kalshi/types.js';
2
+
3
+ // ─── Box header helper ───────────────────────────────────────────────────────
4
+
5
+ const BOX_WIDTH = 40;
6
+
7
+ export function formatBoxHeader(title: string): string[] {
8
+ const inner = BOX_WIDTH - 2; // space between ║ walls
9
+ const safeTitle = title.length > inner ? title.slice(0, inner - 1) + '…' : title;
10
+ const pad = inner - safeTitle.length;
11
+ const left = Math.floor(pad / 2);
12
+ const right = pad - left;
13
+ return [
14
+ '',
15
+ '╔' + '═'.repeat(inner) + '╗',
16
+ '║' + ' '.repeat(left) + safeTitle + ' '.repeat(right) + '║',
17
+ '╚' + '═'.repeat(inner) + '╝',
18
+ ];
19
+ }
20
+
21
+ /** Actual Kalshi /portfolio/balance response shape */
22
+ export interface KalshiBalanceResponse {
23
+ balance: number;
24
+ portfolio_value: number;
25
+ updated_ts?: number;
26
+ // Legacy fields (may be present in some API versions)
27
+ payout?: number;
28
+ reserved_fees?: number;
29
+ fees?: number;
30
+ }
31
+
32
+ // ─── Value parsers ────────────────────────────────────────────────────────────
33
+ // Kalshi API returns prices as "_dollars" string fields (e.g. "0.5600")
34
+ // or as integer cents in older API versions. Handle both.
35
+
36
+ function parseDollars(val: string | number | undefined | null): number | undefined {
37
+ if (val === undefined || val === null) return undefined;
38
+ const n = typeof val === 'number' ? val : parseFloat(val as string);
39
+ return isNaN(n) ? undefined : n;
40
+ }
41
+
42
+ function parsePosition(val: string | number | undefined | null): number | undefined {
43
+ if (val === undefined || val === null) return undefined;
44
+ const n = typeof val === 'number' ? val : parseFloat(val as string);
45
+ return isNaN(n) ? undefined : n;
46
+ }
47
+
48
+ /** Format a dollar amount (already in dollars, not cents) */
49
+ function fmtDollars(val: string | number | undefined | null): string {
50
+ const n = parseDollars(val);
51
+ if (n === undefined) return '-';
52
+ return `$${n.toFixed(2)}`;
53
+ }
54
+
55
+ /** Format a price field that may be integer cents OR a dollars string */
56
+ function fmtPrice(val: number | string | undefined | null): string {
57
+ if (val === undefined || val === null) return '-';
58
+ if (typeof val === 'string') {
59
+ const n = parseFloat(val);
60
+ if (isNaN(n)) return '-';
61
+ // If the string looks like "0.5600" (dollars format), show as-is
62
+ return `$${n.toFixed(2)}`;
63
+ }
64
+ // Integer cents (old API format): divide by 100
65
+ return `$${(val / 100).toFixed(2)}`;
66
+ }
67
+
68
+ /** Format a dollar amount from cents (integer) */
69
+ function fmtCents(cents: number | undefined | null): string {
70
+ if (cents === undefined || cents === null) return '-';
71
+ return `$${(cents / 100).toFixed(2)}`;
72
+ }
73
+
74
+ /** Format a number with commas, safely handling null/undefined */
75
+ function fmtNum(n: number | string | undefined | null): string {
76
+ if (n === undefined || n === null) return '-';
77
+ const val = typeof n === 'number' ? n : parseFloat(n as string);
78
+ if (isNaN(val)) return '-';
79
+ if (val === 0) return '0';
80
+ return val.toLocaleString();
81
+ }
82
+
83
+ /** Format ISO date string as short date */
84
+ function fmtDate(iso: string | undefined): string {
85
+ if (!iso) return '-';
86
+ try {
87
+ return new Date(iso).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: '2-digit' });
88
+ } catch {
89
+ return iso.slice(0, 10);
90
+ }
91
+ }
92
+
93
+ // ─── Access helpers (handle both _dollars and raw field names) ────────────────
94
+
95
+ function mktYesAsk(m: any): string | number | undefined {
96
+ return m.yes_ask_dollars ?? m.dollar_yes_ask ?? m.yes_ask;
97
+ }
98
+ function mktNoAsk(m: any): string | number | undefined {
99
+ return m.no_ask_dollars ?? m.dollar_no_ask ?? m.no_ask;
100
+ }
101
+ function mktYesBid(m: any): string | number | undefined {
102
+ return m.yes_bid_dollars ?? m.dollar_yes_bid ?? m.yes_bid;
103
+ }
104
+ function mktNoBid(m: any): string | number | undefined {
105
+ return m.no_bid_dollars ?? m.dollar_no_bid ?? m.no_bid;
106
+ }
107
+ function mktLastPrice(m: any): string | number | undefined {
108
+ return m.last_price_dollars ?? m.dollar_last_price ?? m.last_price;
109
+ }
110
+ function mktVolume(m: any): string | number | undefined {
111
+ return m.volume_fp ?? m.volume;
112
+ }
113
+ function mktOpenInterest(m: any): string | number | undefined {
114
+ return m.open_interest_fp ?? m.open_interest;
115
+ }
116
+
117
+ // ─── Formatters ───────────────────────────────────────────────────────────────
118
+
119
+ export function formatBalance(data: KalshiBalanceResponse): string {
120
+ const lines: string[] = [];
121
+ lines.push('**Account Balance**');
122
+ lines.push('');
123
+ lines.push(`Balance: ${fmtCents(data.balance)}`);
124
+ lines.push(`Portfolio Value: ${fmtCents(data.portfolio_value ?? 0)}`);
125
+ if (data.payout !== undefined) lines.push(`Payout: ${fmtCents(data.payout)}`);
126
+ if (data.reserved_fees !== undefined) lines.push(`Reserved Fees: ${fmtCents(data.reserved_fees)}`);
127
+ if (data.fees !== undefined) lines.push(`Total Fees: ${fmtCents(data.fees)}`);
128
+ return lines.join('\n');
129
+ }
130
+
131
+ export function formatPositions(positions: any[]): string {
132
+ if (!positions.length) return 'No open positions.';
133
+
134
+ const rows = positions.map((p) => {
135
+ // position_fp is the net position (number of contracts)
136
+ const pos = parsePosition(p.position_fp ?? p.position);
137
+ const posStr = pos === undefined ? '-' : pos > 0 ? `+${pos}` : String(pos);
138
+ const pnl = p.realized_pnl_dollars ?? (p.realized_pnl !== undefined ? (p.realized_pnl / 100).toFixed(2) : undefined);
139
+ const exposure = p.market_exposure_dollars ?? (p.market_exposure !== undefined ? (p.market_exposure / 100).toFixed(2) : undefined);
140
+
141
+ return [
142
+ p.ticker,
143
+ posStr,
144
+ fmtDollars(pnl),
145
+ fmtDollars(exposure),
146
+ String(p.resting_orders_count ?? 0),
147
+ ];
148
+ });
149
+
150
+ return formatTable(
151
+ ['Ticker', 'Position', 'Realized P&L', 'Exposure', 'Orders'],
152
+ rows
153
+ );
154
+ }
155
+
156
+ export function formatOrders(orders: KalshiOrder[]): string {
157
+ if (!orders.length) return 'No orders found.';
158
+
159
+ const rows = orders.map((o) => {
160
+ const price = o.yes_price_dollars
161
+ ? fmtDollars(o.yes_price_dollars)
162
+ : o.yes_price != null ? fmtCents(o.yes_price) : '-';
163
+ const remaining = o.remaining_count_fp ?? o.remaining_count ?? '-';
164
+ const initial = o.initial_count_fp ?? o.contracts_count ?? '-';
165
+ return [
166
+ o.ticker,
167
+ `${o.action}/${o.side}`,
168
+ price,
169
+ `${remaining}/${initial}`,
170
+ o.status,
171
+ (o.order_id ?? '').slice(0, 8) + '…',
172
+ ];
173
+ });
174
+
175
+ return formatTable(
176
+ ['Ticker', 'Action/Side', 'Price', 'Remaining', 'Status', 'Order ID'],
177
+ rows
178
+ );
179
+ }
180
+
181
+ export function formatMarkets(markets: any[]): string {
182
+ if (!markets.length) return 'No markets found.';
183
+
184
+ const rows = markets.map((m) => [
185
+ m.ticker,
186
+ truncate(m.title ?? '', 40),
187
+ fmtPrice(mktYesAsk(m)),
188
+ fmtPrice(mktNoAsk(m)),
189
+ fmtNum(mktVolume(m)),
190
+ fmtDate(m.close_time),
191
+ ]);
192
+
193
+ return formatTable(
194
+ ['Ticker', 'Title', 'YES Ask', 'NO Ask', 'Volume', 'Closes'],
195
+ rows
196
+ );
197
+ }
198
+
199
+ export function formatMarketDetail(market: any): string {
200
+ const lines: string[] = [];
201
+ lines.push(`**${market.ticker}**`);
202
+ if (market.title) lines.push(market.title);
203
+ if (market.subtitle) lines.push(market.subtitle);
204
+ lines.push('');
205
+ lines.push(`Status: ${market.status ?? '-'}`);
206
+ lines.push(`YES Bid: ${fmtPrice(mktYesBid(market))} YES Ask: ${fmtPrice(mktYesAsk(market))}`);
207
+ lines.push(`NO Bid: ${fmtPrice(mktNoBid(market))} NO Ask: ${fmtPrice(mktNoAsk(market))}`);
208
+ lines.push(`Last Price: ${fmtPrice(mktLastPrice(market))}`);
209
+ lines.push(`Volume: ${fmtNum(mktVolume(market))} Open Interest: ${fmtNum(mktOpenInterest(market))}`);
210
+ lines.push(`Closes: ${fmtDate(market.close_time)}`);
211
+ if (market.result) lines.push(`Result: ${market.result}`);
212
+ return lines.join('\n');
213
+ }
214
+
215
+ export function formatExchangeStatus(data: Record<string, unknown>): string {
216
+ const active = data.exchange_active ? '✓ Exchange Active' : '✗ Exchange Inactive';
217
+ const trading = data.trading_active ? '✓ Trading Active' : '✗ Trading Paused';
218
+ return `${active}\n${trading}`;
219
+ }
220
+
221
+ export function formatOrderConfirmation(
222
+ ticker: string,
223
+ action: 'buy' | 'sell',
224
+ side: 'yes' | 'no',
225
+ count: number,
226
+ price: number | undefined
227
+ ): string {
228
+ const priceStr = price !== undefined ? `$${(price / 100).toFixed(2)}` : 'market price';
229
+ const estCost = price !== undefined ? `$${((price / 100) * count).toFixed(2)}` : 'variable';
230
+ const lines = [
231
+ '**Order Preview**',
232
+ '',
233
+ `Ticker: ${ticker}`,
234
+ `Action: ${action.toUpperCase()} ${side.toUpperCase()}`,
235
+ `Count: ${count} contract${count !== 1 ? 's' : ''}`,
236
+ `Price: ${priceStr}`,
237
+ `Est. Cost: ${estCost}`,
238
+ ];
239
+ return lines.join('\n');
240
+ }
241
+
242
+ export function formatEvents(events: any[]): string {
243
+ if (!events.length) return 'No events found.';
244
+
245
+ const rows = events.map((e) => {
246
+ const markets = e.markets ?? [];
247
+ const marketCount = markets.length > 0 ? String(markets.length) : '-';
248
+
249
+ // Find the leading outcome (highest YES price) for the top outcome column
250
+ let topOutcome = '-';
251
+ let topPct = '-';
252
+ if (markets.length > 0) {
253
+ // For multi-market events, show the frontrunner
254
+ // For binary events (1 market), show the YES probability
255
+ const sorted = [...markets].sort((a: any, b: any) => {
256
+ const volA = parseFloat(a.volume_fp ?? a.volume ?? '0') || 0;
257
+ const volB = parseFloat(b.volume_fp ?? b.volume ?? '0') || 0;
258
+ return volB - volA;
259
+ });
260
+ const top = sorted[0];
261
+ // Handle both dollar strings ("0.1800") and integer cents (18)
262
+ const rawAsk = top.yes_ask_dollars ?? top.yes_ask;
263
+ let yesAsk = 0;
264
+ if (rawAsk !== undefined && rawAsk !== null) {
265
+ const n = parseFloat(String(rawAsk));
266
+ yesAsk = !isNaN(n) ? (n > 1 ? n / 100 : n) : 0;
267
+ }
268
+ topOutcome = truncate(top.yes_sub_title || top.subtitle || top.ticker?.split('-').pop() || '', 25);
269
+ if (yesAsk > 0) topPct = `${Math.round(yesAsk * 100)}%`;
270
+ }
271
+
272
+ return [
273
+ e.event_ticker,
274
+ truncate(e.title ?? '', 35),
275
+ marketCount,
276
+ topOutcome,
277
+ topPct,
278
+ ];
279
+ });
280
+
281
+ return formatTable(
282
+ ['Ticker', 'Title', 'Mkts', 'Top Outcome', 'YES'],
283
+ rows
284
+ );
285
+ }
286
+
287
+ export function formatEventDetail(event: any): string {
288
+ const lines: string[] = [];
289
+ lines.push(`**${event.event_ticker}**`);
290
+ if (event.title) lines.push(event.title);
291
+ if (event.sub_title) lines.push(event.sub_title);
292
+ lines.push('');
293
+ lines.push(`Series: ${event.series_ticker ?? '-'}`);
294
+ lines.push(`Category: ${event.category ?? '-'}`);
295
+ lines.push(`Strike: ${fmtDate(event.strike_date)}`);
296
+ if (event.mutually_exclusive !== undefined) {
297
+ lines.push(`Mutually Exclusive: ${event.mutually_exclusive ? 'Yes' : 'No'}`);
298
+ }
299
+
300
+ const markets = event.markets ?? [];
301
+ if (markets.length > 0) {
302
+ lines.push('');
303
+ lines.push(`**Markets (${markets.length})**`);
304
+ const rows = markets.map((m: any) => [
305
+ m.ticker,
306
+ truncate(m.title ?? m.subtitle ?? '', 35),
307
+ fmtPrice(mktYesAsk(m)),
308
+ fmtPrice(mktNoAsk(m)),
309
+ fmtNum(mktVolume(m)),
310
+ ]);
311
+ lines.push(formatTable(['Ticker', 'Title', 'YES Ask', 'NO Ask', 'Volume'], rows));
312
+ }
313
+
314
+ return lines.join('\n');
315
+ }
316
+
317
+ function truncate(s: string, max: number): string {
318
+ return s.length > max ? s.slice(0, max - 1) + '…' : s;
319
+ }
320
+
321
+ function formatTable(headers: string[], rows: string[][]): string {
322
+ const colWidths = headers.map((h, i) =>
323
+ Math.max(h.length, ...rows.map((r) => (r[i] ?? '').length))
324
+ );
325
+
326
+ const pad = (s: string, w: number) => s.padEnd(w);
327
+ const sep = '─';
328
+
329
+ const topBorder = '┌' + colWidths.map((w) => sep.repeat(w + 2)).join('┬') + '┐';
330
+ const headerRow = '│' + headers.map((h, i) => ` ${pad(h, colWidths[i])} `).join('│') + '│';
331
+ const midBorder = '├' + colWidths.map((w) => sep.repeat(w + 2)).join('┼') + '┤';
332
+ const bottomBorder = '└' + colWidths.map((w) => sep.repeat(w + 2)).join('┴') + '┘';
333
+
334
+ const dataRows = rows.map(
335
+ (row) => '│' + colWidths.map((w, i) => ` ${pad(row[i] ?? '', w)} `).join('│') + '│'
336
+ );
337
+
338
+ return [topBorder, headerRow, midBorder, ...dataRows, bottomBorder].join('\n');
339
+ }
@@ -0,0 +1,263 @@
1
+ // ─── Shared help content for both TUI slash commands and CLI batch mode ─────
2
+
3
+ /** Context determines prefix style: slash commands use "/", CLI uses "kalshi" */
4
+ type HelpContext = 'slash' | 'cli';
5
+
6
+ function prefix(ctx: HelpContext): string {
7
+ return ctx === 'slash' ? '/' : 'kalshi ';
8
+ }
9
+
10
+ function buildTopics(ctx: HelpContext): Record<string, string> {
11
+ const p = prefix(ctx);
12
+ return {
13
+ search: `**${p}search** — Discovery
14
+
15
+ ${p}search [theme|ticker|query] Search events by theme, ticker, or free-text
16
+ ${p}search themes List all available themes and subcategories
17
+ ${p}search edge Scan all markets by Octagon model edge
18
+ ${p}search edge --min-edge 30 Markets with ≥30pp edge
19
+ ${p}search edge --limit 50 Top 50 results
20
+ ${p}search edge --category crypto Filter by category
21
+
22
+ Examples:
23
+ ${p}search crypto
24
+ ${p}search crypto:btc
25
+ ${p}search "bitcoin price"
26
+ ${p}search edge --min-edge 30 --category crypto`,
27
+
28
+ portfolio: `**${p}portfolio** — Account state
29
+
30
+ ${p}portfolio Full overview: positions, P&L, risk snapshot
31
+ ${p}portfolio positions Open positions with P&L
32
+ ${p}portfolio orders Resting orders
33
+ ${p}portfolio balance Account balance
34
+ ${p}portfolio status Exchange status${ctx === 'cli' ? ' and setup verification' : ''}
35
+ ${ctx === 'cli' ? `
36
+ Flags:
37
+ --performance Include win rate, Sharpe, Brier scores
38
+ --json JSON output` : ''}`,
39
+
40
+ analyze: `**${p}analyze** — Deep market analysis
41
+
42
+ ${p}analyze <ticker> Full analysis: edge, drivers, catalysts, Kelly sizing
43
+ ${p}analyze <ticker> ${ctx === 'cli' ? '--' : ''}refresh Force fresh Octagon report
44
+ ${ctx === 'cli' ? `
45
+ Legacy aliases (still work):
46
+ ${p}edge [--ticker X] Edge history / snapshots (default: last 24h)
47
+ ${p}edge --since <date> Edges since date (e.g. 2026-03-01)` : ''}`,
48
+
49
+ watch: `**${p}watch** — Live monitoring
50
+
51
+ Modes:
52
+ ${p}watch <ticker> Per-ticker price/orderbook feed (5s default)
53
+ ${p}watch --theme <theme> Continuous theme scan${ctx === 'cli' ? ' (default: every 60m)' : ' (press Esc to stop)'}
54
+ ${ctx === 'cli' ? `
55
+ Flags:
56
+ --interval <minutes> Scan interval for theme mode (min 15)
57
+ --live Force 15m interval
58
+ --json NDJSON output (one line per tick/cycle)
59
+ --dry-run Scan without persisting edges
60
+
61
+ Press Ctrl+C to stop.` : `
62
+ Per-ticker mode shows live price, bid/ask, spread, volume, and top-5 orderbook.
63
+ Theme mode runs recurring Octagon scans and displays an edge table.`}`,
64
+
65
+ buy: `**${p}buy** — Buy contracts
66
+
67
+ ${p}buy <ticker> <count> [price${ctx === 'cli' ? '_in_cents' : ''}] [yes|no]${ctx === 'slash' ? ' Buy contracts (price in cents)' : ''}
68
+
69
+ Example${ctx === 'cli' ? 's' : ''}:
70
+ ${p}buy KXBTC-26MAR14-T50049 10 ${ctx === 'cli' ? ' Buy at best ask (10 YES contracts)' : '56'}
71
+ ${p}buy KXBTC-26MAR14-T50049 10 ${ctx === 'cli' ? '56 Limit order at $0.56' : '56 no Buy NO contracts'}
72
+ ${ctx === 'cli' ? ` ${p}buy KXBTC-26MAR14-T50049 10 56 no Limit order for NO contracts at $0.56` : ''}
73
+ Side defaults to YES if omitted.`,
74
+
75
+ sell: `**${p}sell** — Sell contracts
76
+
77
+ ${p}sell <ticker> <count> [price${ctx === 'cli' ? '_in_cents' : ''}] [yes|no]${ctx === 'slash' ? ' Sell contracts (price in cents)' : ''}
78
+
79
+ Example${ctx === 'cli' ? 's' : ''}:
80
+ ${p}sell KXBTC-26MAR14-T50049 10 ${ctx === 'cli' ? ' Sell at best ask (10 YES contracts)' : '72'}
81
+ ${p}sell KXBTC-26MAR14-T50049 10 ${ctx === 'cli' ? '72 Limit order at $0.72' : '72 no Sell NO contracts'}
82
+ ${ctx === 'cli' ? ` ${p}sell KXBTC-26MAR14-T50049 10 72 no Limit order for NO contracts at $0.72` : ''}
83
+ Side defaults to YES if omitted.`,
84
+
85
+ cancel: `**${p}cancel** — Cancel a resting order
86
+
87
+ ${p}cancel <order_id>`,
88
+
89
+ backtest: `**${p}backtest** — Model accuracy scorecard & edge scanner
90
+
91
+ ${p}backtest 15-day lookback, both sections (default)
92
+ ${p}backtest --days 30 30-day lookback
93
+ ${p}backtest --max-age 14 Reject predictions older than 14 days (default = --days)
94
+ ${p}backtest --resolved Resolved markets only
95
+ ${p}backtest --unresolved Unresolved markets only
96
+ ${p}backtest --category crypto Filter by category
97
+ ${p}backtest --min-edge 10 Stricter edge threshold in pp (default 0.5pp)
98
+ ${p}backtest --min-volume 10 Per-contract volume gate (default 1)
99
+ ${p}backtest --min-price 5 --max-price 95 Tradeable price band 0-100 (defaults: 5 / 95)
100
+ ${p}backtest --export results.csv Per-market detail CSV
101
+ ${p}backtest --json Machine-readable output
102
+
103
+ Looks back N days, compares what the model said then to where the market is now.
104
+ Resolved markets: scored against Kalshi settlement (0 or 100).
105
+ Unresolved markets: mark-to-market vs current Kalshi trading price.
106
+ Per-contract entry: mp/kp come from the per-contract outcome_probabilities on the
107
+ Octagon snapshot (no event-level fallback). Volume gate uses per-contract volume
108
+ from the snapshot when available, else current Kalshi lifetime volume.
109
+ ROI is capital-weighted: sum(pnl) / sum(capital) across edge signals, where capital
110
+ is kp/100 for YES edges and (100-kp)/100 for NO edges (matches Supabase methodology).`,
111
+
112
+ 'clear-cache': `**${ctx === 'cli' ? '' : 'bun start '}clear-cache** — Delete local cache
113
+
114
+ ${ctx === 'cli' ? `${p}` : 'bun start '}clear-cache Delete the local SQLite database (~/.kalshi-bot/kalshi-bot.db)
115
+ A fresh database will be created on next command.
116
+
117
+ Use this when the local cache is corrupted or you want to start fresh.${ctx !== 'cli' ? '\nRun from terminal: bun start clear-cache' : ''}`,
118
+
119
+ init: `**${p}init** — Re-run setup wizard
120
+
121
+ ${p}init Launch the TUI with the setup wizard open
122
+ Use this to configure or reconfigure API keys and preferences.`,
123
+
124
+ help: `**${p}help** — Show help
125
+
126
+ ${p}help Show all commands
127
+ ${p}help <command> Show detailed help for a command`,
128
+ };
129
+ }
130
+
131
+ function buildOverview(ctx: HelpContext): string {
132
+ const p = prefix(ctx);
133
+ if (ctx === 'cli') {
134
+ return `**Kalshi Trading Bot CLI — CLI Commands**
135
+
136
+ Quick start:
137
+ kalshi search crypto Find markets by keyword or theme
138
+ kalshi analyze <ticker> Deep analysis + trade recommendation
139
+ kalshi watch --theme crypto Continuous scan across a theme
140
+
141
+ Discovery:
142
+ search [theme|ticker|query] Find markets by keyword or theme
143
+ search --refresh <query> Force index rebuild then search
144
+ search themes List all themes and subcategories
145
+ search edge [--min-edge N] Scan all markets by Octagon model edge
146
+ watch <ticker> Live price/orderbook feed
147
+ watch --theme <theme> Continuous theme scan (Ctrl+C to stop)
148
+ watch --refresh Force index rebuild before watching
149
+
150
+ Analysis & Trading:
151
+ analyze <ticker> Full report: edge, drivers, Kelly sizing
152
+ analyze <ticker> --refresh Force fresh Octagon report
153
+ buy <ticker> <n> [price] [yes|no] Buy contracts (price in cents)
154
+ sell <ticker> <n> [price] [yes|no] Sell contracts
155
+ cancel <order_id> Cancel a resting order
156
+
157
+ Analysis:
158
+ backtest Model accuracy scorecard + live edge scanner
159
+ backtest --resolved Resolved markets scorecard only
160
+ backtest --unresolved Live edge scanner only
161
+
162
+ Account:
163
+ portfolio Overview: positions, P&L, risk snapshot
164
+ portfolio positions Open positions
165
+ portfolio orders Resting orders
166
+ portfolio balance Account balance
167
+
168
+ System:
169
+ init Launch with setup wizard (configure API keys)
170
+ clear-cache Delete local SQLite cache and start fresh
171
+ setup Re-run setup wizard
172
+ help [command] Show help for a command
173
+
174
+ Flags: --json, --refresh, --performance, --dry-run, --verbose
175
+ Backtest flags: --days, --max-age, --resolved, --unresolved, --category, --min-edge,
176
+ --min-volume, --min-price, --max-price, --export
177
+ Run "kalshi help <command>" for detailed usage.`;
178
+ }
179
+
180
+ return `**Kalshi Trading Bot CLI — Commands**
181
+
182
+ Quick start:
183
+ /search crypto Find markets by keyword or theme
184
+ /analyze <ticker> Deep analysis + trade recommendation
185
+ /watch --theme crypto Continuous scan across a theme
186
+
187
+ Discovery:
188
+ /search [theme|ticker|query] Find markets by keyword or theme
189
+ /search --refresh <query> Force index rebuild then search
190
+ /search themes List all themes and subcategories
191
+ /search edge [--min-edge N] Scan all markets by Octagon model edge
192
+ /watch <ticker> Live price/orderbook feed
193
+ /watch --theme <theme> Continuous theme scan (Esc to stop)
194
+ /watch --refresh Force index rebuild before watching
195
+
196
+ Analysis:
197
+ /backtest Model accuracy scorecard + live edge scanner
198
+ /analyze <ticker> Full report: edge, drivers, Kelly sizing
199
+ /analyze <ticker> refresh Force fresh Octagon report
200
+ /buy <ticker> <n> [price] [yes|no] Buy contracts (price in cents)
201
+ /sell <ticker> <n> [price] [yes|no] Sell contracts
202
+ /review Review positions for close signals
203
+ /cancel <order_id> Cancel a resting order
204
+
205
+ Account:
206
+ /portfolio Overview: positions, P&L, risk snapshot
207
+ /portfolio positions Open positions
208
+ /portfolio orders Resting orders
209
+ /portfolio balance Account balance
210
+
211
+ System:
212
+ /model Change LLM model/provider
213
+ /setup Re-run setup wizard
214
+ init Launch with setup wizard (run: bun start init)
215
+ clear-cache Delete local cache (run: bun start clear-cache)
216
+ /help [command] Show help for a command
217
+ /quit Quit
218
+
219
+ Tips:
220
+ Type natural language — e.g. "analyze KXBTC", "show my portfolio"
221
+ Press Esc to cancel a running query`;
222
+ }
223
+
224
+ export function buildHelp(ctx: HelpContext, topic?: string): { text: string } | { error: string } {
225
+ const topics = buildTopics(ctx);
226
+
227
+ if (topic && topics[topic]) {
228
+ return { text: topics[topic] };
229
+ }
230
+
231
+ if (topic) {
232
+ return { error: `Unknown help topic: "${topic}". Available: ${Object.keys(topics).join(', ')}` };
233
+ }
234
+
235
+ return { text: buildOverview(ctx) };
236
+ }
237
+
238
+ /** Shared trade argument validation for both dispatch and slash handlers. */
239
+ export function validateTradeArgs(
240
+ countStr: string,
241
+ priceStr?: string,
242
+ ): { count: number; price: number | undefined } | { error: string } {
243
+ if (!/^\d+$/.test(countStr)) {
244
+ return { error: `Invalid count: ${countStr}` };
245
+ }
246
+ const count = Number(countStr);
247
+ if (count <= 0) {
248
+ return { error: `Invalid count: ${countStr}` };
249
+ }
250
+
251
+ let price: number | undefined;
252
+ if (priceStr !== undefined) {
253
+ if (!/^\d+$/.test(priceStr)) {
254
+ return { error: `Invalid price: ${priceStr}. Price must be 1-99 (cents).` };
255
+ }
256
+ price = Number(priceStr);
257
+ if (price < 1 || price > 99) {
258
+ return { error: `Invalid price: ${priceStr}. Price must be 1-99 (cents).` };
259
+ }
260
+ }
261
+
262
+ return { count, price };
263
+ }
@@ -0,0 +1,48 @@
1
+ import { callKalshiApi } from '../tools/kalshi/api.js';
2
+
3
+ /**
4
+ * Parse a market quote from API response, handling both cent and dollar-string fields.
5
+ * Returns cents (1-99) or NaN if no valid quote found.
6
+ */
7
+ function parseQuoteCents(market: Record<string, unknown>, field: 'yes_ask' | 'yes_bid' | 'no_ask' | 'no_bid'): number {
8
+ // Try dollar-string fields first (new API: yes_ask_dollars, legacy: dollar_yes_ask)
9
+ const dollarKey = `${field}_dollars`; // e.g. yes_ask_dollars
10
+ const legacyDollarKey = `dollar_${field}`; // e.g. dollar_yes_ask
11
+
12
+ const dollarStr = market[dollarKey] as string | undefined;
13
+ const legacyStr = market[legacyDollarKey] as string | undefined;
14
+
15
+ const d = dollarStr != null ? parseFloat(dollarStr) : legacyStr != null ? parseFloat(legacyStr) : NaN;
16
+ if (Number.isFinite(d) && d > 0) return Math.round(d * 100);
17
+
18
+ // Fall back to cent field
19
+ const cents = Number(market[field] ?? 0);
20
+ if (Number.isFinite(cents) && cents > 0) return cents;
21
+
22
+ return NaN;
23
+ }
24
+
25
+ /**
26
+ * Fetch the best available quote for a market order.
27
+ * Returns cents for the appropriate side/action, or an error message.
28
+ */
29
+ export async function fetchMarketQuote(
30
+ ticker: string,
31
+ action: 'buy' | 'sell',
32
+ side: 'yes' | 'no' = 'yes',
33
+ ): Promise<{ cents: number } | { error: string }> {
34
+ const marketData = await callKalshiApi('GET', `/markets/${ticker}`) as Record<string, unknown>;
35
+ const market = (marketData.market ?? marketData) as Record<string, unknown>;
36
+
37
+ const field = side === 'no'
38
+ ? (action === 'sell' ? 'no_bid' : 'no_ask')
39
+ : (action === 'sell' ? 'yes_bid' : 'yes_ask');
40
+ const cents = parseQuoteCents(market, field);
41
+
42
+ if (!Number.isFinite(cents) || cents <= 0) {
43
+ const label = action === 'sell' ? 'bid' : 'ask';
44
+ return { error: `No ${label} available for ${ticker} — cannot place market order. Specify a price.` };
45
+ }
46
+
47
+ return { cents };
48
+ }