@gaberoo/kalshitools 1.0.2 → 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.
Files changed (61) hide show
  1. package/README.md +328 -27
  2. package/dist/commands/config/init.js +4 -4
  3. package/dist/commands/config/show.js +5 -5
  4. package/dist/commands/markets/list.d.ts +5 -1
  5. package/dist/commands/markets/list.js +28 -8
  6. package/dist/commands/markets/orderbook.d.ts +13 -0
  7. package/dist/commands/markets/orderbook.js +83 -0
  8. package/dist/commands/markets/scan.d.ts +18 -0
  9. package/dist/commands/markets/scan.js +237 -0
  10. package/dist/commands/markets/show.d.ts +3 -3
  11. package/dist/commands/markets/show.js +7 -7
  12. package/dist/commands/orders/cancel.d.ts +3 -3
  13. package/dist/commands/orders/cancel.js +7 -7
  14. package/dist/commands/orders/create.d.ts +5 -5
  15. package/dist/commands/orders/create.js +33 -33
  16. package/dist/commands/orders/list.d.ts +1 -1
  17. package/dist/commands/orders/list.js +9 -9
  18. package/dist/commands/portfolio/analytics.d.ts +12 -0
  19. package/dist/commands/portfolio/analytics.js +192 -0
  20. package/dist/commands/portfolio/fills.d.ts +1 -1
  21. package/dist/commands/portfolio/fills.js +7 -7
  22. package/dist/commands/portfolio/history.d.ts +14 -0
  23. package/dist/commands/portfolio/history.js +245 -0
  24. package/dist/commands/portfolio/positions.d.ts +1 -0
  25. package/dist/commands/portfolio/positions.js +11 -2
  26. package/dist/commands/portfolio/risk.d.ts +11 -0
  27. package/dist/commands/portfolio/risk.js +206 -0
  28. package/dist/lib/analytics.d.ts +64 -0
  29. package/dist/lib/analytics.js +236 -0
  30. package/dist/lib/base-command.d.ts +2 -2
  31. package/dist/lib/base-command.js +8 -8
  32. package/dist/lib/config/manager.d.ts +25 -25
  33. package/dist/lib/config/manager.js +51 -51
  34. package/dist/lib/config/schema.d.ts +11 -11
  35. package/dist/lib/config/schema.js +6 -6
  36. package/dist/lib/errors/base.d.ts +10 -10
  37. package/dist/lib/errors/base.js +7 -7
  38. package/dist/lib/kalshi/auth.d.ts +4 -4
  39. package/dist/lib/kalshi/auth.js +24 -24
  40. package/dist/lib/kalshi/client.d.ts +35 -35
  41. package/dist/lib/kalshi/client.js +93 -91
  42. package/dist/lib/kalshi/index.d.ts +1 -1
  43. package/dist/lib/kalshi/index.js +1 -1
  44. package/dist/lib/kalshi/types.d.ts +53 -53
  45. package/dist/lib/logger.js +3 -3
  46. package/dist/lib/output/formatter.d.ts +20 -20
  47. package/dist/lib/output/formatter.js +55 -55
  48. package/dist/lib/retry.d.ts +2 -2
  49. package/dist/lib/retry.js +8 -10
  50. package/dist/lib/risk.d.ts +51 -0
  51. package/dist/lib/risk.js +153 -0
  52. package/dist/lib/sanitize.js +9 -9
  53. package/dist/lib/scanner.d.ts +58 -0
  54. package/dist/lib/scanner.js +160 -0
  55. package/dist/lib/shutdown.d.ts +4 -4
  56. package/dist/lib/shutdown.js +7 -7
  57. package/dist/lib/validation.d.ts +5 -5
  58. package/dist/lib/validation.js +14 -20
  59. package/docs/TRADING_STRATEGIES.md +538 -0
  60. package/oclif.manifest.json +559 -170
  61. package/package.json +1 -1
