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,70 @@
|
|
|
1
|
+
import { callKalshiApi } from '../tools/kalshi/api.js';
|
|
2
|
+
import { PROVIDERS } from '@/providers';
|
|
3
|
+
import { getDefaultModelForProvider } from '@/utils/model';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Verify setup: check API keys, exchange connectivity, and optional services.
|
|
7
|
+
* Designed to be the first command a new user runs after `cp env.example .env`.
|
|
8
|
+
*/
|
|
9
|
+
export async function handleStatus(): Promise<string> {
|
|
10
|
+
const lines: string[] = [];
|
|
11
|
+
let allGood = true;
|
|
12
|
+
|
|
13
|
+
lines.push('Checking setup...');
|
|
14
|
+
lines.push('');
|
|
15
|
+
|
|
16
|
+
// 1. Kalshi API key
|
|
17
|
+
const hasKalshiKey = !!process.env.KALSHI_API_KEY;
|
|
18
|
+
const hasKalshiPem = !!(process.env.KALSHI_PRIVATE_KEY_FILE || process.env.KALSHI_PRIVATE_KEY);
|
|
19
|
+
lines.push(hasKalshiKey ? '✓ KALSHI_API_KEY set' : '✗ KALSHI_API_KEY missing');
|
|
20
|
+
lines.push(hasKalshiPem ? '✓ Kalshi private key configured' : '✗ Kalshi private key missing (set KALSHI_PRIVATE_KEY_FILE or KALSHI_PRIVATE_KEY)');
|
|
21
|
+
if (!hasKalshiKey || !hasKalshiPem) allGood = false;
|
|
22
|
+
|
|
23
|
+
// 2. Exchange connectivity
|
|
24
|
+
if (hasKalshiKey && hasKalshiPem) {
|
|
25
|
+
try {
|
|
26
|
+
const data = await callKalshiApi('GET', '/exchange/status');
|
|
27
|
+
const active = (data as any).exchange_active;
|
|
28
|
+
const trading = (data as any).trading_active;
|
|
29
|
+
lines.push(active ? '✓ Exchange reachable' : '✗ Exchange not active');
|
|
30
|
+
lines.push(trading ? '✓ Trading enabled' : '⚠ Trading paused');
|
|
31
|
+
if (!active) allGood = false;
|
|
32
|
+
} catch (e: any) {
|
|
33
|
+
lines.push(`✗ Cannot reach Kalshi API: ${e.message}`);
|
|
34
|
+
allGood = false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 3. LLM provider — detect which provider is configured and show its default model
|
|
39
|
+
const configuredProvider = PROVIDERS.find(
|
|
40
|
+
(p) => p.apiKeyEnvVar && process.env[p.apiKeyEnvVar],
|
|
41
|
+
);
|
|
42
|
+
const defaultModel =
|
|
43
|
+
process.env.DEFAULT_MODEL ??
|
|
44
|
+
(configuredProvider ? getDefaultModelForProvider(configuredProvider.id) : undefined);
|
|
45
|
+
const llmKey = !!configuredProvider;
|
|
46
|
+
lines.push(
|
|
47
|
+
llmKey
|
|
48
|
+
? `✓ LLM provider configured (${configuredProvider!.displayName}${defaultModel ? `, default model: ${defaultModel}` : ''})`
|
|
49
|
+
: '✗ No LLM API key set (need at least one: OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.)',
|
|
50
|
+
);
|
|
51
|
+
if (!llmKey) allGood = false;
|
|
52
|
+
|
|
53
|
+
// 4. Octagon
|
|
54
|
+
const hasOctagon = !!process.env.OCTAGON_API_KEY;
|
|
55
|
+
lines.push(hasOctagon ? '✓ OCTAGON_API_KEY set' : '⚠ OCTAGON_API_KEY missing — /scan and deep research will not work');
|
|
56
|
+
|
|
57
|
+
// 5. Optional: Tavily
|
|
58
|
+
const hasTavily = !!process.env.TAVILY_API_KEY;
|
|
59
|
+
lines.push(hasTavily ? '✓ TAVILY_API_KEY set (web search enabled)' : ' TAVILY_API_KEY not set (web search disabled — optional)');
|
|
60
|
+
|
|
61
|
+
// 6. Demo mode
|
|
62
|
+
if (process.env.KALSHI_USE_DEMO === 'true') {
|
|
63
|
+
lines.push('⚠ KALSHI_USE_DEMO=true — using demo environment (no real money)');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
lines.push('');
|
|
67
|
+
lines.push(allGood ? '✓ All good — ready to trade.' : '✗ Fix the issues above before continuing.');
|
|
68
|
+
|
|
69
|
+
return lines.join('\n');
|
|
70
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { getDb } from '../db/index.js';
|
|
2
|
+
import { getActiveThemes } from '../db/themes.js';
|
|
3
|
+
import { CATEGORY_MAP, fetchSubcategories } from '../scan/theme-resolver.js';
|
|
4
|
+
import { formatTable } from './scan-formatters.js';
|
|
5
|
+
import { wrapSuccess } from './json.js';
|
|
6
|
+
import type { CLIResponse } from './json.js';
|
|
7
|
+
import type { ParsedArgs } from './parse-args.js';
|
|
8
|
+
|
|
9
|
+
export interface ThemeInfo {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
type: 'built-in' | 'category' | 'custom';
|
|
13
|
+
subcategories?: string[];
|
|
14
|
+
tickerCount?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ThemesResult {
|
|
18
|
+
themes: ThemeInfo[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function handleThemes(args: ParsedArgs): Promise<CLIResponse<ThemesResult>> {
|
|
22
|
+
const themes: ThemeInfo[] = [];
|
|
23
|
+
|
|
24
|
+
// Special built-in theme
|
|
25
|
+
themes.push({ id: 'top50', name: 'Top 50 markets by 24h volume', type: 'built-in' });
|
|
26
|
+
|
|
27
|
+
// Fetch subcategories from Kalshi API
|
|
28
|
+
let subcatMap: Record<string, string[]> = {};
|
|
29
|
+
try {
|
|
30
|
+
subcatMap = await fetchSubcategories();
|
|
31
|
+
} catch {
|
|
32
|
+
// API unavailable — show categories without subcategories
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// All Kalshi categories with their subcategories
|
|
36
|
+
for (const [id, label] of Object.entries(CATEGORY_MAP)) {
|
|
37
|
+
const subs = subcatMap[label] ?? [];
|
|
38
|
+
themes.push({
|
|
39
|
+
id,
|
|
40
|
+
name: label,
|
|
41
|
+
type: 'category',
|
|
42
|
+
...(subs.length > 0 ? { subcategories: subs } : {}),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Custom themes from DB
|
|
47
|
+
try {
|
|
48
|
+
const db = getDb();
|
|
49
|
+
const custom = getActiveThemes(db);
|
|
50
|
+
for (const t of custom) {
|
|
51
|
+
const tickers = t.tickers ? JSON.parse(t.tickers) as string[] : [];
|
|
52
|
+
themes.push({
|
|
53
|
+
id: t.theme_id,
|
|
54
|
+
name: t.name,
|
|
55
|
+
type: 'custom',
|
|
56
|
+
tickerCount: tickers.length,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// DB not initialized yet — just show built-in themes
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return wrapSuccess('themes', { themes });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Wrap a comma-separated list into lines of at most `maxWidth` characters */
|
|
67
|
+
function wrapSubs(subs: string[], maxWidth: number): string[] {
|
|
68
|
+
if (subs.length === 0) return [''];
|
|
69
|
+
const wrapped: string[] = [];
|
|
70
|
+
let line = '';
|
|
71
|
+
for (const s of subs) {
|
|
72
|
+
const addition = line ? `, ${s}` : s;
|
|
73
|
+
if (line && (line + addition).length > maxWidth) {
|
|
74
|
+
wrapped.push(line);
|
|
75
|
+
line = s;
|
|
76
|
+
} else {
|
|
77
|
+
line += addition;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (line) wrapped.push(line);
|
|
81
|
+
return wrapped;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function formatThemesHuman(data: ThemesResult): string {
|
|
85
|
+
const lines: string[] = [];
|
|
86
|
+
const SUB_WIDTH = 55;
|
|
87
|
+
|
|
88
|
+
const rows: string[][] = [];
|
|
89
|
+
|
|
90
|
+
// Built-in special themes
|
|
91
|
+
for (const t of data.themes.filter((t) => t.type === 'built-in')) {
|
|
92
|
+
rows.push([t.id, t.name]);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Categories — main row, then one subcategory per line
|
|
96
|
+
for (const t of data.themes.filter((t) => t.type === 'category')) {
|
|
97
|
+
rows.push([t.id, t.name]);
|
|
98
|
+
for (const s of t.subcategories ?? []) {
|
|
99
|
+
rows.push(['', ` ${t.id}:${s.toLowerCase()}`]);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Custom themes
|
|
104
|
+
for (const t of data.themes.filter((t) => t.type === 'custom')) {
|
|
105
|
+
const extra = t.tickerCount !== undefined ? `${t.name} (${t.tickerCount} tickers)` : t.name;
|
|
106
|
+
rows.push([t.id, extra]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
lines.push(formatTable(['Theme', 'Description / Subcategories'], rows));
|
|
110
|
+
|
|
111
|
+
lines.push('');
|
|
112
|
+
lines.push('Usage: search crypto');
|
|
113
|
+
lines.push(' search crypto:btc');
|
|
114
|
+
lines.push(' analyze <TICKER>');
|
|
115
|
+
|
|
116
|
+
return lines.join('\n');
|
|
117
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import type { ParsedArgs } from './parse-args.js';
|
|
2
|
+
import { wrapSuccess, wrapError } from './json.js';
|
|
3
|
+
import { getDb } from '../db/index.js';
|
|
4
|
+
import { auditTrail } from '../audit/index.js';
|
|
5
|
+
import { ScanLoop } from '../scan/loop.js';
|
|
6
|
+
import { createOctagonInvoker } from '../scan/invoker.js';
|
|
7
|
+
import { formatScanTable } from './scan-formatters.js';
|
|
8
|
+
import { callKalshiApi } from '../tools/kalshi/api.js';
|
|
9
|
+
import { getBotSetting } from '../utils/bot-config.js';
|
|
10
|
+
import type { ScanResult } from '../scan/loop.js';
|
|
11
|
+
|
|
12
|
+
export async function handleWatch(args: ParsedArgs): Promise<void> {
|
|
13
|
+
const db = getDb();
|
|
14
|
+
const invoker = createOctagonInvoker();
|
|
15
|
+
const loop = new ScanLoop(db, auditTrail, invoker);
|
|
16
|
+
|
|
17
|
+
const rawMinInterval = Number(getBotSetting('watch.min_interval_minutes'));
|
|
18
|
+
const minIntervalMinutes = Number.isFinite(rawMinInterval) && rawMinInterval > 0 ? rawMinInterval : 15;
|
|
19
|
+
const intervalMinutes = args.live
|
|
20
|
+
? minIntervalMinutes
|
|
21
|
+
: Math.max(minIntervalMinutes, args.interval ?? 60);
|
|
22
|
+
const intervalMs = intervalMinutes * 60_000;
|
|
23
|
+
const theme = args.theme ?? 'top50';
|
|
24
|
+
|
|
25
|
+
let totalCycles = 0;
|
|
26
|
+
let totalEdges = 0;
|
|
27
|
+
const startTime = Date.now();
|
|
28
|
+
let stopped = false;
|
|
29
|
+
let timer: ReturnType<typeof setInterval>;
|
|
30
|
+
|
|
31
|
+
const shutdown = () => {
|
|
32
|
+
if (stopped) return;
|
|
33
|
+
stopped = true;
|
|
34
|
+
clearInterval(timer);
|
|
35
|
+
|
|
36
|
+
const durationSec = ((Date.now() - startTime) / 1000).toFixed(0);
|
|
37
|
+
if (args.json) {
|
|
38
|
+
console.log(JSON.stringify({
|
|
39
|
+
event: 'watch_stopped',
|
|
40
|
+
totalCycles,
|
|
41
|
+
totalEdges,
|
|
42
|
+
durationSeconds: Number(durationSec),
|
|
43
|
+
}));
|
|
44
|
+
} else {
|
|
45
|
+
console.log('');
|
|
46
|
+
console.log(`Watch stopped. ${totalCycles} cycles, ${totalEdges} edges found in ${durationSec}s`);
|
|
47
|
+
}
|
|
48
|
+
process.exit(0);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
process.once('SIGINT', shutdown);
|
|
52
|
+
process.once('SIGTERM', shutdown);
|
|
53
|
+
|
|
54
|
+
if (!args.json) {
|
|
55
|
+
console.log(`Watching theme "${theme}" every ${intervalMinutes}m (Ctrl+C to stop)\n`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const runCycle = async (): Promise<void> => {
|
|
59
|
+
try {
|
|
60
|
+
const result = await loop.runOnce({ theme, dryRun: args.dryRun });
|
|
61
|
+
totalCycles++;
|
|
62
|
+
totalEdges += result.edgeSnapshots.length;
|
|
63
|
+
|
|
64
|
+
if (args.json) {
|
|
65
|
+
const actionable = result.edgeSnapshots.filter(
|
|
66
|
+
(s) => s.confidence === 'high' || s.confidence === 'very_high'
|
|
67
|
+
).length;
|
|
68
|
+
console.log(JSON.stringify(wrapSuccess('watch', result, {
|
|
69
|
+
scan_id: result.scanId,
|
|
70
|
+
theme,
|
|
71
|
+
events_scanned: result.eventsScanned,
|
|
72
|
+
actionable,
|
|
73
|
+
octagon_credits_used: result.octagonCreditsUsed,
|
|
74
|
+
})));
|
|
75
|
+
} else {
|
|
76
|
+
console.clear();
|
|
77
|
+
console.log(`Watch cycle #${totalCycles} — theme "${theme}" — every ${intervalMinutes}m\n`);
|
|
78
|
+
console.log(formatScanTable(result));
|
|
79
|
+
}
|
|
80
|
+
} catch (err) {
|
|
81
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
82
|
+
if (args.json) {
|
|
83
|
+
console.log(JSON.stringify(wrapError('watch', 'SCAN_ERROR', message)));
|
|
84
|
+
} else {
|
|
85
|
+
console.error(`[watch] Scan error: ${message}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Run first cycle immediately
|
|
91
|
+
await runCycle();
|
|
92
|
+
|
|
93
|
+
// Continue running on interval until stopped
|
|
94
|
+
timer = setInterval(() => {
|
|
95
|
+
if (stopped) return;
|
|
96
|
+
runCycle().catch((err) => {
|
|
97
|
+
console.error(`[watch] Scan cycle failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
98
|
+
});
|
|
99
|
+
}, intervalMs);
|
|
100
|
+
|
|
101
|
+
// Keep process alive — the SIGINT handler will exit
|
|
102
|
+
await new Promise<void>(() => {});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ─── Per-ticker watch mode ──────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
interface TickerSnapshot {
|
|
108
|
+
ticker: string;
|
|
109
|
+
lastPrice: string;
|
|
110
|
+
yesAsk: string;
|
|
111
|
+
yesBid: string;
|
|
112
|
+
noAsk: string;
|
|
113
|
+
noBid: string;
|
|
114
|
+
spread: string;
|
|
115
|
+
volume: string;
|
|
116
|
+
openInterest: string;
|
|
117
|
+
orderbook: { price: string; quantity: number }[];
|
|
118
|
+
timestamp: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function parseDollarField(val: string | number | undefined | null, isCentField = false): number {
|
|
122
|
+
if (val === undefined || val === null) return 0;
|
|
123
|
+
const n = typeof val === 'number' ? val : parseFloat(val as string);
|
|
124
|
+
if (isNaN(n)) return 0;
|
|
125
|
+
return isCentField ? n / 100 : n;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Format a value already in dollars (0.00–1.00) */
|
|
129
|
+
function fmtDollars(val: number): string {
|
|
130
|
+
return `$${val.toFixed(2)}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Format a cent integer (1–99) as dollars */
|
|
134
|
+
function fmtCents(val: number): string {
|
|
135
|
+
return `$${(val / 100).toFixed(2)}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function fmtNum(n: number | string | undefined | null): string {
|
|
139
|
+
if (n === undefined || n === null) return '-';
|
|
140
|
+
const val = typeof n === 'number' ? n : parseFloat(n as string);
|
|
141
|
+
if (isNaN(val)) return '-';
|
|
142
|
+
return val.toLocaleString();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function fetchTickerSnapshot(ticker: string): Promise<TickerSnapshot> {
|
|
146
|
+
// Fetch market data
|
|
147
|
+
const market = await callKalshiApi('GET', `/markets/${ticker}`) as any;
|
|
148
|
+
const m = market.market ?? market;
|
|
149
|
+
|
|
150
|
+
const hasDollarYesAsk = m.yes_ask_dollars != null || m.dollar_yes_ask != null;
|
|
151
|
+
const hasDollarYesBid = m.yes_bid_dollars != null || m.dollar_yes_bid != null;
|
|
152
|
+
const hasDollarNoAsk = m.no_ask_dollars != null || m.dollar_no_ask != null;
|
|
153
|
+
const hasDollarNoBid = m.no_bid_dollars != null || m.dollar_no_bid != null;
|
|
154
|
+
const yesAsk = parseDollarField(m.yes_ask_dollars ?? m.dollar_yes_ask ?? m.yes_ask, !hasDollarYesAsk);
|
|
155
|
+
const yesBid = parseDollarField(m.yes_bid_dollars ?? m.dollar_yes_bid ?? m.yes_bid, !hasDollarYesBid);
|
|
156
|
+
const noAsk = parseDollarField(m.no_ask_dollars ?? m.dollar_no_ask ?? m.no_ask, !hasDollarNoAsk);
|
|
157
|
+
const noBid = parseDollarField(m.no_bid_dollars ?? m.dollar_no_bid ?? m.no_bid, !hasDollarNoBid);
|
|
158
|
+
const spread = yesAsk - yesBid;
|
|
159
|
+
|
|
160
|
+
// Fetch orderbook
|
|
161
|
+
let orderbook: { price: string; quantity: number }[] = [];
|
|
162
|
+
try {
|
|
163
|
+
const ob = await callKalshiApi('GET', `/markets/${ticker}/orderbook`) as any;
|
|
164
|
+
const book = ob.orderbook ?? ob;
|
|
165
|
+
const rawEntries = Array.isArray(book.yes) ? book.yes : [];
|
|
166
|
+
orderbook = rawEntries
|
|
167
|
+
.filter((entry: unknown): entry is [number, number] =>
|
|
168
|
+
Array.isArray(entry) && entry.length === 2 &&
|
|
169
|
+
typeof entry[0] === 'number' && typeof entry[1] === 'number'
|
|
170
|
+
)
|
|
171
|
+
.slice(0, 5)
|
|
172
|
+
.map(([price, qty]: [number, number]) => ({
|
|
173
|
+
price: fmtCents(price),
|
|
174
|
+
quantity: qty,
|
|
175
|
+
}));
|
|
176
|
+
} catch {
|
|
177
|
+
// Orderbook not available for all markets
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Resolve last price — dollar string fields are already in dollars; last_price is cents
|
|
181
|
+
const dollarLastStr = m.last_price_dollars ?? m.dollar_last_price;
|
|
182
|
+
const parsedDollarLast = dollarLastStr != null ? parseFloat(dollarLastStr) : NaN;
|
|
183
|
+
const lastPriceDollars = Number.isFinite(parsedDollarLast)
|
|
184
|
+
? parsedDollarLast
|
|
185
|
+
: (m.last_price != null ? m.last_price / 100 : NaN);
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
ticker,
|
|
189
|
+
lastPrice: Number.isFinite(lastPriceDollars) ? fmtDollars(lastPriceDollars) : '-',
|
|
190
|
+
yesAsk: fmtDollars(yesAsk),
|
|
191
|
+
yesBid: fmtDollars(yesBid),
|
|
192
|
+
noAsk: fmtDollars(noAsk),
|
|
193
|
+
noBid: fmtDollars(noBid),
|
|
194
|
+
spread: `$${spread.toFixed(4)}`,
|
|
195
|
+
volume: fmtNum(m.volume_fp ?? m.volume),
|
|
196
|
+
openInterest: fmtNum(m.open_interest_fp ?? m.open_interest),
|
|
197
|
+
orderbook,
|
|
198
|
+
timestamp: new Date().toISOString(),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function formatTickerDashboard(snap: TickerSnapshot, tick: number): string {
|
|
203
|
+
const lines: string[] = [];
|
|
204
|
+
lines.push(` ${snap.ticker} (tick #${tick}) ${new Date(snap.timestamp).toLocaleTimeString()}`);
|
|
205
|
+
lines.push('');
|
|
206
|
+
lines.push(` Last Price: ${snap.lastPrice}`);
|
|
207
|
+
lines.push(` YES Bid / Ask: ${snap.yesBid} / ${snap.yesAsk} Spread: ${snap.spread}`);
|
|
208
|
+
lines.push(` NO Bid / Ask: ${snap.noBid} / ${snap.noAsk}`);
|
|
209
|
+
lines.push(` Volume: ${snap.volume} Open Interest: ${snap.openInterest}`);
|
|
210
|
+
|
|
211
|
+
if (snap.orderbook.length > 0) {
|
|
212
|
+
lines.push('');
|
|
213
|
+
lines.push(' Orderbook (YES, top 5):');
|
|
214
|
+
for (const level of snap.orderbook) {
|
|
215
|
+
lines.push(` ${level.price} ×${level.quantity}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return lines.join('\n');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export async function handleWatchTicker(ticker: string, args: ParsedArgs): Promise<void> {
|
|
223
|
+
let totalTicks = 0;
|
|
224
|
+
const startTime = Date.now();
|
|
225
|
+
let stopped = false;
|
|
226
|
+
let timer: ReturnType<typeof setInterval>;
|
|
227
|
+
|
|
228
|
+
const shutdown = () => {
|
|
229
|
+
if (stopped) return;
|
|
230
|
+
stopped = true;
|
|
231
|
+
clearInterval(timer);
|
|
232
|
+
|
|
233
|
+
const durationSec = ((Date.now() - startTime) / 1000).toFixed(0);
|
|
234
|
+
if (args.json) {
|
|
235
|
+
console.log(JSON.stringify({
|
|
236
|
+
event: 'watch_stopped',
|
|
237
|
+
ticker,
|
|
238
|
+
totalTicks,
|
|
239
|
+
durationSeconds: Number(durationSec),
|
|
240
|
+
}));
|
|
241
|
+
} else {
|
|
242
|
+
console.log('');
|
|
243
|
+
console.log(`Watch stopped. ${totalTicks} ticks in ${durationSec}s`);
|
|
244
|
+
}
|
|
245
|
+
process.exit(0);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
process.once('SIGINT', shutdown);
|
|
249
|
+
process.once('SIGTERM', shutdown);
|
|
250
|
+
|
|
251
|
+
const rawTickerInterval = Number(getBotSetting('watch.ticker_interval_seconds'));
|
|
252
|
+
const tickerIntervalMs = (Number.isFinite(rawTickerInterval) && rawTickerInterval > 0 ? rawTickerInterval : 5) * 1000;
|
|
253
|
+
const intervalMs = args.interval ? args.interval * 1000 : tickerIntervalMs;
|
|
254
|
+
const intervalLabel = intervalMs >= 60_000 ? `${(intervalMs / 60_000).toFixed(0)}m` : `${(intervalMs / 1000).toFixed(0)}s`;
|
|
255
|
+
|
|
256
|
+
if (!args.json) {
|
|
257
|
+
console.log(`Watching ${ticker} every ${intervalLabel} (Ctrl+C to stop)\n`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const runTick = async (): Promise<void> => {
|
|
261
|
+
try {
|
|
262
|
+
const snap = await fetchTickerSnapshot(ticker);
|
|
263
|
+
totalTicks++;
|
|
264
|
+
|
|
265
|
+
if (args.json) {
|
|
266
|
+
console.log(JSON.stringify(wrapSuccess('watch:ticker', snap)));
|
|
267
|
+
} else {
|
|
268
|
+
console.clear();
|
|
269
|
+
console.log(formatTickerDashboard(snap, totalTicks));
|
|
270
|
+
}
|
|
271
|
+
} catch (err) {
|
|
272
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
273
|
+
if (args.json) {
|
|
274
|
+
console.log(JSON.stringify(wrapError('watch:ticker', 'FETCH_ERROR', message)));
|
|
275
|
+
} else {
|
|
276
|
+
console.error(`[watch] Error: ${message}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
// First tick immediately
|
|
282
|
+
await runTick();
|
|
283
|
+
|
|
284
|
+
// Continue on interval
|
|
285
|
+
timer = setInterval(() => {
|
|
286
|
+
if (stopped) return;
|
|
287
|
+
runTick().catch((err) => {
|
|
288
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
289
|
+
console.error(`[watch-ticker] Tick failed: ${message}`);
|
|
290
|
+
});
|
|
291
|
+
}, intervalMs);
|
|
292
|
+
|
|
293
|
+
// Keep process alive
|
|
294
|
+
await new Promise<void>(() => {});
|
|
295
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Container, Markdown, Spacer, type TUI } from '@mariozechner/pi-tui';
|
|
2
|
+
import { formatResponse } from '../utils/markdown-table.js';
|
|
3
|
+
import { markdownTheme, theme } from '../theme.js';
|
|
4
|
+
|
|
5
|
+
const SPINNER_FRAMES = ['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'];
|
|
6
|
+
const SPINNER_INTERVAL_MS = 80;
|
|
7
|
+
|
|
8
|
+
export class AnswerBoxComponent extends Container {
|
|
9
|
+
private readonly body: Markdown;
|
|
10
|
+
private value = '';
|
|
11
|
+
private spinnerInterval: ReturnType<typeof setInterval> | null = null;
|
|
12
|
+
private spinnerFrame = 0;
|
|
13
|
+
private tui: TUI | null = null;
|
|
14
|
+
|
|
15
|
+
constructor(initialText = '') {
|
|
16
|
+
super();
|
|
17
|
+
this.addChild(new Spacer(1));
|
|
18
|
+
this.body = new Markdown('', 0, 0, markdownTheme, { color: (line) => line });
|
|
19
|
+
this.addChild(this.body);
|
|
20
|
+
this.setText(initialText);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
setText(text: string) {
|
|
24
|
+
this.value = text;
|
|
25
|
+
this.render_();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Start an animated spinner prefix. Call stopSpinner() when done. */
|
|
29
|
+
startSpinner(tui: TUI) {
|
|
30
|
+
this.tui = tui;
|
|
31
|
+
this.spinnerFrame = 0;
|
|
32
|
+
if (this.spinnerInterval) return;
|
|
33
|
+
this.spinnerInterval = setInterval(() => {
|
|
34
|
+
this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER_FRAMES.length;
|
|
35
|
+
this.render_();
|
|
36
|
+
this.tui?.requestRender();
|
|
37
|
+
}, SPINNER_INTERVAL_MS);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Stop the spinner and revert to static ⏺ prefix. */
|
|
41
|
+
stopSpinner() {
|
|
42
|
+
if (this.spinnerInterval) {
|
|
43
|
+
clearInterval(this.spinnerInterval);
|
|
44
|
+
this.spinnerInterval = null;
|
|
45
|
+
}
|
|
46
|
+
this.render_();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private render_() {
|
|
50
|
+
const rendered = formatResponse(this.value);
|
|
51
|
+
const normalized = rendered.replace(/^\n+/, '');
|
|
52
|
+
const prefix = this.spinnerInterval
|
|
53
|
+
? theme.primary(SPINNER_FRAMES[this.spinnerFrame])
|
|
54
|
+
: theme.primary('⏺');
|
|
55
|
+
this.body.setText(`${prefix}\n${normalized}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Container, Text } from '@mariozechner/pi-tui';
|
|
2
|
+
import type { ApprovalDecision } from '../agent/types.js';
|
|
3
|
+
import { createApprovalSelector } from './select-list.js';
|
|
4
|
+
import { theme } from '../theme.js';
|
|
5
|
+
|
|
6
|
+
function formatToolLabel(tool: string): string {
|
|
7
|
+
return tool
|
|
8
|
+
.split('_')
|
|
9
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
10
|
+
.join(' ');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class ApprovalPromptComponent extends Container {
|
|
14
|
+
readonly selector: any;
|
|
15
|
+
onSelect?: (decision: ApprovalDecision) => void;
|
|
16
|
+
|
|
17
|
+
constructor(tool: string, args: Record<string, unknown>) {
|
|
18
|
+
super();
|
|
19
|
+
this.selector = createApprovalSelector((decision) => this.onSelect?.(decision));
|
|
20
|
+
const width = Math.max(20, process.stdout.columns ?? 80);
|
|
21
|
+
const border = theme.warning('─'.repeat(width));
|
|
22
|
+
const path = (args.path as string) || '<unknown>';
|
|
23
|
+
|
|
24
|
+
this.addChild(new Text(border, 0, 0));
|
|
25
|
+
this.addChild(new Text(theme.warning(theme.bold('Permission required')), 0, 0));
|
|
26
|
+
this.addChild(new Text(`${formatToolLabel(tool)} ${path}`, 0, 0));
|
|
27
|
+
this.addChild(new Text(theme.muted('Do you want to allow this?'), 0, 0));
|
|
28
|
+
this.addChild(new Text('', 0, 0));
|
|
29
|
+
this.addChild(this.selector);
|
|
30
|
+
this.addChild(new Text('', 0, 0));
|
|
31
|
+
this.addChild(new Text(theme.muted('Enter to confirm · esc to deny'), 0, 0));
|
|
32
|
+
this.addChild(new Text(border, 0, 0));
|
|
33
|
+
}
|
|
34
|
+
}
|