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,7 @@
1
+ export { AgentRunnerController } from './agent-runner.js';
2
+ export type { RunQueryResult } from './agent-runner.js';
3
+ export { InputHistoryController } from './input-history.js';
4
+ export { ModelSelectionController } from './model-selection.js';
5
+ export type { AppState, ModelSelectionState, SelectionState } from './model-selection.js';
6
+ export { BrowseController } from './browse.js';
7
+ export type { BrowseState, BrowseAppState, BrowseEventRow, BrowseMarketRow } from './browse.js';
@@ -0,0 +1,76 @@
1
+ import { LongTermChatHistory } from '../utils/long-term-chat-history.js';
2
+
3
+ type ChangeListener = () => void;
4
+
5
+ export class InputHistoryController {
6
+ private store = new LongTermChatHistory();
7
+ private messages: string[] = [];
8
+ private historyIndex = -1;
9
+ private onChange?: ChangeListener;
10
+
11
+ constructor(onChange?: ChangeListener) {
12
+ this.onChange = onChange;
13
+ }
14
+
15
+ async init() {
16
+ await this.store.load();
17
+ this.messages = this.store.getMessageStrings();
18
+ this.emitChange();
19
+ }
20
+
21
+ setOnChange(onChange?: ChangeListener) {
22
+ this.onChange = onChange;
23
+ }
24
+
25
+ get historyValue(): string | null {
26
+ return this.historyIndex === -1 ? null : (this.messages[this.historyIndex] ?? null);
27
+ }
28
+
29
+ getMessages(): string[] {
30
+ return [...this.messages];
31
+ }
32
+
33
+ navigateUp() {
34
+ if (this.messages.length === 0) {
35
+ return;
36
+ }
37
+ const maxIndex = this.messages.length - 1;
38
+ if (this.historyIndex === -1) {
39
+ this.historyIndex = 0;
40
+ } else if (this.historyIndex < maxIndex) {
41
+ this.historyIndex += 1;
42
+ }
43
+ this.emitChange();
44
+ }
45
+
46
+ navigateDown() {
47
+ if (this.historyIndex === -1) {
48
+ return;
49
+ }
50
+ if (this.historyIndex === 0) {
51
+ this.historyIndex = -1;
52
+ } else {
53
+ this.historyIndex -= 1;
54
+ }
55
+ this.emitChange();
56
+ }
57
+
58
+ resetNavigation() {
59
+ this.historyIndex = -1;
60
+ this.emitChange();
61
+ }
62
+
63
+ async saveMessage(message: string) {
64
+ await this.store.addUserMessage(message);
65
+ this.messages = this.store.getMessageStrings();
66
+ this.emitChange();
67
+ }
68
+
69
+ async updateAgentResponse(response: string) {
70
+ await this.store.updateAgentResponse(response);
71
+ }
72
+
73
+ private emitChange() {
74
+ this.onChange?.();
75
+ }
76
+ }
@@ -0,0 +1,244 @@
1
+ import { getSetting, setSetting } from '../utils/config.js';
2
+ import { trackEvent } from '../utils/telemetry.js';
3
+ import {
4
+ checkApiKeyExistsForProvider,
5
+ getProviderDisplayName,
6
+ saveApiKeyForProvider,
7
+ } from '../utils/env.js';
8
+ import {
9
+ getDefaultModelForProvider,
10
+ getModelsForProvider,
11
+ type Model,
12
+ } from '../utils/model.js';
13
+ import { getOllamaModels } from '../utils/ollama.js';
14
+ import { DEFAULT_MODEL, DEFAULT_PROVIDER } from '../model/llm.js';
15
+ import { InMemoryChatHistory } from '../utils/in-memory-chat-history.js';
16
+
17
+ const SELECTION_STATES = [
18
+ 'provider_select',
19
+ 'model_select',
20
+ 'model_input',
21
+ 'api_key_confirm',
22
+ 'api_key_input',
23
+ ] as const;
24
+
25
+ export type SelectionState = (typeof SELECTION_STATES)[number];
26
+ export type AppState = 'idle' | SelectionState;
27
+
28
+ export interface ModelSelectionState {
29
+ appState: AppState;
30
+ pendingProvider: string | null;
31
+ pendingModels: Model[];
32
+ }
33
+
34
+ type ChangeListener = () => void;
35
+
36
+ export class ModelSelectionController {
37
+ private providerValue: string;
38
+ private modelValue: string;
39
+ private appStateValue: AppState = 'idle';
40
+ private pendingProviderValue: string | null = null;
41
+ private pendingModelsValue: Model[] = [];
42
+ private pendingSelectedModelId: string | null = null;
43
+ private readonly onError: (message: string) => void;
44
+ private readonly onChange?: ChangeListener;
45
+ private readonly chatHistory = new InMemoryChatHistory(DEFAULT_MODEL);
46
+
47
+ constructor(onError: (message: string) => void, onChange?: ChangeListener) {
48
+ this.onError = onError;
49
+ this.onChange = onChange;
50
+ this.providerValue = getSetting('provider', DEFAULT_PROVIDER);
51
+ const savedModel = getSetting('modelId', null) as string | null;
52
+ this.modelValue =
53
+ savedModel ?? getDefaultModelForProvider(this.providerValue) ?? DEFAULT_MODEL;
54
+ this.chatHistory.setModel(this.modelValue);
55
+ }
56
+
57
+ get state(): ModelSelectionState {
58
+ return {
59
+ appState: this.appStateValue,
60
+ pendingProvider: this.pendingProviderValue,
61
+ pendingModels: this.pendingModelsValue,
62
+ };
63
+ }
64
+
65
+ get provider(): string {
66
+ return this.providerValue;
67
+ }
68
+
69
+ get model(): string {
70
+ return this.modelValue;
71
+ }
72
+
73
+ get inMemoryChatHistory(): InMemoryChatHistory {
74
+ return this.chatHistory;
75
+ }
76
+
77
+ isInSelectionFlow(): boolean {
78
+ return this.appStateValue !== 'idle';
79
+ }
80
+
81
+ startSelection() {
82
+ this.appStateValue = 'provider_select';
83
+ this.emitChange();
84
+ }
85
+
86
+ cancelSelection() {
87
+ this.resetPendingState();
88
+ }
89
+
90
+ async handleProviderSelect(providerId: string | null) {
91
+ if (!providerId) {
92
+ this.appStateValue = 'idle';
93
+ this.emitChange();
94
+ return;
95
+ }
96
+
97
+ this.pendingProviderValue = providerId;
98
+ if (providerId === 'openrouter') {
99
+ this.pendingModelsValue = [];
100
+ this.appStateValue = 'model_input';
101
+ this.emitChange();
102
+ return;
103
+ }
104
+
105
+ if (providerId === 'ollama') {
106
+ const ollamaModelIds = await getOllamaModels();
107
+ this.pendingModelsValue = ollamaModelIds.map((id) => ({ id, displayName: id }));
108
+ this.appStateValue = 'model_select';
109
+ this.emitChange();
110
+ return;
111
+ }
112
+
113
+ this.pendingModelsValue = getModelsForProvider(providerId);
114
+ this.appStateValue = 'model_select';
115
+ this.emitChange();
116
+ }
117
+
118
+ handleModelSelect(modelId: string | null) {
119
+ if (!modelId || !this.pendingProviderValue) {
120
+ this.pendingProviderValue = null;
121
+ this.pendingModelsValue = [];
122
+ this.pendingSelectedModelId = null;
123
+ this.appStateValue = 'provider_select';
124
+ this.emitChange();
125
+ return;
126
+ }
127
+
128
+ if (this.pendingProviderValue === 'ollama') {
129
+ this.completeModelSwitch(this.pendingProviderValue, `ollama:${modelId}`);
130
+ return;
131
+ }
132
+
133
+ if (checkApiKeyExistsForProvider(this.pendingProviderValue)) {
134
+ this.completeModelSwitch(this.pendingProviderValue, modelId);
135
+ return;
136
+ }
137
+
138
+ this.pendingSelectedModelId = modelId;
139
+ this.appStateValue = 'api_key_confirm';
140
+ this.emitChange();
141
+ }
142
+
143
+ handleModelInputSubmit(modelName: string | null) {
144
+ if (!modelName || !this.pendingProviderValue) {
145
+ this.pendingProviderValue = null;
146
+ this.pendingModelsValue = [];
147
+ this.pendingSelectedModelId = null;
148
+ this.appStateValue = 'provider_select';
149
+ this.emitChange();
150
+ return;
151
+ }
152
+
153
+ const fullModelId = `${this.pendingProviderValue}:${modelName}`;
154
+ if (checkApiKeyExistsForProvider(this.pendingProviderValue)) {
155
+ this.completeModelSwitch(this.pendingProviderValue, fullModelId);
156
+ return;
157
+ }
158
+
159
+ this.pendingSelectedModelId = fullModelId;
160
+ this.appStateValue = 'api_key_confirm';
161
+ this.emitChange();
162
+ }
163
+
164
+ handleApiKeyConfirm(wantsToSet: boolean) {
165
+ if (wantsToSet) {
166
+ this.appStateValue = 'api_key_input';
167
+ this.emitChange();
168
+ return;
169
+ }
170
+
171
+ if (
172
+ this.pendingProviderValue &&
173
+ this.pendingSelectedModelId &&
174
+ checkApiKeyExistsForProvider(this.pendingProviderValue)
175
+ ) {
176
+ this.completeModelSwitch(this.pendingProviderValue, this.pendingSelectedModelId);
177
+ return;
178
+ }
179
+
180
+ this.onError(
181
+ `Cannot use ${
182
+ this.pendingProviderValue ? getProviderDisplayName(this.pendingProviderValue) : 'provider'
183
+ } without an API key.`,
184
+ );
185
+ this.resetPendingState();
186
+ }
187
+
188
+ handleApiKeySubmit(apiKey: string | null) {
189
+ if (!this.pendingSelectedModelId) {
190
+ this.onError('No model selected.');
191
+ this.resetPendingState();
192
+ return;
193
+ }
194
+
195
+ if (apiKey && this.pendingProviderValue) {
196
+ const saved = saveApiKeyForProvider(this.pendingProviderValue, apiKey);
197
+ if (saved) {
198
+ this.completeModelSwitch(this.pendingProviderValue, this.pendingSelectedModelId);
199
+ } else {
200
+ this.onError('Failed to save API key.');
201
+ this.resetPendingState();
202
+ }
203
+ return;
204
+ }
205
+
206
+ if (
207
+ !apiKey &&
208
+ this.pendingProviderValue &&
209
+ checkApiKeyExistsForProvider(this.pendingProviderValue)
210
+ ) {
211
+ this.completeModelSwitch(this.pendingProviderValue, this.pendingSelectedModelId);
212
+ return;
213
+ }
214
+
215
+ this.onError('API key not set. Provider unchanged.');
216
+ this.resetPendingState();
217
+ }
218
+
219
+ private completeModelSwitch(newProvider: string, newModelId: string) {
220
+ trackEvent('model_change', { provider: newProvider, model: newModelId });
221
+ this.providerValue = newProvider;
222
+ this.modelValue = newModelId;
223
+ setSetting('provider', newProvider);
224
+ setSetting('modelId', newModelId);
225
+ this.chatHistory.setModel(newModelId);
226
+ this.pendingProviderValue = null;
227
+ this.pendingModelsValue = [];
228
+ this.pendingSelectedModelId = null;
229
+ this.appStateValue = 'idle';
230
+ this.emitChange();
231
+ }
232
+
233
+ private resetPendingState() {
234
+ this.pendingProviderValue = null;
235
+ this.pendingModelsValue = [];
236
+ this.pendingSelectedModelId = null;
237
+ this.appStateValue = 'idle';
238
+ this.emitChange();
239
+ }
240
+
241
+ private emitChange() {
242
+ this.onChange?.();
243
+ }
244
+ }
@@ -0,0 +1,77 @@
1
+ import type { Database } from 'bun:sqlite';
2
+
3
+ export interface Alert {
4
+ alert_id: string;
5
+ ticker?: string | null;
6
+ alert_type: string;
7
+ edge?: number | null;
8
+ message: string;
9
+ channels?: string | null;
10
+ status?: string | null;
11
+ created_at?: number | null;
12
+ }
13
+
14
+ export function createAlert(db: Database, alert: Alert): void {
15
+ db.prepare(`
16
+ INSERT INTO alerts
17
+ (alert_id, ticker, alert_type, edge, message, channels, status, created_at)
18
+ VALUES
19
+ ($alert_id, $ticker, $alert_type, $edge, $message, $channels, $status, $created_at)
20
+ `).run({
21
+ $alert_id: alert.alert_id,
22
+ $ticker: alert.ticker ?? null,
23
+ $alert_type: alert.alert_type,
24
+ $edge: alert.edge ?? null,
25
+ $message: alert.message,
26
+ $channels: alert.channels ?? null,
27
+ $status: alert.status ?? 'pending',
28
+ $created_at: alert.created_at ?? null,
29
+ });
30
+ }
31
+
32
+ export function getPendingAlerts(db: Database): Alert[] {
33
+ return db.query("SELECT * FROM alerts WHERE status = 'pending'").all() as Alert[];
34
+ }
35
+
36
+ export function markAlertSent(db: Database, alertId: string): void {
37
+ db.prepare("UPDATE alerts SET status = 'sent' WHERE alert_id = $id").run({
38
+ $id: alertId,
39
+ });
40
+ }
41
+
42
+ export interface AlertQueryOpts {
43
+ ticker?: string;
44
+ since?: number;
45
+ status?: string;
46
+ alertType?: string;
47
+ limit?: number;
48
+ }
49
+
50
+ export function getAllAlerts(db: Database, opts?: AlertQueryOpts): Alert[] {
51
+ const conditions: string[] = [];
52
+ const params: Record<string, string | number> = {};
53
+
54
+ if (opts?.ticker) {
55
+ conditions.push('ticker = $ticker');
56
+ params.$ticker = opts.ticker;
57
+ }
58
+ if (opts?.since) {
59
+ conditions.push('created_at >= $since');
60
+ params.$since = opts.since;
61
+ }
62
+ if (opts?.status) {
63
+ conditions.push('status = $status');
64
+ params.$status = opts.status;
65
+ }
66
+ if (opts?.alertType) {
67
+ conditions.push('alert_type = $alertType');
68
+ params.$alertType = opts.alertType;
69
+ }
70
+
71
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
72
+ const limit = opts?.limit ?? 100;
73
+
74
+ return db.prepare(
75
+ `SELECT * FROM alerts ${where} ORDER BY created_at DESC LIMIT ${limit}`
76
+ ).all(params) as Alert[];
77
+ }
package/src/db/edge.ts ADDED
@@ -0,0 +1,105 @@
1
+ import type { Database } from 'bun:sqlite';
2
+
3
+ export interface EdgeRow {
4
+ id?: number;
5
+ ticker: string;
6
+ event_ticker: string;
7
+ timestamp: number;
8
+ model_prob: number;
9
+ market_prob: number;
10
+ edge: number;
11
+ octagon_report_id?: string | null;
12
+ drivers_json?: string | null;
13
+ sources_json?: string | null;
14
+ catalysts_json?: string | null;
15
+ cache_hit?: number | null;
16
+ cache_miss?: number | null;
17
+ confidence?: string | null;
18
+ }
19
+
20
+ const CONFIDENCE_RANK: Record<string, number> = {
21
+ low: 0,
22
+ moderate: 1,
23
+ high: 2,
24
+ very_high: 3,
25
+ };
26
+
27
+ export function insertEdge(db: Database, edge: EdgeRow): void {
28
+ db.prepare(`
29
+ INSERT INTO edge_history
30
+ (ticker, event_ticker, timestamp, model_prob, market_prob, edge,
31
+ octagon_report_id, drivers_json, sources_json, catalysts_json, cache_hit, cache_miss, confidence)
32
+ VALUES
33
+ ($ticker, $event_ticker, $timestamp, $model_prob, $market_prob, $edge,
34
+ $octagon_report_id, $drivers_json, $sources_json, $catalysts_json, $cache_hit, $cache_miss, $confidence)
35
+ `).run({
36
+ $ticker: edge.ticker,
37
+ $event_ticker: edge.event_ticker,
38
+ $timestamp: edge.timestamp,
39
+ $model_prob: edge.model_prob,
40
+ $market_prob: edge.market_prob,
41
+ $edge: edge.edge,
42
+ $octagon_report_id: edge.octagon_report_id ?? null,
43
+ $drivers_json: edge.drivers_json ?? null,
44
+ $sources_json: edge.sources_json ?? null,
45
+ $catalysts_json: edge.catalysts_json ?? null,
46
+ $cache_hit: edge.cache_hit ?? 0,
47
+ $cache_miss: edge.cache_miss ?? 0,
48
+ $confidence: edge.confidence ?? null,
49
+ });
50
+ }
51
+
52
+ export function getLatestEdge(db: Database, ticker: string): EdgeRow | null {
53
+ return db.query(
54
+ 'SELECT * FROM edge_history WHERE ticker = $ticker ORDER BY timestamp DESC LIMIT 1'
55
+ ).get({ $ticker: ticker }) as EdgeRow | null;
56
+ }
57
+
58
+ export function getEdgeHistory(db: Database, ticker: string, since: number): EdgeRow[] {
59
+ return db.query(
60
+ 'SELECT * FROM edge_history WHERE ticker = $ticker AND timestamp >= $since ORDER BY timestamp ASC'
61
+ ).all({ $ticker: ticker, $since: since }) as EdgeRow[];
62
+ }
63
+
64
+ /**
65
+ * Returns the latest edge per ticker where confidence >= minConfidence.
66
+ * Confidence levels: low < moderate < high < very_high
67
+ */
68
+ export function getActionableEdges(db: Database, minConfidence: string): EdgeRow[] {
69
+ const minRank = CONFIDENCE_RANK[minConfidence];
70
+ if (minRank === undefined) return [];
71
+
72
+ const allowedLevels = Object.entries(CONFIDENCE_RANK)
73
+ .filter(([, rank]) => rank >= minRank)
74
+ .map(([level]) => level);
75
+
76
+ // Get the latest edge per ticker, then filter to only those with qualifying confidence.
77
+ // This ensures we don't return stale high-confidence edges when a newer low-confidence one exists.
78
+ const placeholders = allowedLevels.map(() => '?').join(', ');
79
+ return db.query(`
80
+ SELECT e.* FROM edge_history e
81
+ INNER JOIN (
82
+ SELECT ticker, MAX(timestamp) as max_ts
83
+ FROM edge_history
84
+ GROUP BY ticker
85
+ ) latest ON e.ticker = latest.ticker AND e.timestamp = latest.max_ts
86
+ WHERE e.confidence IN (${placeholders})
87
+ ORDER BY e.timestamp DESC
88
+ `).all(...allowedLevels) as EdgeRow[];
89
+ }
90
+
91
+ /**
92
+ * Returns the latest edge per ticker where confidence is exactly the given level.
93
+ */
94
+ export function getEdgesByExactConfidence(db: Database, confidence: string): EdgeRow[] {
95
+ return db.query(`
96
+ SELECT e.* FROM edge_history e
97
+ INNER JOIN (
98
+ SELECT ticker, MAX(timestamp) as max_ts
99
+ FROM edge_history
100
+ GROUP BY ticker
101
+ ) latest ON e.ticker = latest.ticker AND e.timestamp = latest.max_ts
102
+ WHERE e.confidence = ?
103
+ ORDER BY e.timestamp DESC
104
+ `).all(confidence) as EdgeRow[];
105
+ }