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,40 @@
|
|
|
1
|
+
import type { GatewayConfig } from '../../config.js';
|
|
2
|
+
|
|
3
|
+
export type ReconnectPolicy = {
|
|
4
|
+
initialMs: number;
|
|
5
|
+
maxMs: number;
|
|
6
|
+
factor: number;
|
|
7
|
+
jitter: number;
|
|
8
|
+
maxAttempts: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_RECONNECT_POLICY: ReconnectPolicy = {
|
|
12
|
+
initialMs: 2_000,
|
|
13
|
+
maxMs: 30_000,
|
|
14
|
+
factor: 1.8,
|
|
15
|
+
jitter: 0.25,
|
|
16
|
+
maxAttempts: 12,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function clamp(value: number, min: number, max: number): number {
|
|
20
|
+
return Math.min(max, Math.max(min, value));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function computeBackoff(policy: ReconnectPolicy, attempt: number): number {
|
|
24
|
+
const base = policy.initialMs * policy.factor ** Math.max(attempt - 1, 0);
|
|
25
|
+
const jitter = base * policy.jitter * Math.random();
|
|
26
|
+
return Math.min(policy.maxMs, Math.round(base + jitter));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function resolveReconnectPolicy(cfg: GatewayConfig): ReconnectPolicy {
|
|
30
|
+
const merged: ReconnectPolicy = {
|
|
31
|
+
...DEFAULT_RECONNECT_POLICY,
|
|
32
|
+
...(cfg.gateway.reconnect ?? {}),
|
|
33
|
+
};
|
|
34
|
+
merged.initialMs = Math.max(250, merged.initialMs);
|
|
35
|
+
merged.maxMs = Math.max(merged.initialMs, merged.maxMs);
|
|
36
|
+
merged.factor = clamp(merged.factor, 1.1, 10);
|
|
37
|
+
merged.jitter = clamp(merged.jitter, 0, 1);
|
|
38
|
+
merged.maxAttempts = Math.max(0, Math.floor(merged.maxAttempts));
|
|
39
|
+
return merged;
|
|
40
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { monitorWebInbox } from './inbound.js';
|
|
2
|
+
import { setActiveWebListener } from './outbound.js';
|
|
3
|
+
import { logout } from './auth-store.js';
|
|
4
|
+
import type { WhatsAppInboundMessage } from './types.js';
|
|
5
|
+
import type { ReconnectPolicy } from './reconnect.js';
|
|
6
|
+
import { computeBackoff, DEFAULT_RECONNECT_POLICY } from './reconnect.js';
|
|
7
|
+
|
|
8
|
+
export async function monitorWhatsAppChannel(params: {
|
|
9
|
+
accountId: string;
|
|
10
|
+
authDir: string;
|
|
11
|
+
verbose: boolean;
|
|
12
|
+
allowFrom: string[];
|
|
13
|
+
dmPolicy: 'pairing' | 'allowlist' | 'open' | 'disabled';
|
|
14
|
+
groupPolicy: 'open' | 'allowlist' | 'disabled';
|
|
15
|
+
groupAllowFrom: string[];
|
|
16
|
+
sendReadReceipts?: boolean;
|
|
17
|
+
heartbeatSeconds?: number;
|
|
18
|
+
reconnect?: ReconnectPolicy;
|
|
19
|
+
abortSignal: AbortSignal;
|
|
20
|
+
onMessage: (msg: WhatsAppInboundMessage) => Promise<void>;
|
|
21
|
+
onStatus?: (status: { connected: boolean; lastError?: string | null }) => void;
|
|
22
|
+
}): Promise<void> {
|
|
23
|
+
const MESSAGE_TIMEOUT_MS = 30 * 60 * 1000;
|
|
24
|
+
const WATCHDOG_CHECK_MS = 60 * 1000;
|
|
25
|
+
const heartbeatSeconds =
|
|
26
|
+
typeof params.heartbeatSeconds === 'number' && params.heartbeatSeconds > 0
|
|
27
|
+
? params.heartbeatSeconds
|
|
28
|
+
: 60;
|
|
29
|
+
const reconnectPolicy = params.reconnect ?? DEFAULT_RECONNECT_POLICY;
|
|
30
|
+
let reconnectAttempts = 0;
|
|
31
|
+
while (!params.abortSignal.aborted) {
|
|
32
|
+
const startedAt = Date.now();
|
|
33
|
+
let handledMessages = 0;
|
|
34
|
+
let lastMessageAt: number | null = null;
|
|
35
|
+
let heartbeat: NodeJS.Timeout | null = null;
|
|
36
|
+
let watchdog: NodeJS.Timeout | null = null;
|
|
37
|
+
try {
|
|
38
|
+
const listener = await monitorWebInbox({
|
|
39
|
+
accountId: params.accountId,
|
|
40
|
+
authDir: params.authDir,
|
|
41
|
+
verbose: params.verbose,
|
|
42
|
+
allowFrom: params.allowFrom,
|
|
43
|
+
dmPolicy: params.dmPolicy,
|
|
44
|
+
groupPolicy: params.groupPolicy,
|
|
45
|
+
groupAllowFrom: params.groupAllowFrom,
|
|
46
|
+
sendReadReceipts: params.sendReadReceipts,
|
|
47
|
+
onMessage: async (msg) => {
|
|
48
|
+
handledMessages += 1;
|
|
49
|
+
lastMessageAt = Date.now();
|
|
50
|
+
await params.onMessage(msg);
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
setActiveWebListener(params.accountId, listener.sock);
|
|
54
|
+
params.onStatus?.({ connected: true, lastError: null });
|
|
55
|
+
heartbeat = setInterval(() => {
|
|
56
|
+
const uptimeMs = Date.now() - startedAt;
|
|
57
|
+
if (params.verbose) {
|
|
58
|
+
console.log(
|
|
59
|
+
`[whatsapp heartbeat] account=${params.accountId} messages=${handledMessages} uptimeMs=${uptimeMs}`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}, heartbeatSeconds * 1000);
|
|
63
|
+
watchdog = setInterval(() => {
|
|
64
|
+
if (!lastMessageAt) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (Date.now() - lastMessageAt <= MESSAGE_TIMEOUT_MS) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
void listener.close();
|
|
71
|
+
}, WATCHDOG_CHECK_MS);
|
|
72
|
+
const closeReason = await listener.onClose;
|
|
73
|
+
await listener.close();
|
|
74
|
+
setActiveWebListener(params.accountId, null);
|
|
75
|
+
if (heartbeat) {
|
|
76
|
+
clearInterval(heartbeat);
|
|
77
|
+
}
|
|
78
|
+
if (watchdog) {
|
|
79
|
+
clearInterval(watchdog);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (closeReason.isLoggedOut) {
|
|
83
|
+
// Clear stale credentials when WhatsApp reports logged out (401)
|
|
84
|
+
await logout(params.authDir);
|
|
85
|
+
params.onStatus?.({ connected: false, lastError: 'logged out - please re-run gateway:login' });
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const uptimeMs = Date.now() - startedAt;
|
|
90
|
+
if (uptimeMs > heartbeatSeconds * 1000) {
|
|
91
|
+
reconnectAttempts = 0;
|
|
92
|
+
}
|
|
93
|
+
reconnectAttempts += 1;
|
|
94
|
+
const delayMs = computeBackoff(reconnectPolicy, reconnectAttempts);
|
|
95
|
+
params.onStatus?.({
|
|
96
|
+
connected: false,
|
|
97
|
+
lastError: `disconnected (attempt ${reconnectAttempts})`,
|
|
98
|
+
});
|
|
99
|
+
if (reconnectPolicy.maxAttempts > 0 && reconnectAttempts >= reconnectPolicy.maxAttempts) {
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if (heartbeat) {
|
|
105
|
+
clearInterval(heartbeat);
|
|
106
|
+
}
|
|
107
|
+
if (watchdog) {
|
|
108
|
+
clearInterval(watchdog);
|
|
109
|
+
}
|
|
110
|
+
reconnectAttempts += 1;
|
|
111
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
112
|
+
params.onStatus?.({ connected: false, lastError: message });
|
|
113
|
+
const delayMs = computeBackoff(reconnectPolicy, reconnectAttempts);
|
|
114
|
+
if (reconnectPolicy.maxAttempts > 0 && reconnectAttempts >= reconnectPolicy.maxAttempts) {
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
setActiveWebListener(params.accountId, null);
|
|
121
|
+
}
|
|
122
|
+
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DisconnectReason,
|
|
3
|
+
fetchLatestBaileysVersion,
|
|
4
|
+
makeCacheableSignalKeyStore,
|
|
5
|
+
makeWASocket,
|
|
6
|
+
useMultiFileAuthState,
|
|
7
|
+
type ConnectionState,
|
|
8
|
+
} from '@whiskeysockets/baileys';
|
|
9
|
+
import { mkdirSync } from 'node:fs';
|
|
10
|
+
import { createSilentLogger } from './logger.js';
|
|
11
|
+
import { maybeRestoreCredsFromBackup, backupCredsBeforeSave } from './auth-store.js';
|
|
12
|
+
|
|
13
|
+
export type WaSocket = ReturnType<typeof makeWASocket>;
|
|
14
|
+
|
|
15
|
+
export async function createWaSocket(params: {
|
|
16
|
+
authDir: string;
|
|
17
|
+
printQr: boolean;
|
|
18
|
+
onQr?: (qr: string) => void;
|
|
19
|
+
verbose?: boolean;
|
|
20
|
+
}): Promise<WaSocket> {
|
|
21
|
+
mkdirSync(params.authDir, { recursive: true });
|
|
22
|
+
|
|
23
|
+
// Restore credentials from backup if main creds.json is corrupted
|
|
24
|
+
maybeRestoreCredsFromBackup(params.authDir);
|
|
25
|
+
|
|
26
|
+
const { state, saveCreds } = await useMultiFileAuthState(params.authDir);
|
|
27
|
+
const { version } = await fetchLatestBaileysVersion();
|
|
28
|
+
const logger = createSilentLogger();
|
|
29
|
+
const sock = makeWASocket({
|
|
30
|
+
auth: {
|
|
31
|
+
creds: state.creds,
|
|
32
|
+
keys: makeCacheableSignalKeyStore(state.keys, logger),
|
|
33
|
+
},
|
|
34
|
+
version,
|
|
35
|
+
logger,
|
|
36
|
+
printQRInTerminal: params.printQr,
|
|
37
|
+
browser: ['octagon', 'cli', '1.0.0'],
|
|
38
|
+
markOnlineOnConnect: false,
|
|
39
|
+
syncFullHistory: false,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Backup credentials before each save
|
|
43
|
+
sock.ev.on('creds.update', () => {
|
|
44
|
+
backupCredsBeforeSave(params.authDir);
|
|
45
|
+
saveCreds();
|
|
46
|
+
});
|
|
47
|
+
sock.ev.on('connection.update', (update: Partial<ConnectionState>) => {
|
|
48
|
+
if (update.qr) {
|
|
49
|
+
params.onQr?.(update.qr);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Handle WebSocket-level errors to prevent unhandled exceptions
|
|
54
|
+
if (sock.ws && typeof (sock.ws as unknown as { on?: unknown }).on === 'function') {
|
|
55
|
+
sock.ws.on('error', () => {
|
|
56
|
+
// Silently handle WebSocket errors - reconnection logic handles recovery
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return sock;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function waitForWaConnection(sock: WaSocket): Promise<void> {
|
|
64
|
+
return await new Promise<void>((resolve, reject) => {
|
|
65
|
+
const onUpdate = (update: Partial<ConnectionState>) => {
|
|
66
|
+
if (update.connection === 'open') {
|
|
67
|
+
sock.ev.off('connection.update', onUpdate);
|
|
68
|
+
resolve();
|
|
69
|
+
}
|
|
70
|
+
if (update.connection === 'close') {
|
|
71
|
+
sock.ev.off('connection.update', onUpdate);
|
|
72
|
+
reject(update.lastDisconnect?.error ?? new Error('Connection closed'));
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
sock.ev.on('connection.update', onUpdate);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function getStatusCode(error: unknown): number | undefined {
|
|
80
|
+
return (
|
|
81
|
+
(error as { output?: { statusCode?: number } })?.output?.statusCode ??
|
|
82
|
+
(error as { status?: number })?.status
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function isLoggedOutReason(error: unknown): boolean {
|
|
87
|
+
return getStatusCode(error) === DisconnectReason.loggedOut;
|
|
88
|
+
}
|
|
89
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { AnyMessageContent } from '@whiskeysockets/baileys';
|
|
2
|
+
|
|
3
|
+
export type WhatsAppInboundMessage = {
|
|
4
|
+
id?: string;
|
|
5
|
+
accountId: string;
|
|
6
|
+
chatId: string;
|
|
7
|
+
/** The JID to use when replying (resolved from LID to phone JID if applicable) */
|
|
8
|
+
replyToJid: string;
|
|
9
|
+
chatType: 'direct' | 'group';
|
|
10
|
+
from: string;
|
|
11
|
+
senderId: string;
|
|
12
|
+
senderName?: string;
|
|
13
|
+
isFromMe?: boolean;
|
|
14
|
+
selfE164?: string | null;
|
|
15
|
+
groupSubject?: string;
|
|
16
|
+
groupParticipants?: string[];
|
|
17
|
+
mentionedJids?: string[];
|
|
18
|
+
selfJid?: string | null;
|
|
19
|
+
selfLid?: string | null;
|
|
20
|
+
body: string;
|
|
21
|
+
timestamp?: number;
|
|
22
|
+
sendComposing: () => Promise<void>;
|
|
23
|
+
reply: (text: string) => Promise<void>;
|
|
24
|
+
sendMedia: (payload: AnyMessageContent) => Promise<void>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type WhatsAppCloseReason = {
|
|
28
|
+
status?: number;
|
|
29
|
+
isLoggedOut: boolean;
|
|
30
|
+
error?: unknown;
|
|
31
|
+
};
|
|
32
|
+
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { CommandIntent } from './parser.js';
|
|
2
|
+
import type { AlertRouter } from '../alerts/router.js';
|
|
3
|
+
import type { ParsedArgs } from '../../commands/parse-args.js';
|
|
4
|
+
import { handleScan } from '../../commands/scan.js';
|
|
5
|
+
import { handleEdge } from '../../commands/edge.js';
|
|
6
|
+
import { handlePortfolio } from '../../commands/portfolio.js';
|
|
7
|
+
import {
|
|
8
|
+
formatScanForWhatsApp,
|
|
9
|
+
formatEdgeForWhatsApp,
|
|
10
|
+
formatPortfolioForWhatsApp,
|
|
11
|
+
} from './wa-formatters.js';
|
|
12
|
+
|
|
13
|
+
function makeArgs(overrides: Partial<ParsedArgs>): ParsedArgs {
|
|
14
|
+
return {
|
|
15
|
+
subcommand: 'chat',
|
|
16
|
+
positionalArgs: [],
|
|
17
|
+
json: false,
|
|
18
|
+
live: false,
|
|
19
|
+
refresh: false,
|
|
20
|
+
report: false,
|
|
21
|
+
dryRun: false,
|
|
22
|
+
verbose: false,
|
|
23
|
+
performance: false,
|
|
24
|
+
resolved: false,
|
|
25
|
+
unresolved: false,
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
parseErrors: [],
|
|
29
|
+
...overrides,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function handleCommand(
|
|
34
|
+
intent: CommandIntent,
|
|
35
|
+
alertRouter: AlertRouter,
|
|
36
|
+
sessionKey: string,
|
|
37
|
+
): Promise<string | null> {
|
|
38
|
+
switch (intent.type) {
|
|
39
|
+
case 'none':
|
|
40
|
+
return null;
|
|
41
|
+
|
|
42
|
+
case 'scan': {
|
|
43
|
+
const args = makeArgs({ theme: intent.theme });
|
|
44
|
+
const result = await handleScan(args);
|
|
45
|
+
if (!result.ok) return `Scan failed: ${result.error?.message ?? 'unknown error'}`;
|
|
46
|
+
return formatScanForWhatsApp(result.data);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
case 'edge': {
|
|
50
|
+
const args = makeArgs({ subcommand: 'edge', ticker: intent.ticker });
|
|
51
|
+
const result = await handleEdge(args);
|
|
52
|
+
if (!result.ok) return `Edge failed: ${result.error?.message ?? 'unknown error'}`;
|
|
53
|
+
return formatEdgeForWhatsApp(result.data);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
case 'portfolio': {
|
|
57
|
+
const args = makeArgs({ subcommand: 'portfolio' });
|
|
58
|
+
const result = await handlePortfolio(args);
|
|
59
|
+
if (!result.ok) return `Portfolio failed: ${result.error?.message ?? 'unknown error'}`;
|
|
60
|
+
return formatPortfolioForWhatsApp(result.data);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type CommandIntent =
|
|
2
|
+
| { type: 'scan'; theme?: string }
|
|
3
|
+
| { type: 'edge'; ticker?: string }
|
|
4
|
+
| { type: 'portfolio' }
|
|
5
|
+
| { type: 'none' };
|
|
6
|
+
|
|
7
|
+
export function parseCommand(body: string): CommandIntent {
|
|
8
|
+
const trimmed = body.trim();
|
|
9
|
+
const lower = trimmed.toLowerCase();
|
|
10
|
+
|
|
11
|
+
// Portfolio / positions
|
|
12
|
+
if (lower === 'portfolio' || lower === 'positions') {
|
|
13
|
+
return { type: 'portfolio' };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Scan [theme]
|
|
17
|
+
if (lower === 'scan' || lower.startsWith('scan ')) {
|
|
18
|
+
const rest = trimmed.slice(4).trim();
|
|
19
|
+
return { type: 'scan', theme: rest || undefined };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Edge [ticker]
|
|
23
|
+
if (lower === 'edge' || lower.startsWith('edge ')) {
|
|
24
|
+
const rest = trimmed.slice(4).trim();
|
|
25
|
+
return { type: 'edge', ticker: rest || undefined };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return { type: 'none' };
|
|
29
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { ScanResult } from '../../scan/loop.js';
|
|
2
|
+
import type { PortfolioData } from '../../commands/portfolio.js';
|
|
3
|
+
import type { EdgeRow } from '../../db/edge.js';
|
|
4
|
+
|
|
5
|
+
const MAX_ITEMS = 8;
|
|
6
|
+
|
|
7
|
+
export function formatScanForWhatsApp(result: ScanResult): string {
|
|
8
|
+
const lines: string[] = [];
|
|
9
|
+
lines.push(`*SCAN COMPLETE*`);
|
|
10
|
+
lines.push(`Events: ${result.eventsScanned} | Edges: ${result.edgeSnapshots.length} | Alerts: ${result.alerts.length}`);
|
|
11
|
+
lines.push('');
|
|
12
|
+
|
|
13
|
+
const actionable = result.edgeSnapshots
|
|
14
|
+
.filter((s) => s.confidence === 'high' || s.confidence === 'very_high')
|
|
15
|
+
.sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge))
|
|
16
|
+
.slice(0, MAX_ITEMS);
|
|
17
|
+
|
|
18
|
+
if (actionable.length === 0) {
|
|
19
|
+
lines.push('No actionable edges found.');
|
|
20
|
+
} else {
|
|
21
|
+
lines.push('*Top Edges:*');
|
|
22
|
+
for (const snap of actionable) {
|
|
23
|
+
const sign = snap.edge >= 0 ? '+' : '';
|
|
24
|
+
const edgePct = `${sign}${(snap.edge * 100).toFixed(1)}%`;
|
|
25
|
+
const conf = snap.confidence === 'very_high' ? 'V.HIGH' : snap.confidence.toUpperCase();
|
|
26
|
+
lines.push(`• *${snap.ticker}* ${edgePct} (${conf})`);
|
|
27
|
+
if (snap.drivers[0]) {
|
|
28
|
+
lines.push(` _${snap.drivers[0].claim}_`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
lines.push('');
|
|
34
|
+
lines.push(`Duration: ${(result.duration / 1000).toFixed(1)}s | Credits: ${result.octagonCreditsUsed}`);
|
|
35
|
+
return lines.join('\n');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function formatEdgeForWhatsApp(rows: EdgeRow[]): string {
|
|
39
|
+
const lines: string[] = [];
|
|
40
|
+
lines.push(`*EDGE SNAPSHOT*`);
|
|
41
|
+
lines.push('');
|
|
42
|
+
|
|
43
|
+
const sorted = [...rows]
|
|
44
|
+
.sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge))
|
|
45
|
+
.slice(0, MAX_ITEMS);
|
|
46
|
+
|
|
47
|
+
if (sorted.length === 0) {
|
|
48
|
+
lines.push('No edges found.');
|
|
49
|
+
} else {
|
|
50
|
+
for (const row of sorted) {
|
|
51
|
+
const sign = row.edge >= 0 ? '+' : '';
|
|
52
|
+
const edgePct = `${sign}${(row.edge * 100).toFixed(1)}%`;
|
|
53
|
+
const conf = row.confidence === 'very_high' ? 'V.HIGH' : (row.confidence ?? 'N/A').toUpperCase();
|
|
54
|
+
lines.push(`• *${row.ticker}* ${edgePct} (${conf})`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return lines.join('\n');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function formatPortfolioForWhatsApp(data: PortfolioData): string {
|
|
62
|
+
const lines: string[] = [];
|
|
63
|
+
lines.push(`*PORTFOLIO*`);
|
|
64
|
+
lines.push('');
|
|
65
|
+
|
|
66
|
+
const acct = data.accountSummary;
|
|
67
|
+
if (acct) {
|
|
68
|
+
lines.push(`Cash: $${(acct.cashBalance / 100).toFixed(2)}`);
|
|
69
|
+
lines.push(`Portfolio: $${(acct.portfolioValue / 100).toFixed(2)}`);
|
|
70
|
+
lines.push(`Exposure: $${(acct.openExposure / 100).toFixed(2)}`);
|
|
71
|
+
lines.push(`Available: $${(acct.available / 100).toFixed(2)}`);
|
|
72
|
+
} else {
|
|
73
|
+
lines.push(`Account data unavailable`);
|
|
74
|
+
}
|
|
75
|
+
lines.push('');
|
|
76
|
+
|
|
77
|
+
if (data.positions.length === 0) {
|
|
78
|
+
lines.push('No open positions.');
|
|
79
|
+
} else {
|
|
80
|
+
lines.push(`*Positions (${data.positions.length}):*`);
|
|
81
|
+
for (const p of data.positions.slice(0, MAX_ITEMS)) {
|
|
82
|
+
const edgeTxt = p.currentEdge !== null ? `${(p.currentEdge * 100).toFixed(1)}%` : '-';
|
|
83
|
+
const pnlTxt = p.unrealizedPnl !== null ? `$${(p.unrealizedPnl / 100).toFixed(2)}` : '-';
|
|
84
|
+
lines.push(`• *${p.ticker}* ${p.direction.toUpperCase()} x${p.size} | Edge: ${edgeTxt} | P&L: ${pnlTxt}`);
|
|
85
|
+
}
|
|
86
|
+
if (data.positions.length > MAX_ITEMS) {
|
|
87
|
+
lines.push(` _...and ${data.positions.length - MAX_ITEMS} more_`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return lines.join('\n');
|
|
92
|
+
}
|