@discomedia/utils 1.0.25 → 1.0.27

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 (50) hide show
  1. package/dist/alpaca-trading-api-6NxNgQBn.js +1413 -0
  2. package/dist/alpaca-trading-api-6NxNgQBn.js.map +1 -0
  3. package/dist/index-frontend.cjs +105 -12
  4. package/dist/index-frontend.cjs.map +1 -1
  5. package/dist/index-frontend.mjs +105 -13
  6. package/dist/index-frontend.mjs.map +1 -1
  7. package/dist/index.cjs +257 -43
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.mjs +257 -44
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/package.json +3 -3
  12. package/dist/test.js +868 -5517
  13. package/dist/test.js.map +1 -1
  14. package/dist/types/alpaca-trading-api.d.ts +33 -0
  15. package/dist/types/alpaca-trading-api.d.ts.map +1 -1
  16. package/dist/types/index-frontend.d.ts +1 -1
  17. package/dist/types/index.d.ts +3 -1
  18. package/dist/types/index.d.ts.map +1 -1
  19. package/dist/types/json-tools.d.ts.map +1 -1
  20. package/dist/types/llm-deepseek.d.ts +1 -1
  21. package/dist/types/llm-deepseek.d.ts.map +1 -1
  22. package/dist/types/llm-images.d.ts.map +1 -1
  23. package/dist/types/llm-openai.d.ts +2 -2
  24. package/dist/types/llm-openai.d.ts.map +1 -1
  25. package/dist/types/llm-openrouter.d.ts +28 -0
  26. package/dist/types/llm-openrouter.d.ts.map +1 -0
  27. package/dist/types/misc-utils.d.ts.map +1 -1
  28. package/dist/types/types/llm-types.d.ts +26 -3
  29. package/dist/types/types/llm-types.d.ts.map +1 -1
  30. package/dist/types/types/logging-types.d.ts +1 -1
  31. package/dist/types/types/logging-types.d.ts.map +1 -1
  32. package/dist/types-frontend/alpaca-trading-api.d.ts +33 -0
  33. package/dist/types-frontend/alpaca-trading-api.d.ts.map +1 -1
  34. package/dist/types-frontend/index-frontend.d.ts +1 -1
  35. package/dist/types-frontend/index.d.ts +3 -1
  36. package/dist/types-frontend/index.d.ts.map +1 -1
  37. package/dist/types-frontend/json-tools.d.ts.map +1 -1
  38. package/dist/types-frontend/llm-deepseek.d.ts +1 -1
  39. package/dist/types-frontend/llm-deepseek.d.ts.map +1 -1
  40. package/dist/types-frontend/llm-images.d.ts.map +1 -1
  41. package/dist/types-frontend/llm-openai.d.ts +2 -2
  42. package/dist/types-frontend/llm-openai.d.ts.map +1 -1
  43. package/dist/types-frontend/llm-openrouter.d.ts +28 -0
  44. package/dist/types-frontend/llm-openrouter.d.ts.map +1 -0
  45. package/dist/types-frontend/misc-utils.d.ts.map +1 -1
  46. package/dist/types-frontend/types/llm-types.d.ts +26 -3
  47. package/dist/types-frontend/types/llm-types.d.ts.map +1 -1
  48. package/dist/types-frontend/types/logging-types.d.ts +1 -1
  49. package/dist/types-frontend/types/logging-types.d.ts.map +1 -1
  50. package/package.json +3 -3
