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,227 @@
1
+ import type { Database } from 'bun:sqlite';
2
+
3
+ /**
4
+ * Run all CREATE TABLE IF NOT EXISTS statements and indexes.
5
+ * Safe to call on every startup — idempotent.
6
+ */
7
+ export function migrate(db: Database): void {
8
+ db.exec(`
9
+ CREATE TABLE IF NOT EXISTS themes (
10
+ theme_id TEXT PRIMARY KEY,
11
+ name TEXT NOT NULL,
12
+ filter_query TEXT,
13
+ tickers TEXT,
14
+ last_resolved_at INTEGER
15
+ );
16
+
17
+ CREATE TABLE IF NOT EXISTS events (
18
+ ticker TEXT PRIMARY KEY,
19
+ category TEXT,
20
+ expiry INTEGER,
21
+ vol_24h REAL,
22
+ theme_id TEXT REFERENCES themes,
23
+ active INTEGER DEFAULT 1,
24
+ updated_at INTEGER
25
+ );
26
+
27
+ CREATE TABLE IF NOT EXISTS edge_history (
28
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
29
+ ticker TEXT NOT NULL,
30
+ event_ticker TEXT NOT NULL,
31
+ timestamp INTEGER NOT NULL,
32
+ model_prob REAL NOT NULL,
33
+ market_prob REAL NOT NULL,
34
+ edge REAL NOT NULL,
35
+ octagon_report_id TEXT,
36
+ drivers_json TEXT,
37
+ sources_json TEXT,
38
+ catalysts_json TEXT,
39
+ cache_hit INTEGER,
40
+ cache_miss INTEGER DEFAULT 0,
41
+ confidence TEXT,
42
+ UNIQUE(ticker, timestamp)
43
+ );
44
+
45
+ CREATE INDEX IF NOT EXISTS idx_edge_ticker_ts
46
+ ON edge_history(ticker, timestamp DESC);
47
+
48
+ CREATE INDEX IF NOT EXISTS idx_edge_confidence
49
+ ON edge_history(confidence, timestamp DESC);
50
+
51
+ CREATE TABLE IF NOT EXISTS octagon_reports (
52
+ report_id TEXT PRIMARY KEY,
53
+ ticker TEXT NOT NULL,
54
+ event_ticker TEXT NOT NULL,
55
+ model_prob REAL NOT NULL,
56
+ market_prob REAL,
57
+ mispricing_signal TEXT,
58
+ drivers_json TEXT,
59
+ catalysts_json TEXT,
60
+ sources_json TEXT,
61
+ resolution_history_json TEXT,
62
+ contract_snapshot_json TEXT,
63
+ raw_response TEXT,
64
+ model_accuracy REAL,
65
+ variant_used TEXT,
66
+ fetched_at INTEGER NOT NULL,
67
+ expires_at INTEGER NOT NULL
68
+ );
69
+
70
+ CREATE INDEX IF NOT EXISTS idx_octagon_ticker
71
+ ON octagon_reports(ticker, fetched_at DESC);
72
+
73
+ CREATE TABLE IF NOT EXISTS positions (
74
+ position_id TEXT PRIMARY KEY,
75
+ ticker TEXT NOT NULL,
76
+ event_ticker TEXT NOT NULL,
77
+ direction TEXT NOT NULL,
78
+ size REAL NOT NULL,
79
+ entry_price REAL NOT NULL,
80
+ entry_edge REAL,
81
+ entry_kelly REAL,
82
+ current_pnl REAL DEFAULT 0,
83
+ status TEXT DEFAULT 'open',
84
+ opened_at INTEGER,
85
+ closed_at INTEGER
86
+ );
87
+
88
+ CREATE TABLE IF NOT EXISTS trades (
89
+ trade_id TEXT PRIMARY KEY,
90
+ position_id TEXT REFERENCES positions,
91
+ order_id TEXT,
92
+ ticker TEXT NOT NULL,
93
+ action TEXT NOT NULL,
94
+ side TEXT NOT NULL,
95
+ size REAL NOT NULL,
96
+ price REAL NOT NULL,
97
+ fill_status TEXT,
98
+ kalshi_response TEXT,
99
+ created_at INTEGER
100
+ );
101
+
102
+ CREATE TABLE IF NOT EXISTS risk_snapshots (
103
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
104
+ timestamp INTEGER NOT NULL,
105
+ cash_balance REAL,
106
+ portfolio_value REAL,
107
+ open_exposure REAL,
108
+ available_bankroll REAL,
109
+ daily_pnl REAL,
110
+ drawdown_current REAL,
111
+ drawdown_max REAL,
112
+ correlation_max REAL,
113
+ positions_count INTEGER,
114
+ circuit_breaker_on INTEGER DEFAULT 0
115
+ );
116
+
117
+ CREATE TABLE IF NOT EXISTS alerts (
118
+ alert_id TEXT PRIMARY KEY,
119
+ ticker TEXT,
120
+ alert_type TEXT NOT NULL,
121
+ edge REAL,
122
+ message TEXT NOT NULL,
123
+ channels TEXT,
124
+ status TEXT DEFAULT 'pending',
125
+ created_at INTEGER
126
+ );
127
+
128
+ CREATE TABLE IF NOT EXISTS brier_scores (
129
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
130
+ ticker TEXT NOT NULL,
131
+ event_ticker TEXT NOT NULL,
132
+ category TEXT NOT NULL,
133
+ model_prob REAL NOT NULL,
134
+ actual_outcome INTEGER NOT NULL,
135
+ brier_score REAL NOT NULL,
136
+ settled_at INTEGER NOT NULL
137
+ );
138
+
139
+ CREATE INDEX IF NOT EXISTS idx_brier_category
140
+ ON brier_scores(category, settled_at DESC);
141
+
142
+ CREATE TABLE IF NOT EXISTS event_index (
143
+ event_ticker TEXT PRIMARY KEY,
144
+ series_ticker TEXT,
145
+ title TEXT NOT NULL,
146
+ category TEXT,
147
+ strike_date TEXT,
148
+ sub_title TEXT,
149
+ tags TEXT,
150
+ markets_json TEXT,
151
+ indexed_at INTEGER NOT NULL
152
+ );
153
+
154
+ CREATE INDEX IF NOT EXISTS idx_event_index_title
155
+ ON event_index(title);
156
+ CREATE INDEX IF NOT EXISTS idx_event_index_series
157
+ ON event_index(series_ticker);
158
+ CREATE INDEX IF NOT EXISTS idx_event_index_category
159
+ ON event_index(category);
160
+
161
+ CREATE TABLE IF NOT EXISTS event_index_meta (
162
+ key TEXT PRIMARY KEY,
163
+ value TEXT NOT NULL
164
+ );
165
+
166
+ CREATE TABLE IF NOT EXISTS octagon_history (
167
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
168
+ history_id INTEGER NOT NULL,
169
+ event_ticker TEXT NOT NULL,
170
+ captured_at TEXT NOT NULL,
171
+ model_probability REAL NOT NULL,
172
+ market_probability REAL NOT NULL,
173
+ edge_pp REAL,
174
+ confidence_score REAL,
175
+ series_category TEXT,
176
+ close_time TEXT,
177
+ name TEXT,
178
+ outcome_probabilities_json TEXT,
179
+ UNIQUE(event_ticker, history_id)
180
+ );
181
+
182
+ CREATE INDEX IF NOT EXISTS idx_history_event
183
+ ON octagon_history(event_ticker, captured_at);
184
+ `);
185
+
186
+ // Schema migrations for columns added after initial release
187
+ const edgeCols = db.query(`PRAGMA table_info(edge_history)`).all() as Array<{ name: string }>;
188
+ if (!edgeCols.some((c) => c.name === 'cache_miss')) {
189
+ db.exec(`ALTER TABLE edge_history ADD COLUMN cache_miss INTEGER DEFAULT 0`);
190
+ }
191
+
192
+ const reportCols = db.query(`PRAGMA table_info(octagon_reports)`).all() as Array<{ name: string }>;
193
+ if (!reportCols.some((c) => c.name === 'raw_response')) {
194
+ db.exec(`ALTER TABLE octagon_reports ADD COLUMN raw_response TEXT`);
195
+ }
196
+
197
+ const eventIndexCols = db.query(`PRAGMA table_info(event_index)`).all() as Array<{ name: string }>;
198
+ if (!eventIndexCols.some((c) => c.name === 'tags')) {
199
+ db.exec(`ALTER TABLE event_index ADD COLUMN tags TEXT`);
200
+ // Force re-index so tags get populated on next ensureIndex() call
201
+ db.exec(`DELETE FROM event_index_meta WHERE key = 'last_refresh'`);
202
+ }
203
+
204
+ if (!reportCols.some((c) => c.name === 'has_history')) {
205
+ db.exec(`ALTER TABLE octagon_reports ADD COLUMN has_history INTEGER DEFAULT 0`);
206
+ }
207
+ if (!reportCols.some((c) => c.name === 'mutually_exclusive')) {
208
+ db.exec(`ALTER TABLE octagon_reports ADD COLUMN mutually_exclusive INTEGER DEFAULT 0`);
209
+ }
210
+ if (!reportCols.some((c) => c.name === 'series_category')) {
211
+ db.exec(`ALTER TABLE octagon_reports ADD COLUMN series_category TEXT`);
212
+ }
213
+ if (!reportCols.some((c) => c.name === 'confidence_score')) {
214
+ db.exec(`ALTER TABLE octagon_reports ADD COLUMN confidence_score REAL`);
215
+ }
216
+ if (!reportCols.some((c) => c.name === 'outcome_probabilities_json')) {
217
+ db.exec(`ALTER TABLE octagon_reports ADD COLUMN outcome_probabilities_json TEXT`);
218
+ }
219
+ if (!reportCols.some((c) => c.name === 'close_time')) {
220
+ db.exec(`ALTER TABLE octagon_reports ADD COLUMN close_time TEXT`);
221
+ }
222
+
223
+ const historyCols = db.query(`PRAGMA table_info(octagon_history)`).all() as Array<{ name: string }>;
224
+ if (!historyCols.some((c) => c.name === 'outcome_probabilities_json')) {
225
+ db.exec(`ALTER TABLE octagon_history ADD COLUMN outcome_probabilities_json TEXT`);
226
+ }
227
+ }
@@ -0,0 +1,34 @@
1
+ import type { Database } from 'bun:sqlite';
2
+
3
+ export interface Theme {
4
+ theme_id: string;
5
+ name: string;
6
+ filter_query?: string | null;
7
+ tickers?: string | null;
8
+ last_resolved_at?: number | null;
9
+ }
10
+
11
+ export function upsertTheme(db: Database, theme: Theme): void {
12
+ db.prepare(`
13
+ INSERT OR REPLACE INTO themes (theme_id, name, filter_query, tickers, last_resolved_at)
14
+ VALUES ($theme_id, $name, $filter_query, $tickers, $last_resolved_at)
15
+ `).run({
16
+ $theme_id: theme.theme_id,
17
+ $name: theme.name,
18
+ $filter_query: theme.filter_query ?? null,
19
+ $tickers: theme.tickers ?? null,
20
+ $last_resolved_at: theme.last_resolved_at ?? null,
21
+ });
22
+ }
23
+
24
+ export function getActiveThemes(db: Database): Theme[] {
25
+ return db.query('SELECT * FROM themes').all() as Theme[];
26
+ }
27
+
28
+ export function getThemeTickers(db: Database, themeId: string): string[] {
29
+ const row = db.query('SELECT tickers FROM themes WHERE theme_id = $id').get({
30
+ $id: themeId,
31
+ }) as { tickers: string | null } | null;
32
+ if (!row?.tickers) return [];
33
+ return JSON.parse(row.tickers) as string[];
34
+ }
@@ -0,0 +1,50 @@
1
+ import type { Database } from 'bun:sqlite';
2
+
3
+ export interface Trade {
4
+ trade_id: string;
5
+ position_id?: string | null;
6
+ order_id?: string | null;
7
+ ticker: string;
8
+ action: string;
9
+ side: string;
10
+ size: number;
11
+ price: number;
12
+ fill_status?: string | null;
13
+ kalshi_response?: string | null;
14
+ created_at?: number | null;
15
+ }
16
+
17
+ export function logTrade(db: Database, trade: Trade): void {
18
+ db.prepare(`
19
+ INSERT INTO trades
20
+ (trade_id, position_id, order_id, ticker, action, side, size, price,
21
+ fill_status, kalshi_response, created_at)
22
+ VALUES
23
+ ($trade_id, $position_id, $order_id, $ticker, $action, $side, $size, $price,
24
+ $fill_status, $kalshi_response, $created_at)
25
+ `).run({
26
+ $trade_id: trade.trade_id,
27
+ $position_id: trade.position_id ?? null,
28
+ $order_id: trade.order_id ?? null,
29
+ $ticker: trade.ticker,
30
+ $action: trade.action,
31
+ $side: trade.side,
32
+ $size: trade.size,
33
+ $price: trade.price,
34
+ $fill_status: trade.fill_status ?? null,
35
+ $kalshi_response: trade.kalshi_response ?? null,
36
+ $created_at: trade.created_at ?? null,
37
+ });
38
+ }
39
+
40
+ export function getTradesForPosition(db: Database, positionId: string): Trade[] {
41
+ return db.query('SELECT * FROM trades WHERE position_id = $id').all({
42
+ $id: positionId,
43
+ }) as Trade[];
44
+ }
45
+
46
+ export function getRecentTrades(db: Database, limit: number): Trade[] {
47
+ return db.query('SELECT * FROM trades ORDER BY created_at DESC LIMIT $limit').all({
48
+ $limit: limit,
49
+ }) as Trade[];
50
+ }
@@ -0,0 +1,90 @@
1
+ import type { Database } from 'bun:sqlite';
2
+ import { auditTrail } from '../audit/index.js';
3
+
4
+ /**
5
+ * Pure Brier score: (forecast - outcome)²
6
+ */
7
+ export function computeBrier(modelProb: number, actualOutcome: 0 | 1): number {
8
+ return (modelProb - actualOutcome) ** 2;
9
+ }
10
+
11
+ export interface SettlementResult {
12
+ brierScore: number;
13
+ isUnderperforming: boolean;
14
+ }
15
+
16
+ /**
17
+ * Record a market settlement: compute Brier score from the latest edge_history
18
+ * row for the ticker, persist to brier_scores, and flag underperforming categories.
19
+ */
20
+ export function recordSettlement(
21
+ ticker: string,
22
+ actualOutcome: 0 | 1,
23
+ db: Database,
24
+ ): SettlementResult {
25
+ const edge = db.query(
26
+ 'SELECT * FROM edge_history WHERE ticker = $ticker ORDER BY timestamp DESC LIMIT 1',
27
+ ).get({ $ticker: ticker }) as {
28
+ event_ticker: string;
29
+ model_prob: number;
30
+ } | null;
31
+
32
+ if (!edge) {
33
+ throw new Error(`No edge_history found for ticker: ${ticker}`);
34
+ }
35
+
36
+ const eventTicker = edge.event_ticker;
37
+ const category = eventTicker.split('-')[0] ?? 'unknown';
38
+ const brierScore = computeBrier(edge.model_prob, actualOutcome);
39
+
40
+ db.prepare(`
41
+ INSERT INTO brier_scores (ticker, event_ticker, category, model_prob, actual_outcome, brier_score, settled_at)
42
+ VALUES ($ticker, $event_ticker, $category, $model_prob, $actual_outcome, $brier_score, $settled_at)
43
+ `).run({
44
+ $ticker: ticker,
45
+ $event_ticker: eventTicker,
46
+ $category: category,
47
+ $model_prob: edge.model_prob,
48
+ $actual_outcome: actualOutcome,
49
+ $brier_score: brierScore,
50
+ $settled_at: Date.now(),
51
+ });
52
+
53
+ const accuracy = getCategoryAccuracy(category, db);
54
+
55
+ if (accuracy.isUnderperforming) {
56
+ auditTrail.log({
57
+ type: 'CONFIG_CHANGE',
58
+ category,
59
+ avg_brier: accuracy.avgBrier,
60
+ trigger: 'auto_calibration',
61
+ recommendation: `Category "${category}" avg Brier ${accuracy.avgBrier.toFixed(3)} > 0.30 threshold — review model inputs`,
62
+ });
63
+ }
64
+
65
+ return { brierScore, isUnderperforming: accuracy.isUnderperforming };
66
+ }
67
+
68
+ export interface CategoryAccuracy {
69
+ avgBrier: number;
70
+ count: number;
71
+ isUnderperforming: boolean;
72
+ }
73
+
74
+ /**
75
+ * Aggregate Brier score stats for a category.
76
+ */
77
+ export function getCategoryAccuracy(category: string, db: Database): CategoryAccuracy {
78
+ const row = db.query(
79
+ 'SELECT AVG(brier_score) as avg_brier, COUNT(*) as count FROM brier_scores WHERE category = $category',
80
+ ).get({ $category: category }) as { avg_brier: number | null; count: number };
81
+
82
+ const avgBrier = row.avg_brier ?? 0;
83
+ const count = row.count;
84
+
85
+ return {
86
+ avgBrier,
87
+ count,
88
+ isUnderperforming: avgBrier > 0.30,
89
+ };
90
+ }
@@ -0,0 +1,4 @@
1
+ export { computeBrier, recordSettlement, getCategoryAccuracy } from './brier.js';
2
+ export type { SettlementResult, CategoryAccuracy } from './brier.js';
3
+ export { computePerformance } from './performance.js';
4
+ export type { PerformanceStats } from './performance.js';
@@ -0,0 +1,87 @@
1
+ import type { Database } from 'bun:sqlite';
2
+
3
+ export interface PerformanceStats {
4
+ sharpeRatio: number | null;
5
+ winRate: number | null;
6
+ totalPnl: number;
7
+ brierByCategory: Record<string, number>;
8
+ pnlByCategory: Record<string, number>;
9
+ underperformingCategories: string[];
10
+ }
11
+
12
+ export function computePerformance(db: Database): PerformanceStats {
13
+ // Closed positions for win rate / P&L
14
+ const closedPositions = db.query(
15
+ "SELECT * FROM positions WHERE status = 'closed'",
16
+ ).all() as Array<{ current_pnl: number | null; event_ticker: string }>;
17
+
18
+ const wins = closedPositions.filter((p) => (p.current_pnl ?? 0) > 0).length;
19
+ const winRate = closedPositions.length > 0 ? wins / closedPositions.length : null;
20
+ const totalPnl = closedPositions.reduce((sum, p) => sum + (p.current_pnl ?? 0), 0);
21
+
22
+ // P&L by category
23
+ const pnlByCategory: Record<string, number> = {};
24
+ for (const p of closedPositions) {
25
+ const cat = p.event_ticker.split('-')[0] ?? 'unknown';
26
+ pnlByCategory[cat] = (pnlByCategory[cat] ?? 0) + (p.current_pnl ?? 0);
27
+ }
28
+
29
+ // Sharpe from risk snapshots
30
+ const snapshots = db.query(
31
+ 'SELECT daily_pnl FROM risk_snapshots WHERE daily_pnl IS NOT NULL ORDER BY timestamp ASC',
32
+ ).all() as Array<{ daily_pnl: number }>;
33
+
34
+ let sharpeRatio: number | null = null;
35
+ if (snapshots.length >= 2) {
36
+ const pnls = snapshots.map((s) => s.daily_pnl);
37
+ const mean = pnls.reduce((a, b) => a + b, 0) / pnls.length;
38
+ const variance = pnls.reduce((sum, p) => sum + (p - mean) ** 2, 0) / pnls.length;
39
+ const std = Math.sqrt(variance);
40
+ if (std > 0) {
41
+ sharpeRatio = (mean / std) * Math.sqrt(252); // annualized
42
+ }
43
+ }
44
+
45
+ // Brier scores by category — prefer brier_scores table, fall back to edge_history proxy
46
+ const brierByCategory: Record<string, number> = {};
47
+ const underperformingCategories: string[] = [];
48
+
49
+ const brierRows = db.query(
50
+ 'SELECT category, AVG(brier_score) as avg_brier FROM brier_scores GROUP BY category',
51
+ ).all() as Array<{ category: string; avg_brier: number }>;
52
+
53
+ if (brierRows.length > 0) {
54
+ for (const row of brierRows) {
55
+ brierByCategory[row.category] = Math.round(row.avg_brier * 1000) / 1000;
56
+ if (row.avg_brier > 0.30) {
57
+ underperformingCategories.push(row.category);
58
+ }
59
+ }
60
+ } else {
61
+ // Fallback: derive from edge_history + positions (P&L proxy)
62
+ const resolvedEdges = db.query(`
63
+ SELECT eh.model_prob, eh.event_ticker, p.current_pnl
64
+ FROM edge_history eh
65
+ JOIN positions p ON eh.ticker = p.ticker
66
+ WHERE p.status = 'closed'
67
+ `).all() as Array<{ model_prob: number; event_ticker: string; current_pnl: number }>;
68
+
69
+ const catGroups: Record<string, Array<{ modelProb: number; outcome: number }>> = {};
70
+ for (const row of resolvedEdges) {
71
+ const cat = row.event_ticker.split('-')[0] ?? 'unknown';
72
+ if (!catGroups[cat]) catGroups[cat] = [];
73
+ const outcome = (row.current_pnl ?? 0) > 0 ? 1 : 0;
74
+ catGroups[cat].push({ modelProb: row.model_prob, outcome });
75
+ }
76
+
77
+ for (const [cat, entries] of Object.entries(catGroups)) {
78
+ const brier = entries.reduce((sum, e) => sum + (e.modelProb - e.outcome) ** 2, 0) / entries.length;
79
+ brierByCategory[cat] = Math.round(brier * 1000) / 1000;
80
+ if (brier > 0.30) {
81
+ underperformingCategories.push(cat);
82
+ }
83
+ }
84
+ }
85
+
86
+ return { sharpeRatio, winRate, totalPnl, brierByCategory, pnlByCategory, underperformingCategories };
87
+ }