@spfunctions/cli 0.1.3 → 0.1.5
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/commands/agent.d.ts +18 -0
- package/dist/commands/agent.js +290 -0
- package/dist/commands/positions.js +40 -2
- package/dist/index.js +11 -0
- package/dist/kalshi.d.ts +14 -0
- package/dist/kalshi.js +86 -13
- package/package.json +3 -1
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sf agent — Interactive natural language terminal agent.
|
|
3
|
+
*
|
|
4
|
+
* Uses pi-agent-core's Agent class for the tool-calling loop.
|
|
5
|
+
* Uses pi-ai for unified LLM access via OpenRouter.
|
|
6
|
+
*
|
|
7
|
+
* The agent has 6 tools:
|
|
8
|
+
* get_context — thesis snapshot (causal tree, edges, confidence)
|
|
9
|
+
* inject_signal — inject news/note/external signal
|
|
10
|
+
* trigger_evaluation — trigger deep eval (heavy model)
|
|
11
|
+
* scan_markets — search Kalshi markets
|
|
12
|
+
* list_theses — list all theses
|
|
13
|
+
* get_positions — Kalshi portfolio positions
|
|
14
|
+
*/
|
|
15
|
+
export declare function agentCommand(thesisId?: string, opts?: {
|
|
16
|
+
model?: string;
|
|
17
|
+
modelKey?: string;
|
|
18
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* sf agent — Interactive natural language terminal agent.
|
|
4
|
+
*
|
|
5
|
+
* Uses pi-agent-core's Agent class for the tool-calling loop.
|
|
6
|
+
* Uses pi-ai for unified LLM access via OpenRouter.
|
|
7
|
+
*
|
|
8
|
+
* The agent has 6 tools:
|
|
9
|
+
* get_context — thesis snapshot (causal tree, edges, confidence)
|
|
10
|
+
* inject_signal — inject news/note/external signal
|
|
11
|
+
* trigger_evaluation — trigger deep eval (heavy model)
|
|
12
|
+
* scan_markets — search Kalshi markets
|
|
13
|
+
* list_theses — list all theses
|
|
14
|
+
* get_positions — Kalshi portfolio positions
|
|
15
|
+
*/
|
|
16
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
17
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
|
+
};
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.agentCommand = agentCommand;
|
|
21
|
+
const readline_1 = __importDefault(require("readline"));
|
|
22
|
+
const client_js_1 = require("../client.js");
|
|
23
|
+
const kalshi_js_1 = require("../kalshi.js");
|
|
24
|
+
async function agentCommand(thesisId, opts) {
|
|
25
|
+
// ── Dynamic imports for ESM-only packages ──────────────────────────────────
|
|
26
|
+
const piAi = await import('@mariozechner/pi-ai');
|
|
27
|
+
const piAgent = await import('@mariozechner/pi-agent-core');
|
|
28
|
+
const { getModel, streamSimple, Type } = piAi;
|
|
29
|
+
const { Agent } = piAgent;
|
|
30
|
+
// ── Validate API keys ──────────────────────────────────────────────────────
|
|
31
|
+
const openrouterKey = opts?.modelKey || process.env.OPENROUTER_API_KEY;
|
|
32
|
+
if (!openrouterKey) {
|
|
33
|
+
console.error('Need OpenRouter API key. Set OPENROUTER_API_KEY or use --model-key.');
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
const sfClient = new client_js_1.SFClient();
|
|
37
|
+
// ── Resolve thesis ID ──────────────────────────────────────────────────────
|
|
38
|
+
let resolvedThesisId = thesisId;
|
|
39
|
+
if (!resolvedThesisId) {
|
|
40
|
+
const data = await sfClient.listTheses();
|
|
41
|
+
const theses = data.theses || data;
|
|
42
|
+
const active = theses.find((t) => t.status === 'active');
|
|
43
|
+
if (!active) {
|
|
44
|
+
console.error('No active thesis. Create one first: sf create "..."');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
resolvedThesisId = active.id;
|
|
48
|
+
}
|
|
49
|
+
// ── Fetch initial context ──────────────────────────────────────────────────
|
|
50
|
+
const context = await sfClient.getContext(resolvedThesisId);
|
|
51
|
+
// ── Define tools ───────────────────────────────────────────────────────────
|
|
52
|
+
const thesisIdParam = Type.Object({
|
|
53
|
+
thesisId: Type.String({ description: 'Thesis ID (short or full UUID)' }),
|
|
54
|
+
});
|
|
55
|
+
const signalParams = Type.Object({
|
|
56
|
+
thesisId: Type.String({ description: 'Thesis ID' }),
|
|
57
|
+
content: Type.String({ description: 'Signal content' }),
|
|
58
|
+
type: Type.Optional(Type.String({ description: 'Signal type: news, user_note, external. Default: user_note' })),
|
|
59
|
+
});
|
|
60
|
+
const scanParams = Type.Object({
|
|
61
|
+
query: Type.Optional(Type.String({ description: 'Keyword search for Kalshi markets' })),
|
|
62
|
+
series: Type.Optional(Type.String({ description: 'Kalshi series ticker (e.g. KXWTIMAX)' })),
|
|
63
|
+
market: Type.Optional(Type.String({ description: 'Specific market ticker' })),
|
|
64
|
+
});
|
|
65
|
+
const emptyParams = Type.Object({});
|
|
66
|
+
const tools = [
|
|
67
|
+
{
|
|
68
|
+
name: 'get_context',
|
|
69
|
+
label: 'Get Context',
|
|
70
|
+
description: 'Get thesis snapshot: causal tree, edge prices, last evaluation, confidence',
|
|
71
|
+
parameters: thesisIdParam,
|
|
72
|
+
execute: async (_toolCallId, params) => {
|
|
73
|
+
const ctx = await sfClient.getContext(params.thesisId);
|
|
74
|
+
return {
|
|
75
|
+
content: [{ type: 'text', text: JSON.stringify(ctx, null, 2) }],
|
|
76
|
+
details: {},
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'inject_signal',
|
|
82
|
+
label: 'Inject Signal',
|
|
83
|
+
description: 'Inject a signal into the thesis (news, note, external event)',
|
|
84
|
+
parameters: signalParams,
|
|
85
|
+
execute: async (_toolCallId, params) => {
|
|
86
|
+
const result = await sfClient.injectSignal(params.thesisId, params.type || 'user_note', params.content);
|
|
87
|
+
return {
|
|
88
|
+
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
89
|
+
details: {},
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: 'trigger_evaluation',
|
|
95
|
+
label: 'Evaluate',
|
|
96
|
+
description: 'Trigger a deep evaluation cycle (heavy model, takes longer)',
|
|
97
|
+
parameters: thesisIdParam,
|
|
98
|
+
execute: async (_toolCallId, params) => {
|
|
99
|
+
const result = await sfClient.evaluate(params.thesisId);
|
|
100
|
+
return {
|
|
101
|
+
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
102
|
+
details: {},
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'scan_markets',
|
|
108
|
+
label: 'Scan Markets',
|
|
109
|
+
description: 'Search Kalshi prediction markets: by keywords, series ticker, or specific market ticker',
|
|
110
|
+
parameters: scanParams,
|
|
111
|
+
execute: async (_toolCallId, params) => {
|
|
112
|
+
let result;
|
|
113
|
+
if (params.market) {
|
|
114
|
+
result = await (0, client_js_1.kalshiFetchMarket)(params.market);
|
|
115
|
+
}
|
|
116
|
+
else if (params.series) {
|
|
117
|
+
result = await (0, client_js_1.kalshiFetchMarketsBySeries)(params.series);
|
|
118
|
+
}
|
|
119
|
+
else if (params.query) {
|
|
120
|
+
const series = await (0, client_js_1.kalshiFetchAllSeries)();
|
|
121
|
+
const keywords = params.query.toLowerCase().split(/\s+/);
|
|
122
|
+
const matched = series
|
|
123
|
+
.filter((s) => keywords.some((kw) => (s.title || '').toLowerCase().includes(kw) ||
|
|
124
|
+
(s.ticker || '').toLowerCase().includes(kw)))
|
|
125
|
+
.slice(0, 15);
|
|
126
|
+
result = matched;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
result = { error: 'Provide query, series, or market parameter' };
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
133
|
+
details: {},
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'list_theses',
|
|
139
|
+
label: 'List Theses',
|
|
140
|
+
description: 'List all theses for the current user',
|
|
141
|
+
parameters: emptyParams,
|
|
142
|
+
execute: async () => {
|
|
143
|
+
const theses = await sfClient.listTheses();
|
|
144
|
+
return {
|
|
145
|
+
content: [{ type: 'text', text: JSON.stringify(theses, null, 2) }],
|
|
146
|
+
details: {},
|
|
147
|
+
};
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: 'get_positions',
|
|
152
|
+
label: 'Get Positions',
|
|
153
|
+
description: 'Get Kalshi exchange positions with live prices and PnL (requires local Kalshi key config)',
|
|
154
|
+
parameters: emptyParams,
|
|
155
|
+
execute: async () => {
|
|
156
|
+
const positions = await (0, kalshi_js_1.getPositions)();
|
|
157
|
+
if (!positions) {
|
|
158
|
+
return {
|
|
159
|
+
content: [
|
|
160
|
+
{
|
|
161
|
+
type: 'text',
|
|
162
|
+
text: 'Kalshi not configured. Set KALSHI_API_KEY_ID and KALSHI_PRIVATE_KEY_PATH environment variables.',
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
details: {},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
// Enrich with live prices (same logic as sf positions command)
|
|
169
|
+
for (const pos of positions) {
|
|
170
|
+
const livePrice = await (0, kalshi_js_1.getMarketPrice)(pos.ticker);
|
|
171
|
+
if (livePrice !== null) {
|
|
172
|
+
pos.current_value = livePrice;
|
|
173
|
+
pos.unrealized_pnl = Math.round((livePrice - pos.average_price_paid) * pos.quantity);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
content: [{ type: 'text', text: JSON.stringify(positions, null, 2) }],
|
|
178
|
+
details: {},
|
|
179
|
+
};
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
];
|
|
183
|
+
// ── System prompt ──────────────────────────────────────────────────────────
|
|
184
|
+
const confidencePct = typeof context.confidence === 'number'
|
|
185
|
+
? Math.round(context.confidence * 100)
|
|
186
|
+
: context.confidence;
|
|
187
|
+
const systemPrompt = `You are a SimpleFunctions prediction market trading assistant.
|
|
188
|
+
|
|
189
|
+
Current thesis: ${context.thesis || context.rawThesis || 'N/A'}
|
|
190
|
+
Confidence: ${confidencePct}%
|
|
191
|
+
Status: ${context.status}
|
|
192
|
+
Thesis ID: ${context.thesisId || resolvedThesisId}
|
|
193
|
+
|
|
194
|
+
You have six tools available. Use them when you need real-time data. Answer directly when you don't.
|
|
195
|
+
Be concise. Use Chinese if the user writes in Chinese, English if they write in English.
|
|
196
|
+
Do NOT make up data. Always call tools to get current state.`;
|
|
197
|
+
// ── Configure model via OpenRouter ─────────────────────────────────────────
|
|
198
|
+
const rawModelName = opts?.model || 'anthropic/claude-sonnet-4-20250514';
|
|
199
|
+
// Strip 'openrouter/' prefix if user passed it (provider is already openrouter)
|
|
200
|
+
const modelName = rawModelName.replace(/^openrouter\//, '');
|
|
201
|
+
// Try the registry first. If the model isn't registered, construct manually.
|
|
202
|
+
// OpenRouter accepts any model ID — the registry just may not have it yet.
|
|
203
|
+
let model;
|
|
204
|
+
try {
|
|
205
|
+
model = getModel('openrouter', modelName);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// Manual fallback: construct a Model object compatible with pi-ai's openai-completions API
|
|
209
|
+
// (OpenRouter uses OpenAI-compatible API)
|
|
210
|
+
model = {
|
|
211
|
+
modelId: modelName,
|
|
212
|
+
provider: 'openrouter',
|
|
213
|
+
api: 'openai-completions',
|
|
214
|
+
baseUrl: 'https://openrouter.ai/api/v1',
|
|
215
|
+
id: modelName,
|
|
216
|
+
name: modelName,
|
|
217
|
+
inputPrice: 0,
|
|
218
|
+
outputPrice: 0,
|
|
219
|
+
contextWindow: 200000,
|
|
220
|
+
supportsImages: true,
|
|
221
|
+
supportsTools: true,
|
|
222
|
+
};
|
|
223
|
+
console.log(`\x1b[33mModel '${modelName}' not in registry, using direct OpenRouter API\x1b[0m`);
|
|
224
|
+
}
|
|
225
|
+
// ── Create Agent ───────────────────────────────────────────────────────────
|
|
226
|
+
const agent = new Agent({
|
|
227
|
+
initialState: {
|
|
228
|
+
systemPrompt,
|
|
229
|
+
model,
|
|
230
|
+
tools,
|
|
231
|
+
thinkingLevel: 'off',
|
|
232
|
+
},
|
|
233
|
+
streamFn: streamSimple,
|
|
234
|
+
getApiKey: (provider) => {
|
|
235
|
+
if (provider === 'openrouter')
|
|
236
|
+
return openrouterKey;
|
|
237
|
+
return undefined;
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
// ── Subscribe to streaming events ──────────────────────────────────────────
|
|
241
|
+
agent.subscribe((event) => {
|
|
242
|
+
if (event.type === 'message_update') {
|
|
243
|
+
const e = event.assistantMessageEvent;
|
|
244
|
+
if (e.type === 'text_delta') {
|
|
245
|
+
process.stdout.write(e.delta);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (event.type === 'tool_execution_start') {
|
|
249
|
+
process.stdout.write(`\n\x1b[33m⚡ ${event.toolName}\x1b[0m `);
|
|
250
|
+
}
|
|
251
|
+
if (event.type === 'tool_execution_end') {
|
|
252
|
+
if (event.isError) {
|
|
253
|
+
process.stdout.write('\x1b[31m✗\x1b[0m\n');
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
process.stdout.write('\x1b[32m✓\x1b[0m\n');
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
// ── Terminal REPL ──────────────────────────────────────────────────────────
|
|
261
|
+
const shortId = (resolvedThesisId || '').slice(0, 8);
|
|
262
|
+
console.log(`\n\x1b[32mSimpleFunctions Agent\x1b[0m — ${shortId}`);
|
|
263
|
+
console.log(`Model: ${modelName} | Type "exit" to quit\n`);
|
|
264
|
+
const rl = readline_1.default.createInterface({
|
|
265
|
+
input: process.stdin,
|
|
266
|
+
output: process.stdout,
|
|
267
|
+
});
|
|
268
|
+
const prompt = () => {
|
|
269
|
+
rl.question('\x1b[36m> \x1b[0m', async (input) => {
|
|
270
|
+
const trimmed = input.trim();
|
|
271
|
+
if (!trimmed) {
|
|
272
|
+
prompt();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (trimmed === 'exit' || trimmed === 'quit') {
|
|
276
|
+
rl.close();
|
|
277
|
+
process.exit(0);
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
await agent.prompt(trimmed);
|
|
281
|
+
console.log('\n');
|
|
282
|
+
}
|
|
283
|
+
catch (err) {
|
|
284
|
+
console.error(`\n\x1b[31mError: ${err.message}\x1b[0m\n`);
|
|
285
|
+
}
|
|
286
|
+
prompt();
|
|
287
|
+
});
|
|
288
|
+
};
|
|
289
|
+
prompt();
|
|
290
|
+
}
|
|
@@ -69,6 +69,25 @@ async function positionsCommand(opts) {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
+
// ── Step 2.5: Enrich positions with live prices ──
|
|
73
|
+
if (positions && positions.length > 0) {
|
|
74
|
+
console.log(`${utils_js_1.c.dim}Fetching live prices for ${positions.length} positions...${utils_js_1.c.reset}`);
|
|
75
|
+
for (const pos of positions) {
|
|
76
|
+
try {
|
|
77
|
+
const livePrice = await (0, kalshi_js_1.getMarketPrice)(pos.ticker);
|
|
78
|
+
if (livePrice !== null) {
|
|
79
|
+
pos.current_value = livePrice;
|
|
80
|
+
// P&L in cents: (currentPrice - avgEntry) * quantity
|
|
81
|
+
pos.unrealized_pnl = (livePrice - pos.average_price_paid) * pos.quantity;
|
|
82
|
+
}
|
|
83
|
+
// Small delay to avoid rate limits
|
|
84
|
+
await new Promise(r => setTimeout(r, 100));
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// skip — live price optional
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
72
91
|
// ── Step 3: Merge ──
|
|
73
92
|
// Build a lookup from ticker → edge
|
|
74
93
|
const edgeByTicker = new Map();
|
|
@@ -156,6 +175,23 @@ async function positionsCommand(opts) {
|
|
|
156
175
|
if (unpositionedEdges.length > 0) {
|
|
157
176
|
// Sort by absolute edge size descending
|
|
158
177
|
unpositionedEdges.sort((a, b) => Math.abs(b.edge.edge ?? b.edge.edgeSize ?? 0) - Math.abs(a.edge.edge ?? a.edge.edgeSize ?? 0));
|
|
178
|
+
// Pre-fetch orderbooks locally for top Kalshi edges that don't already have server OB data
|
|
179
|
+
const topEdgesForOB = unpositionedEdges.slice(0, 10).filter(item => item.edge.venue === 'kalshi' && !item.edge.orderbook && Math.abs(item.edge.edge ?? item.edge.edgeSize ?? 0) > 5);
|
|
180
|
+
const localObMap = new Map();
|
|
181
|
+
if (topEdgesForOB.length > 0 && (0, kalshi_js_1.isKalshiConfigured)()) {
|
|
182
|
+
console.log(`${utils_js_1.c.dim}Fetching orderbooks for ${topEdgesForOB.length} edges...${utils_js_1.c.reset}`);
|
|
183
|
+
for (const item of topEdgesForOB) {
|
|
184
|
+
try {
|
|
185
|
+
const ob = await (0, kalshi_js_1.getOrderbook)(item.edge.marketId);
|
|
186
|
+
if (ob)
|
|
187
|
+
localObMap.set(item.edge.marketId, ob);
|
|
188
|
+
await new Promise(r => setTimeout(r, 150));
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
// skip
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
159
195
|
const thesisLabel = opts.thesis ? ` (thesis ${(0, utils_js_1.shortId)(opts.thesis)})` : '';
|
|
160
196
|
(0, utils_js_1.header)(`Unpositioned Edges${thesisLabel}`);
|
|
161
197
|
console.log(' ' + utils_js_1.c.bold +
|
|
@@ -175,8 +211,10 @@ async function positionsCommand(opts) {
|
|
|
175
211
|
const mktPrice = e.marketPrice ?? 0;
|
|
176
212
|
const thesisPrice = e.thesisPrice ?? e.thesisImpliedPrice ?? 0;
|
|
177
213
|
const title = (e.market || e.marketTitle || e.marketId || '?').slice(0, 29);
|
|
178
|
-
// Orderbook
|
|
179
|
-
const
|
|
214
|
+
// Orderbook: prefer server data, fallback to local fetch
|
|
215
|
+
const serverOb = e.orderbook;
|
|
216
|
+
const localOb = localObMap.get(e.marketId);
|
|
217
|
+
const ob = serverOb || localOb;
|
|
180
218
|
const spreadStr = ob ? `${ob.spread}¢` : '-';
|
|
181
219
|
const liqStr = ob ? ob.liquidityScore : '-';
|
|
182
220
|
const liqColor = ob?.liquidityScore === 'high' ? utils_js_1.c.green : ob?.liquidityScore === 'medium' ? utils_js_1.c.yellow : utils_js_1.c.dim;
|
package/dist/index.js
CHANGED
|
@@ -25,6 +25,7 @@ const signal_js_1 = require("./commands/signal.js");
|
|
|
25
25
|
const evaluate_js_1 = require("./commands/evaluate.js");
|
|
26
26
|
const scan_js_1 = require("./commands/scan.js");
|
|
27
27
|
const positions_js_1 = require("./commands/positions.js");
|
|
28
|
+
const agent_js_1 = require("./commands/agent.js");
|
|
28
29
|
const utils_js_1 = require("./utils.js");
|
|
29
30
|
const program = new commander_1.Command();
|
|
30
31
|
program
|
|
@@ -130,6 +131,16 @@ program
|
|
|
130
131
|
apiUrl: g.apiUrl,
|
|
131
132
|
}));
|
|
132
133
|
});
|
|
134
|
+
// ── sf agent [thesisId] ───────────────────────────────────────────────────────
|
|
135
|
+
program
|
|
136
|
+
.command('agent [thesisId]')
|
|
137
|
+
.description('Interactive agent mode — natural language interface to SimpleFunctions')
|
|
138
|
+
.option('--model <model>', 'Model via OpenRouter (default: anthropic/claude-sonnet-4-20250514)')
|
|
139
|
+
.option('--model-key <key>', 'OpenRouter API key (or set OPENROUTER_API_KEY)')
|
|
140
|
+
.action(async (thesisId, opts, cmd) => {
|
|
141
|
+
const g = cmd.optsWithGlobals();
|
|
142
|
+
await run(() => (0, agent_js_1.agentCommand)(thesisId, { model: opts.model, modelKey: opts.modelKey }));
|
|
143
|
+
});
|
|
133
144
|
// ── Error wrapper ─────────────────────────────────────────────────────────────
|
|
134
145
|
async function run(fn) {
|
|
135
146
|
try {
|
package/dist/kalshi.d.ts
CHANGED
|
@@ -37,5 +37,19 @@ export interface KalshiPortfolio {
|
|
|
37
37
|
export declare function getPositions(): Promise<KalshiPosition[] | null>;
|
|
38
38
|
/**
|
|
39
39
|
* Get the current market price for a given ticker (public, no auth).
|
|
40
|
+
* Returns price in cents (0-100) or null.
|
|
40
41
|
*/
|
|
41
42
|
export declare function getMarketPrice(ticker: string): Promise<number | null>;
|
|
43
|
+
/**
|
|
44
|
+
* Fetch orderbook for a ticker using local Kalshi auth credentials.
|
|
45
|
+
* Returns simplified spread/depth info or null.
|
|
46
|
+
*/
|
|
47
|
+
export interface LocalOrderbook {
|
|
48
|
+
bestBid: number;
|
|
49
|
+
bestAsk: number;
|
|
50
|
+
spread: number;
|
|
51
|
+
bidDepth: number;
|
|
52
|
+
askDepth: number;
|
|
53
|
+
liquidityScore: 'high' | 'medium' | 'low';
|
|
54
|
+
}
|
|
55
|
+
export declare function getOrderbook(ticker: string): Promise<LocalOrderbook | null>;
|
package/dist/kalshi.js
CHANGED
|
@@ -17,6 +17,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
17
17
|
exports.isKalshiConfigured = isKalshiConfigured;
|
|
18
18
|
exports.getPositions = getPositions;
|
|
19
19
|
exports.getMarketPrice = getMarketPrice;
|
|
20
|
+
exports.getOrderbook = getOrderbook;
|
|
20
21
|
const fs_1 = __importDefault(require("fs"));
|
|
21
22
|
const path_1 = __importDefault(require("path"));
|
|
22
23
|
const crypto_1 = __importDefault(require("crypto"));
|
|
@@ -105,18 +106,32 @@ async function getPositions() {
|
|
|
105
106
|
const data = await kalshiAuthGet('/portfolio/positions');
|
|
106
107
|
// Kalshi returns { market_positions: [...] } or { positions: [...] }
|
|
107
108
|
const raw = data.market_positions || data.positions || [];
|
|
108
|
-
return raw.map((p) =>
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
109
|
+
return raw.map((p) => {
|
|
110
|
+
// Kalshi actual fields:
|
|
111
|
+
// position_fp: "795.00" (string, contract count — positive=YES, negative=NO)
|
|
112
|
+
// total_traded_dollars: "453.1500" (string, total cost in dollars)
|
|
113
|
+
// ticker: "KXWTIMAX-26DEC31-T135"
|
|
114
|
+
// event_ticker: "KXWTIMAX-26DEC31"
|
|
115
|
+
// market_result: "", resting_orders_count: 0, etc.
|
|
116
|
+
const positionFp = parseFloat(p.position_fp || '0');
|
|
117
|
+
const totalTradedDollars = parseFloat(p.total_traded_dollars || '0');
|
|
118
|
+
const quantity = Math.abs(positionFp);
|
|
119
|
+
const side = positionFp >= 0 ? 'yes' : 'no';
|
|
120
|
+
// avg price in cents = (total_traded_dollars / quantity) * 100
|
|
121
|
+
const avgPriceCents = quantity > 0 ? Math.round((totalTradedDollars / quantity) * 100) : 0;
|
|
122
|
+
return {
|
|
123
|
+
ticker: p.ticker || p.market_ticker || '',
|
|
124
|
+
event_ticker: p.event_ticker || '',
|
|
125
|
+
market_title: p.market_title || p.title || '',
|
|
126
|
+
side,
|
|
127
|
+
quantity,
|
|
128
|
+
average_price_paid: avgPriceCents,
|
|
129
|
+
current_value: 0, // will be enriched by live price lookup if needed
|
|
130
|
+
realized_pnl: Math.round(parseFloat(p.realized_pnl || '0') * 100),
|
|
131
|
+
unrealized_pnl: 0, // Kalshi doesn't give this directly, needs live price
|
|
132
|
+
total_cost: Math.round(totalTradedDollars * 100), // dollars → cents
|
|
133
|
+
};
|
|
134
|
+
});
|
|
120
135
|
}
|
|
121
136
|
catch (err) {
|
|
122
137
|
console.warn(`[Kalshi] Failed to fetch positions:`, err);
|
|
@@ -125,6 +140,7 @@ async function getPositions() {
|
|
|
125
140
|
}
|
|
126
141
|
/**
|
|
127
142
|
* Get the current market price for a given ticker (public, no auth).
|
|
143
|
+
* Returns price in cents (0-100) or null.
|
|
128
144
|
*/
|
|
129
145
|
async function getMarketPrice(ticker) {
|
|
130
146
|
try {
|
|
@@ -134,7 +150,64 @@ async function getMarketPrice(ticker) {
|
|
|
134
150
|
return null;
|
|
135
151
|
const data = await res.json();
|
|
136
152
|
const m = data.market || data;
|
|
137
|
-
|
|
153
|
+
// Try cents fields first, then dollar string fields
|
|
154
|
+
if (m.last_price && m.last_price > 0)
|
|
155
|
+
return m.last_price;
|
|
156
|
+
if (m.yes_bid && m.yes_bid > 0)
|
|
157
|
+
return m.yes_bid;
|
|
158
|
+
if (m.last_price_dollars) {
|
|
159
|
+
const parsed = parseFloat(m.last_price_dollars);
|
|
160
|
+
if (!isNaN(parsed) && parsed > 0)
|
|
161
|
+
return Math.round(parsed * 100);
|
|
162
|
+
}
|
|
163
|
+
if (m.yes_bid_dollars) {
|
|
164
|
+
const parsed = parseFloat(m.yes_bid_dollars);
|
|
165
|
+
if (!isNaN(parsed) && parsed > 0)
|
|
166
|
+
return Math.round(parsed * 100);
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async function getOrderbook(ticker) {
|
|
175
|
+
if (!isKalshiConfigured())
|
|
176
|
+
return null;
|
|
177
|
+
try {
|
|
178
|
+
const data = await kalshiAuthGet(`/markets/${ticker}/orderbook`);
|
|
179
|
+
const ob = data.orderbook_fp || data.orderbook;
|
|
180
|
+
if (!ob)
|
|
181
|
+
return null;
|
|
182
|
+
// Kalshi orderbook_fp uses yes_dollars/no_dollars: [["0.57", "100"], ...]
|
|
183
|
+
// Fallback to yes/no (cents format)
|
|
184
|
+
const rawYes = ob.yes_dollars || ob.yes || [];
|
|
185
|
+
const rawNo = ob.no_dollars || ob.no || [];
|
|
186
|
+
const isDollar = !!(ob.yes_dollars || ob.no_dollars);
|
|
187
|
+
// Parse to cents
|
|
188
|
+
const parsedYes = rawYes.map(l => ({
|
|
189
|
+
price: isDollar ? Math.round(parseFloat(l[0]) * 100) : Number(l[0]),
|
|
190
|
+
qty: parseFloat(l[1]),
|
|
191
|
+
})).filter(l => l.price > 0);
|
|
192
|
+
const parsedNo = rawNo.map(l => ({
|
|
193
|
+
price: isDollar ? Math.round(parseFloat(l[0]) * 100) : Number(l[0]),
|
|
194
|
+
qty: parseFloat(l[1]),
|
|
195
|
+
})).filter(l => l.price > 0);
|
|
196
|
+
// Sort descending by price
|
|
197
|
+
parsedYes.sort((a, b) => b.price - a.price);
|
|
198
|
+
parsedNo.sort((a, b) => b.price - a.price);
|
|
199
|
+
const bestBid = parsedYes.length > 0 ? parsedYes[0].price : 0;
|
|
200
|
+
const bestAsk = parsedNo.length > 0 ? (100 - parsedNo[0].price) : 100;
|
|
201
|
+
const spread = bestAsk - bestBid;
|
|
202
|
+
const bidDepth = parsedYes.slice(0, 3).reduce((sum, l) => sum + l.qty, 0);
|
|
203
|
+
const askDepth = parsedNo.slice(0, 3).reduce((sum, l) => sum + l.qty, 0);
|
|
204
|
+
const minDepth = Math.min(bidDepth, askDepth);
|
|
205
|
+
let liquidityScore = 'low';
|
|
206
|
+
if (spread <= 2 && minDepth >= 500)
|
|
207
|
+
liquidityScore = 'high';
|
|
208
|
+
else if (spread <= 5 && minDepth >= 100)
|
|
209
|
+
liquidityScore = 'medium';
|
|
210
|
+
return { bestBid, bestAsk, spread, bidDepth, askDepth, liquidityScore };
|
|
138
211
|
}
|
|
139
212
|
catch {
|
|
140
213
|
return null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spfunctions/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "CLI for SimpleFunctions prediction market thesis agent",
|
|
5
5
|
"bin": {
|
|
6
6
|
"sf": "./dist/index.js"
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
"prepublishOnly": "npm run build"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
+
"@mariozechner/pi-agent-core": "^0.57.1",
|
|
15
|
+
"@mariozechner/pi-ai": "^0.57.1",
|
|
14
16
|
"commander": "^12.0.0",
|
|
15
17
|
"kalshi-typescript": "^3.9.0"
|
|
16
18
|
},
|