@@ -0,0 +1,1413 @@
1
+ import { l as log, W as WebSocket, m as marketDataAPI } from './test.js';
2
+ import 'events';
3
+ import 'https';
4
+ import 'http';
5
+ import 'net';
6
+ import 'tls';
7
+ import 'crypto';
8
+ import 'stream';
9
+ import 'url';
10
+ import 'zlib';
11
+ import 'buffer';
12
+ import 'fs';
13
+ import 'path';
14
+ import 'os';
15
+
16
+ const limitPriceSlippagePercent100 = 0.1; // 0.1%
17
+ /**
18
+ Websocket example
19
+ const alpacaAPI = createAlpacaTradingAPI(credentials); // type AlpacaCredentials
20
+ alpacaAPI.onTradeUpdate((update: TradeUpdate) => {
21
+ this.log(`Received trade update: event ${update.event} for an order to ${update.order.side} ${update.order.qty} of ${update.order.symbol}`);
22
+ });
23
+ alpacaAPI.connectWebsocket(); // necessary to connect to the WebSocket
24
+
25
+ Portfolio History examples
26
+ // Get standard portfolio history
27
+ const portfolioHistory = await alpacaAPI.getPortfolioHistory({
28
+ timeframe: '1D',
29
+ period: '1M'
30
+ });
31
+
32
+ // Get daily portfolio history with current day included (if available from hourly data)
33
+ const dailyHistory = await alpacaAPI.getPortfolioDailyHistory({
34
+ period: '1M'
35
+ });
36
+ */
37
+ class AlpacaTradingAPI {
38
+ static new(credentials) {
39
+ return new AlpacaTradingAPI(credentials);
40
+ }
41
+ static getInstance(credentials) {
42
+ return new AlpacaTradingAPI(credentials);
43
+ }
44
+ ws = null;
45
+ headers;
46
+ tradeUpdateCallback = null;
47
+ credentials;
48
+ apiBaseUrl;
49
+ wsUrl;
50
+ authenticated = false;
51
+ connecting = false;
52
+ reconnectDelay = 10000; // 10 seconds between reconnection attempts
53
+ reconnectTimeout = null;
54
+ messageHandlers = new Map();
55
+ debugLogging = false;
56
+ /**
57
+ * Constructor for AlpacaTradingAPI
58
+ * @param credentials - Alpaca credentials,
59
+ * accountName: string; // The account identifier used inthis.logs and tracking
60
+ * apiKey: string; // Alpaca API key
61
+ * apiSecret: string; // Alpaca API secret
62
+ * type: AlpacaAccountType;
63
+ * orderType: AlpacaOrderType;
64
+ * @param options - Optional options
65
+ * debugLogging: boolean; // Whether to log messages of type 'debug'
66
+ */
67
+ constructor(credentials, options) {
68
+ this.credentials = credentials;
69
+ // Set URLs based on account type
70
+ this.apiBaseUrl =
71
+ credentials.type === 'PAPER' ? 'https://paper-api.alpaca.markets/v2' : 'https://api.alpaca.markets/v2';
72
+ this.wsUrl =
73
+ credentials.type === 'PAPER' ? 'wss://paper-api.alpaca.markets/stream' : 'wss://api.alpaca.markets/stream';
74
+ this.headers = {
75
+ 'APCA-API-KEY-ID': credentials.apiKey,
76
+ 'APCA-API-SECRET-KEY': credentials.apiSecret,
77
+ 'Content-Type': 'application/json',
78
+ };
79
+ // Initialize message handlers
80
+ this.messageHandlers.set('authorization', this.handleAuthMessage.bind(this));
81
+ this.messageHandlers.set('listening', this.handleListenMessage.bind(this));
82
+ this.messageHandlers.set('trade_updates', this.handleTradeUpdate.bind(this));
83
+ this.debugLogging = options?.debugLogging || false;
84
+ }
85
+ log(message, options = { type: 'info' }) {
86
+ if (this.debugLogging && options.type === 'debug') {
87
+ return;
88
+ }
89
+ log(message, { ...options, source: 'AlpacaTradingAPI', account: this.credentials.accountName });
90
+ }
91
+ /**
92
+ * Round a price to the nearest 2 decimal places for Alpaca, or 4 decimal places for prices less than $1
93
+ * @param price - The price to round
94
+ * @returns The rounded price
95
+ */
96
+ roundPriceForAlpaca = (price) => {
97
+ return price >= 1 ? Math.round(price * 100) / 100 : Math.round(price * 10000) / 10000;
98
+ };
99
+ handleAuthMessage(data) {
100
+ if (data.status === 'authorized') {
101
+ this.authenticated = true;
102
+ this.log('WebSocket authenticated');
103
+ }
104
+ else {
105
+ this.log(`Authentication failed: ${data.message || 'Unknown error'}`, {
106
+ type: 'error',
107
+ });
108
+ }
109
+ }
110
+ handleListenMessage(data) {
111
+ if (data.streams?.includes('trade_updates')) {
112
+ this.log('Successfully subscribed to trade updates');
113
+ }
114
+ }
115
+ handleTradeUpdate(data) {
116
+ if (this.tradeUpdateCallback) {
117
+ this.log(`Trade update: ${data.event} to ${data.order.side} ${data.order.qty} shares, type ${data.order.type}`, {
118
+ symbol: data.order.symbol,
119
+ type: 'debug',
120
+ });
121
+ this.tradeUpdateCallback(data);
122
+ }
123
+ }
124
+ handleMessage(message) {
125
+ try {
126
+ const data = JSON.parse(message);
127
+ const handler = this.messageHandlers.get(data.stream);
128
+ if (handler) {
129
+ handler(data.data);
130
+ }
131
+ else {
132
+ this.log(`Received message for unknown stream: ${data.stream}`, {
133
+ type: 'warn',
134
+ });
135
+ }
136
+ }
137
+ catch (error) {
138
+ this.log('Failed to parse WebSocket message', {
139
+ type: 'error',
140
+ metadata: { error: error instanceof Error ? error.message : 'Unknown error' },
141
+ });
142
+ }
143
+ }
144
+ connectWebsocket() {
145
+ if (this.connecting) {
146
+ this.log('Connection attempt skipped - already connecting');
147
+ return;
148
+ }
149
+ if (this.ws?.readyState === WebSocket.OPEN) {
150
+ this.log('Connection attempt skipped - already connected');
151
+ return;
152
+ }
153
+ this.connecting = true;
154
+ if (this.ws) {
155
+ this.ws.removeAllListeners();
156
+ this.ws.terminate();
157
+ this.ws = null;
158
+ }
159
+ this.log(`Connecting to WebSocket at ${this.wsUrl}...`);
160
+ this.ws = new WebSocket(this.wsUrl);
161
+ this.ws.on('open', async () => {
162
+ try {
163
+ this.log('WebSocket connected');
164
+ await this.authenticate();
165
+ await this.subscribeToTradeUpdates();
166
+ this.connecting = false;
167
+ }
168
+ catch (error) {
169
+ this.log('Failed to setup WebSocket connection', {
170
+ type: 'error',
171
+ metadata: { error: error instanceof Error ? error.message : 'Unknown error' },
172
+ });
173
+ this.ws?.close();
174
+ }
175
+ });
176
+ this.ws.on('message', (data) => {
177
+ this.handleMessage(data.toString());
178
+ });
179
+ this.ws.on('error', (error) => {
180
+ this.log('WebSocket error', {
181
+ type: 'error',
182
+ metadata: { error: error instanceof Error ? error.message : 'Unknown error' },
183
+ });
184
+ this.connecting = false;
185
+ });
186
+ this.ws.on('close', () => {
187
+ this.log('WebSocket connection closed');
188
+ this.authenticated = false;
189
+ this.connecting = false;
190
+ // Clear any existing reconnect timeout
191
+ if (this.reconnectTimeout) {
192
+ clearTimeout(this.reconnectTimeout);
193
+ this.reconnectTimeout = null;
194
+ }
195
+ // Schedule reconnection
196
+ this.reconnectTimeout = setTimeout(() => {
197
+ this.log('Attempting to reconnect...');
198
+ this.connectWebsocket();
199
+ }, this.reconnectDelay);
200
+ });
201
+ }
202
+ async authenticate() {
203
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
204
+ throw new Error('WebSocket not ready for authentication');
205
+ }
206
+ const authMessage = {
207
+ action: 'auth',
208
+ key: this.credentials.apiKey,
209
+ secret: this.credentials.apiSecret,
210
+ };
211
+ this.ws.send(JSON.stringify(authMessage));
212
+ return new Promise((resolve, reject) => {
213
+ const authTimeout = setTimeout(() => {
214
+ this.log('Authentication timeout', { type: 'error' });
215
+ reject(new Error('Authentication timed out'));
216
+ }, 10000);
217
+ const handleAuthResponse = (data) => {
218
+ try {
219
+ const message = JSON.parse(data.toString());
220
+ if (message.stream === 'authorization') {
221
+ this.ws?.removeListener('message', handleAuthResponse);
222
+ clearTimeout(authTimeout);
223
+ if (message.data?.status === 'authorized') {
224
+ this.authenticated = true;
225
+ this.log('WebSocket authenticated');
226
+ resolve();
227
+ }
228
+ else {
229
+ const error = `Authentication failed: ${message.data?.message || 'Unknown error'}`;
230
+ this.log(error, { type: 'error' });
231
+ reject(new Error(error));
232
+ }
233
+ }
234
+ }
235
+ catch (error) {
236
+ this.log('Failed to parse auth response', {
237
+ type: 'error',
238
+ metadata: { error: error instanceof Error ? error.message : 'Unknown error' },
239
+ });
240
+ }
241
+ };
242
+ this.ws?.on('message', handleAuthResponse);
243
+ });
244
+ }
245
+ async subscribeToTradeUpdates() {
246
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.authenticated) {
247
+ throw new Error('WebSocket not ready for subscription');
248
+ }
249
+ const listenMessage = {
250
+ action: 'listen',
251
+ data: {
252
+ streams: ['trade_updates'],
253
+ },
254
+ };
255
+ this.ws.send(JSON.stringify(listenMessage));
256
+ return new Promise((resolve, reject) => {
257
+ const listenTimeout = setTimeout(() => {
258
+ reject(new Error('Subscribe timeout'));
259
+ }, 10000);
260
+ const handleListenResponse = (data) => {
261
+ try {
262
+ const message = JSON.parse(data.toString());
263
+ if (message.stream === 'listening') {
264
+ this.ws?.removeListener('message', handleListenResponse);
265
+ clearTimeout(listenTimeout);
266
+ if (message.data?.streams?.includes('trade_updates')) {
267
+ this.log('Subscribed to trade updates');
268
+ resolve();
269
+ }
270
+ else {
271
+ reject(new Error('Failed to subscribe to trade updates'));
272
+ }
273
+ }
274
+ }
275
+ catch (error) {
276
+ this.log('Failed to parse listen response', {
277
+ type: 'error',
278
+ metadata: { error: error instanceof Error ? error.message : 'Unknown error' },
279
+ });
280
+ }
281
+ };
282
+ this.ws?.on('message', handleListenResponse);
283
+ });
284
+ }
285
+ async makeRequest(endpoint, method = 'GET', body, queryString = '') {
286
+ const url = `${this.apiBaseUrl}${endpoint}${queryString}`;
287
+ try {
288
+ const response = await fetch(url, {
289
+ method,
290
+ headers: this.headers,
291
+ body: body ? JSON.stringify(body) : undefined,
292
+ });
293
+ if (!response.ok) {
294
+ const errorText = await response.text();
295
+ this.log(`Alpaca API error (${response.status}): ${errorText}`, { type: 'error' });
296
+ throw new Error(`Alpaca API error (${response.status}): ${errorText}`);
297
+ }
298
+ // Handle responses with no content (e.g., 204 No Content)
299
+ if (response.status === 204 || response.headers.get('content-length') === '0') {
300
+ return null;
301
+ }
302
+ const contentType = response.headers.get('content-type');
303
+ if (contentType && contentType.includes('application/json')) {
304
+ return await response.json();
305
+ }
306
+ // For non-JSON responses, return the text content
307
+ const textContent = await response.text();
308
+ return textContent || null;
309
+ }
310
+ catch (err) {
311
+ const error = err;
312
+ this.log(`Error in makeRequest: ${error.message}. Url: ${url}`, {
313
+ source: 'AlpacaAPI',
314
+ type: 'error',
315
+ });
316
+ throw error;
317
+ }
318
+ }
319
+ async getPositions(assetClass) {
320
+ const positions = (await this.makeRequest('/positions'));
321
+ if (assetClass) {
322
+ return positions.filter((position) => position.asset_class === assetClass);
323
+ }
324
+ return positions;
325
+ }
326
+ /**
327
+ * Get all orders
328
+ * @param params (GetOrdersParams) - optional parameters to filter the orders
329
+ * - status: 'open' | 'closed' | 'all'
330
+ * - limit: number
331
+ * - after: string
332
+ * - until: string
333
+ * - direction: 'asc' | 'desc'
334
+ * - nested: boolean
335
+ * - symbols: string[], an array of all the symbols
336
+ * - side: 'buy' | 'sell'
337
+ * @returns all orders
338
+ */
339
+ async getOrders(params = {}) {
340
+ const queryParams = new URLSearchParams();
341
+ if (params.status)
342
+ queryParams.append('status', params.status);
343
+ if (params.limit)
344
+ queryParams.append('limit', params.limit.toString());
345
+ if (params.after)
346
+ queryParams.append('after', params.after);
347
+ if (params.until)
348
+ queryParams.append('until', params.until);
349
+ if (params.direction)
350
+ queryParams.append('direction', params.direction);
351
+ if (params.nested)
352
+ queryParams.append('nested', params.nested.toString());
353
+ if (params.symbols)
354
+ queryParams.append('symbols', params.symbols.join(','));
355
+ if (params.side)
356
+ queryParams.append('side', params.side);
357
+ const endpoint = `/orders${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
358
+ try {
359
+ return await this.makeRequest(endpoint);
360
+ }
361
+ catch (error) {
362
+ this.log(`Error getting orders: ${error}`, { type: 'error' });
363
+ throw error;
364
+ }
365
+ }
366
+ async getAccountDetails() {
367
+ try {
368
+ return await this.makeRequest('/account');
369
+ }
370
+ catch (error) {
371
+ this.log(`Error getting account details: ${error}`, { type: 'error' });
372
+ throw error;
373
+ }
374
+ }
375
+ /**
376
+ * Create a trailing stop order
377
+ * @param symbol (string) - the symbol of the order
378
+ * @param qty (number) - the quantity of the order
379
+ * @param side (string) - the side of the order
380
+ * @param trailPercent100 (number) - the trail percent of the order (scale 100, i.e. 0.5 = 0.5%)
381
+ * @param position_intent (string) - the position intent of the order
382
+ */
383
+ async createTrailingStop(symbol, qty, side, trailPercent100, position_intent) {
384
+ this.log(`Creating trailing stop ${side.toUpperCase()} ${qty} shares for ${symbol} with trail percent ${trailPercent100}%`, {
385
+ symbol,
386
+ });
387
+ try {
388
+ await this.makeRequest(`/orders`, 'POST', {
389
+ symbol,
390
+ qty: Math.abs(qty),
391
+ side,
392
+ position_intent,
393
+ order_class: 'simple',
394
+ type: 'trailing_stop',
395
+ trail_percent: trailPercent100, // Already in decimal form (e.g., 4 for 4%)
396
+ time_in_force: 'gtc',
397
+ });
398
+ }
399
+ catch (error) {
400
+ this.log(`Error creating trailing stop: ${error}`, {
401
+ symbol,
402
+ type: 'error',
403
+ });
404
+ throw error;
405
+ }
406
+ }
407
+ /**
408
+ * Create a market order
409
+ * @param symbol (string) - the symbol of the order
410
+ * @param qty (number) - the quantity of the order
411
+ * @param side (string) - the side of the order
412
+ * @param position_intent (string) - the position intent of the order. Important for knowing if a position needs a trailing stop.
413
+ */
414
+ async createMarketOrder(symbol, qty, side, position_intent, client_order_id) {
415
+ this.log(`Creating market order for ${symbol}: ${side} ${qty} shares (${position_intent})`, {
416
+ symbol,
417
+ });
418
+ const body = {
419
+ symbol,
420
+ qty: Math.abs(qty).toString(),
421
+ side,
422
+ position_intent,
423
+ type: 'market',
424
+ time_in_force: 'day',
425
+ order_class: 'simple',
426
+ };
427
+ if (client_order_id !== undefined) {
428
+ body.client_order_id = client_order_id;
429
+ }
430
+ try {
431
+ return await this.makeRequest('/orders', 'POST', body);
432
+ }
433
+ catch (error) {
434
+ this.log(`Error creating market order: ${error}`, { type: 'error' });
435
+ throw error;
436
+ }
437
+ }
438
+ /**
439
+ * Get the current trail percent for a symbol, assuming that it has an open position and a trailing stop order to close it. Because this relies on an orders request for one symbol, you can't do it too often.
440
+ * @param symbol (string) - the symbol of the order
441
+ * @returns the current trail percent
442
+ */
443
+ async getCurrentTrailPercent(symbol) {
444
+ try {
445
+ const orders = await this.getOrders({
446
+ status: 'open',
447
+ symbols: [symbol],
448
+ });
449
+ const trailingStopOrder = orders.find((order) => order.type === 'trailing_stop' &&
450
+ (order.position_intent === 'sell_to_close' || order.position_intent === 'buy_to_close'));
451
+ if (!trailingStopOrder) {
452
+ this.log(`No closing trailing stop order found for ${symbol}`, {
453
+ symbol,
454
+ });
455
+ return null;
456
+ }
457
+ if (!trailingStopOrder.trail_percent) {
458
+ this.log(`Trailing stop order found for ${symbol} but no trail_percent value`, {
459
+ symbol,
460
+ });
461
+ return null;
462
+ }
463
+ const trailPercent = parseFloat(trailingStopOrder.trail_percent);
464
+ return trailPercent;
465
+ }
466
+ catch (error) {
467
+ this.log(`Error getting current trail percent: ${error}`, {
468
+ symbol,
469
+ type: 'error',
470
+ });
471
+ throw error;
472
+ }
473
+ }
474
+ /**
475
+ * Update the trail percent for a trailing stop order
476
+ * @param symbol (string) - the symbol of the order
477
+ * @param trailPercent100 (number) - the trail percent of the order (scale 100, i.e. 0.5 = 0.5%)
478
+ */
479
+ async updateTrailingStop(symbol, trailPercent100) {
480
+ // First get all open orders for this symbol
481
+ const orders = await this.getOrders({
482
+ status: 'open',
483
+ symbols: [symbol],
484
+ });
485
+ // Find the trailing stop order
486
+ const trailingStopOrder = orders.find((order) => order.type === 'trailing_stop');
487
+ if (!trailingStopOrder) {
488
+ this.log(`No open trailing stop order found for ${symbol}`, { type: 'error', symbol });
489
+ return;
490
+ }
491
+ // Check if the trail_percent is already set to the desired value
492
+ const currentTrailPercent = trailingStopOrder.trail_percent ? parseFloat(trailingStopOrder.trail_percent) : null;
493
+ // Compare with a small epsilon to handle floating point precision
494
+ const epsilon = 0.0001;
495
+ if (currentTrailPercent !== null && Math.abs(currentTrailPercent - trailPercent100) < epsilon) {
496
+ this.log(`Trailing stop for ${symbol} already set to ${trailPercent100}% (current: ${currentTrailPercent}%), skipping update`, {
497
+ symbol,
498
+ });
499
+ return;
500
+ }
501
+ this.log(`Updating trailing stop for ${symbol} from ${currentTrailPercent}% to ${trailPercent100}%`, {
502
+ symbol,
503
+ });
504
+ try {
505
+ await this.makeRequest(`/orders/${trailingStopOrder.id}`, 'PATCH', {
506
+ trail: trailPercent100.toString(), // Changed from trail_percent to trail
507
+ });
508
+ }
509
+ catch (error) {
510
+ this.log(`Error updating trailing stop: ${error}`, {
511
+ symbol,
512
+ type: 'error',
513
+ });
514
+ throw error;
515
+ }
516
+ }
517
+ /**
518
+ * Cancel all open orders
519
+ */
520
+ async cancelAllOrders() {
521
+ this.log(`Canceling all open orders`);
522
+ try {
523
+ await this.makeRequest('/orders', 'DELETE');
524
+ }
525
+ catch (error) {
526
+ this.log(`Error canceling all orders: ${error}`, { type: 'error' });
527
+ }
528
+ }
529
+ /**
530
+ * Cancel a specific order by its ID
531
+ * @param orderId The id of the order to cancel
532
+ * @throws Error if the order is not cancelable (status 422) or if the order doesn't exist
533
+ * @returns Promise that resolves when the order is successfully canceled
534
+ */
535
+ async cancelOrder(orderId) {
536
+ this.log(`Attempting to cancel order ${orderId}`);
537
+ try {
538
+ await this.makeRequest(`/orders/${orderId}`, 'DELETE');
539
+ this.log(`Successfully canceled order ${orderId}`);
540
+ }
541
+ catch (error) {
542
+ // If the error is a 422, it means the order is not cancelable
543
+ if (error instanceof Error && error.message.includes('422')) {
544
+ this.log(`Order ${orderId} is not cancelable`, {
545
+ type: 'error',
546
+ });
547
+ throw new Error(`Order ${orderId} is not cancelable`);
548
+ }
549
+ // Re-throw other errors
550
+ throw error;
551
+ }
552
+ }
553
+ /**
554
+ * Create a limit order
555
+ * @param symbol (string) - the symbol of the order
556
+ * @param qty (number) - the quantity of the order
557
+ * @param side (string) - the side of the order
558
+ * @param limitPrice (number) - the limit price of the order
559
+ * @param position_intent (string) - the position intent of the order
560
+ * @param extended_hours (boolean) - whether the order is in extended hours
561
+ * @param client_order_id (string) - the client order id of the order
562
+ */
563
+ async createLimitOrder(symbol, qty, side, limitPrice, position_intent, extended_hours = false, client_order_id) {
564
+ this.log(`Creating limit order for ${symbol}: ${side} ${qty} shares at $${limitPrice.toFixed(2)} (${position_intent})`, {
565
+ symbol,
566
+ });
567
+ const body = {
568
+ symbol,
569
+ qty: Math.abs(qty).toString(),
570
+ side,
571
+ position_intent,
572
+ type: 'limit',
573
+ limit_price: this.roundPriceForAlpaca(limitPrice).toString(),
574
+ time_in_force: 'day',
575
+ order_class: 'simple',
576
+ extended_hours,
577
+ };
578
+ if (client_order_id !== undefined) {
579
+ body.client_order_id = client_order_id;
580
+ }
581
+ try {
582
+ return await this.makeRequest('/orders', 'POST', body);
583
+ }
584
+ catch (error) {
585
+ this.log(`Error creating limit order: ${error}`, { type: 'error' });
586
+ throw error;
587
+ }
588
+ }
589
+ /**
590
+ * Close all equities positions
591
+ * @param options (object) - the options for closing the positions
592
+ * - cancel_orders (boolean) - whether to cancel related orders
593
+ * - useLimitOrders (boolean) - whether to use limit orders to close the positions
594
+ */
595
+ async closeAllPositions(options = { cancel_orders: true, useLimitOrders: false }) {
596
+ this.log(`Closing all positions${options.useLimitOrders ? ' using limit orders' : ''}${options.cancel_orders ? ' and canceling open orders' : ''}`);
597
+ if (options.useLimitOrders) {
598
+ // Get all positions
599
+ const positions = await this.getPositions('us_equity');
600
+ if (positions.length === 0) {
601
+ this.log('No positions to close');
602
+ return;
603
+ }
604
+ this.log(`Found ${positions.length} positions to close`);
605
+ // Get latest quotes for all positions
606
+ const symbols = positions.map((position) => position.symbol);
607
+ const quotesResponse = await marketDataAPI.getLatestQuotes(symbols);
608
+ const lengthOfQuotes = Object.keys(quotesResponse.quotes).length;
609
+ if (lengthOfQuotes === 0) {
610
+ this.log('No quotes available for positions, received 0 quotes', {
611
+ type: 'error',
612
+ });
613
+ return;
614
+ }
615
+ if (lengthOfQuotes !== positions.length) {
616
+ this.log(`Received ${lengthOfQuotes} quotes for ${positions.length} positions, expected ${positions.length} quotes`, { type: 'warn' });
617
+ return;
618
+ }
619
+ // Create limit orders to close each position
620
+ for (const position of positions) {
621
+ const quote = quotesResponse.quotes[position.symbol];
622
+ if (!quote) {
623
+ this.log(`No quote available for ${position.symbol}, skipping limit order`, {
624
+ symbol: position.symbol,
625
+ type: 'warn',
626
+ });
627
+ continue;
628
+ }
629
+ const qty = Math.abs(parseFloat(position.qty));
630
+ const side = position.side === 'long' ? 'sell' : 'buy';
631
+ const positionIntent = side === 'sell' ? 'sell_to_close' : 'buy_to_close';
632
+ // Get the current price from the quote
633
+ const currentPrice = side === 'sell' ? quote.bp : quote.ap; // Use bid for sells, ask for buys
634
+ if (!currentPrice) {
635
+ this.log(`No valid price available for ${position.symbol}, skipping limit order`, {
636
+ symbol: position.symbol,
637
+ type: 'warn',
638
+ });
639
+ continue;
640
+ }
641
+ // Apply slippage from config
642
+ const limitSlippagePercent1 = limitPriceSlippagePercent100 / 100;
643
+ const limitPrice = side === 'sell'
644
+ ? this.roundPriceForAlpaca(currentPrice * (1 - limitSlippagePercent1)) // Sell slightly lower
645
+ : this.roundPriceForAlpaca(currentPrice * (1 + limitSlippagePercent1)); // Buy slightly higher
646
+ this.log(`Creating limit order to close ${position.symbol} position: ${side} ${qty} shares at $${limitPrice.toFixed(2)}`, {
647
+ symbol: position.symbol,
648
+ });
649
+ await this.createLimitOrder(position.symbol, qty, side, limitPrice, positionIntent);
650
+ }
651
+ }
652
+ else {
653
+ await this.makeRequest('/positions', 'DELETE', undefined, options.cancel_orders ? '?cancel_orders=true' : '');
654
+ }
655
+ }
656
+ /**
657
+ * Close all equities positions using limit orders during extended hours trading
658
+ * @param cancelOrders Whether to cancel related orders (default: true)
659
+ * @returns Promise that resolves when all positions are closed
660
+ */
661
+ async closeAllPositionsAfterHours() {
662
+ this.log('Closing all positions using limit orders during extended hours trading');
663
+ // Get all positions
664
+ const positions = await this.getPositions();
665
+ this.log(`Found ${positions.length} positions to close`);
666
+ if (positions.length === 0) {
667
+ this.log('No positions to close');
668
+ return;
669
+ }
670
+ await this.cancelAllOrders();
671
+ this.log(`Cancelled all open orders`);
672
+ // Get latest quotes for all positions
673
+ const symbols = positions.map((position) => position.symbol);
674
+ const quotesResponse = await marketDataAPI.getLatestQuotes(symbols);
675
+ // Create limit orders to close each position
676
+ for (const position of positions) {
677
+ const quote = quotesResponse.quotes[position.symbol];
678
+ if (!quote) {
679
+ this.log(`No quote available for ${position.symbol}, skipping limit order`, {
680
+ symbol: position.symbol,
681
+ type: 'warn',
682
+ });
683
+ continue;
684
+ }
685
+ const qty = Math.abs(parseFloat(position.qty));
686
+ const side = position.side === 'long' ? 'sell' : 'buy';
687
+ const positionIntent = side === 'sell' ? 'sell_to_close' : 'buy_to_close';
688
+ // Get the current price from the quote
689
+ const currentPrice = side === 'sell' ? quote.bp : quote.ap; // Use bid for sells, ask for buys
690
+ if (!currentPrice) {
691
+ this.log(`No valid price available for ${position.symbol}, skipping limit order`, {
692
+ symbol: position.symbol,
693
+ type: 'warn',
694
+ });
695
+ continue;
696
+ }
697
+ // Apply slippage from config
698
+ const limitSlippagePercent1 = limitPriceSlippagePercent100 / 100;
699
+ const limitPrice = side === 'sell'
700
+ ? this.roundPriceForAlpaca(currentPrice * (1 - limitSlippagePercent1)) // Sell slightly lower
701
+ : this.roundPriceForAlpaca(currentPrice * (1 + limitSlippagePercent1)); // Buy slightly higher
702
+ this.log(`Creating extended hours limit order to close ${position.symbol} position: ${side} ${qty} shares at $${limitPrice.toFixed(2)}`, {
703
+ symbol: position.symbol,
704
+ });
705
+ await this.createLimitOrder(position.symbol, qty, side, limitPrice, positionIntent, true // Enable extended hours trading
706
+ );
707
+ }
708
+ this.log(`All positions closed: ${positions.map((p) => p.symbol).join(', ')}`);
709
+ }
710
+ onTradeUpdate(callback) {
711
+ this.tradeUpdateCallback = callback;
712
+ }
713
+ /**
714
+ * Get portfolio history for the account
715
+ * @param params Parameters for the portfolio history request
716
+ * @returns Portfolio history data
717
+ */
718
+ async getPortfolioHistory(params) {
719
+ const queryParams = new URLSearchParams();
720
+ if (params.timeframe)
721
+ queryParams.append('timeframe', params.timeframe);
722
+ if (params.period)
723
+ queryParams.append('period', params.period);
724
+ if (params.extended_hours !== undefined)
725
+ queryParams.append('extended_hours', params.extended_hours.toString());
726
+ if (params.start)
727
+ queryParams.append('start', params.start);
728
+ if (params.end)
729
+ queryParams.append('end', params.end);
730
+ if (params.date_end)
731
+ queryParams.append('date_end', params.date_end);
732
+ const response = await this.makeRequest(`/account/portfolio/history?${queryParams.toString()}`);
733
+ return response;
734
+ }
735
+ /**
736
+ * Get portfolio daily history for the account, ensuring the most recent day is included
737
+ * by combining daily and hourly history if needed.
738
+ *
739
+ * This function performs two API calls:
740
+ * 1. Retrieves daily portfolio history
741
+ * 2. Retrieves hourly portfolio history to check for more recent data
742
+ *
743
+ * If hourly history has timestamps more recent than the last timestamp in daily history,
744
+ * it appends one additional day to the daily history using the most recent hourly values.
745
+ *
746
+ * @param params Parameters for the portfolio history request (same as getPortfolioHistory except timeframe is forced to '1D')
747
+ * @returns Portfolio history data with daily timeframe, including the most recent day if available from hourly data
748
+ */
749
+ async getPortfolioDailyHistory(params) {
750
+ // Get daily history
751
+ const dailyParams = { ...params, timeframe: '1D' };
752
+ const dailyHistory = await this.getPortfolioHistory(dailyParams);
753
+ // Get hourly history for the last day to check for more recent data
754
+ const hourlyParams = { timeframe: '1H', period: '1D' };
755
+ const hourlyHistory = await this.getPortfolioHistory(hourlyParams);
756
+ // If no hourly history, return daily as-is
757
+ if (!hourlyHistory.timestamp || hourlyHistory.timestamp.length === 0) {
758
+ return dailyHistory;
759
+ }
760
+ // Get the last timestamp from daily history
761
+ const lastDailyTimestamp = dailyHistory.timestamp[dailyHistory.timestamp.length - 1];
762
+ // Check if hourly history has more recent data
763
+ const recentHourlyData = hourlyHistory.timestamp
764
+ .map((timestamp, index) => ({ timestamp, index }))
765
+ .filter(({ timestamp }) => timestamp > lastDailyTimestamp);
766
+ // If no more recent hourly data, return daily history as-is
767
+ if (recentHourlyData.length === 0) {
768
+ return dailyHistory;
769
+ }
770
+ // Get the most recent hourly data point
771
+ const mostRecentHourly = recentHourlyData[recentHourlyData.length - 1];
772
+ const mostRecentIndex = mostRecentHourly.index;
773
+ // Calculate the timestamp for the new daily entry (most recent day + 1 day worth of seconds)
774
+ const oneDayInSeconds = 24 * 60 * 60;
775
+ const newDailyTimestamp = mostRecentHourly.timestamp + oneDayInSeconds;
776
+ // Create a new daily history entry with the most recent hourly values
777
+ const updatedDailyHistory = {
778
+ ...dailyHistory,
779
+ timestamp: [...dailyHistory.timestamp, newDailyTimestamp],
780
+ equity: [...dailyHistory.equity, hourlyHistory.equity[mostRecentIndex]],
781
+ profit_loss: [...dailyHistory.profit_loss, hourlyHistory.profit_loss[mostRecentIndex]],
782
+ profit_loss_pct: [...dailyHistory.profit_loss_pct, hourlyHistory.profit_loss_pct[mostRecentIndex]],
783
+ };
784
+ return updatedDailyHistory;
785
+ }
786
+ /**
787
+ * Get option contracts based on specified parameters
788
+ * @param params Parameters to filter option contracts
789
+ * @returns Option contracts matching the criteria
790
+ */
791
+ async getOptionContracts(params) {
792
+ const queryParams = new URLSearchParams();
793
+ queryParams.append('underlying_symbols', params.underlying_symbols.join(','));
794
+ if (params.expiration_date_gte)
795
+ queryParams.append('expiration_date_gte', params.expiration_date_gte);
796
+ if (params.expiration_date_lte)
797
+ queryParams.append('expiration_date_lte', params.expiration_date_lte);
798
+ if (params.strike_price_gte)
799
+ queryParams.append('strike_price_gte', params.strike_price_gte);
800
+ if (params.strike_price_lte)
801
+ queryParams.append('strike_price_lte', params.strike_price_lte);
802
+ if (params.type)
803
+ queryParams.append('type', params.type);
804
+ if (params.status)
805
+ queryParams.append('status', params.status);
806
+ if (params.limit)
807
+ queryParams.append('limit', params.limit.toString());
808
+ if (params.page_token)
809
+ queryParams.append('page_token', params.page_token);
810
+ this.log(`Fetching option contracts for ${params.underlying_symbols.join(', ')}`, {
811
+ symbol: params.underlying_symbols.join(', '),
812
+ });
813
+ const response = (await this.makeRequest(`/options/contracts?${queryParams.toString()}`));
814
+ this.log(`Found ${response.option_contracts.length} option contracts`, {
815
+ symbol: params.underlying_symbols.join(', '),
816
+ });
817
+ return response;
818
+ }
819
+ /**
820
+ * Get a specific option contract by symbol or ID
821
+ * @param symbolOrId The symbol or ID of the option contract
822
+ * @returns The option contract details
823
+ */
824
+ async getOptionContract(symbolOrId) {
825
+ this.log(`Fetching option contract details for ${symbolOrId}`, {
826
+ symbol: symbolOrId,
827
+ });
828
+ const response = (await this.makeRequest(`/options/contracts/${symbolOrId}`));
829
+ this.log(`Found option contract details for ${symbolOrId}: ${response.name}`, {
830
+ symbol: symbolOrId,
831
+ });
832
+ return response;
833
+ }
834
+ /**
835
+ * Create a simple option order (market or limit)
836
+ * @param symbol Option contract symbol
837
+ * @param qty Quantity of contracts (must be a whole number)
838
+ * @param side Buy or sell
839
+ * @param position_intent Position intent (buy_to_open, buy_to_close, sell_to_open, sell_to_close)
840
+ * @param type Order type (market or limit)
841
+ * @param limitPrice Limit price (required for limit orders)
842
+ * @returns The created order
843
+ */
844
+ async createOptionOrder(symbol, qty, side, position_intent, type, limitPrice) {
845
+ if (!Number.isInteger(qty) || qty <= 0) {
846
+ this.log('Quantity must be a positive whole number for option orders', { type: 'error' });
847
+ }
848
+ if (type === 'limit' && limitPrice === undefined) {
849
+ this.log('Limit price is required for limit orders', { type: 'error' });
850
+ }
851
+ this.log(`Creating ${type} option order for ${symbol}: ${side} ${qty} contracts (${position_intent})${type === 'limit' ? ` at $${limitPrice?.toFixed(2)}` : ''}`, {
852
+ symbol,
853
+ });
854
+ const orderData = {
855
+ symbol,
856
+ qty: qty.toString(),
857
+ side,
858
+ position_intent,
859
+ type,
860
+ time_in_force: 'day',
861
+ order_class: 'simple',
862
+ extended_hours: false,
863
+ };
864
+ if (type === 'limit' && limitPrice !== undefined) {
865
+ orderData.limit_price = this.roundPriceForAlpaca(limitPrice).toString();
866
+ }
867
+ return this.makeRequest('/orders', 'POST', orderData);
868
+ }
869
+ /**
870
+ * Create a multi-leg option order
871
+ * @param legs Array of order legs
872
+ * @param qty Quantity of the multi-leg order (must be a whole number)
873
+ * @param type Order type (market or limit)
874
+ * @param limitPrice Limit price (required for limit orders)
875
+ * @returns The created multi-leg order
876
+ */
877
+ async createMultiLegOptionOrder(legs, qty, type, limitPrice) {
878
+ if (!Number.isInteger(qty) || qty <= 0) {
879
+ this.log('Quantity must be a positive whole number for option orders', { type: 'error' });
880
+ }
881
+ if (type === 'limit' && limitPrice === undefined) {
882
+ this.log('Limit price is required for limit orders', { type: 'error' });
883
+ }
884
+ if (legs.length < 2) {
885
+ this.log('Multi-leg orders require at least 2 legs', { type: 'error' });
886
+ }
887
+ const legSymbols = legs.map((leg) => leg.symbol).join(', ');
888
+ this.log(`Creating multi-leg ${type} option order with ${legs.length} legs (${legSymbols})${type === 'limit' ? ` at $${limitPrice?.toFixed(2)}` : ''}`, {
889
+ symbol: legSymbols,
890
+ });
891
+ const orderData = {
892
+ order_class: 'mleg',
893
+ qty: qty.toString(),
894
+ type,
895
+ time_in_force: 'day',
896
+ legs,
897
+ };
898
+ if (type === 'limit' && limitPrice !== undefined) {
899
+ orderData.limit_price = this.roundPriceForAlpaca(limitPrice).toString();
900
+ }
901
+ return this.makeRequest('/orders', 'POST', orderData);
902
+ }
903
+ /**
904
+ * Exercise an option contract
905
+ * @param symbolOrContractId The symbol or ID of the option contract to exercise
906
+ * @returns Response from the exercise request
907
+ */
908
+ async exerciseOption(symbolOrContractId) {
909
+ this.log(`Exercising option contract ${symbolOrContractId}`, {
910
+ symbol: symbolOrContractId,
911
+ });
912
+ return this.makeRequest(`/positions/${symbolOrContractId}/exercise`, 'POST');
913
+ }
914
+ /**
915
+ * Get option positions
916
+ * @returns Array of option positions
917
+ */
918
+ async getOptionPositions() {
919
+ this.log('Fetching option positions');
920
+ const positions = await this.getPositions('us_option');
921
+ return positions;
922
+ }
923
+ async getOptionsOpenSpreadTrades() {
924
+ this.log('Fetching option open trades');
925
+ // this function will get all open positions, extract the symbol and see when they were created.
926
+ // figures out when the earliest date was (should be today)
927
+ // then it pulls all orders after the earliest date that were closed and that were of class 'mleg'
928
+ // Each of these contains two orders. they look like this:
929
+ }
930
+ /**
931
+ * Get option account activities (exercises, assignments, expirations)
932
+ * @param activityType Type of option activity to filter by
933
+ * @param date Date to filter activities (YYYY-MM-DD format)
934
+ * @returns Array of option account activities
935
+ */
936
+ async getOptionActivities(activityType, date) {
937
+ const queryParams = new URLSearchParams();
938
+ if (activityType) {
939
+ queryParams.append('activity_types', activityType);
940
+ }
941
+ else {
942
+ queryParams.append('activity_types', 'OPEXC,OPASN,OPEXP');
943
+ }
944
+ if (date) {
945
+ queryParams.append('date', date);
946
+ }
947
+ this.log(`Fetching option activities${activityType ? ` of type ${activityType}` : ''}${date ? ` for date ${date}` : ''}`);
948
+ return this.makeRequest(`/account/activities?${queryParams.toString()}`);
949
+ }
950
+ /**
951
+ * Create a long call spread (buy lower strike call, sell higher strike call)
952
+ * @param lowerStrikeCallSymbol Symbol of the lower strike call option
953
+ * @param higherStrikeCallSymbol Symbol of the higher strike call option
954
+ * @param qty Quantity of spreads to create (must be a whole number)
955
+ * @param limitPrice Limit price for the spread
956
+ * @returns The created multi-leg order
957
+ */
958
+ async createLongCallSpread(lowerStrikeCallSymbol, higherStrikeCallSymbol, qty, limitPrice) {
959
+ this.log(`Creating long call spread: Buy ${lowerStrikeCallSymbol}, Sell ${higherStrikeCallSymbol}, Qty: ${qty}, Price: $${limitPrice.toFixed(2)}`, {
960
+ symbol: `${lowerStrikeCallSymbol},${higherStrikeCallSymbol}`,
961
+ });
962
+ const legs = [
963
+ {
964
+ symbol: lowerStrikeCallSymbol,
965
+ ratio_qty: '1',
966
+ side: 'buy',
967
+ position_intent: 'buy_to_open',
968
+ },
969
+ {
970
+ symbol: higherStrikeCallSymbol,
971
+ ratio_qty: '1',
972
+ side: 'sell',
973
+ position_intent: 'sell_to_open',
974
+ },
975
+ ];
976
+ return this.createMultiLegOptionOrder(legs, qty, 'limit', limitPrice);
977
+ }
978
+ /**
979
+ * Create a long put spread (buy higher strike put, sell lower strike put)
980
+ * @param higherStrikePutSymbol Symbol of the higher strike put option
981
+ * @param lowerStrikePutSymbol Symbol of the lower strike put option
982
+ * @param qty Quantity of spreads to create (must be a whole number)
983
+ * @param limitPrice Limit price for the spread
984
+ * @returns The created multi-leg order
985
+ */
986
+ async createLongPutSpread(higherStrikePutSymbol, lowerStrikePutSymbol, qty, limitPrice) {
987
+ this.log(`Creating long put spread: Buy ${higherStrikePutSymbol}, Sell ${lowerStrikePutSymbol}, Qty: ${qty}, Price: $${limitPrice.toFixed(2)}`, {
988
+ symbol: `${higherStrikePutSymbol},${lowerStrikePutSymbol}`,
989
+ });
990
+ const legs = [
991
+ {
992
+ symbol: higherStrikePutSymbol,
993
+ ratio_qty: '1',
994
+ side: 'buy',
995
+ position_intent: 'buy_to_open',
996
+ },
997
+ {
998
+ symbol: lowerStrikePutSymbol,
999
+ ratio_qty: '1',
1000
+ side: 'sell',
1001
+ position_intent: 'sell_to_open',
1002
+ },
1003
+ ];
1004
+ return this.createMultiLegOptionOrder(legs, qty, 'limit', limitPrice);
1005
+ }
1006
+ /**
1007
+ * Create an iron condor (sell call spread and put spread)
1008
+ * @param longPutSymbol Symbol of the lower strike put (long)
1009
+ * @param shortPutSymbol Symbol of the higher strike put (short)
1010
+ * @param shortCallSymbol Symbol of the lower strike call (short)
1011
+ * @param longCallSymbol Symbol of the higher strike call (long)
1012
+ * @param qty Quantity of iron condors to create (must be a whole number)
1013
+ * @param limitPrice Limit price for the iron condor (credit)
1014
+ * @returns The created multi-leg order
1015
+ */
1016
+ async createIronCondor(longPutSymbol, shortPutSymbol, shortCallSymbol, longCallSymbol, qty, limitPrice) {
1017
+ this.log(`Creating iron condor with ${qty} contracts at $${limitPrice.toFixed(2)}`, {
1018
+ symbol: `${longPutSymbol},${shortPutSymbol},${shortCallSymbol},${longCallSymbol}`,
1019
+ });
1020
+ const legs = [
1021
+ {
1022
+ symbol: longPutSymbol,
1023
+ ratio_qty: '1',
1024
+ side: 'buy',
1025
+ position_intent: 'buy_to_open',
1026
+ },
1027
+ {
1028
+ symbol: shortPutSymbol,
1029
+ ratio_qty: '1',
1030
+ side: 'sell',
1031
+ position_intent: 'sell_to_open',
1032
+ },
1033
+ {
1034
+ symbol: shortCallSymbol,
1035
+ ratio_qty: '1',
1036
+ side: 'sell',
1037
+ position_intent: 'sell_to_open',
1038
+ },
1039
+ {
1040
+ symbol: longCallSymbol,
1041
+ ratio_qty: '1',
1042
+ side: 'buy',
1043
+ position_intent: 'buy_to_open',
1044
+ },
1045
+ ];
1046
+ try {
1047
+ return await this.createMultiLegOptionOrder(legs, qty, 'limit', limitPrice);
1048
+ }
1049
+ catch (error) {
1050
+ this.log(`Error creating iron condor: ${error}`, { type: 'error' });
1051
+ throw error;
1052
+ }
1053
+ }
1054
+ /**
1055
+ * Create a covered call (sell call option against owned stock)
1056
+ * @param stockSymbol Symbol of the underlying stock
1057
+ * @param callOptionSymbol Symbol of the call option to sell
1058
+ * @param qty Quantity of covered calls to create (must be a whole number)
1059
+ * @param limitPrice Limit price for the call option
1060
+ * @returns The created order
1061
+ */
1062
+ async createCoveredCall(stockSymbol, callOptionSymbol, qty, limitPrice) {
1063
+ this.log(`Creating covered call: Sell ${callOptionSymbol} against ${stockSymbol}, Qty: ${qty}, Price: $${limitPrice.toFixed(2)}`, {
1064
+ symbol: `${stockSymbol},${callOptionSymbol}`,
1065
+ });
1066
+ // For covered calls, we don't need to include the stock leg if we already own the shares
1067
+ // We just create a simple sell order for the call option
1068
+ try {
1069
+ return await this.createOptionOrder(callOptionSymbol, qty, 'sell', 'sell_to_open', 'limit', limitPrice);
1070
+ }
1071
+ catch (error) {
1072
+ this.log(`Error creating covered call: ${error}`, { type: 'error' });
1073
+ throw error;
1074
+ }
1075
+ }
1076
+ /**
1077
+ * Roll an option position to a new expiration or strike
1078
+ * @param currentOptionSymbol Symbol of the current option position
1079
+ * @param newOptionSymbol Symbol of the new option to roll to
1080
+ * @param qty Quantity of options to roll (must be a whole number)
1081
+ * @param currentPositionSide Side of the current position ('buy' or 'sell')
1082
+ * @param limitPrice Net limit price for the roll
1083
+ * @returns The created multi-leg order
1084
+ */
1085
+ async rollOptionPosition(currentOptionSymbol, newOptionSymbol, qty, currentPositionSide, limitPrice) {
1086
+ this.log(`Rolling ${qty} ${currentOptionSymbol} to ${newOptionSymbol} at net price $${limitPrice.toFixed(2)}`, {
1087
+ symbol: `${currentOptionSymbol},${newOptionSymbol}`,
1088
+ });
1089
+ // If current position is long, we need to sell to close and buy to open
1090
+ // If current position is short, we need to buy to close and sell to open
1091
+ const closePositionSide = currentPositionSide === 'buy' ? 'sell' : 'buy';
1092
+ const openPositionSide = currentPositionSide;
1093
+ const closePositionIntent = closePositionSide === 'buy' ? 'buy_to_close' : 'sell_to_close';
1094
+ const openPositionIntent = openPositionSide === 'buy' ? 'buy_to_open' : 'sell_to_open';
1095
+ const legs = [
1096
+ {
1097
+ symbol: currentOptionSymbol,
1098
+ ratio_qty: '1',
1099
+ side: closePositionSide,
1100
+ position_intent: closePositionIntent,
1101
+ },
1102
+ {
1103
+ symbol: newOptionSymbol,
1104
+ ratio_qty: '1',
1105
+ side: openPositionSide,
1106
+ position_intent: openPositionIntent,
1107
+ },
1108
+ ];
1109
+ try {
1110
+ return await this.createMultiLegOptionOrder(legs, qty, 'limit', limitPrice);
1111
+ }
1112
+ catch (error) {
1113
+ this.log(`Error rolling option position: ${error}`, { type: 'error' });
1114
+ throw error;
1115
+ }
1116
+ }
1117
+ /**
1118
+ * Get option chain for a specific underlying symbol and expiration date
1119
+ * @param underlyingSymbol The underlying stock symbol
1120
+ * @param expirationDate The expiration date (YYYY-MM-DD format)
1121
+ * @returns Option contracts for the specified symbol and expiration date
1122
+ */
1123
+ async getOptionChain(underlyingSymbol, expirationDate) {
1124
+ this.log(`Fetching option chain for ${underlyingSymbol} with expiration date ${expirationDate}`, {
1125
+ symbol: underlyingSymbol,
1126
+ });
1127
+ try {
1128
+ const params = {
1129
+ underlying_symbols: [underlyingSymbol],
1130
+ expiration_date_gte: expirationDate,
1131
+ expiration_date_lte: expirationDate,
1132
+ status: 'active',
1133
+ limit: 500, // Get a large number to ensure we get all strikes
1134
+ };
1135
+ const response = await this.getOptionContracts(params);
1136
+ return response.option_contracts || [];
1137
+ }
1138
+ catch (error) {
1139
+ this.log(`Failed to fetch option chain for ${underlyingSymbol}: ${error instanceof Error ? error.message : 'Unknown error'}`, {
1140
+ type: 'error',
1141
+ symbol: underlyingSymbol,
1142
+ });
1143
+ return [];
1144
+ }
1145
+ }
1146
+ /**
1147
+ * Get all available expiration dates for a specific underlying symbol
1148
+ * @param underlyingSymbol The underlying stock symbol
1149
+ * @returns Array of available expiration dates
1150
+ */
1151
+ async getOptionExpirationDates(underlyingSymbol) {
1152
+ this.log(`Fetching available expiration dates for ${underlyingSymbol}`, {
1153
+ symbol: underlyingSymbol,
1154
+ });
1155
+ try {
1156
+ const params = {
1157
+ underlying_symbols: [underlyingSymbol],
1158
+ status: 'active',
1159
+ limit: 1000, // Get a large number to ensure we get contracts with all expiration dates
1160
+ };
1161
+ const response = await this.getOptionContracts(params);
1162
+ // Extract unique expiration dates
1163
+ const expirationDates = new Set();
1164
+ if (response.option_contracts) {
1165
+ response.option_contracts.forEach((contract) => {
1166
+ expirationDates.add(contract.expiration_date);
1167
+ });
1168
+ }
1169
+ // Convert to array and sort
1170
+ return Array.from(expirationDates).sort();
1171
+ }
1172
+ catch (error) {
1173
+ this.log(`Failed to fetch expiration dates for ${underlyingSymbol}: ${error instanceof Error ? error.message : 'Unknown error'}`, {
1174
+ type: 'error',
1175
+ symbol: underlyingSymbol,
1176
+ });
1177
+ return [];
1178
+ }
1179
+ }
1180
+ /**
1181
+ * Get the current options trading level for the account
1182
+ * @returns The options trading level (0-3)
1183
+ */
1184
+ async getOptionsTradingLevel() {
1185
+ this.log('Fetching options trading level');
1186
+ const accountDetails = await this.getAccountDetails();
1187
+ return accountDetails.options_trading_level || 0;
1188
+ }
1189
+ /**
1190
+ * Check if the account has options trading enabled
1191
+ * @returns Boolean indicating if options trading is enabled
1192
+ */
1193
+ async isOptionsEnabled() {
1194
+ this.log('Checking if options trading is enabled');
1195
+ const accountDetails = await this.getAccountDetails();
1196
+ // Check if options trading level is 2 or higher (Level 2+ allows buying calls/puts)
1197
+ // Level 0: Options disabled
1198
+ // Level 1: Only covered calls and cash-secured puts
1199
+ // Level 2+: Can buy calls and puts (required for executeOptionsOrder)
1200
+ const optionsLevel = accountDetails.options_trading_level || 0;
1201
+ const isEnabled = optionsLevel >= 2;
1202
+ this.log(`Options trading level: ${optionsLevel}, enabled: ${isEnabled}`);
1203
+ return isEnabled;
1204
+ }
1205
+ /**
1206
+ * Close all option positions
1207
+ * @param cancelOrders Whether to cancel related orders (default: true)
1208
+ * @returns Response from the close positions request
1209
+ */
1210
+ async closeAllOptionPositions(cancelOrders = true) {
1211
+ this.log(`Closing all option positions${cancelOrders ? ' and canceling related orders' : ''}`);
1212
+ const optionPositions = await this.getOptionPositions();
1213
+ if (optionPositions.length === 0) {
1214
+ this.log('No option positions to close');
1215
+ return;
1216
+ }
1217
+ // Create market orders to close each position
1218
+ for (const position of optionPositions) {
1219
+ const side = position.side === 'long' ? 'sell' : 'buy';
1220
+ const positionIntent = side === 'sell' ? 'sell_to_close' : 'buy_to_close';
1221
+ this.log(`Closing ${position.side} position of ${position.qty} contracts for ${position.symbol}`, {
1222
+ symbol: position.symbol,
1223
+ });
1224
+ await this.createOptionOrder(position.symbol, parseInt(position.qty), side, positionIntent, 'market');
1225
+ }
1226
+ if (cancelOrders) {
1227
+ // Get all open option orders
1228
+ const orders = await this.getOrders({ status: 'open' });
1229
+ const optionOrders = orders.filter((order) => order.asset_class === 'us_option');
1230
+ // Cancel each open option order
1231
+ for (const order of optionOrders) {
1232
+ this.log(`Canceling open order for ${order.symbol}`, {
1233
+ symbol: order.symbol,
1234
+ });
1235
+ await this.makeRequest(`/orders/${order.id}`, 'DELETE');
1236
+ }
1237
+ }
1238
+ }
1239
+ /**
1240
+ * Close a specific option position
1241
+ * @param symbol The option contract symbol
1242
+ * @param qty Optional quantity to close (defaults to entire position)
1243
+ * @returns The created order
1244
+ */
1245
+ async closeOptionPosition(symbol, qty) {
1246
+ this.log(`Closing option position for ${symbol}${qty ? ` (${qty} contracts)` : ''}`, {
1247
+ symbol,
1248
+ });
1249
+ // Get the position details
1250
+ const positions = await this.getOptionPositions();
1251
+ const position = positions.find((p) => p.symbol === symbol);
1252
+ if (!position) {
1253
+ throw new Error(`No position found for option contract ${symbol}`);
1254
+ }
1255
+ const quantityToClose = qty || parseInt(position.qty);
1256
+ const side = position.side === 'long' ? 'sell' : 'buy';
1257
+ const positionIntent = side === 'sell' ? 'sell_to_close' : 'buy_to_close';
1258
+ try {
1259
+ return await this.createOptionOrder(symbol, quantityToClose, side, positionIntent, 'market');
1260
+ }
1261
+ catch (error) {
1262
+ this.log(`Error closing option position: ${error}`, { type: 'error' });
1263
+ throw error;
1264
+ }
1265
+ }
1266
+ /**
1267
+ * Create a complete equities trade with optional stop loss and take profit
1268
+ * @param params Trade parameters including symbol, qty, side, and optional referencePrice
1269
+ * @param options Trade options including order type, extended hours, stop loss, and take profit settings
1270
+ * @returns The created order
1271
+ */
1272
+ async createEquitiesTrade(params, options) {
1273
+ const { symbol, qty, side, referencePrice } = params;
1274
+ const { type = 'market', limitPrice, extendedHours = false, useStopLoss = false, stopPrice, stopPercent100, useTakeProfit = false, takeProfitPrice, takeProfitPercent100, clientOrderId, } = options || {};
1275
+ // Validation: Extended hours + market order is not allowed
1276
+ if (extendedHours && type === 'market') {
1277
+ this.log('Cannot create market order with extended hours enabled', {
1278
+ symbol,
1279
+ type: 'error',
1280
+ });
1281
+ throw new Error('Cannot create market order with extended hours enabled');
1282
+ }
1283
+ // Validation: Limit orders require limit price
1284
+ if (type === 'limit' && limitPrice === undefined) {
1285
+ this.log('Limit price is required for limit orders', {
1286
+ symbol,
1287
+ type: 'error',
1288
+ });
1289
+ throw new Error('Limit price is required for limit orders');
1290
+ }
1291
+ let calculatedStopPrice;
1292
+ let calculatedTakeProfitPrice;
1293
+ // Handle stop loss validation and calculation
1294
+ if (useStopLoss) {
1295
+ if (stopPrice === undefined && stopPercent100 === undefined) {
1296
+ this.log('Either stopPrice or stopPercent100 must be provided when useStopLoss is true', {
1297
+ symbol,
1298
+ type: 'error',
1299
+ });
1300
+ throw new Error('Either stopPrice or stopPercent100 must be provided when useStopLoss is true');
1301
+ }
1302
+ if (stopPercent100 !== undefined) {
1303
+ if (referencePrice === undefined) {
1304
+ this.log('referencePrice is required when using stopPercent100', {
1305
+ symbol,
1306
+ type: 'error',
1307
+ });
1308
+ throw new Error('referencePrice is required when using stopPercent100');
1309
+ }
1310
+ // Calculate stop price based on percentage and side
1311
+ const stopPercentDecimal = stopPercent100 / 100;
1312
+ if (side === 'buy') {
1313
+ // For buy orders, stop loss is below the reference price
1314
+ calculatedStopPrice = referencePrice * (1 - stopPercentDecimal);
1315
+ }
1316
+ else {
1317
+ // For sell orders, stop loss is above the reference price
1318
+ calculatedStopPrice = referencePrice * (1 + stopPercentDecimal);
1319
+ }
1320
+ }
1321
+ else {
1322
+ calculatedStopPrice = stopPrice;
1323
+ }
1324
+ }
1325
+ // Handle take profit validation and calculation
1326
+ if (useTakeProfit) {
1327
+ if (takeProfitPrice === undefined && takeProfitPercent100 === undefined) {
1328
+ this.log('Either takeProfitPrice or takeProfitPercent100 must be provided when useTakeProfit is true', {
1329
+ symbol,
1330
+ type: 'error',
1331
+ });
1332
+ throw new Error('Either takeProfitPrice or takeProfitPercent100 must be provided when useTakeProfit is true');
1333
+ }
1334
+ if (takeProfitPercent100 !== undefined) {
1335
+ if (referencePrice === undefined) {
1336
+ this.log('referencePrice is required when using takeProfitPercent100', {
1337
+ symbol,
1338
+ type: 'error',
1339
+ });
1340
+ throw new Error('referencePrice is required when using takeProfitPercent100');
1341
+ }
1342
+ // Calculate take profit price based on percentage and side
1343
+ const takeProfitPercentDecimal = takeProfitPercent100 / 100;
1344
+ if (side === 'buy') {
1345
+ // For buy orders, take profit is above the reference price
1346
+ calculatedTakeProfitPrice = referencePrice * (1 + takeProfitPercentDecimal);
1347
+ }
1348
+ else {
1349
+ // For sell orders, take profit is below the reference price
1350
+ calculatedTakeProfitPrice = referencePrice * (1 - takeProfitPercentDecimal);
1351
+ }
1352
+ }
1353
+ else {
1354
+ calculatedTakeProfitPrice = takeProfitPrice;
1355
+ }
1356
+ }
1357
+ // Determine order class based on what's enabled
1358
+ let orderClass = 'simple';
1359
+ if (useStopLoss && useTakeProfit) {
1360
+ orderClass = 'bracket';
1361
+ }
1362
+ else if (useStopLoss || useTakeProfit) {
1363
+ orderClass = 'oto';
1364
+ }
1365
+ // Build the order request
1366
+ const orderData = {
1367
+ symbol,
1368
+ qty: Math.abs(qty).toString(),
1369
+ side,
1370
+ type,
1371
+ time_in_force: 'day',
1372
+ order_class: orderClass,
1373
+ extended_hours: extendedHours,
1374
+ position_intent: side === 'buy' ? 'buy_to_open' : 'sell_to_open',
1375
+ };
1376
+ if (clientOrderId) {
1377
+ orderData.client_order_id = clientOrderId;
1378
+ }
1379
+ // Add limit price for limit orders
1380
+ if (type === 'limit' && limitPrice !== undefined) {
1381
+ orderData.limit_price = this.roundPriceForAlpaca(limitPrice).toString();
1382
+ }
1383
+ // Add stop loss if enabled
1384
+ if (useStopLoss && calculatedStopPrice !== undefined) {
1385
+ orderData.stop_loss = {
1386
+ stop_price: this.roundPriceForAlpaca(calculatedStopPrice).toString(),
1387
+ };
1388
+ }
1389
+ // Add take profit if enabled
1390
+ if (useTakeProfit && calculatedTakeProfitPrice !== undefined) {
1391
+ orderData.take_profit = {
1392
+ limit_price: this.roundPriceForAlpaca(calculatedTakeProfitPrice).toString(),
1393
+ };
1394
+ }
1395
+ const logMessage = `Creating ${orderClass} ${type} ${side} order for ${symbol}: ${qty} shares${type === 'limit' ? ` at $${limitPrice?.toFixed(2)}` : ''}${useStopLoss ? ` with stop loss at $${calculatedStopPrice?.toFixed(2)}` : ''}${useTakeProfit ? ` with take profit at $${calculatedTakeProfitPrice?.toFixed(2)}` : ''}${extendedHours ? ' (extended hours)' : ''}`;
1396
+ this.log(logMessage, {
1397
+ symbol,
1398
+ });
1399
+ try {
1400
+ return await this.makeRequest('/orders', 'POST', orderData);
1401
+ }
1402
+ catch (error) {
1403
+ this.log(`Error creating equities trade: ${error}`, {
1404
+ symbol,
1405
+ type: 'error',
1406
+ });
1407
+ throw error;
1408
+ }
1409
+ }
1410
+ }
1411
+
1412
+ export { AlpacaTradingAPI };
1413
+ //# sourceMappingURL=alpaca-trading-api-6NxNgQBn.js.map