@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.
Files changed (57) hide show
  1. package/README.md +666 -0
  2. package/bin/dev.cmd +3 -0
  3. package/bin/dev.js +5 -0
  4. package/bin/run.cmd +3 -0
  5. package/bin/run.js +5 -0
  6. package/dist/commands/config/init.d.ts +13 -0
  7. package/dist/commands/config/init.js +89 -0
  8. package/dist/commands/config/show.d.ts +10 -0
  9. package/dist/commands/config/show.js +77 -0
  10. package/dist/commands/markets/list.d.ts +11 -0
  11. package/dist/commands/markets/list.js +64 -0
  12. package/dist/commands/markets/show.d.ts +13 -0
  13. package/dist/commands/markets/show.js +79 -0
  14. package/dist/commands/orders/cancel.d.ts +14 -0
  15. package/dist/commands/orders/cancel.js +129 -0
  16. package/dist/commands/orders/create.d.ts +19 -0
  17. package/dist/commands/orders/create.js +211 -0
  18. package/dist/commands/orders/list.d.ts +13 -0
  19. package/dist/commands/orders/list.js +92 -0
  20. package/dist/commands/portfolio/balance.d.ts +9 -0
  21. package/dist/commands/portfolio/balance.js +36 -0
  22. package/dist/commands/portfolio/fills.d.ts +11 -0
  23. package/dist/commands/portfolio/fills.js +80 -0
  24. package/dist/commands/portfolio/positions.d.ts +9 -0
  25. package/dist/commands/portfolio/positions.js +58 -0
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.js +1 -0
  28. package/dist/lib/base-command.d.ts +13 -0
  29. package/dist/lib/base-command.js +38 -0
  30. package/dist/lib/config/manager.d.ts +71 -0
  31. package/dist/lib/config/manager.js +137 -0
  32. package/dist/lib/config/schema.d.ts +175 -0
  33. package/dist/lib/config/schema.js +59 -0
  34. package/dist/lib/errors/base.d.ts +84 -0
  35. package/dist/lib/errors/base.js +106 -0
  36. package/dist/lib/kalshi/auth.d.ts +17 -0
  37. package/dist/lib/kalshi/auth.js +71 -0
  38. package/dist/lib/kalshi/client.d.ts +86 -0
  39. package/dist/lib/kalshi/client.js +228 -0
  40. package/dist/lib/kalshi/index.d.ts +8 -0
  41. package/dist/lib/kalshi/index.js +19 -0
  42. package/dist/lib/kalshi/types.d.ts +155 -0
  43. package/dist/lib/kalshi/types.js +4 -0
  44. package/dist/lib/logger.d.ts +9 -0
  45. package/dist/lib/logger.js +41 -0
  46. package/dist/lib/output/formatter.d.ts +69 -0
  47. package/dist/lib/output/formatter.js +111 -0
  48. package/dist/lib/retry.d.ts +18 -0
  49. package/dist/lib/retry.js +81 -0
  50. package/dist/lib/sanitize.d.ts +28 -0
  51. package/dist/lib/sanitize.js +124 -0
  52. package/dist/lib/shutdown.d.ts +43 -0
  53. package/dist/lib/shutdown.js +106 -0
  54. package/dist/lib/validation.d.ts +37 -0
  55. package/dist/lib/validation.js +120 -0
  56. package/oclif.manifest.json +520 -0
  57. 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
+ }