@spfunctions/cli 0.1.1 → 0.1.3

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/README.md CHANGED
@@ -12,7 +12,7 @@ npm install -g @spfunctions/cli
12
12
 
13
13
  ```bash
14
14
  export SF_API_KEY=sf_live_xxx # required
15
- export SF_API_URL=https://simplefunctions.com # optional, defaults to production
15
+ export SF_API_URL=https://simplefunctions.dev # optional, defaults to production
16
16
  ```
17
17
 
18
18
  Or pass inline:
package/dist/client.js CHANGED
@@ -12,7 +12,7 @@ exports.kalshiFetchEvents = kalshiFetchEvents;
12
12
  exports.kalshiFetchMarket = kalshiFetchMarket;
13
13
  exports.kalshiFetchMarketsBySeries = kalshiFetchMarketsBySeries;
14
14
  exports.kalshiFetchMarketsByEvent = kalshiFetchMarketsByEvent;
15
- const DEFAULT_API_URL = 'https://simplefunctions.com';
15
+ const DEFAULT_API_URL = 'https://simplefunctions.dev';
16
16
  const KALSHI_BASE = 'https://api.elections.kalshi.com/trade-api/v2';
17
17
  // ===== SF API Client =====
18
18
  class SFClient {
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.contextCommand = contextCommand;
4
4
  const client_js_1 = require("../client.js");
5
+ const kalshi_js_1 = require("../kalshi.js");
5
6
  const utils_js_1 = require("../utils.js");
6
7
  async function contextCommand(id, opts) {
7
8
  const client = new client_js_1.SFClient(opts.apiKey, opts.apiUrl);
@@ -33,6 +34,22 @@ async function contextCommand(id, opts) {
33
34
  console.log(`${indent}${utils_js_1.c.cyan}${node.id}${utils_js_1.c.reset} ${(0, utils_js_1.pad)(label, 40)} ${(0, utils_js_1.rpad)(prob, 5)}`);
34
35
  }
35
36
  }
37
+ // Fetch positions if Kalshi is configured (local only, no server)
38
+ let positions = null;
39
+ if ((0, kalshi_js_1.isKalshiConfigured)()) {
40
+ try {
41
+ positions = await (0, kalshi_js_1.getPositions)();
42
+ }
43
+ catch {
44
+ // silently skip — positions are optional
45
+ }
46
+ }
47
+ const posMap = new Map();
48
+ if (positions) {
49
+ for (const p of positions) {
50
+ posMap.set(p.ticker, p);
51
+ }
52
+ }
36
53
  // Top edges (sorted by absolute edge size)
37
54
  const edges = ctx.edges;
38
55
  if (edges && edges.length > 0) {
@@ -43,10 +60,24 @@ async function contextCommand(id, opts) {
43
60
  const edgeColor = edgeSize > 10 ? utils_js_1.c.green : edgeSize > 0 ? utils_js_1.c.yellow : utils_js_1.c.red;
44
61
  const mktPrice = edge.marketPrice ?? edge.currentPrice ?? 0;
45
62
  const title = edge.market || edge.marketTitle || edge.marketId || '?';
63
+ // Orderbook info (if enriched by rescan)
64
+ const ob = edge.orderbook;
65
+ const obStr = ob ? ` ${utils_js_1.c.dim}spread ${ob.spread}¢ ${ob.liquidityScore}${utils_js_1.c.reset}` : '';
66
+ // Position overlay (if user has Kalshi positions)
67
+ const pos = posMap.get(edge.marketId);
68
+ let posStr = '';
69
+ if (pos) {
70
+ const pnl = pos.unrealized_pnl || 0;
71
+ const pnlColor = pnl > 0 ? utils_js_1.c.green : pnl < 0 ? utils_js_1.c.red : utils_js_1.c.dim;
72
+ const pnlFmt = pnl >= 0 ? `+$${(pnl / 100).toFixed(0)}` : `-$${(Math.abs(pnl) / 100).toFixed(0)}`;
73
+ posStr = ` ${utils_js_1.c.cyan}← ${pos.quantity}张 @ ${pos.average_price_paid}¢ ${pnlColor}${pnlFmt}${utils_js_1.c.reset}`;
74
+ }
46
75
  console.log(` ${(0, utils_js_1.pad)(title, 35)}` +
47
76
  ` ${(0, utils_js_1.rpad)(mktPrice.toFixed(0) + '¢', 5)}` +
48
77
  ` ${edgeColor}edge ${edgeSize > 0 ? '+' : ''}${edgeSize.toFixed(1)}${utils_js_1.c.reset}` +
49
- ` ${utils_js_1.c.dim}${edge.venue || ''}${utils_js_1.c.reset}`);
78
+ ` ${utils_js_1.c.dim}${edge.venue || ''}${utils_js_1.c.reset}` +
79
+ obStr +
80
+ posStr);
50
81
  }
51
82
  }
