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,473 @@
|
|
|
1
|
+
import type { ParsedArgs, Subcommand } from './parse-args.js';
|
|
2
|
+
import { wrapSuccess, wrapError } from './json.js';
|
|
3
|
+
import type { CLIResponse } from './json.js';
|
|
4
|
+
import { handleEdge, formatEdgeHuman } from './edge.js';
|
|
5
|
+
import { handleAnalyze, formatAnalyzeHuman, promptAnalyzeActions } from './analyze.js';
|
|
6
|
+
import { formatRawReport } from '../controllers/browse.js';
|
|
7
|
+
import { handlePortfolio, formatPortfolioHuman } from './portfolio.js';
|
|
8
|
+
import { handleConfig, formatConfigHuman } from './config.js';
|
|
9
|
+
import { handleAlerts, formatAlertsHuman } from './alerts.js';
|
|
10
|
+
import { handleStatus } from './status.js';
|
|
11
|
+
import { handleThemes, formatThemesHuman } from './themes.js';
|
|
12
|
+
import { handleWatch } from './watch.js';
|
|
13
|
+
import { handleBacktest, formatBacktestHuman } from './backtest.js';
|
|
14
|
+
import { callKalshiApi } from '../tools/kalshi/api.js';
|
|
15
|
+
import {
|
|
16
|
+
formatBalance,
|
|
17
|
+
formatPositions,
|
|
18
|
+
formatOrders,
|
|
19
|
+
} from './formatters.js';
|
|
20
|
+
import type { KalshiOrder, KalshiPosition } from '../tools/kalshi/types.js';
|
|
21
|
+
import { buildHelp, validateTradeArgs } from './help.js';
|
|
22
|
+
import { fetchMarketQuote } from './helpers.js';
|
|
23
|
+
import { ensureIndex, forceRefreshIndex } from '../tools/kalshi/search-index.js';
|
|
24
|
+
import { searchEventIndex } from '../db/event-index.js';
|
|
25
|
+
import { scanEdges, formatEdgeScanHuman } from './search-edge.js';
|
|
26
|
+
import type { KalshiBalanceResponse } from './formatters.js';
|
|
27
|
+
import { ExitCode, exitCodeFromError } from '../utils/errors.js';
|
|
28
|
+
import { trackEvent } from '../utils/telemetry.js';
|
|
29
|
+
|
|
30
|
+
// ─── Alias resolution ────────────────────────────────────────────────────────
|
|
31
|
+
// Maps legacy CLI subcommands to canonical commands with mode/subview context
|
|
32
|
+
|
|
33
|
+
interface ResolvedCommand {
|
|
34
|
+
canonical: Subcommand;
|
|
35
|
+
mode?: string;
|
|
36
|
+
subview?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function resolveAlias(subcommand: Subcommand, positionalArgs: string[]): ResolvedCommand {
|
|
40
|
+
switch (subcommand) {
|
|
41
|
+
// Legacy analysis aliases → analyze
|
|
42
|
+
case 'edge':
|
|
43
|
+
return { canonical: 'edge', mode: 'edge-only' };
|
|
44
|
+
// Legacy account aliases → portfolio
|
|
45
|
+
case 'status':
|
|
46
|
+
return { canonical: 'portfolio', subview: 'status' };
|
|
47
|
+
|
|
48
|
+
// themes → search themes
|
|
49
|
+
case 'themes':
|
|
50
|
+
return { canonical: 'search', subview: 'themes' };
|
|
51
|
+
|
|
52
|
+
default:
|
|
53
|
+
return { canonical: subcommand };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function dispatch(args: ParsedArgs): Promise<void> {
|
|
58
|
+
const { subcommand, json } = args;
|
|
59
|
+
const resolved = resolveAlias(subcommand, args.positionalArgs);
|
|
60
|
+
trackEvent('cli_command', { command: resolved.canonical, subview: resolved.subview ?? '' });
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
// ─── reject invalid flags early (for all commands) ───────────────
|
|
64
|
+
if (args.parseErrors.length > 0) {
|
|
65
|
+
const msg = args.parseErrors.join('; ');
|
|
66
|
+
if (json) {
|
|
67
|
+
console.log(JSON.stringify(wrapError(subcommand, 'INVALID_ARGS', msg)));
|
|
68
|
+
process.exit(ExitCode.USER_ERROR);
|
|
69
|
+
} else {
|
|
70
|
+
console.error(msg);
|
|
71
|
+
process.exit(ExitCode.USER_ERROR);
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── search ────────────────────────────────────────────────────────
|
|
77
|
+
if (resolved.canonical === 'search') {
|
|
78
|
+
const sub = resolved.subview ?? args.positionalArgs[0];
|
|
79
|
+
if (sub === 'themes' || resolved.subview === 'themes') {
|
|
80
|
+
const resp = await handleThemes(args);
|
|
81
|
+
if (json) {
|
|
82
|
+
console.log(JSON.stringify(resp));
|
|
83
|
+
} else {
|
|
84
|
+
console.log(formatThemesHuman(resp.data));
|
|
85
|
+
}
|
|
86
|
+
process.exit(resp.ok ? ExitCode.SUCCESS : ExitCode.USER_ERROR);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (sub === 'edge') {
|
|
90
|
+
const db = (await import('../db/index.js')).getDb();
|
|
91
|
+
const minEdgePp = (args.minEdge ?? 0.05) * 100;
|
|
92
|
+
const result = scanEdges(db, { minEdgePp, limit: args.limit, category: args.category });
|
|
93
|
+
if (json) {
|
|
94
|
+
console.log(JSON.stringify(wrapSuccess('search', result)));
|
|
95
|
+
} else {
|
|
96
|
+
console.log(formatEdgeScanHuman(result, minEdgePp));
|
|
97
|
+
}
|
|
98
|
+
process.exit(ExitCode.SUCCESS);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (!sub) {
|
|
102
|
+
// No query provided — show themes as a starting point
|
|
103
|
+
const resp = await handleThemes(args);
|
|
104
|
+
if (json) {
|
|
105
|
+
console.log(JSON.stringify(resp));
|
|
106
|
+
} else {
|
|
107
|
+
console.log(formatThemesHuman(resp.data));
|
|
108
|
+
}
|
|
109
|
+
process.exit(resp.ok ? ExitCode.SUCCESS : ExitCode.USER_ERROR);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// General search: query the local event index
|
|
113
|
+
if (args.refresh) {
|
|
114
|
+
await forceRefreshIndex();
|
|
115
|
+
} else {
|
|
116
|
+
await ensureIndex();
|
|
117
|
+
}
|
|
118
|
+
const db = (await import('../db/index.js')).getDb();
|
|
119
|
+
const query = args.positionalArgs.join(' ');
|
|
120
|
+
const results = searchEventIndex(db, query, 30);
|
|
121
|
+
if (json) {
|
|
122
|
+
console.log(JSON.stringify(wrapSuccess('search', { events: results })));
|
|
123
|
+
} else {
|
|
124
|
+
if (results.length === 0) {
|
|
125
|
+
console.log(`No events found for "${query}".`);
|
|
126
|
+
} else {
|
|
127
|
+
console.log(`Found ${results.length} event(s) for "${query}":\n`);
|
|
128
|
+
for (const ev of results) {
|
|
129
|
+
const markets = ev.markets_json ? JSON.parse(ev.markets_json) : [];
|
|
130
|
+
const openMarkets = markets.filter((m: any) => m.status === 'open' || m.status === 'active');
|
|
131
|
+
console.log(` ${ev.event_ticker} ${ev.title} (${openMarkets.length} market${openMarkets.length !== 1 ? 's' : ''})`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─── portfolio (with subviews) ─────────────────────────────────────
|
|
139
|
+
if (resolved.canonical === 'portfolio') {
|
|
140
|
+
const subview = resolved.subview ?? args.positionalArgs[0] ?? 'overview';
|
|
141
|
+
|
|
142
|
+
if (subview === 'positions') {
|
|
143
|
+
const data = await callKalshiApi('GET', '/portfolio/positions');
|
|
144
|
+
const allPositions = (data.market_positions ?? data.positions ?? []) as KalshiPosition[];
|
|
145
|
+
const positions = allPositions.filter((p) => {
|
|
146
|
+
const pos = parseFloat(String(p.position ?? '0'));
|
|
147
|
+
return pos !== 0;
|
|
148
|
+
});
|
|
149
|
+
if (json) {
|
|
150
|
+
console.log(JSON.stringify(wrapSuccess('portfolio:positions', { positions })));
|
|
151
|
+
} else {
|
|
152
|
+
console.log(formatPositions(positions));
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (subview === 'orders') {
|
|
158
|
+
const data = await callKalshiApi('GET', '/portfolio/orders', { params: { status: 'resting' } });
|
|
159
|
+
const orders = (data.orders ?? []) as KalshiOrder[];
|
|
160
|
+
if (json) {
|
|
161
|
+
console.log(JSON.stringify(wrapSuccess('portfolio:orders', { orders })));
|
|
162
|
+
} else {
|
|
163
|
+
console.log(formatOrders(orders));
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (subview === 'balance') {
|
|
169
|
+
const data = await callKalshiApi('GET', '/portfolio/balance') as unknown as KalshiBalanceResponse;
|
|
170
|
+
if (json) {
|
|
171
|
+
console.log(JSON.stringify(wrapSuccess('portfolio:balance', data)));
|
|
172
|
+
} else {
|
|
173
|
+
console.log(formatBalance(data));
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (subview === 'status') {
|
|
179
|
+
const output = await handleStatus();
|
|
180
|
+
if (json) {
|
|
181
|
+
console.log(JSON.stringify({ ok: true, output }));
|
|
182
|
+
} else {
|
|
183
|
+
console.log(output);
|
|
184
|
+
}
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Default: full portfolio overview
|
|
189
|
+
const resp = await handlePortfolio(args);
|
|
190
|
+
if (json) {
|
|
191
|
+
console.log(JSON.stringify(resp));
|
|
192
|
+
} else {
|
|
193
|
+
console.log(formatPortfolioHuman(resp.data));
|
|
194
|
+
const warnings = (resp.meta as Record<string, unknown>)?.warnings;
|
|
195
|
+
if (Array.isArray(warnings) && warnings.length > 0) {
|
|
196
|
+
for (const w of warnings) console.error(` ⚠ ${String(w)}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
process.exit(resp.ok ? ExitCode.SUCCESS : ExitCode.USER_ERROR);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ─── analyze ───────────────────────────────────────────────────────
|
|
204
|
+
if (resolved.canonical === 'analyze') {
|
|
205
|
+
const ticker = args.positionalArgs[0];
|
|
206
|
+
if (!ticker) {
|
|
207
|
+
const errResp = wrapError('analyze', 'MISSING_TICKER', 'Usage: analyze <ticker> [--refresh] [--report]');
|
|
208
|
+
if (json) {
|
|
209
|
+
console.log(JSON.stringify(errResp));
|
|
210
|
+
process.exit(ExitCode.USER_ERROR);
|
|
211
|
+
} else {
|
|
212
|
+
console.error('Usage: analyze <ticker> [--refresh] [--report]');
|
|
213
|
+
process.exit(ExitCode.USER_ERROR);
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const refresh = args.refresh;
|
|
218
|
+
const data = await handleAnalyze(ticker, refresh);
|
|
219
|
+
if (json) {
|
|
220
|
+
console.log(JSON.stringify(wrapSuccess('analyze', data)));
|
|
221
|
+
} else {
|
|
222
|
+
console.log(formatAnalyzeHuman(data));
|
|
223
|
+
if (args.report && data.rawReport) {
|
|
224
|
+
console.log('\n' + formatRawReport(data.rawReport, ticker));
|
|
225
|
+
}
|
|
226
|
+
await promptAnalyzeActions(data);
|
|
227
|
+
}
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ─── watch ─────────────────────────────────────────────────────────
|
|
232
|
+
if (resolved.canonical === 'watch') {
|
|
233
|
+
// Force index rebuild before watching if --refresh is set
|
|
234
|
+
if (args.refresh) {
|
|
235
|
+
await forceRefreshIndex();
|
|
236
|
+
}
|
|
237
|
+
// Per-ticker mode if a positional arg is given and no --theme
|
|
238
|
+
const ticker = args.positionalArgs[0];
|
|
239
|
+
if (ticker && !args.theme) {
|
|
240
|
+
const { handleWatchTicker } = await import('./watch.js');
|
|
241
|
+
await handleWatchTicker(ticker.toUpperCase(), args);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
// Theme scan mode (existing behavior)
|
|
245
|
+
await handleWatch(args);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ─── backtest ──────────────────────────────────────────────────────
|
|
250
|
+
if (resolved.canonical === 'backtest') {
|
|
251
|
+
const resp = await handleBacktest(args);
|
|
252
|
+
if (json) {
|
|
253
|
+
console.log(JSON.stringify(resp));
|
|
254
|
+
} else if (resp.ok && resp.data) {
|
|
255
|
+
console.log(formatBacktestHuman(resp.data, {
|
|
256
|
+
minEdge: args.minEdge ?? 0.005,
|
|
257
|
+
}));
|
|
258
|
+
} else {
|
|
259
|
+
console.error(resp.error?.message ?? 'Backtest failed');
|
|
260
|
+
}
|
|
261
|
+
process.exit(resp.ok ? ExitCode.SUCCESS : ExitCode.USER_ERROR);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ─── buy / sell ────────────────────────────────────────────────────
|
|
266
|
+
if (subcommand === 'buy' || subcommand === 'sell') {
|
|
267
|
+
const [ticker, countStr, priceStr] = args.positionalArgs;
|
|
268
|
+
if (!ticker || !countStr) {
|
|
269
|
+
const usage = `Usage: ${subcommand} <ticker> <count> [price_in_cents] [--side yes|no]`;
|
|
270
|
+
const errResp = wrapError(subcommand, 'MISSING_ARGS', usage);
|
|
271
|
+
if (json) {
|
|
272
|
+
console.log(JSON.stringify(errResp));
|
|
273
|
+
process.exit(ExitCode.USER_ERROR);
|
|
274
|
+
} else {
|
|
275
|
+
console.error(usage);
|
|
276
|
+
process.exit(ExitCode.USER_ERROR);
|
|
277
|
+
}
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const validated = validateTradeArgs(countStr, priceStr);
|
|
281
|
+
if ('error' in validated) {
|
|
282
|
+
const errResp = wrapError(subcommand, 'INVALID_ARGS', validated.error);
|
|
283
|
+
if (json) {
|
|
284
|
+
console.log(JSON.stringify(errResp));
|
|
285
|
+
process.exit(ExitCode.USER_ERROR);
|
|
286
|
+
} else {
|
|
287
|
+
console.error(validated.error);
|
|
288
|
+
process.exit(ExitCode.USER_ERROR);
|
|
289
|
+
}
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
let effectivePrice = validated.price;
|
|
293
|
+
// When no price given, fetch best quote to simulate a market order
|
|
294
|
+
// (Kalshi API requires a price field even for market-like orders)
|
|
295
|
+
const tradeSide = args.side ?? 'yes';
|
|
296
|
+
if (effectivePrice === undefined) {
|
|
297
|
+
const quoteResult = await fetchMarketQuote(ticker.toUpperCase(), subcommand as 'buy' | 'sell', tradeSide);
|
|
298
|
+
if ('error' in quoteResult) {
|
|
299
|
+
if (json) {
|
|
300
|
+
console.log(JSON.stringify(wrapError(subcommand, 'NO_QUOTE', quoteResult.error)));
|
|
301
|
+
process.exit(ExitCode.EXTERNAL_ERROR);
|
|
302
|
+
} else {
|
|
303
|
+
console.error(quoteResult.error);
|
|
304
|
+
process.exit(ExitCode.EXTERNAL_ERROR);
|
|
305
|
+
}
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
effectivePrice = quoteResult.cents;
|
|
309
|
+
}
|
|
310
|
+
const body: Record<string, unknown> = {
|
|
311
|
+
ticker: ticker.toUpperCase(),
|
|
312
|
+
action: subcommand,
|
|
313
|
+
side: tradeSide,
|
|
314
|
+
type: 'limit',
|
|
315
|
+
count: validated.count,
|
|
316
|
+
...(tradeSide === 'no'
|
|
317
|
+
? { no_price: effectivePrice }
|
|
318
|
+
: { yes_price: effectivePrice }),
|
|
319
|
+
};
|
|
320
|
+
const data = await callKalshiApi('POST', '/portfolio/orders', { body });
|
|
321
|
+
if (json) {
|
|
322
|
+
console.log(JSON.stringify(wrapSuccess(subcommand, data)));
|
|
323
|
+
} else {
|
|
324
|
+
const order = data.order as Record<string, unknown> | undefined;
|
|
325
|
+
console.log(order ? `Order placed. ID: ${order.order_id} | Status: ${order.status}` : `Order submitted.`);
|
|
326
|
+
}
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ─── cancel ────────────────────────────────────────────────────────
|
|
331
|
+
if (subcommand === 'cancel') {
|
|
332
|
+
const orderId = args.positionalArgs[0];
|
|
333
|
+
if (!orderId) {
|
|
334
|
+
const errResp = wrapError('cancel', 'MISSING_ARGS', 'Usage: cancel <order_id>');
|
|
335
|
+
if (json) {
|
|
336
|
+
console.log(JSON.stringify(errResp));
|
|
337
|
+
process.exit(ExitCode.USER_ERROR);
|
|
338
|
+
} else {
|
|
339
|
+
console.error('Usage: cancel <order_id>');
|
|
340
|
+
process.exit(ExitCode.USER_ERROR);
|
|
341
|
+
}
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
try {
|
|
345
|
+
await callKalshiApi('DELETE', `/portfolio/orders/${orderId}`);
|
|
346
|
+
} catch (err) {
|
|
347
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
348
|
+
const hint = msg.includes('404') ? ' (order not found or already filled)' : '';
|
|
349
|
+
const code = exitCodeFromError(err);
|
|
350
|
+
if (json) {
|
|
351
|
+
console.log(JSON.stringify(wrapError('cancel', 'CANCEL_FAILED', msg + hint)));
|
|
352
|
+
process.exit(code);
|
|
353
|
+
} else {
|
|
354
|
+
console.error(`Cancel failed: ${msg}${hint}`);
|
|
355
|
+
process.exit(code);
|
|
356
|
+
}
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (json) {
|
|
360
|
+
console.log(JSON.stringify(wrapSuccess('cancel', { orderId, canceled: true })));
|
|
361
|
+
} else {
|
|
362
|
+
console.log(`Order ${orderId} canceled.`);
|
|
363
|
+
}
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ─── help ──────────────────────────────────────────────────────────
|
|
368
|
+
if (subcommand === 'help') {
|
|
369
|
+
const topic = args.positionalArgs[0];
|
|
370
|
+
const result = buildHelp('cli', topic);
|
|
371
|
+
if ('error' in result) {
|
|
372
|
+
const errResp = wrapError('help', 'UNKNOWN_TOPIC', result.error);
|
|
373
|
+
if (json) {
|
|
374
|
+
console.log(JSON.stringify(errResp));
|
|
375
|
+
process.exit(ExitCode.USER_ERROR);
|
|
376
|
+
} else {
|
|
377
|
+
console.error(result.error);
|
|
378
|
+
process.exit(ExitCode.USER_ERROR);
|
|
379
|
+
}
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (json) {
|
|
383
|
+
console.log(JSON.stringify(wrapSuccess('help', { text: result.text })));
|
|
384
|
+
} else {
|
|
385
|
+
console.log(result.text);
|
|
386
|
+
}
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ─── Legacy commands (kept for backward compat) ────────────────────
|
|
391
|
+
|
|
392
|
+
// Edge command
|
|
393
|
+
if (subcommand === 'edge') {
|
|
394
|
+
const resp = await handleEdge(args);
|
|
395
|
+
if (json) {
|
|
396
|
+
console.log(JSON.stringify(resp));
|
|
397
|
+
} else {
|
|
398
|
+
console.log(formatEdgeHuman(resp.data));
|
|
399
|
+
}
|
|
400
|
+
process.exit(resp.ok ? ExitCode.SUCCESS : ExitCode.USER_ERROR);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Config command
|
|
405
|
+
if (subcommand === 'config') {
|
|
406
|
+
const resp = await handleConfig(args);
|
|
407
|
+
if (json) {
|
|
408
|
+
console.log(JSON.stringify(resp));
|
|
409
|
+
} else if (!resp.ok) {
|
|
410
|
+
const errMsg = (resp as { error?: { message?: string } }).error?.message ?? 'Config error';
|
|
411
|
+
console.error(errMsg);
|
|
412
|
+
} else {
|
|
413
|
+
console.log(formatConfigHuman(resp.data));
|
|
414
|
+
}
|
|
415
|
+
process.exit(resp.ok ? ExitCode.SUCCESS : ExitCode.USER_ERROR);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Clear cache command
|
|
420
|
+
if (subcommand === 'clear-cache') {
|
|
421
|
+
const { handleClearCache } = await import('./clear-cache.js');
|
|
422
|
+
const result = handleClearCache();
|
|
423
|
+
if (json) {
|
|
424
|
+
console.log(JSON.stringify(wrapSuccess('clear-cache', result)));
|
|
425
|
+
} else {
|
|
426
|
+
console.log(result.message);
|
|
427
|
+
}
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Alerts command
|
|
432
|
+
if (subcommand === 'alerts') {
|
|
433
|
+
const resp = await handleAlerts(args);
|
|
434
|
+
if (json) {
|
|
435
|
+
console.log(JSON.stringify(resp));
|
|
436
|
+
} else {
|
|
437
|
+
console.log(formatAlertsHuman(resp.data));
|
|
438
|
+
}
|
|
439
|
+
process.exit(resp.ok ? ExitCode.SUCCESS : ExitCode.USER_ERROR);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Unknown command
|
|
444
|
+
const resp = wrapError(subcommand, 'UNKNOWN_COMMAND', `Unknown command: ${subcommand}`);
|
|
445
|
+
if (json) {
|
|
446
|
+
console.log(JSON.stringify(resp));
|
|
447
|
+
process.exit(ExitCode.USER_ERROR);
|
|
448
|
+
} else {
|
|
449
|
+
console.error(`Error: unknown command "${subcommand}"`);
|
|
450
|
+
process.exit(ExitCode.USER_ERROR);
|
|
451
|
+
}
|
|
452
|
+
} catch (err) {
|
|
453
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
454
|
+
const code = exitCodeFromError(err);
|
|
455
|
+
const errorCode = code === ExitCode.AUTH_ERROR
|
|
456
|
+
? 'AUTH_ERROR'
|
|
457
|
+
: code === ExitCode.EXTERNAL_ERROR
|
|
458
|
+
? 'EXTERNAL_ERROR'
|
|
459
|
+
: code === ExitCode.USER_ERROR
|
|
460
|
+
? 'USER_ERROR'
|
|
461
|
+
: 'INTERNAL_ERROR';
|
|
462
|
+
const resp = wrapError(subcommand, errorCode, message);
|
|
463
|
+
trackEvent('error_occurred', { command: subcommand, error_code: errorCode });
|
|
464
|
+
|
|
465
|
+
if (json) {
|
|
466
|
+
console.log(JSON.stringify(resp));
|
|
467
|
+
process.exit(code);
|
|
468
|
+
} else {
|
|
469
|
+
console.error(`Error running "${subcommand}": ${message}`);
|
|
470
|
+
process.exit(code);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ParsedArgs } from './parse-args.js';
|
|
2
|
+
import type { CLIResponse } from './json.js';
|
|
3
|
+
import type { EdgeRow } from '../db/edge.js';
|
|
4
|
+
import { wrapSuccess, wrapError } from './json.js';
|
|
5
|
+
import { getDb } from '../db/index.js';
|
|
6
|
+
import { getEdgeHistory, getActionableEdges, getLatestEdge } from '../db/edge.js';
|
|
7
|
+
import { formatEdgeTable } from './scan-formatters.js';
|
|
8
|
+
|
|
9
|
+
export async function handleEdge(args: ParsedArgs): Promise<CLIResponse<EdgeRow[]>> {
|
|
10
|
+
const db = getDb();
|
|
11
|
+
let rows: EdgeRow[];
|
|
12
|
+
|
|
13
|
+
const DEFAULT_MAX_AGE_S = 24 * 60 * 60; // 24 hours
|
|
14
|
+
let sinceEpoch: number;
|
|
15
|
+
if (args.since) {
|
|
16
|
+
const parsed = new Date(args.since).getTime();
|
|
17
|
+
if (!Number.isFinite(parsed)) {
|
|
18
|
+
return wrapError('edge', 'INVALID_DATE', `Invalid date format for --since: "${args.since}"`);
|
|
19
|
+
}
|
|
20
|
+
sinceEpoch = Math.floor(parsed / 1000);
|
|
21
|
+
} else {
|
|
22
|
+
sinceEpoch = Math.floor(Date.now() / 1000) - DEFAULT_MAX_AGE_S;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (args.ticker) {
|
|
26
|
+
// History for a single ticker
|
|
27
|
+
rows = getEdgeHistory(db, args.ticker, sinceEpoch);
|
|
28
|
+
} else if (args.minConfidence) {
|
|
29
|
+
// Latest per ticker filtered by confidence
|
|
30
|
+
rows = getActionableEdges(db, args.minConfidence);
|
|
31
|
+
rows = rows.filter((r) => r.timestamp >= sinceEpoch);
|
|
32
|
+
} else if (args.theme) {
|
|
33
|
+
// Get tickers for theme, then latest edge per ticker
|
|
34
|
+
const themeRows = db.query(
|
|
35
|
+
`SELECT DISTINCT ticker FROM edge_history
|
|
36
|
+
WHERE event_ticker IN (SELECT ticker FROM events WHERE theme_id = $theme)`
|
|
37
|
+
).all({ $theme: args.theme }) as { ticker: string }[];
|
|
38
|
+
|
|
39
|
+
rows = themeRows
|
|
40
|
+
.map((t) => getLatestEdge(db, t.ticker))
|
|
41
|
+
.filter((r): r is EdgeRow => r !== null);
|
|
42
|
+
|
|
43
|
+
rows = rows.filter((r) => r.timestamp >= sinceEpoch);
|
|
44
|
+
} else {
|
|
45
|
+
// Default: all latest edges
|
|
46
|
+
rows = getActionableEdges(db, 'low');
|
|
47
|
+
rows = rows.filter((r) => r.timestamp >= sinceEpoch);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Sort by |edge| descending
|
|
51
|
+
rows.sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge));
|
|
52
|
+
|
|
53
|
+
if (args.minEdge != null) {
|
|
54
|
+
rows = rows.filter((r) => Math.abs(r.edge) >= args.minEdge!);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return wrapSuccess('edge', rows);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function formatEdgeHuman(rows: EdgeRow[]): string {
|
|
61
|
+
return formatEdgeTable(rows);
|
|
62
|
+
}
|