@@ -0,0 +1,206 @@
1
+ import { Flags } from '@oclif/core';
2
+ import chalk from 'chalk';
3
+ import { BaseCommand } from '../../lib/base-command.js';
4
+ import { createClientFromConfig } from '../../lib/kalshi/index.js';
5
+ import { logger } from '../../lib/logger.js';
6
+ import { calculateConcentrationByGroup, calculatePortfolioRiskScore, detectCorrelatedPositions, identifyHighRiskPositions, suggestMaxPositionSize, } from '../../lib/risk.js';
7
+ export default class PortfolioRisk extends BaseCommand {
8
+ static description = 'Real-time portfolio risk analysis and warnings';
9
+ static examples = [
10
+ '<%= config.bin %> <%= command.id %>',
11
+ '<%= config.bin %> <%= command.id %> --threshold 15',
12
+ '<%= config.bin %> <%= command.id %> --group-by event',
13
+ '<%= config.bin %> <%= command.id %> --json',
14
+ ];
15
+ static flags = {
16
+ ...BaseCommand.baseFlags,
17
+ 'group-by': Flags.string({
18
+ default: 'ticker',
19
+ description: 'Grouping for concentration analysis',
20
+ options: ['event', 'series', 'ticker'],
21
+ }),
22
+ 'threshold': Flags.integer({
23
+ default: 20,
24
+ description: 'Alert threshold for position concentration (%)',
25
+ }),
26
+ };
27
+ async run() {
28
+ const { flags } = await this.parse(PortfolioRisk);
29
+ try {
30
+ const client = createClientFromConfig();
31
+ // Fetch data
32
+ const [balance, positions] = await Promise.all([
33
+ client.getBalance(),
34
+ client.getPositions({ settlement_status: 'open' }),
35
+ ]);
36
+ if (positions.length === 0) {
37
+ if (this.formatter.isJSONMode()) {
38
+ this.formatter.success({
39
+ alerts: [],
40
+ concentrations: { by_event: [], by_ticker: [] },
41
+ correlations: [],
42
+ exposure_ratio: 0,
43
+ position_count: 0,
44
+ recommendations: {
45
+ max_new_position_size: balance.balance / 100,
46
+ max_single_position_pct: flags.threshold,
47
+ },
48
+ risk_level: 'LOW',
49
+ risk_score: 0,
50
+ total_exposure: 0,
51
+ });
52
+ }
53
+ else {
54
+ this.log(chalk.yellow('No open positions'));
55
+ }
56
+ return;
57
+ }
58
+ const portfolioValue = balance.portfolio_value / 100;
59
+ // Fetch market details for all positions to enable grouping
60
+ const tickers = positions.map(p => p.ticker).join(',');
61
+ const marketsResult = await client.getMarkets({ limit: positions.length, tickers });
62
+ const { markets } = marketsResult;
63
+ // Calculate risk metrics
64
+ const totalExposure = positions.reduce((sum, pos) => sum + pos.market_exposure, 0);
65
+ const exposureRatio = portfolioValue > 0 ? totalExposure / portfolioValue : 0;
66
+ const riskScore = calculatePortfolioRiskScore(positions, portfolioValue);
67
+ const highRiskPositions = identifyHighRiskPositions(positions, portfolioValue, flags.threshold);
68
+ // Concentration analysis
69
+ const concentrationByTicker = calculateConcentrationByGroup(positions, markets, portfolioValue, 'ticker', flags.threshold);
70
+ const concentrationByEvent = calculateConcentrationByGroup(positions, markets, portfolioValue, 'event', flags.threshold);
71
+ // Detect correlations
72
+ const correlations = detectCorrelatedPositions(positions, markets);
73
+ const correlationWarnings = [...correlations.entries()].map(([event, data]) => ({
74
+ event,
75
+ markets: data.markets,
76
+ total_exposure: data.exposure,
77
+ warning: 'Multiple positions in same event',
78
+ }));
79
+ // Identify alerts
80
+ const alerts = [];
81
+ if (riskScore.level === 'HIGH') {
82
+ alerts.push({
83
+ message: `High portfolio risk score: ${riskScore.score}/100`,
84
+ severity: 'high',
85
+ });
86
+ }
87
+ for (const pos of highRiskPositions) {
88
+ const pct = (pos.market_exposure / portfolioValue) * 100;
89
+ alerts.push({
90
+ message: `${pos.ticker} exceeds threshold: ${pct.toFixed(1)}% of portfolio`,
91
+ severity: pct > flags.threshold * 1.5 ? 'high' : 'medium',
92
+ });
93
+ }
94
+ if (exposureRatio > 0.8) {
95
+ alerts.push({
96
+ message: `High exposure ratio: ${(exposureRatio * 100).toFixed(1)}%`,
97
+ severity: exposureRatio > 1 ? 'high' : 'medium',
98
+ });
99
+ }
100
+ // Recommendations
101
+ const maxNewPositionSize = suggestMaxPositionSize(balance.balance / 100, portfolioValue, flags.threshold);
102
+ if (this.formatter.isJSONMode()) {
103
+ this.formatter.success({
104
+ alerts,
105
+ concentrations: {
106
+ by_event: concentrationByEvent,
107
+ by_ticker: concentrationByTicker,
108
+ },
109
+ correlations: correlationWarnings,
110
+ exposure_ratio: exposureRatio,
111
+ position_count: positions.length,
112
+ recommendations: {
113
+ max_new_position_size: maxNewPositionSize,
114
+ max_single_position_pct: flags.threshold,
115
+ },
116
+ risk_level: riskScore.level,
117
+ risk_score: riskScore.score,
118
+ total_exposure: totalExposure,
119
+ });
120
+ }
121
+ else {
122
+ // Human-readable output
123
+ this.log(chalk.cyan.bold('Portfolio Risk Analysis'));
124
+ this.log();
125
+ // Overall Risk
126
+ const riskLevelColor = riskScore.level === 'HIGH' ? chalk.red : riskScore.level === 'MEDIUM' ? chalk.yellow : chalk.green;
127
+ this.log(chalk.yellow('Overall Risk:'));
128
+ this.log(` Risk Score: ${riskLevelColor(riskScore.score + '/100')}`);
129
+ this.log(` Risk Level: ${riskLevelColor(riskScore.level)}`);
130
+ this.log(` Total Exposure: ${chalk.cyan('$' + totalExposure.toFixed(2))}`);
131
+ this.log(` Exposure Ratio: ${exposureRatio > 0.8 ? chalk.red((exposureRatio * 100).toFixed(1) + '%') : chalk.cyan((exposureRatio * 100).toFixed(1) + '%')}`);
132
+ this.log(` Positions: ${positions.length}`);
133
+ this.log();
134
+ // Alerts
135
+ if (alerts.length > 0) {
136
+ this.log(chalk.yellow('⚠️ Alerts:'));
137
+ for (const alert of alerts) {
138
+ const color = alert.severity === 'high' ? chalk.red : alert.severity === 'medium' ? chalk.yellow : chalk.blue;
139
+ this.log(` ${color('•')} ${alert.message}`);
140
+ }
141
+ this.log();
142
+ }
143
+ // Concentration
144
+ this.log(chalk.yellow('Concentration by Ticker:'));
145
+ const topConcentrations = concentrationByTicker.slice(0, 10);
146
+ if (topConcentrations.length > 0) {
147
+ const rows = topConcentrations.map(c => [
148
+ c.name,
149
+ '$' + c.exposure.toFixed(2),
150
+ c.pct_of_portfolio.toFixed(1) + '%',
151
+ c.alert ? chalk.red('⚠️ ALERT') : chalk.green('OK'),
152
+ ]);
153
+ this.formatter.outputTable(['Ticker', 'Exposure', '% of Portfolio', 'Status'], rows);
154
+ }
155
+ else {
156
+ this.log(' No positions');
157
+ }
158
+ this.log();
159
+ // Event concentration
160
+ if (concentrationByEvent.length > 0) {
161
+ this.log(chalk.yellow('Concentration by Event:'));
162
+ const topEvents = concentrationByEvent.slice(0, 5);
163
+ const eventRows = topEvents.map(c => [
164
+ c.name,
165
+ (c.markets || []).length.toString() + ' markets',
166
+ '$' + c.exposure.toFixed(2),
167
+ c.pct_of_portfolio.toFixed(1) + '%',
168
+ c.alert ? chalk.red('⚠️ ALERT') : chalk.green('OK'),
169
+ ]);
170
+ this.formatter.outputTable(['Event', 'Markets', 'Exposure', '% of Portfolio', 'Status'], eventRows);
171
+ this.log();
172
+ }
173
+ // Correlations
174
+ if (correlationWarnings.length > 0) {
175
+ this.log(chalk.yellow('⚠️ Correlation Warnings:'));
176
+ for (const corr of correlationWarnings) {
177
+ this.log(` ${chalk.cyan(corr.event)}: ${corr.markets.length} positions, $${corr.total_exposure.toFixed(2)} total exposure`);
178
+ this.log(` Markets: ${corr.markets.join(', ')}`);
179
+ }
180
+ this.log();
181
+ }
182
+ // Recommendations
183
+ this.log(chalk.yellow('Recommendations:'));
184
+ this.log(` Max New Position Size: ${chalk.cyan('$' + maxNewPositionSize.toFixed(2))} (${flags.threshold}% of portfolio)`);
185
+ this.log(` Position Size Threshold: ${chalk.cyan(flags.threshold + '%')}`);
186
+ if (riskScore.level === 'HIGH') {
187
+ this.log();
188
+ this.log(chalk.red('⚠️ Consider reducing exposure or diversifying positions'));
189
+ }
190
+ else if (riskScore.level === 'MEDIUM') {
191
+ this.log();
192
+ this.log(chalk.yellow('ℹ️ Monitor positions and avoid over-concentration'));
193
+ }
194
+ }
195
+ logger.info({
196
+ alerts: alerts.length,
197
+ correlations: correlationWarnings.length,
198
+ risk_level: riskScore.level,
199
+ risk_score: riskScore.score,
200
+ }, 'Risk analysis completed');
201
+ }
202
+ catch (error) {
203
+ throw error;
204
+ }
205
+ }
206
+ }
@@ -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
+ }
@@ -4,10 +4,10 @@ import { OutputFormatter } from './output/formatter.js';
4
4
  * Base command class that all kalshitools commands extend