52
83
  // Last evaluation summary
@@ -0,0 +1,19 @@
1
+ /**
2
+ * sf positions — Show Kalshi positions with thesis edge overlay
3
+ *
4
+ * Flow:
5
+ * 1. Call local kalshi.getPositions() (Kalshi SDK + local private key) → real positions
6
+ * 2. Call SF API /api/thesis (via SF_API_KEY) → all theses' edge_analysis
7
+ * 3. Local merge: match position ticker to edge marketId
8
+ * 4. Output
9
+ *
10
+ * No server involvement for positions. Kalshi credentials never leave the machine.
11
+ */
12
+ interface PositionsOpts {
13
+ json?: boolean;
14
+ thesis?: string;
15
+ apiKey?: string;
16
+ apiUrl?: string;
17
+ }
18
+ export declare function positionsCommand(opts: PositionsOpts): Promise<void>;
19
+ export {};
@@ -0,0 +1,206 @@
1
+ "use strict";
2
+ /**
3
+ * sf positions — Show Kalshi positions with thesis edge overlay
4
+ *
5
+ * Flow:
6
+ * 1. Call local kalshi.getPositions() (Kalshi SDK + local private key) → real positions
7
+ * 2. Call SF API /api/thesis (via SF_API_KEY) → all theses' edge_analysis
8
+ * 3. Local merge: match position ticker to edge marketId
9
+ * 4. Output
10
+ *
11
+ * No server involvement for positions. Kalshi credentials never leave the machine.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.positionsCommand = positionsCommand;
15
+ const client_js_1 = require("../client.js");
16
+ const kalshi_js_1 = require("../kalshi.js");
17
+ const utils_js_1 = require("../utils.js");
18
+ async function positionsCommand(opts) {
19
+ const client = new client_js_1.SFClient(opts.apiKey, opts.apiUrl);
20
+ // ── Step 1: Fetch Kalshi positions (local) ──
21
+ let positions = null;
22
+ if ((0, kalshi_js_1.isKalshiConfigured)()) {
23
+ console.log(`${utils_js_1.c.dim}Fetching Kalshi positions...${utils_js_1.c.reset}`);
24
+ positions = await (0, kalshi_js_1.getPositions)();
25
+ }
26
+ // ── Step 2: Fetch all theses and their edges (via SF API) ──
27
+ console.log(`${utils_js_1.c.dim}Fetching thesis edges...${utils_js_1.c.reset}`);
28
+ let theses = [];
29
+ try {
30
+ const data = await client.listTheses();
31
+ theses = data.theses || [];
32
+ }
33
+ catch (err) {
34
+ console.warn(`${utils_js_1.c.yellow}Warning: Could not fetch theses: ${err}${utils_js_1.c.reset}`);
35
+ }
36
+ // If filtering by thesis, only fetch that one's context
37
+ let allEdges = [];
38
+ if (opts.thesis) {
39
+ try {
40
+ const ctx = await client.getContext(opts.thesis);
41
+ for (const edge of (ctx.edges || [])) {
42
+ allEdges.push({
43
+ thesisId: ctx.thesisId,
44
+ thesisTitle: ctx.thesis || ctx.title || '',
45
+ edge,
46
+ });
47
+ }
48
+ }
49
+ catch (err) {
50
+ console.warn(`${utils_js_1.c.yellow}Warning: Could not fetch context for ${opts.thesis}: ${err}${utils_js_1.c.reset}`);
51
+ }
52
+ }
53
+ else {
54
+ // Fetch context for all monitoring theses
55
+ const monitoringTheses = theses.filter(t => t.status === 'monitoring' || t.status === 'active');
56
+ for (const thesis of monitoringTheses.slice(0, 5)) { // limit to 5 to avoid rate limits
57
+ try {
58
+ const ctx = await client.getContext(thesis.id);
59
+ for (const edge of (ctx.edges || [])) {
60
+ allEdges.push({
61
+ thesisId: thesis.id,
62
+ thesisTitle: ctx.thesis || ctx.title || thesis.title || '',
63
+ edge,
64
+ });
65
+ }
66
+ }
67
+ catch {
68
+ // skip failed
69
+ }
70
+ }
71
+ }
72
+ // ── Step 3: Merge ──
73
+ // Build a lookup from ticker → edge
74
+ const edgeByTicker = new Map();
75
+ for (const item of allEdges) {
76
+ const ticker = item.edge.marketId;
77
+ if (!edgeByTicker.has(ticker))
78
+ edgeByTicker.set(ticker, []);
79
+ edgeByTicker.get(ticker).push(item);
80
+ }
81
+ if (opts.json) {
82
+ console.log(JSON.stringify({
83
+ kalshiConfigured: (0, kalshi_js_1.isKalshiConfigured)(),
84
+ positions: positions || [],
85
+ edges: allEdges.map(e => ({ ...e.edge, thesisId: e.thesisId })),
86
+ }, null, 2));
87
+ return;
88
+ }
89
+ // ── Step 4: Display ──
90
+ // A) Positioned edges (positions that match thesis edges)
91
+ if (positions && positions.length > 0) {
92
+ (0, utils_js_1.header)('Your Positions (via Kalshi)');
93
+ console.log(' ' + utils_js_1.c.bold +
94
+ (0, utils_js_1.pad)('Ticker', 25) +
95
+ (0, utils_js_1.rpad)('Side', 5) +
96
+ (0, utils_js_1.rpad)('Qty', 7) +
97
+ (0, utils_js_1.rpad)('Avg', 6) +
98
+ (0, utils_js_1.rpad)('Now', 6) +
99
+ (0, utils_js_1.rpad)('P&L', 9) +
100
+ (0, utils_js_1.rpad)('Edge', 7) +
101
+ ' Signal' +
102
+ utils_js_1.c.reset);
103
+ console.log(' ' + utils_js_1.c.dim + '─'.repeat(85) + utils_js_1.c.reset);
104
+ for (const pos of positions) {
105
+ // Try to get current price
106
+ const nowPrice = pos.current_value || null;
107
+ const avgPrice = pos.average_price_paid || 0;
108
+ const pnl = pos.unrealized_pnl || 0;
109
+ const pnlColor = pnl > 0 ? utils_js_1.c.green : pnl < 0 ? utils_js_1.c.red : utils_js_1.c.dim;
110
+ const pnlStr = pnl >= 0 ? `+$${(pnl / 100).toFixed(2)}` : `-$${(Math.abs(pnl) / 100).toFixed(2)}`;
111
+ // Find matching edge
112
+ const matchingEdges = edgeByTicker.get(pos.ticker) || [];
113
+ const topEdge = matchingEdges[0];
114
+ const edgeSize = topEdge?.edge?.edge ?? topEdge?.edge?.edgeSize ?? 0;
115
+ const edgeColor = Math.abs(edgeSize) > 10 ? utils_js_1.c.green : Math.abs(edgeSize) > 5 ? utils_js_1.c.yellow : utils_js_1.c.dim;
116
+ // Signal: HOLD if edge still positive in same direction, WATCH otherwise
117
+ let signal = 'HOLD';
118
+ if (topEdge) {
119
+ const posDirection = pos.side;
120
+ const edgeDirection = topEdge.edge.direction;
121
+ if (posDirection === edgeDirection && edgeSize > 3) {
122
+ signal = 'HOLD';
123
+ }
124
+ else if (edgeSize < -3) {
125
+ signal = 'CLOSE';
126
+ }
127
+ else {
128
+ signal = 'HOLD';
129
+ }
130
+ }
131
+ else {
132
+ signal = '—';
133
+ }
134
+ const signalColor = signal === 'HOLD' ? utils_js_1.c.dim : signal === 'CLOSE' ? utils_js_1.c.red : utils_js_1.c.yellow;
135
+ console.log(' ' +
136
+ (0, utils_js_1.pad)(pos.ticker, 25) +
137
+ (0, utils_js_1.rpad)(pos.side.toUpperCase(), 5) +
138
+ (0, utils_js_1.rpad)(String(pos.quantity), 7) +
139
+ (0, utils_js_1.rpad)(`${avgPrice}¢`, 6) +
140
+ (0, utils_js_1.rpad)(nowPrice ? `${nowPrice}¢` : '-', 6) +
141
+ `${pnlColor}${(0, utils_js_1.rpad)(pnlStr, 9)}${utils_js_1.c.reset}` +
142
+ `${edgeColor}${(0, utils_js_1.rpad)(edgeSize ? `${edgeSize > 0 ? '+' : ''}${edgeSize.toFixed(0)}` : '-', 7)}${utils_js_1.c.reset}` +
143
+ ` ${signalColor}${signal}${utils_js_1.c.reset}`);
144
+ }
145
+ console.log('');
146
+ }
147
+ else if ((0, kalshi_js_1.isKalshiConfigured)()) {
148
+ console.log(`\n${utils_js_1.c.dim}No open positions on Kalshi.${utils_js_1.c.reset}\n`);
149
+ }
150
+ else {
151
+ console.log(`\n${utils_js_1.c.yellow}Kalshi not configured.${utils_js_1.c.reset} Set KALSHI_API_KEY_ID and KALSHI_PRIVATE_KEY_PATH to see positions.\n`);
152
+ }
153
+ // B) Unpositioned edges (edges without matching positions)
154
+ const positionedTickers = new Set((positions || []).map(p => p.ticker));
155
+ const unpositionedEdges = allEdges.filter(e => !positionedTickers.has(e.edge.marketId));
156
+ if (unpositionedEdges.length > 0) {
157
+ // Sort by absolute edge size descending
158
+ unpositionedEdges.sort((a, b) => Math.abs(b.edge.edge ?? b.edge.edgeSize ?? 0) - Math.abs(a.edge.edge ?? a.edge.edgeSize ?? 0));
159
+ const thesisLabel = opts.thesis ? ` (thesis ${(0, utils_js_1.shortId)(opts.thesis)})` : '';
160
+ (0, utils_js_1.header)(`Unpositioned Edges${thesisLabel}`);
161
+ console.log(' ' + utils_js_1.c.bold +
162
+ (0, utils_js_1.pad)('Market', 30) +
163
+ (0, utils_js_1.rpad)('Mkt', 6) +
164
+ (0, utils_js_1.rpad)('Thesis', 8) +
165
+ (0, utils_js_1.rpad)('Edge', 7) +
166
+ (0, utils_js_1.rpad)('Spread', 8) +
167
+ (0, utils_js_1.rpad)('Liq', 8) +
168
+ ' Signal' +
169
+ utils_js_1.c.reset);
170
+ console.log(' ' + utils_js_1.c.dim + '─'.repeat(85) + utils_js_1.c.reset);
171
+ for (const item of unpositionedEdges.slice(0, 20)) {
172
+ const e = item.edge;
173
+ const edgeSize = e.edge ?? e.edgeSize ?? 0;
174
+ const edgeColor = edgeSize > 10 ? utils_js_1.c.green : edgeSize > 5 ? utils_js_1.c.yellow : edgeSize > 0 ? utils_js_1.c.dim : utils_js_1.c.red;
175
+ const mktPrice = e.marketPrice ?? 0;
176
+ const thesisPrice = e.thesisPrice ?? e.thesisImpliedPrice ?? 0;
177
+ const title = (e.market || e.marketTitle || e.marketId || '?').slice(0, 29);
178
+ // Orderbook info (if present from rescan)
179
+ const ob = e.orderbook;
180
+ const spreadStr = ob ? `${ob.spread}¢` : '-';
181
+ const liqStr = ob ? ob.liquidityScore : '-';
182
+ const liqColor = ob?.liquidityScore === 'high' ? utils_js_1.c.green : ob?.liquidityScore === 'medium' ? utils_js_1.c.yellow : utils_js_1.c.dim;
183
+ // Signal
184
+ let signal = 'WATCH';
185
+ if (edgeSize > 10 && ob?.liquidityScore !== 'low') {
186
+ signal = 'CONSIDER';
187
+ }
188
+ else if (edgeSize > 5 && ob?.liquidityScore === 'high') {
189
+ signal = 'CONSIDER';
190
+ }
191
+ const signalColor = signal === 'CONSIDER' ? utils_js_1.c.green : utils_js_1.c.dim;
192
+ console.log(' ' +
193
+ (0, utils_js_1.pad)(title, 30) +
194
+ (0, utils_js_1.rpad)(`${mktPrice.toFixed(0)}¢`, 6) +
195
+ (0, utils_js_1.rpad)(`${thesisPrice.toFixed(0)}¢`, 8) +
196
+ `${edgeColor}${(0, utils_js_1.rpad)(`${edgeSize > 0 ? '+' : ''}${edgeSize.toFixed(0)}`, 7)}${utils_js_1.c.reset}` +
197
+ (0, utils_js_1.rpad)(spreadStr, 8) +
198
+ `${liqColor}${(0, utils_js_1.rpad)(liqStr, 8)}${utils_js_1.c.reset}` +
199
+ ` ${signalColor}${signal}${utils_js_1.c.reset}`);
200
+ }
201
+ console.log('');
202
+ }
203
+ else if (allEdges.length === 0) {
204
+ console.log(`\n${utils_js_1.c.dim}No thesis edges found.${utils_js_1.c.reset}\n`);
205
+ }
206
+ }
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ const create_js_1 = require("./commands/create.js");
24
24
  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
+ const positions_js_1 = require("./commands/positions.js");
27
28
  const utils_js_1 = require("./utils.js");
28
29
  const program = new commander_1.Command();
29
30
  program
@@ -114,6 +115,21 @@ program
114
115
  apiUrl: g.apiUrl,
115
116
  }));
116
117
  });
118
+ // ── sf positions ──────────────────────────────────────────────────────────────
119
+ program
120
+ .command('positions')
121
+ .description('Show Kalshi positions with thesis edge overlay (requires KALSHI_API_KEY_ID + KALSHI_PRIVATE_KEY_PATH)')
122
+ .option('--json', 'JSON output for agents')
123
+ .option('--thesis <id>', 'Filter by thesis ID')
124
+ .action(async (opts, cmd) => {
125
+ const g = cmd.optsWithGlobals();
126
+ await run(() => (0, positions_js_1.positionsCommand)({
127
+ json: opts.json,
128
+ thesis: opts.thesis,
129
+ apiKey: g.apiKey,
130
+ apiUrl: g.apiUrl,
131
+ }));
132
+ });
117
133
  // ── Error wrapper ─────────────────────────────────────────────────────────────
118
134
  async function run(fn) {
119
135
  try {
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Kalshi Authenticated Client (CLI-side only)
3
+ *
4
+ * Uses the kalshi-typescript SDK with RSA-PSS private key auth.
5
+ * Credentials NEVER leave the user's machine — they are read from env vars
6
+ * pointing to a local PEM file.
7
+ *
8
+ * Required env vars:
9
+ * KALSHI_API_KEY_ID — Key ID from Kalshi dashboard
10
+ * KALSHI_PRIVATE_KEY_PATH — Path to RSA private key PEM file (e.g. ~/.kalshi/private.pem)
11
+ */
12
+ /**
13
+ * Check if Kalshi credentials are configured locally.
14
+ */
15
+ export declare function isKalshiConfigured(): boolean;
16
+ export interface KalshiPosition {
17
+ ticker: string;
18
+ event_ticker: string;
19
+ market_title: string;
20
+ side: 'yes' | 'no';
21
+ quantity: number;
22
+ average_price_paid: number;
23
+ current_value: number;
24
+ realized_pnl: number;
25
+ unrealized_pnl: number;
26
+ total_cost: number;
27
+ settlement_value?: number;
28
+ [key: string]: unknown;
29
+ }
30
+ export interface KalshiPortfolio {
31
+ positions: KalshiPosition[];
32
+ }
33
+ /**
34
+ * Get user's live positions from Kalshi.
35
+ * Returns null if Kalshi is not configured.
36
+ */
37
+ export declare function getPositions(): Promise<KalshiPosition[] | null>;
38
+ /**
39
+ * Get the current market price for a given ticker (public, no auth).
40
+ */
41
+ export declare function getMarketPrice(ticker: string): Promise<number | null>;
package/dist/kalshi.js ADDED
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ /**
3
+ * Kalshi Authenticated Client (CLI-side only)
4
+ *
5
+ * Uses the kalshi-typescript SDK with RSA-PSS private key auth.
6
+ * Credentials NEVER leave the user's machine — they are read from env vars
7
+ * pointing to a local PEM file.
8
+ *
9
+ * Required env vars:
10
+ * KALSHI_API_KEY_ID — Key ID from Kalshi dashboard
11
+ * KALSHI_PRIVATE_KEY_PATH — Path to RSA private key PEM file (e.g. ~/.kalshi/private.pem)
12
+ */
13
+ var __importDefault = (this && this.__importDefault) || function (mod) {
14
+ return (mod && mod.__esModule) ? mod : { "default": mod };
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.isKalshiConfigured = isKalshiConfigured;
18
+ exports.getPositions = getPositions;
19
+ exports.getMarketPrice = getMarketPrice;
20
+ const fs_1 = __importDefault(require("fs"));
21
+ const path_1 = __importDefault(require("path"));
22
+ const crypto_1 = __importDefault(require("crypto"));
23
+ const KALSHI_API_BASE = 'https://api.elections.kalshi.com/trade-api/v2';
24
+ // ============================================================================
25
+ // AUTH
26
+ // ============================================================================
27
+ /**
28
+ * Check if Kalshi credentials are configured locally.
29
+ */
30
+ function isKalshiConfigured() {
31
+ return !!(process.env.KALSHI_API_KEY_ID && process.env.KALSHI_PRIVATE_KEY_PATH);
32
+ }
33
+ /**
34
+ * Load the RSA private key from disk.
35
+ */
36
+ function loadPrivateKey() {
37
+ const keyPath = process.env.KALSHI_PRIVATE_KEY_PATH;
38
+ if (!keyPath)
39
+ return null;
40
+ const resolved = keyPath.startsWith('~')
41
+ ? path_1.default.join(process.env.HOME || '', keyPath.slice(1))
42
+ : path_1.default.resolve(keyPath);
43
+ try {
44
+ const pem = fs_1.default.readFileSync(resolved, 'utf-8');
45
+ return crypto_1.default.createPrivateKey(pem);
46
+ }
47
+ catch (err) {
48
+ console.warn(`[Kalshi] Failed to load private key from ${resolved}:`, err);
49
+ return null;
50
+ }
51
+ }
52
+ /**
53
+ * Sign a request using RSA-PSS (Kalshi's auth scheme).
54
+ *
55
+ * Kalshi expects:
56
+ * Header: KALSHI-ACCESS-KEY = API key ID
57
+ * Header: KALSHI-ACCESS-SIGNATURE = base64(RSA-PSS-sign(timestamp_ms + method + path))
58
+ * Header: KALSHI-ACCESS-TIMESTAMP = milliseconds since epoch (string)
59
+ */
60
+ function signRequest(method, urlPath, privateKey) {
61
+ const keyId = process.env.KALSHI_API_KEY_ID;
62
+ const timestampMs = Date.now().toString();
63
+ // Kalshi signing payload: timestamp_ms + method + path (no body)
64
+ const payload = timestampMs + method.toUpperCase() + urlPath;
65
+ const signature = crypto_1.default.sign('sha256', Buffer.from(payload), {
66
+ key: privateKey,
67
+ padding: crypto_1.default.constants.RSA_PKCS1_PSS_PADDING,
68
+ saltLength: crypto_1.default.constants.RSA_PSS_SALTLEN_DIGEST,
69
+ });
70
+ return {
71
+ headers: {
72
+ 'KALSHI-ACCESS-KEY': keyId,
73
+ 'KALSHI-ACCESS-SIGNATURE': signature.toString('base64'),
74
+ 'KALSHI-ACCESS-TIMESTAMP': timestampMs,
75
+ 'Content-Type': 'application/json',
76
+ 'Accept': 'application/json',
77
+ },
78
+ };
79
+ }
80
+ /**
81
+ * Make an authenticated Kalshi API request.
82
+ */
83
+ async function kalshiAuthGet(apiPath) {
84
+ const privateKey = loadPrivateKey();
85
+ if (!privateKey) {
86
+ throw new Error('Kalshi private key not loaded. Check KALSHI_PRIVATE_KEY_PATH.');
87
+ }
88
+ const url = `${KALSHI_API_BASE}${apiPath}`;
89
+ const { headers } = signRequest('GET', `/trade-api/v2${apiPath}`, privateKey);
90
+ const res = await fetch(url, { method: 'GET', headers });
91
+ if (!res.ok) {
92
+ const text = await res.text();
93
+ throw new Error(`Kalshi API ${res.status}: ${text}`);
94
+ }
95
+ return res.json();
96
+ }
97
+ /**
98
+ * Get user's live positions from Kalshi.
99
+ * Returns null if Kalshi is not configured.
100
+ */
101
+ async function getPositions() {
102
+ if (!isKalshiConfigured())
103
+ return null;
104
+ try {
105
+ const data = await kalshiAuthGet('/portfolio/positions');
106
+ // Kalshi returns { market_positions: [...] } or { positions: [...] }
107
+ 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
+ }));
120
+ }
121
+ catch (err) {
122
+ console.warn(`[Kalshi] Failed to fetch positions:`, err);
123
+ return null;
124
+ }
125
+ }
126
+ /**
127
+ * Get the current market price for a given ticker (public, no auth).
128
+ */
129
+ async function getMarketPrice(ticker) {
130
+ try {
131
+ const url = `https://api.elections.kalshi.com/trade-api/v2/markets/${ticker}`;
132
+ const res = await fetch(url, { headers: { 'Accept': 'application/json' } });
133
+ if (!res.ok)
134
+ return null;
135
+ const data = await res.json();
136
+ const m = data.market || data;
137
+ return m.last_price || m.yes_bid || null;
138
+ }
139
+ catch {
140
+ return null;
141
+ }
142
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spfunctions/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "CLI for SimpleFunctions prediction market thesis agent",
5
5
  "bin": {
6
6
  "sf": "./dist/index.js"
@@ -11,12 +11,13 @@
11
11
  "prepublishOnly": "npm run build"
12
12
  },
13
13
  "dependencies": {
14
- "commander": "^12.0.0"
14
+ "commander": "^12.0.0",
15
+ "kalshi-typescript": "^3.9.0"
15
16
  },
16
17
  "devDependencies": {
17
- "typescript": "^5.0.0",
18
+ "@types/node": "^20.0.0",
18
19
  "tsx": "^4.0.0",
19
- "@types/node": "^20.0.0"
20
+ "typescript": "^5.0.0"
20
21
  },
21
22
  "files": [
22
23
  "dist"