@spfunctions/cli 1.1.5 → 1.1.6
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/dist/client.d.ts +4 -0
- package/dist/client.js +14 -0
- package/dist/commands/agent.d.ts +1 -0
- package/dist/commands/agent.js +891 -14
- package/dist/commands/announcements.d.ts +3 -0
- package/dist/commands/announcements.js +28 -0
- package/dist/commands/balance.d.ts +3 -0
- package/dist/commands/balance.js +17 -0
- package/dist/commands/cancel.d.ts +5 -0
- package/dist/commands/cancel.js +41 -0
- package/dist/commands/dashboard.d.ts +11 -0
- package/dist/commands/dashboard.js +195 -0
- package/dist/commands/fills.d.ts +4 -0
- package/dist/commands/fills.js +29 -0
- package/dist/commands/forecast.d.ts +4 -0
- package/dist/commands/forecast.js +53 -0
- package/dist/commands/history.d.ts +3 -0
- package/dist/commands/history.js +38 -0
- package/dist/commands/milestones.d.ts +8 -0
- package/dist/commands/milestones.js +56 -0
- package/dist/commands/orders.d.ts +4 -0
- package/dist/commands/orders.js +28 -0
- package/dist/commands/publish.js +21 -2
- package/dist/commands/rfq.d.ts +5 -0
- package/dist/commands/rfq.js +35 -0
- package/dist/commands/schedule.d.ts +3 -0
- package/dist/commands/schedule.js +38 -0
- package/dist/commands/settlements.d.ts +6 -0
- package/dist/commands/settlements.js +50 -0
- package/dist/commands/setup.d.ts +2 -0
- package/dist/commands/setup.js +45 -3
- package/dist/commands/signal.js +12 -1
- package/dist/commands/strategies.d.ts +11 -0
- package/dist/commands/strategies.js +130 -0
- package/dist/commands/trade.d.ts +12 -0
- package/dist/commands/trade.js +78 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +13 -0
- package/dist/index.js +154 -3
- package/dist/kalshi.d.ts +71 -0
- package/dist/kalshi.js +257 -17
- package/package.json +1 -1
package/dist/commands/agent.js
CHANGED
|
@@ -23,6 +23,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
23
23
|
const os_1 = __importDefault(require("os"));
|
|
24
24
|
const client_js_1 = require("../client.js");
|
|
25
25
|
const kalshi_js_1 = require("../kalshi.js");
|
|
26
|
+
const config_js_1 = require("../config.js");
|
|
26
27
|
// ─── Session persistence ─────────────────────────────────────────────────────
|
|
27
28
|
function getSessionDir() {
|
|
28
29
|
return path_1.default.join(os_1.default.homedir(), '.sf', 'sessions');
|
|
@@ -286,23 +287,36 @@ function renderPositions(positions) {
|
|
|
286
287
|
}
|
|
287
288
|
// ─── Main command ────────────────────────────────────────────────────────────
|
|
288
289
|
async function agentCommand(thesisId, opts) {
|
|
289
|
-
// ── Dynamic imports (all ESM-only packages) ────────────────────────────────
|
|
290
|
-
const piTui = await import('@mariozechner/pi-tui');
|
|
291
|
-
const piAi = await import('@mariozechner/pi-ai');
|
|
292
|
-
const piAgent = await import('@mariozechner/pi-agent-core');
|
|
293
|
-
const { TUI, ProcessTerminal, Container, Text, Markdown, Editor, Loader, Spacer, CombinedAutocompleteProvider, truncateToWidth, visibleWidth, } = piTui;
|
|
294
|
-
const { getModel, streamSimple, Type } = piAi;
|
|
295
|
-
const { Agent } = piAgent;
|
|
296
|
-
// ── Component class factories (need piTui ref) ─────────────────────────────
|
|
297
|
-
const MutableLine = createMutableLine(piTui);
|
|
298
|
-
const HeaderBar = createHeaderBar(piTui);
|
|
299
|
-
const FooterBar = createFooterBar(piTui);
|
|
300
290
|
// ── Validate API keys ──────────────────────────────────────────────────────
|
|
301
291
|
const openrouterKey = opts?.modelKey || process.env.OPENROUTER_API_KEY;
|
|
302
292
|
if (!openrouterKey) {
|
|
303
|
-
console.error('Need OpenRouter API key
|
|
293
|
+
console.error('Need OpenRouter API key to power the agent LLM.');
|
|
294
|
+
console.error('');
|
|
295
|
+
console.error(' 1. Get a key at https://openrouter.ai/keys');
|
|
296
|
+
console.error(' 2. Then either:');
|
|
297
|
+
console.error(' export OPENROUTER_API_KEY=sk-or-v1-...');
|
|
298
|
+
console.error(' sf agent --model-key sk-or-v1-...');
|
|
299
|
+
console.error(' sf setup (saves to ~/.sf/config.json)');
|
|
304
300
|
process.exit(1);
|
|
305
301
|
}
|
|
302
|
+
// Pre-flight: validate OpenRouter key
|
|
303
|
+
try {
|
|
304
|
+
const checkRes = await fetch('https://openrouter.ai/api/v1/auth/key', {
|
|
305
|
+
headers: { 'Authorization': `Bearer ${openrouterKey}` },
|
|
306
|
+
signal: AbortSignal.timeout(8000),
|
|
307
|
+
});
|
|
308
|
+
if (!checkRes.ok) {
|
|
309
|
+
console.error('OpenRouter API key is invalid or expired.');
|
|
310
|
+
console.error('Get a new key at https://openrouter.ai/keys');
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
316
|
+
if (!msg.includes('timeout')) {
|
|
317
|
+
console.warn(`Warning: Could not verify OpenRouter key (${msg}). Continuing anyway.`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
306
320
|
const sfClient = new client_js_1.SFClient();
|
|
307
321
|
// ── Resolve thesis ID ──────────────────────────────────────────────────────
|
|
308
322
|
let resolvedThesisId = thesisId;
|
|
@@ -318,6 +332,21 @@ async function agentCommand(thesisId, opts) {
|
|
|
318
332
|
}
|
|
319
333
|
// ── Fetch initial context ──────────────────────────────────────────────────
|
|
320
334
|
let latestContext = await sfClient.getContext(resolvedThesisId);
|
|
335
|
+
// ── Branch: plain-text mode ────────────────────────────────────────────────
|
|
336
|
+
if (opts?.noTui) {
|
|
337
|
+
return runPlainTextAgent({ openrouterKey, sfClient, resolvedThesisId: resolvedThesisId, latestContext, opts });
|
|
338
|
+
}
|
|
339
|
+
// ── Dynamic imports (all ESM-only packages) ────────────────────────────────
|
|
340
|
+
const piTui = await import('@mariozechner/pi-tui');
|
|
341
|
+
const piAi = await import('@mariozechner/pi-ai');
|
|
342
|
+
const piAgent = await import('@mariozechner/pi-agent-core');
|
|
343
|
+
const { TUI, ProcessTerminal, Container, Text, Markdown, Editor, Loader, Spacer, CombinedAutocompleteProvider, truncateToWidth, visibleWidth, } = piTui;
|
|
344
|
+
const { getModel, streamSimple, Type } = piAi;
|
|
345
|
+
const { Agent } = piAgent;
|
|
346
|
+
// ── Component class factories (need piTui ref) ─────────────────────────────
|
|
347
|
+
const MutableLine = createMutableLine(piTui);
|
|
348
|
+
const HeaderBar = createHeaderBar(piTui);
|
|
349
|
+
const FooterBar = createFooterBar(piTui);
|
|
321
350
|
// ── Model setup ────────────────────────────────────────────────────────────
|
|
322
351
|
const rawModelName = opts?.model || 'anthropic/claude-sonnet-4.6';
|
|
323
352
|
let currentModelName = rawModelName.replace(/^openrouter\//, '');
|
|
@@ -349,6 +378,10 @@ async function agentCommand(thesisId, opts) {
|
|
|
349
378
|
let isProcessing = false;
|
|
350
379
|
// Cache for positions (fetched by /pos or get_positions tool)
|
|
351
380
|
let cachedPositions = null;
|
|
381
|
+
// ── Inline confirmation mechanism ─────────────────────────────────────────
|
|
382
|
+
// Tools can call promptUser() during execution to ask the user a question.
|
|
383
|
+
// This temporarily unlocks the editor, waits for input, then resumes.
|
|
384
|
+
let pendingPrompt = null;
|
|
352
385
|
// ── Setup TUI ──────────────────────────────────────────────────────────────
|
|
353
386
|
const terminal = new ProcessTerminal();
|
|
354
387
|
const tui = new TUI(terminal);
|
|
@@ -395,7 +428,7 @@ async function agentCommand(thesisId, opts) {
|
|
|
395
428
|
const chatContainer = new Container();
|
|
396
429
|
const editor = new Editor(tui, editorTheme, { paddingX: 1 });
|
|
397
430
|
// Slash command autocomplete
|
|
398
|
-
const
|
|
431
|
+
const slashCommands = [
|
|
399
432
|
{ name: 'help', description: 'Show available commands' },
|
|
400
433
|
{ name: 'tree', description: 'Display causal tree' },
|
|
401
434
|
{ name: 'edges', description: 'Display edge/spread table' },
|
|
@@ -408,7 +441,13 @@ async function agentCommand(thesisId, opts) {
|
|
|
408
441
|
{ name: 'env', description: 'Show environment variable status' },
|
|
409
442
|
{ name: 'clear', description: 'Clear screen (keeps history)' },
|
|
410
443
|
{ name: 'exit', description: 'Exit agent (auto-saves)' },
|
|
411
|
-
]
|
|
444
|
+
];
|
|
445
|
+
// Add trading commands if enabled
|
|
446
|
+
if ((0, config_js_1.loadConfig)().tradingEnabled) {
|
|
447
|
+
slashCommands.splice(-2, 0, // insert before /clear and /exit
|
|
448
|
+
{ name: 'buy', description: 'TICKER QTY PRICE — quick buy' }, { name: 'sell', description: 'TICKER QTY PRICE — quick sell' }, { name: 'cancel', description: 'ORDER_ID — cancel order' });
|
|
449
|
+
}
|
|
450
|
+
const autocompleteProvider = new CombinedAutocompleteProvider(slashCommands, process.cwd());
|
|
412
451
|
editor.setAutocompleteProvider(autocompleteProvider);
|
|
413
452
|
// Assemble TUI tree
|
|
414
453
|
tui.addChild(topSpacer);
|
|
@@ -438,6 +477,19 @@ async function agentCommand(thesisId, opts) {
|
|
|
438
477
|
function addSpacer() {
|
|
439
478
|
chatContainer.addChild(new Spacer(1));
|
|
440
479
|
}
|
|
480
|
+
/**
|
|
481
|
+
* Ask the user a question during tool execution.
|
|
482
|
+
* Temporarily unlocks the editor, waits for input, then resumes.
|
|
483
|
+
* Used for order confirmations and other dangerous operations.
|
|
484
|
+
*/
|
|
485
|
+
function promptUser(question) {
|
|
486
|
+
return new Promise(resolve => {
|
|
487
|
+
addSystemText(C.amber(bold('\u26A0 ')) + C.zinc200(question));
|
|
488
|
+
addSpacer();
|
|
489
|
+
tui.requestRender();
|
|
490
|
+
pendingPrompt = { resolve };
|
|
491
|
+
});
|
|
492
|
+
}
|
|
441
493
|
// ── Define agent tools (same as before) ────────────────────────────────────
|
|
442
494
|
const thesisIdParam = Type.Object({
|
|
443
495
|
thesisId: Type.String({ description: 'Thesis ID (short or full UUID)' }),
|
|
@@ -636,7 +688,303 @@ async function agentCommand(thesisId, opts) {
|
|
|
636
688
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
637
689
|
},
|
|
638
690
|
},
|
|
691
|
+
{
|
|
692
|
+
name: 'create_strategy',
|
|
693
|
+
label: 'Create Strategy',
|
|
694
|
+
description: 'Create a trading strategy for a thesis. Extract hard conditions (entryBelow/stopLoss/takeProfit as cents) and soft conditions from conversation. Called when user mentions specific trade ideas.',
|
|
695
|
+
parameters: Type.Object({
|
|
696
|
+
thesisId: Type.String({ description: 'Thesis ID' }),
|
|
697
|
+
marketId: Type.String({ description: 'Market ticker e.g. KXWTIMAX-26DEC31-T150' }),
|
|
698
|
+
market: Type.String({ description: 'Human-readable market name' }),
|
|
699
|
+
direction: Type.String({ description: 'yes or no' }),
|
|
700
|
+
horizon: Type.Optional(Type.String({ description: 'short, medium, or long. Default: medium' })),
|
|
701
|
+
entryBelow: Type.Optional(Type.Number({ description: 'Entry trigger: ask <= this value (cents)' })),
|
|
702
|
+
entryAbove: Type.Optional(Type.Number({ description: 'Entry trigger: ask >= this value (cents, for NO direction)' })),
|
|
703
|
+
stopLoss: Type.Optional(Type.Number({ description: 'Stop loss: bid <= this value (cents)' })),
|
|
704
|
+
takeProfit: Type.Optional(Type.Number({ description: 'Take profit: bid >= this value (cents)' })),
|
|
705
|
+
maxQuantity: Type.Optional(Type.Number({ description: 'Max total contracts. Default: 500' })),
|
|
706
|
+
perOrderQuantity: Type.Optional(Type.Number({ description: 'Contracts per order. Default: 50' })),
|
|
707
|
+
softConditions: Type.Optional(Type.String({ description: 'LLM-evaluated conditions e.g. "only enter when n3 > 60%"' })),
|
|
708
|
+
rationale: Type.Optional(Type.String({ description: 'Full logic description' })),
|
|
709
|
+
}),
|
|
710
|
+
execute: async (_toolCallId, params) => {
|
|
711
|
+
const result = await sfClient.createStrategyAPI(params.thesisId, {
|
|
712
|
+
marketId: params.marketId,
|
|
713
|
+
market: params.market,
|
|
714
|
+
direction: params.direction,
|
|
715
|
+
horizon: params.horizon,
|
|
716
|
+
entryBelow: params.entryBelow,
|
|
717
|
+
entryAbove: params.entryAbove,
|
|
718
|
+
stopLoss: params.stopLoss,
|
|
719
|
+
takeProfit: params.takeProfit,
|
|
720
|
+
maxQuantity: params.maxQuantity,
|
|
721
|
+
perOrderQuantity: params.perOrderQuantity,
|
|
722
|
+
softConditions: params.softConditions,
|
|
723
|
+
rationale: params.rationale,
|
|
724
|
+
createdBy: 'agent',
|
|
725
|
+
});
|
|
726
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }], details: {} };
|
|
727
|
+
},
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
name: 'list_strategies',
|
|
731
|
+
label: 'List Strategies',
|
|
732
|
+
description: 'List strategies for a thesis. Filter by status (active/watching/executed/cancelled/review) or omit for all.',
|
|
733
|
+
parameters: Type.Object({
|
|
734
|
+
thesisId: Type.String({ description: 'Thesis ID' }),
|
|
735
|
+
status: Type.Optional(Type.String({ description: 'Filter by status. Omit for all.' })),
|
|
736
|
+
}),
|
|
737
|
+
execute: async (_toolCallId, params) => {
|
|
738
|
+
const result = await sfClient.getStrategies(params.thesisId, params.status);
|
|
739
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], details: {} };
|
|
740
|
+
},
|
|
741
|
+
},
|
|
742
|
+
{
|
|
743
|
+
name: 'update_strategy',
|
|
744
|
+
label: 'Update Strategy',
|
|
745
|
+
description: 'Update a strategy (change stop loss, take profit, status, priority, etc.)',
|
|
746
|
+
parameters: Type.Object({
|
|
747
|
+
thesisId: Type.String({ description: 'Thesis ID' }),
|
|
748
|
+
strategyId: Type.String({ description: 'Strategy ID (UUID)' }),
|
|
749
|
+
stopLoss: Type.Optional(Type.Number({ description: 'New stop loss (cents)' })),
|
|
750
|
+
takeProfit: Type.Optional(Type.Number({ description: 'New take profit (cents)' })),
|
|
751
|
+
entryBelow: Type.Optional(Type.Number({ description: 'New entry below trigger (cents)' })),
|
|
752
|
+
entryAbove: Type.Optional(Type.Number({ description: 'New entry above trigger (cents)' })),
|
|
753
|
+
status: Type.Optional(Type.String({ description: 'New status: active|watching|executed|cancelled|review' })),
|
|
754
|
+
priority: Type.Optional(Type.Number({ description: 'New priority' })),
|
|
755
|
+
softConditions: Type.Optional(Type.String({ description: 'Updated soft conditions' })),
|
|
756
|
+
rationale: Type.Optional(Type.String({ description: 'Updated rationale' })),
|
|
757
|
+
}),
|
|
758
|
+
execute: async (_toolCallId, params) => {
|
|
759
|
+
const { thesisId, strategyId, ...updates } = params;
|
|
760
|
+
const result = await sfClient.updateStrategyAPI(thesisId, strategyId, updates);
|
|
761
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }], details: {} };
|
|
762
|
+
},
|
|
763
|
+
},
|
|
764
|
+
{
|
|
765
|
+
name: 'get_milestones',
|
|
766
|
+
label: 'Milestones',
|
|
767
|
+
description: 'Get upcoming events from Kalshi calendar. Use to check economic releases, political events, or other catalysts coming up that might affect the thesis.',
|
|
768
|
+
parameters: Type.Object({
|
|
769
|
+
hours: Type.Optional(Type.Number({ description: 'Hours ahead to look (default 168 = 1 week)' })),
|
|
770
|
+
category: Type.Optional(Type.String({ description: 'Filter by category (e.g. Economics, Politics, Sports)' })),
|
|
771
|
+
}),
|
|
772
|
+
execute: async (_toolCallId, params) => {
|
|
773
|
+
const hours = params.hours || 168;
|
|
774
|
+
const now = new Date();
|
|
775
|
+
const url = `https://api.elections.kalshi.com/trade-api/v2/milestones?limit=200&minimum_start_date=${now.toISOString()}` +
|
|
776
|
+
(params.category ? `&category=${params.category}` : '');
|
|
777
|
+
const res = await fetch(url, { headers: { 'Accept': 'application/json' } });
|
|
778
|
+
if (!res.ok)
|
|
779
|
+
return { content: [{ type: 'text', text: `Milestones API error: ${res.status}` }], details: {} };
|
|
780
|
+
const data = await res.json();
|
|
781
|
+
const cutoff = now.getTime() + hours * 3600000;
|
|
782
|
+
const filtered = (data.milestones || [])
|
|
783
|
+
.filter((m) => new Date(m.start_date).getTime() <= cutoff)
|
|
784
|
+
.slice(0, 30)
|
|
785
|
+
.map((m) => ({
|
|
786
|
+
title: m.title,
|
|
787
|
+
category: m.category,
|
|
788
|
+
start_date: m.start_date,
|
|
789
|
+
related_event_tickers: m.related_event_tickers,
|
|
790
|
+
hours_until: Math.round((new Date(m.start_date).getTime() - now.getTime()) / 3600000),
|
|
791
|
+
}));
|
|
792
|
+
return { content: [{ type: 'text', text: JSON.stringify(filtered, null, 2) }], details: {} };
|
|
793
|
+
},
|
|
794
|
+
},
|
|
795
|
+
{
|
|
796
|
+
name: 'get_forecast',
|
|
797
|
+
label: 'Forecast',
|
|
798
|
+
description: 'Get market distribution (P50/P75/P90 percentile history) for a Kalshi event. Shows how market consensus has shifted over time.',
|
|
799
|
+
parameters: Type.Object({
|
|
800
|
+
eventTicker: Type.String({ description: 'Kalshi event ticker (e.g. KXWTIMAX-26DEC31)' }),
|
|
801
|
+
days: Type.Optional(Type.Number({ description: 'Days of history (default 7)' })),
|
|
802
|
+
}),
|
|
803
|
+
execute: async (_toolCallId, params) => {
|
|
804
|
+
const { getForecastHistory } = await import('../kalshi.js');
|
|
805
|
+
const days = params.days || 7;
|
|
806
|
+
// Get series ticker from event
|
|
807
|
+
const evtRes = await fetch(`https://api.elections.kalshi.com/trade-api/v2/events/${params.eventTicker}`, { headers: { 'Accept': 'application/json' } });
|
|
808
|
+
if (!evtRes.ok)
|
|
809
|
+
return { content: [{ type: 'text', text: `Event not found: ${params.eventTicker}` }], details: {} };
|
|
810
|
+
const evtData = await evtRes.json();
|
|
811
|
+
const seriesTicker = evtData.event?.series_ticker;
|
|
812
|
+
if (!seriesTicker)
|
|
813
|
+
return { content: [{ type: 'text', text: `No series_ticker for ${params.eventTicker}` }], details: {} };
|
|
814
|
+
const history = await getForecastHistory({
|
|
815
|
+
seriesTicker,
|
|
816
|
+
eventTicker: params.eventTicker,
|
|
817
|
+
percentiles: [5000, 7500, 9000],
|
|
818
|
+
startTs: Math.floor((Date.now() - days * 86400000) / 1000),
|
|
819
|
+
endTs: Math.floor(Date.now() / 1000),
|
|
820
|
+
periodInterval: 1440,
|
|
821
|
+
});
|
|
822
|
+
if (!history || history.length === 0)
|
|
823
|
+
return { content: [{ type: 'text', text: 'No forecast data available' }], details: {} };
|
|
824
|
+
return { content: [{ type: 'text', text: JSON.stringify(history, null, 2) }], details: {} };
|
|
825
|
+
},
|
|
826
|
+
},
|
|
827
|
+
{
|
|
828
|
+
name: 'get_settlements',
|
|
829
|
+
label: 'Settlements',
|
|
830
|
+
description: 'Get settled (resolved) contracts with P&L. Shows which contracts won/lost and realized returns.',
|
|
831
|
+
parameters: Type.Object({
|
|
832
|
+
ticker: Type.Optional(Type.String({ description: 'Filter by market ticker' })),
|
|
833
|
+
}),
|
|
834
|
+
execute: async (_toolCallId, params) => {
|
|
835
|
+
const { getSettlements } = await import('../kalshi.js');
|
|
836
|
+
const result = await getSettlements({ limit: 100, ticker: params.ticker });
|
|
837
|
+
if (!result)
|
|
838
|
+
return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
|
|
839
|
+
return { content: [{ type: 'text', text: JSON.stringify(result.settlements, null, 2) }], details: {} };
|
|
840
|
+
},
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
name: 'get_balance',
|
|
844
|
+
label: 'Balance',
|
|
845
|
+
description: 'Get Kalshi account balance and portfolio value.',
|
|
846
|
+
parameters: emptyParams,
|
|
847
|
+
execute: async () => {
|
|
848
|
+
const { getBalance } = await import('../kalshi.js');
|
|
849
|
+
const result = await getBalance();
|
|
850
|
+
if (!result)
|
|
851
|
+
return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
|
|
852
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], details: {} };
|
|
853
|
+
},
|
|
854
|
+
},
|
|
855
|
+
{
|
|
856
|
+
name: 'get_orders',
|
|
857
|
+
label: 'Orders',
|
|
858
|
+
description: 'Get current resting orders on Kalshi.',
|
|
859
|
+
parameters: Type.Object({
|
|
860
|
+
status: Type.Optional(Type.String({ description: 'Filter by status: resting, canceled, executed. Default: resting' })),
|
|
861
|
+
}),
|
|
862
|
+
execute: async (_toolCallId, params) => {
|
|
863
|
+
const { getOrders } = await import('../kalshi.js');
|
|
864
|
+
const result = await getOrders({ status: params.status || 'resting', limit: 100 });
|
|
865
|
+
if (!result)
|
|
866
|
+
return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
|
|
867
|
+
return { content: [{ type: 'text', text: JSON.stringify(result.orders, null, 2) }], details: {} };
|
|
868
|
+
},
|
|
869
|
+
},
|
|
870
|
+
{
|
|
871
|
+
name: 'get_fills',
|
|
872
|
+
label: 'Fills',
|
|
873
|
+
description: 'Get recent trade fills (executed trades) on Kalshi.',
|
|
874
|
+
parameters: Type.Object({
|
|
875
|
+
ticker: Type.Optional(Type.String({ description: 'Filter by market ticker' })),
|
|
876
|
+
}),
|
|
877
|
+
execute: async (_toolCallId, params) => {
|
|
878
|
+
const { getFills } = await import('../kalshi.js');
|
|
879
|
+
const result = await getFills({ ticker: params.ticker, limit: 50 });
|
|
880
|
+
if (!result)
|
|
881
|
+
return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
|
|
882
|
+
return { content: [{ type: 'text', text: JSON.stringify(result.fills, null, 2) }], details: {} };
|
|
883
|
+
},
|
|
884
|
+
},
|
|
885
|
+
{
|
|
886
|
+
name: 'get_schedule',
|
|
887
|
+
label: 'Schedule',
|
|
888
|
+
description: 'Get exchange status (open/closed) and trading hours. Use to check if low liquidity is due to off-hours.',
|
|
889
|
+
parameters: emptyParams,
|
|
890
|
+
execute: async () => {
|
|
891
|
+
try {
|
|
892
|
+
const res = await fetch('https://api.elections.kalshi.com/trade-api/v2/exchange/status', { headers: { 'Accept': 'application/json' } });
|
|
893
|
+
if (!res.ok)
|
|
894
|
+
return { content: [{ type: 'text', text: `Exchange API error: ${res.status}` }], details: {} };
|
|
895
|
+
const data = await res.json();
|
|
896
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
897
|
+
}
|
|
898
|
+
catch (err) {
|
|
899
|
+
return { content: [{ type: 'text', text: `Failed: ${err.message}` }], details: {} };
|
|
900
|
+
}
|
|
901
|
+
},
|
|
902
|
+
},
|
|
639
903
|
];
|
|
904
|
+
// ── Trading tools (conditional on tradingEnabled) ──────────────────────────
|
|
905
|
+
const config = (0, config_js_1.loadConfig)();
|
|
906
|
+
if (config.tradingEnabled) {
|
|
907
|
+
tools.push({
|
|
908
|
+
name: 'place_order',
|
|
909
|
+
label: 'Place Order',
|
|
910
|
+
description: 'Place a buy or sell order on Kalshi. Shows a preview and asks for user confirmation before executing. Use for limit or market orders.',
|
|
911
|
+
parameters: Type.Object({
|
|
912
|
+
ticker: Type.String({ description: 'Market ticker e.g. KXWTIMAX-26DEC31-T135' }),
|
|
913
|
+
side: Type.String({ description: 'yes or no' }),
|
|
914
|
+
action: Type.String({ description: 'buy or sell' }),
|
|
915
|
+
type: Type.String({ description: 'limit or market' }),
|
|
916
|
+
count: Type.Number({ description: 'Number of contracts' }),
|
|
917
|
+
price_cents: Type.Optional(Type.Number({ description: 'Limit price in cents (1-99). Required for limit orders.' })),
|
|
918
|
+
}),
|
|
919
|
+
execute: async (_toolCallId, params) => {
|
|
920
|
+
const { createOrder } = await import('../kalshi.js');
|
|
921
|
+
const priceDollars = params.price_cents ? (params.price_cents / 100).toFixed(2) : undefined;
|
|
922
|
+
const maxCost = ((params.price_cents || 99) * params.count / 100).toFixed(2);
|
|
923
|
+
// Show preview
|
|
924
|
+
const preview = [
|
|
925
|
+
C.zinc200(bold('ORDER PREVIEW')),
|
|
926
|
+
` Ticker: ${params.ticker}`,
|
|
927
|
+
` Side: ${params.side === 'yes' ? C.emerald('YES') : C.red('NO')}`,
|
|
928
|
+
` Action: ${params.action.toUpperCase()}`,
|
|
929
|
+
` Quantity: ${params.count}`,
|
|
930
|
+
` Type: ${params.type}`,
|
|
931
|
+
params.price_cents ? ` Price: ${params.price_cents}\u00A2` : '',
|
|
932
|
+
` Max cost: $${maxCost}`,
|
|
933
|
+
].filter(Boolean).join('\n');
|
|
934
|
+
addSystemText(preview);
|
|
935
|
+
addSpacer();
|
|
936
|
+
tui.requestRender();
|
|
937
|
+
// Ask for confirmation via promptUser
|
|
938
|
+
const answer = await promptUser('Execute this order? (y/n)');
|
|
939
|
+
if (!answer.toLowerCase().startsWith('y')) {
|
|
940
|
+
return { content: [{ type: 'text', text: 'Order cancelled by user.' }], details: {} };
|
|
941
|
+
}
|
|
942
|
+
try {
|
|
943
|
+
const result = await createOrder({
|
|
944
|
+
ticker: params.ticker,
|
|
945
|
+
side: params.side,
|
|
946
|
+
action: params.action,
|
|
947
|
+
type: params.type,
|
|
948
|
+
count: params.count,
|
|
949
|
+
...(priceDollars ? { yes_price: priceDollars } : {}),
|
|
950
|
+
});
|
|
951
|
+
const order = result.order || result;
|
|
952
|
+
return {
|
|
953
|
+
content: [{ type: 'text', text: `Order placed: ${order.order_id || 'OK'}\nStatus: ${order.status || '-'}\nFilled: ${order.fill_count_fp || 0}/${order.initial_count_fp || params.count}` }],
|
|
954
|
+
details: {},
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
catch (err) {
|
|
958
|
+
const msg = err.message || String(err);
|
|
959
|
+
if (msg.includes('403')) {
|
|
960
|
+
return { content: [{ type: 'text', text: '403 Forbidden \u2014 your Kalshi key lacks write permission. Get a read+write key at kalshi.com/account/api-keys' }], details: {} };
|
|
961
|
+
}
|
|
962
|
+
return { content: [{ type: 'text', text: `Order failed: ${msg}` }], details: {} };
|
|
963
|
+
}
|
|
964
|
+
},
|
|
965
|
+
}, {
|
|
966
|
+
name: 'cancel_order',
|
|
967
|
+
label: 'Cancel Order',
|
|
968
|
+
description: 'Cancel a resting order by order ID.',
|
|
969
|
+
parameters: Type.Object({
|
|
970
|
+
order_id: Type.String({ description: 'Order ID to cancel' }),
|
|
971
|
+
}),
|
|
972
|
+
execute: async (_toolCallId, params) => {
|
|
973
|
+
const { cancelOrder } = await import('../kalshi.js');
|
|
974
|
+
const answer = await promptUser(`Cancel order ${params.order_id}? (y/n)`);
|
|
975
|
+
if (!answer.toLowerCase().startsWith('y')) {
|
|
976
|
+
return { content: [{ type: 'text', text: 'Cancel aborted by user.' }], details: {} };
|
|
977
|
+
}
|
|
978
|
+
try {
|
|
979
|
+
await cancelOrder(params.order_id);
|
|
980
|
+
return { content: [{ type: 'text', text: `Order ${params.order_id} cancelled.` }], details: {} };
|
|
981
|
+
}
|
|
982
|
+
catch (err) {
|
|
983
|
+
return { content: [{ type: 'text', text: `Cancel failed: ${err.message}` }], details: {} };
|
|
984
|
+
}
|
|
985
|
+
},
|
|
986
|
+
});
|
|
987
|
+
}
|
|
640
988
|
// ── System prompt builder ──────────────────────────────────────────────────
|
|
641
989
|
function buildSystemPrompt(ctx) {
|
|
642
990
|
const edgesSummary = ctx.edges
|
|
@@ -677,6 +1025,15 @@ Short-term markets (weekly/monthly contracts) settle into hard data that calibra
|
|
|
677
1025
|
- For any question about prices, positions, or P&L, ALWAYS call a tool to get fresh data first. Never answer price-related questions using the cached data in this system prompt.
|
|
678
1026
|
- Align tables. Be precise with numbers to the cent.
|
|
679
1027
|
|
|
1028
|
+
## Strategy rules
|
|
1029
|
+
|
|
1030
|
+
When the conversation produces a concrete trade idea (specific contract, direction, price conditions), use create_strategy to record it immediately. Don't wait for the user to say "record this."
|
|
1031
|
+
- Extract hard conditions (specific prices in cents) into entryBelow/stopLoss/takeProfit.
|
|
1032
|
+
- Put fuzzy conditions into softConditions (e.g. "only if n3 > 60%", "spread < 3¢").
|
|
1033
|
+
- Put the full reasoning into rationale.
|
|
1034
|
+
- After creating, confirm the strategy details and mention that sf runtime --dangerous can execute it.
|
|
1035
|
+
- If the user says "change the stop loss on T150 to 30", use update_strategy.
|
|
1036
|
+
|
|
680
1037
|
## Current thesis state
|
|
681
1038
|
|
|
682
1039
|
Thesis: ${ctx.thesis || ctx.rawThesis || 'N/A'}
|
|
@@ -853,6 +1210,11 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
|
|
|
853
1210
|
C.emerald('/new ') + C.zinc400('Start fresh session') + '\n' +
|
|
854
1211
|
C.emerald('/model <m> ') + C.zinc400('Switch model') + '\n' +
|
|
855
1212
|
C.emerald('/env ') + C.zinc400('Show environment variable status') + '\n' +
|
|
1213
|
+
(config.tradingEnabled ? (C.zinc600('\u2500'.repeat(30)) + '\n' +
|
|
1214
|
+
C.emerald('/buy ') + C.zinc400('TICKER QTY PRICE \u2014 quick buy') + '\n' +
|
|
1215
|
+
C.emerald('/sell ') + C.zinc400('TICKER QTY PRICE \u2014 quick sell') + '\n' +
|
|
1216
|
+
C.emerald('/cancel ') + C.zinc400('ORDER_ID \u2014 cancel order') + '\n' +
|
|
1217
|
+
C.zinc600('\u2500'.repeat(30)) + '\n') : '') +
|
|
856
1218
|
C.emerald('/clear ') + C.zinc400('Clear screen (keeps history)') + '\n' +
|
|
857
1219
|
C.emerald('/exit ') + C.zinc400('Exit (auto-saves)'));
|
|
858
1220
|
addSpacer();
|
|
@@ -1195,6 +1557,93 @@ Output a structured summary. Be concise but preserve every important detail —
|
|
|
1195
1557
|
tui.requestRender();
|
|
1196
1558
|
return true;
|
|
1197
1559
|
}
|
|
1560
|
+
case '/buy': {
|
|
1561
|
+
// /buy TICKER QTY PRICE — quick trade without LLM
|
|
1562
|
+
const [, ticker, qtyStr, priceStr] = parts;
|
|
1563
|
+
if (!ticker || !qtyStr || !priceStr) {
|
|
1564
|
+
addSystemText(C.zinc400('Usage: /buy TICKER QTY PRICE_CENTS (e.g. /buy KXWTIMAX-26DEC31-T135 100 50)'));
|
|
1565
|
+
return true;
|
|
1566
|
+
}
|
|
1567
|
+
if (!config.tradingEnabled) {
|
|
1568
|
+
addSystemText(C.red('Trading disabled. Run: sf setup --enable-trading'));
|
|
1569
|
+
return true;
|
|
1570
|
+
}
|
|
1571
|
+
addSpacer();
|
|
1572
|
+
const answer = await promptUser(`BUY ${qtyStr}x ${ticker} YES @ ${priceStr}\u00A2 — execute? (y/n)`);
|
|
1573
|
+
if (answer.toLowerCase().startsWith('y')) {
|
|
1574
|
+
try {
|
|
1575
|
+
const { createOrder } = await import('../kalshi.js');
|
|
1576
|
+
const result = await createOrder({
|
|
1577
|
+
ticker, side: 'yes', action: 'buy', type: 'limit',
|
|
1578
|
+
count: parseInt(qtyStr),
|
|
1579
|
+
yes_price: (parseInt(priceStr) / 100).toFixed(2),
|
|
1580
|
+
});
|
|
1581
|
+
addSystemText(C.emerald('\u2713 Order placed: ' + ((result.order || result).order_id || 'OK')));
|
|
1582
|
+
}
|
|
1583
|
+
catch (err) {
|
|
1584
|
+
addSystemText(C.red('\u2717 ' + err.message));
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
else {
|
|
1588
|
+
addSystemText(C.zinc400('Cancelled.'));
|
|
1589
|
+
}
|
|
1590
|
+
addSpacer();
|
|
1591
|
+
return true;
|
|
1592
|
+
}
|
|
1593
|
+
case '/sell': {
|
|
1594
|
+
const [, ticker, qtyStr, priceStr] = parts;
|
|
1595
|
+
if (!ticker || !qtyStr || !priceStr) {
|
|
1596
|
+
addSystemText(C.zinc400('Usage: /sell TICKER QTY PRICE_CENTS'));
|
|
1597
|
+
return true;
|
|
1598
|
+
}
|
|
1599
|
+
if (!config.tradingEnabled) {
|
|
1600
|
+
addSystemText(C.red('Trading disabled. Run: sf setup --enable-trading'));
|
|
1601
|
+
return true;
|
|
1602
|
+
}
|
|
1603
|
+
addSpacer();
|
|
1604
|
+
const answer = await promptUser(`SELL ${qtyStr}x ${ticker} YES @ ${priceStr}\u00A2 — execute? (y/n)`);
|
|
1605
|
+
if (answer.toLowerCase().startsWith('y')) {
|
|
1606
|
+
try {
|
|
1607
|
+
const { createOrder } = await import('../kalshi.js');
|
|
1608
|
+
const result = await createOrder({
|
|
1609
|
+
ticker, side: 'yes', action: 'sell', type: 'limit',
|
|
1610
|
+
count: parseInt(qtyStr),
|
|
1611
|
+
yes_price: (parseInt(priceStr) / 100).toFixed(2),
|
|
1612
|
+
});
|
|
1613
|
+
addSystemText(C.emerald('\u2713 Order placed: ' + ((result.order || result).order_id || 'OK')));
|
|
1614
|
+
}
|
|
1615
|
+
catch (err) {
|
|
1616
|
+
addSystemText(C.red('\u2717 ' + err.message));
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
else {
|
|
1620
|
+
addSystemText(C.zinc400('Cancelled.'));
|
|
1621
|
+
}
|
|
1622
|
+
addSpacer();
|
|
1623
|
+
return true;
|
|
1624
|
+
}
|
|
1625
|
+
case '/cancel': {
|
|
1626
|
+
const [, orderId] = parts;
|
|
1627
|
+
if (!orderId) {
|
|
1628
|
+
addSystemText(C.zinc400('Usage: /cancel ORDER_ID'));
|
|
1629
|
+
return true;
|
|
1630
|
+
}
|
|
1631
|
+
if (!config.tradingEnabled) {
|
|
1632
|
+
addSystemText(C.red('Trading disabled. Run: sf setup --enable-trading'));
|
|
1633
|
+
return true;
|
|
1634
|
+
}
|
|
1635
|
+
addSpacer();
|
|
1636
|
+
try {
|
|
1637
|
+
const { cancelOrder } = await import('../kalshi.js');
|
|
1638
|
+
await cancelOrder(orderId);
|
|
1639
|
+
addSystemText(C.emerald(`\u2713 Order ${orderId} cancelled.`));
|
|
1640
|
+
}
|
|
1641
|
+
catch (err) {
|
|
1642
|
+
addSystemText(C.red('\u2717 ' + err.message));
|
|
1643
|
+
}
|
|
1644
|
+
addSpacer();
|
|
1645
|
+
return true;
|
|
1646
|
+
}
|
|
1198
1647
|
case '/exit':
|
|
1199
1648
|
case '/quit': {
|
|
1200
1649
|
cleanup();
|
|
@@ -1209,6 +1658,17 @@ Output a structured summary. Be concise but preserve every important detail —
|
|
|
1209
1658
|
const trimmed = input.trim();
|
|
1210
1659
|
if (!trimmed)
|
|
1211
1660
|
return;
|
|
1661
|
+
// If a tool is waiting for user confirmation, resolve it
|
|
1662
|
+
if (pendingPrompt) {
|
|
1663
|
+
const { resolve } = pendingPrompt;
|
|
1664
|
+
pendingPrompt = null;
|
|
1665
|
+
const userResponse = new Text(C.zinc400(' > ') + C.zinc200(trimmed), 1, 0);
|
|
1666
|
+
chatContainer.addChild(userResponse);
|
|
1667
|
+
addSpacer();
|
|
1668
|
+
tui.requestRender();
|
|
1669
|
+
resolve(trimmed);
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1212
1672
|
if (isProcessing)
|
|
1213
1673
|
return;
|
|
1214
1674
|
// Add to editor history
|
|
@@ -1275,3 +1735,420 @@ Output a structured summary. Be concise but preserve every important detail —
|
|
|
1275
1735
|
// ── Start TUI ──────────────────────────────────────────────────────────────
|
|
1276
1736
|
tui.start();
|
|
1277
1737
|
}
|
|
1738
|
+
// ============================================================================
|
|
1739
|
+
// PLAIN-TEXT MODE (--no-tui)
|
|
1740
|
+
// ============================================================================
|
|
1741
|
+
async function runPlainTextAgent(params) {
|
|
1742
|
+
const { openrouterKey, sfClient, resolvedThesisId, opts } = params;
|
|
1743
|
+
let latestContext = params.latestContext;
|
|
1744
|
+
const readline = await import('readline');
|
|
1745
|
+
const piAi = await import('@mariozechner/pi-ai');
|
|
1746
|
+
const piAgent = await import('@mariozechner/pi-agent-core');
|
|
1747
|
+
const { getModel, streamSimple, Type } = piAi;
|
|
1748
|
+
const { Agent } = piAgent;
|
|
1749
|
+
const rawModelName = opts?.model || 'anthropic/claude-sonnet-4.6';
|
|
1750
|
+
let currentModelName = rawModelName.replace(/^openrouter\//, '');
|
|
1751
|
+
function resolveModel(name) {
|
|
1752
|
+
try {
|
|
1753
|
+
return getModel('openrouter', name);
|
|
1754
|
+
}
|
|
1755
|
+
catch {
|
|
1756
|
+
return {
|
|
1757
|
+
modelId: name, provider: 'openrouter', api: 'openai-completions',
|
|
1758
|
+
baseUrl: 'https://openrouter.ai/api/v1', id: name, name,
|
|
1759
|
+
inputPrice: 0, outputPrice: 0, contextWindow: 200000,
|
|
1760
|
+
supportsImages: true, supportsTools: true,
|
|
1761
|
+
};
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
let model = resolveModel(currentModelName);
|
|
1765
|
+
// ── Tools (same definitions as TUI mode) ──────────────────────────────────
|
|
1766
|
+
const thesisIdParam = Type.Object({ thesisId: Type.String({ description: 'Thesis ID' }) });
|
|
1767
|
+
const signalParams = Type.Object({
|
|
1768
|
+
thesisId: Type.String({ description: 'Thesis ID' }),
|
|
1769
|
+
content: Type.String({ description: 'Signal content' }),
|
|
1770
|
+
type: Type.Optional(Type.String({ description: 'Signal type: news, user_note, external' })),
|
|
1771
|
+
});
|
|
1772
|
+
const scanParams = Type.Object({
|
|
1773
|
+
query: Type.Optional(Type.String({ description: 'Keyword search' })),
|
|
1774
|
+
series: Type.Optional(Type.String({ description: 'Series ticker' })),
|
|
1775
|
+
market: Type.Optional(Type.String({ description: 'Market ticker' })),
|
|
1776
|
+
});
|
|
1777
|
+
const webSearchParams = Type.Object({ query: Type.String({ description: 'Search keywords' }) });
|
|
1778
|
+
const emptyParams = Type.Object({});
|
|
1779
|
+
const tools = [
|
|
1780
|
+
{
|
|
1781
|
+
name: 'get_context', label: 'Get Context',
|
|
1782
|
+
description: 'Get thesis snapshot: causal tree, edge prices, last evaluation, confidence',
|
|
1783
|
+
parameters: thesisIdParam,
|
|
1784
|
+
execute: async (_id, p) => {
|
|
1785
|
+
const ctx = await sfClient.getContext(p.thesisId);
|
|
1786
|
+
latestContext = ctx;
|
|
1787
|
+
return { content: [{ type: 'text', text: JSON.stringify(ctx, null, 2) }], details: {} };
|
|
1788
|
+
},
|
|
1789
|
+
},
|
|
1790
|
+
{
|
|
1791
|
+
name: 'inject_signal', label: 'Inject Signal',
|
|
1792
|
+
description: 'Inject a signal into the thesis',
|
|
1793
|
+
parameters: signalParams,
|
|
1794
|
+
execute: async (_id, p) => {
|
|
1795
|
+
const result = await sfClient.injectSignal(p.thesisId, p.type || 'user_note', p.content);
|
|
1796
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }], details: {} };
|
|
1797
|
+
},
|
|
1798
|
+
},
|
|
1799
|
+
{
|
|
1800
|
+
name: 'trigger_evaluation', label: 'Evaluate',
|
|
1801
|
+
description: 'Trigger a deep evaluation cycle',
|
|
1802
|
+
parameters: thesisIdParam,
|
|
1803
|
+
execute: async (_id, p) => {
|
|
1804
|
+
const result = await sfClient.evaluate(p.thesisId);
|
|
1805
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }], details: {} };
|
|
1806
|
+
},
|
|
1807
|
+
},
|
|
1808
|
+
{
|
|
1809
|
+
name: 'scan_markets', label: 'Scan Markets',
|
|
1810
|
+
description: 'Search Kalshi prediction markets',
|
|
1811
|
+
parameters: scanParams,
|
|
1812
|
+
execute: async (_id, p) => {
|
|
1813
|
+
let result;
|
|
1814
|
+
if (p.market) {
|
|
1815
|
+
result = await (0, client_js_1.kalshiFetchMarket)(p.market);
|
|
1816
|
+
}
|
|
1817
|
+
else if (p.series) {
|
|
1818
|
+
result = await (0, client_js_1.kalshiFetchMarketsBySeries)(p.series);
|
|
1819
|
+
}
|
|
1820
|
+
else if (p.query) {
|
|
1821
|
+
const series = await (0, client_js_1.kalshiFetchAllSeries)();
|
|
1822
|
+
const kws = p.query.toLowerCase().split(/\s+/);
|
|
1823
|
+
result = series.filter((s) => kws.every((k) => ((s.title || '') + (s.ticker || '')).toLowerCase().includes(k))).slice(0, 15);
|
|
1824
|
+
}
|
|
1825
|
+
else {
|
|
1826
|
+
result = { error: 'Provide query, series, or market' };
|
|
1827
|
+
}
|
|
1828
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], details: {} };
|
|
1829
|
+
},
|
|
1830
|
+
},
|
|
1831
|
+
{
|
|
1832
|
+
name: 'list_theses', label: 'List Theses',
|
|
1833
|
+
description: 'List all theses',
|
|
1834
|
+
parameters: emptyParams,
|
|
1835
|
+
execute: async () => {
|
|
1836
|
+
const theses = await sfClient.listTheses();
|
|
1837
|
+
return { content: [{ type: 'text', text: JSON.stringify(theses, null, 2) }], details: {} };
|
|
1838
|
+
},
|
|
1839
|
+
},
|
|
1840
|
+
{
|
|
1841
|
+
name: 'get_positions', label: 'Get Positions',
|
|
1842
|
+
description: 'Get Kalshi positions with live prices',
|
|
1843
|
+
parameters: emptyParams,
|
|
1844
|
+
execute: async () => {
|
|
1845
|
+
const positions = await (0, kalshi_js_1.getPositions)();
|
|
1846
|
+
if (!positions)
|
|
1847
|
+
return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
|
|
1848
|
+
for (const pos of positions) {
|
|
1849
|
+
const livePrice = await (0, kalshi_js_1.getMarketPrice)(pos.ticker);
|
|
1850
|
+
if (livePrice !== null) {
|
|
1851
|
+
pos.current_value = livePrice;
|
|
1852
|
+
pos.unrealized_pnl = Math.round((livePrice - pos.average_price_paid) * pos.quantity);
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
return { content: [{ type: 'text', text: JSON.stringify(positions, null, 2) }], details: {} };
|
|
1856
|
+
},
|
|
1857
|
+
},
|
|
1858
|
+
{
|
|
1859
|
+
name: 'web_search', label: 'Web Search',
|
|
1860
|
+
description: 'Search latest news and information',
|
|
1861
|
+
parameters: webSearchParams,
|
|
1862
|
+
execute: async (_id, p) => {
|
|
1863
|
+
const apiKey = process.env.TAVILY_API_KEY;
|
|
1864
|
+
if (!apiKey)
|
|
1865
|
+
return { content: [{ type: 'text', text: 'Tavily not configured. Set TAVILY_API_KEY.' }], details: {} };
|
|
1866
|
+
const res = await fetch('https://api.tavily.com/search', {
|
|
1867
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1868
|
+
body: JSON.stringify({ api_key: apiKey, query: p.query, max_results: 5, search_depth: 'basic', include_answer: true }),
|
|
1869
|
+
});
|
|
1870
|
+
if (!res.ok)
|
|
1871
|
+
return { content: [{ type: 'text', text: `Search failed: ${res.status}` }], details: {} };
|
|
1872
|
+
const data = await res.json();
|
|
1873
|
+
const results = (data.results || []).map((r) => `[${r.title}](${r.url})\n${r.content?.slice(0, 200)}`).join('\n\n');
|
|
1874
|
+
const answer = data.answer ? `Summary: ${data.answer}\n\n---\n\n` : '';
|
|
1875
|
+
return { content: [{ type: 'text', text: `${answer}${results}` }], details: {} };
|
|
1876
|
+
},
|
|
1877
|
+
},
|
|
1878
|
+
{
|
|
1879
|
+
name: 'get_milestones', label: 'Milestones',
|
|
1880
|
+
description: 'Get upcoming events from Kalshi calendar. Use to check economic releases, political events, or other catalysts.',
|
|
1881
|
+
parameters: Type.Object({
|
|
1882
|
+
hours: Type.Optional(Type.Number({ description: 'Hours ahead to look (default 168 = 1 week)' })),
|
|
1883
|
+
category: Type.Optional(Type.String({ description: 'Filter by category (e.g. Economics, Politics, Sports)' })),
|
|
1884
|
+
}),
|
|
1885
|
+
execute: async (_id, p) => {
|
|
1886
|
+
const hours = p.hours || 168;
|
|
1887
|
+
const now = new Date();
|
|
1888
|
+
const url = `https://api.elections.kalshi.com/trade-api/v2/milestones?limit=200&minimum_start_date=${now.toISOString()}` +
|
|
1889
|
+
(p.category ? `&category=${p.category}` : '');
|
|
1890
|
+
const res = await fetch(url, { headers: { 'Accept': 'application/json' } });
|
|
1891
|
+
if (!res.ok)
|
|
1892
|
+
return { content: [{ type: 'text', text: `Milestones API error: ${res.status}` }], details: {} };
|
|
1893
|
+
const data = await res.json();
|
|
1894
|
+
const cutoff = now.getTime() + hours * 3600000;
|
|
1895
|
+
const filtered = (data.milestones || [])
|
|
1896
|
+
.filter((m) => new Date(m.start_date).getTime() <= cutoff)
|
|
1897
|
+
.slice(0, 30)
|
|
1898
|
+
.map((m) => ({
|
|
1899
|
+
title: m.title, category: m.category, start_date: m.start_date,
|
|
1900
|
+
related_event_tickers: m.related_event_tickers,
|
|
1901
|
+
hours_until: Math.round((new Date(m.start_date).getTime() - now.getTime()) / 3600000),
|
|
1902
|
+
}));
|
|
1903
|
+
return { content: [{ type: 'text', text: JSON.stringify(filtered, null, 2) }], details: {} };
|
|
1904
|
+
},
|
|
1905
|
+
},
|
|
1906
|
+
{
|
|
1907
|
+
name: 'get_forecast', label: 'Forecast',
|
|
1908
|
+
description: 'Get market distribution (P50/P75/P90 percentile history) for a Kalshi event.',
|
|
1909
|
+
parameters: Type.Object({
|
|
1910
|
+
eventTicker: Type.String({ description: 'Kalshi event ticker (e.g. KXWTIMAX-26DEC31)' }),
|
|
1911
|
+
days: Type.Optional(Type.Number({ description: 'Days of history (default 7)' })),
|
|
1912
|
+
}),
|
|
1913
|
+
execute: async (_id, p) => {
|
|
1914
|
+
const { getForecastHistory } = await import('../kalshi.js');
|
|
1915
|
+
const days = p.days || 7;
|
|
1916
|
+
const evtRes = await fetch(`https://api.elections.kalshi.com/trade-api/v2/events/${p.eventTicker}`, { headers: { 'Accept': 'application/json' } });
|
|
1917
|
+
if (!evtRes.ok)
|
|
1918
|
+
return { content: [{ type: 'text', text: `Event not found: ${p.eventTicker}` }], details: {} };
|
|
1919
|
+
const evtData = await evtRes.json();
|
|
1920
|
+
const seriesTicker = evtData.event?.series_ticker;
|
|
1921
|
+
if (!seriesTicker)
|
|
1922
|
+
return { content: [{ type: 'text', text: `No series_ticker for ${p.eventTicker}` }], details: {} };
|
|
1923
|
+
const history = await getForecastHistory({
|
|
1924
|
+
seriesTicker, eventTicker: p.eventTicker, percentiles: [5000, 7500, 9000],
|
|
1925
|
+
startTs: Math.floor((Date.now() - days * 86400000) / 1000),
|
|
1926
|
+
endTs: Math.floor(Date.now() / 1000), periodInterval: 1440,
|
|
1927
|
+
});
|
|
1928
|
+
if (!history || history.length === 0)
|
|
1929
|
+
return { content: [{ type: 'text', text: 'No forecast data available' }], details: {} };
|
|
1930
|
+
return { content: [{ type: 'text', text: JSON.stringify(history, null, 2) }], details: {} };
|
|
1931
|
+
},
|
|
1932
|
+
},
|
|
1933
|
+
{
|
|
1934
|
+
name: 'get_settlements', label: 'Settlements',
|
|
1935
|
+
description: 'Get settled (resolved) contracts with P&L.',
|
|
1936
|
+
parameters: Type.Object({ ticker: Type.Optional(Type.String({ description: 'Filter by market ticker' })) }),
|
|
1937
|
+
execute: async (_id, p) => {
|
|
1938
|
+
const { getSettlements } = await import('../kalshi.js');
|
|
1939
|
+
const result = await getSettlements({ limit: 100, ticker: p.ticker });
|
|
1940
|
+
if (!result)
|
|
1941
|
+
return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
|
|
1942
|
+
return { content: [{ type: 'text', text: JSON.stringify(result.settlements, null, 2) }], details: {} };
|
|
1943
|
+
},
|
|
1944
|
+
},
|
|
1945
|
+
{
|
|
1946
|
+
name: 'get_balance', label: 'Balance',
|
|
1947
|
+
description: 'Get Kalshi account balance and portfolio value.',
|
|
1948
|
+
parameters: emptyParams,
|
|
1949
|
+
execute: async () => {
|
|
1950
|
+
const { getBalance } = await import('../kalshi.js');
|
|
1951
|
+
const result = await getBalance();
|
|
1952
|
+
if (!result)
|
|
1953
|
+
return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
|
|
1954
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], details: {} };
|
|
1955
|
+
},
|
|
1956
|
+
},
|
|
1957
|
+
{
|
|
1958
|
+
name: 'get_orders', label: 'Orders',
|
|
1959
|
+
description: 'Get current resting orders on Kalshi.',
|
|
1960
|
+
parameters: Type.Object({ status: Type.Optional(Type.String({ description: 'Filter by status: resting, canceled, executed. Default: resting' })) }),
|
|
1961
|
+
execute: async (_id, p) => {
|
|
1962
|
+
const { getOrders } = await import('../kalshi.js');
|
|
1963
|
+
const result = await getOrders({ status: p.status || 'resting', limit: 100 });
|
|
1964
|
+
if (!result)
|
|
1965
|
+
return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
|
|
1966
|
+
return { content: [{ type: 'text', text: JSON.stringify(result.orders, null, 2) }], details: {} };
|
|
1967
|
+
},
|
|
1968
|
+
},
|
|
1969
|
+
{
|
|
1970
|
+
name: 'get_fills', label: 'Fills',
|
|
1971
|
+
description: 'Get recent trade fills (executed trades) on Kalshi.',
|
|
1972
|
+
parameters: Type.Object({ ticker: Type.Optional(Type.String({ description: 'Filter by market ticker' })) }),
|
|
1973
|
+
execute: async (_id, p) => {
|
|
1974
|
+
const { getFills } = await import('../kalshi.js');
|
|
1975
|
+
const result = await getFills({ ticker: p.ticker, limit: 50 });
|
|
1976
|
+
if (!result)
|
|
1977
|
+
return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
|
|
1978
|
+
return { content: [{ type: 'text', text: JSON.stringify(result.fills, null, 2) }], details: {} };
|
|
1979
|
+
},
|
|
1980
|
+
},
|
|
1981
|
+
{
|
|
1982
|
+
name: 'get_schedule',
|
|
1983
|
+
label: 'Schedule',
|
|
1984
|
+
description: 'Get exchange status (open/closed) and trading hours. Use to check if low liquidity is due to off-hours.',
|
|
1985
|
+
parameters: emptyParams,
|
|
1986
|
+
execute: async () => {
|
|
1987
|
+
try {
|
|
1988
|
+
const res = await fetch('https://api.elections.kalshi.com/trade-api/v2/exchange/status', { headers: { 'Accept': 'application/json' } });
|
|
1989
|
+
if (!res.ok)
|
|
1990
|
+
return { content: [{ type: 'text', text: `Exchange API error: ${res.status}` }], details: {} };
|
|
1991
|
+
const data = await res.json();
|
|
1992
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
1993
|
+
}
|
|
1994
|
+
catch (err) {
|
|
1995
|
+
return { content: [{ type: 'text', text: `Failed: ${err.message}` }], details: {} };
|
|
1996
|
+
}
|
|
1997
|
+
},
|
|
1998
|
+
},
|
|
1999
|
+
];
|
|
2000
|
+
// ── System prompt ─────────────────────────────────────────────────────────
|
|
2001
|
+
const ctx = latestContext;
|
|
2002
|
+
const edgesSummary = ctx.edges
|
|
2003
|
+
?.sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge))
|
|
2004
|
+
.slice(0, 5)
|
|
2005
|
+
.map((e) => ` ${(e.market || '').slice(0, 40)} | ${e.venue || 'kalshi'} | mkt ${e.marketPrice}\u00A2 | edge ${e.edge > 0 ? '+' : ''}${e.edge}`)
|
|
2006
|
+
.join('\n') || ' (no edges)';
|
|
2007
|
+
const nodesSummary = ctx.causalTree?.nodes
|
|
2008
|
+
?.filter((n) => n.depth === 0)
|
|
2009
|
+
.map((n) => ` ${n.id} ${(n.label || '').slice(0, 40)} \u2014 ${Math.round(n.probability * 100)}%`)
|
|
2010
|
+
.join('\n') || ' (no causal tree)';
|
|
2011
|
+
const conf = typeof ctx.confidence === 'number' ? Math.round(ctx.confidence * 100) : 0;
|
|
2012
|
+
const systemPrompt = `You are a prediction market trading assistant. Help the user make correct trading decisions.
|
|
2013
|
+
|
|
2014
|
+
Current thesis: ${ctx.thesis || ctx.rawThesis || 'N/A'}
|
|
2015
|
+
ID: ${resolvedThesisId}
|
|
2016
|
+
Confidence: ${conf}%
|
|
2017
|
+
Status: ${ctx.status}
|
|
2018
|
+
|
|
2019
|
+
Causal tree nodes:
|
|
2020
|
+
${nodesSummary}
|
|
2021
|
+
|
|
2022
|
+
Top edges:
|
|
2023
|
+
${edgesSummary}
|
|
2024
|
+
|
|
2025
|
+
${ctx.lastEvaluation?.summary ? `Latest evaluation: ${ctx.lastEvaluation.summary.slice(0, 300)}` : ''}
|
|
2026
|
+
|
|
2027
|
+
Rules: Be concise. Use tools when needed. Don't ask "anything else?".`;
|
|
2028
|
+
// ── Create agent ──────────────────────────────────────────────────────────
|
|
2029
|
+
const agent = new Agent({
|
|
2030
|
+
initialState: { systemPrompt, model, tools, thinkingLevel: 'off' },
|
|
2031
|
+
streamFn: streamSimple,
|
|
2032
|
+
getApiKey: (provider) => provider === 'openrouter' ? openrouterKey : undefined,
|
|
2033
|
+
});
|
|
2034
|
+
// ── Session restore ───────────────────────────────────────────────────────
|
|
2035
|
+
if (!opts?.newSession) {
|
|
2036
|
+
const saved = loadSession(resolvedThesisId);
|
|
2037
|
+
if (saved?.messages?.length > 0) {
|
|
2038
|
+
try {
|
|
2039
|
+
agent.replaceMessages(saved.messages);
|
|
2040
|
+
agent.setSystemPrompt(systemPrompt);
|
|
2041
|
+
}
|
|
2042
|
+
catch { /* start fresh */ }
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
// ── Subscribe to agent events → plain stdout ──────────────────────────────
|
|
2046
|
+
let currentText = '';
|
|
2047
|
+
agent.subscribe((event) => {
|
|
2048
|
+
if (event.type === 'message_update') {
|
|
2049
|
+
const e = event.assistantMessageEvent;
|
|
2050
|
+
if (e.type === 'text_delta') {
|
|
2051
|
+
process.stdout.write(e.delta);
|
|
2052
|
+
currentText += e.delta;
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
if (event.type === 'message_end') {
|
|
2056
|
+
if (currentText) {
|
|
2057
|
+
process.stdout.write('\n');
|
|
2058
|
+
currentText = '';
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
if (event.type === 'tool_execution_start') {
|
|
2062
|
+
process.stderr.write(` \u26A1 ${event.toolName}...\n`);
|
|
2063
|
+
}
|
|
2064
|
+
if (event.type === 'tool_execution_end') {
|
|
2065
|
+
const status = event.isError ? '\u2717' : '\u2713';
|
|
2066
|
+
process.stderr.write(` ${status} ${event.toolName}\n`);
|
|
2067
|
+
}
|
|
2068
|
+
});
|
|
2069
|
+
// ── Welcome ───────────────────────────────────────────────────────────────
|
|
2070
|
+
const thesisText = ctx.thesis || ctx.rawThesis || 'N/A';
|
|
2071
|
+
console.log(`SF Agent — ${resolvedThesisId.slice(0, 8)} | ${conf}% | ${currentModelName}`);
|
|
2072
|
+
console.log(`Thesis: ${thesisText.length > 100 ? thesisText.slice(0, 100) + '...' : thesisText}`);
|
|
2073
|
+
console.log(`Edges: ${(ctx.edges || []).length} | Status: ${ctx.status}`);
|
|
2074
|
+
console.log('Type /help for commands, /exit to quit.\n');
|
|
2075
|
+
// ── REPL loop ─────────────────────────────────────────────────────────────
|
|
2076
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: '> ' });
|
|
2077
|
+
rl.prompt();
|
|
2078
|
+
for await (const line of rl) {
|
|
2079
|
+
const trimmed = line.trim();
|
|
2080
|
+
if (!trimmed) {
|
|
2081
|
+
rl.prompt();
|
|
2082
|
+
continue;
|
|
2083
|
+
}
|
|
2084
|
+
if (trimmed === '/exit' || trimmed === '/quit') {
|
|
2085
|
+
try {
|
|
2086
|
+
saveSession(resolvedThesisId, currentModelName, agent.state.messages);
|
|
2087
|
+
}
|
|
2088
|
+
catch { }
|
|
2089
|
+
rl.close();
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
2092
|
+
if (trimmed === '/help') {
|
|
2093
|
+
console.log('Commands: /help /exit /tree /edges /eval /model <name>');
|
|
2094
|
+
rl.prompt();
|
|
2095
|
+
continue;
|
|
2096
|
+
}
|
|
2097
|
+
if (trimmed === '/tree') {
|
|
2098
|
+
latestContext = await sfClient.getContext(resolvedThesisId);
|
|
2099
|
+
const nodes = latestContext.causalTree?.nodes || [];
|
|
2100
|
+
for (const n of nodes) {
|
|
2101
|
+
const indent = ' '.repeat(n.depth || 0);
|
|
2102
|
+
console.log(`${indent}${n.id} ${(n.label || '').slice(0, 60)} — ${Math.round(n.probability * 100)}%`);
|
|
2103
|
+
}
|
|
2104
|
+
rl.prompt();
|
|
2105
|
+
continue;
|
|
2106
|
+
}
|
|
2107
|
+
if (trimmed === '/edges') {
|
|
2108
|
+
latestContext = await sfClient.getContext(resolvedThesisId);
|
|
2109
|
+
const edges = (latestContext.edges || []).sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge)).slice(0, 15);
|
|
2110
|
+
for (const e of edges) {
|
|
2111
|
+
const sign = e.edge > 0 ? '+' : '';
|
|
2112
|
+
console.log(` ${(e.market || '').slice(0, 45).padEnd(45)} ${e.marketPrice}¢ edge ${sign}${e.edge} ${e.venue}`);
|
|
2113
|
+
}
|
|
2114
|
+
rl.prompt();
|
|
2115
|
+
continue;
|
|
2116
|
+
}
|
|
2117
|
+
if (trimmed === '/eval') {
|
|
2118
|
+
console.log('Triggering evaluation...');
|
|
2119
|
+
const result = await sfClient.evaluate(resolvedThesisId);
|
|
2120
|
+
console.log(`Confidence: ${result.previousConfidence} → ${result.newConfidence}`);
|
|
2121
|
+
if (result.summary)
|
|
2122
|
+
console.log(result.summary);
|
|
2123
|
+
rl.prompt();
|
|
2124
|
+
continue;
|
|
2125
|
+
}
|
|
2126
|
+
if (trimmed.startsWith('/model')) {
|
|
2127
|
+
const newModel = trimmed.slice(6).trim();
|
|
2128
|
+
if (!newModel) {
|
|
2129
|
+
console.log(`Current: ${currentModelName}`);
|
|
2130
|
+
rl.prompt();
|
|
2131
|
+
continue;
|
|
2132
|
+
}
|
|
2133
|
+
currentModelName = newModel.replace(/^openrouter\//, '');
|
|
2134
|
+
model = resolveModel(currentModelName);
|
|
2135
|
+
agent.setModel(model);
|
|
2136
|
+
console.log(`Model: ${currentModelName}`);
|
|
2137
|
+
rl.prompt();
|
|
2138
|
+
continue;
|
|
2139
|
+
}
|
|
2140
|
+
// Regular message → agent
|
|
2141
|
+
try {
|
|
2142
|
+
await agent.prompt(trimmed);
|
|
2143
|
+
}
|
|
2144
|
+
catch (err) {
|
|
2145
|
+
console.error(`Error: ${err.message}`);
|
|
2146
|
+
}
|
|
2147
|
+
// Save after each turn
|
|
2148
|
+
try {
|
|
2149
|
+
saveSession(resolvedThesisId, currentModelName, agent.state.messages);
|
|
2150
|
+
}
|
|
2151
|
+
catch { }
|
|
2152
|
+
rl.prompt();
|
|
2153
|
+
}
|
|
2154
|
+
}
|