5
5
  */
6
6
  export declare abstract class BaseCommand extends Command {
7
- protected formatter: OutputFormatter;
8
7
  static baseFlags: {
9
8
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
9
  };
11
- init(): Promise<void>;
10
+ protected formatter: OutputFormatter;
12
11
  protected catch(error: Error): Promise<unknown>;
12
+ init(): Promise<void>;
13
13
  }
@@ -6,20 +6,15 @@ import { OutputFormatter } from './output/formatter.js';
6
6
  * Base command class that all kalshitools commands extend
7
7
  */
8
8
  export class BaseCommand extends Command {
9
- formatter;
10
9
  static baseFlags = {
11
10
  json: Flags.boolean({
12
- description: 'Output in JSON format',
13
11
  default: false,
12
+ description: 'Output in JSON format',
14
13
  }),
15
14
  };
16
- async init() {
17
- await super.init();
18
- const { flags } = await this.parse(this.constructor);
19
- this.formatter = new OutputFormatter(flags.json, this.id);
20
- }
15
+ formatter;
21
16
  async catch(error) {
22
- logger.error({ error, command: this.id }, 'Command failed');
17
+ logger.error({ command: this.id, error }, 'Command failed');
23
18
  if (error instanceof KalshiToolsError) {
24
19
  if (this.formatter?.isJSONMode()) {
25
20
  this.formatter.error(error.code, error.message, error.details);
@@ -35,4 +30,9 @@ export class BaseCommand extends Command {
35
30
  }
36
31
  throw error;
37
32
  }
33
+ async init() {
34
+ await super.init();
35
+ const { flags } = await this.parse(this.constructor);
36
+ this.formatter = new OutputFormatter(flags.json, this.id);
37
+ }
38
38
  }
@@ -1,45 +1,37 @@
1
- import { type Config, type ApiEnvConfig } from './schema.js';
1
+ import { type ApiEnvConfig, type Config } from './schema.js';
2
2
  /**
3
3
  * Configuration manager for kalshitools
4
4
  */
5
5
  export declare class ConfigManager {
6
- private store;
7
6
  private static instance;
7
+ private store;
8
8
  constructor();
9
9
  /**
10
10
  * Get singleton instance
11
11
  */
12
12
  static getInstance(): ConfigManager;
13
- /**
14
- * Get the full configuration
15
- */
16
- getConfig(): Config;
17
- /**
18
- * Get the current environment (demo or production)
19
- */
20
- getEnvironment(): 'demo' | 'production';
21
- /**
22
- * Set the environment
23
- */
24
- setEnvironment(env: 'demo' | 'production'): void;
25
13
  /**
26
14
  * Get API configuration for the current environment
27
15
  */
28
16
  getApiConfig(): ApiEnvConfig;
29
17
  /**
30
- * Read private key from file
18
+ * Get the full configuration
31
19
  */
32
- readPrivateKey(): string;
20
+ getConfig(): Config;
33
21
  /**
34
- * Set API configuration for an environment
22
+ * Get the configuration file path
35
23
  */
36
- setApiConfig(env: 'demo' | 'production', config: Partial<ApiEnvConfig>): void;
24
+ getConfigPath(): string;
25
+ /**
26
+ * Get the current environment (demo or production)
27
+ */
28
+ getEnvironment(): 'demo' | 'production';
37
29
  /**
38
30
  * Get output configuration
39
31
  */
40
32
  getOutputConfig(): {
41
- defaultFormat: "json" | "table";
42
33
  colors: boolean;
34
+ defaultFormat: "json" | "table";
43
35
  };
44
36
  /**
45
37
  * Get trading configuration
@@ -49,21 +41,29 @@ export declare class ConfigManager {
49
41
  maxOrderSize: number;
50
42
  };
51
43
  /**
52
- * Set a configuration value
44
+ * Check if configuration is initialized
53
45
  */
54
- set<K extends keyof Config>(key: K, value: Config[K]): void;
46
+ isConfigured(): boolean;
55
47
  /**
56
- * Get the configuration file path
48
+ * Read private key from file
57
49
  */
58
- getConfigPath(): string;
50
+ readPrivateKey(): string;
59
51
  /**
60
52
  * Reset configuration to defaults
61
53
  */
62
54
  reset(): void;
63
55
  /**
64
- * Check if configuration is initialized
56
+ * Set a configuration value
65
57
  */
66
- isConfigured(): boolean;
58
+ set<K extends keyof Config>(key: K, value: Config[K]): void;
59
+ /**
60
+ * Set API configuration for an environment
61
+ */
62
+ setApiConfig(env: 'demo' | 'production', config: Partial<ApiEnvConfig>): void;
63
+ /**
64
+ * Set the environment
65
+ */
66
+ setEnvironment(env: 'demo' | 'production'): void;
67
67
  }
68
68
  /**
69
69
  * Get the global configuration manager instance