@spfunctions/cli 0.1.3 → 0.1.4
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/positions.js +40 -2
- package/dist/kalshi.d.ts +14 -0
- package/dist/kalshi.js +89 -13
- package/package.json +1 -1
|
@@ -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/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"));
|
|
@@ -103,20 +104,37 @@ async function getPositions() {
|
|
|
103
104
|
return null;
|
|
104
105
|
try {
|
|
105
106
|
const data = await kalshiAuthGet('/portfolio/positions');
|
|
107
|
+
// DEBUG: dump raw Kalshi response to see actual field names
|
|
108
|
+
console.log('[Kalshi DEBUG] Raw /portfolio/positions response:');
|
|
109
|
+
console.log(JSON.stringify(data, null, 2));
|
|
106
110
|
// Kalshi returns { market_positions: [...] } or { positions: [...] }
|
|
107
111
|
const raw = data.market_positions || data.positions || [];
|
|
108
|
-
return raw.map((p) =>
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
112
|
+
return raw.map((p) => {
|
|
113
|
+
// Kalshi actual fields:
|
|
114
|
+
// position_fp: "795.00" (string, contract count — positive=YES, negative=NO)
|
|
115
|
+
// total_traded_dollars: "453.1500" (string, total cost in dollars)
|
|
116
|
+
// ticker: "KXWTIMAX-26DEC31-T135"
|
|
117
|
+
// event_ticker: "KXWTIMAX-26DEC31"
|
|
118
|
+
// market_result: "", resting_orders_count: 0, etc.
|
|
119
|
+
const positionFp = parseFloat(p.position_fp || '0');
|
|
120
|
+
const totalTradedDollars = parseFloat(p.total_traded_dollars || '0');
|
|
121
|
+
const quantity = Math.abs(positionFp);
|
|
122
|
+
const side = positionFp >= 0 ? 'yes' : 'no';
|
|
123
|
+
// avg price in cents = (total_traded_dollars / quantity) * 100
|
|
124
|
+
const avgPriceCents = quantity > 0 ? Math.round((totalTradedDollars / quantity) * 100) : 0;
|
|
125
|
+
return {
|
|
126
|
+
ticker: p.ticker || p.market_ticker || '',
|
|
127
|
+
event_ticker: p.event_ticker || '',
|
|
128
|
+
market_title: p.market_title || p.title || '',
|
|
129
|
+
side,
|
|
130
|
+
quantity,
|
|
131
|
+
average_price_paid: avgPriceCents,
|
|
132
|
+
current_value: 0, // will be enriched by live price lookup if needed
|
|
133
|
+
realized_pnl: Math.round(parseFloat(p.realized_pnl || '0') * 100),
|
|
134
|
+
unrealized_pnl: 0, // Kalshi doesn't give this directly, needs live price
|
|
135
|
+
total_cost: Math.round(totalTradedDollars * 100), // dollars → cents
|
|
136
|
+
};
|
|
137
|
+
});
|
|
120
138
|
}
|
|
121
139
|
catch (err) {
|
|
122
140
|
console.warn(`[Kalshi] Failed to fetch positions:`, err);
|
|
@@ -125,6 +143,7 @@ async function getPositions() {
|
|
|
125
143
|
}
|
|
126
144
|
/**
|
|
127
145
|
* Get the current market price for a given ticker (public, no auth).
|
|
146
|
+
* Returns price in cents (0-100) or null.
|
|
128
147
|
*/
|
|
129
148
|
async function getMarketPrice(ticker) {
|
|
130
149
|
try {
|
|
@@ -134,7 +153,64 @@ async function getMarketPrice(ticker) {
|
|
|
134
153
|
return null;
|
|
135
154
|
const data = await res.json();
|
|
136
155
|
const m = data.market || data;
|
|
137
|
-
|
|
156
|
+
// Try cents fields first, then dollar string fields
|
|
157
|
+
if (m.last_price && m.last_price > 0)
|
|
158
|
+
return m.last_price;
|
|
159
|
+
if (m.yes_bid && m.yes_bid > 0)
|
|
160
|
+
return m.yes_bid;
|
|
161
|
+
if (m.last_price_dollars) {
|
|
162
|
+
const parsed = parseFloat(m.last_price_dollars);
|
|
163
|
+
if (!isNaN(parsed) && parsed > 0)
|
|
164
|
+
return Math.round(parsed * 100);
|
|
165
|
+
}
|
|
166
|
+
if (m.yes_bid_dollars) {
|
|
167
|
+
const parsed = parseFloat(m.yes_bid_dollars);
|
|
168
|
+
if (!isNaN(parsed) && parsed > 0)
|
|
169
|
+
return Math.round(parsed * 100);
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async function getOrderbook(ticker) {
|
|
178
|
+
if (!isKalshiConfigured())
|
|
179
|
+
return null;
|
|
180
|
+
try {
|
|
181
|
+
const data = await kalshiAuthGet(`/markets/${ticker}/orderbook`);
|
|
182
|
+
const ob = data.orderbook_fp || data.orderbook;
|
|
183
|
+
if (!ob)
|
|
184
|
+
return null;
|
|
185
|
+
// Kalshi orderbook_fp uses yes_dollars/no_dollars: [["0.57", "100"], ...]
|
|
186
|
+
// Fallback to yes/no (cents format)
|
|
187
|
+
const rawYes = ob.yes_dollars || ob.yes || [];
|
|
188
|
+
const rawNo = ob.no_dollars || ob.no || [];
|
|
189
|
+
const isDollar = !!(ob.yes_dollars || ob.no_dollars);
|
|
190
|
+
// Parse to cents
|
|
191
|
+
const parsedYes = rawYes.map(l => ({
|
|
192
|
+
price: isDollar ? Math.round(parseFloat(l[0]) * 100) : Number(l[0]),
|
|
193
|
+
qty: parseFloat(l[1]),
|
|
194
|
+
})).filter(l => l.price > 0);
|
|
195
|
+
const parsedNo = rawNo.map(l => ({
|
|
196
|
+
price: isDollar ? Math.round(parseFloat(l[0]) * 100) : Number(l[0]),
|
|
197
|
+
qty: parseFloat(l[1]),
|
|
198
|
+
})).filter(l => l.price > 0);
|
|
199
|
+
// Sort descending by price
|
|
200
|
+
parsedYes.sort((a, b) => b.price - a.price);
|
|
201
|
+
parsedNo.sort((a, b) => b.price - a.price);
|
|
202
|
+
const bestBid = parsedYes.length > 0 ? parsedYes[0].price : 0;
|
|
203
|
+
const bestAsk = parsedNo.length > 0 ? (100 - parsedNo[0].price) : 100;
|
|
204
|
+
const spread = bestAsk - bestBid;
|
|
205
|
+
const bidDepth = parsedYes.slice(0, 3).reduce((sum, l) => sum + l.qty, 0);
|
|
206
|
+
const askDepth = parsedNo.slice(0, 3).reduce((sum, l) => sum + l.qty, 0);
|
|
207
|
+
const minDepth = Math.min(bidDepth, askDepth);
|
|
208
|
+
let liquidityScore = 'low';
|
|
209
|
+
if (spread <= 2 && minDepth >= 500)
|
|
210
|
+
liquidityScore = 'high';
|
|
211
|
+
else if (spread <= 5 && minDepth >= 100)
|
|
212
|
+
liquidityScore = 'medium';
|
|
213
|
+
return { bestBid, bestAsk, spread, bidDepth, askDepth, liquidityScore };
|
|
138
214
|
}
|
|
139
215
|
catch {
|
|
140
216
|
return null;
|