@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,12 @@
|
|
|
1
|
+
import { BaseCommand } from '../../lib/base-command.js';
|
|
2
|
+
export default class PortfolioAnalytics extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
'max-ts': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
'min-ts': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
period: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
};
|
|
11
|
+
run(): Promise<void>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { calculateAveragePnL, calculateExposureRatio, calculatePortfolioConcentration, calculateTotalFees, calculateWinRate, } from '../../lib/analytics.js';
|
|
4
|
+
import { BaseCommand } from '../../lib/base-command.js';
|
|
5
|
+
import { createClientFromConfig } from '../../lib/kalshi/index.js';
|
|
6
|
+
import { logger } from '../../lib/logger.js';
|
|
7
|
+
export default class PortfolioAnalytics extends BaseCommand {
|
|
8
|
+
static description = 'Comprehensive portfolio performance analytics';
|
|
9
|
+
static examples = [
|
|
10
|
+
'<%= config.bin %> <%= command.id %>',
|
|
11
|
+
'<%= config.bin %> <%= command.id %> --period 7',
|
|
12
|
+
'<%= config.bin %> <%= command.id %> --min-ts 2026-01-01T00:00:00Z',
|
|
13
|
+
'<%= config.bin %> <%= command.id %> --json',
|
|
14
|
+
];
|
|
15
|
+
static flags = {
|
|
16
|
+
...BaseCommand.baseFlags,
|
|
17
|
+
'max-ts': Flags.string({
|
|
18
|
+
description: 'End date for analysis (ISO-8601 format)',
|
|
19
|
+
}),
|
|
20
|
+
'min-ts': Flags.string({
|
|
21
|
+
description: 'Start date for analysis (ISO-8601 format)',
|
|
22
|
+
}),
|
|
23
|
+
'period': Flags.integer({
|
|
24
|
+
description: 'Analysis period in days (overridden by min-ts/max-ts)',
|
|
25
|
+
}),
|
|
26
|
+
};
|
|
27
|
+
async run() {
|
|
28
|
+
const { flags } = await this.parse(PortfolioAnalytics);
|
|
29
|
+
try {
|
|
30
|
+
const client = createClientFromConfig();
|
|
31
|
+
// Calculate time range
|
|
32
|
+
const now = new Date();
|
|
33
|
+
let minTs;
|
|
34
|
+
let maxTs;
|
|
35
|
+
let periodDays = flags.period || 30;
|
|
36
|
+
if (flags['min-ts']) {
|
|
37
|
+
minTs = Math.floor(new Date(flags['min-ts']).getTime() / 1000);
|
|
38
|
+
}
|
|
39
|
+
else if (flags.period) {
|
|
40
|
+
minTs = Math.floor((now.getTime() - flags.period * 24 * 60 * 60 * 1000) / 1000);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
// Default to 30 days
|
|
44
|
+
minTs = Math.floor((now.getTime() - 30 * 24 * 60 * 60 * 1000) / 1000);
|
|
45
|
+
}
|
|
46
|
+
if (flags['max-ts']) {
|
|
47
|
+
maxTs = Math.floor(new Date(flags['max-ts']).getTime() / 1000);
|
|
48
|
+
}
|
|
49
|
+
// Calculate actual period if we have both timestamps
|
|
50
|
+
if (minTs && maxTs) {
|
|
51
|
+
periodDays = Math.floor((maxTs - minTs) / (24 * 60 * 60));
|
|
52
|
+
}
|
|
53
|
+
else if (minTs) {
|
|
54
|
+
periodDays = Math.floor((now.getTime() / 1000 - minTs) / (24 * 60 * 60));
|
|
55
|
+
}
|
|
56
|
+
// Fetch data from API
|
|
57
|
+
const [balance, positions, fillsResult] = await Promise.all([
|
|
58
|
+
client.getBalance(),
|
|
59
|
+
client.getPositions({ settlement_status: 'open' }),
|
|
60
|
+
client.getFills({ limit: 500, max_ts: maxTs, min_ts: minTs }),
|
|
61
|
+
]);
|
|
62
|
+
const { fills } = fillsResult;
|
|
63
|
+
// Calculate metrics
|
|
64
|
+
const winRate = calculateWinRate(fills);
|
|
65
|
+
const { avgLoss, avgWin } = calculateAveragePnL(fills);
|
|
66
|
+
const totalFees = calculateTotalFees(fills);
|
|
67
|
+
// Portfolio metrics
|
|
68
|
+
const portfolioValue = balance.portfolio_value / 100; // Convert from cents
|
|
69
|
+
const cashBalance = balance.balance / 100;
|
|
70
|
+
const totalCost = positions.reduce((sum, pos) => sum + pos.total_cost, 0);
|
|
71
|
+
const totalExposure = positions.reduce((sum, pos) => sum + pos.market_exposure, 0);
|
|
72
|
+
const realizedPnL = positions.reduce((sum, pos) => sum + pos.realized_pnl, 0);
|
|
73
|
+
const unrealizedPnL = totalExposure - totalCost;
|
|
74
|
+
const exposureRatio = calculateExposureRatio(positions, portfolioValue);
|
|
75
|
+
const concentration = calculatePortfolioConcentration(positions, portfolioValue);
|
|
76
|
+
// Calculate total return
|
|
77
|
+
const initialCapital = portfolioValue - realizedPnL - unrealizedPnL;
|
|
78
|
+
const totalReturnPct = initialCapital > 0 ? ((portfolioValue - initialCapital) / initialCapital) * 100 : 0;
|
|
79
|
+
// Trade statistics
|
|
80
|
+
const totalTrades = fills.length;
|
|
81
|
+
const winningTrades = Math.round(totalTrades * winRate);
|
|
82
|
+
const losingTrades = totalTrades - winningTrades;
|
|
83
|
+
// Concentration by ticker
|
|
84
|
+
const concentrationByTicker = {};
|
|
85
|
+
for (const [ticker, pct] of concentration) {
|
|
86
|
+
concentrationByTicker[ticker] = Math.round(pct * 100) / 100;
|
|
87
|
+
}
|
|
88
|
+
// Find largest position
|
|
89
|
+
const largestPositionPct = concentration.size > 0
|
|
90
|
+
? Math.max(...concentration.values())
|
|
91
|
+
: 0;
|
|
92
|
+
// Fee impact
|
|
93
|
+
const feeImpactPct = portfolioValue > 0 ? (totalFees / portfolioValue) * 100 : 0;
|
|
94
|
+
if (this.formatter.isJSONMode()) {
|
|
95
|
+
this.formatter.success({
|
|
96
|
+
fees: {
|
|
97
|
+
fee_impact_pct: -Math.abs(feeImpactPct),
|
|
98
|
+
total_fees: totalFees,
|
|
99
|
+
},
|
|
100
|
+
period: {
|
|
101
|
+
days: periodDays,
|
|
102
|
+
end: maxTs ? new Date(maxTs * 1000).toISOString() : now.toISOString(),
|
|
103
|
+
start: new Date(minTs * 1000).toISOString(),
|
|
104
|
+
},
|
|
105
|
+
pnl: {
|
|
106
|
+
avg_loss: avgLoss,
|
|
107
|
+
avg_win: avgWin,
|
|
108
|
+
realized_pnl: realizedPnL,
|
|
109
|
+
total_pnl: realizedPnL + unrealizedPnL,
|
|
110
|
+
total_return_pct: totalReturnPct,
|
|
111
|
+
unrealized_pnl: unrealizedPnL,
|
|
112
|
+
},
|
|
113
|
+
portfolio: {
|
|
114
|
+
cash_balance: cashBalance,
|
|
115
|
+
current_value: portfolioValue,
|
|
116
|
+
exposure: totalExposure,
|
|
117
|
+
exposure_ratio: exposureRatio,
|
|
118
|
+
total_cost: totalCost,
|
|
119
|
+
},
|
|
120
|
+
risk: {
|
|
121
|
+
concentration_by_ticker: concentrationByTicker,
|
|
122
|
+
largest_position_pct: largestPositionPct,
|
|
123
|
+
},
|
|
124
|
+
trades: {
|
|
125
|
+
losing: losingTrades,
|
|
126
|
+
total: totalTrades,
|
|
127
|
+
win_rate: winRate,
|
|
128
|
+
winning: winningTrades,
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
// Human-readable output
|
|
134
|
+
this.log(chalk.cyan.bold('Portfolio Analytics'));
|
|
135
|
+
this.log();
|
|
136
|
+
// Period
|
|
137
|
+
this.log(chalk.yellow('Period:'));
|
|
138
|
+
this.log(` ${periodDays} days (${new Date(minTs * 1000).toLocaleDateString()} - ${maxTs ? new Date(maxTs * 1000).toLocaleDateString() : 'now'})`);
|
|
139
|
+
this.log();
|
|
140
|
+
// Trading Performance
|
|
141
|
+
this.log(chalk.yellow('Trading Performance:'));
|
|
142
|
+
this.log(` Total Trades: ${totalTrades}`);
|
|
143
|
+
this.log(` Win Rate: ${chalk.green((winRate * 100).toFixed(1) + '%')} (${winningTrades}W / ${losingTrades}L)`);
|
|
144
|
+
this.log(` Avg Win: ${chalk.green('+$' + avgWin.toFixed(2))}`);
|
|
145
|
+
this.log(` Avg Loss: ${chalk.red('$' + avgLoss.toFixed(2))}`);
|
|
146
|
+
this.log();
|
|
147
|
+
// P&L
|
|
148
|
+
this.log(chalk.yellow('Profit & Loss:'));
|
|
149
|
+
const totalPnL = realizedPnL + unrealizedPnL;
|
|
150
|
+
this.log(` Total P&L: ${totalPnL >= 0 ? chalk.green('+$' + totalPnL.toFixed(2)) : chalk.red('-$' + Math.abs(totalPnL).toFixed(2))}`);
|
|
151
|
+
this.log(` Realized: ${realizedPnL >= 0 ? chalk.green('+$' + realizedPnL.toFixed(2)) : chalk.red('-$' + Math.abs(realizedPnL).toFixed(2))}`);
|
|
152
|
+
this.log(` Unrealized: ${unrealizedPnL >= 0 ? chalk.green('+$' + unrealizedPnL.toFixed(2)) : chalk.red('-$' + Math.abs(unrealizedPnL).toFixed(2))}`);
|
|
153
|
+
this.log(` Total Return: ${totalReturnPct >= 0 ? chalk.green('+' + totalReturnPct.toFixed(2) + '%') : chalk.red(totalReturnPct.toFixed(2) + '%')}`);
|
|
154
|
+
this.log();
|
|
155
|
+
// Portfolio
|
|
156
|
+
this.log(chalk.yellow('Portfolio:'));
|
|
157
|
+
this.log(` Current Value: ${chalk.cyan('$' + portfolioValue.toFixed(2))}`);
|
|
158
|
+
this.log(` Cash Balance: ${chalk.cyan('$' + cashBalance.toFixed(2))}`);
|
|
159
|
+
this.log(` Total Exposure: ${chalk.cyan('$' + totalExposure.toFixed(2))}`);
|
|
160
|
+
this.log(` Exposure Ratio: ${chalk.cyan((exposureRatio * 100).toFixed(1) + '%')}`);
|
|
161
|
+
this.log();
|
|
162
|
+
// Risk
|
|
163
|
+
this.log(chalk.yellow('Risk:'));
|
|
164
|
+
this.log(` Largest Position: ${largestPositionPct >= 20 ? chalk.red(largestPositionPct.toFixed(1) + '%') : chalk.green(largestPositionPct.toFixed(1) + '%')}`);
|
|
165
|
+
if (concentration.size > 0) {
|
|
166
|
+
this.log(` Top Concentrations:`);
|
|
167
|
+
const sorted = [...concentration.entries()]
|
|
168
|
+
.sort((a, b) => b[1] - a[1])
|
|
169
|
+
.slice(0, 5);
|
|
170
|
+
for (const [ticker, pct] of sorted) {
|
|
171
|
+
this.log(` ${ticker}: ${pct >= 20 ? chalk.red(pct.toFixed(1) + '%') : chalk.cyan(pct.toFixed(1) + '%')}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
this.log();
|
|
175
|
+
// Fees
|
|
176
|
+
this.log(chalk.yellow('Fees:'));
|
|
177
|
+
this.log(` Total Fees: ${chalk.red('$' + totalFees.toFixed(2))}`);
|
|
178
|
+
this.log(` Fee Impact: ${chalk.red(feeImpactPct.toFixed(2) + '%')}`);
|
|
179
|
+
}
|
|
180
|
+
logger.info({
|
|
181
|
+
exposure_ratio: exposureRatio,
|
|
182
|
+
period_days: periodDays,
|
|
183
|
+
total_pnl: realizedPnL + unrealizedPnL,
|
|
184
|
+
total_trades: totalTrades,
|
|
185
|
+
win_rate: winRate,
|
|
186
|
+
}, 'Portfolio analytics generated');
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -3,8 +3,8 @@ export default class PortfolioFills extends BaseCommand {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
|
-
ticker: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
6
|
limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
ticker: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
8
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
9
|
};
|
|
10
10
|
run(): Promise<void>;
|
|
@@ -13,12 +13,12 @@ export default class PortfolioFills extends BaseCommand {
|
|
|
13
13
|
];
|
|
14
14
|
static flags = {
|
|
15
15
|
...BaseCommand.baseFlags,
|
|
16
|
-
ticker: Flags.string({
|
|
17
|
-
description: 'Filter by ticker',
|
|
18
|
-
}),
|
|
19
16
|
limit: Flags.integer({
|
|
20
|
-
description: 'Maximum number of fills to return',
|
|
21
17
|
default: 50,
|
|
18
|
+
description: 'Maximum number of fills to return',
|
|
19
|
+
}),
|
|
20
|
+
ticker: Flags.string({
|
|
21
|
+
description: 'Filter by ticker',
|
|
22
22
|
}),
|
|
23
23
|
};
|
|
24
24
|
async run() {
|
|
@@ -28,12 +28,12 @@ export default class PortfolioFills extends BaseCommand {
|
|
|
28
28
|
const client = createClientFromConfig();
|
|
29
29
|
// Fetch fills
|
|
30
30
|
const result = await client.getFills({
|
|
31
|
-
ticker: flags.ticker,
|
|
32
31
|
limit: flags.limit,
|
|
32
|
+
ticker: flags.ticker,
|
|
33
33
|
});
|
|
34
|
-
const fills = result
|
|
34
|
+
const { fills } = result;
|
|
35
35
|
if (this.formatter.isJSONMode()) {
|
|
36
|
-
this.formatter.success({
|
|
36
|
+
this.formatter.success({ cursor: result.cursor, fills });
|
|
37
37
|
}
|
|
38
38
|
else {
|
|
39
39
|
if (fills.length === 0) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { BaseCommand } from '../../lib/base-command.js';
|
|
2
|
+
export default class PortfolioHistory extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
'group-by': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
'max-ts': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
'min-ts': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
ticker: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
};
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { calculateMakerTakerStats, groupFillsByPeriod, groupFillsByTicker, } from '../../lib/analytics.js';
|
|
4
|
+
import { BaseCommand } from '../../lib/base-command.js';
|
|
5
|
+
import { createClientFromConfig } from '../../lib/kalshi/index.js';
|
|
6
|
+
import { logger } from '../../lib/logger.js';
|
|
7
|
+
export default class PortfolioHistory extends BaseCommand {
|
|
8
|
+
static description = 'Historical performance analysis with time-series data';
|
|
9
|
+
static examples = [
|
|
10
|
+
'<%= config.bin %> <%= command.id %>',
|
|
11
|
+
'<%= config.bin %> <%= command.id %> --ticker MARKET-A',
|
|
12
|
+
'<%= config.bin %> <%= command.id %> --group-by week',
|
|
13
|
+
'<%= config.bin %> <%= command.id %> --min-ts 2026-01-01T00:00:00Z --max-ts 2026-02-01T00:00:00Z',
|
|
14
|
+
'<%= config.bin %> <%= command.id %> --json',
|
|
15
|
+
];
|
|
16
|
+
static flags = {
|
|
17
|
+
...BaseCommand.baseFlags,
|
|
18
|
+
'group-by': Flags.string({
|
|
19
|
+
default: 'day',
|
|
20
|
+
description: 'Time aggregation level',
|
|
21
|
+
options: ['day', 'week', 'month'],
|
|
22
|
+
}),
|
|
23
|
+
'limit': Flags.integer({
|
|
24
|
+
default: 500,
|
|
25
|
+
description: 'Maximum fills to fetch (handles pagination)',
|
|
26
|
+
}),
|
|
27
|
+
'max-ts': Flags.string({
|
|
28
|
+
description: 'End date (ISO-8601 format)',
|
|
29
|
+
}),
|
|
30
|
+
'min-ts': Flags.string({
|
|
31
|
+
description: 'Start date (ISO-8601 format)',
|
|
32
|
+
}),
|
|
33
|
+
'ticker': Flags.string({
|
|
34
|
+
description: 'Filter by specific market ticker',
|
|
35
|
+
}),
|
|
36
|
+
};
|
|
37
|
+
async run() {
|
|
38
|
+
const { flags } = await this.parse(PortfolioHistory);
|
|
39
|
+
try {
|
|
40
|
+
const client = createClientFromConfig();
|
|
41
|
+
// Convert timestamps
|
|
42
|
+
const minTs = flags['min-ts'] ? Math.floor(new Date(flags['min-ts']).getTime() / 1000) : undefined;
|
|
43
|
+
const maxTs = flags['max-ts'] ? Math.floor(new Date(flags['max-ts']).getTime() / 1000) : undefined;
|
|
44
|
+
// Fetch fills with pagination
|
|
45
|
+
const allFills = [];
|
|
46
|
+
let cursor;
|
|
47
|
+
let remaining = flags.limit;
|
|
48
|
+
while (remaining > 0) {
|
|
49
|
+
const batchSize = Math.min(remaining, 100); // API limit per request
|
|
50
|
+
const result = await client.getFills({
|
|
51
|
+
cursor,
|
|
52
|
+
limit: batchSize,
|
|
53
|
+
max_ts: maxTs,
|
|
54
|
+
min_ts: minTs,
|
|
55
|
+
ticker: flags.ticker,
|
|
56
|
+
});
|
|
57
|
+
allFills.push(...result.fills);
|
|
58
|
+
remaining -= result.fills.length;
|
|
59
|
+
if (!result.cursor || result.fills.length === 0) {
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
cursor = result.cursor;
|
|
63
|
+
}
|
|
64
|
+
if (allFills.length === 0) {
|
|
65
|
+
if (this.formatter.isJSONMode()) {
|
|
66
|
+
this.formatter.success({
|
|
67
|
+
by_ticker: {},
|
|
68
|
+
maker_taker: {
|
|
69
|
+
maker: { fills: 0, volume: 0 },
|
|
70
|
+
taker: { fills: 0, volume: 0 },
|
|
71
|
+
},
|
|
72
|
+
period: {
|
|
73
|
+
days: 0,
|
|
74
|
+
end: maxTs ? new Date(maxTs * 1000).toISOString() : new Date().toISOString(),
|
|
75
|
+
start: minTs ? new Date(minTs * 1000).toISOString() : new Date().toISOString(),
|
|
76
|
+
},
|
|
77
|
+
summary: {
|
|
78
|
+
buy_volume: 0,
|
|
79
|
+
maker_fills: 0,
|
|
80
|
+
net_volume: 0,
|
|
81
|
+
sell_volume: 0,
|
|
82
|
+
taker_fills: 0,
|
|
83
|
+
total_fees: 0,
|
|
84
|
+
total_fills: 0,
|
|
85
|
+
total_volume: 0,
|
|
86
|
+
},
|
|
87
|
+
time_series: {},
|
|
88
|
+
top_trades: {},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
this.log(chalk.yellow('No fills found for the specified period'));
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
// Calculate period
|
|
97
|
+
const oldestFill = allFills[allFills.length - 1];
|
|
98
|
+
const startDate = minTs ? new Date(minTs * 1000) : new Date(oldestFill.created_time);
|
|
99
|
+
const endDate = maxTs ? new Date(maxTs * 1000) : new Date(allFills[0].created_time);
|
|
100
|
+
const periodDays = Math.ceil((endDate.getTime() - startDate.getTime()) / (24 * 60 * 60 * 1000));
|
|
101
|
+
// Calculate summary statistics
|
|
102
|
+
const totalVolume = allFills.reduce((sum, f) => sum + f.count * f.price, 0);
|
|
103
|
+
const buyVolume = allFills.filter(f => f.action === 'buy').reduce((sum, f) => sum + f.count * f.price, 0);
|
|
104
|
+
const sellVolume = allFills.filter(f => f.action === 'sell').reduce((sum, f) => sum + f.count * f.price, 0);
|
|
105
|
+
const makerFills = allFills.filter(f => !f.is_taker).length;
|
|
106
|
+
const takerFills = allFills.filter(f => f.is_taker).length;
|
|
107
|
+
// Estimate fees
|
|
108
|
+
const MAKER_FEE = 0.005;
|
|
109
|
+
const TAKER_FEE = 0.015;
|
|
110
|
+
const totalFees = allFills.reduce((sum, f) => {
|
|
111
|
+
const notional = f.count * f.price;
|
|
112
|
+
return sum + notional * (f.is_taker ? TAKER_FEE : MAKER_FEE);
|
|
113
|
+
}, 0);
|
|
114
|
+
// Group by ticker
|
|
115
|
+
const byTicker = groupFillsByTicker(allFills);
|
|
116
|
+
const byTickerArray = [...byTicker.entries()].map(([ticker, stats]) => ({
|
|
117
|
+
fills: stats.count,
|
|
118
|
+
pnl: stats.estimatedPnL,
|
|
119
|
+
ticker,
|
|
120
|
+
volume: stats.volume,
|
|
121
|
+
win_rate: 0, // TODO: Calculate from individual trades
|
|
122
|
+
}));
|
|
123
|
+
// Group by time period
|
|
124
|
+
const timeSeries = groupFillsByPeriod(allFills, flags['group-by']);
|
|
125
|
+
const timeSeriesArray = [...timeSeries.entries()]
|
|
126
|
+
.map(([date, fills]) => ({
|
|
127
|
+
date,
|
|
128
|
+
fills: fills.length,
|
|
129
|
+
pnl: 0, // TODO: Calculate P&L for period
|
|
130
|
+
volume: fills.reduce((sum, f) => sum + f.count * f.price, 0),
|
|
131
|
+
}))
|
|
132
|
+
.sort((a, b) => a.date.localeCompare(b.date));
|
|
133
|
+
// Maker vs taker analysis
|
|
134
|
+
const makerTakerStats = calculateMakerTakerStats(allFills);
|
|
135
|
+
// Find top trades (by volume)
|
|
136
|
+
const sortedByVolume = [...allFills].sort((a, b) => b.count * b.price - a.count * a.price);
|
|
137
|
+
const largestTrade = sortedByVolume[0];
|
|
138
|
+
if (this.formatter.isJSONMode()) {
|
|
139
|
+
this.formatter.success({
|
|
140
|
+
by_ticker: Object.fromEntries(byTickerArray.map(t => [t.ticker, {
|
|
141
|
+
fills: t.fills,
|
|
142
|
+
pnl: t.pnl,
|
|
143
|
+
volume: t.volume,
|
|
144
|
+
win_rate: t.win_rate,
|
|
145
|
+
}])),
|
|
146
|
+
maker_taker: makerTakerStats,
|
|
147
|
+
period: {
|
|
148
|
+
days: periodDays,
|
|
149
|
+
end: endDate.toISOString(),
|
|
150
|
+
start: startDate.toISOString(),
|
|
151
|
+
},
|
|
152
|
+
summary: {
|
|
153
|
+
buy_volume: buyVolume,
|
|
154
|
+
maker_fills: makerFills,
|
|
155
|
+
net_volume: sellVolume - buyVolume,
|
|
156
|
+
sell_volume: sellVolume,
|
|
157
|
+
taker_fills: takerFills,
|
|
158
|
+
total_fees: totalFees,
|
|
159
|
+
total_fills: allFills.length,
|
|
160
|
+
total_volume: totalVolume,
|
|
161
|
+
},
|
|
162
|
+
time_series: {
|
|
163
|
+
[flags['group-by']]: timeSeriesArray,
|
|
164
|
+
},
|
|
165
|
+
top_trades: {
|
|
166
|
+
largest_by_volume: largestTrade ? {
|
|
167
|
+
date: largestTrade.created_time,
|
|
168
|
+
ticker: largestTrade.ticker,
|
|
169
|
+
volume: largestTrade.count * largestTrade.price,
|
|
170
|
+
} : null,
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
// Human-readable output
|
|
176
|
+
this.log(chalk.cyan.bold('Historical Performance Analysis'));
|
|
177
|
+
this.log();
|
|
178
|
+
// Period
|
|
179
|
+
this.log(chalk.yellow('Period:'));
|
|
180
|
+
this.log(` ${startDate.toLocaleDateString()} - ${endDate.toLocaleDateString()} (${periodDays} days)`);
|
|
181
|
+
this.log();
|
|
182
|
+
// Summary
|
|
183
|
+
this.log(chalk.yellow('Summary:'));
|
|
184
|
+
this.log(` Total Fills: ${allFills.length}`);
|
|
185
|
+
this.log(` Total Volume: ${chalk.cyan('$' + totalVolume.toFixed(2))}`);
|
|
186
|
+
this.log(` Buy Volume: ${chalk.green('$' + buyVolume.toFixed(2))}`);
|
|
187
|
+
this.log(` Sell Volume: ${chalk.red('$' + sellVolume.toFixed(2))}`);
|
|
188
|
+
this.log(` Net Volume: ${sellVolume - buyVolume >= 0 ? chalk.green('+$' + (sellVolume - buyVolume).toFixed(2)) : chalk.red('-$' + Math.abs(sellVolume - buyVolume).toFixed(2))}`);
|
|
189
|
+
this.log(` Total Fees: ${chalk.red('$' + totalFees.toFixed(2))}`);
|
|
190
|
+
this.log();
|
|
191
|
+
// Maker vs Taker
|
|
192
|
+
this.log(chalk.yellow('Maker vs Taker:'));
|
|
193
|
+
this.log(` Maker Fills: ${makerFills} (${((makerFills / allFills.length) * 100).toFixed(1)}%)`);
|
|
194
|
+
this.log(` Maker Volume: ${chalk.cyan('$' + makerTakerStats.maker.volume.toFixed(2))}`);
|
|
195
|
+
this.log(` Taker Fills: ${takerFills} (${((takerFills / allFills.length) * 100).toFixed(1)}%)`);
|
|
196
|
+
this.log(` Taker Volume: ${chalk.cyan('$' + makerTakerStats.taker.volume.toFixed(2))}`);
|
|
197
|
+
this.log();
|
|
198
|
+
// By Ticker
|
|
199
|
+
if (byTickerArray.length > 0) {
|
|
200
|
+
this.log(chalk.yellow('Performance by Ticker:'));
|
|
201
|
+
const topTickers = byTickerArray
|
|
202
|
+
.sort((a, b) => b.volume - a.volume)
|
|
203
|
+
.slice(0, 10);
|
|
204
|
+
const tickerRows = topTickers.map(t => [
|
|
205
|
+
t.ticker,
|
|
206
|
+
t.fills.toString(),
|
|
207
|
+
'$' + t.volume.toFixed(2),
|
|
208
|
+
t.pnl >= 0 ? chalk.green('+$' + t.pnl.toFixed(2)) : chalk.red('$' + t.pnl.toFixed(2)),
|
|
209
|
+
]);
|
|
210
|
+
this.formatter.outputTable(['Ticker', 'Fills', 'Volume', 'Est. P&L'], tickerRows);
|
|
211
|
+
this.log();
|
|
212
|
+
}
|
|
213
|
+
// Time Series
|
|
214
|
+
this.log(chalk.yellow(`Time Series (${flags['group-by']}ly):`));
|
|
215
|
+
if (timeSeriesArray.length > 0) {
|
|
216
|
+
const recent = timeSeriesArray.slice(-10); // Show last 10 periods
|
|
217
|
+
const timeRows = recent.map(t => [
|
|
218
|
+
t.date,
|
|
219
|
+
t.fills.toString(),
|
|
220
|
+
'$' + t.volume.toFixed(2),
|
|
221
|
+
]);
|
|
222
|
+
this.formatter.outputTable(['Period', 'Fills', 'Volume'], timeRows);
|
|
223
|
+
if (timeSeriesArray.length > 10) {
|
|
224
|
+
this.log();
|
|
225
|
+
this.log(chalk.gray(` ... showing last 10 of ${timeSeriesArray.length} periods`));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
this.log();
|
|
229
|
+
// Top Trades
|
|
230
|
+
if (largestTrade) {
|
|
231
|
+
this.log(chalk.yellow('Top Trades:'));
|
|
232
|
+
this.log(` Largest by Volume: ${chalk.cyan(largestTrade.ticker)} - $${(largestTrade.count * largestTrade.price).toFixed(2)} (${new Date(largestTrade.created_time).toLocaleDateString()})`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
logger.info({
|
|
236
|
+
fills: allFills.length,
|
|
237
|
+
period_days: periodDays,
|
|
238
|
+
total_volume: totalVolume,
|
|
239
|
+
}, 'Historical analysis completed');
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -3,6 +3,7 @@ export default class PortfolioPositions extends BaseCommand {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
|
+
'settlement-status': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
7
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
8
|
};
|
|
8
9
|
run(): Promise<void>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
1
2
|
import chalk from 'chalk';
|
|
2
3
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
3
4
|
import { createClientFromConfig } from '../../lib/kalshi/index.js';
|
|
@@ -6,18 +7,26 @@ export default class PortfolioPositions extends BaseCommand {
|
|
|
6
7
|
static description = 'View current positions with P&L';
|
|
7
8
|
static examples = [
|
|
8
9
|
'<%= config.bin %> <%= command.id %>',
|
|
10
|
+
'<%= config.bin %> <%= command.id %> --settlement-status open',
|
|
11
|
+
'<%= config.bin %> <%= command.id %> --settlement-status settled',
|
|
9
12
|
'<%= config.bin %> <%= command.id %> --json',
|
|
10
13
|
];
|
|
11
14
|
static flags = {
|
|
12
15
|
...BaseCommand.baseFlags,
|
|
16
|
+
'settlement-status': Flags.string({
|
|
17
|
+
description: 'Filter by settlement status',
|
|
18
|
+
options: ['open', 'pending', 'settled'],
|
|
19
|
+
}),
|
|
13
20
|
};
|
|
14
21
|
async run() {
|
|
15
|
-
await this.parse(PortfolioPositions);
|
|
22
|
+
const { flags } = await this.parse(PortfolioPositions);
|
|
16
23
|
try {
|
|
17
24
|
// Create API client from configuration
|
|
18
25
|
const client = createClientFromConfig();
|
|
19
26
|
// Fetch positions
|
|
20
|
-
const positions = await client.getPositions(
|
|
27
|
+
const positions = await client.getPositions({
|
|
28
|
+
settlement_status: flags['settlement-status'],
|
|
29
|
+
});
|
|
21
30
|
if (this.formatter.isJSONMode()) {
|
|
22
31
|
this.formatter.success(positions);
|
|
23
32
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { BaseCommand } from '../../lib/base-command.js';
|
|
2
|
+
export default class PortfolioRisk extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
'group-by': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
threshold: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
};
|
|
10
|
+
run(): Promise<void>;
|
|
11
|
+
}
|