@gaberoo/kalshitools 1.0.1 → 1.0.3

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 (47) hide show
  1. package/README.md +91 -27
  2. package/dist/commands/config/init.js +4 -4
  3. package/dist/commands/config/show.js +5 -5
  4. package/dist/commands/markets/list.d.ts +5 -1
  5. package/dist/commands/markets/list.js +28 -8
  6. package/dist/commands/markets/orderbook.d.ts +13 -0
  7. package/dist/commands/markets/orderbook.js +83 -0
  8. package/dist/commands/markets/show.d.ts +3 -3
  9. package/dist/commands/markets/show.js +7 -7
  10. package/dist/commands/orders/cancel.d.ts +3 -3
  11. package/dist/commands/orders/cancel.js +7 -7
  12. package/dist/commands/orders/create.d.ts +5 -5
  13. package/dist/commands/orders/create.js +33 -33
  14. package/dist/commands/orders/list.d.ts +1 -1
  15. package/dist/commands/orders/list.js +9 -9
  16. package/dist/commands/portfolio/balance.js +11 -6
  17. package/dist/commands/portfolio/fills.d.ts +1 -1
  18. package/dist/commands/portfolio/fills.js +7 -7
  19. package/dist/commands/portfolio/positions.d.ts +1 -0
  20. package/dist/commands/portfolio/positions.js +11 -2
  21. package/dist/lib/base-command.d.ts +2 -2
  22. package/dist/lib/base-command.js +8 -8
  23. package/dist/lib/config/manager.d.ts +25 -25
  24. package/dist/lib/config/manager.js +51 -51
  25. package/dist/lib/config/schema.d.ts +11 -11
  26. package/dist/lib/config/schema.js +6 -6
  27. package/dist/lib/errors/base.d.ts +10 -10
  28. package/dist/lib/errors/base.js +7 -7
  29. package/dist/lib/kalshi/auth.d.ts +4 -4
  30. package/dist/lib/kalshi/auth.js +24 -24
  31. package/dist/lib/kalshi/client.d.ts +35 -35
  32. package/dist/lib/kalshi/client.js +101 -86
  33. package/dist/lib/kalshi/index.d.ts +1 -1
  34. package/dist/lib/kalshi/index.js +1 -1
  35. package/dist/lib/kalshi/types.d.ts +54 -54
  36. package/dist/lib/logger.js +3 -3
  37. package/dist/lib/output/formatter.d.ts +20 -20
  38. package/dist/lib/output/formatter.js +55 -55
  39. package/dist/lib/retry.d.ts +2 -2
  40. package/dist/lib/retry.js +8 -10
  41. package/dist/lib/sanitize.js +9 -9
  42. package/dist/lib/shutdown.d.ts +4 -4
  43. package/dist/lib/shutdown.js +7 -7
  44. package/dist/lib/validation.d.ts +5 -5
  45. package/dist/lib/validation.js +14 -20
  46. package/oclif.manifest.json +138 -47
  47. package/package.json +1 -1
