@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.
- package/README.md +328 -27
- package/dist/commands/config/init.js +4 -4
- package/dist/commands/config/show.js +5 -5
- package/dist/commands/markets/list.d.ts +5 -1
- package/dist/commands/markets/list.js +28 -8
- package/dist/commands/markets/orderbook.d.ts +13 -0
- package/dist/commands/markets/orderbook.js +83 -0
- package/dist/commands/markets/scan.d.ts +18 -0
- package/dist/commands/markets/scan.js +237 -0
- package/dist/commands/markets/show.d.ts +3 -3
- package/dist/commands/markets/show.js +7 -7
- package/dist/commands/orders/cancel.d.ts +3 -3
- package/dist/commands/orders/cancel.js +7 -7
- package/dist/commands/orders/create.d.ts +5 -5
- package/dist/commands/orders/create.js +33 -33
- package/dist/commands/orders/list.d.ts +1 -1
- package/dist/commands/orders/list.js +9 -9
- package/dist/commands/portfolio/analytics.d.ts +12 -0
- package/dist/commands/portfolio/analytics.js +192 -0
- package/dist/commands/portfolio/fills.d.ts +1 -1
- package/dist/commands/portfolio/fills.js +7 -7
- package/dist/commands/portfolio/history.d.ts +14 -0
- package/dist/commands/portfolio/history.js +245 -0
- package/dist/commands/portfolio/positions.d.ts +1 -0
- package/dist/commands/portfolio/positions.js +11 -2
- package/dist/commands/portfolio/risk.d.ts +11 -0
- package/dist/commands/portfolio/risk.js +206 -0
- package/dist/lib/analytics.d.ts +64 -0
- package/dist/lib/analytics.js +236 -0
- package/dist/lib/base-command.d.ts +2 -2
- package/dist/lib/base-command.js +8 -8
- package/dist/lib/config/manager.d.ts +25 -25
- package/dist/lib/config/manager.js +51 -51
- package/dist/lib/config/schema.d.ts +11 -11
- package/dist/lib/config/schema.js +6 -6
- package/dist/lib/errors/base.d.ts +10 -10
- package/dist/lib/errors/base.js +7 -7
- package/dist/lib/kalshi/auth.d.ts +4 -4
- package/dist/lib/kalshi/auth.js +24 -24
- package/dist/lib/kalshi/client.d.ts +35 -35
- package/dist/lib/kalshi/client.js +93 -91
- package/dist/lib/kalshi/index.d.ts +1 -1
- package/dist/lib/kalshi/index.js +1 -1
- package/dist/lib/kalshi/types.d.ts +53 -53
- package/dist/lib/logger.js +3 -3
- package/dist/lib/output/formatter.d.ts +20 -20
- package/dist/lib/output/formatter.js +55 -55
- package/dist/lib/retry.d.ts +2 -2
- package/dist/lib/retry.js +8 -10
- package/dist/lib/risk.d.ts +51 -0
- package/dist/lib/risk.js +153 -0
- package/dist/lib/sanitize.js +9 -9
- package/dist/lib/scanner.d.ts +58 -0
- package/dist/lib/scanner.js +160 -0
- package/dist/lib/shutdown.d.ts +4 -4
- package/dist/lib/shutdown.js +7 -7
- package/dist/lib/validation.d.ts +5 -5
- package/dist/lib/validation.js +14 -20
- package/docs/TRADING_STRATEGIES.md +538 -0
- package/oclif.manifest.json +559 -170
- 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
|
-
|
|
10
|
+
protected formatter: OutputFormatter;
|
|
12
11
|
protected catch(error: Error): Promise<unknown>;
|
|
12
|
+
init(): Promise<void>;
|
|
13
13
|
}
|
package/dist/lib/base-command.js
CHANGED
|
@@ -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
|
-
|
|
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({
|
|
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
|
|
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
|
-
*
|
|
18
|
+
* Get the full configuration
|
|
31
19
|
*/
|
|
32
|
-
|
|
20
|
+
getConfig(): Config;
|
|
33
21
|
/**
|
|
34
|
-
*
|
|
22
|
+
* Get the configuration file path
|
|
35
23
|
*/
|
|
36
|
-
|
|
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
|
-
*
|
|
44
|
+
* Check if configuration is initialized
|
|
53
45
|
*/
|
|
54
|
-
|
|
46
|
+
isConfigured(): boolean;
|
|
55
47
|
/**
|
|
56
|
-
*
|
|
48
|
+
* Read private key from file
|
|
57
49
|
*/
|
|
58
|
-
|
|
50
|
+
readPrivateKey(): string;
|
|
59
51
|
/**
|
|
60
52
|
* Reset configuration to defaults
|
|
61
53
|
*/
|
|
62
54
|
reset(): void;
|
|
63
55
|
/**
|
|
64
|
-
*
|
|
56
|
+
* Set a configuration value
|
|
65
57
|
*/
|
|
66
|
-
|
|
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
|