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.
- package/LICENSE +21 -0
- package/README.md +360 -0
- package/assets/kalshi-flow-light.png +0 -0
- package/assets/screenshot.png +0 -0
- package/env.example +43 -0
- package/kalshi-flow-light.png +0 -0
- package/package.json +66 -0
- package/src/agent/agent.ts +249 -0
- package/src/agent/channels.ts +53 -0
- package/src/agent/index.ts +29 -0
- package/src/agent/prompts.ts +171 -0
- package/src/agent/run-context.ts +23 -0
- package/src/agent/scratchpad.ts +465 -0
- package/src/agent/token-counter.ts +33 -0
- package/src/agent/tool-executor.ts +166 -0
- package/src/agent/types.ts +221 -0
- package/src/audit/index.ts +25 -0
- package/src/audit/reader.ts +43 -0
- package/src/audit/trail.ts +29 -0
- package/src/audit/types.ts +133 -0
- package/src/backtest/discovery.ts +170 -0
- package/src/backtest/fetcher.ts +247 -0
- package/src/backtest/metrics.ts +165 -0
- package/src/backtest/renderer.ts +196 -0
- package/src/backtest/types.ts +45 -0
- package/src/cli.ts +943 -0
- package/src/commands/alerts.ts +48 -0
- package/src/commands/analyze.ts +662 -0
- package/src/commands/backtest.ts +276 -0
- package/src/commands/clear-cache.ts +24 -0
- package/src/commands/config.ts +107 -0
- package/src/commands/dispatch.ts +473 -0
- package/src/commands/edge.ts +62 -0
- package/src/commands/formatters.ts +339 -0
- package/src/commands/help.ts +263 -0
- package/src/commands/helpers.ts +48 -0
- package/src/commands/index.ts +287 -0
- package/src/commands/json.ts +43 -0
- package/src/commands/parse-args.ts +229 -0
- package/src/commands/portfolio.ts +236 -0
- package/src/commands/review.ts +176 -0
- package/src/commands/scan-formatters.ts +98 -0
- package/src/commands/scan.ts +38 -0
- package/src/commands/search-edge.ts +139 -0
- package/src/commands/status.ts +70 -0
- package/src/commands/themes.ts +117 -0
- package/src/commands/watch.ts +295 -0
- package/src/components/answer-box.ts +57 -0
- package/src/components/approval-prompt.ts +34 -0
- package/src/components/browse-list.ts +134 -0
- package/src/components/chat-log.ts +291 -0
- package/src/components/custom-editor.ts +18 -0
- package/src/components/debug-panel.ts +52 -0
- package/src/components/index.ts +17 -0
- package/src/components/intro.ts +92 -0
- package/src/components/select-list.ts +155 -0
- package/src/components/tool-event.ts +127 -0
- package/src/components/user-query.ts +18 -0
- package/src/components/working-indicator.ts +87 -0
- package/src/controllers/agent-runner.ts +283 -0
- package/src/controllers/browse.ts +1013 -0
- package/src/controllers/index.ts +7 -0
- package/src/controllers/input-history.ts +76 -0
- package/src/controllers/model-selection.ts +244 -0
- package/src/db/alerts.ts +77 -0
- package/src/db/edge.ts +105 -0
- package/src/db/event-index.ts +323 -0
- package/src/db/events.ts +41 -0
- package/src/db/index.ts +60 -0
- package/src/db/octagon-cache.ts +118 -0
- package/src/db/positions.ts +71 -0
- package/src/db/risk.ts +51 -0
- package/src/db/schema.ts +227 -0
- package/src/db/themes.ts +34 -0
- package/src/db/trades.ts +50 -0
- package/src/eval/brier.ts +90 -0
- package/src/eval/index.ts +4 -0
- package/src/eval/performance.ts +87 -0
- package/src/gateway/access-control.ts +253 -0
- package/src/gateway/agent-runner.ts +75 -0
- package/src/gateway/alerts/formatter.ts +90 -0
- package/src/gateway/alerts/index.ts +4 -0
- package/src/gateway/alerts/router.ts +32 -0
- package/src/gateway/alerts/terminal.ts +16 -0
- package/src/gateway/alerts/types.ts +13 -0
- package/src/gateway/channels/index.ts +9 -0
- package/src/gateway/channels/manager.ts +153 -0
- package/src/gateway/channels/types.ts +48 -0
- package/src/gateway/channels/whatsapp/README.md +234 -0
- package/src/gateway/channels/whatsapp/auth-store.ts +140 -0
- package/src/gateway/channels/whatsapp/dedupe.ts +60 -0
- package/src/gateway/channels/whatsapp/error.ts +122 -0
- package/src/gateway/channels/whatsapp/inbound.ts +326 -0
- package/src/gateway/channels/whatsapp/index.ts +5 -0
- package/src/gateway/channels/whatsapp/lid.ts +56 -0
- package/src/gateway/channels/whatsapp/logger.ts +25 -0
- package/src/gateway/channels/whatsapp/login.ts +94 -0
- package/src/gateway/channels/whatsapp/outbound.ts +119 -0
- package/src/gateway/channels/whatsapp/plugin.ts +54 -0
- package/src/gateway/channels/whatsapp/reconnect.ts +40 -0
- package/src/gateway/channels/whatsapp/runtime.ts +122 -0
- package/src/gateway/channels/whatsapp/session.ts +89 -0
- package/src/gateway/channels/whatsapp/types.ts +32 -0
- package/src/gateway/commands/handler.ts +64 -0
- package/src/gateway/commands/index.ts +7 -0
- package/src/gateway/commands/parser.ts +29 -0
- package/src/gateway/commands/wa-formatters.ts +92 -0
- package/src/gateway/config.ts +244 -0
- package/src/gateway/extension-points.ts +17 -0
- package/src/gateway/gateway.ts +301 -0
- package/src/gateway/group/history-buffer.ts +75 -0
- package/src/gateway/group/index.ts +8 -0
- package/src/gateway/group/member-tracker.ts +60 -0
- package/src/gateway/group/mention-detection.ts +42 -0
- package/src/gateway/heartbeat/index.ts +8 -0
- package/src/gateway/heartbeat/prompt.ts +73 -0
- package/src/gateway/heartbeat/runner.ts +200 -0
- package/src/gateway/heartbeat/suppression.ts +74 -0
- package/src/gateway/index.ts +138 -0
- package/src/gateway/routing/resolve-route.ts +119 -0
- package/src/gateway/sessions/store.ts +65 -0
- package/src/gateway/types.ts +11 -0
- package/src/gateway/utils.ts +82 -0
- package/src/index.tsx +30 -0
- package/src/model/llm.ts +247 -0
- package/src/providers.ts +94 -0
- package/src/risk/circuit-breaker.ts +113 -0
- package/src/risk/correlation.ts +40 -0
- package/src/risk/gate.ts +125 -0
- package/src/risk/index.ts +10 -0
- package/src/risk/kelly.ts +230 -0
- package/src/scan/alerter.ts +64 -0
- package/src/scan/edge-computer.ts +164 -0
- package/src/scan/invoker.ts +199 -0
- package/src/scan/loop.ts +184 -0
- package/src/scan/octagon-client.ts +627 -0
- package/src/scan/octagon-events-api.ts +105 -0
- package/src/scan/octagon-prefetch.ts +172 -0
- package/src/scan/theme-resolver.ts +179 -0
- package/src/scan/types.ts +62 -0
- package/src/scan/watchdog.ts +126 -0
- package/src/setup/wizard.ts +659 -0
- package/src/theme.ts +67 -0
- package/src/tools/fetch/cache.ts +95 -0
- package/src/tools/fetch/external-content.ts +200 -0
- package/src/tools/fetch/index.ts +1 -0
- package/src/tools/fetch/web-fetch-utils.ts +122 -0
- package/src/tools/fetch/web-fetch.ts +419 -0
- package/src/tools/index.ts +10 -0
- package/src/tools/kalshi/api.ts +251 -0
- package/src/tools/kalshi/dlq.ts +35 -0
- package/src/tools/kalshi/events.ts +84 -0
- package/src/tools/kalshi/exchange.ts +24 -0
- package/src/tools/kalshi/historical.ts +89 -0
- package/src/tools/kalshi/index.ts +11 -0
- package/src/tools/kalshi/kalshi-search.ts +437 -0
- package/src/tools/kalshi/kalshi-trade.ts +102 -0
- package/src/tools/kalshi/markets.ts +76 -0
- package/src/tools/kalshi/portfolio.ts +100 -0
- package/src/tools/kalshi/search-index.ts +198 -0
- package/src/tools/kalshi/series.ts +16 -0
- package/src/tools/kalshi/trading.ts +115 -0
- package/src/tools/kalshi/types.ts +199 -0
- package/src/tools/registry.ts +160 -0
- package/src/tools/search/index.ts +25 -0
- package/src/tools/search/tavily.ts +35 -0
- package/src/tools/types.ts +53 -0
- package/src/tools/v2/edge-query.ts +135 -0
- package/src/tools/v2/octagon-report.ts +112 -0
- package/src/tools/v2/portfolio-query.ts +79 -0
- package/src/tools/v2/portfolio-review.ts +59 -0
- package/src/tools/v2/risk-status.ts +94 -0
- package/src/tools/v2/scan.ts +78 -0
- package/src/types/qrcode-terminal.d.ts +7 -0
- package/src/types/whiskeysockets-baileys.d.ts +41 -0
- package/src/types.ts +22 -0
- package/src/utils/ai-message.ts +26 -0
- package/src/utils/bot-config.ts +219 -0
- package/src/utils/cache.ts +195 -0
- package/src/utils/config.ts +113 -0
- package/src/utils/env.ts +111 -0
- package/src/utils/errors.ts +313 -0
- package/src/utils/history-context.ts +32 -0
- package/src/utils/in-memory-chat-history.ts +268 -0
- package/src/utils/index.ts +28 -0
- package/src/utils/input-key-handlers.ts +64 -0
- package/src/utils/logger.ts +67 -0
- package/src/utils/long-term-chat-history.ts +138 -0
- package/src/utils/markdown-table.ts +227 -0
- package/src/utils/model.ts +70 -0
- package/src/utils/ollama.ts +37 -0
- package/src/utils/paths.ts +12 -0
- package/src/utils/progress-channel.ts +84 -0
- package/src/utils/telemetry.ts +103 -0
- package/src/utils/text-navigation.ts +81 -0
- package/src/utils/thinking-verbs.ts +18 -0
- package/src/utils/tokens.ts +36 -0
- package/src/utils/tool-description.ts +61 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Channel Profiles
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Per-channel formatting profile that controls how the agent responds.
|
|
7
|
+
* Add new entries to CHANNEL_PROFILES in prompts.ts when adding channels.
|
|
8
|
+
*/
|
|
9
|
+
export interface ChannelProfile {
|
|
10
|
+
/** Human-readable label used in the system prompt preamble (e.g., "CLI", "WhatsApp") */
|
|
11
|
+
label: string;
|
|
12
|
+
/** One-liner describing the output surface, injected after the date line */
|
|
13
|
+
preamble: string;
|
|
14
|
+
/** Bullet points for the ## Behavior section */
|
|
15
|
+
behavior: string[];
|
|
16
|
+
/** Bullet points for the ## Response Format section */
|
|
17
|
+
responseFormat: string[];
|
|
18
|
+
/** Full tables instruction block, or null to omit the section entirely */
|
|
19
|
+
tables: string | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Approval
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* User's response to a tool approval prompt.
|
|
28
|
+
* - 'allow-once': approve this single invocation
|
|
29
|
+
* - 'allow-session': approve all invocations of this tool for the rest of the session
|
|
30
|
+
* - 'deny': reject and immediately end the agent's turn
|
|
31
|
+
*/
|
|
32
|
+
export type ApprovalDecision = 'allow-once' | 'allow-session' | 'deny';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Agent configuration
|
|
36
|
+
*/
|
|
37
|
+
export interface AgentConfig {
|
|
38
|
+
/** Model to use for LLM calls (e.g., 'gpt-5.4', 'claude-sonnet-4-20250514') */
|
|
39
|
+
model?: string;
|
|
40
|
+
/** Model provider (e.g., 'openai', 'anthropic', 'google', 'ollama') */
|
|
41
|
+
modelProvider?: string;
|
|
42
|
+
/** Maximum agent loop iterations (default: 10) */
|
|
43
|
+
maxIterations?: number;
|
|
44
|
+
/** AbortSignal for cancelling agent execution */
|
|
45
|
+
signal?: AbortSignal;
|
|
46
|
+
/** Delivery channel (e.g., 'cli') — affects response formatting */
|
|
47
|
+
channel?: string;
|
|
48
|
+
/** Group chat context (e.g., WhatsApp groups) */
|
|
49
|
+
groupContext?: import('./prompts.js').GroupContext;
|
|
50
|
+
/** Called when a tool needs explicit user approval to proceed */
|
|
51
|
+
requestToolApproval?: (request: { tool: string; args: Record<string, unknown> }) => Promise<ApprovalDecision>;
|
|
52
|
+
/** Shared set of tool names that have been session-approved (persists across queries) */
|
|
53
|
+
sessionApprovedTools?: Set<string>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Message in conversation history
|
|
58
|
+
*/
|
|
59
|
+
export interface Message {
|
|
60
|
+
role: 'user' | 'assistant' | 'tool';
|
|
61
|
+
content: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// Agent Events (for real-time streaming UI)
|
|
66
|
+
// ============================================================================
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Agent is processing/thinking
|
|
70
|
+
*/
|
|
71
|
+
export interface ThinkingEvent {
|
|
72
|
+
type: 'thinking';
|
|
73
|
+
message: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Tool execution started
|
|
78
|
+
*/
|
|
79
|
+
export interface ToolStartEvent {
|
|
80
|
+
type: 'tool_start';
|
|
81
|
+
tool: string;
|
|
82
|
+
args: Record<string, unknown>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Tool execution completed successfully
|
|
87
|
+
*/
|
|
88
|
+
export interface ToolEndEvent {
|
|
89
|
+
type: 'tool_end';
|
|
90
|
+
tool: string;
|
|
91
|
+
args: Record<string, unknown>;
|
|
92
|
+
result: string;
|
|
93
|
+
duration: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Tool execution failed
|
|
98
|
+
*/
|
|
99
|
+
export interface ToolErrorEvent {
|
|
100
|
+
type: 'tool_error';
|
|
101
|
+
tool: string;
|
|
102
|
+
error: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Mid-execution progress update from a subagent tool
|
|
107
|
+
*/
|
|
108
|
+
export interface ToolProgressEvent {
|
|
109
|
+
type: 'tool_progress';
|
|
110
|
+
tool: string;
|
|
111
|
+
message: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Tool call warning due to approaching/exceeding suggested limits
|
|
116
|
+
*/
|
|
117
|
+
export interface ToolLimitEvent {
|
|
118
|
+
type: 'tool_limit';
|
|
119
|
+
tool: string;
|
|
120
|
+
/** Warning message about tool usage limits */
|
|
121
|
+
warning?: string;
|
|
122
|
+
/** Whether the tool call was blocked (always false - we only warn, never block) */
|
|
123
|
+
blocked: boolean;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Tool approval decision event for sensitive tools.
|
|
128
|
+
*/
|
|
129
|
+
export interface ToolApprovalEvent {
|
|
130
|
+
type: 'tool_approval';
|
|
131
|
+
tool: string;
|
|
132
|
+
args: Record<string, unknown>;
|
|
133
|
+
approved: ApprovalDecision;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Tool execution was denied by user approval flow.
|
|
138
|
+
*/
|
|
139
|
+
export interface ToolDeniedEvent {
|
|
140
|
+
type: 'tool_denied';
|
|
141
|
+
tool: string;
|
|
142
|
+
args: Record<string, unknown>;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Context was cleared due to exceeding token threshold (Anthropic-style)
|
|
147
|
+
*/
|
|
148
|
+
export interface ContextClearedEvent {
|
|
149
|
+
type: 'context_cleared';
|
|
150
|
+
/** Number of tool results that were cleared from context */
|
|
151
|
+
clearedCount: number;
|
|
152
|
+
/** Number of most recent tool results that were kept */
|
|
153
|
+
keptCount: number;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Session-start memory context was loaded into the system prompt.
|
|
158
|
+
*/
|
|
159
|
+
export interface MemoryRecalledEvent {
|
|
160
|
+
type: 'memory_recalled';
|
|
161
|
+
filesLoaded: string[];
|
|
162
|
+
tokenCount: number;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Pre-compaction memory flush lifecycle event.
|
|
167
|
+
*/
|
|
168
|
+
export interface MemoryFlushEvent {
|
|
169
|
+
type: 'memory_flush';
|
|
170
|
+
phase: 'start' | 'end';
|
|
171
|
+
filesWritten?: string[];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Token usage statistics
|
|
176
|
+
*/
|
|
177
|
+
export interface TokenUsage {
|
|
178
|
+
inputTokens: number;
|
|
179
|
+
outputTokens: number;
|
|
180
|
+
totalTokens: number;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Agent completed with final result
|
|
185
|
+
*/
|
|
186
|
+
export interface DoneEvent {
|
|
187
|
+
type: 'done';
|
|
188
|
+
answer: string;
|
|
189
|
+
toolCalls: Array<{ tool: string; args: Record<string, unknown>; result: string }>;
|
|
190
|
+
iterations: number;
|
|
191
|
+
totalTime: number;
|
|
192
|
+
tokenUsage?: TokenUsage;
|
|
193
|
+
tokensPerSecond?: number;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Union type for all agent events
|
|
198
|
+
*/
|
|
199
|
+
export type AgentEvent =
|
|
200
|
+
| ThinkingEvent
|
|
201
|
+
| ToolStartEvent
|
|
202
|
+
| ToolProgressEvent
|
|
203
|
+
| ToolEndEvent
|
|
204
|
+
| ToolErrorEvent
|
|
205
|
+
| ToolApprovalEvent
|
|
206
|
+
| ToolDeniedEvent
|
|
207
|
+
| ToolLimitEvent
|
|
208
|
+
| ContextClearedEvent
|
|
209
|
+
| DoneEvent;
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Aggregated event used by the CLI history renderer.
|
|
213
|
+
* Combines lifecycle events (tool_start/tool_end/tool_error) into a single display row.
|
|
214
|
+
*/
|
|
215
|
+
export interface DisplayEvent {
|
|
216
|
+
id: string;
|
|
217
|
+
event: AgentEvent;
|
|
218
|
+
completed?: boolean;
|
|
219
|
+
endEvent?: AgentEvent;
|
|
220
|
+
progressMessage?: string;
|
|
221
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export { AuditTrail } from './trail.js';
|
|
2
|
+
export { readAuditLog } from './reader.js';
|
|
3
|
+
export type { ReadAuditLogOpts } from './reader.js';
|
|
4
|
+
export type {
|
|
5
|
+
AuditEvent,
|
|
6
|
+
AuditEventType,
|
|
7
|
+
AuditBase,
|
|
8
|
+
DistributiveOmit,
|
|
9
|
+
ScanStartEvent,
|
|
10
|
+
ScanCompleteEvent,
|
|
11
|
+
OctagonCallEvent,
|
|
12
|
+
EdgeDetectedEvent,
|
|
13
|
+
RecommendationEvent,
|
|
14
|
+
TradeExecutedEvent,
|
|
15
|
+
AlertSentEvent,
|
|
16
|
+
WatchdogCheckEvent,
|
|
17
|
+
ApiRetryEvent,
|
|
18
|
+
DlqEntryEvent,
|
|
19
|
+
ConfigChangeEvent,
|
|
20
|
+
ConfigSetEvent,
|
|
21
|
+
} from './types.js';
|
|
22
|
+
|
|
23
|
+
import { AuditTrail } from './trail.js';
|
|
24
|
+
|
|
25
|
+
export const auditTrail = new AuditTrail();
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'fs';
|
|
2
|
+
import type { AuditEvent } from './types.js';
|
|
3
|
+
import { appPath } from '../utils/paths.js';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_PATH = appPath('audit.jsonl');
|
|
6
|
+
|
|
7
|
+
export interface ReadAuditLogOpts {
|
|
8
|
+
filePath?: string;
|
|
9
|
+
since?: Date;
|
|
10
|
+
type?: string;
|
|
11
|
+
ticker?: string;
|
|
12
|
+
limit?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function readAuditLog(opts: ReadAuditLogOpts = {}): AuditEvent[] {
|
|
16
|
+
const filePath = opts.filePath ?? DEFAULT_PATH;
|
|
17
|
+
|
|
18
|
+
if (!existsSync(filePath)) return [];
|
|
19
|
+
|
|
20
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
21
|
+
const lines = content.split('\n').filter((line) => line.length > 0);
|
|
22
|
+
|
|
23
|
+
let events: AuditEvent[] = lines.map((line) => JSON.parse(line));
|
|
24
|
+
|
|
25
|
+
if (opts.since) {
|
|
26
|
+
const sinceIso = opts.since.toISOString();
|
|
27
|
+
events = events.filter((e) => e.ts >= sinceIso);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (opts.type) {
|
|
31
|
+
events = events.filter((e) => e.type === opts.type);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (opts.ticker) {
|
|
35
|
+
events = events.filter((e) => 'ticker' in e && (e as any).ticker === opts.ticker);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (opts.limit !== undefined) {
|
|
39
|
+
events = events.slice(0, opts.limit);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return events;
|
|
43
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from 'fs';
|
|
2
|
+
import { dirname } from 'path';
|
|
3
|
+
import type { AuditEvent, DistributiveOmit } from './types.js';
|
|
4
|
+
import { appPath } from '../utils/paths.js';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_PATH = appPath('audit.jsonl');
|
|
7
|
+
|
|
8
|
+
export class AuditTrail {
|
|
9
|
+
private filePath: string;
|
|
10
|
+
private dirCreated = false;
|
|
11
|
+
|
|
12
|
+
constructor(filePath?: string) {
|
|
13
|
+
this.filePath = filePath ?? DEFAULT_PATH;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
log(event: DistributiveOmit<AuditEvent, 'ts'>): void {
|
|
17
|
+
if (!this.dirCreated) {
|
|
18
|
+
mkdirSync(dirname(this.filePath), { recursive: true });
|
|
19
|
+
this.dirCreated = true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const record = { ts: new Date().toISOString(), ...event };
|
|
23
|
+
appendFileSync(this.filePath, JSON.stringify(record) + '\n');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
close(): void {
|
|
27
|
+
// No-op for sync I/O — included for interface symmetry
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { ConfidenceLevel } from '../scan/types.js';
|
|
2
|
+
|
|
3
|
+
export interface AuditBase {
|
|
4
|
+
ts: string; // ISO 8601 UTC
|
|
5
|
+
type: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ScanStartEvent extends AuditBase {
|
|
9
|
+
type: 'SCAN_START';
|
|
10
|
+
theme: string;
|
|
11
|
+
events_count: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ScanCompleteEvent extends AuditBase {
|
|
15
|
+
type: 'SCAN_COMPLETE';
|
|
16
|
+
scan_id: string;
|
|
17
|
+
theme: string;
|
|
18
|
+
events_scanned: number;
|
|
19
|
+
edges_found: number;
|
|
20
|
+
duration_ms: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface OctagonCallEvent extends AuditBase {
|
|
24
|
+
type: 'OCTAGON_CALL';
|
|
25
|
+
ticker: string;
|
|
26
|
+
variant: string;
|
|
27
|
+
cache_hit: boolean;
|
|
28
|
+
credits_used: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface EdgeDetectedEvent extends AuditBase {
|
|
32
|
+
type: 'EDGE_DETECTED';
|
|
33
|
+
ticker: string;
|
|
34
|
+
model_prob: number;
|
|
35
|
+
market_prob: number;
|
|
36
|
+
edge: number;
|
|
37
|
+
confidence: ConfidenceLevel;
|
|
38
|
+
drivers: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface RecommendationEvent extends AuditBase {
|
|
42
|
+
type: 'RECOMMENDATION';
|
|
43
|
+
ticker: string;
|
|
44
|
+
action: string;
|
|
45
|
+
size: number;
|
|
46
|
+
kelly: number;
|
|
47
|
+
risk_gate: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface TradeExecutedEvent extends AuditBase {
|
|
51
|
+
type: 'TRADE_EXECUTED';
|
|
52
|
+
ticker: string;
|
|
53
|
+
order_id: string;
|
|
54
|
+
fill_price: number;
|
|
55
|
+
size: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface AlertSentEvent extends AuditBase {
|
|
59
|
+
type: 'ALERT_SENT';
|
|
60
|
+
alert_id: string;
|
|
61
|
+
channels: string[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface WatchdogCheckEvent extends AuditBase {
|
|
65
|
+
type: 'WATCHDOG_CHECK';
|
|
66
|
+
ticker: string;
|
|
67
|
+
entry_edge: number;
|
|
68
|
+
current_edge: number;
|
|
69
|
+
status: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ApiRetryEvent extends AuditBase {
|
|
73
|
+
type: 'API_RETRY';
|
|
74
|
+
method: string;
|
|
75
|
+
path: string;
|
|
76
|
+
attempt: number;
|
|
77
|
+
max_retries: number;
|
|
78
|
+
status_code: number;
|
|
79
|
+
delay_ms: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface DlqEntryEvent extends AuditBase {
|
|
83
|
+
type: 'DLQ_ENTRY';
|
|
84
|
+
method: string;
|
|
85
|
+
path: string;
|
|
86
|
+
error: string;
|
|
87
|
+
attempts: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface ConfigChangeEvent extends AuditBase {
|
|
91
|
+
type: 'CONFIG_CHANGE';
|
|
92
|
+
category: string;
|
|
93
|
+
avg_brier: number;
|
|
94
|
+
trigger: string;
|
|
95
|
+
recommendation: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface ConfigSetEvent extends AuditBase {
|
|
99
|
+
type: 'CONFIG_SET';
|
|
100
|
+
key: string;
|
|
101
|
+
old_value: string; // JSON.stringify'd
|
|
102
|
+
new_value: string; // JSON.stringify'd
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface OctagonErrorEvent extends AuditBase {
|
|
106
|
+
type: 'OCTAGON_ERROR';
|
|
107
|
+
ticker: string;
|
|
108
|
+
event_ticker: string;
|
|
109
|
+
error: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export type AuditEvent =
|
|
113
|
+
| ScanStartEvent
|
|
114
|
+
| ScanCompleteEvent
|
|
115
|
+
| OctagonCallEvent
|
|
116
|
+
| EdgeDetectedEvent
|
|
117
|
+
| RecommendationEvent
|
|
118
|
+
| TradeExecutedEvent
|
|
119
|
+
| AlertSentEvent
|
|
120
|
+
| WatchdogCheckEvent
|
|
121
|
+
| ApiRetryEvent
|
|
122
|
+
| DlqEntryEvent
|
|
123
|
+
| ConfigChangeEvent
|
|
124
|
+
| ConfigSetEvent
|
|
125
|
+
| OctagonErrorEvent;
|
|
126
|
+
|
|
127
|
+
export type AuditEventType = AuditEvent['type'];
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Distributive Omit that preserves discriminated union narrowing.
|
|
131
|
+
* Standard Omit<Union, K> collapses the union; this distributes over each member.
|
|
132
|
+
*/
|
|
133
|
+
export type DistributiveOmit<T, K extends keyof T> = T extends unknown ? Omit<T, K> : never;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
import { callKalshiApi } from '../tools/kalshi/api.js';
|
|
3
|
+
import type { KalshiMarket } from '../tools/kalshi/types.js';
|
|
4
|
+
|
|
5
|
+
const CONCURRENCY = 10;
|
|
6
|
+
|
|
7
|
+
export interface SettledMarket {
|
|
8
|
+
ticker: string;
|
|
9
|
+
event_ticker: string;
|
|
10
|
+
result: 'yes' | 'no';
|
|
11
|
+
close_time: string;
|
|
12
|
+
series_category: string;
|
|
13
|
+
last_price: number; // last traded price (0-1)
|
|
14
|
+
volume: number; // lifetime trading volume (used for tradeability gate)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface OpenMarket {
|
|
18
|
+
ticker: string;
|
|
19
|
+
event_ticker: string;
|
|
20
|
+
market_prob: number; // current trading price (0-1)
|
|
21
|
+
close_time: string;
|
|
22
|
+
series_category: string;
|
|
23
|
+
volume: number; // lifetime trading volume (tradeability gate)
|
|
24
|
+
volume_24h: number; // 24-hour volume (liquidity-now gate)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Parse market price from Kalshi response (handles both cents and dollars formats). */
|
|
28
|
+
function parsePrice(m: KalshiMarket): number {
|
|
29
|
+
const dollars = parseFloat(m.last_price_dollars ?? '');
|
|
30
|
+
if (Number.isFinite(dollars)) return dollars;
|
|
31
|
+
return typeof m.last_price === 'number' ? m.last_price / 100 : 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Parse lifetime volume (prefers volume_fp string from new API). */
|
|
35
|
+
function parseVolume(m: KalshiMarket): number {
|
|
36
|
+
const fp = parseFloat(m.volume_fp ?? '');
|
|
37
|
+
if (Number.isFinite(fp)) return fp;
|
|
38
|
+
return typeof m.volume === 'number' ? m.volume : 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Parse 24h volume (prefers volume_24h_fp string from new API). */
|
|
42
|
+
function parseVolume24h(m: KalshiMarket): number {
|
|
43
|
+
const fp = parseFloat(m.volume_24h_fp ?? '');
|
|
44
|
+
if (Number.isFinite(fp)) return fp;
|
|
45
|
+
return typeof m.volume_24h === 'number' ? m.volume_24h : 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Fetch event markets from Kalshi, returning empty array on error. */
|
|
49
|
+
async function fetchEventMarkets(eventTicker: string): Promise<KalshiMarket[]> {
|
|
50
|
+
try {
|
|
51
|
+
const response = await callKalshiApi('GET', `/events/${eventTicker}`, {
|
|
52
|
+
params: { with_nested_markets: true },
|
|
53
|
+
});
|
|
54
|
+
if (!response || typeof response !== 'object') return [];
|
|
55
|
+
const obj = response as Record<string, unknown>;
|
|
56
|
+
const event = (obj.event ?? obj) as Record<string, unknown>;
|
|
57
|
+
const markets = event.markets;
|
|
58
|
+
return Array.isArray(markets) ? markets as KalshiMarket[] : [];
|
|
59
|
+
} catch {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Build the event discovery query with optional category filter and extra WHERE clauses. */
|
|
65
|
+
function buildEventQuery(
|
|
66
|
+
extraWhere: string,
|
|
67
|
+
category?: string,
|
|
68
|
+
): { query: string; params: Record<string, string> } {
|
|
69
|
+
let query = `SELECT event_ticker, MAX(series_category) as category
|
|
70
|
+
FROM octagon_reports r WHERE variant_used = 'events-api'${extraWhere}`;
|
|
71
|
+
const params: Record<string, string> = {};
|
|
72
|
+
if (category) {
|
|
73
|
+
query += ' AND LOWER(series_category) LIKE $cat';
|
|
74
|
+
params.$cat = `%${category.toLowerCase()}%`;
|
|
75
|
+
}
|
|
76
|
+
query += ' GROUP BY event_ticker';
|
|
77
|
+
return { query, params };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Process items in parallel batches of `concurrency`. */
|
|
81
|
+
export async function parallelMap<T, R>(
|
|
82
|
+
items: T[],
|
|
83
|
+
fn: (item: T) => Promise<R>,
|
|
84
|
+
concurrency: number,
|
|
85
|
+
): Promise<R[]> {
|
|
86
|
+
const results: R[] = [];
|
|
87
|
+
for (let i = 0; i < items.length; i += concurrency) {
|
|
88
|
+
const batch = items.slice(i, i + concurrency);
|
|
89
|
+
const batchResults = await Promise.all(batch.map(fn));
|
|
90
|
+
results.push(...batchResults);
|
|
91
|
+
}
|
|
92
|
+
return results;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Discover settled Kalshi markets that have Octagon coverage with history.
|
|
97
|
+
*
|
|
98
|
+
* No close_time filter: the backtest's prediction-age window is enforced
|
|
99
|
+
* downstream via `selectSnapshotByDate`. Filtering on close_time here would
|
|
100
|
+
* wrongly exclude events that closed before the lookback window but whose
|
|
101
|
+
* predictions were still made within the prediction-age window (they'd still
|
|
102
|
+
* have outcomes to score against).
|
|
103
|
+
*/
|
|
104
|
+
export async function discoverSettledMarkets(
|
|
105
|
+
db: Database,
|
|
106
|
+
opts?: { category?: string },
|
|
107
|
+
): Promise<SettledMarket[]> {
|
|
108
|
+
const { query, params } = buildEventQuery('', opts?.category);
|
|
109
|
+
const events = db.query(query).all(params) as Array<{ event_ticker: string; category: string | null }>;
|
|
110
|
+
|
|
111
|
+
const batchResults = await parallelMap(events, async ({ event_ticker, category: cat }) => {
|
|
112
|
+
const markets = await fetchEventMarkets(event_ticker);
|
|
113
|
+
const settled: SettledMarket[] = [];
|
|
114
|
+
|
|
115
|
+
for (const m of markets) {
|
|
116
|
+
const result = (m.result ?? '').toLowerCase();
|
|
117
|
+
if (result !== 'yes' && result !== 'no') continue;
|
|
118
|
+
|
|
119
|
+
settled.push({
|
|
120
|
+
ticker: m.ticker,
|
|
121
|
+
event_ticker,
|
|
122
|
+
result: result as 'yes' | 'no',
|
|
123
|
+
close_time: m.close_time ?? '',
|
|
124
|
+
series_category: cat ?? '',
|
|
125
|
+
last_price: parsePrice(m),
|
|
126
|
+
volume: parseVolume(m),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return settled;
|
|
130
|
+
}, CONCURRENCY);
|
|
131
|
+
|
|
132
|
+
return batchResults.flat();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Discover open Kalshi markets that have Octagon coverage.
|
|
137
|
+
*/
|
|
138
|
+
export async function discoverOpenMarkets(
|
|
139
|
+
db: Database,
|
|
140
|
+
opts?: { category?: string },
|
|
141
|
+
): Promise<OpenMarket[]> {
|
|
142
|
+
const { query: q2, params: p2 } = buildEventQuery('', opts?.category);
|
|
143
|
+
const events2 = db.query(q2).all(p2) as Array<{ event_ticker: string; category: string | null }>;
|
|
144
|
+
|
|
145
|
+
const batchResults = await parallelMap(events2, async ({ event_ticker, category: cat }) => {
|
|
146
|
+
const markets = await fetchEventMarkets(event_ticker);
|
|
147
|
+
const open: OpenMarket[] = [];
|
|
148
|
+
|
|
149
|
+
for (const m of markets) {
|
|
150
|
+
const status = (m.status ?? '').toLowerCase();
|
|
151
|
+
if (status !== 'open' && status !== 'active') continue;
|
|
152
|
+
|
|
153
|
+
const marketProb = parsePrice(m);
|
|
154
|
+
if (marketProb <= 0) continue;
|
|
155
|
+
|
|
156
|
+
open.push({
|
|
157
|
+
ticker: m.ticker,
|
|
158
|
+
event_ticker,
|
|
159
|
+
market_prob: marketProb,
|
|
160
|
+
close_time: m.close_time ?? '',
|
|
161
|
+
series_category: cat ?? '',
|
|
162
|
+
volume: parseVolume(m),
|
|
163
|
+
volume_24h: parseVolume24h(m),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return open;
|
|
167
|
+
}, CONCURRENCY);
|
|
168
|
+
|
|
169
|
+
return batchResults.flat();
|
|
170
|
+
}
|