@gaberoo/kalshitools 1.0.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 +666 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +5 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +5 -0
- package/dist/commands/config/init.d.ts +13 -0
- package/dist/commands/config/init.js +89 -0
- package/dist/commands/config/show.d.ts +10 -0
- package/dist/commands/config/show.js +77 -0
- package/dist/commands/markets/list.d.ts +11 -0
- package/dist/commands/markets/list.js +64 -0
- package/dist/commands/markets/show.d.ts +13 -0
- package/dist/commands/markets/show.js +79 -0
- package/dist/commands/orders/cancel.d.ts +14 -0
- package/dist/commands/orders/cancel.js +129 -0
- package/dist/commands/orders/create.d.ts +19 -0
- package/dist/commands/orders/create.js +211 -0
- package/dist/commands/orders/list.d.ts +13 -0
- package/dist/commands/orders/list.js +92 -0
- package/dist/commands/portfolio/balance.d.ts +9 -0
- package/dist/commands/portfolio/balance.js +36 -0
- package/dist/commands/portfolio/fills.d.ts +11 -0
- package/dist/commands/portfolio/fills.js +80 -0
- package/dist/commands/portfolio/positions.d.ts +9 -0
- package/dist/commands/portfolio/positions.js +58 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/base-command.d.ts +13 -0
- package/dist/lib/base-command.js +38 -0
- package/dist/lib/config/manager.d.ts +71 -0
- package/dist/lib/config/manager.js +137 -0
- package/dist/lib/config/schema.d.ts +175 -0
- package/dist/lib/config/schema.js +59 -0
- package/dist/lib/errors/base.d.ts +84 -0
- package/dist/lib/errors/base.js +106 -0
- package/dist/lib/kalshi/auth.d.ts +17 -0
- package/dist/lib/kalshi/auth.js +71 -0
- package/dist/lib/kalshi/client.d.ts +86 -0
- package/dist/lib/kalshi/client.js +228 -0
- package/dist/lib/kalshi/index.d.ts +8 -0
- package/dist/lib/kalshi/index.js +19 -0
- package/dist/lib/kalshi/types.d.ts +155 -0
- package/dist/lib/kalshi/types.js +4 -0
- package/dist/lib/logger.d.ts +9 -0
- package/dist/lib/logger.js +41 -0
- package/dist/lib/output/formatter.d.ts +69 -0
- package/dist/lib/output/formatter.js +111 -0
- package/dist/lib/retry.d.ts +18 -0
- package/dist/lib/retry.js +81 -0
- package/dist/lib/sanitize.d.ts +28 -0
- package/dist/lib/sanitize.js +124 -0
- package/dist/lib/shutdown.d.ts +43 -0
- package/dist/lib/shutdown.js +106 -0
- package/dist/lib/validation.d.ts +37 -0
- package/dist/lib/validation.js +120 -0
- package/oclif.manifest.json +520 -0
- package/package.json +98 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import Table from 'cli-table3';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
/**
|
|
4
|
+
* Output formatter for both human and JSON formats
|
|
5
|
+
*/
|
|
6
|
+
export class OutputFormatter {
|
|
7
|
+
jsonMode;
|
|
8
|
+
startTime;
|
|
9
|
+
command;
|
|
10
|
+
constructor(jsonMode = false, command) {
|
|
11
|
+
this.jsonMode = jsonMode;
|
|
12
|
+
this.startTime = Date.now();
|
|
13
|
+
this.command = command;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Output a success response
|
|
17
|
+
*/
|
|
18
|
+
success(data) {
|
|
19
|
+
if (this.jsonMode) {
|
|
20
|
+
this.outputJSON(this.createSuccessResponse(data));
|
|
21
|
+
}
|
|
22
|
+
// For human-readable output, let the caller format the data
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Output an error response
|
|
26
|
+
*/
|
|
27
|
+
error(code, message, details) {
|
|
28
|
+
if (this.jsonMode) {
|
|
29
|
+
this.outputJSON(this.createErrorResponse(code, message, details));
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
console.error(chalk.red(`Error: ${message}`));
|
|
33
|
+
if (details) {
|
|
34
|
+
console.error(chalk.gray(JSON.stringify(details, null, 2)));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Create a table for human-readable output
|
|
40
|
+
*/
|
|
41
|
+
createTable(head, rows) {
|
|
42
|
+
const table = new Table({
|
|
43
|
+
head: head.map((h) => chalk.cyan(h)),
|
|
44
|
+
style: {
|
|
45
|
+
head: [],
|
|
46
|
+
border: ['gray'],
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
for (const row of rows) {
|
|
50
|
+
table.push(row);
|
|
51
|
+
}
|
|
52
|
+
return table;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Output a table
|
|
56
|
+
*/
|
|
57
|
+
outputTable(head, rows) {
|
|
58
|
+
if (this.jsonMode) {
|
|
59
|
+
// In JSON mode, output structured data instead of a table
|
|
60
|
+
const data = rows.map((row) => Object.fromEntries(head.map((h, i) => [h.toLowerCase().replace(/\s+/g, '_'), row[i]])));
|
|
61
|
+
this.success(data);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
const table = this.createTable(head, rows);
|
|
65
|
+
console.log(table.toString());
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Create a success response object
|
|
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
|
|
84
|
+
*/
|
|
85
|
+
createErrorResponse(code, message, details) {
|
|
86
|
+
return {
|
|
87
|
+
success: false,
|
|
88
|
+
error: {
|
|
89
|
+
code,
|
|
90
|
+
message,
|
|
91
|
+
...(details && { details }),
|
|
92
|
+
},
|
|
93
|
+
metadata: {
|
|
94
|
+
timestamp: new Date().toISOString(),
|
|
95
|
+
...(this.command && { command: this.command }),
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Output JSON to stdout
|
|
101
|
+
*/
|
|
102
|
+
outputJSON(data) {
|
|
103
|
+
console.log(JSON.stringify(data, null, 2));
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Check if JSON mode is enabled
|
|
107
|
+
*/
|
|
108
|
+
isJSONMode() {
|
|
109
|
+
return this.jsonMode;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry configuration
|
|
3
|
+
*/
|
|
4
|
+
export interface RetryConfig {
|
|
5
|
+
maxAttempts?: number;
|
|
6
|
+
initialDelayMs?: number;
|
|
7
|
+
maxDelayMs?: number;
|
|
8
|
+
backoffMultiplier?: number;
|
|
9
|
+
retryableErrors?: Array<new (...args: any[]) => Error>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Retry a function with exponential backoff
|
|
13
|
+
*/
|
|
14
|
+
export declare function retry<T>(fn: () => Promise<T>, config?: RetryConfig, context?: string): Promise<T>;
|
|
15
|
+
/**
|
|
16
|
+
* Retry wrapper for async functions
|
|
17
|
+
*/
|
|
18
|
+
export declare function withRetry<T extends (...args: any[]) => Promise<any>>(fn: T, config?: RetryConfig, context?: string): T;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { RateLimitError } from './errors/base.js';
|
|
2
|
+
import { logger } from './logger.js';
|
|
3
|
+
/**
|
|
4
|
+
* Default retry configuration
|
|
5
|
+
*/
|
|
6
|
+
const DEFAULT_RETRY_CONFIG = {
|
|
7
|
+
maxAttempts: 3,
|
|
8
|
+
initialDelayMs: 1000, // 1 second
|
|
9
|
+
maxDelayMs: 10000, // 10 seconds
|
|
10
|
+
backoffMultiplier: 2,
|
|
11
|
+
retryableErrors: [RateLimitError],
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Sleep for a specified duration
|
|
15
|
+
*/
|
|
16
|
+
function sleep(ms) {
|
|
17
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Calculate delay with exponential backoff and jitter
|
|
21
|
+
*/
|
|
22
|
+
function calculateDelay(attempt, config) {
|
|
23
|
+
const exponentialDelay = config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt - 1);
|
|
24
|
+
const cappedDelay = Math.min(exponentialDelay, config.maxDelayMs);
|
|
25
|
+
// Add jitter (±25% randomization)
|
|
26
|
+
const jitter = cappedDelay * 0.25 * (Math.random() * 2 - 1);
|
|
27
|
+
return Math.floor(cappedDelay + jitter);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Check if error is retryable
|
|
31
|
+
*/
|
|
32
|
+
function isRetryable(error, retryableErrors) {
|
|
33
|
+
return retryableErrors.some((ErrorClass) => error instanceof ErrorClass);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Retry a function with exponential backoff
|
|
37
|
+
*/
|
|
38
|
+
export async function retry(fn, config = {}, context) {
|
|
39
|
+
const fullConfig = { ...DEFAULT_RETRY_CONFIG, ...config };
|
|
40
|
+
let lastError;
|
|
41
|
+
for (let attempt = 1; attempt <= fullConfig.maxAttempts; attempt++) {
|
|
42
|
+
try {
|
|
43
|
+
logger.debug({ attempt, maxAttempts: fullConfig.maxAttempts, context }, 'Attempting operation');
|
|
44
|
+
return await fn();
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
48
|
+
// Check if we should retry
|
|
49
|
+
const shouldRetry = isRetryable(lastError, fullConfig.retryableErrors);
|
|
50
|
+
if (!shouldRetry || attempt >= fullConfig.maxAttempts) {
|
|
51
|
+
logger.warn({
|
|
52
|
+
attempt,
|
|
53
|
+
context,
|
|
54
|
+
error: lastError.message,
|
|
55
|
+
shouldRetry,
|
|
56
|
+
}, 'Operation failed, not retrying');
|
|
57
|
+
throw lastError;
|
|
58
|
+
}
|
|
59
|
+
// Calculate delay and wait
|
|
60
|
+
const delayMs = calculateDelay(attempt, fullConfig);
|
|
61
|
+
logger.info({
|
|
62
|
+
attempt,
|
|
63
|
+
maxAttempts: fullConfig.maxAttempts,
|
|
64
|
+
delayMs,
|
|
65
|
+
context,
|
|
66
|
+
error: lastError.message,
|
|
67
|
+
}, 'Operation failed, retrying after delay');
|
|
68
|
+
await sleep(delayMs);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Should never reach here, but TypeScript needs it
|
|
72
|
+
throw lastError || new Error('Retry failed with unknown error');
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Retry wrapper for async functions
|
|
76
|
+
*/
|
|
77
|
+
export function withRetry(fn, config = {}, context) {
|
|
78
|
+
return ((...args) => {
|
|
79
|
+
return retry(() => fn(...args), config, context || fn.name);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanitize a string to prevent injection attacks
|
|
3
|
+
*/
|
|
4
|
+
export declare function sanitizeString(input: string, maxLength?: number): string;
|
|
5
|
+
/**
|
|
6
|
+
* Sanitize a ticker symbol
|
|
7
|
+
*/
|
|
8
|
+
export declare function sanitizeTicker(ticker: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Sanitize a file path
|
|
11
|
+
*/
|
|
12
|
+
export declare function sanitizePath(path: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Sanitize an email address
|
|
15
|
+
*/
|
|
16
|
+
export declare function sanitizeEmail(email: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Sanitize a number (ensure it's actually a number)
|
|
19
|
+
*/
|
|
20
|
+
export declare function sanitizeNumber(input: unknown, min?: number, max?: number): number;
|
|
21
|
+
/**
|
|
22
|
+
* Sanitize boolean input
|
|
23
|
+
*/
|
|
24
|
+
export declare function sanitizeBoolean(input: unknown): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Redact sensitive information from a string
|
|
27
|
+
*/
|
|
28
|
+
export declare function redactSensitive(input: string): string;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { ValidationError } from './errors/base.js';
|
|
2
|
+
/**
|
|
3
|
+
* Sanitize a string to prevent injection attacks
|
|
4
|
+
*/
|
|
5
|
+
export function sanitizeString(input, maxLength = 1000) {
|
|
6
|
+
if (typeof input !== 'string') {
|
|
7
|
+
throw new ValidationError('Input must be a string');
|
|
8
|
+
}
|
|
9
|
+
// Limit length
|
|
10
|
+
if (input.length > maxLength) {
|
|
11
|
+
throw new ValidationError(`Input exceeds maximum length of ${maxLength}`, {
|
|
12
|
+
length: input.length,
|
|
13
|
+
maxLength,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
// Remove null bytes (can cause issues in some systems)
|
|
17
|
+
const sanitized = input.replace(/\0/g, '');
|
|
18
|
+
// Remove control characters except newline, carriage return, and tab
|
|
19
|
+
return sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Sanitize a ticker symbol
|
|
23
|
+
*/
|
|
24
|
+
export function sanitizeTicker(ticker) {
|
|
25
|
+
const sanitized = sanitizeString(ticker, 100);
|
|
26
|
+
// Ensure uppercase
|
|
27
|
+
const uppercased = sanitized.toUpperCase();
|
|
28
|
+
// Remove any characters that aren't alphanumeric or hyphens
|
|
29
|
+
const cleaned = uppercased.replace(/[^A-Z0-9-]/g, '');
|
|
30
|
+
if (cleaned !== uppercased) {
|
|
31
|
+
throw new ValidationError('Ticker contains invalid characters', {
|
|
32
|
+
original: ticker,
|
|
33
|
+
sanitized: cleaned,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return cleaned;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Sanitize a file path
|
|
40
|
+
*/
|
|
41
|
+
export function sanitizePath(path) {
|
|
42
|
+
const sanitized = sanitizeString(path, 500);
|
|
43
|
+
// Check for path traversal attempts
|
|
44
|
+
if (sanitized.includes('..')) {
|
|
45
|
+
throw new ValidationError('Path contains path traversal sequence', { path });
|
|
46
|
+
}
|
|
47
|
+
// Check for null byte injection
|
|
48
|
+
if (sanitized.includes('\0')) {
|
|
49
|
+
throw new ValidationError('Path contains null byte', { path });
|
|
50
|
+
}
|
|
51
|
+
return sanitized;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Sanitize an email address
|
|
55
|
+
*/
|
|
56
|
+
export function sanitizeEmail(email) {
|
|
57
|
+
const sanitized = sanitizeString(email, 254); // Max email length per RFC
|
|
58
|
+
// Basic email format validation
|
|
59
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
60
|
+
if (!emailRegex.test(sanitized)) {
|
|
61
|
+
throw new ValidationError('Invalid email format', { email: sanitized });
|
|
62
|
+
}
|
|
63
|
+
return sanitized.toLowerCase();
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Sanitize a number (ensure it's actually a number)
|
|
67
|
+
*/
|
|
68
|
+
export function sanitizeNumber(input, min, max) {
|
|
69
|
+
const num = Number(input);
|
|
70
|
+
if (Number.isNaN(num)) {
|
|
71
|
+
throw new ValidationError('Input is not a valid number', { input });
|
|
72
|
+
}
|
|
73
|
+
if (!Number.isFinite(num)) {
|
|
74
|
+
throw new ValidationError('Input is not a finite number', { input });
|
|
75
|
+
}
|
|
76
|
+
if (min !== undefined && num < min) {
|
|
77
|
+
throw new ValidationError(`Number is below minimum value of ${min}`, {
|
|
78
|
+
value: num,
|
|
79
|
+
min,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (max !== undefined && num > max) {
|
|
83
|
+
throw new ValidationError(`Number exceeds maximum value of ${max}`, {
|
|
84
|
+
value: num,
|
|
85
|
+
max,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return num;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Sanitize boolean input
|
|
92
|
+
*/
|
|
93
|
+
export function sanitizeBoolean(input) {
|
|
94
|
+
if (typeof input === 'boolean') {
|
|
95
|
+
return input;
|
|
96
|
+
}
|
|
97
|
+
if (typeof input === 'string') {
|
|
98
|
+
const lower = input.toLowerCase();
|
|
99
|
+
if (lower === 'true' || lower === '1' || lower === 'yes') {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
if (lower === 'false' || lower === '0' || lower === 'no') {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (typeof input === 'number') {
|
|
107
|
+
return Boolean(input);
|
|
108
|
+
}
|
|
109
|
+
throw new ValidationError('Input cannot be converted to boolean', { input });
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Redact sensitive information from a string
|
|
113
|
+
*/
|
|
114
|
+
export function redactSensitive(input) {
|
|
115
|
+
// Redact potential API keys (long alphanumeric strings)
|
|
116
|
+
let redacted = input.replace(/[a-zA-Z0-9]{32,}/g, '[REDACTED_KEY]');
|
|
117
|
+
// Redact email addresses
|
|
118
|
+
redacted = redacted.replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, '[REDACTED_EMAIL]');
|
|
119
|
+
// Redact potential private keys (PEM format)
|
|
120
|
+
redacted = redacted.replace(/-----BEGIN [A-Z ]+-----[\s\S]*?-----END [A-Z ]+-----/g, '[REDACTED_KEY]');
|
|
121
|
+
// Redact file paths that might contain sensitive info
|
|
122
|
+
redacted = redacted.replace(/\/[a-zA-Z0-9_\-./]+\/\.kalshitools\/[a-zA-Z0-9_\-./]+/g, '[REDACTED_PATH]');
|
|
123
|
+
return redacted;
|
|
124
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cleanup function type
|
|
3
|
+
*/
|
|
4
|
+
type CleanupFunction = () => Promise<void> | void;
|
|
5
|
+
/**
|
|
6
|
+
* Graceful shutdown manager
|
|
7
|
+
*/
|
|
8
|
+
declare class ShutdownManager {
|
|
9
|
+
private cleanupFunctions;
|
|
10
|
+
private isShuttingDown;
|
|
11
|
+
private shutdownTimeout;
|
|
12
|
+
constructor();
|
|
13
|
+
/**
|
|
14
|
+
* Register a cleanup function to be called on shutdown
|
|
15
|
+
*/
|
|
16
|
+
registerCleanup(fn: CleanupFunction): void;
|
|
17
|
+
/**
|
|
18
|
+
* Set shutdown timeout
|
|
19
|
+
*/
|
|
20
|
+
setShutdownTimeout(ms: number): void;
|
|
21
|
+
/**
|
|
22
|
+
* Register signal handlers
|
|
23
|
+
*/
|
|
24
|
+
private registerSignalHandlers;
|
|
25
|
+
/**
|
|
26
|
+
* Perform graceful shutdown
|
|
27
|
+
*/
|
|
28
|
+
private shutdown;
|
|
29
|
+
/**
|
|
30
|
+
* Check if shutdown is in progress
|
|
31
|
+
*/
|
|
32
|
+
isShutdownInProgress(): boolean;
|
|
33
|
+
}
|
|
34
|
+
export declare const shutdownManager: ShutdownManager;
|
|
35
|
+
/**
|
|
36
|
+
* Register a cleanup function
|
|
37
|
+
*/
|
|
38
|
+
export declare function onShutdown(fn: CleanupFunction): void;
|
|
39
|
+
/**
|
|
40
|
+
* Check if shutdown is in progress
|
|
41
|
+
*/
|
|
42
|
+
export declare function isShuttingDown(): boolean;
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { logger } from './logger.js';
|
|
2
|
+
/**
|
|
3
|
+
* Graceful shutdown manager
|
|
4
|
+
*/
|
|
5
|
+
class ShutdownManager {
|
|
6
|
+
cleanupFunctions = [];
|
|
7
|
+
isShuttingDown = false;
|
|
8
|
+
shutdownTimeout = 5000; // 5 seconds
|
|
9
|
+
constructor() {
|
|
10
|
+
// Register signal handlers
|
|
11
|
+
this.registerSignalHandlers();
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Register a cleanup function to be called on shutdown
|
|
15
|
+
*/
|
|
16
|
+
registerCleanup(fn) {
|
|
17
|
+
this.cleanupFunctions.push(fn);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Set shutdown timeout
|
|
21
|
+
*/
|
|
22
|
+
setShutdownTimeout(ms) {
|
|
23
|
+
this.shutdownTimeout = ms;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Register signal handlers
|
|
27
|
+
*/
|
|
28
|
+
registerSignalHandlers() {
|
|
29
|
+
// Handle SIGINT (Ctrl+C)
|
|
30
|
+
process.on('SIGINT', () => {
|
|
31
|
+
logger.info('Received SIGINT, initiating graceful shutdown...');
|
|
32
|
+
this.shutdown('SIGINT');
|
|
33
|
+
});
|
|
34
|
+
// Handle SIGTERM (kill)
|
|
35
|
+
process.on('SIGTERM', () => {
|
|
36
|
+
logger.info('Received SIGTERM, initiating graceful shutdown...');
|
|
37
|
+
this.shutdown('SIGTERM');
|
|
38
|
+
});
|
|
39
|
+
// Handle uncaught exceptions
|
|
40
|
+
process.on('uncaughtException', (error) => {
|
|
41
|
+
logger.error({ error }, 'Uncaught exception, initiating shutdown');
|
|
42
|
+
this.shutdown('uncaughtException', 1);
|
|
43
|
+
});
|
|
44
|
+
// Handle unhandled promise rejections
|
|
45
|
+
process.on('unhandledRejection', (reason) => {
|
|
46
|
+
logger.error({ reason }, 'Unhandled promise rejection, initiating shutdown');
|
|
47
|
+
this.shutdown('unhandledRejection', 1);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Perform graceful shutdown
|
|
52
|
+
*/
|
|
53
|
+
async shutdown(signal, exitCode = 0) {
|
|
54
|
+
if (this.isShuttingDown) {
|
|
55
|
+
logger.warn('Shutdown already in progress');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
this.isShuttingDown = true;
|
|
59
|
+
logger.info({ signal, cleanupCount: this.cleanupFunctions.length }, 'Starting graceful shutdown');
|
|
60
|
+
// Set timeout for shutdown
|
|
61
|
+
const shutdownTimer = setTimeout(() => {
|
|
62
|
+
logger.error({ timeout: this.shutdownTimeout }, 'Shutdown timeout exceeded, forcing exit');
|
|
63
|
+
process.exit(exitCode || 1);
|
|
64
|
+
}, this.shutdownTimeout);
|
|
65
|
+
try {
|
|
66
|
+
// Run all cleanup functions
|
|
67
|
+
await Promise.all(this.cleanupFunctions.map(async (fn, index) => {
|
|
68
|
+
try {
|
|
69
|
+
logger.debug({ index }, 'Running cleanup function');
|
|
70
|
+
await fn();
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
logger.error({ error, index }, 'Cleanup function failed');
|
|
74
|
+
}
|
|
75
|
+
}));
|
|
76
|
+
logger.info('Graceful shutdown complete');
|
|
77
|
+
clearTimeout(shutdownTimer);
|
|
78
|
+
process.exit(exitCode);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
logger.error({ error }, 'Error during shutdown');
|
|
82
|
+
clearTimeout(shutdownTimer);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Check if shutdown is in progress
|
|
88
|
+
*/
|
|
89
|
+
isShutdownInProgress() {
|
|
90
|
+
return this.isShuttingDown;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Singleton instance
|
|
94
|
+
export const shutdownManager = new ShutdownManager();
|
|
95
|
+
/**
|
|
96
|
+
* Register a cleanup function
|
|
97
|
+
*/
|
|
98
|
+
export function onShutdown(fn) {
|
|
99
|
+
shutdownManager.registerCleanup(fn);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Check if shutdown is in progress
|
|
103
|
+
*/
|
|
104
|
+
export function isShuttingDown() {
|
|
105
|
+
return shutdownManager.isShutdownInProgress();
|
|
106
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation utilities for trading commands
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Validate ticker format
|
|
6
|
+
* Kalshi tickers are typically uppercase alphanumeric with hyphens
|
|
7
|
+
*/
|
|
8
|
+
export declare function validateTicker(ticker: string): void;
|
|
9
|
+
/**
|
|
10
|
+
* Validate order quantity
|
|
11
|
+
*/
|
|
12
|
+
export declare function validateQuantity(quantity: number, maxOrderSize: number): void;
|
|
13
|
+
/**
|
|
14
|
+
* Validate price (for limit orders)
|
|
15
|
+
* Kalshi prices are between 0 and 1 (representing probabilities)
|
|
16
|
+
*/
|
|
17
|
+
export declare function validatePrice(price: number): void;
|
|
18
|
+
/**
|
|
19
|
+
* Validate side
|
|
20
|
+
*/
|
|
21
|
+
export declare function validateSide(side: string): 'yes' | 'no';
|
|
22
|
+
/**
|
|
23
|
+
* Validate action
|
|
24
|
+
*/
|
|
25
|
+
export declare function validateAction(action: string): 'buy' | 'sell';
|
|
26
|
+
/**
|
|
27
|
+
* Validate order type
|
|
28
|
+
*/
|
|
29
|
+
export declare function validateOrderType(type: string): 'market' | 'limit';
|
|
30
|
+
/**
|
|
31
|
+
* Calculate order cost estimate
|
|
32
|
+
*/
|
|
33
|
+
export declare function estimateOrderCost(side: 'yes' | 'no', action: 'buy' | 'sell', quantity: number, price?: number): {
|
|
34
|
+
min: number;
|
|
35
|
+
max: number;
|
|
36
|
+
estimate: number;
|
|
37
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { ValidationError } from './errors/base.js';
|
|
2
|
+
/**
|
|
3
|
+
* Validation utilities for trading commands
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Validate ticker format
|
|
7
|
+
* Kalshi tickers are typically uppercase alphanumeric with hyphens
|
|
8
|
+
*/
|
|
9
|
+
export function validateTicker(ticker) {
|
|
10
|
+
if (!ticker || ticker.trim().length === 0) {
|
|
11
|
+
throw new ValidationError('Ticker cannot be empty');
|
|
12
|
+
}
|
|
13
|
+
// Basic format check - alphanumeric and hyphens
|
|
14
|
+
if (!/^[A-Z0-9-]+$/.test(ticker)) {
|
|
15
|
+
throw new ValidationError('Ticker must contain only uppercase letters, numbers, and hyphens', {
|
|
16
|
+
ticker,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
if (ticker.length < 3 || ticker.length > 100) {
|
|
20
|
+
throw new ValidationError('Ticker must be between 3 and 100 characters', {
|
|
21
|
+
ticker,
|
|
22
|
+
length: ticker.length,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Validate order quantity
|
|
28
|
+
*/
|
|
29
|
+
export function validateQuantity(quantity, maxOrderSize) {
|
|
30
|
+
if (!Number.isInteger(quantity)) {
|
|
31
|
+
throw new ValidationError('Quantity must be a whole number', { quantity });
|
|
32
|
+
}
|
|
33
|
+
if (quantity <= 0) {
|
|
34
|
+
throw new ValidationError('Quantity must be positive', { quantity });
|
|
35
|
+
}
|
|
36
|
+
if (quantity > maxOrderSize) {
|
|
37
|
+
throw new ValidationError(`Quantity exceeds maximum order size of ${maxOrderSize}`, {
|
|
38
|
+
quantity,
|
|
39
|
+
maxOrderSize,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Validate price (for limit orders)
|
|
45
|
+
* Kalshi prices are between 0 and 1 (representing probabilities)
|
|
46
|
+
*/
|
|
47
|
+
export function validatePrice(price) {
|
|
48
|
+
if (price < 0.01 || price > 0.99) {
|
|
49
|
+
throw new ValidationError('Price must be between 0.01 and 0.99', { price });
|
|
50
|
+
}
|
|
51
|
+
// Check for reasonable precision (2 decimal places)
|
|
52
|
+
const decimalPlaces = (price.toString().split('.')[1] || '').length;
|
|
53
|
+
if (decimalPlaces > 2) {
|
|
54
|
+
throw new ValidationError('Price cannot have more than 2 decimal places', {
|
|
55
|
+
price,
|
|
56
|
+
decimalPlaces,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Validate side
|
|
62
|
+
*/
|
|
63
|
+
export function validateSide(side) {
|
|
64
|
+
const normalized = side.toLowerCase();
|
|
65
|
+
if (normalized !== 'yes' && normalized !== 'no') {
|
|
66
|
+
throw new ValidationError('Side must be either "yes" or "no"', { side });
|
|
67
|
+
}
|
|
68
|
+
return normalized;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Validate action
|
|
72
|
+
*/
|
|
73
|
+
export function validateAction(action) {
|
|
74
|
+
const normalized = action.toLowerCase();
|
|
75
|
+
if (normalized !== 'buy' && normalized !== 'sell') {
|
|
76
|
+
throw new ValidationError('Action must be either "buy" or "sell"', { action });
|
|
77
|
+
}
|
|
78
|
+
return normalized;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Validate order type
|
|
82
|
+
*/
|
|
83
|
+
export function validateOrderType(type) {
|
|
84
|
+
const normalized = type.toLowerCase();
|
|
85
|
+
if (normalized !== 'market' && normalized !== 'limit') {
|
|
86
|
+
throw new ValidationError('Order type must be either "market" or "limit"', { type });
|
|
87
|
+
}
|
|
88
|
+
return normalized;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Calculate order cost estimate
|
|
92
|
+
*/
|
|
93
|
+
export function estimateOrderCost(side, action, quantity, price) {
|
|
94
|
+
// For buy orders, cost is quantity * price
|
|
95
|
+
// For sell orders, cost is negative (you receive funds)
|
|
96
|
+
if (action === 'buy') {
|
|
97
|
+
if (price) {
|
|
98
|
+
// Limit buy - exact price known
|
|
99
|
+
const cost = quantity * price;
|
|
100
|
+
return { min: cost, max: cost, estimate: cost };
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
// Market buy - could fill at any price
|
|
104
|
+
// Estimate mid-range
|
|
105
|
+
const estimate = quantity * 0.5;
|
|
106
|
+
return { min: quantity * 0.01, max: quantity * 0.99, estimate };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
// Sell orders
|
|
111
|
+
if (price) {
|
|
112
|
+
const proceeds = quantity * price;
|
|
113
|
+
return { min: proceeds, max: proceeds, estimate: proceeds };
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
const estimate = quantity * 0.5;
|
|
117
|
+
return { min: quantity * 0.01, max: quantity * 0.99, estimate };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|