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,662 @@
|
|
|
1
|
+
import { getDb } from '../db/index.js';
|
|
2
|
+
import { formatBoxHeader } from './formatters.js';
|
|
3
|
+
import { insertEdge } from '../db/edge.js';
|
|
4
|
+
import { getLatestReport } from '../db/octagon-cache.js';
|
|
5
|
+
import { auditTrail } from '../audit/index.js';
|
|
6
|
+
import { EdgeComputer } from '../scan/edge-computer.js';
|
|
7
|
+
import { OctagonClient } from '../scan/octagon-client.js';
|
|
8
|
+
import { createOctagonInvoker } from '../scan/invoker.js';
|
|
9
|
+
import * as readline from 'node:readline';
|
|
10
|
+
import { callKalshiApi, KalshiApiError } from '../tools/kalshi/api.js';
|
|
11
|
+
import type { KalshiMarket, KalshiEvent, KalshiOrder, KalshiPosition } from '../tools/kalshi/types.js';
|
|
12
|
+
import { openPosition, closePosition, getOpenPositions } from '../db/positions.js';
|
|
13
|
+
import { logTrade } from '../db/trades.js';
|
|
14
|
+
import { formatRawReport, parseMarketProb, parsePriceField } from '../controllers/browse.js';
|
|
15
|
+
import type { PriceDriver, Catalyst, Source } from '../scan/types.js';
|
|
16
|
+
import { kellySize, getVolume24h } from '../risk/kelly.js';
|
|
17
|
+
import type { KellyResult } from '../risk/kelly.js';
|
|
18
|
+
import { riskGate } from '../risk/gate.js';
|
|
19
|
+
import { getBotSetting } from '../utils/bot-config.js';
|
|
20
|
+
import type { RiskGateResult } from '../risk/gate.js';
|
|
21
|
+
import { formatTable } from './scan-formatters.js';
|
|
22
|
+
|
|
23
|
+
export interface AnalyzeData {
|
|
24
|
+
ticker: string;
|
|
25
|
+
eventTicker: string;
|
|
26
|
+
title: string;
|
|
27
|
+
expirationTime: string | null;
|
|
28
|
+
modelLastUpdated: string | null;
|
|
29
|
+
modelProb: number;
|
|
30
|
+
marketProb: number;
|
|
31
|
+
edge: number;
|
|
32
|
+
edgePp: string;
|
|
33
|
+
confidence: string;
|
|
34
|
+
mispricingSignal: string;
|
|
35
|
+
signal: string;
|
|
36
|
+
drivers: PriceDriver[];
|
|
37
|
+
catalysts: Catalyst[];
|
|
38
|
+
sources: Source[];
|
|
39
|
+
kelly: KellyResult;
|
|
40
|
+
riskGate: RiskGateResult;
|
|
41
|
+
liquidityGrade: string;
|
|
42
|
+
fromCache: boolean;
|
|
43
|
+
reportAge: string | null;
|
|
44
|
+
reportId: string;
|
|
45
|
+
rawReport: string;
|
|
46
|
+
existingPosition?: { direction: 'yes' | 'no'; size: number } | null;
|
|
47
|
+
closePriceCents?: number | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
function deriveLiquidityGrade(market: KalshiMarket): string {
|
|
52
|
+
const bid = parsePriceField(market.yes_bid_dollars, market.dollar_yes_bid, market.yes_bid);
|
|
53
|
+
const ask = parsePriceField(market.yes_ask_dollars, market.dollar_yes_ask, market.yes_ask);
|
|
54
|
+
const spreadCents = Number.isFinite(bid) && Number.isFinite(ask) ? Math.round((ask - bid) * 100) : 99;
|
|
55
|
+
const volume = getVolume24h(market);
|
|
56
|
+
if (spreadCents <= 2 && volume >= 5000) return 'Excellent';
|
|
57
|
+
if (spreadCents <= 4 && volume >= 1000) return 'Good';
|
|
58
|
+
return 'Poor';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function formatAge(epochSeconds: number): string {
|
|
62
|
+
const ageMs = Date.now() - epochSeconds * 1000;
|
|
63
|
+
const mins = Math.floor(ageMs / 60000);
|
|
64
|
+
if (mins < 60) return `${mins}m ago`;
|
|
65
|
+
const hours = Math.floor(mins / 60);
|
|
66
|
+
if (hours < 24) return `${hours}h ago`;
|
|
67
|
+
const days = Math.floor(hours / 24);
|
|
68
|
+
return `${days}d ago`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getVolume(m: KalshiMarket): number {
|
|
72
|
+
if (m.volume_fp != null) {
|
|
73
|
+
const v = parseFloat(m.volume_fp);
|
|
74
|
+
if (Number.isFinite(v)) return v;
|
|
75
|
+
}
|
|
76
|
+
return m.volume || 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Resolve a user-provided ticker to a market ticker.
|
|
81
|
+
* Accepts: market ticker, event ticker, or series ticker.
|
|
82
|
+
* Returns the resolved KalshiMarket (picking the most active open market for events/series).
|
|
83
|
+
*/
|
|
84
|
+
export async function resolveMarket(input: string): Promise<KalshiMarket> {
|
|
85
|
+
// 1. Try as a market ticker first
|
|
86
|
+
try {
|
|
87
|
+
const res = await callKalshiApi('GET', `/markets/${input}`);
|
|
88
|
+
const m = (res.market ?? res) as KalshiMarket;
|
|
89
|
+
if (m.ticker) return m;
|
|
90
|
+
} catch (err) {
|
|
91
|
+
if (!(err instanceof KalshiApiError && err.statusCode === 404)) throw err;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 2. Try as an event ticker
|
|
95
|
+
try {
|
|
96
|
+
const res = await callKalshiApi('GET', `/events/${input}`, {
|
|
97
|
+
params: { with_nested_markets: true },
|
|
98
|
+
});
|
|
99
|
+
const ev = (res.event ?? res) as KalshiEvent;
|
|
100
|
+
const markets = (ev.markets ?? []).filter(
|
|
101
|
+
(m: KalshiMarket) => m.status === 'open' || m.status === 'active',
|
|
102
|
+
);
|
|
103
|
+
if (markets.length > 0) {
|
|
104
|
+
markets.sort((a, b) => getVolume(b) - getVolume(a));
|
|
105
|
+
return markets[0];
|
|
106
|
+
}
|
|
107
|
+
} catch (err) {
|
|
108
|
+
if (!(err instanceof KalshiApiError && err.statusCode === 404)) throw err;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 3. Try as a series ticker — fetch recent events, then get their markets
|
|
112
|
+
try {
|
|
113
|
+
const res = await callKalshiApi('GET', '/events', {
|
|
114
|
+
params: { series_ticker: input, status: 'open', limit: 5 },
|
|
115
|
+
});
|
|
116
|
+
const events = (res.events ?? []) as KalshiEvent[];
|
|
117
|
+
const allMarkets: KalshiMarket[] = [];
|
|
118
|
+
for (const ev of events) {
|
|
119
|
+
if (!ev.event_ticker) continue;
|
|
120
|
+
try {
|
|
121
|
+
const evRes = await callKalshiApi('GET', `/events/${ev.event_ticker}`, {
|
|
122
|
+
params: { with_nested_markets: true },
|
|
123
|
+
});
|
|
124
|
+
const fullEv = (evRes.event ?? evRes) as KalshiEvent;
|
|
125
|
+
for (const m of (fullEv.markets ?? []) as KalshiMarket[]) {
|
|
126
|
+
if (m.status === 'open' || m.status === 'active') {
|
|
127
|
+
allMarkets.push(m);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
// skip events that fail to fetch
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (allMarkets.length > 0) {
|
|
135
|
+
allMarkets.sort((a, b) => getVolume(b) - getVolume(a));
|
|
136
|
+
return allMarkets[0];
|
|
137
|
+
}
|
|
138
|
+
} catch (err) {
|
|
139
|
+
if (!(err instanceof KalshiApiError && err.statusCode === 404)) throw err;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
throw new Error(`Could not find a market for "${input}". Try a full market ticker (e.g. KXBTC-26MAR14-T50049), event ticker (e.g. KXBTC-26MAR14), or series ticker (e.g. KXBTC).`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function handleAnalyze(
|
|
146
|
+
ticker: string,
|
|
147
|
+
refresh = false,
|
|
148
|
+
providedPosition?: { direction: 'yes' | 'no'; size: number } | null,
|
|
149
|
+
): Promise<AnalyzeData> {
|
|
150
|
+
const db = getDb();
|
|
151
|
+
|
|
152
|
+
// Resolve input to a market — accepts market, event, or series tickers
|
|
153
|
+
const market = await resolveMarket(ticker);
|
|
154
|
+
const resolvedTicker = market.ticker;
|
|
155
|
+
const eventTicker = market.event_ticker;
|
|
156
|
+
const marketProb = parseMarketProb(market);
|
|
157
|
+
if (marketProb === null) {
|
|
158
|
+
throw new Error(`No last traded price for ${resolvedTicker} — market may have no trades yet.`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const invoker = createOctagonInvoker();
|
|
162
|
+
const octagonClient = new OctagonClient(invoker, db, auditTrail);
|
|
163
|
+
const edgeComputer = new EdgeComputer(db, auditTrail);
|
|
164
|
+
|
|
165
|
+
// Use cache by default; only refresh when explicitly requested
|
|
166
|
+
// Try prefetch first to avoid an individual Octagon API call
|
|
167
|
+
let variant: 'cache' | 'refresh' = refresh ? 'refresh' : 'cache';
|
|
168
|
+
let report = (!refresh ? octagonClient.tryFromPrefetch(resolvedTicker, eventTicker) : null)
|
|
169
|
+
?? await octagonClient.fetchReport(resolvedTicker, eventTicker, variant);
|
|
170
|
+
|
|
171
|
+
// If cache returned no meaningful data, auto-fetch fresh
|
|
172
|
+
let usedFresh = refresh;
|
|
173
|
+
if (!refresh && report.cacheMiss) {
|
|
174
|
+
try {
|
|
175
|
+
report = await octagonClient.fetchReport(resolvedTicker, eventTicker, 'refresh');
|
|
176
|
+
usedFresh = true;
|
|
177
|
+
} catch (err) {
|
|
178
|
+
// Auto-refresh failed — continue with cache-miss report rather than crashing
|
|
179
|
+
// The user can explicitly --refresh to retry
|
|
180
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
181
|
+
console.error(` ⚠ Auto-refresh failed: ${msg}`);
|
|
182
|
+
console.error(` Showing cached data. Run \`analyze ${ticker} --refresh\` to retry.`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const fromCache = !usedFresh;
|
|
187
|
+
const latestDbReport = getLatestReport(db, resolvedTicker);
|
|
188
|
+
const reportAge = latestDbReport ? formatAge(latestDbReport.fetched_at) : null;
|
|
189
|
+
|
|
190
|
+
const snapshot = edgeComputer.computeEdge(resolvedTicker, report, marketProb);
|
|
191
|
+
|
|
192
|
+
// Persist edge
|
|
193
|
+
insertEdge(db, {
|
|
194
|
+
ticker: snapshot.ticker,
|
|
195
|
+
event_ticker: snapshot.eventTicker,
|
|
196
|
+
timestamp: snapshot.timestamp,
|
|
197
|
+
model_prob: snapshot.modelProb,
|
|
198
|
+
market_prob: snapshot.marketProb,
|
|
199
|
+
edge: snapshot.edge,
|
|
200
|
+
octagon_report_id: snapshot.octagonReportId,
|
|
201
|
+
drivers_json: JSON.stringify(snapshot.drivers),
|
|
202
|
+
sources_json: JSON.stringify(snapshot.sources),
|
|
203
|
+
catalysts_json: JSON.stringify(snapshot.catalysts),
|
|
204
|
+
cache_hit: fromCache ? 1 : 0,
|
|
205
|
+
cache_miss: report.cacheMiss ? 1 : 0,
|
|
206
|
+
confidence: snapshot.confidence,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Kelly sizing — wrapped in try/catch for demo mode (portfolio endpoints may 401)
|
|
210
|
+
let kelly: KellyResult;
|
|
211
|
+
try {
|
|
212
|
+
kelly = await kellySize({
|
|
213
|
+
edge: snapshot.edge,
|
|
214
|
+
marketProb,
|
|
215
|
+
market,
|
|
216
|
+
multiplier: getBotSetting('risk.kelly_multiplier') as number | undefined,
|
|
217
|
+
minEdgeThreshold: getBotSetting('risk.min_edge_threshold') as number | undefined,
|
|
218
|
+
});
|
|
219
|
+
} catch {
|
|
220
|
+
kelly = {
|
|
221
|
+
side: snapshot.edge >= 0 ? 'yes' : 'no',
|
|
222
|
+
fraction: 0,
|
|
223
|
+
adjustedFraction: 0,
|
|
224
|
+
contracts: 0,
|
|
225
|
+
dollarAmountCents: 0,
|
|
226
|
+
entryPriceCents: 0,
|
|
227
|
+
availableBankroll: 0,
|
|
228
|
+
openExposure: 0,
|
|
229
|
+
cashBalance: 0,
|
|
230
|
+
portfolioValue: 0,
|
|
231
|
+
liquidityAdjusted: false,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Risk gate
|
|
236
|
+
const gate = riskGate({ ticker: resolvedTicker, eventTicker, kelly, market, db });
|
|
237
|
+
|
|
238
|
+
// Use caller-provided position or fetch from API when not provided
|
|
239
|
+
let existingPosition: { direction: 'yes' | 'no'; size: number } | null =
|
|
240
|
+
providedPosition !== undefined ? (providedPosition ?? null) : null;
|
|
241
|
+
if (providedPosition === undefined) {
|
|
242
|
+
try {
|
|
243
|
+
const posData = await callKalshiApi('GET', '/portfolio/positions', {
|
|
244
|
+
params: { ticker: resolvedTicker },
|
|
245
|
+
});
|
|
246
|
+
const positions = (posData.market_positions ?? posData.positions ?? []) as KalshiPosition[];
|
|
247
|
+
const match = positions.find((p) => p.ticker === resolvedTicker);
|
|
248
|
+
if (match) {
|
|
249
|
+
const rawPos = parseFloat(String(match.position ?? '0'));
|
|
250
|
+
if (rawPos !== 0) {
|
|
251
|
+
existingPosition = {
|
|
252
|
+
direction: rawPos > 0 ? 'yes' : 'no',
|
|
253
|
+
size: Math.abs(Math.round(rawPos)),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
} catch {
|
|
258
|
+
// Position fetch failed (e.g. demo mode) — continue without
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Build signal — position-aware
|
|
263
|
+
const side = snapshot.edge > 0 ? 'YES' : 'NO';
|
|
264
|
+
const yesAsk = parsePriceField(market.yes_ask_dollars, market.dollar_yes_ask, market.yes_ask);
|
|
265
|
+
const noAsk = parsePriceField(market.no_ask_dollars, market.dollar_no_ask, market.no_ask);
|
|
266
|
+
const yesBid = parsePriceField(market.yes_bid_dollars, market.dollar_yes_bid, market.yes_bid);
|
|
267
|
+
const noBid = parsePriceField(market.no_bid_dollars, market.dollar_no_bid, market.no_bid);
|
|
268
|
+
const entryPrice = (snapshot.edge > 0 ? yesAsk : noAsk);
|
|
269
|
+
|
|
270
|
+
let signal: string;
|
|
271
|
+
if (existingPosition) {
|
|
272
|
+
const holdDir = existingPosition.direction.toUpperCase();
|
|
273
|
+
const edgeReversed =
|
|
274
|
+
(existingPosition.direction === 'yes' && snapshot.edge < -0.03) ||
|
|
275
|
+
(existingPosition.direction === 'no' && snapshot.edge > 0.03);
|
|
276
|
+
if (edgeReversed) {
|
|
277
|
+
const closePrice = existingPosition.direction === 'yes' ? yesBid : noBid;
|
|
278
|
+
signal = Number.isFinite(closePrice)
|
|
279
|
+
? `SELL ${holdDir} @ $${closePrice.toFixed(2)} (close position)`
|
|
280
|
+
: `SELL ${holdDir} (close position)`;
|
|
281
|
+
} else {
|
|
282
|
+
signal = `HOLD (long ${holdDir} ×${existingPosition.size})`;
|
|
283
|
+
}
|
|
284
|
+
} else {
|
|
285
|
+
signal = Number.isFinite(entryPrice) ? `BUY ${side} @ $${entryPrice.toFixed(2)}` : `BUY ${side}`;
|
|
286
|
+
}
|
|
287
|
+
const edgePp = `${snapshot.edge >= 0 ? '+' : ''}${(snapshot.edge * 100).toFixed(0)}pp`;
|
|
288
|
+
|
|
289
|
+
const mispricingSignal = snapshot.edge > 0.02
|
|
290
|
+
? 'underpriced'
|
|
291
|
+
: snapshot.edge < -0.02
|
|
292
|
+
? 'overpriced'
|
|
293
|
+
: 'fair_value';
|
|
294
|
+
|
|
295
|
+
// Audit
|
|
296
|
+
auditTrail.log({
|
|
297
|
+
type: 'RECOMMENDATION',
|
|
298
|
+
ticker: resolvedTicker,
|
|
299
|
+
action: signal,
|
|
300
|
+
size: kelly.contracts,
|
|
301
|
+
kelly: kelly.adjustedFraction,
|
|
302
|
+
risk_gate: gate.passed ? 'PASSED' : 'FAILED',
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Model last-updated timestamp
|
|
306
|
+
const modelUpdatedAt = latestDbReport
|
|
307
|
+
? new Date(latestDbReport.fetched_at * 1000).toISOString().replace('T', ' ').slice(0, 16) + ' UTC'
|
|
308
|
+
: null;
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
ticker: resolvedTicker,
|
|
312
|
+
eventTicker,
|
|
313
|
+
title: market.title || market.subtitle || resolvedTicker,
|
|
314
|
+
expirationTime: market.expiration_time || market.expected_expiration_time || market.close_time || null,
|
|
315
|
+
modelLastUpdated: modelUpdatedAt,
|
|
316
|
+
modelProb: snapshot.modelProb,
|
|
317
|
+
marketProb,
|
|
318
|
+
edge: snapshot.edge,
|
|
319
|
+
edgePp,
|
|
320
|
+
confidence: snapshot.confidence,
|
|
321
|
+
mispricingSignal,
|
|
322
|
+
signal,
|
|
323
|
+
drivers: snapshot.drivers,
|
|
324
|
+
catalysts: snapshot.catalysts,
|
|
325
|
+
sources: snapshot.sources,
|
|
326
|
+
kelly,
|
|
327
|
+
riskGate: gate,
|
|
328
|
+
liquidityGrade: deriveLiquidityGrade(market),
|
|
329
|
+
fromCache,
|
|
330
|
+
reportAge,
|
|
331
|
+
reportId: report.reportId,
|
|
332
|
+
rawReport: report.rawResponse,
|
|
333
|
+
existingPosition,
|
|
334
|
+
closePriceCents: existingPosition
|
|
335
|
+
? Math.round((existingPosition.direction === 'yes' ? yesBid : noBid) * 100) || null
|
|
336
|
+
: null,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function formatAnalyzeHuman(data: AnalyzeData): string {
|
|
341
|
+
const lines: string[] = [];
|
|
342
|
+
|
|
343
|
+
lines.push(...formatBoxHeader('MARKET ANALYSIS'));
|
|
344
|
+
lines.push('');
|
|
345
|
+
lines.push(` Title: ${data.title}`);
|
|
346
|
+
lines.push(` Ticker: ${data.ticker}`);
|
|
347
|
+
lines.push(` Event: ${data.eventTicker}`);
|
|
348
|
+
if (data.expirationTime) {
|
|
349
|
+
const exp = new Date(data.expirationTime);
|
|
350
|
+
lines.push(` Expires: ${exp.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })} ${exp.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', timeZoneName: 'short' })}`);
|
|
351
|
+
}
|
|
352
|
+
lines.push(` Signal: ${data.signal}`);
|
|
353
|
+
if (data.existingPosition) {
|
|
354
|
+
lines.push(` Position: ${data.existingPosition.direction.toUpperCase()} ×${data.existingPosition.size}`);
|
|
355
|
+
}
|
|
356
|
+
lines.push('');
|
|
357
|
+
|
|
358
|
+
// Edge & Probabilities
|
|
359
|
+
lines.push(` Model Prob: ${(data.modelProb * 100).toFixed(1)}%`);
|
|
360
|
+
lines.push(` Market Prob: ${(data.marketProb * 100).toFixed(1)}%`);
|
|
361
|
+
lines.push(` Edge: ${data.edgePp} (${(data.edge * 100).toFixed(1)}%)`);
|
|
362
|
+
lines.push(` Confidence: ${data.confidence}`);
|
|
363
|
+
lines.push(` Mispricing: ${data.mispricingSignal}`);
|
|
364
|
+
lines.push('');
|
|
365
|
+
|
|
366
|
+
// Price Drivers
|
|
367
|
+
if (data.drivers.length > 0) {
|
|
368
|
+
lines.push(' Price Drivers:');
|
|
369
|
+
for (const d of data.drivers) {
|
|
370
|
+
const src = d.sourceUrl ? ` (${d.sourceUrl})` : '';
|
|
371
|
+
lines.push(` • [${d.impact.toUpperCase()}/${d.category}] ${d.claim}${src}`);
|
|
372
|
+
}
|
|
373
|
+
lines.push('');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Catalyst Calendar
|
|
377
|
+
if (data.catalysts.length > 0) {
|
|
378
|
+
lines.push(' Catalyst Calendar:');
|
|
379
|
+
const catRows = data.catalysts.map((c) => [
|
|
380
|
+
c.date || '-',
|
|
381
|
+
c.event,
|
|
382
|
+
c.impact.toUpperCase(),
|
|
383
|
+
c.potentialMove || '-',
|
|
384
|
+
]);
|
|
385
|
+
lines.push(formatTable(
|
|
386
|
+
['Date', 'Event', 'Impact', 'Potential Move'],
|
|
387
|
+
catRows,
|
|
388
|
+
));
|
|
389
|
+
lines.push('');
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Kelly Sizing
|
|
393
|
+
lines.push(' Position Sizing (Half-Kelly):');
|
|
394
|
+
lines.push(` Side: ${data.kelly.side.toUpperCase()}`);
|
|
395
|
+
lines.push(` Cash Balance: $${(data.kelly.cashBalance / 100).toFixed(2)}`);
|
|
396
|
+
lines.push(` Open Exposure: $${(data.kelly.openExposure / 100).toFixed(2)}`);
|
|
397
|
+
lines.push(` Available: $${(data.kelly.availableBankroll / 100).toFixed(2)}`);
|
|
398
|
+
lines.push(` Contracts: ${data.kelly.contracts}`);
|
|
399
|
+
lines.push(` Dollar Amount: $${(data.kelly.dollarAmountCents / 100).toFixed(2)}`);
|
|
400
|
+
lines.push(` Entry Price: ${data.kelly.entryPriceCents}¢`);
|
|
401
|
+
lines.push(` Kelly f*: ${(data.kelly.fraction * 100).toFixed(1)}%`);
|
|
402
|
+
lines.push(` Adjusted f: ${(data.kelly.adjustedFraction * 100).toFixed(1)}%`);
|
|
403
|
+
if (data.kelly.liquidityAdjusted) {
|
|
404
|
+
lines.push(' ⚠ Liquidity-adjusted (wide spread or low volume)');
|
|
405
|
+
}
|
|
406
|
+
if (data.kelly.skippedReason) {
|
|
407
|
+
lines.push(` ⚠ ${data.kelly.skippedReason}`);
|
|
408
|
+
}
|
|
409
|
+
lines.push('');
|
|
410
|
+
|
|
411
|
+
// Risk Gate
|
|
412
|
+
const gateIcon = data.riskGate.passed ? '✓' : '✗';
|
|
413
|
+
lines.push(` Risk Gate: ${gateIcon} ${data.riskGate.passed ? 'PASSED' : 'FAILED'}`);
|
|
414
|
+
for (const check of data.riskGate.checks) {
|
|
415
|
+
const icon = check.passed ? '✓' : '✗';
|
|
416
|
+
lines.push(` ${icon} ${check.name}: ${check.reason}`);
|
|
417
|
+
}
|
|
418
|
+
lines.push('');
|
|
419
|
+
lines.push(` Liquidity: ${data.liquidityGrade}`);
|
|
420
|
+
|
|
421
|
+
// Sources
|
|
422
|
+
if (data.sources.length > 0) {
|
|
423
|
+
lines.push('');
|
|
424
|
+
lines.push(' Sources:');
|
|
425
|
+
for (const s of data.sources) {
|
|
426
|
+
const title = s.title ? `${s.title}: ` : '';
|
|
427
|
+
lines.push(` • ${title}${s.url}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Cache status & model timestamp
|
|
432
|
+
lines.push('');
|
|
433
|
+
if (data.modelLastUpdated) {
|
|
434
|
+
lines.push(` Model Updated: ${data.modelLastUpdated}`);
|
|
435
|
+
}
|
|
436
|
+
if (data.fromCache && data.reportAge) {
|
|
437
|
+
lines.push(` Data: cached (${data.reportAge}). Run \`analyze ${data.ticker} --refresh\` for latest (costs 3 credits).`);
|
|
438
|
+
} else if (data.fromCache) {
|
|
439
|
+
lines.push(` Data: cached. Run \`analyze ${data.ticker} --refresh\` for latest (costs 3 credits).`);
|
|
440
|
+
} else {
|
|
441
|
+
lines.push(' Data: freshly generated.');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return lines.join('\n');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Interactive post-analyze menu. Presents options to view the full report,
|
|
449
|
+
* refresh the report, or place the suggested trade.
|
|
450
|
+
*/
|
|
451
|
+
export async function promptAnalyzeActions(data: AnalyzeData): Promise<void> {
|
|
452
|
+
if (!process.stdin.isTTY) return;
|
|
453
|
+
|
|
454
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
455
|
+
const ask = (q: string) => new Promise<string>((resolve) => {
|
|
456
|
+
rl.question(q, (ans) => resolve(ans.trim()));
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
const menu = [
|
|
460
|
+
' 1) View full report',
|
|
461
|
+
' 2) Refresh report (costs credits)',
|
|
462
|
+
' 3) Make suggested trade',
|
|
463
|
+
' 4) Exit',
|
|
464
|
+
].join('\n');
|
|
465
|
+
|
|
466
|
+
let running = true;
|
|
467
|
+
while (running) {
|
|
468
|
+
console.log(`\n${menu}`);
|
|
469
|
+
const choice = await ask('\n Choose [1-4]: ');
|
|
470
|
+
|
|
471
|
+
switch (choice) {
|
|
472
|
+
case '1': {
|
|
473
|
+
if (data.rawReport) {
|
|
474
|
+
console.log('\n' + formatRawReport(data.rawReport, data.ticker));
|
|
475
|
+
} else {
|
|
476
|
+
console.log(' No report available. Try option 2 to refresh.');
|
|
477
|
+
}
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
case '2': {
|
|
482
|
+
console.log(' Fetching fresh report…');
|
|
483
|
+
try {
|
|
484
|
+
const freshData = await handleAnalyze(data.ticker, true);
|
|
485
|
+
data = freshData;
|
|
486
|
+
console.log(formatAnalyzeHuman(data));
|
|
487
|
+
} catch (err) {
|
|
488
|
+
console.error(` Refresh failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
489
|
+
}
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
case '3': {
|
|
494
|
+
// Determine if this is a SELL (close position) or BUY (open position)
|
|
495
|
+
const isSell = data.signal.startsWith('SELL');
|
|
496
|
+
const isHold = data.signal.startsWith('HOLD');
|
|
497
|
+
|
|
498
|
+
if (isHold) {
|
|
499
|
+
console.log(' Signal is HOLD — no trade suggested.');
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (isSell && data.existingPosition) {
|
|
504
|
+
// Close position: sell what we hold
|
|
505
|
+
const sellSide = data.existingPosition.direction;
|
|
506
|
+
const sellSize = data.existingPosition.size;
|
|
507
|
+
const closePrice = data.closePriceCents ?? Math.round(
|
|
508
|
+
(sellSide === 'yes' ? data.marketProb : 1 - data.marketProb) * 100
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
console.log(` Signal: SELL ${sellSize} ${sellSide.toUpperCase()} @ ${closePrice}¢ (close position)`);
|
|
512
|
+
const confirm = await ask(' Execute? [y/n] ');
|
|
513
|
+
if (confirm.toLowerCase() !== 'y' && confirm.toLowerCase() !== 'yes') {
|
|
514
|
+
console.log(' Trade cancelled.');
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
try {
|
|
519
|
+
const orderPayload: Record<string, unknown> = {
|
|
520
|
+
ticker: data.ticker,
|
|
521
|
+
action: 'sell',
|
|
522
|
+
side: sellSide,
|
|
523
|
+
type: 'limit',
|
|
524
|
+
count: sellSize,
|
|
525
|
+
};
|
|
526
|
+
if (sellSide === 'yes') orderPayload.yes_price = closePrice;
|
|
527
|
+
else orderPayload.no_price = closePrice;
|
|
528
|
+
|
|
529
|
+
const orderRes = await callKalshiApi('POST', '/portfolio/orders', { body: orderPayload });
|
|
530
|
+
const order = (orderRes.order ?? orderRes) as KalshiOrder;
|
|
531
|
+
|
|
532
|
+
const db = getDb();
|
|
533
|
+
const now = Math.floor(Date.now() / 1000);
|
|
534
|
+
|
|
535
|
+
// Find matching open DB position for this ticker to close
|
|
536
|
+
const dbPositions = getOpenPositions(db);
|
|
537
|
+
const dbMatch = dbPositions.find(
|
|
538
|
+
(p) => p.ticker === data.ticker && p.direction === sellSide,
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
logTrade(db, {
|
|
542
|
+
trade_id: crypto.randomUUID(),
|
|
543
|
+
position_id: dbMatch?.position_id ?? '',
|
|
544
|
+
order_id: order.order_id,
|
|
545
|
+
ticker: data.ticker,
|
|
546
|
+
action: 'sell',
|
|
547
|
+
side: sellSide,
|
|
548
|
+
size: sellSize,
|
|
549
|
+
price: closePrice,
|
|
550
|
+
fill_status: order.status,
|
|
551
|
+
kalshi_response: JSON.stringify(order),
|
|
552
|
+
created_at: now,
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
auditTrail.log({
|
|
556
|
+
type: 'TRADE_EXECUTED',
|
|
557
|
+
ticker: data.ticker,
|
|
558
|
+
order_id: order.order_id,
|
|
559
|
+
fill_price: closePrice,
|
|
560
|
+
size: sellSize,
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// If order filled immediately, close the DB position
|
|
564
|
+
if (dbMatch && order.status === 'filled') {
|
|
565
|
+
closePosition(db, dbMatch.position_id, now);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
console.log(` Sell order placed: ${order.order_id} (${order.status})`);
|
|
569
|
+
} catch (err) {
|
|
570
|
+
console.error(` Trade failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
571
|
+
}
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (!data.riskGate.passed) {
|
|
576
|
+
console.log(' Risk gate FAILED — trade blocked.');
|
|
577
|
+
break;
|
|
578
|
+
}
|
|
579
|
+
if (data.kelly.contracts === 0) {
|
|
580
|
+
console.log(` Kelly sizing produced 0 contracts${data.kelly.skippedReason ? `: ${data.kelly.skippedReason}` : ''}.`);
|
|
581
|
+
break;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const side = data.edge > 0 ? 'yes' : 'no';
|
|
585
|
+
const price = data.kelly.entryPriceCents;
|
|
586
|
+
console.log(` Signal: BUY ${data.kelly.contracts} ${side.toUpperCase()} @ ${price}¢`);
|
|
587
|
+
const confirm = await ask(' Execute? [y/n] ');
|
|
588
|
+
if (confirm.toLowerCase() !== 'y' && confirm.toLowerCase() !== 'yes') {
|
|
589
|
+
console.log(' Trade cancelled.');
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
try {
|
|
594
|
+
const orderPayload: Record<string, unknown> = {
|
|
595
|
+
ticker: data.ticker,
|
|
596
|
+
action: 'buy',
|
|
597
|
+
side,
|
|
598
|
+
type: 'limit',
|
|
599
|
+
count: data.kelly.contracts,
|
|
600
|
+
};
|
|
601
|
+
if (side === 'yes') orderPayload.yes_price = price;
|
|
602
|
+
else orderPayload.no_price = price;
|
|
603
|
+
|
|
604
|
+
const orderRes = await callKalshiApi('POST', '/portfolio/orders', { body: orderPayload });
|
|
605
|
+
const order = (orderRes.order ?? orderRes) as KalshiOrder;
|
|
606
|
+
|
|
607
|
+
const db = getDb();
|
|
608
|
+
const positionId = crypto.randomUUID();
|
|
609
|
+
const now = Math.floor(Date.now() / 1000);
|
|
610
|
+
|
|
611
|
+
openPosition(db, {
|
|
612
|
+
position_id: positionId,
|
|
613
|
+
ticker: data.ticker,
|
|
614
|
+
event_ticker: data.eventTicker,
|
|
615
|
+
direction: side,
|
|
616
|
+
size: data.kelly.contracts,
|
|
617
|
+
entry_price: price,
|
|
618
|
+
entry_edge: data.edge,
|
|
619
|
+
entry_kelly: data.kelly.adjustedFraction,
|
|
620
|
+
current_pnl: 0,
|
|
621
|
+
status: 'open',
|
|
622
|
+
opened_at: now,
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
logTrade(db, {
|
|
626
|
+
trade_id: crypto.randomUUID(),
|
|
627
|
+
position_id: positionId,
|
|
628
|
+
order_id: order.order_id,
|
|
629
|
+
ticker: data.ticker,
|
|
630
|
+
action: 'buy',
|
|
631
|
+
side,
|
|
632
|
+
size: data.kelly.contracts,
|
|
633
|
+
price,
|
|
634
|
+
fill_status: order.status,
|
|
635
|
+
kalshi_response: JSON.stringify(order),
|
|
636
|
+
created_at: now,
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
auditTrail.log({
|
|
640
|
+
type: 'TRADE_EXECUTED',
|
|
641
|
+
ticker: data.ticker,
|
|
642
|
+
order_id: order.order_id,
|
|
643
|
+
fill_price: price,
|
|
644
|
+
size: data.kelly.contracts,
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
console.log(` Order placed: ${order.order_id} (${order.status})`);
|
|
648
|
+
} catch (err) {
|
|
649
|
+
console.error(` Trade failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
650
|
+
}
|
|
651
|
+
break;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
case '4':
|
|
655
|
+
default:
|
|
656
|
+
running = false;
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
rl.close();
|
|
662
|
+
}
|