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,276 @@
|
|
|
1
|
+
import type { ParsedArgs } from './parse-args.js';
|
|
2
|
+
import type { CLIResponse } from './json.js';
|
|
3
|
+
import { wrapSuccess } from './json.js';
|
|
4
|
+
import { getDb } from '../db/index.js';
|
|
5
|
+
import { discoverSettledMarkets, discoverOpenMarkets, parallelMap } from '../backtest/discovery.js';
|
|
6
|
+
import { fetchAndCacheHistory, selectSnapshotByDate, SubscriptionRequiredError, type OutcomeProbability } from '../backtest/fetcher.js';
|
|
7
|
+
import { computeMetrics } from '../backtest/metrics.js';
|
|
8
|
+
import type { BacktestResult, ScoredSignal } from '../backtest/types.js';
|
|
9
|
+
import { formatBacktestHuman, exportCSV, type FormatOpts } from '../backtest/renderer.js';
|
|
10
|
+
|
|
11
|
+
/** Look up the per-contract outcome entry from outcome_probabilities array. */
|
|
12
|
+
function findOutcomeProb(
|
|
13
|
+
outcomes: OutcomeProbability[] | null | undefined,
|
|
14
|
+
marketTicker: string,
|
|
15
|
+
): OutcomeProbability | null {
|
|
16
|
+
if (!outcomes || !Array.isArray(outcomes)) return null;
|
|
17
|
+
const match = outcomes.find(
|
|
18
|
+
o => o.market_ticker.toUpperCase() === marketTicker.toUpperCase(),
|
|
19
|
+
);
|
|
20
|
+
return match ?? null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Absolute-edge bucket label matching the Supabase-methodology buckets. */
|
|
24
|
+
function edgeBucketLabel(edgePp: number): string {
|
|
25
|
+
const abs = Math.abs(edgePp);
|
|
26
|
+
if (abs < 5) return '0-5%';
|
|
27
|
+
if (abs < 10) return '5-10%';
|
|
28
|
+
if (abs < 20) return '10-20%';
|
|
29
|
+
if (abs < 30) return '20-30%';
|
|
30
|
+
if (abs < 40) return '30-40%';
|
|
31
|
+
if (abs < 50) return '40-50%';
|
|
32
|
+
if (abs < 60) return '50-60%';
|
|
33
|
+
if (abs < 70) return '60-70%';
|
|
34
|
+
if (abs < 80) return '70-80%';
|
|
35
|
+
if (abs < 90) return '80-90%';
|
|
36
|
+
return '90%+';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Return the tradeable volume for a contract.
|
|
41
|
+
* Prefers per-contract volume fields from the Octagon snapshot (as the
|
|
42
|
+
* Supabase methodology does); falls back to Kalshi lifetime volume for
|
|
43
|
+
* older cached snapshots that pre-date the API's per-contract volume.
|
|
44
|
+
*/
|
|
45
|
+
function contractVolume(
|
|
46
|
+
perContract: OutcomeProbability | null,
|
|
47
|
+
fallbackLifetimeVolume: number,
|
|
48
|
+
): number {
|
|
49
|
+
if (perContract) {
|
|
50
|
+
const v = typeof perContract.volume === 'number' ? perContract.volume : null;
|
|
51
|
+
const v24 = typeof perContract.volume_24h === 'number' ? perContract.volume_24h : null;
|
|
52
|
+
if (v !== null || v24 !== null) return Math.max(v ?? 0, v24 ?? 0);
|
|
53
|
+
}
|
|
54
|
+
return fallbackLifetimeVolume;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { formatBacktestHuman };
|
|
58
|
+
export type { FormatOpts };
|
|
59
|
+
|
|
60
|
+
export async function handleBacktest(args: ParsedArgs): Promise<CLIResponse<BacktestResult>> {
|
|
61
|
+
const db = getDb();
|
|
62
|
+
const days = args.days ?? 15;
|
|
63
|
+
const maxAgeDays = args.maxAge ?? days;
|
|
64
|
+
// Default 0.5pp matches the Supabase reference methodology — enough to
|
|
65
|
+
// skip near-zero-edge noise without excluding the 0-5% bucket.
|
|
66
|
+
const minEdge = args.minEdge ?? 0.005;
|
|
67
|
+
const minEdgePp = minEdge * 100;
|
|
68
|
+
const minVolume = args.minVolume ?? 1;
|
|
69
|
+
const minPrice = args.minPrice ?? 5; // 0-100 scale
|
|
70
|
+
const maxPrice = args.maxPrice ?? 95; // 0-100 scale
|
|
71
|
+
const now = new Date();
|
|
72
|
+
const lookbackDate = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
|
|
73
|
+
const minPredictionDate = new Date(lookbackDate.getTime() - maxAgeDays * 24 * 60 * 60 * 1000);
|
|
74
|
+
|
|
75
|
+
const signals: ScoredSignal[] = [];
|
|
76
|
+
let subscriptionNotice: string | undefined;
|
|
77
|
+
|
|
78
|
+
// ─── RESOLVED: settled markets with historical Octagon snapshots ────────
|
|
79
|
+
if (!args.unresolved) {
|
|
80
|
+
try {
|
|
81
|
+
const settled = await discoverSettledMarkets(db, { category: args.category });
|
|
82
|
+
|
|
83
|
+
if (settled.length > 0) {
|
|
84
|
+
// Group by event_ticker to batch history fetches
|
|
85
|
+
const byEvent = new Map<string, typeof settled>();
|
|
86
|
+
for (const m of settled) {
|
|
87
|
+
const arr = byEvent.get(m.event_ticker) ?? [];
|
|
88
|
+
arr.push(m);
|
|
89
|
+
byEvent.set(m.event_ticker, arr);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Fetch & score events concurrently — Octagon history is I/O-bound and
|
|
93
|
+
// processing them serially made cold-cache backtests take 15+ min.
|
|
94
|
+
const perEvent = await parallelMap([...byEvent.entries()], async ([eventTicker, markets]) => {
|
|
95
|
+
let snapshots;
|
|
96
|
+
try {
|
|
97
|
+
snapshots = await fetchAndCacheHistory(db, eventTicker, { maxAgeDays });
|
|
98
|
+
} catch (err) {
|
|
99
|
+
if (err instanceof SubscriptionRequiredError) throw err;
|
|
100
|
+
return [] as ScoredSignal[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Find the snapshot closest to N days ago, rejecting snapshots
|
|
104
|
+
// older than the prediction-age window so we don't score stale
|
|
105
|
+
// model outputs as if they were recent.
|
|
106
|
+
const snap = selectSnapshotByDate(snapshots, lookbackDate, minPredictionDate);
|
|
107
|
+
if (!snap) return [];
|
|
108
|
+
|
|
109
|
+
const out: ScoredSignal[] = [];
|
|
110
|
+
for (const m of markets) {
|
|
111
|
+
// Strict per-contract extraction — no event-level fallback.
|
|
112
|
+
const perMarket = findOutcomeProb(snap.outcome_probabilities, m.ticker);
|
|
113
|
+
if (!perMarket) continue;
|
|
114
|
+
const modelProb = perMarket.model_probability;
|
|
115
|
+
const marketThen = perMarket.market_probability;
|
|
116
|
+
if (!Number.isFinite(modelProb) || !Number.isFinite(marketThen)) continue;
|
|
117
|
+
const marketNow = m.result === 'yes' ? 100 : 0;
|
|
118
|
+
const edgePp = Math.round((modelProb - marketThen) * 10) / 10;
|
|
119
|
+
|
|
120
|
+
// Tradeable filter — per-contract volume from the Octagon snapshot
|
|
121
|
+
// (matches Supabase methodology); falls back to Kalshi lifetime
|
|
122
|
+
// volume for pre-API-change cached snapshots.
|
|
123
|
+
const vol = contractVolume(perMarket, m.volume);
|
|
124
|
+
if (vol < minVolume) continue;
|
|
125
|
+
// Price is marketThen (the price you'd transact at for a resolved bet).
|
|
126
|
+
if (marketThen < minPrice || marketThen > maxPrice) continue;
|
|
127
|
+
|
|
128
|
+
// P&L and capital per $1 face value.
|
|
129
|
+
let pnl = 0;
|
|
130
|
+
let capital = 0;
|
|
131
|
+
if (edgePp > 0) {
|
|
132
|
+
// Buy YES at marketThen, settles at marketNow
|
|
133
|
+
pnl = (marketNow - marketThen) / 100;
|
|
134
|
+
capital = marketThen / 100;
|
|
135
|
+
} else if (edgePp < 0) {
|
|
136
|
+
// Buy NO at (100 - marketThen), settles at (100 - marketNow)
|
|
137
|
+
pnl = (marketThen - marketNow) / 100;
|
|
138
|
+
capital = (100 - marketThen) / 100;
|
|
139
|
+
} else {
|
|
140
|
+
// Zero edge: capital still reflects the tradeable side implied by sign
|
|
141
|
+
// (use YES side so divide-by-zero checks don't fire on 0-edge signals).
|
|
142
|
+
capital = marketThen / 100;
|
|
143
|
+
}
|
|
144
|
+
if (capital <= 0) continue;
|
|
145
|
+
|
|
146
|
+
out.push({
|
|
147
|
+
event_ticker: m.event_ticker,
|
|
148
|
+
market_ticker: m.ticker,
|
|
149
|
+
series_category: m.series_category,
|
|
150
|
+
model_prob: modelProb,
|
|
151
|
+
market_then: marketThen,
|
|
152
|
+
market_now: marketNow,
|
|
153
|
+
resolved: true,
|
|
154
|
+
edge_pp: edgePp,
|
|
155
|
+
pnl: Math.round(pnl * 10000) / 10000,
|
|
156
|
+
capital: Math.round(capital * 10000) / 10000,
|
|
157
|
+
edge_bucket: edgeBucketLabel(edgePp),
|
|
158
|
+
confidence_score: snap.confidence_score ?? 0,
|
|
159
|
+
close_time: m.close_time,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}, 10);
|
|
164
|
+
for (const arr of perEvent) signals.push(...arr);
|
|
165
|
+
}
|
|
166
|
+
} catch (err) {
|
|
167
|
+
if (err instanceof SubscriptionRequiredError) {
|
|
168
|
+
subscriptionNotice = err.message;
|
|
169
|
+
} else {
|
|
170
|
+
throw err;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ─── UNRESOLVED: open markets with current Kalshi prices ───────────────
|
|
176
|
+
if (!args.resolved) {
|
|
177
|
+
try {
|
|
178
|
+
const openMarkets = await discoverOpenMarkets(db, { category: args.category });
|
|
179
|
+
|
|
180
|
+
// Group by event_ticker to batch history fetches (same as resolved path).
|
|
181
|
+
const openByEvent = new Map<string, typeof openMarkets>();
|
|
182
|
+
for (const m of openMarkets) {
|
|
183
|
+
const arr = openByEvent.get(m.event_ticker) ?? [];
|
|
184
|
+
arr.push(m);
|
|
185
|
+
openByEvent.set(m.event_ticker, arr);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const perEvent = await parallelMap([...openByEvent.entries()], async ([eventTicker, markets]) => {
|
|
189
|
+
let snapshots;
|
|
190
|
+
try {
|
|
191
|
+
snapshots = await fetchAndCacheHistory(db, eventTicker, { maxAgeDays });
|
|
192
|
+
} catch (err) {
|
|
193
|
+
if (err instanceof SubscriptionRequiredError) throw err;
|
|
194
|
+
return [] as ScoredSignal[];
|
|
195
|
+
}
|
|
196
|
+
const snap = selectSnapshotByDate(snapshots, lookbackDate, minPredictionDate);
|
|
197
|
+
if (!snap) return [];
|
|
198
|
+
|
|
199
|
+
const out: ScoredSignal[] = [];
|
|
200
|
+
for (const m of markets) {
|
|
201
|
+
// Strict per-contract extraction — no event-level fallback.
|
|
202
|
+
const perMarket = findOutcomeProb(snap.outcome_probabilities, m.ticker);
|
|
203
|
+
if (!perMarket) continue;
|
|
204
|
+
const modelProb = perMarket.model_probability;
|
|
205
|
+
const marketThen = perMarket.market_probability;
|
|
206
|
+
if (!Number.isFinite(modelProb) || !Number.isFinite(marketThen)) continue;
|
|
207
|
+
const confidenceScore = snap.confidence_score ?? 0;
|
|
208
|
+
|
|
209
|
+
const marketNow = m.market_prob * 100; // current Kalshi price (0-100)
|
|
210
|
+
const edgePp = Math.round((modelProb - marketThen) * 10) / 10;
|
|
211
|
+
|
|
212
|
+
// Tradeable filter — per-contract volume from the Octagon snapshot.
|
|
213
|
+
const vol = contractVolume(perMarket, m.volume);
|
|
214
|
+
if (vol < minVolume) continue;
|
|
215
|
+
// Price is marketNow (the current transactable price for an open position).
|
|
216
|
+
if (marketNow < minPrice || marketNow > maxPrice) continue;
|
|
217
|
+
|
|
218
|
+
// M2M P&L and capital per $1 face value.
|
|
219
|
+
let pnl = 0;
|
|
220
|
+
let capital = 0;
|
|
221
|
+
if (edgePp > 0) {
|
|
222
|
+
pnl = (marketNow - marketThen) / 100;
|
|
223
|
+
capital = marketThen / 100;
|
|
224
|
+
} else if (edgePp < 0) {
|
|
225
|
+
pnl = (marketThen - marketNow) / 100;
|
|
226
|
+
capital = (100 - marketThen) / 100;
|
|
227
|
+
} else {
|
|
228
|
+
capital = marketThen / 100;
|
|
229
|
+
}
|
|
230
|
+
if (capital <= 0) continue;
|
|
231
|
+
|
|
232
|
+
out.push({
|
|
233
|
+
event_ticker: m.event_ticker,
|
|
234
|
+
market_ticker: m.ticker,
|
|
235
|
+
series_category: m.series_category,
|
|
236
|
+
model_prob: modelProb,
|
|
237
|
+
market_then: marketThen,
|
|
238
|
+
market_now: marketNow,
|
|
239
|
+
resolved: false,
|
|
240
|
+
edge_pp: edgePp,
|
|
241
|
+
pnl: Math.round(pnl * 10000) / 10000,
|
|
242
|
+
capital: Math.round(capital * 10000) / 10000,
|
|
243
|
+
edge_bucket: edgeBucketLabel(edgePp),
|
|
244
|
+
confidence_score: confidenceScore,
|
|
245
|
+
close_time: m.close_time,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
return out;
|
|
249
|
+
}, 10);
|
|
250
|
+
for (const arr of perEvent) signals.push(...arr);
|
|
251
|
+
} catch (err) {
|
|
252
|
+
// Mirror the resolved block: a subscription wall hit while scoring open
|
|
253
|
+
// markets becomes a notice rather than crashing out before CSV export.
|
|
254
|
+
if (err instanceof SubscriptionRequiredError) {
|
|
255
|
+
subscriptionNotice = subscriptionNotice ?? err.message;
|
|
256
|
+
} else {
|
|
257
|
+
throw err;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ─── COMPUTE METRICS ───────────────────────────────────────────────────
|
|
263
|
+
const metrics = computeMetrics(signals, minEdgePp);
|
|
264
|
+
|
|
265
|
+
const result: BacktestResult = {
|
|
266
|
+
...metrics,
|
|
267
|
+
days,
|
|
268
|
+
subscription_notice: subscriptionNotice,
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
if (args.exportPath) {
|
|
272
|
+
exportCSV(result, args.exportPath);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return wrapSuccess('backtest', result);
|
|
276
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { existsSync, unlinkSync } from 'fs';
|
|
2
|
+
import { closeDb } from '../db/index.js';
|
|
3
|
+
import { appPath } from '../utils/paths.js';
|
|
4
|
+
|
|
5
|
+
const DB_PATH = appPath('kalshi-bot.db');
|
|
6
|
+
|
|
7
|
+
export function handleClearCache(): { deleted: boolean; path: string; message: string } {
|
|
8
|
+
if (!existsSync(DB_PATH)) {
|
|
9
|
+
return { deleted: false, path: DB_PATH, message: `No cache file found at ${DB_PATH}` };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Close the singleton if open so file descriptors are released
|
|
13
|
+
closeDb();
|
|
14
|
+
|
|
15
|
+
// Remove WAL/SHM files if present
|
|
16
|
+
for (const suffix of ['', '-wal', '-shm']) {
|
|
17
|
+
const file = DB_PATH + suffix;
|
|
18
|
+
if (existsSync(file)) {
|
|
19
|
+
unlinkSync(file);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return { deleted: true, path: DB_PATH, message: `Cache cleared: ${DB_PATH}\nA fresh database will be created on next command.` };
|
|
24
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { ParsedArgs } from './parse-args.js';
|
|
2
|
+
import type { CLIResponse } from './json.js';
|
|
3
|
+
import { wrapSuccess, wrapError } from './json.js';
|
|
4
|
+
import { formatTable } from './scan-formatters.js';
|
|
5
|
+
import { auditTrail } from '../audit/index.js';
|
|
6
|
+
import {
|
|
7
|
+
getAllSettings,
|
|
8
|
+
getBotSetting,
|
|
9
|
+
setBotSetting,
|
|
10
|
+
type FlatSetting,
|
|
11
|
+
} from '../utils/bot-config.js';
|
|
12
|
+
|
|
13
|
+
export interface ConfigEntry {
|
|
14
|
+
key: string;
|
|
15
|
+
value: unknown;
|
|
16
|
+
default: unknown;
|
|
17
|
+
isDefault: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ConfigListData {
|
|
21
|
+
mode: 'list';
|
|
22
|
+
entries: ConfigEntry[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ConfigGetData {
|
|
26
|
+
mode: 'get';
|
|
27
|
+
entry: ConfigEntry;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ConfigSetData {
|
|
31
|
+
mode: 'set';
|
|
32
|
+
key: string;
|
|
33
|
+
oldValue: unknown;
|
|
34
|
+
newValue: unknown;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type ConfigData = ConfigListData | ConfigGetData | ConfigSetData;
|
|
38
|
+
|
|
39
|
+
function toEntry(s: FlatSetting): ConfigEntry {
|
|
40
|
+
return { key: s.key, value: s.value, default: s.default, isDefault: s.isDefault };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function handleConfig(args: ParsedArgs): Promise<CLIResponse<ConfigData>> {
|
|
44
|
+
const positional = args.positionalArgs;
|
|
45
|
+
|
|
46
|
+
// 0 args → list all
|
|
47
|
+
if (positional.length === 0) {
|
|
48
|
+
const entries = getAllSettings().map(toEntry);
|
|
49
|
+
return wrapSuccess('config', { mode: 'list', entries } as ConfigData);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const key = positional[0];
|
|
53
|
+
|
|
54
|
+
// 1 arg → get
|
|
55
|
+
if (positional.length === 1) {
|
|
56
|
+
const value = getBotSetting(key);
|
|
57
|
+
if (value === undefined) {
|
|
58
|
+
return wrapError('config', 'UNKNOWN_KEY', `Unknown config key: ${key}`) as CLIResponse<ConfigData>;
|
|
59
|
+
}
|
|
60
|
+
const all = getAllSettings();
|
|
61
|
+
const match = all.find((s) => s.key === key);
|
|
62
|
+
const entry: ConfigEntry = match
|
|
63
|
+
? toEntry(match)
|
|
64
|
+
: { key, value, default: value, isDefault: true };
|
|
65
|
+
return wrapSuccess('config', { mode: 'get', entry } as ConfigData);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 2 args → set
|
|
69
|
+
const rawValue = positional[1];
|
|
70
|
+
try {
|
|
71
|
+
const { oldValue, newValue } = setBotSetting(key, rawValue);
|
|
72
|
+
|
|
73
|
+
auditTrail.log({
|
|
74
|
+
type: 'CONFIG_SET',
|
|
75
|
+
key,
|
|
76
|
+
old_value: JSON.stringify(oldValue),
|
|
77
|
+
new_value: JSON.stringify(newValue),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return wrapSuccess('config', { mode: 'set', key, oldValue, newValue } as ConfigData);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
83
|
+
return wrapError('config', 'INVALID_CONFIG', msg) as CLIResponse<ConfigData>;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function formatValue(v: unknown): string {
|
|
88
|
+
if (typeof v === 'string') return v;
|
|
89
|
+
return JSON.stringify(v);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function formatConfigHuman(data: ConfigData): string {
|
|
93
|
+
switch (data.mode) {
|
|
94
|
+
case 'list': {
|
|
95
|
+
const rows = data.entries.map((e) => [
|
|
96
|
+
e.key,
|
|
97
|
+
formatValue(e.value),
|
|
98
|
+
formatValue(e.default),
|
|
99
|
+
]);
|
|
100
|
+
return formatTable(['Key', 'Value', 'Default'], rows);
|
|
101
|
+
}
|
|
102
|
+
case 'get':
|
|
103
|
+
return `${data.entry.key} = ${formatValue(data.entry.value)}`;
|
|
104
|
+
case 'set':
|
|
105
|
+
return `${data.key}: ${formatValue(data.oldValue)} → ${formatValue(data.newValue)}`;
|
|
106
|
+
}
|
|
107
|
+
}
|