@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.
@@ -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 info (if present from rescan)
179
- const ob = e.orderbook;
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
- ticker: p.ticker || p.market_ticker || '',
110
- event_ticker: p.event_ticker || '',
111
- market_title: p.market_title || p.title || '',
112
- side: (p.position_side || p.side || 'yes').toLowerCase(),
113
- quantity: p.total_traded || p.quantity || p.position || 0,
114
- average_price_paid: p.average_price_paid || p.avg_price || 0,
115
- current_value: p.market_value || p.current_value || 0,
116
- realized_pnl: p.realized_pnl || 0,
117
- unrealized_pnl: p.unrealized_pnl || 0,
118
- total_cost: p.total_cost || 0,
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
- return m.last_price || m.yes_bid || null;
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",
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
  },