@gaberoo/kalshitools 1.0.3 → 1.1.0

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,64 @@
1
+ /**
2
+ * Portfolio analytics calculation utilities
3
+ */
4
+ import type { Fill, Position } from './kalshi/types.js';
5
+ /**
6
+ * Calculate win rate from a set of fills
7
+ * Matches buys and sells for the same ticker to determine wins/losses
8
+ */
9
+ export declare function calculateWinRate(fills: Fill[]): number;
10
+ /**
11
+ * Calculate average P&L for winning and losing trades
12
+ */
13
+ export declare function calculateAveragePnL(fills: Fill[]): {
14
+ avgLoss: number;
15
+ avgWin: number;
16
+ };
17
+ /**
18
+ * Calculate portfolio concentration by ticker
19
+ * Returns map of ticker to percentage of portfolio value
20
+ */
21
+ export declare function calculatePortfolioConcentration(positions: Position[], portfolioValue: number): Map<string, number>;
22
+ /**
23
+ * Calculate exposure ratio (total market exposure / portfolio value)
24
+ */
25
+ export declare function calculateExposureRatio(positions: Position[], portfolioValue: number): number;
26
+ /**
27
+ * Group fills by time period (day, week, or month)
28
+ */
29
+ export declare function groupFillsByPeriod(fills: Fill[], period: 'day' | 'month' | 'week'): Map<string, Fill[]>;
30
+ /**
31
+ * Estimate total P&L from fills by matching buys and sells
32
+ * Note: This is an estimate based on fills alone, actual P&L should come from positions
33
+ */
34
+ export declare function estimatePnLFromFills(fills: Fill[]): number;
35
+ /**
36
+ * Calculate total fees paid from fills
37
+ * Fees are calculated based on whether the fill was a maker or taker
38
+ */
39
+ export declare function calculateTotalFees(fills: Fill[]): number;
40
+ /**
41
+ * Group fills by ticker and calculate statistics
42
+ */
43
+ export declare function groupFillsByTicker(fills: Fill[]): Map<string, {
44
+ buyVolume: number;
45
+ count: number;
46
+ estimatedPnL: number;
47
+ sellVolume: number;
48
+ volume: number;
49
+ }>;
50
+ /**
51
+ * Calculate maker vs taker statistics
52
+ */
53
+ export declare function calculateMakerTakerStats(fills: Fill[]): {
54
+ maker: {
55
+ avgFeePct: number;
56
+ fills: number;
57
+ volume: number;
58
+ };
59
+ taker: {
60
+ avgFeePct: number;
61
+ fills: number;
62
+ volume: number;
63
+ };
64
+ };
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Portfolio analytics calculation utilities
3
+ */
4
+ /**
5
+ * Calculate win rate from a set of fills
6
+ * Matches buys and sells for the same ticker to determine wins/losses
7
+ */
8
+ export function calculateWinRate(fills) {
9
+ if (fills.length === 0)
10
+ return 0;
11
+ // Group fills by ticker
12
+ const fillsByTicker = new Map();
13
+ for (const fill of fills) {
14
+ const existing = fillsByTicker.get(fill.ticker) || [];
15
+ existing.push(fill);
16
+ fillsByTicker.set(fill.ticker, existing);
17
+ }
18
+ let winningTrades = 0;
19
+ let losingTrades = 0;
20
+ // For each ticker, match buys and sells to calculate P&L
21
+ for (const [, tickerFills] of fillsByTicker) {
22
+ const sorted = tickerFills.sort((a, b) => new Date(a.created_time).getTime() - new Date(b.created_time).getTime());
23
+ let position = 0;
24
+ let totalCost = 0;
25
+ for (const fill of sorted) {
26
+ const quantity = fill.action === 'buy' ? fill.count : -fill.count;
27
+ const cost = fill.action === 'buy' ? fill.count * fill.price : -fill.count * fill.price;
28
+ if (fill.action === 'sell' && position > 0) {
29
+ // Closing a position - calculate P&L
30
+ const avgEntryPrice = totalCost / position;
31
+ const sellPrice = fill.price;
32
+ const pnl = (sellPrice - avgEntryPrice) * fill.count;
33
+ if (pnl > 0)
34
+ winningTrades++;
35
+ else if (pnl < 0)
36
+ losingTrades++;
37
+ }
38
+ position += quantity;
39
+ totalCost += cost;
40
+ }
41
+ }
42
+ const totalTrades = winningTrades + losingTrades;
43
+ return totalTrades > 0 ? winningTrades / totalTrades : 0;
44
+ }
45
+ /**
46
+ * Calculate average P&L for winning and losing trades
47
+ */
48
+ export function calculateAveragePnL(fills) {
49
+ if (fills.length === 0)
50
+ return { avgLoss: 0, avgWin: 0 };
51
+ // Group fills by ticker
52
+ const fillsByTicker = new Map();
53
+ for (const fill of fills) {
54
+ const existing = fillsByTicker.get(fill.ticker) || [];
55
+ existing.push(fill);
56
+ fillsByTicker.set(fill.ticker, existing);
57
+ }
58
+ const wins = [];
59
+ const losses = [];
60
+ // For each ticker, match buys and sells to calculate P&L
61
+ for (const [, tickerFills] of fillsByTicker) {
62
+ const sorted = tickerFills.sort((a, b) => new Date(a.created_time).getTime() - new Date(b.created_time).getTime());
63
+ let position = 0;
64
+ let totalCost = 0;
65
+ for (const fill of sorted) {
66
+ const quantity = fill.action === 'buy' ? fill.count : -fill.count;
67
+ const cost = fill.action === 'buy' ? fill.count * fill.price : -fill.count * fill.price;
68
+ if (fill.action === 'sell' && position > 0) {
69
+ // Closing a position - calculate P&L
70
+ const avgEntryPrice = totalCost / position;
71
+ const sellPrice = fill.price;
72
+ const pnl = (sellPrice - avgEntryPrice) * fill.count;
73
+ if (pnl > 0)
74
+ wins.push(pnl);
75
+ else if (pnl < 0)
76
+ losses.push(pnl);
77
+ }
78
+ position += quantity;
79
+ totalCost += cost;
80
+ }
81
+ }
82
+ const avgWin = wins.length > 0 ? wins.reduce((sum, w) => sum + w, 0) / wins.length : 0;
83
+ const avgLoss = losses.length > 0 ? losses.reduce((sum, l) => sum + l, 0) / losses.length : 0;
84
+ return { avgLoss, avgWin };
85
+ }
86
+ /**
87
+ * Calculate portfolio concentration by ticker
88
+ * Returns map of ticker to percentage of portfolio value
89
+ */
90
+ export function calculatePortfolioConcentration(positions, portfolioValue) {
91
+ const concentration = new Map();
92
+ if (portfolioValue <= 0)
93
+ return concentration;
94
+ for (const position of positions) {
95
+ const percentage = (position.market_exposure / portfolioValue) * 100;
96
+ concentration.set(position.ticker, percentage);
97
+ }
98
+ return concentration;
99
+ }
100
+ /**
101
+ * Calculate exposure ratio (total market exposure / portfolio value)
102
+ */
103
+ export function calculateExposureRatio(positions, portfolioValue) {
104
+ if (portfolioValue <= 0)
105
+ return 0;
106
+ const totalExposure = positions.reduce((sum, pos) => sum + pos.market_exposure, 0);
107
+ return totalExposure / portfolioValue;
108
+ }
109
+ /**
110
+ * Group fills by time period (day, week, or month)
111
+ */
112
+ export function groupFillsByPeriod(fills, period) {
113
+ const grouped = new Map();
114
+ for (const fill of fills) {
115
+ const date = new Date(fill.created_time);
116
+ let key;
117
+ if (period === 'day') {
118
+ key = date.toISOString().split('T')[0]; // YYYY-MM-DD
119
+ }
120
+ else if (period === 'week') {
121
+ // Get ISO week number
122
+ const onejan = new Date(date.getFullYear(), 0, 1);
123
+ const weekNumber = Math.ceil((((date.getTime() - onejan.getTime()) / 86_400_000) + onejan.getDay() + 1) / 7);
124
+ key = `${date.getFullYear()}-W${weekNumber.toString().padStart(2, '0')}`;
125
+ }
126
+ else {
127
+ // month
128
+ key = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}`;
129
+ }
130
+ const existing = grouped.get(key) || [];
131
+ existing.push(fill);
132
+ grouped.set(key, existing);
133
+ }
134
+ return grouped;
135
+ }
136
+ /**
137
+ * Estimate total P&L from fills by matching buys and sells
138
+ * Note: This is an estimate based on fills alone, actual P&L should come from positions
139
+ */
140
+ export function estimatePnLFromFills(fills) {
141
+ // Group fills by ticker
142
+ const fillsByTicker = new Map();
143
+ for (const fill of fills) {
144
+ const existing = fillsByTicker.get(fill.ticker) || [];
145
+ existing.push(fill);
146
+ fillsByTicker.set(fill.ticker, existing);
147
+ }
148
+ let totalPnL = 0;
149
+ // For each ticker, match buys and sells to calculate P&L
150
+ for (const [, tickerFills] of fillsByTicker) {
151
+ const sorted = tickerFills.sort((a, b) => new Date(a.created_time).getTime() - new Date(b.created_time).getTime());
152
+ let position = 0;
153
+ let totalCost = 0;
154
+ for (const fill of sorted) {
155
+ const quantity = fill.action === 'buy' ? fill.count : -fill.count;
156
+ const cost = fill.action === 'buy' ? fill.count * fill.price : -fill.count * fill.price;
157
+ if (fill.action === 'sell' && position > 0) {
158
+ // Closing a position - calculate P&L
159
+ const avgEntryPrice = totalCost / position;
160
+ const sellPrice = fill.price;
161
+ const pnl = (sellPrice - avgEntryPrice) * fill.count;
162
+ totalPnL += pnl;
163
+ }
164
+ position += quantity;
165
+ totalCost += cost;
166
+ }
167
+ }
168
+ return totalPnL;
169
+ }
170
+ /**
171
+ * Calculate total fees paid from fills
172
+ * Fees are calculated based on whether the fill was a maker or taker
173
+ */
174
+ export function calculateTotalFees(fills) {
175
+ // Kalshi fee structure (approximate):
176
+ // Maker: 0.5% (0.005)
177
+ // Taker: 1.5% (0.015)
178
+ const MAKER_FEE = 0.005;
179
+ const TAKER_FEE = 0.015;
180
+ let totalFees = 0;
181
+ for (const fill of fills) {
182
+ const notionalValue = fill.count * fill.price;
183
+ const feeRate = fill.is_taker ? TAKER_FEE : MAKER_FEE;
184
+ totalFees += notionalValue * feeRate;
185
+ }
186
+ return totalFees;
187
+ }
188
+ /**
189
+ * Group fills by ticker and calculate statistics
190
+ */
191
+ export function groupFillsByTicker(fills) {
192
+ const grouped = new Map();
193
+ const fillsByTicker = new Map();
194
+ for (const fill of fills) {
195
+ const existing = fillsByTicker.get(fill.ticker) || [];
196
+ existing.push(fill);
197
+ fillsByTicker.set(fill.ticker, existing);
198
+ }
199
+ for (const [ticker, tickerFills] of fillsByTicker) {
200
+ const volume = tickerFills.reduce((sum, f) => sum + f.count * f.price, 0);
201
+ const buyVolume = tickerFills.filter(f => f.action === 'buy').reduce((sum, f) => sum + f.count * f.price, 0);
202
+ const sellVolume = tickerFills.filter(f => f.action === 'sell').reduce((sum, f) => sum + f.count * f.price, 0);
203
+ const estimatedPnL = estimatePnLFromFills(tickerFills);
204
+ grouped.set(ticker, {
205
+ buyVolume,
206
+ count: tickerFills.length,
207
+ estimatedPnL,
208
+ sellVolume,
209
+ volume,
210
+ });
211
+ }
212
+ return grouped;
213
+ }
214
+ /**
215
+ * Calculate maker vs taker statistics
216
+ */
217
+ export function calculateMakerTakerStats(fills) {
218
+ const MAKER_FEE = 0.005;
219
+ const TAKER_FEE = 0.015;
220
+ const makerFills = fills.filter(f => !f.is_taker);
221
+ const takerFills = fills.filter(f => f.is_taker);
222
+ const makerVolume = makerFills.reduce((sum, f) => sum + f.count * f.price, 0);
223
+ const takerVolume = takerFills.reduce((sum, f) => sum + f.count * f.price, 0);
224
+ return {
225
+ maker: {
226
+ avgFeePct: MAKER_FEE,
227
+ fills: makerFills.length,
228
+ volume: makerVolume,
229
+ },
230
+ taker: {
231
+ avgFeePct: TAKER_FEE,
232
+ fills: takerFills.length,
233
+ volume: takerVolume,
234
+ },
235
+ };
236
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Risk management calculation utilities
3
+ */
4
+ import type { Market, Position } from './kalshi/types.js';
5
+ export type RiskLevel = 'HIGH' | 'LOW' | 'MEDIUM';
6
+ /**
7
+ * Calculate risk score for a single position (0-100)
8
+ * Higher score = higher risk
9
+ */
10
+ export declare function calculatePositionRiskScore(position: Position, portfolioValue: number, threshold: number): number;
11
+ /**
12
+ * Detect correlated positions by grouping them by event or series
13
+ * Returns map of event/series ticker to positions and total exposure
14
+ */
15
+ export declare function detectCorrelatedPositions(positions: Position[], markets: Market[]): Map<string, {
16
+ exposure: number;
17
+ markets: string[];
18
+ }>;
19
+ /**
20
+ * Suggest maximum safe position size based on portfolio value and risk tolerance
21
+ */
22
+ export declare function suggestMaxPositionSize(balance: number, portfolioValue: number, maxPct: number): number;
23
+ /**
24
+ * Group positions by event, series, or ticker
25
+ */
26
+ export declare function groupPositionsBy(positions: Position[], markets: Market[], groupBy: 'event' | 'series' | 'ticker'): Map<string, {
27
+ positions: Position[];
28
+ totalExposure: number;
29
+ }>;
30
+ /**
31
+ * Calculate overall portfolio risk score (0-100) and categorize risk level
32
+ */
33
+ export declare function calculatePortfolioRiskScore(positions: Position[], portfolioValue: number): {
34
+ level: RiskLevel;
35
+ score: number;
36
+ };
37
+ /**
38
+ * Identify positions that exceed a risk threshold
39
+ */
40
+ export declare function identifyHighRiskPositions(positions: Position[], portfolioValue: number, threshold: number): Position[];
41
+ /**
42
+ * Calculate concentration by grouping
43
+ * Returns sorted array of concentrations
44
+ */
45
+ export declare function calculateConcentrationByGroup(positions: Position[], markets: Market[], portfolioValue: number, groupBy: 'event' | 'series' | 'ticker', threshold: number): Array<{
46
+ alert: boolean;
47
+ exposure: number;
48
+ markets?: string[];
49
+ name: string;
50
+ pct_of_portfolio: number;
51
+ }>;
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Risk management calculation utilities
3
+ */
4
+ /**
5
+ * Calculate risk score for a single position (0-100)
6
+ * Higher score = higher risk
7
+ */
8
+ export function calculatePositionRiskScore(position, portfolioValue, threshold) {
9
+ if (portfolioValue <= 0)
10
+ return 0;
11
+ const concentrationPct = (position.market_exposure / portfolioValue) * 100;
12
+ // Risk increases exponentially as concentration approaches/exceeds threshold
13
+ let score = (concentrationPct / threshold) * 50;
14
+ // Additional risk if position is unrealized loss
15
+ if (position.realized_pnl < 0) {
16
+ const lossRatio = Math.abs(position.realized_pnl) / position.total_cost;
17
+ score += lossRatio * 25;
18
+ }
19
+ return Math.min(100, Math.max(0, score));
20
+ }
21
+ /**
22
+ * Detect correlated positions by grouping them by event or series
23
+ * Returns map of event/series ticker to positions and total exposure
24
+ */
25
+ export function detectCorrelatedPositions(positions, markets) {
26
+ const correlations = new Map();
27
+ // Create a map of ticker to market for quick lookup
28
+ const marketMap = new Map(markets.map(m => [m.ticker, m]));
29
+ // Group positions by event ticker
30
+ for (const position of positions) {
31
+ const market = marketMap.get(position.ticker);
32
+ if (!market)
33
+ continue;
34
+ const eventTicker = market.event_ticker;
35
+ const existing = correlations.get(eventTicker) || { exposure: 0, markets: [] };
36
+ existing.markets.push(position.ticker);
37
+ existing.exposure += position.market_exposure;
38
+ correlations.set(eventTicker, existing);
39
+ }
40
+ // Filter to only events with multiple positions
41
+ const filtered = new Map();
42
+ for (const [eventTicker, data] of correlations) {
43
+ if (data.markets.length > 1) {
44
+ filtered.set(eventTicker, data);
45
+ }
46
+ }
47
+ return filtered;
48
+ }
49
+ /**
50
+ * Suggest maximum safe position size based on portfolio value and risk tolerance
51
+ */
52
+ export function suggestMaxPositionSize(balance, portfolioValue, maxPct) {
53
+ if (portfolioValue <= 0)
54
+ return balance;
55
+ return (portfolioValue * maxPct) / 100;
56
+ }
57
+ /**
58
+ * Group positions by event, series, or ticker
59
+ */
60
+ export function groupPositionsBy(positions, markets, groupBy) {
61
+ const grouped = new Map();
62
+ // Create a map of ticker to market for quick lookup
63
+ const marketMap = new Map(markets.map(m => [m.ticker, m]));
64
+ for (const position of positions) {
65
+ let key;
66
+ if (groupBy === 'ticker') {
67
+ key = position.ticker;
68
+ }
69
+ else {
70
+ const market = marketMap.get(position.ticker);
71
+ if (!market)
72
+ continue;
73
+ key = groupBy === 'event' ? market.event_ticker : market.event_ticker;
74
+ // series - extract from event ticker (format: SERIES-SPECIFIC or just use event_ticker)
75
+ // For now, use event_ticker as series identifier
76
+ }
77
+ const existing = grouped.get(key) || { positions: [], totalExposure: 0 };
78
+ existing.positions.push(position);
79
+ existing.totalExposure += position.market_exposure;
80
+ grouped.set(key, existing);
81
+ }
82
+ return grouped;
83
+ }
84
+ /**
85
+ * Calculate overall portfolio risk score (0-100) and categorize risk level
86
+ */
87
+ export function calculatePortfolioRiskScore(positions, portfolioValue) {
88
+ if (positions.length === 0 || portfolioValue <= 0) {
89
+ return { level: 'LOW', score: 0 };
90
+ }
91
+ // Calculate exposure ratio
92
+ const totalExposure = positions.reduce((sum, pos) => sum + pos.market_exposure, 0);
93
+ const exposureRatio = totalExposure / portfolioValue;
94
+ // Calculate concentration (largest position as % of portfolio)
95
+ const largestExposure = Math.max(...positions.map(pos => pos.market_exposure));
96
+ const largestConcentration = (largestExposure / portfolioValue) * 100;
97
+ // Calculate unrealized loss ratio
98
+ const totalUnrealizedLoss = positions
99
+ .filter(pos => pos.realized_pnl < 0)
100
+ .reduce((sum, pos) => sum + Math.abs(pos.realized_pnl), 0);
101
+ const lossRatio = totalExposure > 0 ? totalUnrealizedLoss / totalExposure : 0;
102
+ // Weighted risk score
103
+ let score = 0;
104
+ // Exposure contributes up to 40 points (0.5 exposure = 20 points, 1.0 = 40 points)
105
+ score += Math.min(40, exposureRatio * 40);
106
+ // Concentration contributes up to 30 points (20% = 15 points, 40% = 30 points)
107
+ score += Math.min(30, (largestConcentration / 40) * 30);
108
+ // Loss ratio contributes up to 30 points
109
+ score += Math.min(30, lossRatio * 100 * 0.3);
110
+ // Categorize risk level
111
+ let level;
112
+ if (score < 30)
113
+ level = 'LOW';
114
+ else if (score < 60)
115
+ level = 'MEDIUM';
116
+ else
117
+ level = 'HIGH';
118
+ return { level, score: Math.round(score) };
119
+ }
120
+ /**
121
+ * Identify positions that exceed a risk threshold
122
+ */
123
+ export function identifyHighRiskPositions(positions, portfolioValue, threshold) {
124
+ if (portfolioValue <= 0)
125
+ return [];
126
+ return positions.filter(pos => {
127
+ const concentrationPct = (pos.market_exposure / portfolioValue) * 100;
128
+ return concentrationPct > threshold;
129
+ });
130
+ }
131
+ /**
132
+ * Calculate concentration by grouping
133
+ * Returns sorted array of concentrations
134
+ */
135
+ // eslint-disable-next-line max-params
136
+ export function calculateConcentrationByGroup(positions, markets, portfolioValue, groupBy, threshold) {
137
+ if (portfolioValue <= 0)
138
+ return [];
139
+ const grouped = groupPositionsBy(positions, markets, groupBy);
140
+ const concentrations = [];
141
+ for (const [name, data] of grouped) {
142
+ const pctOfPortfolio = (data.totalExposure / portfolioValue) * 100;
143
+ concentrations.push({
144
+ alert: pctOfPortfolio > threshold,
145
+ exposure: data.totalExposure,
146
+ markets: groupBy === 'ticker' ? undefined : data.positions.map(p => p.ticker),
147
+ name,
148
+ pct_of_portfolio: pctOfPortfolio,
149
+ });
150
+ }
151
+ // Sort by exposure descending
152
+ return concentrations.sort((a, b) => b.exposure - a.exposure);
153
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Market scanning and opportunity detection utilities
3
+ */
4
+ import type { Market, OrderBook } from './kalshi/types.js';
5
+ export type OpportunityType = 'balanced' | 'high_liquidity' | 'high_volume' | 'wide_spread';
6
+ /**
7
+ * Calculate total liquidity from an orderbook
8
+ */
9
+ export declare function calculateOrderbookLiquidity(orderbook: OrderBook): {
10
+ noTotal: number;
11
+ total: number;
12
+ yesTotal: number;
13
+ };
14
+ /**
15
+ * Calculate bid-ask spread from an orderbook
16
+ *
17
+ * In Kalshi orderbooks:
18
+ * - Lower prices in the yes array are bids (buyers)
19
+ * - Higher prices in the yes array are asks (sellers)
20
+ * - Spread = ask - bid
21
+ */
22
+ export declare function calculateSpread(orderbook: OrderBook): null | {
23
+ spreadCents: number;
24
+ spreadPct: number;
25
+ yesAsk: number;
26
+ yesBid: number;
27
+ };
28
+ /**
29
+ * Score a market based on liquidity, spread, and volume
30
+ * Returns score from 0-100
31
+ */
32
+ export declare function scoreMarket(market: Market, orderbook: OrderBook, weights?: {
33
+ liquidity: number;
34
+ spread: number;
35
+ volume: number;
36
+ }): number;
37
+ /**
38
+ * Classify the type of opportunity a market presents
39
+ */
40
+ export declare function classifyOpportunity(spread: number, liquidity: number, volume: number): OpportunityType;
41
+ /**
42
+ * Generate human-readable reason for opportunity
43
+ */
44
+ export declare function generateOpportunityReason(type: OpportunityType, spread: number, liquidity: number, volume: number): string;
45
+ /**
46
+ * Filter markets based on criteria
47
+ */
48
+ export declare function filterMarketsByCriteria<T extends {
49
+ liquidity: number;
50
+ market: Market;
51
+ spread: null | number;
52
+ volume: number;
53
+ }>(markets: T[], criteria: {
54
+ maxSpread?: number;
55
+ minLiquidity?: number;
56
+ minSpread?: number;
57
+ minVolume?: number;
58
+ }): T[];