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,199 @@
1
+ export interface KalshiBalance {
2
+ balance: number;
3
+ portfolio_value: number;
4
+ updated_ts?: number;
5
+ }
6
+
7
+ export interface KalshiMarket {
8
+ ticker: string;
9
+ event_ticker: string;
10
+ market_type: string;
11
+ title: string;
12
+ subtitle: string;
13
+ yes_sub_title: string;
14
+ no_sub_title: string;
15
+ open_time: string;
16
+ close_time: string;
17
+ expected_expiration_time: string;
18
+ expiration_time: string;
19
+ latest_expiration_time: string;
20
+ settlement_timer_seconds: number;
21
+ status: string;
22
+ response_price_units: string;
23
+ notional_value: number;
24
+ tick_size: number;
25
+ yes_bid: number;
26
+ yes_ask: number;
27
+ no_bid: number;
28
+ no_ask: number;
29
+ last_price: number;
30
+ previous_yes_bid: number;
31
+ previous_yes_ask: number;
32
+ previous_price: number;
33
+ volume: number;
34
+ volume_fp?: string;
35
+ volume_24h: number;
36
+ volume_24h_fp?: string;
37
+ liquidity: number;
38
+ open_interest: number;
39
+ result: string;
40
+ settlement_value: string;
41
+ can_close_early: boolean;
42
+ expiration_value: string;
43
+ category: string;
44
+ risk_limit_cents: number;
45
+ strike_type: string;
46
+ floor_strike: number;
47
+ cap_strike: number;
48
+ supports_fractional?: boolean;
49
+ // New API format (yes_*_dollars)
50
+ yes_bid_dollars?: string;
51
+ yes_ask_dollars?: string;
52
+ no_bid_dollars?: string;
53
+ no_ask_dollars?: string;
54
+ last_price_dollars?: string;
55
+ // Legacy API format (dollar_yes_*)
56
+ dollar_yes_bid?: string;
57
+ dollar_yes_ask?: string;
58
+ dollar_no_bid?: string;
59
+ dollar_no_ask?: string;
60
+ dollar_last_price?: string;
61
+ }
62
+
63
+ export interface KalshiEvent {
64
+ event_ticker: string;
65
+ series_ticker: string;
66
+ sub_title: string;
67
+ title: string;
68
+ mutually_exclusive: boolean;
69
+ category: string;
70
+ strike_date: string;
71
+ markets?: KalshiMarket[];
72
+ }
73
+
74
+ export interface KalshiSeries {
75
+ ticker: string;
76
+ frequency: string;
77
+ title: string;
78
+ category: string;
79
+ tags: string[];
80
+ settlement_sources: Array<{ url: string; name: string }>;
81
+ contract_url: string;
82
+ }
83
+
84
+ export interface KalshiOrder {
85
+ order_id: string;
86
+ user_id: string;
87
+ ticker: string;
88
+ client_order_id?: string;
89
+ status: string;
90
+ yes_price_dollars?: string;
91
+ no_price_dollars?: string;
92
+ /** @deprecated old API field */
93
+ yes_price?: number;
94
+ /** @deprecated old API field */
95
+ no_price?: number;
96
+ created_time: string;
97
+ expiration_time?: string | null;
98
+ action: string;
99
+ side: string;
100
+ type: string;
101
+ initial_count_fp?: string;
102
+ remaining_count_fp?: string;
103
+ fill_count_fp?: string;
104
+ /** @deprecated old API field */
105
+ contracts_count?: number;
106
+ /** @deprecated old API field */
107
+ remaining_count?: number;
108
+ maker_fees_dollars?: string;
109
+ taker_fees_dollars?: string;
110
+ maker_fill_cost_dollars?: string;
111
+ taker_fill_cost_dollars?: string;
112
+ order_group_id?: string | null;
113
+ subaccount_number?: number;
114
+ last_update_time?: string;
115
+ }
116
+
117
+ export interface KalshiPosition {
118
+ ticker: string;
119
+ event_ticker: string;
120
+ position: number;
121
+ position_fp?: number;
122
+ resting_orders_count: number;
123
+ market_exposure: number;
124
+ market_exposure_dollars?: string;
125
+ realized_pnl: number;
126
+ realized_pnl_dollars?: string;
127
+ total_traded: number;
128
+ total_traded_dollars?: string;
129
+ fees_paid: number;
130
+ fees_paid_dollars?: string;
131
+ }
132
+
133
+ export interface KalshiFill {
134
+ trade_id: string;
135
+ order_id: string;
136
+ ticker: string;
137
+ side: string;
138
+ action: string;
139
+ count: number;
140
+ yes_price: number;
141
+ no_price: number;
142
+ is_taker: boolean;
143
+ created_time: string;
144
+ }
145
+
146
+ export interface KalshiOrderbookEntry {
147
+ price: number;
148
+ delta: number;
149
+ }
150
+
151
+ export interface KalshiOrderbook {
152
+ ticker: string;
153
+ yes: KalshiOrderbookEntry[];
154
+ no: KalshiOrderbookEntry[];
155
+ }
156
+
157
+ export interface KalshiCandlestick {
158
+ ts: number;
159
+ yes_bid: { close: number; high: number; low: number; open: number };
160
+ yes_ask: { close: number; high: number; low: number; open: number };
161
+ last_price: { close: number; high: number; low: number; open: number };
162
+ volume: number;
163
+ open_interest: number;
164
+ }
165
+
166
+ export interface KalshiSettlement {
167
+ ticker: string;
168
+ settled_time: string;
169
+ market_result: string;
170
+ no_count: number;
171
+ no_total_cost: number;
172
+ yes_count: number;
173
+ yes_total_cost: number;
174
+ revenue: number;
175
+ }
176
+
177
+ export interface KalshiExchangeStatus {
178
+ exchange_active: boolean;
179
+ trading_active: boolean;
180
+ }
181
+
182
+ export interface KalshiDollarOrderRequest {
183
+ ticker: string;
184
+ action: "buy" | "sell";
185
+ side: "yes" | "no";
186
+ type: "limit" | "market";
187
+ count: number;
188
+ dollar_price?: string;
189
+ expiration_ts?: number;
190
+ client_order_id?: string;
191
+ }
192
+
193
+ export interface KalshiExchangeSchedule {
194
+ schedule: Array<{
195
+ open_time: string;
196
+ close_time: string;
197
+ maintenance_windows?: Array<{ start_time: string; end_time: string }>;
198
+ }>;
199
+ }
@@ -0,0 +1,160 @@
1
+ import { StructuredToolInterface } from '@langchain/core/tools';
2
+ import { DynamicStructuredTool } from '@langchain/core/tools';
3
+ import { z } from 'zod';
4
+ import { createKalshiSearch, KALSHI_SEARCH_DESCRIPTION } from './kalshi/kalshi-search.js';
5
+ import { createKalshiTrade, KALSHI_TRADE_DESCRIPTION } from './kalshi/kalshi-trade.js';
6
+ import { getExchangeStatus } from './kalshi/exchange.js';
7
+ import { callKalshiApi } from './kalshi/api.js';
8
+ import { tavilySearch, WEB_SEARCH_DESCRIPTION } from './search/index.js';
9
+ import { webFetchTool, WEB_FETCH_DESCRIPTION } from './fetch/web-fetch.js';
10
+ import { formatToolResult } from './types.js';
11
+ import { edgeQueryTool, EDGE_QUERY_DESCRIPTION } from './v2/edge-query.js';
12
+ import { portfolioQueryTool, PORTFOLIO_QUERY_DESCRIPTION } from './v2/portfolio-query.js';
13
+ import { riskStatusTool, RISK_STATUS_DESCRIPTION } from './v2/risk-status.js';
14
+ import { octagonReportTool, OCTAGON_REPORT_DESCRIPTION } from './v2/octagon-report.js';
15
+ import { scanTool, SCAN_DESCRIPTION } from './v2/scan.js';
16
+ import { portfolioReviewTool, PORTFOLIO_REVIEW_DESCRIPTION } from './v2/portfolio-review.js';
17
+
18
+ /**
19
+ * A registered tool with its rich description for system prompt injection.
20
+ */
21
+ export interface RegisteredTool {
22
+ /** Tool name (must match the tool's name property) */
23
+ name: string;
24
+ /** The actual tool instance */
25
+ tool: StructuredToolInterface;
26
+ /** Rich description for system prompt (includes when to use, when not to use, etc.) */
27
+ description: string;
28
+ }
29
+
30
+ // Direct portfolio overview tool (balance + positions in one call)
31
+ const portfolioOverviewTool = new DynamicStructuredTool({
32
+ name: 'portfolio_overview',
33
+ description: 'Get a quick overview of the Kalshi portfolio: balance and open positions.',
34
+ schema: z.object({}),
35
+ func: async () => {
36
+ const [balanceData, positionsData] = await Promise.all([
37
+ callKalshiApi('GET', '/portfolio/balance'),
38
+ callKalshiApi('GET', '/portfolio/positions'),
39
+ ]);
40
+ return formatToolResult({ balance: balanceData, positions: positionsData });
41
+ },
42
+ });
43
+
44
+ const PORTFOLIO_OVERVIEW_DESCRIPTION = `
45
+ Quick portfolio overview tool. Returns current account balance and all open positions in a single call.
46
+
47
+ ## When to Use
48
+ - User asks "what's my portfolio?" or "show me my balance and positions"
49
+ - Quick portfolio check before or after trading
50
+
51
+ ## When NOT to Use
52
+ - Detailed fills or order history (use kalshi_search instead)
53
+ `.trim();
54
+
55
+ const EXCHANGE_STATUS_DESCRIPTION = `
56
+ Check whether the Kalshi exchange is currently active and trading is enabled.
57
+
58
+ ## When to Use
59
+ - "Is Kalshi open?" or "Can I trade right now?"
60
+ `.trim();
61
+
62
+ /**
63
+ * Get all registered tools with their descriptions.
64
+ *
65
+ * @param model - The model name (needed for sub-agent meta-tools)
66
+ * @returns Array of registered tools
67
+ */
68
+ export function getToolRegistry(model: string): RegisteredTool[] {
69
+ const tools: RegisteredTool[] = [
70
+ {
71
+ name: 'kalshi_search',
72
+ tool: createKalshiSearch(model),
73
+ description: KALSHI_SEARCH_DESCRIPTION,
74
+ },
75
+ {
76
+ name: 'kalshi_trade',
77
+ tool: createKalshiTrade(model),
78
+ description: KALSHI_TRADE_DESCRIPTION,
79
+ },
80
+ {
81
+ name: 'portfolio_overview',
82
+ tool: portfolioOverviewTool,
83
+ description: PORTFOLIO_OVERVIEW_DESCRIPTION,
84
+ },
85
+ {
86
+ name: 'exchange_status',
87
+ tool: getExchangeStatus,
88
+ description: EXCHANGE_STATUS_DESCRIPTION,
89
+ },
90
+ {
91
+ name: 'web_fetch',
92
+ tool: webFetchTool,
93
+ description: WEB_FETCH_DESCRIPTION,
94
+ },
95
+ {
96
+ name: 'edge_query',
97
+ tool: edgeQueryTool,
98
+ description: EDGE_QUERY_DESCRIPTION,
99
+ },
100
+ {
101
+ name: 'portfolio_query',
102
+ tool: portfolioQueryTool,
103
+ description: PORTFOLIO_QUERY_DESCRIPTION,
104
+ },
105
+ {
106
+ name: 'risk_status',
107
+ tool: riskStatusTool,
108
+ description: RISK_STATUS_DESCRIPTION,
109
+ },
110
+ {
111
+ name: 'octagon_report',
112
+ tool: octagonReportTool,
113
+ description: OCTAGON_REPORT_DESCRIPTION,
114
+ },
115
+ {
116
+ name: 'scan_markets',
117
+ tool: scanTool,
118
+ description: SCAN_DESCRIPTION,
119
+ },
120
+ {
121
+ name: 'portfolio_review',
122
+ tool: portfolioReviewTool,
123
+ description: PORTFOLIO_REVIEW_DESCRIPTION,
124
+ },
125
+ ];
126
+
127
+ // Include web_search if Tavily API key is configured
128
+ if (process.env.TAVILY_API_KEY) {
129
+ tools.push({
130
+ name: 'web_search',
131
+ tool: tavilySearch,
132
+ description: WEB_SEARCH_DESCRIPTION,
133
+ });
134
+ }
135
+
136
+ return tools;
137
+ }
138
+
139
+ /**
140
+ * Get just the tool instances for binding to the LLM.
141
+ *
142
+ * @param model - The model name
143
+ * @returns Array of tool instances
144
+ */
145
+ export function getTools(model: string): StructuredToolInterface[] {
146
+ return getToolRegistry(model).map((t) => t.tool);
147
+ }
148
+
149
+ /**
150
+ * Build the tool descriptions section for the system prompt.
151
+ * Formats each tool's rich description with a header.
152
+ *
153
+ * @param model - The model name
154
+ * @returns Formatted string with all tool descriptions
155
+ */
156
+ export function buildToolDescriptions(model: string): string {
157
+ return getToolRegistry(model)
158
+ .map((t) => `### ${t.name}\n\n${t.description}`)
159
+ .join('\n\n');
160
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Rich description for the web_search tool.
3
+ */
4
+ export const WEB_SEARCH_DESCRIPTION = `
5
+ Search the web for current information on any topic. Returns relevant search results with URLs and content snippets.
6
+
7
+ ## When to Use
8
+
9
+ - Background research on real-world events behind prediction markets
10
+ - Current events, breaking news, recent developments
11
+ - Verifying claims about real-world state
12
+ - Researching topics to inform market analysis
13
+
14
+ ## When NOT to Use
15
+
16
+ - Kalshi market data (use kalshi_search instead)
17
+ - Questions you can answer from knowledge
18
+
19
+ ## Usage Notes
20
+
21
+ - Provide specific, well-formed search queries for best results
22
+ - Returns up to 5 results with URLs and content snippets
23
+ `.trim();
24
+
25
+ export { tavilySearch } from './tavily.js';
@@ -0,0 +1,35 @@
1
+ import { DynamicStructuredTool } from '@langchain/core/tools';
2
+ import { TavilySearch } from '@langchain/tavily';
3
+ import { z } from 'zod';
4
+ import { formatToolResult, parseSearchResults } from '../types.js';
5
+ import { logger } from '../../utils/logger.js';
6
+
7
+ // Lazily initialized to avoid errors when API key is not set
8
+ let tavilyClient: TavilySearch | null = null;
9
+
10
+ function getTavilyClient(): TavilySearch {
11
+ if (!tavilyClient) {
12
+ tavilyClient = new TavilySearch({ maxResults: 5 });
13
+ }
14
+ return tavilyClient;
15
+ }
16
+
17
+ export const tavilySearch = new DynamicStructuredTool({
18
+ name: 'web_search',
19
+ description:
20
+ 'Search the web for current information on any topic. Returns relevant search results with URLs and content snippets.',
21
+ schema: z.object({
22
+ query: z.string().describe('The search query to look up on the web'),
23
+ }),
24
+ func: async (input) => {
25
+ try {
26
+ const result = await getTavilyClient().invoke({ query: input.query });
27
+ const { parsed, urls } = parseSearchResults(result);
28
+ return formatToolResult(parsed, urls);
29
+ } catch (error) {
30
+ const message = error instanceof Error ? error.message : String(error);
31
+ logger.error(`[Tavily API] error: ${message}`);
32
+ throw new Error(`[Tavily API] ${message}`);
33
+ }
34
+ },
35
+ });
@@ -0,0 +1,53 @@
1
+ export interface ToolResult {
2
+ data: unknown;
3
+ sourceUrls?: string[];
4
+ }
5
+
6
+ export function formatToolResult(data: unknown, sourceUrls?: string[]): string {
7
+ const result: ToolResult = { data };
8
+ if (sourceUrls?.length) {
9
+ result.sourceUrls = sourceUrls;
10
+ }
11
+ return JSON.stringify(result);
12
+ }
13
+
14
+ /**
15
+ * Parse search results from a search provider response.
16
+ * Handles both string and object responses, extracting URLs from results.
17
+ * Supports multiple response shapes from different providers.
18
+ */
19
+ export function parseSearchResults(result: unknown): { parsed: unknown; urls: string[] } {
20
+ // Safely parse JSON strings
21
+ let parsed: unknown;
22
+ if (typeof result === 'string') {
23
+ try {
24
+ parsed = JSON.parse(result);
25
+ } catch {
26
+ // If parsing fails, treat the string as the result itself
27
+ parsed = result;
28
+ }
29
+ } else {
30
+ parsed = result;
31
+ }
32
+
33
+ // Extract URLs from multiple possible response shapes
34
+ let urls: string[] = [];
35
+
36
+ // Shape 1: { results: [{ url: string }] } (Exa format)
37
+ if (parsed && typeof parsed === 'object' && 'results' in parsed) {
38
+ const results = (parsed as { results?: unknown[] }).results;
39
+ if (Array.isArray(results)) {
40
+ urls = results
41
+ .map((r) => (r && typeof r === 'object' && 'url' in r ? (r as { url?: string }).url : null))
42
+ .filter((url): url is string => Boolean(url));
43
+ }
44
+ }
45
+ // Shape 2: [{ url: string }] (direct array, Tavily format)
46
+ else if (Array.isArray(parsed)) {
47
+ urls = parsed
48
+ .map((r) => (r && typeof r === 'object' && 'url' in r ? (r as { url?: string }).url : null))
49
+ .filter((url): url is string => Boolean(url));
50
+ }
51
+
52
+ return { parsed, urls };
53
+ }
@@ -0,0 +1,135 @@
1
+ import { DynamicStructuredTool } from '@langchain/core/tools';
2
+ import { z } from 'zod';
3
+ import { getDb } from '../../db/index.js';
4
+ import { getLatestEdge, getActionableEdges, getEdgesByExactConfidence, getEdgeHistory } from '../../db/edge.js';
5
+ import { formatToolResult } from '../types.js';
6
+
7
+ export const edgeQueryTool = new DynamicStructuredTool({
8
+ name: 'edge_query',
9
+ description: 'Query edge signals and mispricing data from the local scan database.',
10
+ schema: z.object({
11
+ ticker: z.string().optional().describe('Specific market ticker to query'),
12
+ theme: z.string().optional().describe('Theme ID to filter by'),
13
+ minConfidence: z.enum(['low', 'moderate', 'high', 'very_high']).optional().describe('Minimum confidence level (returns this level and above). Use exactConfidence instead if user wants only one level.'),
14
+ exactConfidence: z.enum(['low', 'moderate', 'high', 'very_high']).optional().describe('Exact confidence level (returns only this level, not above). Use when user says "moderate edges" or "only high confidence".'),
15
+ excludeKeywords: z.array(z.string()).optional().describe('Exclude edges whose ticker or event title contains any of these keywords (case-insensitive). Use for "skip trump", "exclude crypto", etc.'),
16
+ }),
17
+ func: async ({ ticker, theme, minConfidence, exactConfidence, excludeKeywords }) => {
18
+ const db = getDb();
19
+
20
+ if (ticker) {
21
+ const latest = getLatestEdge(db, ticker);
22
+ if (!latest) return formatToolResult({ message: `No edge data found for ${ticker}` });
23
+
24
+ const history = getEdgeHistory(db, ticker, 0).slice(-10); // last 10 entries
25
+ return formatToolResult({
26
+ latest: {
27
+ ...latest,
28
+ drivers: latest.drivers_json ? JSON.parse(latest.drivers_json) : [],
29
+ sources: latest.sources_json ? JSON.parse(latest.sources_json) : [],
30
+ catalysts: latest.catalysts_json ? JSON.parse(latest.catalysts_json) : [],
31
+ },
32
+ recentHistory: history.map((h) => ({
33
+ timestamp: h.timestamp,
34
+ edge: h.edge,
35
+ modelProb: h.model_prob,
36
+ marketProb: h.market_prob,
37
+ confidence: h.confidence,
38
+ })),
39
+ });
40
+ }
41
+
42
+ if (theme) {
43
+ const themeRows = db.query(
44
+ `SELECT DISTINCT ticker FROM edge_history
45
+ WHERE event_ticker IN (SELECT event_ticker FROM events WHERE theme_id = $theme)`
46
+ ).all({ $theme: theme }) as { ticker: string }[];
47
+
48
+ let edges = themeRows
49
+ .map((t) => getLatestEdge(db, t.ticker))
50
+ .filter((r) => r !== null)
51
+ .sort((a, b) => Math.abs(b!.edge) - Math.abs(a!.edge));
52
+
53
+ if (excludeKeywords?.length) {
54
+ edges = applyKeywordExclusion(db, edges, excludeKeywords);
55
+ }
56
+
57
+ return formatToolResult({ theme, edges, count: edges.length });
58
+ }
59
+
60
+ // Default: actionable edges
61
+ let edges = exactConfidence
62
+ ? getEdgesByExactConfidence(db, exactConfidence)
63
+ : getActionableEdges(db, minConfidence ?? 'moderate');
64
+ edges.sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge));
65
+
66
+ if (excludeKeywords?.length) {
67
+ edges = applyKeywordExclusion(db, edges, excludeKeywords);
68
+ }
69
+
70
+ const confidenceFilter = exactConfidence
71
+ ? { exactConfidence }
72
+ : { minConfidence: minConfidence ?? 'moderate' };
73
+
74
+ return formatToolResult({
75
+ edges: edges.slice(0, 20).map((e) => ({
76
+ ticker: e.ticker,
77
+ edge: e.edge,
78
+ edgePct: `${(e.edge * 100).toFixed(1)}%`,
79
+ modelProb: e.model_prob,
80
+ marketProb: e.market_prob,
81
+ confidence: e.confidence,
82
+ timestamp: e.timestamp,
83
+ })),
84
+ count: edges.length,
85
+ filter: confidenceFilter,
86
+ });
87
+ },
88
+ });
89
+
90
+ /** Filter out edges whose ticker or event title matches any excluded keyword. */
91
+ function applyKeywordExclusion<T extends { ticker: string; event_ticker: string }>(
92
+ db: ReturnType<typeof getDb>,
93
+ edges: T[],
94
+ keywords: string[],
95
+ ): T[] {
96
+ const lowerKeywords = keywords.map((k) => k.toLowerCase());
97
+
98
+ // Build a title lookup from event_index for all relevant event tickers
99
+ const eventTickers = [...new Set(edges.map((e) => e.event_ticker))];
100
+ const titleMap = new Map<string, string>();
101
+ if (eventTickers.length > 0) {
102
+ const placeholders = eventTickers.map(() => '?').join(', ');
103
+ const rows = db.query(
104
+ `SELECT event_ticker, title FROM event_index WHERE event_ticker IN (${placeholders})`
105
+ ).all(...eventTickers) as { event_ticker: string; title: string }[];
106
+ for (const row of rows) {
107
+ titleMap.set(row.event_ticker, row.title.toLowerCase());
108
+ }
109
+ }
110
+
111
+ return edges.filter((e) => {
112
+ const tickerLower = e.ticker.toLowerCase();
113
+ const titleLower = titleMap.get(e.event_ticker) ?? '';
114
+ return !lowerKeywords.some((kw) => tickerLower.includes(kw) || titleLower.includes(kw));
115
+ });
116
+ }
117
+
118
+ export const EDGE_QUERY_DESCRIPTION = `
119
+ Query edge signals and mispricing data from the local scan database.
120
+
121
+ ## When to Use
122
+ - User asks about current edges, mispricings, or opportunities
123
+ - "What's the edge on crypto?" or "Show me high-confidence edges"
124
+ - Checking if a specific market has an actionable edge signal
125
+ - Filtering edges: "show moderate edges", "skip trump", "exclude crypto"
126
+
127
+ ## When NOT to Use
128
+ - For live market data from Kalshi (use kalshi_search)
129
+ - For placing trades (use kalshi_trade)
130
+
131
+ ## Parameters
132
+ - **minConfidence**: Returns edges at this level AND above. "high" → high + very_high.
133
+ - **exactConfidence**: Returns edges at ONLY this level. "moderate" → moderate only, not high/very_high. Prefer this when the user asks for a specific level like "show moderate edges".
134
+ - **excludeKeywords**: Filters out edges whose ticker or event title contains any keyword. Use for "skip trump", "but not crypto", etc.
135
+ `.trim();