@@ -25,30 +25,35 @@ const WRITE_LIMITER = new Bottleneck({
25
25
  * Kalshi API client
26
26
  */
27
27
  export class KalshiClient {
28
- axios;
29
28
  auth;
29
+ axios;
30
30
  baseUrl;
31
31
  constructor(baseUrl, keyId, privateKey) {
32
32
  this.baseUrl = baseUrl;
33
33
  this.auth = new KalshiAuth(keyId, privateKey);
34
34
  this.axios = axios.create({
35
35
  baseURL: baseUrl,
36
- timeout: 30000, // 30 second default timeout
37
36
  headers: {
38
- 'Content-Type': 'application/json',
39
37
  'Accept': 'application/json',
38
+ 'Content-Type': 'application/json',
40
39
  },
40
+ timeout: 30_000, // 30 second default timeout
41
41
  });
42
42
  // Add request interceptor for authentication
43
43
  this.axios.interceptors.request.use((config) => {
44
44
  const method = (config.method || 'GET').toUpperCase();
45
- const path = config.url || '/';
46
- const authHeaders = this.auth.generateAuthHeaders(method, path);
45
+ // CRITICAL: Include baseURL pathname in signature
46
+ // Kalshi expects signature over full path: /trade-api/v2/portfolio/balance
47
+ // Not just the relative path: /portfolio/balance
48
+ const baseUrlPath = new URL(baseUrl).pathname;
49
+ const relativePath = config.url || '/';
50
+ const fullPath = baseUrlPath + relativePath;
51
+ const authHeaders = this.auth.generateAuthHeaders(method, fullPath);
47
52
  // Set auth headers
48
53
  for (const [key, value] of Object.entries(authHeaders)) {
49
54
  config.headers.set(key, value);
50
55
  }
51
- logger.debug({ method, path }, 'Making API request');
56
+ logger.debug({ method, path: fullPath }, 'Making API request');
52
57
  return config;
53
58
  });
54
59
  // Add response interceptor for error handling
@@ -56,8 +61,8 @@ export class KalshiClient {
56
61
  if (axios.isAxiosError(error)) {
57
62
  if (error.code === 'ECONNABORTED') {
58
63
  throw new TimeoutError('Request timed out', {
59
- url: error.config?.url,
60
64
  timeout: error.config?.timeout,
65
+ url: error.config?.url,
61
66
  });
62
67
  }
63
68
  if (!error.response) {
@@ -65,67 +70,36 @@ export class KalshiClient {
65
70
  message: error.message,
66
71
  });
67
72
  }
68
- const status = error.response.status;
73
+ const { status } = error.response;
69
74
  const data = error.response.data;
70
75
  if (status === 429) {
71
76
  throw new RateLimitError('Rate limit exceeded', {
72
- status,
73
77
  retryAfter: error.response.headers['retry-after'],
78
+ status,
74
79
  });
75
80
  }
76
81
  throw new APIError(data.error?.message || `API error: ${status}`, {
77
- status,
78
82
  code: data.error?.code,
79
83
  details: data.error?.details,
84
+ status,
80
85
  });
81
86
  }
82
87
  throw error;
83
88
  });
84
89
  }
85
90
  /**
86
- * Make a rate-limited GET request
87
- */
88
- async get(path, config) {
89
- return READ_LIMITER.schedule(async () => {
90
- const response = await this.axios.get(path, config);
91
- if (response.data.error) {
92
- throw new APIError(response.data.error.message, {
93
- code: response.data.error.code,
94
- details: response.data.error.details,
95
- });
96
- }
97
- return response.data.data;
98
- });
99
- }
100
- /**
101
- * Make a rate-limited POST request
91
+ * Cancel an order
102
92
  */
103
- async post(path, data, config) {
104
- return WRITE_LIMITER.schedule(async () => {
105
- const response = await this.axios.post(path, data, config);
106
- if (response.data.error) {
107
- throw new APIError(response.data.error.message, {
108
- code: response.data.error.code,
109
- details: response.data.error.details,
110
- });
111
- }
112
- return response.data.data;
113
- });
93
+ async cancelOrder(orderId) {
94
+ logger.info({ orderId }, 'Canceling order');
95
+ return this.delete(`/portfolio/orders/${orderId}`);
114
96
  }
115
97
  /**
116
- * Make a rate-limited DELETE request
98
+ * Create an order
117
99
  */
118
- async delete(path, config) {
119
- return WRITE_LIMITER.schedule(async () => {
120
- const response = await this.axios.delete(path, config);
121
- if (response.data.error) {
122
- throw new APIError(response.data.error.message, {
123
- code: response.data.error.code,
124
- details: response.data.error.details,
125
- });
126
- }
127
- return response.data.data;
128
- });
100
+ async createOrder(request) {
101
+ logger.info({ action: request.action, side: request.side, ticker: request.ticker }, 'Creating order');
102
+ return this.post('/portfolio/orders', request);
129
103
  }
130
104
  /**
131
105
  * Get account balance
@@ -135,41 +109,33 @@ export class KalshiClient {
135
109
  return this.get('/portfolio/balance');
136
110
  }
137
111
  /**
138
- * Get portfolio positions
112
+ * Get the base URL
139
113
  */
140
- async getPositions(params) {
141
- logger.info({ params }, 'Fetching portfolio positions');
142
- return this.get('/portfolio/positions', { params });
114
+ getBaseUrl() {
115
+ return this.baseUrl;
143
116
  }
144
117
  /**
145
- * Get markets
118
+ * Get fills (executed trades)
146
119
  */
147
- async getMarkets(query) {
148
- logger.info({ query }, 'Fetching markets');
149
- return this.get('/markets', { params: query });
120
+ async getFills(query) {
121
+ logger.info({ query }, 'Fetching fills');
122
+ return this.get('/portfolio/fills', { params: query });
150
123
  }
151
124
  /**
152
125
  * Get single market by ticker
153
126
  */
154
127
  async getMarket(ticker) {
155
128
  logger.info({ ticker }, 'Fetching market details');
156
- return this.get(`/markets/${ticker}`);
157
- }
158
- /**
159
- * Get order book for a market
160
- */
161
- async getOrderBook(ticker, depth) {
162
- logger.info({ ticker, depth }, 'Fetching order book');
163
- return this.get(`/markets/${ticker}/orderbook`, {
164
- params: depth ? { depth } : undefined,
165
- });
129
+ const response = await this.get(`/markets/${ticker}`);
130
+ // Kalshi returns { market: {...} }
131
+ return response.market;
166
132
  }
167
133
  /**
168
- * Get user orders
134
+ * Get markets
169
135
  */
170
- async getOrders(query) {
171
- logger.info({ query }, 'Fetching orders');
172
- return this.get('/portfolio/orders', { params: query });
136
+ async getMarkets(query) {
137
+ logger.info({ query }, 'Fetching markets');
138
+ return this.get('/markets', { params: query });
173
139
  }
174
140
  /**
175
141
  * Get single order by ID
@@ -179,25 +145,29 @@ export class KalshiClient {
179
145
  return this.get(`/portfolio/orders/${orderId}`);
180
146
  }
181
147
  /**
182
- * Create an order
148
+ * Get order book for a market
183
149
  */
184
- async createOrder(request) {
185
- logger.info({ ticker: request.ticker, side: request.side, action: request.action }, 'Creating order');
186
- return this.post('/portfolio/orders', request);
150
+ async getOrderBook(ticker, depth) {
151
+ logger.info({ depth, ticker }, 'Fetching order book');
152
+ return this.get(`/markets/${ticker}/orderbook`, {
153
+ params: depth ? { depth } : undefined,
154
+ });
187
155
  }
188
156
  /**
189
- * Cancel an order
157
+ * Get user orders
190
158
  */
191
- async cancelOrder(orderId) {
192
- logger.info({ orderId }, 'Canceling order');
193
- return this.delete(`/portfolio/orders/${orderId}`);
159
+ async getOrders(query) {
160
+ logger.info({ query }, 'Fetching orders');
161
+ return this.get('/portfolio/orders', { params: query });
194
162
  }
195
163
  /**
196
- * Get fills (executed trades)
164
+ * Get portfolio positions
197
165
  */
198
- async getFills(query) {
199
- logger.info({ query }, 'Fetching fills');
200
- return this.get('/portfolio/fills', { params: query });
166
+ async getPositions(params) {
167
+ logger.info({ params }, 'Fetching portfolio positions');
168
+ const response = await this.get('/portfolio/positions', { params });
169
+ // Kalshi returns { market_positions: [...] }, not just the array
170
+ return response.market_positions || [];
201
171
  }
202
172
  /**
203
173
  * Test connection to API
@@ -214,10 +184,55 @@ export class KalshiClient {
214
184
  }
215
185
  }
216
186
  /**
217
- * Get the base URL
187
+ * Make a rate-limited DELETE request
218
188
  */
219
- getBaseUrl() {
220
- return this.baseUrl;
189
+ async delete(path, config) {
190
+ return WRITE_LIMITER.schedule(async () => {
191
+ const response = await this.axios.delete(path, config);
192
+ if (response.data.error) {
193
+ throw new APIError(response.data.error.message, {
194
+ code: response.data.error.code,
195
+ details: response.data.error.details,
196
+ });
197
+ }
198
+ // Kalshi API doesn't always wrap in a data envelope
199
+ // Return response.data.data if it exists, otherwise response.data
200
+ return (response.data.data ?? response.data);
201
+ });
202
+ }
203
+ /**
204
+ * Make a rate-limited GET request
205
+ */
206
+ async get(path, config) {
207
+ return READ_LIMITER.schedule(async () => {
208
+ const response = await this.axios.get(path, config);
209
+ if (response.data.error) {
210
+ throw new APIError(response.data.error.message, {
211
+ code: response.data.error.code,
212
+ details: response.data.error.details,
213
+ });
214
+ }
215
+ // Kalshi API doesn't always wrap in a data envelope
216
+ // Return response.data.data if it exists, otherwise response.data
217
+ return (response.data.data ?? response.data);
218
+ });
219
+ }
220
+ /**
221
+ * Make a rate-limited POST request
222
+ */
223
+ async post(path, data, config) {
224
+ return WRITE_LIMITER.schedule(async () => {
225
+ const response = await this.axios.post(path, data, config);
226
+ if (response.data.error) {
227
+ throw new APIError(response.data.error.message, {
228
+ code: response.data.error.code,
229
+ details: response.data.error.details,
230
+ });
231
+ }
232
+ // Kalshi API doesn't always wrap in a data envelope
233
+ // Return response.data.data if it exists, otherwise response.data
234
+ return (response.data.data ?? response.data);
235
+ });
221
236
  }
222
237
  }
223
238
  /**
@@ -1,7 +1,7 @@
1
1
  import { KalshiClient } from './client.js';
2
+ export * from './auth.js';
2
3
  export * from './client.js';
3
4
  export * from './types.js';
4
- export * from './auth.js';
5
5
  /**
6
6
  * Create a Kalshi client from the current configuration
7
7
  */
@@ -1,9 +1,9 @@
1
1
  import { getConfig } from '../config/manager.js';
2
2
  import { logger } from '../logger.js';
3
3
  import { createKalshiClient } from './client.js';
4
+ export * from './auth.js';
4
5
  export * from './client.js';
5
6
  export * from './types.js';
6
- export * from './auth.js';
7
7
  /**
8
8
  * Create a Kalshi client from the current configuration
9
9
  */
@@ -8,8 +8,8 @@ export interface KalshiResponse<T> {
8
8
  data?: T;
9
9
  error?: {
10
10
  code: string;
11
- message: string;
12
11
  details?: Record<string, unknown>;
12
+ message: string;
13
13
  };
14
14
  }
15
15
  /**
@@ -17,119 +17,119 @@ export interface KalshiResponse<T> {
17
17
  */
18
18
  export interface Balance {
19
19
  balance: number;
20
- payout: number;
20
+ portfolio_value: number;
21
21
  }
22
22
  /**
23
23
  * Market data
24
24
  */
25
25
  export interface Market {
26
- ticker: string;
26
+ close_time: string;
27
27
  event_ticker: string;
28
+ expiration_time: string;
29
+ last_price?: number;
30
+ liquidity?: number;
28
31
  market_type: string;
29
- title: string;
30
- subtitle: string;
32
+ no_ask?: number;
33
+ no_bid?: number;
31
34
  open_time: string;
32
- close_time: string;
33
- expiration_time: string;
34
- status: 'active' | 'closed' | 'settled' | 'finalized';
35
- result?: 'yes' | 'no';
35
+ result?: 'no' | 'yes';
36
+ status: 'active' | 'closed' | 'finalized' | 'settled';
37
+ subtitle: string;
38
+ ticker: string;
39
+ title: string;
36
40
  volume?: number;
37
41
  volume_24h?: number;
38
- liquidity?: number;
39
- yes_bid?: number;
40
42
  yes_ask?: number;
41
- no_bid?: number;
42
- no_ask?: number;
43
- last_price?: number;
43
+ yes_bid?: number;
44
44
  }
45
45
  /**
46
46
  * Position data
47
47
  */
48
48
  export interface Position {
49
- ticker: string;
49
+ fees_paid: number;
50
+ market_exposure: number;
50
51
  market_ticker: string;
51
52
  position: number;
52
- market_exposure: number;
53
- total_cost: number;
54
- fees_paid: number;
55
53
  realized_pnl: number;
54
+ ticker: string;
55
+ total_cost: number;
56
56
  }
57
57
  /**
58
58
  * Order data
59
59
  */
60
60
  export interface Order {
61
- order_id: string;
62
- user_id: string;
63
- ticker: string;
64
- status: 'pending' | 'resting' | 'canceled' | 'executed' | 'expired';
65
- yes_price?: number;
66
- no_price?: number;
67
- count: number;
68
- side: 'yes' | 'no';
69
61
  action: 'buy' | 'sell';
70
- type: 'market' | 'limit';
62
+ avg_fill_price?: number;
63
+ count: number;
71
64
  created_time: string;
72
- updated_time: string;
73
65
  expiration_time?: string;
74
- remaining_count: number;
75
66
  filled_count: number;
76
- avg_fill_price?: number;
67
+ no_price?: number;
68
+ order_id: string;
69
+ remaining_count: number;
70
+ side: 'no' | 'yes';
71
+ status: 'canceled' | 'executed' | 'expired' | 'pending' | 'resting';
72
+ ticker: string;
73
+ type: 'limit' | 'market';
74
+ updated_time: string;
75
+ user_id: string;
76
+ yes_price?: number;
77
77
  }
78
78
  /**
79
79
  * Fill (executed trade) data
80
80
  */
81
81
  export interface Fill {
82
- fill_id: string;
83
- order_id: string;
84
- ticker: string;
85
- side: 'yes' | 'no';
86
82
  action: 'buy' | 'sell';
87
83
  count: number;
88
- price: number;
89
84
  created_time: string;
85
+ fill_id: string;
90
86
  is_taker: boolean;
87
+ order_id: string;
88
+ price: number;
89
+ side: 'no' | 'yes';
90
+ ticker: string;
91
91
  trade_id: string;
92
92
  }
93
93
  /**
94
94
  * Order book level
95
95
  */
96
96
  export interface OrderBookLevel {
97
- price: number;
98
97
  count: number;
98
+ price: number;
99
99
  }
100
100
  /**
101
101
  * Order book data
102
102
  */
103
103
  export interface OrderBook {
104
+ no: OrderBookLevel[];
104
105
  ticker: string;
105
106
  yes: OrderBookLevel[];
106
- no: OrderBookLevel[];
107
107
  }
108
108
  /**
109
109
  * Create order request
110
110
  */
111
111
  export interface CreateOrderRequest {
112
- ticker: string;
113
- client_order_id?: string;
114
- side: 'yes' | 'no';
115
112
  action: 'buy' | 'sell';
113
+ buy_max_cost?: number;
114
+ client_order_id?: string;
116
115
  count: number;
117
- type: 'market' | 'limit';
118
- yes_price?: number;
119
- no_price?: number;
120
116
  expiration_ts?: number;
117
+ no_price?: number;
121
118
  sell_position_floor?: number;
122
- buy_max_cost?: number;
119
+ side: 'no' | 'yes';
120
+ ticker: string;
121
+ type: 'limit' | 'market';
122
+ yes_price?: number;
123
123
  }
124
124
  /**
125
125
  * Markets query parameters
126
126
  */
127
127
  export interface MarketsQuery {
128
- limit?: number;
129
128
  cursor?: string;
130
129
  event_ticker?: string;
130
+ limit?: number;
131
131
  series_ticker?: string;
132
- status?: 'active' | 'closed' | 'settled' | 'finalized';
132
+ status?: 'active' | 'closed' | 'finalized' | 'settled';
133
133
  tickers?: string;
134
134
  with_nested_markets?: boolean;
135
135
  }
@@ -137,19 +137,19 @@ export interface MarketsQuery {
137
137
  * Orders query parameters
138
138
  */
139
139
  export interface OrdersQuery {
140
- ticker?: string;
141
- status?: 'pending' | 'resting' | 'canceled' | 'executed' | 'expired';
142
- limit?: number;
143
140
  cursor?: string;
141
+ limit?: number;
142
+ status?: 'canceled' | 'executed' | 'expired' | 'pending' | 'resting';
143
+ ticker?: string;
144
144
  }
145
145
  /**
146
146
  * Fills query parameters
147
147
  */
148
148
  export interface FillsQuery {
149
- ticker?: string;
150
- order_id?: string;
151
- min_ts?: number;
152
- max_ts?: number;
153
- limit?: number;
154
149
  cursor?: string;
150
+ limit?: number;
151
+ max_ts?: number;
152
+ min_ts?: number;
153
+ order_id?: string;
154
+ ticker?: string;
155
155
  }
@@ -8,6 +8,7 @@ export function createLogger(options = {}) {
8
8
  level,
9
9
  // Redact sensitive fields
10
10
  redact: {
11
+ censor: '[REDACTED]',
11
12
  paths: [
12
13
  'keyId',
13
14
  'privateKey',
@@ -22,17 +23,16 @@ export function createLogger(options = {}) {
22
23
  '*.password',
23
24
  '*.token',
24
25
  ],
25
- censor: '[REDACTED]',
26
26
  },
27
27
  };
28
28
  if (pretty) {
29
29
  return pino(pinoOptions, pino.transport({
30
- target: 'pino-pretty',
31
30
  options: {
32
31
  colorize: true,
33
- translateTime: 'HH:MM:ss',
34
32
  ignore: 'pid,hostname',
33
+ translateTime: 'HH:MM:ss',
35
34
  },
35
+ target: 'pino-pretty',
36
36
  }));
37
37
  }
38
38
  return pino(pinoOptions);
@@ -3,67 +3,67 @@ import Table from 'cli-table3';
3
3
  * Standard success response structure
4
4
  */
5
5
  export interface SuccessResponse<T = unknown> {
6
- success: true;
7
6
  data: T;
8
7
  metadata: {
9
- timestamp: string;
10
8
  command?: string;
11
9
  duration_ms?: number;
10
+ timestamp: string;
12
11
  };
12
+ success: true;
13
13
  }
14
14
  /**
15
15
  * Standard error response structure
16
16
  */
17
17
  export interface ErrorResponse {
18
- success: false;
19
18
  error: {
20
19
  code: string;
21
- message: string;
22
20
  details?: Record<string, unknown>;
21
+ message: string;
23
22
  };
24
23
  metadata: {
25
- timestamp: string;
26
24
  command?: string;
25
+ timestamp: string;
27
26
  };
27
+ success: false;
28
28
  }
29
29
  /**
30
30
  * Output formatter for both human and JSON formats
31
31
  */
32
32
  export declare class OutputFormatter {
33
+ private command?;
33
34
  private jsonMode;
34
35
  private startTime;
35
- private command?;
36
36
  constructor(jsonMode?: boolean, command?: string);
37
37
  /**
38
- * Output a success response
38
+ * Create an error response object
39
39
  */
40
- success<T>(data: T): void;
40
+ createErrorResponse(code: string, message: string, details?: Record<string, unknown>): ErrorResponse;
41
41
  /**
42
- * Output an error response
42
+ * Create a success response object
43
43
  */
44
- error(code: string, message: string, details?: Record<string, unknown>): void;
44
+ createSuccessResponse<T>(data: T): SuccessResponse<T>;
45
45
  /**
46
46
  * Create a table for human-readable output
47
47
  */
48
48
  createTable(head: string[], rows: string[][]): Table.Table;
49
49
  /**
50
- * Output a table
50
+ * Output an error response
51
51
  */
52
- outputTable(head: string[], rows: string[][]): void;
52
+ error(code: string, message: string, details?: Record<string, unknown>): void;
53
53
  /**
54
- * Create a success response object
54
+ * Check if JSON mode is enabled
55
55
  */
56
- createSuccessResponse<T>(data: T): SuccessResponse<T>;
56
+ isJSONMode(): boolean;
57
57
  /**
58
- * Create an error response object
58
+ * Output a table
59
59
  */
60
- createErrorResponse(code: string, message: string, details?: Record<string, unknown>): ErrorResponse;
60
+ outputTable(head: string[], rows: string[][]): void;
61
61
  /**
62
- * Output JSON to stdout
62
+ * Output a success response
63
63
  */
64
- private outputJSON;
64
+ success<T>(data: T): void;
65
65
  /**
66
- * Check if JSON mode is enabled
66
+ * Output JSON to stdout
67
67
  */
68
- isJSONMode(): boolean;
68
+ private outputJSON;
69
69
  }