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,112 @@
|
|
|
1
|
+
import { DynamicStructuredTool } from '@langchain/core/tools';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getDb } from '../../db/index.js';
|
|
4
|
+
import { getLatestReport } from '../../db/octagon-cache.js';
|
|
5
|
+
import { createOctagonInvoker, callOctagon } from '../../scan/invoker.js';
|
|
6
|
+
import { OctagonClient } from '../../scan/octagon-client.js';
|
|
7
|
+
import { auditTrail } from '../../audit/index.js';
|
|
8
|
+
import { formatToolResult } from '../types.js';
|
|
9
|
+
|
|
10
|
+
export const octagonReportTool = new DynamicStructuredTool({
|
|
11
|
+
name: 'octagon_report',
|
|
12
|
+
description: 'Fetch a full Octagon AI research report for a Kalshi market. Accepts a ticker or a full Kalshi market URL.',
|
|
13
|
+
schema: z.object({
|
|
14
|
+
ticker: z.string().describe('Market ticker (e.g. KXBTC-26MAR-B80000) or full Kalshi URL (e.g. https://kalshi.com/markets/kxcpiyoy/inflation/kxcpiyoy-26mar)'),
|
|
15
|
+
forceRefresh: z.boolean().optional().describe('Force a fresh API call instead of using cache'),
|
|
16
|
+
}),
|
|
17
|
+
func: async ({ ticker, forceRefresh }) => {
|
|
18
|
+
const db = getDb();
|
|
19
|
+
const isUrl = ticker.startsWith('http');
|
|
20
|
+
|
|
21
|
+
// Extract ticker from URL for cache lookup if needed
|
|
22
|
+
const cacheKey = isUrl ? ticker.split('/').pop() ?? ticker : ticker;
|
|
23
|
+
|
|
24
|
+
// Always call Octagon API — no local DB cache
|
|
25
|
+
// Octagon's own API has a cache variant (0 credits) that handles caching server-side
|
|
26
|
+
if (!process.env.OCTAGON_API_KEY) {
|
|
27
|
+
return formatToolResult({
|
|
28
|
+
error: 'OCTAGON_API_KEY not set. Cannot fetch fresh report.',
|
|
29
|
+
ticker: cacheKey,
|
|
30
|
+
...((() => {
|
|
31
|
+
const stale = getLatestReport(db, cacheKey);
|
|
32
|
+
if (stale) {
|
|
33
|
+
return {
|
|
34
|
+
staleCache: true,
|
|
35
|
+
modelProb: stale.model_prob,
|
|
36
|
+
marketProb: stale.market_prob,
|
|
37
|
+
mispricingSignal: stale.mispricing_signal,
|
|
38
|
+
drivers: stale.drivers_json ? JSON.parse(stale.drivers_json) : [],
|
|
39
|
+
fetchedAt: stale.fetched_at,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return {};
|
|
43
|
+
})()),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const invoker = createOctagonInvoker();
|
|
48
|
+
const client = new OctagonClient(invoker, db, auditTrail);
|
|
49
|
+
const parts = cacheKey.split('-');
|
|
50
|
+
const eventTicker = parts.length >= 2 ? `${parts[0]}-${parts[1]}` : cacheKey;
|
|
51
|
+
|
|
52
|
+
// Try Octagon's cache first (0 credits), fall back to default if useless
|
|
53
|
+
const input = isUrl ? ticker : cacheKey;
|
|
54
|
+
let variant: 'cache' | 'default' | 'refresh' = 'cache';
|
|
55
|
+
let raw = await callOctagon(input, variant);
|
|
56
|
+
let report = client.parseReport(raw, cacheKey, eventTicker, variant);
|
|
57
|
+
|
|
58
|
+
// If cache returned a useless report (default 0.5/0.5), retry with 'default'
|
|
59
|
+
if (report.modelProb === 0.5 && report.drivers.length === 0) {
|
|
60
|
+
variant = 'default';
|
|
61
|
+
raw = await callOctagon(input, variant);
|
|
62
|
+
report = client.parseReport(raw, cacheKey, eventTicker, variant);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Persist to DB
|
|
66
|
+
const { insertReport } = await import('../../db/octagon-cache.js');
|
|
67
|
+
const dbRow = client.toDbRow(report);
|
|
68
|
+
insertReport(db, dbRow);
|
|
69
|
+
|
|
70
|
+
return formatToolResult({
|
|
71
|
+
ticker: cacheKey,
|
|
72
|
+
cached: variant === 'cache',
|
|
73
|
+
modelProb: report.modelProb,
|
|
74
|
+
marketProb: report.marketProb,
|
|
75
|
+
mispricingSignal: report.mispricingSignal,
|
|
76
|
+
drivers: report.drivers,
|
|
77
|
+
catalysts: report.catalysts,
|
|
78
|
+
sources: report.sources,
|
|
79
|
+
resolutionHistory: report.resolutionHistory,
|
|
80
|
+
contractSnapshot: report.contractSnapshot,
|
|
81
|
+
variantUsed: report.variantUsed,
|
|
82
|
+
fetchedAt: report.fetchedAt,
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
export const OCTAGON_REPORT_DESCRIPTION = `
|
|
88
|
+
Fetch a full Octagon AI research report for a Kalshi market. Returns model probability, price drivers, catalysts, and sources.
|
|
89
|
+
|
|
90
|
+
## When to Use
|
|
91
|
+
- User asks for a deep dive, analysis, or research on any market
|
|
92
|
+
- User asks about edge, mispricing, or probability estimates
|
|
93
|
+
- Any time you want model fair value vs market price
|
|
94
|
+
- Use alongside kalshi_search for comprehensive analysis
|
|
95
|
+
|
|
96
|
+
## Input
|
|
97
|
+
- IMPORTANT: NEVER guess or construct tickers yourself — only use exact tickers returned by kalshi_search results
|
|
98
|
+
- kalshi_search already auto-fetches an Octagon report for the top result — check if the data you need is already in the kalshi_search response before calling this tool separately
|
|
99
|
+
- PREFERRED: Pass a full Kalshi URL (e.g. https://kalshi.com/markets/kxcpiyoy/inflation/kxcpiyoy-26mar) — this is what Octagon expects
|
|
100
|
+
- Also accepts a market ticker (e.g. KXBTC-26MAR-B80000) — will be resolved to a URL automatically
|
|
101
|
+
- If you got market data from kalshi_search, construct the URL as: https://kalshi.com/markets/SERIES/EVENT/TICKER (all lowercase) using the series_ticker, event_ticker, and ticker fields
|
|
102
|
+
|
|
103
|
+
## When NOT to Use
|
|
104
|
+
- For quick edge data already in the database (use edge_query)
|
|
105
|
+
- For market prices or orderbook data only (use kalshi_search)
|
|
106
|
+
- When kalshi_search already returned an octagon_report in its response — don't call again
|
|
107
|
+
|
|
108
|
+
## Notes
|
|
109
|
+
- Returns cached reports when available (< 24h old)
|
|
110
|
+
- Use forceRefresh=true to get a fresh report (costs API credits)
|
|
111
|
+
- When analyzing an event with multiple markets, pick the most relevant ticker and call this tool — don't ask the user to choose
|
|
112
|
+
`.trim();
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { DynamicStructuredTool } from '@langchain/core/tools';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getDb } from '../../db/index.js';
|
|
4
|
+
import { getOpenPositions, getPositionWithEdge } from '../../db/positions.js';
|
|
5
|
+
import { fetchLiveBankroll } from '../../risk/kelly.js';
|
|
6
|
+
import { getLatestSnapshot } from '../../db/risk.js';
|
|
7
|
+
import { computePerformance } from '../../eval/performance.js';
|
|
8
|
+
import { formatToolResult } from '../types.js';
|
|
9
|
+
|
|
10
|
+
export const portfolioQueryTool = new DynamicStructuredTool({
|
|
11
|
+
name: 'portfolio_query',
|
|
12
|
+
description: 'Get positions with current edge data, P&L, bankroll summary, and optional performance stats.',
|
|
13
|
+
schema: z.object({
|
|
14
|
+
includePerformance: z.boolean().optional().describe('Include historical performance stats (win rate, Sharpe, Brier scores)'),
|
|
15
|
+
}),
|
|
16
|
+
func: async ({ includePerformance }) => {
|
|
17
|
+
const db = getDb();
|
|
18
|
+
|
|
19
|
+
const openPositions = getOpenPositions(db);
|
|
20
|
+
const positions = openPositions.map((pos) => {
|
|
21
|
+
const withEdge = getPositionWithEdge(db, pos.position_id);
|
|
22
|
+
return {
|
|
23
|
+
ticker: pos.ticker,
|
|
24
|
+
direction: pos.direction,
|
|
25
|
+
size: pos.size,
|
|
26
|
+
entryPrice: pos.entry_price,
|
|
27
|
+
entryEdge: pos.entry_edge ?? null,
|
|
28
|
+
currentEdge: withEdge?.latest_edge?.edge ?? null,
|
|
29
|
+
unrealizedPnl: pos.current_pnl ?? null,
|
|
30
|
+
status: pos.status,
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
let bankroll;
|
|
35
|
+
try {
|
|
36
|
+
bankroll = await fetchLiveBankroll();
|
|
37
|
+
} catch {
|
|
38
|
+
bankroll = null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const riskSnapshot = getLatestSnapshot(db);
|
|
42
|
+
|
|
43
|
+
const result: Record<string, unknown> = {
|
|
44
|
+
positions,
|
|
45
|
+
positionsCount: positions.length,
|
|
46
|
+
bankroll: bankroll ? {
|
|
47
|
+
cashBalance: bankroll.cashBalance,
|
|
48
|
+
portfolioValue: bankroll.portfolioValue,
|
|
49
|
+
openExposure: bankroll.openExposure,
|
|
50
|
+
available: bankroll.availableBankroll,
|
|
51
|
+
} : null,
|
|
52
|
+
riskSnapshot: riskSnapshot ? {
|
|
53
|
+
drawdownCurrent: riskSnapshot.drawdown_current,
|
|
54
|
+
drawdownMax: riskSnapshot.drawdown_max,
|
|
55
|
+
dailyPnl: riskSnapshot.daily_pnl,
|
|
56
|
+
circuitBreakerOn: !!riskSnapshot.circuit_breaker_on,
|
|
57
|
+
} : null,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
if (includePerformance) {
|
|
61
|
+
result.performance = computePerformance(db);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return formatToolResult(result);
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export const PORTFOLIO_QUERY_DESCRIPTION = `
|
|
69
|
+
Get positions with current edge data, P&L, bankroll summary, and optional performance stats.
|
|
70
|
+
|
|
71
|
+
## When to Use
|
|
72
|
+
- User asks about their positions with edge context ("how are my positions doing?")
|
|
73
|
+
- Needs bankroll + positions + risk in one call
|
|
74
|
+
- Wants performance stats (win rate, Sharpe, Brier)
|
|
75
|
+
|
|
76
|
+
## When NOT to Use
|
|
77
|
+
- Quick balance check only (use portfolio_overview)
|
|
78
|
+
- Detailed order history or fills (use kalshi_search)
|
|
79
|
+
`.trim();
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { DynamicStructuredTool } from '@langchain/core/tools';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { reviewPortfolio } from '../../commands/review.js';
|
|
4
|
+
import { formatToolResult } from '../types.js';
|
|
5
|
+
|
|
6
|
+
export const portfolioReviewTool = new DynamicStructuredTool({
|
|
7
|
+
name: 'portfolio_review',
|
|
8
|
+
description: 'Review all open positions for close recommendations. Analyzes edge direction vs position direction and flags positions where the edge has reversed.',
|
|
9
|
+
schema: z.object({
|
|
10
|
+
sellOnly: z.boolean().optional().describe('If true, only return positions with SELL signals'),
|
|
11
|
+
}),
|
|
12
|
+
func: async ({ sellOnly }) => {
|
|
13
|
+
const reviews = await reviewPortfolio();
|
|
14
|
+
|
|
15
|
+
const filtered = sellOnly ? reviews.filter((r) => r.signal === 'SELL') : reviews;
|
|
16
|
+
|
|
17
|
+
const summary = {
|
|
18
|
+
totalPositions: reviews.length,
|
|
19
|
+
sellSignals: reviews.filter((r) => r.signal === 'SELL').length,
|
|
20
|
+
holdSignals: reviews.filter((r) => r.signal === 'HOLD').length,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return formatToolResult({
|
|
24
|
+
summary,
|
|
25
|
+
positions: filtered.map((r) => ({
|
|
26
|
+
ticker: r.ticker,
|
|
27
|
+
direction: r.direction,
|
|
28
|
+
size: r.size,
|
|
29
|
+
edge: r.edge,
|
|
30
|
+
edgePp: `${r.edge >= 0 ? '+' : ''}${(r.edge * 100).toFixed(0)}pp`,
|
|
31
|
+
signal: r.signal,
|
|
32
|
+
reason: r.reason,
|
|
33
|
+
closePriceCents: r.closePriceCents,
|
|
34
|
+
sellCommand: r.signal === 'SELL'
|
|
35
|
+
? `/sell ${r.ticker} ${r.size} ${r.closePriceCents} ${r.direction}`
|
|
36
|
+
: null,
|
|
37
|
+
analyzeError: r.analyzeError ?? null,
|
|
38
|
+
})),
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export const PORTFOLIO_REVIEW_DESCRIPTION = `
|
|
44
|
+
Review all open positions for close (sell) recommendations based on edge reversal.
|
|
45
|
+
|
|
46
|
+
## When to Use
|
|
47
|
+
- User asks "what should I close?", "review my positions", "any sells?"
|
|
48
|
+
- User wants to close underwater or reversed-edge positions
|
|
49
|
+
- Before executing portfolio-wide close actions
|
|
50
|
+
- User asks to "close my positions" or "sell my holdings"
|
|
51
|
+
|
|
52
|
+
## When NOT to Use
|
|
53
|
+
- Quick balance check only (use portfolio_overview)
|
|
54
|
+
- Opening new positions (use edge_query or scan_markets)
|
|
55
|
+
|
|
56
|
+
## How to Act on Results
|
|
57
|
+
For each position with signal=SELL, use kalshi_trade to execute the close.
|
|
58
|
+
Each trade requires user approval. Present the sell recommendations first, then execute.
|
|
59
|
+
`.trim();
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { DynamicStructuredTool } from '@langchain/core/tools';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getDb } from '../../db/index.js';
|
|
4
|
+
import { getOpenPositions } from '../../db/positions.js';
|
|
5
|
+
import { getLatestSnapshot } from '../../db/risk.js';
|
|
6
|
+
import { formatToolResult } from '../types.js';
|
|
7
|
+
|
|
8
|
+
export const riskStatusTool = new DynamicStructuredTool({
|
|
9
|
+
name: 'risk_status',
|
|
10
|
+
description: 'Check risk gate status: circuit breaker, drawdown, correlation, concentration limits.',
|
|
11
|
+
schema: z.object({}),
|
|
12
|
+
func: async () => {
|
|
13
|
+
const db = getDb();
|
|
14
|
+
|
|
15
|
+
const snapshot = getLatestSnapshot(db);
|
|
16
|
+
const openPositions = getOpenPositions(db);
|
|
17
|
+
|
|
18
|
+
const maxDrawdownPct = 0.20;
|
|
19
|
+
const maxTotalPositions = 10;
|
|
20
|
+
|
|
21
|
+
const checks = [];
|
|
22
|
+
|
|
23
|
+
// Circuit breaker
|
|
24
|
+
const circuitBreakerOn = !!snapshot?.circuit_breaker_on;
|
|
25
|
+
checks.push({
|
|
26
|
+
name: 'circuit_breaker',
|
|
27
|
+
status: circuitBreakerOn ? 'TRIGGERED' : 'OK',
|
|
28
|
+
detail: circuitBreakerOn ? 'Circuit breaker is active — no new trades allowed' : 'Circuit breaker inactive',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Drawdown
|
|
32
|
+
const drawdown = snapshot?.drawdown_current ?? 0;
|
|
33
|
+
const drawdownOk = drawdown < maxDrawdownPct;
|
|
34
|
+
checks.push({
|
|
35
|
+
name: 'drawdown',
|
|
36
|
+
status: drawdownOk ? 'OK' : 'EXCEEDED',
|
|
37
|
+
detail: `Current: ${(drawdown * 100).toFixed(1)}% / Max allowed: ${maxDrawdownPct * 100}%`,
|
|
38
|
+
current: drawdown,
|
|
39
|
+
limit: maxDrawdownPct,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Concentration
|
|
43
|
+
const posCount = openPositions.length;
|
|
44
|
+
const concentrationOk = posCount < maxTotalPositions;
|
|
45
|
+
checks.push({
|
|
46
|
+
name: 'concentration',
|
|
47
|
+
status: concentrationOk ? 'OK' : 'EXCEEDED',
|
|
48
|
+
detail: `${posCount} open positions / Max: ${maxTotalPositions}`,
|
|
49
|
+
current: posCount,
|
|
50
|
+
limit: maxTotalPositions,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Max drawdown
|
|
54
|
+
checks.push({
|
|
55
|
+
name: 'max_drawdown',
|
|
56
|
+
status: 'INFO',
|
|
57
|
+
detail: snapshot?.drawdown_max != null
|
|
58
|
+
? `Peak drawdown: ${(snapshot.drawdown_max * 100).toFixed(1)}%`
|
|
59
|
+
: 'No drawdown history yet',
|
|
60
|
+
current: snapshot?.drawdown_max ?? null,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Daily P&L
|
|
64
|
+
checks.push({
|
|
65
|
+
name: 'daily_pnl',
|
|
66
|
+
status: 'INFO',
|
|
67
|
+
detail: snapshot?.daily_pnl != null
|
|
68
|
+
? `Daily P&L: $${(snapshot.daily_pnl / 100).toFixed(2)}`
|
|
69
|
+
: 'No daily P&L data',
|
|
70
|
+
current: snapshot?.daily_pnl ?? null,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const allPassed = !circuitBreakerOn && drawdownOk && concentrationOk;
|
|
74
|
+
|
|
75
|
+
return formatToolResult({
|
|
76
|
+
overallStatus: allPassed ? 'CLEAR' : 'BLOCKED',
|
|
77
|
+
checks,
|
|
78
|
+
snapshotTimestamp: snapshot?.timestamp ?? null,
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
export const RISK_STATUS_DESCRIPTION = `
|
|
84
|
+
Check the current risk gate status including circuit breaker, drawdown, and concentration limits.
|
|
85
|
+
|
|
86
|
+
## When to Use
|
|
87
|
+
- User asks "can I trade?" or "what's the risk status?"
|
|
88
|
+
- Before recommending trades, to check if risk gates allow it
|
|
89
|
+
- Checking if circuit breaker has been triggered
|
|
90
|
+
|
|
91
|
+
## When NOT to Use
|
|
92
|
+
- For portfolio positions or P&L (use portfolio_query)
|
|
93
|
+
- For specific market risk analysis
|
|
94
|
+
`.trim();
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { DynamicStructuredTool } from '@langchain/core/tools';
|
|
2
|
+
import { z } from 'zod';
|
|
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 { formatToolResult } from '../types.js';
|
|
8
|
+
|
|
9
|
+
export const scanTool = new DynamicStructuredTool({
|
|
10
|
+
name: 'scan_markets',
|
|
11
|
+
description: 'Run a live market scan: fetches events from Kalshi, calls Octagon for model probabilities, computes edges, and stores results in the local database.',
|
|
12
|
+
schema: z.object({
|
|
13
|
+
theme: z.string().optional().describe('Theme to scan: "top50" (default), or any Kalshi category — "climate", "companies", "crypto", "economics", "elections", "entertainment", "financials", "health", "mentions", "politics", "science", "social", "sports", "transportation", "world", or a custom theme ID'),
|
|
14
|
+
}),
|
|
15
|
+
func: async ({ theme }) => {
|
|
16
|
+
const db = getDb();
|
|
17
|
+
const invoker = createOctagonInvoker();
|
|
18
|
+
const loop = new ScanLoop(db, auditTrail, invoker);
|
|
19
|
+
|
|
20
|
+
const result = await loop.runOnce({
|
|
21
|
+
theme: theme ?? 'top50',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const actionable = result.edgeSnapshots.filter(
|
|
25
|
+
(s) => s.confidence === 'high' || s.confidence === 'very_high'
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// Return a compact summary with the most interesting edges
|
|
29
|
+
const edges = result.edgeSnapshots
|
|
30
|
+
.sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge))
|
|
31
|
+
.slice(0, 30)
|
|
32
|
+
.map((s) => ({
|
|
33
|
+
ticker: s.ticker,
|
|
34
|
+
eventTicker: s.eventTicker,
|
|
35
|
+
modelProb: s.modelProb,
|
|
36
|
+
marketProb: s.marketProb,
|
|
37
|
+
edge: s.edge,
|
|
38
|
+
edgePct: `${(s.edge * 100).toFixed(1)}%`,
|
|
39
|
+
confidence: s.confidence,
|
|
40
|
+
drivers: s.drivers.slice(0, 2),
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
return formatToolResult({
|
|
44
|
+
scanId: result.scanId,
|
|
45
|
+
theme: theme ?? 'top50',
|
|
46
|
+
eventsScanned: result.eventsScanned,
|
|
47
|
+
totalEdges: result.edgeSnapshots.length,
|
|
48
|
+
actionableCount: actionable.length,
|
|
49
|
+
octagonCreditsUsed: result.octagonCreditsUsed,
|
|
50
|
+
durationMs: result.duration,
|
|
51
|
+
topEdges: edges,
|
|
52
|
+
alerts: result.alerts.map((a) => ({ alertType: a.alertType, message: a.message })),
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export const SCAN_DESCRIPTION = `
|
|
58
|
+
Run a live market scan. Fetches events from Kalshi, calls Octagon AI for model probabilities, computes pricing edges (model vs market), and stores all results in the local database.
|
|
59
|
+
|
|
60
|
+
## When to Use
|
|
61
|
+
- User says "scan", "scan crypto", "scan politics", "find edges", "run a scan"
|
|
62
|
+
- User wants to discover mispriced markets across a category
|
|
63
|
+
- User wants to populate/refresh the edge database before querying it
|
|
64
|
+
|
|
65
|
+
## When NOT to Use
|
|
66
|
+
- For querying existing edge data already in the database (use edge_query instead — it's instant)
|
|
67
|
+
- For looking up a specific market's price or details (use kalshi_search)
|
|
68
|
+
|
|
69
|
+
## Themes
|
|
70
|
+
- "top50" (default): Top 50 markets by 24h volume
|
|
71
|
+
- "crypto", "politics", "economics", "sports", "entertainment", "science", "climate": Category-based scans
|
|
72
|
+
- Custom theme ID: Pre-configured in database
|
|
73
|
+
|
|
74
|
+
## Notes
|
|
75
|
+
- Scans can take 30-120 seconds depending on the number of events
|
|
76
|
+
- Each fresh Octagon call costs 3 credits (cached calls are free)
|
|
77
|
+
- Results are stored in edge_history and can be queried later with edge_query
|
|
78
|
+
`.trim();
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
declare module '@whiskeysockets/baileys' {
|
|
3
|
+
export type AnyMessageContent = Record<string, any>;
|
|
4
|
+
export type ConnectionState = {
|
|
5
|
+
connection: 'open' | 'close' | 'connecting';
|
|
6
|
+
lastDisconnect?: { error?: { output?: { statusCode?: number } }; date: Date };
|
|
7
|
+
qr?: string;
|
|
8
|
+
isNewLogin?: boolean;
|
|
9
|
+
isOnline?: boolean;
|
|
10
|
+
receivedPendingNotifications?: boolean;
|
|
11
|
+
};
|
|
12
|
+
export type WAMessage = {
|
|
13
|
+
key: { remoteJid?: string; fromMe?: boolean; id?: string; participant?: string };
|
|
14
|
+
message?: Record<string, any> | null;
|
|
15
|
+
messageTimestamp?: number | Long;
|
|
16
|
+
pushName?: string;
|
|
17
|
+
};
|
|
18
|
+
export namespace proto {
|
|
19
|
+
interface IWebMessageInfo {
|
|
20
|
+
key?: { remoteJid?: string; fromMe?: boolean; id?: string; participant?: string };
|
|
21
|
+
message?: Record<string, any> | null;
|
|
22
|
+
messageTimestamp?: number | Long;
|
|
23
|
+
}
|
|
24
|
+
interface Message {
|
|
25
|
+
conversation?: string;
|
|
26
|
+
extendedTextMessage?: { text?: string; contextInfo?: { mentionedJid?: string[] } };
|
|
27
|
+
[key: string]: any;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export const DisconnectReason: Record<string, number>;
|
|
31
|
+
export function fetchLatestBaileysVersion(): Promise<{ version: number[]; isLatest: boolean }>;
|
|
32
|
+
export function makeCacheableSignalKeyStore(store: any, logger: any): any;
|
|
33
|
+
export function makeWASocket(config: Record<string, any>): any;
|
|
34
|
+
export function useMultiFileAuthState(folder: string): Promise<{
|
|
35
|
+
state: any;
|
|
36
|
+
saveCreds: () => Promise<void>;
|
|
37
|
+
}>;
|
|
38
|
+
export function isJidGroup(jid: string): boolean;
|
|
39
|
+
export function normalizeMessageContent(content: any): Record<string, any> | undefined;
|
|
40
|
+
export function extractMessageContent(content: any): Record<string, any> | undefined;
|
|
41
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { DisplayEvent, TokenUsage } from './agent/types.js';
|
|
2
|
+
|
|
3
|
+
export type WorkingState =
|
|
4
|
+
| { status: 'idle' }
|
|
5
|
+
| { status: 'thinking' }
|
|
6
|
+
| { status: 'tool'; toolName: string }
|
|
7
|
+
| { status: 'approval'; toolName: string };
|
|
8
|
+
|
|
9
|
+
export type HistoryItemStatus = 'processing' | 'complete' | 'error' | 'interrupted';
|
|
10
|
+
|
|
11
|
+
export interface HistoryItem {
|
|
12
|
+
id: string;
|
|
13
|
+
query: string;
|
|
14
|
+
events: DisplayEvent[];
|
|
15
|
+
answer: string;
|
|
16
|
+
status: HistoryItemStatus;
|
|
17
|
+
activeToolId?: string;
|
|
18
|
+
startTime?: number;
|
|
19
|
+
duration?: number;
|
|
20
|
+
tokenUsage?: TokenUsage;
|
|
21
|
+
tokensPerSecond?: number;
|
|
22
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { AIMessage } from '@langchain/core/messages';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extract text content from an AIMessage
|
|
5
|
+
*/
|
|
6
|
+
export function extractTextContent(message: AIMessage): string {
|
|
7
|
+
if (typeof message.content === 'string') {
|
|
8
|
+
return message.content;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (Array.isArray(message.content)) {
|
|
12
|
+
return message.content
|
|
13
|
+
.filter(block => typeof block === 'object' && 'type' in block && block.type === 'text')
|
|
14
|
+
.map(block => (block as { text: string }).text)
|
|
15
|
+
.join('\n');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return '';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if an AIMessage has tool calls
|
|
23
|
+
*/
|
|
24
|
+
export function hasToolCalls(message: AIMessage): boolean {
|
|
25
|
+
return Array.isArray(message.tool_calls) && message.tool_calls.length > 0;
|
|
26
|
+
}
|