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,339 @@
|
|
|
1
|
+
import type { KalshiMarket, KalshiPosition, KalshiOrder } from '../tools/kalshi/types.js';
|
|
2
|
+
|
|
3
|
+
// ─── Box header helper ───────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
const BOX_WIDTH = 40;
|
|
6
|
+
|
|
7
|
+
export function formatBoxHeader(title: string): string[] {
|
|
8
|
+
const inner = BOX_WIDTH - 2; // space between ║ walls
|
|
9
|
+
const safeTitle = title.length > inner ? title.slice(0, inner - 1) + '…' : title;
|
|
10
|
+
const pad = inner - safeTitle.length;
|
|
11
|
+
const left = Math.floor(pad / 2);
|
|
12
|
+
const right = pad - left;
|
|
13
|
+
return [
|
|
14
|
+
'',
|
|
15
|
+
'╔' + '═'.repeat(inner) + '╗',
|
|
16
|
+
'║' + ' '.repeat(left) + safeTitle + ' '.repeat(right) + '║',
|
|
17
|
+
'╚' + '═'.repeat(inner) + '╝',
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Actual Kalshi /portfolio/balance response shape */
|
|
22
|
+
export interface KalshiBalanceResponse {
|
|
23
|
+
balance: number;
|
|
24
|
+
portfolio_value: number;
|
|
25
|
+
updated_ts?: number;
|
|
26
|
+
// Legacy fields (may be present in some API versions)
|
|
27
|
+
payout?: number;
|
|
28
|
+
reserved_fees?: number;
|
|
29
|
+
fees?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ─── Value parsers ────────────────────────────────────────────────────────────
|
|
33
|
+
// Kalshi API returns prices as "_dollars" string fields (e.g. "0.5600")
|
|
34
|
+
// or as integer cents in older API versions. Handle both.
|
|
35
|
+
|
|
36
|
+
function parseDollars(val: string | number | undefined | null): number | undefined {
|
|
37
|
+
if (val === undefined || val === null) return undefined;
|
|
38
|
+
const n = typeof val === 'number' ? val : parseFloat(val as string);
|
|
39
|
+
return isNaN(n) ? undefined : n;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parsePosition(val: string | number | undefined | null): number | undefined {
|
|
43
|
+
if (val === undefined || val === null) return undefined;
|
|
44
|
+
const n = typeof val === 'number' ? val : parseFloat(val as string);
|
|
45
|
+
return isNaN(n) ? undefined : n;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Format a dollar amount (already in dollars, not cents) */
|
|
49
|
+
function fmtDollars(val: string | number | undefined | null): string {
|
|
50
|
+
const n = parseDollars(val);
|
|
51
|
+
if (n === undefined) return '-';
|
|
52
|
+
return `$${n.toFixed(2)}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Format a price field that may be integer cents OR a dollars string */
|
|
56
|
+
function fmtPrice(val: number | string | undefined | null): string {
|
|
57
|
+
if (val === undefined || val === null) return '-';
|
|
58
|
+
if (typeof val === 'string') {
|
|
59
|
+
const n = parseFloat(val);
|
|
60
|
+
if (isNaN(n)) return '-';
|
|
61
|
+
// If the string looks like "0.5600" (dollars format), show as-is
|
|
62
|
+
return `$${n.toFixed(2)}`;
|
|
63
|
+
}
|
|
64
|
+
// Integer cents (old API format): divide by 100
|
|
65
|
+
return `$${(val / 100).toFixed(2)}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Format a dollar amount from cents (integer) */
|
|
69
|
+
function fmtCents(cents: number | undefined | null): string {
|
|
70
|
+
if (cents === undefined || cents === null) return '-';
|
|
71
|
+
return `$${(cents / 100).toFixed(2)}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Format a number with commas, safely handling null/undefined */
|
|
75
|
+
function fmtNum(n: number | string | undefined | null): string {
|
|
76
|
+
if (n === undefined || n === null) return '-';
|
|
77
|
+
const val = typeof n === 'number' ? n : parseFloat(n as string);
|
|
78
|
+
if (isNaN(val)) return '-';
|
|
79
|
+
if (val === 0) return '0';
|
|
80
|
+
return val.toLocaleString();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Format ISO date string as short date */
|
|
84
|
+
function fmtDate(iso: string | undefined): string {
|
|
85
|
+
if (!iso) return '-';
|
|
86
|
+
try {
|
|
87
|
+
return new Date(iso).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: '2-digit' });
|
|
88
|
+
} catch {
|
|
89
|
+
return iso.slice(0, 10);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─── Access helpers (handle both _dollars and raw field names) ────────────────
|
|
94
|
+
|
|
95
|
+
function mktYesAsk(m: any): string | number | undefined {
|
|
96
|
+
return m.yes_ask_dollars ?? m.dollar_yes_ask ?? m.yes_ask;
|
|
97
|
+
}
|
|
98
|
+
function mktNoAsk(m: any): string | number | undefined {
|
|
99
|
+
return m.no_ask_dollars ?? m.dollar_no_ask ?? m.no_ask;
|
|
100
|
+
}
|
|
101
|
+
function mktYesBid(m: any): string | number | undefined {
|
|
102
|
+
return m.yes_bid_dollars ?? m.dollar_yes_bid ?? m.yes_bid;
|
|
103
|
+
}
|
|
104
|
+
function mktNoBid(m: any): string | number | undefined {
|
|
105
|
+
return m.no_bid_dollars ?? m.dollar_no_bid ?? m.no_bid;
|
|
106
|
+
}
|
|
107
|
+
function mktLastPrice(m: any): string | number | undefined {
|
|
108
|
+
return m.last_price_dollars ?? m.dollar_last_price ?? m.last_price;
|
|
109
|
+
}
|
|
110
|
+
function mktVolume(m: any): string | number | undefined {
|
|
111
|
+
return m.volume_fp ?? m.volume;
|
|
112
|
+
}
|
|
113
|
+
function mktOpenInterest(m: any): string | number | undefined {
|
|
114
|
+
return m.open_interest_fp ?? m.open_interest;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─── Formatters ───────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
export function formatBalance(data: KalshiBalanceResponse): string {
|
|
120
|
+
const lines: string[] = [];
|
|
121
|
+
lines.push('**Account Balance**');
|
|
122
|
+
lines.push('');
|
|
123
|
+
lines.push(`Balance: ${fmtCents(data.balance)}`);
|
|
124
|
+
lines.push(`Portfolio Value: ${fmtCents(data.portfolio_value ?? 0)}`);
|
|
125
|
+
if (data.payout !== undefined) lines.push(`Payout: ${fmtCents(data.payout)}`);
|
|
126
|
+
if (data.reserved_fees !== undefined) lines.push(`Reserved Fees: ${fmtCents(data.reserved_fees)}`);
|
|
127
|
+
if (data.fees !== undefined) lines.push(`Total Fees: ${fmtCents(data.fees)}`);
|
|
128
|
+
return lines.join('\n');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function formatPositions(positions: any[]): string {
|
|
132
|
+
if (!positions.length) return 'No open positions.';
|
|
133
|
+
|
|
134
|
+
const rows = positions.map((p) => {
|
|
135
|
+
// position_fp is the net position (number of contracts)
|
|
136
|
+
const pos = parsePosition(p.position_fp ?? p.position);
|
|
137
|
+
const posStr = pos === undefined ? '-' : pos > 0 ? `+${pos}` : String(pos);
|
|
138
|
+
const pnl = p.realized_pnl_dollars ?? (p.realized_pnl !== undefined ? (p.realized_pnl / 100).toFixed(2) : undefined);
|
|
139
|
+
const exposure = p.market_exposure_dollars ?? (p.market_exposure !== undefined ? (p.market_exposure / 100).toFixed(2) : undefined);
|
|
140
|
+
|
|
141
|
+
return [
|
|
142
|
+
p.ticker,
|
|
143
|
+
posStr,
|
|
144
|
+
fmtDollars(pnl),
|
|
145
|
+
fmtDollars(exposure),
|
|
146
|
+
String(p.resting_orders_count ?? 0),
|
|
147
|
+
];
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return formatTable(
|
|
151
|
+
['Ticker', 'Position', 'Realized P&L', 'Exposure', 'Orders'],
|
|
152
|
+
rows
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function formatOrders(orders: KalshiOrder[]): string {
|
|
157
|
+
if (!orders.length) return 'No orders found.';
|
|
158
|
+
|
|
159
|
+
const rows = orders.map((o) => {
|
|
160
|
+
const price = o.yes_price_dollars
|
|
161
|
+
? fmtDollars(o.yes_price_dollars)
|
|
162
|
+
: o.yes_price != null ? fmtCents(o.yes_price) : '-';
|
|
163
|
+
const remaining = o.remaining_count_fp ?? o.remaining_count ?? '-';
|
|
164
|
+
const initial = o.initial_count_fp ?? o.contracts_count ?? '-';
|
|
165
|
+
return [
|
|
166
|
+
o.ticker,
|
|
167
|
+
`${o.action}/${o.side}`,
|
|
168
|
+
price,
|
|
169
|
+
`${remaining}/${initial}`,
|
|
170
|
+
o.status,
|
|
171
|
+
(o.order_id ?? '').slice(0, 8) + '…',
|
|
172
|
+
];
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
return formatTable(
|
|
176
|
+
['Ticker', 'Action/Side', 'Price', 'Remaining', 'Status', 'Order ID'],
|
|
177
|
+
rows
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function formatMarkets(markets: any[]): string {
|
|
182
|
+
if (!markets.length) return 'No markets found.';
|
|
183
|
+
|
|
184
|
+
const rows = markets.map((m) => [
|
|
185
|
+
m.ticker,
|
|
186
|
+
truncate(m.title ?? '', 40),
|
|
187
|
+
fmtPrice(mktYesAsk(m)),
|
|
188
|
+
fmtPrice(mktNoAsk(m)),
|
|
189
|
+
fmtNum(mktVolume(m)),
|
|
190
|
+
fmtDate(m.close_time),
|
|
191
|
+
]);
|
|
192
|
+
|
|
193
|
+
return formatTable(
|
|
194
|
+
['Ticker', 'Title', 'YES Ask', 'NO Ask', 'Volume', 'Closes'],
|
|
195
|
+
rows
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function formatMarketDetail(market: any): string {
|
|
200
|
+
const lines: string[] = [];
|
|
201
|
+
lines.push(`**${market.ticker}**`);
|
|
202
|
+
if (market.title) lines.push(market.title);
|
|
203
|
+
if (market.subtitle) lines.push(market.subtitle);
|
|
204
|
+
lines.push('');
|
|
205
|
+
lines.push(`Status: ${market.status ?? '-'}`);
|
|
206
|
+
lines.push(`YES Bid: ${fmtPrice(mktYesBid(market))} YES Ask: ${fmtPrice(mktYesAsk(market))}`);
|
|
207
|
+
lines.push(`NO Bid: ${fmtPrice(mktNoBid(market))} NO Ask: ${fmtPrice(mktNoAsk(market))}`);
|
|
208
|
+
lines.push(`Last Price: ${fmtPrice(mktLastPrice(market))}`);
|
|
209
|
+
lines.push(`Volume: ${fmtNum(mktVolume(market))} Open Interest: ${fmtNum(mktOpenInterest(market))}`);
|
|
210
|
+
lines.push(`Closes: ${fmtDate(market.close_time)}`);
|
|
211
|
+
if (market.result) lines.push(`Result: ${market.result}`);
|
|
212
|
+
return lines.join('\n');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function formatExchangeStatus(data: Record<string, unknown>): string {
|
|
216
|
+
const active = data.exchange_active ? '✓ Exchange Active' : '✗ Exchange Inactive';
|
|
217
|
+
const trading = data.trading_active ? '✓ Trading Active' : '✗ Trading Paused';
|
|
218
|
+
return `${active}\n${trading}`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function formatOrderConfirmation(
|
|
222
|
+
ticker: string,
|
|
223
|
+
action: 'buy' | 'sell',
|
|
224
|
+
side: 'yes' | 'no',
|
|
225
|
+
count: number,
|
|
226
|
+
price: number | undefined
|
|
227
|
+
): string {
|
|
228
|
+
const priceStr = price !== undefined ? `$${(price / 100).toFixed(2)}` : 'market price';
|
|
229
|
+
const estCost = price !== undefined ? `$${((price / 100) * count).toFixed(2)}` : 'variable';
|
|
230
|
+
const lines = [
|
|
231
|
+
'**Order Preview**',
|
|
232
|
+
'',
|
|
233
|
+
`Ticker: ${ticker}`,
|
|
234
|
+
`Action: ${action.toUpperCase()} ${side.toUpperCase()}`,
|
|
235
|
+
`Count: ${count} contract${count !== 1 ? 's' : ''}`,
|
|
236
|
+
`Price: ${priceStr}`,
|
|
237
|
+
`Est. Cost: ${estCost}`,
|
|
238
|
+
];
|
|
239
|
+
return lines.join('\n');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function formatEvents(events: any[]): string {
|
|
243
|
+
if (!events.length) return 'No events found.';
|
|
244
|
+
|
|
245
|
+
const rows = events.map((e) => {
|
|
246
|
+
const markets = e.markets ?? [];
|
|
247
|
+
const marketCount = markets.length > 0 ? String(markets.length) : '-';
|
|
248
|
+
|
|
249
|
+
// Find the leading outcome (highest YES price) for the top outcome column
|
|
250
|
+
let topOutcome = '-';
|
|
251
|
+
let topPct = '-';
|
|
252
|
+
if (markets.length > 0) {
|
|
253
|
+
// For multi-market events, show the frontrunner
|
|
254
|
+
// For binary events (1 market), show the YES probability
|
|
255
|
+
const sorted = [...markets].sort((a: any, b: any) => {
|
|
256
|
+
const volA = parseFloat(a.volume_fp ?? a.volume ?? '0') || 0;
|
|
257
|
+
const volB = parseFloat(b.volume_fp ?? b.volume ?? '0') || 0;
|
|
258
|
+
return volB - volA;
|
|
259
|
+
});
|
|
260
|
+
const top = sorted[0];
|
|
261
|
+
// Handle both dollar strings ("0.1800") and integer cents (18)
|
|
262
|
+
const rawAsk = top.yes_ask_dollars ?? top.yes_ask;
|
|
263
|
+
let yesAsk = 0;
|
|
264
|
+
if (rawAsk !== undefined && rawAsk !== null) {
|
|
265
|
+
const n = parseFloat(String(rawAsk));
|
|
266
|
+
yesAsk = !isNaN(n) ? (n > 1 ? n / 100 : n) : 0;
|
|
267
|
+
}
|
|
268
|
+
topOutcome = truncate(top.yes_sub_title || top.subtitle || top.ticker?.split('-').pop() || '', 25);
|
|
269
|
+
if (yesAsk > 0) topPct = `${Math.round(yesAsk * 100)}%`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return [
|
|
273
|
+
e.event_ticker,
|
|
274
|
+
truncate(e.title ?? '', 35),
|
|
275
|
+
marketCount,
|
|
276
|
+
topOutcome,
|
|
277
|
+
topPct,
|
|
278
|
+
];
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
return formatTable(
|
|
282
|
+
['Ticker', 'Title', 'Mkts', 'Top Outcome', 'YES'],
|
|
283
|
+
rows
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function formatEventDetail(event: any): string {
|
|
288
|
+
const lines: string[] = [];
|
|
289
|
+
lines.push(`**${event.event_ticker}**`);
|
|
290
|
+
if (event.title) lines.push(event.title);
|
|
291
|
+
if (event.sub_title) lines.push(event.sub_title);
|
|
292
|
+
lines.push('');
|
|
293
|
+
lines.push(`Series: ${event.series_ticker ?? '-'}`);
|
|
294
|
+
lines.push(`Category: ${event.category ?? '-'}`);
|
|
295
|
+
lines.push(`Strike: ${fmtDate(event.strike_date)}`);
|
|
296
|
+
if (event.mutually_exclusive !== undefined) {
|
|
297
|
+
lines.push(`Mutually Exclusive: ${event.mutually_exclusive ? 'Yes' : 'No'}`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const markets = event.markets ?? [];
|
|
301
|
+
if (markets.length > 0) {
|
|
302
|
+
lines.push('');
|
|
303
|
+
lines.push(`**Markets (${markets.length})**`);
|
|
304
|
+
const rows = markets.map((m: any) => [
|
|
305
|
+
m.ticker,
|
|
306
|
+
truncate(m.title ?? m.subtitle ?? '', 35),
|
|
307
|
+
fmtPrice(mktYesAsk(m)),
|
|
308
|
+
fmtPrice(mktNoAsk(m)),
|
|
309
|
+
fmtNum(mktVolume(m)),
|
|
310
|
+
]);
|
|
311
|
+
lines.push(formatTable(['Ticker', 'Title', 'YES Ask', 'NO Ask', 'Volume'], rows));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return lines.join('\n');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function truncate(s: string, max: number): string {
|
|
318
|
+
return s.length > max ? s.slice(0, max - 1) + '…' : s;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function formatTable(headers: string[], rows: string[][]): string {
|
|
322
|
+
const colWidths = headers.map((h, i) =>
|
|
323
|
+
Math.max(h.length, ...rows.map((r) => (r[i] ?? '').length))
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
const pad = (s: string, w: number) => s.padEnd(w);
|
|
327
|
+
const sep = '─';
|
|
328
|
+
|
|
329
|
+
const topBorder = '┌' + colWidths.map((w) => sep.repeat(w + 2)).join('┬') + '┐';
|
|
330
|
+
const headerRow = '│' + headers.map((h, i) => ` ${pad(h, colWidths[i])} `).join('│') + '│';
|
|
331
|
+
const midBorder = '├' + colWidths.map((w) => sep.repeat(w + 2)).join('┼') + '┤';
|
|
332
|
+
const bottomBorder = '└' + colWidths.map((w) => sep.repeat(w + 2)).join('┴') + '┘';
|
|
333
|
+
|
|
334
|
+
const dataRows = rows.map(
|
|
335
|
+
(row) => '│' + colWidths.map((w, i) => ` ${pad(row[i] ?? '', w)} `).join('│') + '│'
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
return [topBorder, headerRow, midBorder, ...dataRows, bottomBorder].join('\n');
|
|
339
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
// ─── Shared help content for both TUI slash commands and CLI batch mode ─────
|
|
2
|
+
|
|
3
|
+
/** Context determines prefix style: slash commands use "/", CLI uses "kalshi" */
|
|
4
|
+
type HelpContext = 'slash' | 'cli';
|
|
5
|
+
|
|
6
|
+
function prefix(ctx: HelpContext): string {
|
|
7
|
+
return ctx === 'slash' ? '/' : 'kalshi ';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function buildTopics(ctx: HelpContext): Record<string, string> {
|
|
11
|
+
const p = prefix(ctx);
|
|
12
|
+
return {
|
|
13
|
+
search: `**${p}search** — Discovery
|
|
14
|
+
|
|
15
|
+
${p}search [theme|ticker|query] Search events by theme, ticker, or free-text
|
|
16
|
+
${p}search themes List all available themes and subcategories
|
|
17
|
+
${p}search edge Scan all markets by Octagon model edge
|
|
18
|
+
${p}search edge --min-edge 30 Markets with ≥30pp edge
|
|
19
|
+
${p}search edge --limit 50 Top 50 results
|
|
20
|
+
${p}search edge --category crypto Filter by category
|
|
21
|
+
|
|
22
|
+
Examples:
|
|
23
|
+
${p}search crypto
|
|
24
|
+
${p}search crypto:btc
|
|
25
|
+
${p}search "bitcoin price"
|
|
26
|
+
${p}search edge --min-edge 30 --category crypto`,
|
|
27
|
+
|
|
28
|
+
portfolio: `**${p}portfolio** — Account state
|
|
29
|
+
|
|
30
|
+
${p}portfolio Full overview: positions, P&L, risk snapshot
|
|
31
|
+
${p}portfolio positions Open positions with P&L
|
|
32
|
+
${p}portfolio orders Resting orders
|
|
33
|
+
${p}portfolio balance Account balance
|
|
34
|
+
${p}portfolio status Exchange status${ctx === 'cli' ? ' and setup verification' : ''}
|
|
35
|
+
${ctx === 'cli' ? `
|
|
36
|
+
Flags:
|
|
37
|
+
--performance Include win rate, Sharpe, Brier scores
|
|
38
|
+
--json JSON output` : ''}`,
|
|
39
|
+
|
|
40
|
+
analyze: `**${p}analyze** — Deep market analysis
|
|
41
|
+
|
|
42
|
+
${p}analyze <ticker> Full analysis: edge, drivers, catalysts, Kelly sizing
|
|
43
|
+
${p}analyze <ticker> ${ctx === 'cli' ? '--' : ''}refresh Force fresh Octagon report
|
|
44
|
+
${ctx === 'cli' ? `
|
|
45
|
+
Legacy aliases (still work):
|
|
46
|
+
${p}edge [--ticker X] Edge history / snapshots (default: last 24h)
|
|
47
|
+
${p}edge --since <date> Edges since date (e.g. 2026-03-01)` : ''}`,
|
|
48
|
+
|
|
49
|
+
watch: `**${p}watch** — Live monitoring
|
|
50
|
+
|
|
51
|
+
Modes:
|
|
52
|
+
${p}watch <ticker> Per-ticker price/orderbook feed (5s default)
|
|
53
|
+
${p}watch --theme <theme> Continuous theme scan${ctx === 'cli' ? ' (default: every 60m)' : ' (press Esc to stop)'}
|
|
54
|
+
${ctx === 'cli' ? `
|
|
55
|
+
Flags:
|
|
56
|
+
--interval <minutes> Scan interval for theme mode (min 15)
|
|
57
|
+
--live Force 15m interval
|
|
58
|
+
--json NDJSON output (one line per tick/cycle)
|
|
59
|
+
--dry-run Scan without persisting edges
|
|
60
|
+
|
|
61
|
+
Press Ctrl+C to stop.` : `
|
|
62
|
+
Per-ticker mode shows live price, bid/ask, spread, volume, and top-5 orderbook.
|
|
63
|
+
Theme mode runs recurring Octagon scans and displays an edge table.`}`,
|
|
64
|
+
|
|
65
|
+
buy: `**${p}buy** — Buy contracts
|
|
66
|
+
|
|
67
|
+
${p}buy <ticker> <count> [price${ctx === 'cli' ? '_in_cents' : ''}] [yes|no]${ctx === 'slash' ? ' Buy contracts (price in cents)' : ''}
|
|
68
|
+
|
|
69
|
+
Example${ctx === 'cli' ? 's' : ''}:
|
|
70
|
+
${p}buy KXBTC-26MAR14-T50049 10 ${ctx === 'cli' ? ' Buy at best ask (10 YES contracts)' : '56'}
|
|
71
|
+
${p}buy KXBTC-26MAR14-T50049 10 ${ctx === 'cli' ? '56 Limit order at $0.56' : '56 no Buy NO contracts'}
|
|
72
|
+
${ctx === 'cli' ? ` ${p}buy KXBTC-26MAR14-T50049 10 56 no Limit order for NO contracts at $0.56` : ''}
|
|
73
|
+
Side defaults to YES if omitted.`,
|
|
74
|
+
|
|
75
|
+
sell: `**${p}sell** — Sell contracts
|
|
76
|
+
|
|
77
|
+
${p}sell <ticker> <count> [price${ctx === 'cli' ? '_in_cents' : ''}] [yes|no]${ctx === 'slash' ? ' Sell contracts (price in cents)' : ''}
|
|
78
|
+
|
|
79
|
+
Example${ctx === 'cli' ? 's' : ''}:
|
|
80
|
+
${p}sell KXBTC-26MAR14-T50049 10 ${ctx === 'cli' ? ' Sell at best ask (10 YES contracts)' : '72'}
|
|
81
|
+
${p}sell KXBTC-26MAR14-T50049 10 ${ctx === 'cli' ? '72 Limit order at $0.72' : '72 no Sell NO contracts'}
|
|
82
|
+
${ctx === 'cli' ? ` ${p}sell KXBTC-26MAR14-T50049 10 72 no Limit order for NO contracts at $0.72` : ''}
|
|
83
|
+
Side defaults to YES if omitted.`,
|
|
84
|
+
|
|
85
|
+
cancel: `**${p}cancel** — Cancel a resting order
|
|
86
|
+
|
|
87
|
+
${p}cancel <order_id>`,
|
|
88
|
+
|
|
89
|
+
backtest: `**${p}backtest** — Model accuracy scorecard & edge scanner
|
|
90
|
+
|
|
91
|
+
${p}backtest 15-day lookback, both sections (default)
|
|
92
|
+
${p}backtest --days 30 30-day lookback
|
|
93
|
+
${p}backtest --max-age 14 Reject predictions older than 14 days (default = --days)
|
|
94
|
+
${p}backtest --resolved Resolved markets only
|
|
95
|
+
${p}backtest --unresolved Unresolved markets only
|
|
96
|
+
${p}backtest --category crypto Filter by category
|
|
97
|
+
${p}backtest --min-edge 10 Stricter edge threshold in pp (default 0.5pp)
|
|
98
|
+
${p}backtest --min-volume 10 Per-contract volume gate (default 1)
|
|
99
|
+
${p}backtest --min-price 5 --max-price 95 Tradeable price band 0-100 (defaults: 5 / 95)
|
|
100
|
+
${p}backtest --export results.csv Per-market detail CSV
|
|
101
|
+
${p}backtest --json Machine-readable output
|
|
102
|
+
|
|
103
|
+
Looks back N days, compares what the model said then to where the market is now.
|
|
104
|
+
Resolved markets: scored against Kalshi settlement (0 or 100).
|
|
105
|
+
Unresolved markets: mark-to-market vs current Kalshi trading price.
|
|
106
|
+
Per-contract entry: mp/kp come from the per-contract outcome_probabilities on the
|
|
107
|
+
Octagon snapshot (no event-level fallback). Volume gate uses per-contract volume
|
|
108
|
+
from the snapshot when available, else current Kalshi lifetime volume.
|
|
109
|
+
ROI is capital-weighted: sum(pnl) / sum(capital) across edge signals, where capital
|
|
110
|
+
is kp/100 for YES edges and (100-kp)/100 for NO edges (matches Supabase methodology).`,
|
|
111
|
+
|
|
112
|
+
'clear-cache': `**${ctx === 'cli' ? '' : 'bun start '}clear-cache** — Delete local cache
|
|
113
|
+
|
|
114
|
+
${ctx === 'cli' ? `${p}` : 'bun start '}clear-cache Delete the local SQLite database (~/.kalshi-bot/kalshi-bot.db)
|
|
115
|
+
A fresh database will be created on next command.
|
|
116
|
+
|
|
117
|
+
Use this when the local cache is corrupted or you want to start fresh.${ctx !== 'cli' ? '\nRun from terminal: bun start clear-cache' : ''}`,
|
|
118
|
+
|
|
119
|
+
init: `**${p}init** — Re-run setup wizard
|
|
120
|
+
|
|
121
|
+
${p}init Launch the TUI with the setup wizard open
|
|
122
|
+
Use this to configure or reconfigure API keys and preferences.`,
|
|
123
|
+
|
|
124
|
+
help: `**${p}help** — Show help
|
|
125
|
+
|
|
126
|
+
${p}help Show all commands
|
|
127
|
+
${p}help <command> Show detailed help for a command`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function buildOverview(ctx: HelpContext): string {
|
|
132
|
+
const p = prefix(ctx);
|
|
133
|
+
if (ctx === 'cli') {
|
|
134
|
+
return `**Kalshi Trading Bot CLI — CLI Commands**
|
|
135
|
+
|
|
136
|
+
Quick start:
|
|
137
|
+
kalshi search crypto Find markets by keyword or theme
|
|
138
|
+
kalshi analyze <ticker> Deep analysis + trade recommendation
|
|
139
|
+
kalshi watch --theme crypto Continuous scan across a theme
|
|
140
|
+
|
|
141
|
+
Discovery:
|
|
142
|
+
search [theme|ticker|query] Find markets by keyword or theme
|
|
143
|
+
search --refresh <query> Force index rebuild then search
|
|
144
|
+
search themes List all themes and subcategories
|
|
145
|
+
search edge [--min-edge N] Scan all markets by Octagon model edge
|
|
146
|
+
watch <ticker> Live price/orderbook feed
|
|
147
|
+
watch --theme <theme> Continuous theme scan (Ctrl+C to stop)
|
|
148
|
+
watch --refresh Force index rebuild before watching
|
|
149
|
+
|
|
150
|
+
Analysis & Trading:
|
|
151
|
+
analyze <ticker> Full report: edge, drivers, Kelly sizing
|
|
152
|
+
analyze <ticker> --refresh Force fresh Octagon report
|
|
153
|
+
buy <ticker> <n> [price] [yes|no] Buy contracts (price in cents)
|
|
154
|
+
sell <ticker> <n> [price] [yes|no] Sell contracts
|
|
155
|
+
cancel <order_id> Cancel a resting order
|
|
156
|
+
|
|
157
|
+
Analysis:
|
|
158
|
+
backtest Model accuracy scorecard + live edge scanner
|
|
159
|
+
backtest --resolved Resolved markets scorecard only
|
|
160
|
+
backtest --unresolved Live edge scanner only
|
|
161
|
+
|
|
162
|
+
Account:
|
|
163
|
+
portfolio Overview: positions, P&L, risk snapshot
|
|
164
|
+
portfolio positions Open positions
|
|
165
|
+
portfolio orders Resting orders
|
|
166
|
+
portfolio balance Account balance
|
|
167
|
+
|
|
168
|
+
System:
|
|
169
|
+
init Launch with setup wizard (configure API keys)
|
|
170
|
+
clear-cache Delete local SQLite cache and start fresh
|
|
171
|
+
setup Re-run setup wizard
|
|
172
|
+
help [command] Show help for a command
|
|
173
|
+
|
|
174
|
+
Flags: --json, --refresh, --performance, --dry-run, --verbose
|
|
175
|
+
Backtest flags: --days, --max-age, --resolved, --unresolved, --category, --min-edge,
|
|
176
|
+
--min-volume, --min-price, --max-price, --export
|
|
177
|
+
Run "kalshi help <command>" for detailed usage.`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return `**Kalshi Trading Bot CLI — Commands**
|
|
181
|
+
|
|
182
|
+
Quick start:
|
|
183
|
+
/search crypto Find markets by keyword or theme
|
|
184
|
+
/analyze <ticker> Deep analysis + trade recommendation
|
|
185
|
+
/watch --theme crypto Continuous scan across a theme
|
|
186
|
+
|
|
187
|
+
Discovery:
|
|
188
|
+
/search [theme|ticker|query] Find markets by keyword or theme
|
|
189
|
+
/search --refresh <query> Force index rebuild then search
|
|
190
|
+
/search themes List all themes and subcategories
|
|
191
|
+
/search edge [--min-edge N] Scan all markets by Octagon model edge
|
|
192
|
+
/watch <ticker> Live price/orderbook feed
|
|
193
|
+
/watch --theme <theme> Continuous theme scan (Esc to stop)
|
|
194
|
+
/watch --refresh Force index rebuild before watching
|
|
195
|
+
|
|
196
|
+
Analysis:
|
|
197
|
+
/backtest Model accuracy scorecard + live edge scanner
|
|
198
|
+
/analyze <ticker> Full report: edge, drivers, Kelly sizing
|
|
199
|
+
/analyze <ticker> refresh Force fresh Octagon report
|
|
200
|
+
/buy <ticker> <n> [price] [yes|no] Buy contracts (price in cents)
|
|
201
|
+
/sell <ticker> <n> [price] [yes|no] Sell contracts
|
|
202
|
+
/review Review positions for close signals
|
|
203
|
+
/cancel <order_id> Cancel a resting order
|
|
204
|
+
|
|
205
|
+
Account:
|
|
206
|
+
/portfolio Overview: positions, P&L, risk snapshot
|
|
207
|
+
/portfolio positions Open positions
|
|
208
|
+
/portfolio orders Resting orders
|
|
209
|
+
/portfolio balance Account balance
|
|
210
|
+
|
|
211
|
+
System:
|
|
212
|
+
/model Change LLM model/provider
|
|
213
|
+
/setup Re-run setup wizard
|
|
214
|
+
init Launch with setup wizard (run: bun start init)
|
|
215
|
+
clear-cache Delete local cache (run: bun start clear-cache)
|
|
216
|
+
/help [command] Show help for a command
|
|
217
|
+
/quit Quit
|
|
218
|
+
|
|
219
|
+
Tips:
|
|
220
|
+
Type natural language — e.g. "analyze KXBTC", "show my portfolio"
|
|
221
|
+
Press Esc to cancel a running query`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function buildHelp(ctx: HelpContext, topic?: string): { text: string } | { error: string } {
|
|
225
|
+
const topics = buildTopics(ctx);
|
|
226
|
+
|
|
227
|
+
if (topic && topics[topic]) {
|
|
228
|
+
return { text: topics[topic] };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (topic) {
|
|
232
|
+
return { error: `Unknown help topic: "${topic}". Available: ${Object.keys(topics).join(', ')}` };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return { text: buildOverview(ctx) };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Shared trade argument validation for both dispatch and slash handlers. */
|
|
239
|
+
export function validateTradeArgs(
|
|
240
|
+
countStr: string,
|
|
241
|
+
priceStr?: string,
|
|
242
|
+
): { count: number; price: number | undefined } | { error: string } {
|
|
243
|
+
if (!/^\d+$/.test(countStr)) {
|
|
244
|
+
return { error: `Invalid count: ${countStr}` };
|
|
245
|
+
}
|
|
246
|
+
const count = Number(countStr);
|
|
247
|
+
if (count <= 0) {
|
|
248
|
+
return { error: `Invalid count: ${countStr}` };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let price: number | undefined;
|
|
252
|
+
if (priceStr !== undefined) {
|
|
253
|
+
if (!/^\d+$/.test(priceStr)) {
|
|
254
|
+
return { error: `Invalid price: ${priceStr}. Price must be 1-99 (cents).` };
|
|
255
|
+
}
|
|
256
|
+
price = Number(priceStr);
|
|
257
|
+
if (price < 1 || price > 99) {
|
|
258
|
+
return { error: `Invalid price: ${priceStr}. Price must be 1-99 (cents).` };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return { count, price };
|
|
263
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { callKalshiApi } from '../tools/kalshi/api.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse a market quote from API response, handling both cent and dollar-string fields.
|
|
5
|
+
* Returns cents (1-99) or NaN if no valid quote found.
|
|
6
|
+
*/
|
|
7
|
+
function parseQuoteCents(market: Record<string, unknown>, field: 'yes_ask' | 'yes_bid' | 'no_ask' | 'no_bid'): number {
|
|
8
|
+
// Try dollar-string fields first (new API: yes_ask_dollars, legacy: dollar_yes_ask)
|
|
9
|
+
const dollarKey = `${field}_dollars`; // e.g. yes_ask_dollars
|
|
10
|
+
const legacyDollarKey = `dollar_${field}`; // e.g. dollar_yes_ask
|
|
11
|
+
|
|
12
|
+
const dollarStr = market[dollarKey] as string | undefined;
|
|
13
|
+
const legacyStr = market[legacyDollarKey] as string | undefined;
|
|
14
|
+
|
|
15
|
+
const d = dollarStr != null ? parseFloat(dollarStr) : legacyStr != null ? parseFloat(legacyStr) : NaN;
|
|
16
|
+
if (Number.isFinite(d) && d > 0) return Math.round(d * 100);
|
|
17
|
+
|
|
18
|
+
// Fall back to cent field
|
|
19
|
+
const cents = Number(market[field] ?? 0);
|
|
20
|
+
if (Number.isFinite(cents) && cents > 0) return cents;
|
|
21
|
+
|
|
22
|
+
return NaN;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Fetch the best available quote for a market order.
|
|
27
|
+
* Returns cents for the appropriate side/action, or an error message.
|
|
28
|
+
*/
|
|
29
|
+
export async function fetchMarketQuote(
|
|
30
|
+
ticker: string,
|
|
31
|
+
action: 'buy' | 'sell',
|
|
32
|
+
side: 'yes' | 'no' = 'yes',
|
|
33
|
+
): Promise<{ cents: number } | { error: string }> {
|
|
34
|
+
const marketData = await callKalshiApi('GET', `/markets/${ticker}`) as Record<string, unknown>;
|
|
35
|
+
const market = (marketData.market ?? marketData) as Record<string, unknown>;
|
|
36
|
+
|
|
37
|
+
const field = side === 'no'
|
|
38
|
+
? (action === 'sell' ? 'no_bid' : 'no_ask')
|
|
39
|
+
: (action === 'sell' ? 'yes_bid' : 'yes_ask');
|
|
40
|
+
const cents = parseQuoteCents(market, field);
|
|
41
|
+
|
|
42
|
+
if (!Number.isFinite(cents) || cents <= 0) {
|
|
43
|
+
const label = action === 'sell' ? 'bid' : 'ask';
|
|
44
|
+
return { error: `No ${label} available for ${ticker} — cannot place market order. Specify a price.` };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { cents };
|
|
48
|
+
}
|