@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
|
@@ -1,39 +1,47 @@
|
|
|
1
|
-
import Table from 'cli-table3';
|
|
2
1
|
import chalk from 'chalk';
|
|
2
|
+
import Table from 'cli-table3';
|
|
3
3
|
/**
|
|
4
4
|
* Output formatter for both human and JSON formats
|
|
5
5
|
*/
|
|
6
6
|
export class OutputFormatter {
|
|
7
|
+
command;
|
|
7
8
|
jsonMode;
|
|
8
9
|
startTime;
|
|
9
|
-
command;
|
|
10
10
|
constructor(jsonMode = false, command) {
|
|
11
11
|
this.jsonMode = jsonMode;
|
|
12
12
|
this.startTime = Date.now();
|
|
13
13
|
this.command = command;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
16
|
+
* Create an error response object
|
|
17
17
|
*/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
createErrorResponse(code, message, details) {
|
|
19
|
+
return {
|
|
20
|
+
error: {
|
|
21
|
+
code,
|
|
22
|
+
message,
|
|
23
|
+
...(details && { details }),
|
|
24
|
+
},
|
|
25
|
+
metadata: {
|
|
26
|
+
timestamp: new Date().toISOString(),
|
|
27
|
+
...(this.command && { command: this.command }),
|
|
28
|
+
},
|
|
29
|
+
success: false,
|
|
30
|
+
};
|
|
23
31
|
}
|
|
24
32
|
/**
|
|
25
|
-
*
|
|
33
|
+
* Create a success response object
|
|
26
34
|
*/
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
35
|
+
createSuccessResponse(data) {
|
|
36
|
+
return {
|
|
37
|
+
data,
|
|
38
|
+
metadata: {
|
|
39
|
+
timestamp: new Date().toISOString(),
|
|
40
|
+
...(this.command && { command: this.command }),
|
|
41
|
+
duration_ms: Date.now() - this.startTime,
|
|
42
|
+
},
|
|
43
|
+
success: true,
|
|
44
|
+
};
|
|
37
45
|
}
|
|
38
46
|
/**
|
|
39
47
|
* Create a table for human-readable output
|
|
@@ -42,8 +50,8 @@ export class OutputFormatter {
|
|
|
42
50
|
const table = new Table({
|
|
43
51
|
head: head.map((h) => chalk.cyan(h)),
|
|
44
52
|
style: {
|
|
45
|
-
head: [],
|
|
46
53
|
border: ['gray'],
|
|
54
|
+
head: [],
|
|
47
55
|
},
|
|
48
56
|
});
|
|
49
57
|
for (const row of rows) {
|
|
@@ -51,13 +59,33 @@ export class OutputFormatter {
|
|
|
51
59
|
}
|
|
52
60
|
return table;
|
|
53
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Output an error response
|
|
64
|
+
*/
|
|
65
|
+
error(code, message, details) {
|
|
66
|
+
if (this.jsonMode) {
|
|
67
|
+
this.outputJSON(this.createErrorResponse(code, message, details));
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
console.error(chalk.red(`Error: ${message}`));
|
|
71
|
+
if (details) {
|
|
72
|
+
console.error(chalk.gray(JSON.stringify(details, null, 2)));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Check if JSON mode is enabled
|
|
78
|
+
*/
|
|
79
|
+
isJSONMode() {
|
|
80
|
+
return this.jsonMode;
|
|
81
|
+
}
|
|
54
82
|
/**
|
|
55
83
|
* Output a table
|
|
56
84
|
*/
|
|
57
85
|
outputTable(head, rows) {
|
|
58
86
|
if (this.jsonMode) {
|
|
59
87
|
// In JSON mode, output structured data instead of a table
|
|
60
|
-
const data = rows.map((row) => Object.fromEntries(head.map((h, i) => [h.toLowerCase().
|
|
88
|
+
const data = rows.map((row) => Object.fromEntries(head.map((h, i) => [h.toLowerCase().replaceAll(/\s+/g, '_'), row[i]])));
|
|
61
89
|
this.success(data);
|
|
62
90
|
}
|
|
63
91
|
else {
|
|
@@ -66,35 +94,13 @@ export class OutputFormatter {
|
|
|
66
94
|
}
|
|
67
95
|
}
|
|
68
96
|
/**
|
|
69
|
-
*
|
|
70
|
-
*/
|
|
71
|
-
createSuccessResponse(data) {
|
|
72
|
-
return {
|
|
73
|
-
success: true,
|
|
74
|
-
data,
|
|
75
|
-
metadata: {
|
|
76
|
-
timestamp: new Date().toISOString(),
|
|
77
|
-
...(this.command && { command: this.command }),
|
|
78
|
-
duration_ms: Date.now() - this.startTime,
|
|
79
|
-
},
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Create an error response object
|
|
97
|
+
* Output a success response
|
|
84
98
|
*/
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
message,
|
|
91
|
-
...(details && { details }),
|
|
92
|
-
},
|
|
93
|
-
metadata: {
|
|
94
|
-
timestamp: new Date().toISOString(),
|
|
95
|
-
...(this.command && { command: this.command }),
|
|
96
|
-
},
|
|
97
|
-
};
|
|
99
|
+
success(data) {
|
|
100
|
+
if (this.jsonMode) {
|
|
101
|
+
this.outputJSON(this.createSuccessResponse(data));
|
|
102
|
+
}
|
|
103
|
+
// For human-readable output, let the caller format the data
|
|
98
104
|
}
|
|
99
105
|
/**
|
|
100
106
|
* Output JSON to stdout
|
|
@@ -102,10 +108,4 @@ export class OutputFormatter {
|
|
|
102
108
|
outputJSON(data) {
|
|
103
109
|
console.log(JSON.stringify(data, null, 2));
|
|
104
110
|
}
|
|
105
|
-
/**
|
|
106
|
-
* Check if JSON mode is enabled
|
|
107
|
-
*/
|
|
108
|
-
isJSONMode() {
|
|
109
|
-
return this.jsonMode;
|
|
110
|
-
}
|
|
111
111
|
}
|
package/dist/lib/retry.d.ts
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Retry configuration
|
|
3
3
|
*/
|
|
4
4
|
export interface RetryConfig {
|
|
5
|
-
|
|
5
|
+
backoffMultiplier?: number;
|
|
6
6
|
initialDelayMs?: number;
|
|
7
|
+
maxAttempts?: number;
|
|
7
8
|
maxDelayMs?: number;
|
|
8
|
-
backoffMultiplier?: number;
|
|
9
9
|
retryableErrors?: Array<new (...args: any[]) => Error>;
|
|
10
10
|
}
|
|
11
11
|
/**
|
package/dist/lib/retry.js
CHANGED
|
@@ -4,10 +4,10 @@ import { logger } from './logger.js';
|
|
|
4
4
|
* Default retry configuration
|
|
5
5
|
*/
|
|
6
6
|
const DEFAULT_RETRY_CONFIG = {
|
|
7
|
-
maxAttempts: 3,
|
|
8
|
-
initialDelayMs: 1000, // 1 second
|
|
9
|
-
maxDelayMs: 10000, // 10 seconds
|
|
10
7
|
backoffMultiplier: 2,
|
|
8
|
+
initialDelayMs: 1000, // 1 second
|
|
9
|
+
maxAttempts: 3,
|
|
10
|
+
maxDelayMs: 10_000, // 10 seconds
|
|
11
11
|
retryableErrors: [RateLimitError],
|
|
12
12
|
};
|
|
13
13
|
/**
|
|
@@ -20,7 +20,7 @@ function sleep(ms) {
|
|
|
20
20
|
* Calculate delay with exponential backoff and jitter
|
|
21
21
|
*/
|
|
22
22
|
function calculateDelay(attempt, config) {
|
|
23
|
-
const exponentialDelay = config.initialDelayMs *
|
|
23
|
+
const exponentialDelay = config.initialDelayMs * config.backoffMultiplier ** (attempt - 1);
|
|
24
24
|
const cappedDelay = Math.min(exponentialDelay, config.maxDelayMs);
|
|
25
25
|
// Add jitter (±25% randomization)
|
|
26
26
|
const jitter = cappedDelay * 0.25 * (Math.random() * 2 - 1);
|
|
@@ -40,7 +40,7 @@ export async function retry(fn, config = {}, context) {
|
|
|
40
40
|
let lastError;
|
|
41
41
|
for (let attempt = 1; attempt <= fullConfig.maxAttempts; attempt++) {
|
|
42
42
|
try {
|
|
43
|
-
logger.debug({ attempt, maxAttempts: fullConfig.maxAttempts
|
|
43
|
+
logger.debug({ attempt, context, maxAttempts: fullConfig.maxAttempts }, 'Attempting operation');
|
|
44
44
|
return await fn();
|
|
45
45
|
}
|
|
46
46
|
catch (error) {
|
|
@@ -60,10 +60,10 @@ export async function retry(fn, config = {}, context) {
|
|
|
60
60
|
const delayMs = calculateDelay(attempt, fullConfig);
|
|
61
61
|
logger.info({
|
|
62
62
|
attempt,
|
|
63
|
-
maxAttempts: fullConfig.maxAttempts,
|
|
64
|
-
delayMs,
|
|
65
63
|
context,
|
|
64
|
+
delayMs,
|
|
66
65
|
error: lastError.message,
|
|
66
|
+
maxAttempts: fullConfig.maxAttempts,
|
|
67
67
|
}, 'Operation failed, retrying after delay');
|
|
68
68
|
await sleep(delayMs);
|
|
69
69
|
}
|
|
@@ -75,7 +75,5 @@ export async function retry(fn, config = {}, context) {
|
|
|
75
75
|
* Retry wrapper for async functions
|
|
76
76
|
*/
|
|
77
77
|
export function withRetry(fn, config = {}, context) {
|
|
78
|
-
return ((...args) =>
|
|
79
|
-
return retry(() => fn(...args), config, context || fn.name);
|
|
80
|
-
});
|
|
78
|
+
return ((...args) => retry(() => fn(...args), config, context || fn.name));
|
|
81
79
|
}
|
|
@@ -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
|
+
}>;
|
package/dist/lib/risk.js
ADDED
|
@@ -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
|
+
}
|
package/dist/lib/sanitize.js
CHANGED
|
@@ -14,9 +14,9 @@ export function sanitizeString(input, maxLength = 1000) {
|
|
|
14
14
|
});
|
|
15
15
|
}
|
|
16
16
|
// Remove null bytes (can cause issues in some systems)
|
|
17
|
-
const sanitized = input.
|
|
17
|
+
const sanitized = input.replaceAll('\0', '');
|
|
18
18
|
// Remove control characters except newline, carriage return, and tab
|
|
19
|
-
return sanitized.
|
|
19
|
+
return sanitized.replaceAll(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '');
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
22
22
|
* Sanitize a ticker symbol
|
|
@@ -26,7 +26,7 @@ export function sanitizeTicker(ticker) {
|
|
|
26
26
|
// Ensure uppercase
|
|
27
27
|
const uppercased = sanitized.toUpperCase();
|
|
28
28
|
// Remove any characters that aren't alphanumeric or hyphens
|
|
29
|
-
const cleaned = uppercased.
|
|
29
|
+
const cleaned = uppercased.replaceAll(/[^A-Z0-9-]/g, '');
|
|
30
30
|
if (cleaned !== uppercased) {
|
|
31
31
|
throw new ValidationError('Ticker contains invalid characters', {
|
|
32
32
|
original: ticker,
|
|
@@ -75,14 +75,14 @@ export function sanitizeNumber(input, min, max) {
|
|
|
75
75
|
}
|
|
76
76
|
if (min !== undefined && num < min) {
|
|
77
77
|
throw new ValidationError(`Number is below minimum value of ${min}`, {
|
|
78
|
-
value: num,
|
|
79
78
|
min,
|
|
79
|
+
value: num,
|
|
80
80
|
});
|
|
81
81
|
}
|
|
82
82
|
if (max !== undefined && num > max) {
|
|
83
83
|
throw new ValidationError(`Number exceeds maximum value of ${max}`, {
|
|
84
|
-
value: num,
|
|
85
84
|
max,
|
|
85
|
+
value: num,
|
|
86
86
|
});
|
|
87
87
|
}
|
|
88
88
|
return num;
|
|
@@ -113,12 +113,12 @@ export function sanitizeBoolean(input) {
|
|
|
113
113
|
*/
|
|
114
114
|
export function redactSensitive(input) {
|
|
115
115
|
// Redact potential API keys (long alphanumeric strings)
|
|
116
|
-
let redacted = input.
|
|
116
|
+
let redacted = input.replaceAll(/[a-zA-Z0-9]{32,}/g, '[REDACTED_KEY]');
|
|
117
117
|
// Redact email addresses
|
|
118
|
-
redacted = redacted.
|
|
118
|
+
redacted = redacted.replaceAll(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, '[REDACTED_EMAIL]');
|
|
119
119
|
// Redact potential private keys (PEM format)
|
|
120
|
-
redacted = redacted.
|
|
120
|
+
redacted = redacted.replaceAll(/-----BEGIN [A-Z ]+-----[\s\S]*?-----END [A-Z ]+-----/g, '[REDACTED_KEY]');
|
|
121
121
|
// Redact file paths that might contain sensitive info
|
|
122
|
-
redacted = redacted.
|
|
122
|
+
redacted = redacted.replaceAll(/\/[a-zA-Z0-9_\-./]+\/\.kalshitools\/[a-zA-Z0-9_\-./]+/g, '[REDACTED_PATH]');
|
|
123
123
|
return redacted;
|
|
124
124
|
}
|
|
@@ -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[];
|