@spfunctions/cli 0.1.1 → 0.1.2
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 +1 -1
- package/dist/client.js +1 -1
- package/dist/commands/context.js +32 -1
- package/dist/commands/positions.d.ts +19 -0
- package/dist/commands/positions.js +206 -0
- package/dist/index.js +16 -0
- package/dist/kalshi.d.ts +41 -0
- package/dist/kalshi.js +142 -0
- package/package.json +5 -4
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.
|
|
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.
|
|
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 {
|
package/dist/commands/context.js
CHANGED
|
@@ -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 {
|
package/dist/kalshi.d.ts
ADDED
|
@@ -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://trading-api.kalshi.co/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 = key ID
|
|
57
|
+
* Header: KALSHI-ACCESS-SIGNATURE = base64(RSA-PSS-sign(timestamp + method + path))
|
|
58
|
+
* Header: KALSHI-ACCESS-TIMESTAMP = ISO 8601 timestamp
|
|
59
|
+
*/
|
|
60
|
+
function signRequest(method, urlPath, privateKey) {
|
|
61
|
+
const keyId = process.env.KALSHI_API_KEY_ID;
|
|
62
|
+
const timestamp = new Date().toISOString();
|
|
63
|
+
// Kalshi signing payload: timestamp + method + path (no body)
|
|
64
|
+
const payload = timestamp + 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': timestamp,
|
|
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.
|
|
3
|
+
"version": "0.1.2",
|
|
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
|
-
"
|
|
18
|
+
"@types/node": "^20.0.0",
|
|
18
19
|
"tsx": "^4.0.0",
|
|
19
|
-
"
|
|
20
|
+
"typescript": "^5.0.0"
|
|
20
21
|
},
|
|
21
22
|
"files": [
|
|
22
23
|
"dist"
|