@discomedia/utils 1.0.55 → 1.0.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/test.js CHANGED
@@ -11,7 +11,6 @@ import require$$0$1 from 'buffer';
11
11
  import require$$0$4 from 'fs';
12
12
  import require$$1$1 from 'path';
13
13
  import require$$2$1 from 'os';
14
- import { log as log$2 } from 'console';
15
14
 
16
15
  /**
17
16
  * Logs a message to the console.
@@ -366,6 +365,21 @@ function getLastFullTradingDateImpl(currentDate = new Date()) {
366
365
  function getLastFullTradingDate(currentDate = new Date()) {
367
366
  return getLastFullTradingDateImpl(currentDate);
368
367
  }
368
+ /**
369
+ * Returns the trading date for a given time. Note: Just trims the date string; does not validate if the date is a market day.
370
+ * @param time - a string, number (unix timestamp), or Date object representing the time
371
+ * @returns the trading date as a string in YYYY-MM-DD format
372
+ */
373
+ /**
374
+ * Returns the trading date for a given time in YYYY-MM-DD format (NY time).
375
+ * @param time - string, number, or Date
376
+ * @returns trading date string
377
+ */
378
+ function getTradingDate(time) {
379
+ const date = typeof time === 'number' ? new Date(time) : typeof time === 'string' ? new Date(time) : time;
380
+ const nyDate = toNYTime(date);
381
+ return `${nyDate.getUTCFullYear()}-${String(nyDate.getUTCMonth() + 1).padStart(2, '0')}-${String(nyDate.getUTCDate()).padStart(2, '0')}`;
382
+ }
369
383
 
370
384
  /*
371
385
  How it works:
@@ -453,8 +467,18 @@ class Queue {
453
467
  }
454
468
 
455
469
  function pLimit(concurrency) {
470
+ let rejectOnClear = false;
471
+
472
+ if (typeof concurrency === 'object') {
473
+ ({concurrency, rejectOnClear = false} = concurrency);
474
+ }
475
+
456
476
  validateConcurrency(concurrency);
457
477
 
478
+ if (typeof rejectOnClear !== 'boolean') {
479
+ throw new TypeError('Expected `rejectOnClear` to be a boolean');
480
+ }
481
+
458
482
  const queue = new Queue();
459
483
  let activeCount = 0;
460
484
 
@@ -462,7 +486,7 @@ function pLimit(concurrency) {
462
486
  // Process the next queued function if we're under the concurrency limit
463
487
  if (activeCount < concurrency && queue.size > 0) {
464
488
  activeCount++;
465
- queue.dequeue()();
489
+ queue.dequeue().run();
466
490
  }
467
491
  };
468
492
 
@@ -489,11 +513,14 @@ function pLimit(concurrency) {
489
513
  next();
490
514
  };
491
515
 
492
- const enqueue = (function_, resolve, arguments_) => {
516
+ const enqueue = (function_, resolve, reject, arguments_) => {
517
+ const queueItem = {reject};
518
+
493
519
  // Queue the internal resolve function instead of the run function
494
520
  // to preserve the asynchronous execution context.
495
521
  new Promise(internalResolve => { // eslint-disable-line promise/param-names
496
- queue.enqueue(internalResolve);
522
+ queueItem.run = internalResolve;
523
+ queue.enqueue(queueItem);
497
524
  }).then(run.bind(undefined, function_, resolve, arguments_)); // eslint-disable-line promise/prefer-await-to-then
498
525
 
499
526
  // Start processing immediately if we haven't reached the concurrency limit
@@ -502,8 +529,8 @@ function pLimit(concurrency) {
502
529
  }
503
530
  };
504
531
 
505
- const generator = (function_, ...arguments_) => new Promise(resolve => {
506
- enqueue(function_, resolve, arguments_);
532
+ const generator = (function_, ...arguments_) => new Promise((resolve, reject) => {
533
+ enqueue(function_, resolve, reject, arguments_);
507
534
  });
508
535
 
509
536
  Object.defineProperties(generator, {
@@ -515,7 +542,16 @@ function pLimit(concurrency) {
515
542
  },
516
543
  clearQueue: {
517
544
  value() {
518
- queue.clear();
545
+ if (!rejectOnClear) {
546
+ queue.clear();
547
+ return;
548
+ }
549
+
550
+ const abortError = AbortSignal.abort().reason;
551
+
552
+ while (queue.size > 0) {
553
+ queue.dequeue().reject(abortError);
554
+ }
519
555
  },
520
556
  },
521
557
  concurrency: {
@@ -801,7 +837,7 @@ const safeJSON = (text) => {
801
837
  // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
802
838
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
803
839
 
804
- const VERSION = '6.16.0'; // x-release-please-version
840
+ const VERSION = '6.17.0'; // x-release-please-version
805
841
 
806
842
  // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
807
843
  const isRunningInBrowser = () => {
@@ -4094,12 +4130,7 @@ class Assistants extends APIResource {
4094
4130
  /**
4095
4131
  * Create an assistant with a model and instructions.
4096
4132
  *
4097
- * @example
4098
- * ```ts
4099
- * const assistant = await client.beta.assistants.create({
4100
- * model: 'gpt-4o',
4101
- * });
4102
- * ```
4133
+ * @deprecated
4103
4134
  */
4104
4135
  create(body, options) {
4105
4136
  return this._client.post('/assistants', {
@@ -4111,12 +4142,7 @@ class Assistants extends APIResource {
4111
4142
  /**
4112
4143
  * Retrieves an assistant.
4113
4144
  *
4114
- * @example
4115
- * ```ts
4116
- * const assistant = await client.beta.assistants.retrieve(
4117
- * 'assistant_id',
4118
- * );
4119
- * ```
4145
+ * @deprecated
4120
4146
  */
4121
4147
  retrieve(assistantID, options) {
4122
4148
  return this._client.get(path `/assistants/${assistantID}`, {
@@ -4127,12 +4153,7 @@ class Assistants extends APIResource {
4127
4153
  /**
4128
4154
  * Modifies an assistant.
4129
4155
  *
4130
- * @example
4131
- * ```ts
4132
- * const assistant = await client.beta.assistants.update(
4133
- * 'assistant_id',
4134
- * );
4135
- * ```
4156
+ * @deprecated
4136
4157
  */
4137
4158
  update(assistantID, body, options) {
4138
4159
  return this._client.post(path `/assistants/${assistantID}`, {
@@ -4144,13 +4165,7 @@ class Assistants extends APIResource {
4144
4165
  /**
4145
4166
  * Returns a list of assistants.
4146
4167
  *
4147
- * @example
4148
- * ```ts
4149
- * // Automatically fetches more pages as needed.
4150
- * for await (const assistant of client.beta.assistants.list()) {
4151
- * // ...
4152
- * }
4153
- * ```
4168
+ * @deprecated
4154
4169
  */
4155
4170
  list(query = {}, options) {
4156
4171
  return this._client.getAPIList('/assistants', (CursorPage), {
@@ -4162,11 +4177,7 @@ class Assistants extends APIResource {
4162
4177
  /**
4163
4178
  * Delete an assistant.
4164
4179
  *
4165
- * @example
4166
- * ```ts
4167
- * const assistantDeleted =
4168
- * await client.beta.assistants.delete('assistant_id');
4169
- * ```
4180
+ * @deprecated
4170
4181
  */
4171
4182
  delete(assistantID, options) {
4172
4183
  return this._client.delete(path `/assistants/${assistantID}`, {
@@ -5640,8 +5651,8 @@ Evals.Runs = Runs;
5640
5651
  let Files$1 = class Files extends APIResource {
5641
5652
  /**
5642
5653
  * Upload a file that can be used across various endpoints. Individual files can be
5643
- * up to 512 MB, and the size of all files uploaded by one organization can be up
5644
- * to 1 TB.
5654
+ * up to 512 MB, and each project can store up to 2.5 TB of files in total. There
5655
+ * is no organization-wide storage limit.
5645
5656
  *
5646
5657
  * - The Assistants API supports files up to 2 million tokens and of specific file
5647
5658
  * types. See the
@@ -14504,82 +14515,1774 @@ class AlpacaMarketDataAPI extends EventEmitter {
14504
14515
  }
14505
14516
  }
14506
14517
  // Export the singleton instance
14507
- AlpacaMarketDataAPI.getInstance();
14518
+ const marketDataAPI = AlpacaMarketDataAPI.getInstance();
14508
14519
 
14509
- // Test file for context functionality
14510
- // testGetTradingDate();
14511
- // testGetTradingStartAndEndDates();
14512
- // testGetLastFullTradingDate();
14513
- // testGetMarketOpenClose();
14514
- // testGetNYTimeZone();
14515
- // testGetNextMarketDay();
14516
- // testCountTradingDays();
14517
- // testGetPreviousMarketDay();
14518
- // testOpenRouter();
14519
- // testGetMarketStatus();
14520
- // testCryptoMarketData();
14520
+ const limitPriceSlippagePercent100 = 0.1; // 0.1%
14521
14521
  /**
14522
- * Test market data subscription for long-running monitoring
14523
- * This test subscribes to minute bars and logs incoming data to verify continuous stream
14524
- * @param symbol Symbol to test (use 'FAKEPACA' for test mode)
14525
- */
14526
- function testMarketDataSubscription(symbol) {
14527
- log$2(`Starting market data subscription test for ${symbol}`);
14528
- const marketDataAPI = AlpacaMarketDataAPI.getInstance();
14529
- // If symbol is FAKEPACA, use test mode
14530
- {
14531
- log$2('Using test mode for FAKEPACA');
14532
- marketDataAPI.setMode('test');
14533
- }
14534
- let barCount = 0;
14535
- let lastBarTime = null;
14536
- const startTime = new Date();
14537
- // Subscribe to minute bars
14538
- marketDataAPI.on('stock-b', (data) => {
14539
- barCount++;
14540
- const now = new Date();
14541
- const barTime = new Date(data.t);
14542
- lastBarTime = barTime;
14543
- const timeSinceStart = Math.floor((now.getTime() - startTime.getTime()) / 1000);
14544
- const minutesSinceStart = Math.floor(timeSinceStart / 60);
14545
- const secondsSinceStart = timeSinceStart % 60;
14546
- log$2(`[${minutesSinceStart}m ${secondsSinceStart}s] Bar #${barCount} for ${data.S}: ` +
14547
- `O=$${data.o.toFixed(2)}, H=$${data.h.toFixed(2)}, L=$${data.l.toFixed(2)}, C=$${data.c.toFixed(2)}, ` +
14548
- `V=${data.v.toLocaleString()}, Time=${barTime.toLocaleString('en-US', { timeZone: 'America/New_York' })}`);
14549
- });
14550
- // Connect and subscribe
14551
- log$2('Connecting to stock stream...');
14552
- marketDataAPI.connectStockStream();
14553
- // Give it a moment to connect, then subscribe
14554
- setTimeout(() => {
14555
- log$2(`Subscribing to minute bars for ${symbol}...`);
14556
- marketDataAPI.subscribe('stock', { bars: [symbol] });
14557
- }, 2000);
14558
- // Log status every 5 minutes
14559
- setInterval(() => {
14560
- const now = new Date();
14561
- const timeSinceStart = Math.floor((now.getTime() - startTime.getTime()) / 1000);
14562
- const minutesSinceStart = Math.floor(timeSinceStart / 60);
14563
- const hoursSinceStart = Math.floor(minutesSinceStart / 60);
14564
- const remainingMinutes = minutesSinceStart % 60;
14565
- const lastBarInfo = lastBarTime
14566
- ? `Last bar: ${lastBarTime.toLocaleString('en-US', { timeZone: 'America/New_York' })}`
14567
- : 'No bars received yet';
14568
- log$2(`Status check - Runtime: ${hoursSinceStart}h ${remainingMinutes}m, ` +
14569
- `Total bars: ${barCount}, ${lastBarInfo}`, { type: 'info' });
14570
- }, 5 * 60 * 1000); // Every 5 minutes
14571
- log$2('Market data subscription test running... (press Ctrl+C to stop)');
14572
- }
14573
- // testGetPortfolioDailyHistory();
14574
- // testWebSocketConnectAndDisconnect();
14575
- // testGetAssetsShortableFilter();
14576
- // testLLM();
14577
- // testImageModelDefaults();
14578
- // testGetTradingDaysBack();
14579
- // testMOOAndMOCOrders();
14580
- // testMarketDataAPI();
14581
- // Test market data subscription with a real symbol or FAKEPACA
14582
- // Uncomment one of the following to test:
14583
- // testMarketDataSubscription('SPY');
14584
- testMarketDataSubscription('FAKEPACA');
14522
+ Websocket example
14523
+ const alpacaAPI = createAlpacaTradingAPI(credentials); // type AlpacaCredentials
14524
+ alpacaAPI.onTradeUpdate((update: TradeUpdate) => {
14525
+ this.log(`Received trade update: event ${update.event} for an order to ${update.order.side} ${update.order.qty} of ${update.order.symbol}`);
14526
+ });
14527
+ alpacaAPI.connectWebsocket(); // necessary to connect to the WebSocket
14528
+
14529
+ Portfolio History examples
14530
+ // Get standard portfolio history
14531
+ const portfolioHistory = await alpacaAPI.getPortfolioHistory({
14532
+ timeframe: '1D',
14533
+ period: '1M'
14534
+ });
14535
+
14536
+ // Get daily portfolio history with current day included (if available from hourly data)
14537
+ const dailyHistory = await alpacaAPI.getPortfolioDailyHistory({
14538
+ period: '1M'
14539
+ });
14540
+ */
14541
+ class AlpacaTradingAPI {
14542
+ static new(credentials) {
14543
+ return new AlpacaTradingAPI(credentials);
14544
+ }
14545
+ static getInstance(credentials) {
14546
+ return new AlpacaTradingAPI(credentials);
14547
+ }
14548
+ ws = null;
14549
+ headers;
14550
+ tradeUpdateCallback = null;
14551
+ credentials;
14552
+ apiBaseUrl;
14553
+ wsUrl;
14554
+ authenticated = false;
14555
+ connecting = false;
14556
+ reconnectDelay = 10000; // 10 seconds between reconnection attempts
14557
+ reconnectTimeout = null;
14558
+ messageHandlers = new Map();
14559
+ debugLogging = false;
14560
+ manualDisconnect = false;
14561
+ /**
14562
+ * Constructor for AlpacaTradingAPI
14563
+ * @param credentials - Alpaca credentials,
14564
+ * accountName: string; // The account identifier used inthis.logs and tracking
14565
+ * apiKey: string; // Alpaca API key
14566
+ * apiSecret: string; // Alpaca API secret
14567
+ * type: AlpacaAccountType;
14568
+ * orderType: AlpacaOrderType;
14569
+ * @param options - Optional options
14570
+ * debugLogging: boolean; // Whether to log messages of type 'debug'
14571
+ */
14572
+ constructor(credentials, options) {
14573
+ this.credentials = credentials;
14574
+ // Set URLs based on account type
14575
+ this.apiBaseUrl =
14576
+ credentials.type === 'PAPER' ? 'https://paper-api.alpaca.markets/v2' : 'https://api.alpaca.markets/v2';
14577
+ this.wsUrl =
14578
+ credentials.type === 'PAPER' ? 'wss://paper-api.alpaca.markets/stream' : 'wss://api.alpaca.markets/stream';
14579
+ this.headers = {
14580
+ 'APCA-API-KEY-ID': credentials.apiKey,
14581
+ 'APCA-API-SECRET-KEY': credentials.apiSecret,
14582
+ 'Content-Type': 'application/json',
14583
+ };
14584
+ // Initialize message handlers
14585
+ this.messageHandlers.set('authorization', this.handleAuthMessage.bind(this));
14586
+ this.messageHandlers.set('listening', this.handleListenMessage.bind(this));
14587
+ this.messageHandlers.set('trade_updates', this.handleTradeUpdate.bind(this));
14588
+ this.debugLogging = options?.debugLogging || false;
14589
+ }
14590
+ log(message, options = { type: 'info' }) {
14591
+ if (this.debugLogging && options.type === 'debug') {
14592
+ return;
14593
+ }
14594
+ log$1(message, { ...options, source: 'AlpacaTradingAPI', account: this.credentials.accountName });
14595
+ }
14596
+ /**
14597
+ * Round a price to the nearest 2 decimal places for Alpaca, or 4 decimal places for prices less than $1
14598
+ * @param price - The price to round
14599
+ * @returns The rounded price
14600
+ */
14601
+ roundPriceForAlpaca = (price) => {
14602
+ return price >= 1 ? Math.round(price * 100) / 100 : Math.round(price * 10000) / 10000;
14603
+ };
14604
+ handleAuthMessage(data) {
14605
+ if (data.status === 'authorized') {
14606
+ this.authenticated = true;
14607
+ this.log('WebSocket authenticated');
14608
+ }
14609
+ else {
14610
+ this.log(`Authentication failed: ${data.message || 'Unknown error'}`, {
14611
+ type: 'error',
14612
+ });
14613
+ }
14614
+ }
14615
+ handleListenMessage(data) {
14616
+ if (data.streams?.includes('trade_updates')) {
14617
+ this.log('Successfully subscribed to trade updates');
14618
+ }
14619
+ }
14620
+ handleTradeUpdate(data) {
14621
+ if (this.tradeUpdateCallback) {
14622
+ this.log(`Trade update: ${data.event} to ${data.order.side} ${data.order.qty} shares${data.event === 'partial_fill' ? ` (filled shares: ${data.order.filled_qty})` : ''}, type ${data.order.type}`, {
14623
+ symbol: data.order.symbol,
14624
+ type: 'debug',
14625
+ });
14626
+ this.tradeUpdateCallback(data);
14627
+ }
14628
+ }
14629
+ handleMessage(message) {
14630
+ try {
14631
+ const data = JSON.parse(message);
14632
+ const handler = this.messageHandlers.get(data.stream);
14633
+ if (handler) {
14634
+ handler(data.data);
14635
+ }
14636
+ else {
14637
+ this.log(`Received message for unknown stream: ${data.stream}`, {
14638
+ type: 'warn',
14639
+ });
14640
+ }
14641
+ }
14642
+ catch (error) {
14643
+ this.log('Failed to parse WebSocket message', {
14644
+ type: 'error',
14645
+ metadata: { error: error instanceof Error ? error.message : 'Unknown error' },
14646
+ });
14647
+ }
14648
+ }
14649
+ connectWebsocket() {
14650
+ // Reset manual disconnect flag to allow reconnection logic
14651
+ this.manualDisconnect = false;
14652
+ if (this.connecting) {
14653
+ this.log('Connection attempt skipped - already connecting');
14654
+ return;
14655
+ }
14656
+ if (this.ws?.readyState === WebSocket.OPEN) {
14657
+ this.log('Connection attempt skipped - already connected');
14658
+ return;
14659
+ }
14660
+ this.connecting = true;
14661
+ if (this.ws) {
14662
+ this.ws.removeAllListeners();
14663
+ this.ws.terminate();
14664
+ this.ws = null;
14665
+ }
14666
+ this.log(`Connecting to WebSocket at ${this.wsUrl}...`);
14667
+ this.ws = new WebSocket(this.wsUrl);
14668
+ this.ws.on('open', async () => {
14669
+ try {
14670
+ this.log('WebSocket connected');
14671
+ await this.authenticate();
14672
+ await this.subscribeToTradeUpdates();
14673
+ this.connecting = false;
14674
+ }
14675
+ catch (error) {
14676
+ this.log('Failed to setup WebSocket connection', {
14677
+ type: 'error',
14678
+ metadata: { error: error instanceof Error ? error.message : 'Unknown error' },
14679
+ });
14680
+ this.ws?.close();
14681
+ }
14682
+ });
14683
+ this.ws.on('message', (data) => {
14684
+ this.handleMessage(data.toString());
14685
+ });
14686
+ this.ws.on('error', (error) => {
14687
+ this.log('WebSocket error', {
14688
+ type: 'error',
14689
+ metadata: { error: error instanceof Error ? error.message : 'Unknown error' },
14690
+ });
14691
+ this.connecting = false;
14692
+ });
14693
+ this.ws.on('close', () => {
14694
+ this.log('WebSocket connection closed');
14695
+ this.authenticated = false;
14696
+ this.connecting = false;
14697
+ // Clear any existing reconnect timeout
14698
+ if (this.reconnectTimeout) {
14699
+ clearTimeout(this.reconnectTimeout);
14700
+ this.reconnectTimeout = null;
14701
+ }
14702
+ // Schedule reconnection unless this was a manual disconnect
14703
+ if (!this.manualDisconnect) {
14704
+ this.reconnectTimeout = setTimeout(() => {
14705
+ this.log('Attempting to reconnect...');
14706
+ this.connectWebsocket();
14707
+ }, this.reconnectDelay);
14708
+ }
14709
+ });
14710
+ }
14711
+ /**
14712
+ * Cleanly disconnect from the WebSocket and stop auto-reconnects
14713
+ */
14714
+ disconnect() {
14715
+ // Prevent auto-reconnect scheduling
14716
+ this.manualDisconnect = true;
14717
+ // Clear any scheduled reconnect
14718
+ if (this.reconnectTimeout) {
14719
+ clearTimeout(this.reconnectTimeout);
14720
+ this.reconnectTimeout = null;
14721
+ }
14722
+ if (this.ws) {
14723
+ this.log('Disconnecting WebSocket...');
14724
+ // Remove listeners first to avoid duplicate handlers after reconnects
14725
+ this.ws.removeAllListeners();
14726
+ try {
14727
+ // Attempt graceful close
14728
+ if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
14729
+ this.ws.close(1000, 'Client disconnect');
14730
+ }
14731
+ else {
14732
+ this.ws.terminate();
14733
+ }
14734
+ }
14735
+ catch {
14736
+ // Fallback terminate on any error
14737
+ try {
14738
+ this.ws.terminate();
14739
+ }
14740
+ catch {
14741
+ /* no-op */
14742
+ }
14743
+ }
14744
+ this.ws = null;
14745
+ }
14746
+ this.authenticated = false;
14747
+ this.connecting = false;
14748
+ this.log('WebSocket disconnected');
14749
+ }
14750
+ async authenticate() {
14751
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
14752
+ throw new Error('WebSocket not ready for authentication');
14753
+ }
14754
+ const authMessage = {
14755
+ action: 'auth',
14756
+ key: this.credentials.apiKey,
14757
+ secret: this.credentials.apiSecret,
14758
+ };
14759
+ this.ws.send(JSON.stringify(authMessage));
14760
+ return new Promise((resolve, reject) => {
14761
+ const authTimeout = setTimeout(() => {
14762
+ this.log('Authentication timeout', { type: 'error' });
14763
+ reject(new Error('Authentication timed out'));
14764
+ }, 10000);
14765
+ const handleAuthResponse = (data) => {
14766
+ try {
14767
+ const message = JSON.parse(data.toString());
14768
+ if (message.stream === 'authorization') {
14769
+ this.ws?.removeListener('message', handleAuthResponse);
14770
+ clearTimeout(authTimeout);
14771
+ if (message.data?.status === 'authorized') {
14772
+ this.authenticated = true;
14773
+ resolve();
14774
+ }
14775
+ else {
14776
+ const error = `Authentication failed: ${message.data?.message || 'Unknown error'}`;
14777
+ this.log(error, { type: 'error' });
14778
+ reject(new Error(error));
14779
+ }
14780
+ }
14781
+ }
14782
+ catch (error) {
14783
+ this.log('Failed to parse auth response', {
14784
+ type: 'error',
14785
+ metadata: { error: error instanceof Error ? error.message : 'Unknown error' },
14786
+ });
14787
+ }
14788
+ };
14789
+ this.ws?.on('message', handleAuthResponse);
14790
+ });
14791
+ }
14792
+ async subscribeToTradeUpdates() {
14793
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.authenticated) {
14794
+ throw new Error('WebSocket not ready for subscription');
14795
+ }
14796
+ const listenMessage = {
14797
+ action: 'listen',
14798
+ data: {
14799
+ streams: ['trade_updates'],
14800
+ },
14801
+ };
14802
+ this.ws.send(JSON.stringify(listenMessage));
14803
+ return new Promise((resolve, reject) => {
14804
+ const listenTimeout = setTimeout(() => {
14805
+ reject(new Error('Subscribe timeout'));
14806
+ }, 10000);
14807
+ const handleListenResponse = (data) => {
14808
+ try {
14809
+ const message = JSON.parse(data.toString());
14810
+ if (message.stream === 'listening') {
14811
+ this.ws?.removeListener('message', handleListenResponse);
14812
+ clearTimeout(listenTimeout);
14813
+ if (message.data?.streams?.includes('trade_updates')) {
14814
+ resolve();
14815
+ }
14816
+ else {
14817
+ reject(new Error('Failed to subscribe to trade updates'));
14818
+ }
14819
+ }
14820
+ }
14821
+ catch (error) {
14822
+ this.log('Failed to parse listen response', {
14823
+ type: 'error',
14824
+ metadata: { error: error instanceof Error ? error.message : 'Unknown error' },
14825
+ });
14826
+ }
14827
+ };
14828
+ this.ws?.on('message', handleListenResponse);
14829
+ });
14830
+ }
14831
+ async makeRequest(endpoint, method = 'GET', body, queryString = '') {
14832
+ const url = `${this.apiBaseUrl}${endpoint}${queryString}`;
14833
+ try {
14834
+ const response = await fetch(url, {
14835
+ method,
14836
+ headers: this.headers,
14837
+ body: body ? JSON.stringify(body) : undefined,
14838
+ });
14839
+ if (!response.ok) {
14840
+ const errorText = await response.text();
14841
+ this.log(`Alpaca API error (${response.status}): ${errorText}`, { type: 'error' });
14842
+ throw new Error(`Alpaca API error (${response.status}): ${errorText}`);
14843
+ }
14844
+ // Handle responses with no content (e.g., 204 No Content)
14845
+ if (response.status === 204 || response.headers.get('content-length') === '0') {
14846
+ return null;
14847
+ }
14848
+ const contentType = response.headers.get('content-type');
14849
+ if (contentType && contentType.includes('application/json')) {
14850
+ return await response.json();
14851
+ }
14852
+ // For non-JSON responses, return the text content
14853
+ const textContent = await response.text();
14854
+ return textContent || null;
14855
+ }
14856
+ catch (err) {
14857
+ const error = err;
14858
+ this.log(`Error in makeRequest: ${error.message}. Url: ${url}`, {
14859
+ source: 'AlpacaAPI',
14860
+ type: 'error',
14861
+ });
14862
+ throw error;
14863
+ }
14864
+ }
14865
+ async getPositions(assetClass) {
14866
+ const positions = (await this.makeRequest('/positions'));
14867
+ if (assetClass) {
14868
+ return positions.filter((position) => position.asset_class === assetClass);
14869
+ }
14870
+ return positions;
14871
+ }
14872
+ /**
14873
+ * Get all orders
14874
+ * @param params (GetOrdersParams) - optional parameters to filter the orders
14875
+ * - status: 'open' | 'closed' | 'all'
14876
+ * - limit: number
14877
+ * - after: string
14878
+ * - until: string
14879
+ * - direction: 'asc' | 'desc'
14880
+ * - nested: boolean
14881
+ * - symbols: string[], an array of all the symbols
14882
+ * - side: 'buy' | 'sell'
14883
+ * @returns all orders
14884
+ */
14885
+ async getOrders(params = {}) {
14886
+ const queryParams = new URLSearchParams();
14887
+ if (params.status)
14888
+ queryParams.append('status', params.status);
14889
+ if (params.limit)
14890
+ queryParams.append('limit', params.limit.toString());
14891
+ if (params.after)
14892
+ queryParams.append('after', params.after);
14893
+ if (params.until)
14894
+ queryParams.append('until', params.until);
14895
+ if (params.direction)
14896
+ queryParams.append('direction', params.direction);
14897
+ if (params.nested)
14898
+ queryParams.append('nested', params.nested.toString());
14899
+ if (params.symbols)
14900
+ queryParams.append('symbols', params.symbols.join(','));
14901
+ if (params.side)
14902
+ queryParams.append('side', params.side);
14903
+ const endpoint = `/orders${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
14904
+ try {
14905
+ return await this.makeRequest(endpoint);
14906
+ }
14907
+ catch (error) {
14908
+ this.log(`Error getting orders: ${error}`, { type: 'error' });
14909
+ throw error;
14910
+ }
14911
+ }
14912
+ async getAccountDetails() {
14913
+ try {
14914
+ return await this.makeRequest('/account');
14915
+ }
14916
+ catch (error) {
14917
+ this.log(`Error getting account details: ${error}`, { type: 'error' });
14918
+ throw error;
14919
+ }
14920
+ }
14921
+ /**
14922
+ * Create a trailing stop order
14923
+ * @param symbol (string) - the symbol of the order
14924
+ * @param qty (number) - the quantity of the order
14925
+ * @param side (string) - the side of the order
14926
+ * @param trailPercent100 (number) - the trail percent of the order (scale 100, i.e. 0.5 = 0.5%)
14927
+ * @param position_intent (string) - the position intent of the order
14928
+ * @param client_order_id (string) - optional client order id
14929
+ * @returns The created trailing stop order
14930
+ */
14931
+ async createTrailingStop(symbol, qty, side, trailPercent100, position_intent, client_order_id) {
14932
+ this.log(`Creating trailing stop ${side.toUpperCase()} ${qty} shares for ${symbol} with trail percent ${trailPercent100}%`, {
14933
+ symbol,
14934
+ });
14935
+ const body = {
14936
+ symbol,
14937
+ qty: Math.abs(qty),
14938
+ side,
14939
+ position_intent,
14940
+ order_class: 'simple',
14941
+ type: 'trailing_stop',
14942
+ trail_percent: trailPercent100, // Already in decimal form (e.g., 4 for 4%)
14943
+ time_in_force: 'gtc',
14944
+ };
14945
+ if (client_order_id !== undefined) {
14946
+ body.client_order_id = client_order_id;
14947
+ }
14948
+ try {
14949
+ return await this.makeRequest(`/orders`, 'POST', body);
14950
+ }
14951
+ catch (error) {
14952
+ this.log(`Error creating trailing stop: ${error}`, {
14953
+ symbol,
14954
+ type: 'error',
14955
+ });
14956
+ throw error;
14957
+ }
14958
+ }
14959
+ /**
14960
+ * Create a stop order (stop or stop-limit)
14961
+ * @param symbol (string) - the symbol of the order
14962
+ * @param qty (number) - the quantity of the order
14963
+ * @param side (string) - the side of the order
14964
+ * @param stopPrice (number) - the stop price that triggers the order
14965
+ * @param position_intent (string) - the position intent of the order
14966
+ * @param limitPrice (number) - optional limit price (if provided, creates a stop-limit order)
14967
+ * @param client_order_id (string) - optional client order id
14968
+ * @returns The created stop order
14969
+ */
14970
+ async createStopOrder(symbol, qty, side, stopPrice, position_intent, limitPrice, client_order_id) {
14971
+ const isStopLimit = limitPrice !== undefined;
14972
+ const orderType = isStopLimit ? 'stop-limit' : 'stop';
14973
+ this.log(`Creating ${orderType} ${side.toUpperCase()} ${qty} shares for ${symbol} with stop price ${stopPrice}${isStopLimit ? ` and limit price ${limitPrice}` : ''}`, {
14974
+ symbol,
14975
+ });
14976
+ const body = {
14977
+ symbol,
14978
+ qty: Math.abs(qty).toString(),
14979
+ side,
14980
+ position_intent,
14981
+ order_class: 'simple',
14982
+ type: isStopLimit ? 'stop_limit' : 'stop',
14983
+ stop_price: this.roundPriceForAlpaca(stopPrice),
14984
+ time_in_force: 'gtc',
14985
+ };
14986
+ if (isStopLimit) {
14987
+ body.limit_price = this.roundPriceForAlpaca(limitPrice);
14988
+ }
14989
+ if (client_order_id !== undefined) {
14990
+ body.client_order_id = client_order_id;
14991
+ }
14992
+ try {
14993
+ return await this.makeRequest(`/orders`, 'POST', body);
14994
+ }
14995
+ catch (error) {
14996
+ this.log(`Error creating ${orderType} order: ${error}`, {
14997
+ symbol,
14998
+ type: 'error',
14999
+ });
15000
+ throw error;
15001
+ }
15002
+ }
15003
+ /**
15004
+ * Create a market order
15005
+ * @param symbol (string) - the symbol of the order
15006
+ * @param qty (number) - the quantity of the order
15007
+ * @param side (string) - the side of the order
15008
+ * @param position_intent (string) - the position intent of the order. Important for knowing if a position needs a trailing stop.
15009
+ * @param client_order_id (string) - optional client order id
15010
+ */
15011
+ async createMarketOrder(symbol, qty, side, position_intent, client_order_id) {
15012
+ this.log(`Creating market order for ${symbol}: ${side} ${qty} shares (${position_intent})`, {
15013
+ symbol,
15014
+ });
15015
+ const body = {
15016
+ symbol,
15017
+ qty: Math.abs(qty).toString(),
15018
+ side,
15019
+ position_intent,
15020
+ type: 'market',
15021
+ time_in_force: 'day',
15022
+ order_class: 'simple',
15023
+ };
15024
+ if (client_order_id !== undefined) {
15025
+ body.client_order_id = client_order_id;
15026
+ }
15027
+ try {
15028
+ return await this.makeRequest('/orders', 'POST', body);
15029
+ }
15030
+ catch (error) {
15031
+ this.log(`Error creating market order: ${error}`, { type: 'error' });
15032
+ throw error;
15033
+ }
15034
+ }
15035
+ /**
15036
+ * Create a Market on Open (MOO) order - executes in the opening auction
15037
+ *
15038
+ * IMPORTANT TIMING CONSTRAINTS:
15039
+ * - Valid submission window: After 7:00pm ET and before 9:28am ET
15040
+ * - Orders submitted between 9:28am and 7:00pm ET will be REJECTED
15041
+ * - Orders submitted after 7:00pm ET are queued for the next trading day's opening auction
15042
+ * - Example: An order at 8:00pm Monday will execute at Tuesday's market open (9:30am)
15043
+ *
15044
+ * @param symbol - The symbol of the order
15045
+ * @param qty - The quantity of shares
15046
+ * @param side - Buy or sell
15047
+ * @param position_intent - The position intent (buy_to_open, sell_to_close, etc.)
15048
+ * @param client_order_id - Optional client order id
15049
+ * @returns The created order
15050
+ */
15051
+ async createMOOOrder(symbol, qty, side, position_intent, client_order_id) {
15052
+ this.log(`Creating Market on Open order for ${symbol}: ${side} ${qty} shares (${position_intent})`, {
15053
+ symbol,
15054
+ });
15055
+ const body = {
15056
+ symbol,
15057
+ qty: Math.abs(qty).toString(),
15058
+ side,
15059
+ position_intent,
15060
+ type: 'market',
15061
+ time_in_force: 'opg',
15062
+ order_class: 'simple',
15063
+ };
15064
+ if (client_order_id !== undefined) {
15065
+ body.client_order_id = client_order_id;
15066
+ }
15067
+ try {
15068
+ return await this.makeRequest('/orders', 'POST', body);
15069
+ }
15070
+ catch (error) {
15071
+ this.log(`Error creating MOO order: ${error}`, { type: 'error' });
15072
+ throw error;
15073
+ }
15074
+ }
15075
+ /**
15076
+ * Create a Market on Close (MOC) order - executes in the closing auction
15077
+ *
15078
+ * IMPORTANT TIMING CONSTRAINTS:
15079
+ * - Valid submission window: After 7:00pm ET (previous day) and before 3:50pm ET (same day)
15080
+ * - Orders submitted between 3:50pm and 7:00pm ET will be REJECTED
15081
+ * - Orders submitted after 7:00pm ET are queued for the next trading day's closing auction
15082
+ * - Example: An order at 8:00pm Monday will execute at Tuesday's market close (4:00pm)
15083
+ *
15084
+ * @param symbol - The symbol of the order
15085
+ * @param qty - The quantity of shares
15086
+ * @param side - Buy or sell
15087
+ * @param position_intent - The position intent (buy_to_open, sell_to_close, etc.)
15088
+ * @param client_order_id - Optional client order id
15089
+ * @returns The created order
15090
+ */
15091
+ async createMOCOrder(symbol, qty, side, position_intent, client_order_id) {
15092
+ this.log(`Creating Market on Close order for ${symbol}: ${side} ${qty} shares (${position_intent})`, {
15093
+ symbol,
15094
+ });
15095
+ const body = {
15096
+ symbol,
15097
+ qty: Math.abs(qty).toString(),
15098
+ side,
15099
+ position_intent,
15100
+ type: 'market',
15101
+ time_in_force: 'cls',
15102
+ order_class: 'simple',
15103
+ };
15104
+ if (client_order_id !== undefined) {
15105
+ body.client_order_id = client_order_id;
15106
+ }
15107
+ try {
15108
+ return await this.makeRequest('/orders', 'POST', body);
15109
+ }
15110
+ catch (error) {
15111
+ this.log(`Error creating MOC order: ${error}`, { type: 'error' });
15112
+ throw error;
15113
+ }
15114
+ }
15115
+ /**
15116
+ * Create an OCO (One-Cancels-Other) order with take profit and stop loss
15117
+ * @param symbol (string) - the symbol of the order
15118
+ * @param qty (number) - the quantity of the order
15119
+ * @param side (string) - the side of the order (buy or sell)
15120
+ * @param position_intent (string) - the position intent of the order
15121
+ * @param limitPrice (number) - the limit price for the entry order (OCO orders must be limit orders)
15122
+ * @param takeProfitPrice (number) - the take profit price
15123
+ * @param stopLossPrice (number) - the stop loss price
15124
+ * @param stopLossLimitPrice (number) - optional limit price for stop loss (creates stop-limit instead of stop)
15125
+ * @param client_order_id (string) - optional client order id
15126
+ * @returns The created OCO order
15127
+ */
15128
+ async createOCOOrder(symbol, qty, side, position_intent, limitPrice, takeProfitPrice, stopLossPrice, stopLossLimitPrice, client_order_id) {
15129
+ this.log(`Creating OCO order ${side.toUpperCase()} ${qty} shares for ${symbol} at limit ${limitPrice} with take profit ${takeProfitPrice} and stop loss ${stopLossPrice}`, {
15130
+ symbol,
15131
+ });
15132
+ const body = {
15133
+ symbol,
15134
+ qty: Math.abs(qty).toString(),
15135
+ side,
15136
+ position_intent,
15137
+ order_class: 'oco',
15138
+ type: 'limit',
15139
+ limit_price: this.roundPriceForAlpaca(limitPrice),
15140
+ time_in_force: 'gtc',
15141
+ take_profit: {
15142
+ limit_price: this.roundPriceForAlpaca(takeProfitPrice),
15143
+ },
15144
+ stop_loss: {
15145
+ stop_price: this.roundPriceForAlpaca(stopLossPrice),
15146
+ },
15147
+ };
15148
+ // If stop loss limit price is provided, create stop-limit order
15149
+ if (stopLossLimitPrice !== undefined) {
15150
+ body.stop_loss.limit_price = this.roundPriceForAlpaca(stopLossLimitPrice);
15151
+ }
15152
+ if (client_order_id !== undefined) {
15153
+ body.client_order_id = client_order_id;
15154
+ }
15155
+ try {
15156
+ return await this.makeRequest(`/orders`, 'POST', body);
15157
+ }
15158
+ catch (error) {
15159
+ this.log(`Error creating OCO order: ${error}`, {
15160
+ symbol,
15161
+ type: 'error',
15162
+ });
15163
+ throw error;
15164
+ }
15165
+ }
15166
+ /**
15167
+ * 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.
15168
+ * @param symbol (string) - the symbol of the order
15169
+ * @returns the current trail percent
15170
+ */
15171
+ async getCurrentTrailPercent(symbol) {
15172
+ try {
15173
+ const orders = await this.getOrders({
15174
+ status: 'open',
15175
+ symbols: [symbol],
15176
+ });
15177
+ const trailingStopOrder = orders.find((order) => order.type === 'trailing_stop' &&
15178
+ (order.position_intent === 'sell_to_close' || order.position_intent === 'buy_to_close'));
15179
+ if (!trailingStopOrder) {
15180
+ this.log(`No closing trailing stop order found for ${symbol}`, {
15181
+ symbol,
15182
+ });
15183
+ return null;
15184
+ }
15185
+ if (!trailingStopOrder.trail_percent) {
15186
+ this.log(`Trailing stop order found for ${symbol} but no trail_percent value`, {
15187
+ symbol,
15188
+ });
15189
+ return null;
15190
+ }
15191
+ const trailPercent = parseFloat(trailingStopOrder.trail_percent);
15192
+ return trailPercent;
15193
+ }
15194
+ catch (error) {
15195
+ this.log(`Error getting current trail percent: ${error}`, {
15196
+ symbol,
15197
+ type: 'error',
15198
+ });
15199
+ throw error;
15200
+ }
15201
+ }
15202
+ /**
15203
+ * Update the trail percent for a trailing stop order
15204
+ * @param symbol (string) - the symbol of the order
15205
+ * @param trailPercent100 (number) - the trail percent of the order (scale 100, i.e. 0.5 = 0.5%)
15206
+ */
15207
+ async updateTrailingStop(symbol, trailPercent100) {
15208
+ // First get all open orders for this symbol
15209
+ const orders = await this.getOrders({
15210
+ status: 'open',
15211
+ symbols: [symbol],
15212
+ });
15213
+ // Find the trailing stop order
15214
+ const trailingStopOrder = orders.find((order) => order.type === 'trailing_stop');
15215
+ if (!trailingStopOrder) {
15216
+ this.log(`No open trailing stop order found for ${symbol}`, { type: 'error', symbol });
15217
+ return;
15218
+ }
15219
+ // Check if the trail_percent is already set to the desired value
15220
+ const currentTrailPercent = trailingStopOrder.trail_percent ? parseFloat(trailingStopOrder.trail_percent) : null;
15221
+ // Compare with a small epsilon to handle floating point precision
15222
+ const epsilon = 0.0001;
15223
+ if (currentTrailPercent !== null && Math.abs(currentTrailPercent - trailPercent100) < epsilon) {
15224
+ this.log(`Trailing stop for ${symbol} already set to ${trailPercent100}% (current: ${currentTrailPercent}%), skipping update`, {
15225
+ symbol,
15226
+ });
15227
+ return;
15228
+ }
15229
+ this.log(`Updating trailing stop for ${symbol} from ${currentTrailPercent}% to ${trailPercent100}%`, {
15230
+ symbol,
15231
+ });
15232
+ try {
15233
+ await this.makeRequest(`/orders/${trailingStopOrder.id}`, 'PATCH', {
15234
+ trail: trailPercent100.toString(), // Changed from trail_percent to trail
15235
+ });
15236
+ }
15237
+ catch (error) {
15238
+ this.log(`Error updating trailing stop: ${error}`, {
15239
+ symbol,
15240
+ type: 'error',
15241
+ });
15242
+ throw error;
15243
+ }
15244
+ }
15245
+ /**
15246
+ * Cancel all open orders
15247
+ */
15248
+ async cancelAllOrders() {
15249
+ this.log(`Canceling all open orders`);
15250
+ try {
15251
+ await this.makeRequest('/orders', 'DELETE');
15252
+ }
15253
+ catch (error) {
15254
+ this.log(`Error canceling all orders: ${error}`, { type: 'error' });
15255
+ }
15256
+ }
15257
+ /**
15258
+ * Cancel a specific order by its ID
15259
+ * @param orderId The id of the order to cancel
15260
+ * @throws Error if the order is not cancelable (status 422) or if the order doesn't exist
15261
+ * @returns Promise that resolves when the order is successfully canceled
15262
+ */
15263
+ async cancelOrder(orderId) {
15264
+ this.log(`Attempting to cancel order ${orderId}`);
15265
+ try {
15266
+ await this.makeRequest(`/orders/${orderId}`, 'DELETE');
15267
+ this.log(`Successfully canceled order ${orderId}`);
15268
+ }
15269
+ catch (error) {
15270
+ // If the error is a 422, it means the order is not cancelable
15271
+ if (error instanceof Error && error.message.includes('422')) {
15272
+ this.log(`Order ${orderId} is not cancelable`, {
15273
+ type: 'error',
15274
+ });
15275
+ throw new Error(`Order ${orderId} is not cancelable`);
15276
+ }
15277
+ // Re-throw other errors
15278
+ throw error;
15279
+ }
15280
+ }
15281
+ /**
15282
+ * Create a limit order
15283
+ * @param symbol (string) - the symbol of the order
15284
+ * @param qty (number) - the quantity of the order
15285
+ * @param side (string) - the side of the order
15286
+ * @param limitPrice (number) - the limit price of the order
15287
+ * @param position_intent (string) - the position intent of the order
15288
+ * @param extended_hours (boolean) - whether the order is in extended hours
15289
+ * @param client_order_id (string) - the client order id of the order
15290
+ */
15291
+ async createLimitOrder(symbol, qty, side, limitPrice, position_intent, extended_hours = false, client_order_id) {
15292
+ this.log(`Creating limit order for ${symbol}: ${side} ${qty} shares at $${limitPrice.toFixed(2)} (${position_intent})`, {
15293
+ symbol,
15294
+ });
15295
+ const body = {
15296
+ symbol,
15297
+ qty: Math.abs(qty).toString(),
15298
+ side,
15299
+ position_intent,
15300
+ type: 'limit',
15301
+ limit_price: this.roundPriceForAlpaca(limitPrice).toString(),
15302
+ time_in_force: 'day',
15303
+ order_class: 'simple',
15304
+ extended_hours,
15305
+ };
15306
+ if (client_order_id !== undefined) {
15307
+ body.client_order_id = client_order_id;
15308
+ }
15309
+ try {
15310
+ return await this.makeRequest('/orders', 'POST', body);
15311
+ }
15312
+ catch (error) {
15313
+ this.log(`Error creating limit order: ${error}`, { type: 'error' });
15314
+ throw error;
15315
+ }
15316
+ }
15317
+ /**
15318
+ * Close all equities positions
15319
+ * @param options (object) - the options for closing the positions
15320
+ * - cancel_orders (boolean) - whether to cancel related orders
15321
+ * - useLimitOrders (boolean) - whether to use limit orders to close the positions
15322
+ */
15323
+ async closeAllPositions(options = { cancel_orders: true, useLimitOrders: false }) {
15324
+ this.log(`Closing all positions${options.useLimitOrders ? ' using limit orders' : ''}${options.cancel_orders ? ' and canceling open orders' : ''}`);
15325
+ if (options.useLimitOrders) {
15326
+ // Get all positions
15327
+ const positions = await this.getPositions('us_equity');
15328
+ if (positions.length === 0) {
15329
+ this.log('No positions to close');
15330
+ return;
15331
+ }
15332
+ this.log(`Found ${positions.length} positions to close`);
15333
+ // Get latest quotes for all positions
15334
+ const symbols = positions.map((position) => position.symbol);
15335
+ const quotesResponse = await marketDataAPI.getLatestQuotes(symbols);
15336
+ const lengthOfQuotes = Object.keys(quotesResponse.quotes).length;
15337
+ if (lengthOfQuotes === 0) {
15338
+ this.log('No quotes available for positions, received 0 quotes', {
15339
+ type: 'error',
15340
+ });
15341
+ return;
15342
+ }
15343
+ if (lengthOfQuotes !== positions.length) {
15344
+ this.log(`Received ${lengthOfQuotes} quotes for ${positions.length} positions, expected ${positions.length} quotes`, { type: 'warn' });
15345
+ return;
15346
+ }
15347
+ // Create limit orders to close each position
15348
+ for (const position of positions) {
15349
+ const quote = quotesResponse.quotes[position.symbol];
15350
+ if (!quote) {
15351
+ this.log(`No quote available for ${position.symbol}, skipping limit order`, {
15352
+ symbol: position.symbol,
15353
+ type: 'warn',
15354
+ });
15355
+ continue;
15356
+ }
15357
+ const qty = Math.abs(parseFloat(position.qty));
15358
+ const side = position.side === 'long' ? 'sell' : 'buy';
15359
+ const positionIntent = side === 'sell' ? 'sell_to_close' : 'buy_to_close';
15360
+ // Get the current price from the quote
15361
+ const currentPrice = side === 'sell' ? quote.bp : quote.ap; // Use bid for sells, ask for buys
15362
+ if (!currentPrice) {
15363
+ this.log(`No valid price available for ${position.symbol}, skipping limit order`, {
15364
+ symbol: position.symbol,
15365
+ type: 'warn',
15366
+ });
15367
+ continue;
15368
+ }
15369
+ // Apply slippage from config
15370
+ const limitSlippagePercent1 = limitPriceSlippagePercent100 / 100;
15371
+ const limitPrice = side === 'sell'
15372
+ ? this.roundPriceForAlpaca(currentPrice * (1 - limitSlippagePercent1)) // Sell slightly lower
15373
+ : this.roundPriceForAlpaca(currentPrice * (1 + limitSlippagePercent1)); // Buy slightly higher
15374
+ this.log(`Creating limit order to close ${position.symbol} position: ${side} ${qty} shares at $${limitPrice.toFixed(2)}`, {
15375
+ symbol: position.symbol,
15376
+ });
15377
+ await this.createLimitOrder(position.symbol, qty, side, limitPrice, positionIntent);
15378
+ }
15379
+ }
15380
+ else {
15381
+ await this.makeRequest('/positions', 'DELETE', undefined, options.cancel_orders ? '?cancel_orders=true' : '');
15382
+ }
15383
+ }
15384
+ /**
15385
+ * Close all equities positions using limit orders during extended hours trading
15386
+ * @param cancelOrders Whether to cancel related orders (default: true)
15387
+ * @returns Promise that resolves when all positions are closed
15388
+ */
15389
+ async closeAllPositionsAfterHours() {
15390
+ this.log('Closing all positions using limit orders during extended hours trading');
15391
+ // Get all positions
15392
+ const positions = await this.getPositions();
15393
+ this.log(`Found ${positions.length} positions to close`);
15394
+ if (positions.length === 0) {
15395
+ this.log('No positions to close');
15396
+ return;
15397
+ }
15398
+ await this.cancelAllOrders();
15399
+ this.log(`Cancelled all open orders`);
15400
+ // Get latest quotes for all positions
15401
+ const symbols = positions.map((position) => position.symbol);
15402
+ const quotesResponse = await marketDataAPI.getLatestQuotes(symbols);
15403
+ // Create limit orders to close each position
15404
+ for (const position of positions) {
15405
+ const quote = quotesResponse.quotes[position.symbol];
15406
+ if (!quote) {
15407
+ this.log(`No quote available for ${position.symbol}, skipping limit order`, {
15408
+ symbol: position.symbol,
15409
+ type: 'warn',
15410
+ });
15411
+ continue;
15412
+ }
15413
+ const qty = Math.abs(parseFloat(position.qty));
15414
+ const side = position.side === 'long' ? 'sell' : 'buy';
15415
+ const positionIntent = side === 'sell' ? 'sell_to_close' : 'buy_to_close';
15416
+ // Get the current price from the quote
15417
+ const currentPrice = side === 'sell' ? quote.bp : quote.ap; // Use bid for sells, ask for buys
15418
+ if (!currentPrice) {
15419
+ this.log(`No valid price available for ${position.symbol}, skipping limit order`, {
15420
+ symbol: position.symbol,
15421
+ type: 'warn',
15422
+ });
15423
+ continue;
15424
+ }
15425
+ // Apply slippage from config
15426
+ const limitSlippagePercent1 = limitPriceSlippagePercent100 / 100;
15427
+ const limitPrice = side === 'sell'
15428
+ ? this.roundPriceForAlpaca(currentPrice * (1 - limitSlippagePercent1)) // Sell slightly lower
15429
+ : this.roundPriceForAlpaca(currentPrice * (1 + limitSlippagePercent1)); // Buy slightly higher
15430
+ this.log(`Creating extended hours limit order to close ${position.symbol} position: ${side} ${qty} shares at $${limitPrice.toFixed(2)}`, {
15431
+ symbol: position.symbol,
15432
+ });
15433
+ await this.createLimitOrder(position.symbol, qty, side, limitPrice, positionIntent, true // Enable extended hours trading
15434
+ );
15435
+ }
15436
+ this.log(`All positions closed: ${positions.map((p) => p.symbol).join(', ')}`);
15437
+ }
15438
+ onTradeUpdate(callback) {
15439
+ this.tradeUpdateCallback = callback;
15440
+ }
15441
+ /**
15442
+ * Get portfolio history for the account
15443
+ * @param params Parameters for the portfolio history request
15444
+ * @returns Portfolio history data
15445
+ */
15446
+ async getPortfolioHistory(params) {
15447
+ const queryParams = new URLSearchParams();
15448
+ if (params.timeframe)
15449
+ queryParams.append('timeframe', params.timeframe);
15450
+ if (params.period)
15451
+ queryParams.append('period', params.period);
15452
+ if (params.extended_hours !== undefined)
15453
+ queryParams.append('extended_hours', params.extended_hours.toString());
15454
+ if (params.start)
15455
+ queryParams.append('start', params.start);
15456
+ if (params.end)
15457
+ queryParams.append('end', params.end);
15458
+ if (params.date_end)
15459
+ queryParams.append('date_end', params.date_end);
15460
+ const response = await this.makeRequest(`/account/portfolio/history?${queryParams.toString()}`);
15461
+ return response;
15462
+ }
15463
+ /**
15464
+ * Get portfolio daily history for the account, ensuring the most recent day is included
15465
+ * by combining daily and hourly history if needed.
15466
+ *
15467
+ * This function performs two API calls:
15468
+ * 1. Retrieves daily portfolio history
15469
+ * 2. Retrieves hourly portfolio history to check for more recent data
15470
+ *
15471
+ * If hourly history has timestamps more recent than the last timestamp in daily history,
15472
+ * it appends one additional day to the daily history using the most recent hourly values.
15473
+ *
15474
+ * @param params Parameters for the portfolio history request (same as getPortfolioHistory except timeframe is forced to '1D')
15475
+ * @returns Portfolio history data with daily timeframe, including the most recent day if available from hourly data
15476
+ */
15477
+ async getPortfolioDailyHistory(params) {
15478
+ // Get daily and hourly history in parallel
15479
+ const dailyParams = { ...params, timeframe: '1D' };
15480
+ const hourlyParams = { timeframe: '1Min', period: '1D' };
15481
+ const [dailyHistory, hourlyHistory] = await Promise.all([
15482
+ this.getPortfolioHistory(dailyParams),
15483
+ this.getPortfolioHistory(hourlyParams),
15484
+ ]);
15485
+ // If no hourly history, return daily as-is
15486
+ if (!hourlyHistory.timestamp || hourlyHistory.timestamp.length === 0) {
15487
+ return dailyHistory;
15488
+ }
15489
+ // Get the last timestamp from daily history
15490
+ const lastDailyTimestamp = dailyHistory.timestamp[dailyHistory.timestamp.length - 1];
15491
+ // Check if hourly history has more recent data
15492
+ const recentHourlyData = hourlyHistory.timestamp
15493
+ .map((timestamp, index) => ({ timestamp, index }))
15494
+ .filter(({ timestamp }) => timestamp > lastDailyTimestamp);
15495
+ // If no more recent hourly data, return daily history as-is
15496
+ if (recentHourlyData.length === 0) {
15497
+ return dailyHistory;
15498
+ }
15499
+ // Get the most recent hourly data point
15500
+ const mostRecentHourly = recentHourlyData[recentHourlyData.length - 1];
15501
+ const mostRecentIndex = mostRecentHourly.index;
15502
+ // Calculate the timestamp for the new daily entry.
15503
+ // Alpaca's daily history timestamps are at 00:00:00Z for the calendar day
15504
+ // following the NY trading date. Derive the trading date in NY time from the
15505
+ // most recent intraday timestamp, then set the new daily timestamp to
15506
+ // midnight UTC of the next calendar day.
15507
+ const mostRecentMs = mostRecentHourly.timestamp * 1000; // hourly timestamps are seconds
15508
+ const tradingDateStr = getTradingDate(new Date(mostRecentMs)); // e.g., '2025-09-05' (NY trading date)
15509
+ const [yearStr, monthStr, dayStr] = tradingDateStr.split('-');
15510
+ const year = Number(yearStr);
15511
+ const month = Number(monthStr); // 1-based
15512
+ const day = Number(dayStr);
15513
+ const newDailyTimestamp = Math.floor(Date.UTC(year, month - 1, day + 1, 0, 0, 0, 0) / 1000);
15514
+ // Create a new daily history entry with the most recent hourly values
15515
+ const updatedDailyHistory = {
15516
+ ...dailyHistory,
15517
+ timestamp: [...dailyHistory.timestamp, newDailyTimestamp],
15518
+ equity: [...dailyHistory.equity, hourlyHistory.equity[mostRecentIndex]],
15519
+ profit_loss: [...dailyHistory.profit_loss, hourlyHistory.profit_loss[mostRecentIndex]],
15520
+ profit_loss_pct: [...dailyHistory.profit_loss_pct, hourlyHistory.profit_loss_pct[mostRecentIndex]],
15521
+ };
15522
+ return updatedDailyHistory;
15523
+ }
15524
+ /**
15525
+ * Get option contracts based on specified parameters
15526
+ * @param params Parameters to filter option contracts
15527
+ * @returns Option contracts matching the criteria
15528
+ */
15529
+ async getOptionContracts(params) {
15530
+ const queryParams = new URLSearchParams();
15531
+ queryParams.append('underlying_symbols', params.underlying_symbols.join(','));
15532
+ if (params.expiration_date_gte)
15533
+ queryParams.append('expiration_date_gte', params.expiration_date_gte);
15534
+ if (params.expiration_date_lte)
15535
+ queryParams.append('expiration_date_lte', params.expiration_date_lte);
15536
+ if (params.strike_price_gte)
15537
+ queryParams.append('strike_price_gte', params.strike_price_gte);
15538
+ if (params.strike_price_lte)
15539
+ queryParams.append('strike_price_lte', params.strike_price_lte);
15540
+ if (params.type)
15541
+ queryParams.append('type', params.type);
15542
+ if (params.status)
15543
+ queryParams.append('status', params.status);
15544
+ if (params.limit)
15545
+ queryParams.append('limit', params.limit.toString());
15546
+ if (params.page_token)
15547
+ queryParams.append('page_token', params.page_token);
15548
+ this.log(`Fetching option contracts for ${params.underlying_symbols.join(', ')}`, {
15549
+ symbol: params.underlying_symbols.join(', '),
15550
+ });
15551
+ const response = (await this.makeRequest(`/options/contracts?${queryParams.toString()}`));
15552
+ this.log(`Found ${response.option_contracts.length} option contracts`, {
15553
+ symbol: params.underlying_symbols.join(', '),
15554
+ });
15555
+ return response;
15556
+ }
15557
+ /**
15558
+ * Get a specific option contract by symbol or ID
15559
+ * @param symbolOrId The symbol or ID of the option contract
15560
+ * @returns The option contract details
15561
+ */
15562
+ async getOptionContract(symbolOrId) {
15563
+ this.log(`Fetching option contract details for ${symbolOrId}`, {
15564
+ symbol: symbolOrId,
15565
+ });
15566
+ const response = (await this.makeRequest(`/options/contracts/${symbolOrId}`));
15567
+ this.log(`Found option contract details for ${symbolOrId}: ${response.name}`, {
15568
+ symbol: symbolOrId,
15569
+ });
15570
+ return response;
15571
+ }
15572
+ /**
15573
+ * Create a simple option order (market or limit)
15574
+ * @param symbol Option contract symbol
15575
+ * @param qty Quantity of contracts (must be a whole number)
15576
+ * @param side Buy or sell
15577
+ * @param position_intent Position intent (buy_to_open, buy_to_close, sell_to_open, sell_to_close)
15578
+ * @param type Order type (market or limit)
15579
+ * @param limitPrice Limit price (required for limit orders)
15580
+ * @returns The created order
15581
+ */
15582
+ async createOptionOrder(symbol, qty, side, position_intent, type, limitPrice) {
15583
+ if (!Number.isInteger(qty) || qty <= 0) {
15584
+ this.log('Quantity must be a positive whole number for option orders', { type: 'error' });
15585
+ }
15586
+ if (type === 'limit' && limitPrice === undefined) {
15587
+ this.log('Limit price is required for limit orders', { type: 'error' });
15588
+ }
15589
+ this.log(`Creating ${type} option order for ${symbol}: ${side} ${qty} contracts (${position_intent})${type === 'limit' ? ` at $${limitPrice?.toFixed(2)}` : ''}`, {
15590
+ symbol,
15591
+ });
15592
+ const orderData = {
15593
+ symbol,
15594
+ qty: qty.toString(),
15595
+ side,
15596
+ position_intent,
15597
+ type,
15598
+ time_in_force: 'day',
15599
+ order_class: 'simple',
15600
+ extended_hours: false,
15601
+ };
15602
+ if (type === 'limit' && limitPrice !== undefined) {
15603
+ orderData.limit_price = this.roundPriceForAlpaca(limitPrice).toString();
15604
+ }
15605
+ return this.makeRequest('/orders', 'POST', orderData);
15606
+ }
15607
+ /**
15608
+ * Create a multi-leg option order
15609
+ * @param legs Array of order legs
15610
+ * @param qty Quantity of the multi-leg order (must be a whole number)
15611
+ * @param type Order type (market or limit)
15612
+ * @param limitPrice Limit price (required for limit orders)
15613
+ * @returns The created multi-leg order
15614
+ */
15615
+ async createMultiLegOptionOrder(legs, qty, type, limitPrice) {
15616
+ if (!Number.isInteger(qty) || qty <= 0) {
15617
+ this.log('Quantity must be a positive whole number for option orders', { type: 'error' });
15618
+ }
15619
+ if (type === 'limit' && limitPrice === undefined) {
15620
+ this.log('Limit price is required for limit orders', { type: 'error' });
15621
+ }
15622
+ if (legs.length < 2) {
15623
+ this.log('Multi-leg orders require at least 2 legs', { type: 'error' });
15624
+ }
15625
+ const legSymbols = legs.map((leg) => leg.symbol).join(', ');
15626
+ this.log(`Creating multi-leg ${type} option order with ${legs.length} legs (${legSymbols})${type === 'limit' ? ` at $${limitPrice?.toFixed(2)}` : ''}`, {
15627
+ symbol: legSymbols,
15628
+ });
15629
+ const orderData = {
15630
+ order_class: 'mleg',
15631
+ qty: qty.toString(),
15632
+ type,
15633
+ time_in_force: 'day',
15634
+ legs,
15635
+ };
15636
+ if (type === 'limit' && limitPrice !== undefined) {
15637
+ orderData.limit_price = this.roundPriceForAlpaca(limitPrice).toString();
15638
+ }
15639
+ return this.makeRequest('/orders', 'POST', orderData);
15640
+ }
15641
+ /**
15642
+ * Exercise an option contract
15643
+ * @param symbolOrContractId The symbol or ID of the option contract to exercise
15644
+ * @returns Response from the exercise request
15645
+ */
15646
+ async exerciseOption(symbolOrContractId) {
15647
+ this.log(`Exercising option contract ${symbolOrContractId}`, {
15648
+ symbol: symbolOrContractId,
15649
+ });
15650
+ return this.makeRequest(`/positions/${symbolOrContractId}/exercise`, 'POST');
15651
+ }
15652
+ /**
15653
+ * Get option positions
15654
+ * @returns Array of option positions
15655
+ */
15656
+ async getOptionPositions() {
15657
+ this.log('Fetching option positions');
15658
+ const positions = await this.getPositions('us_option');
15659
+ return positions;
15660
+ }
15661
+ async getOptionsOpenSpreadTrades() {
15662
+ this.log('Fetching option open trades');
15663
+ // this function will get all open positions, extract the symbol and see when they were created.
15664
+ // figures out when the earliest date was (should be today)
15665
+ // then it pulls all orders after the earliest date that were closed and that were of class 'mleg'
15666
+ // Each of these contains two orders. they look like this:
15667
+ }
15668
+ /**
15669
+ * Get option account activities (exercises, assignments, expirations)
15670
+ * @param activityType Type of option activity to filter by
15671
+ * @param date Date to filter activities (YYYY-MM-DD format)
15672
+ * @returns Array of option account activities
15673
+ */
15674
+ async getOptionActivities(activityType, date) {
15675
+ const queryParams = new URLSearchParams();
15676
+ if (activityType) {
15677
+ queryParams.append('activity_types', activityType);
15678
+ }
15679
+ else {
15680
+ queryParams.append('activity_types', 'OPEXC,OPASN,OPEXP');
15681
+ }
15682
+ if (date) {
15683
+ queryParams.append('date', date);
15684
+ }
15685
+ this.log(`Fetching option activities${activityType ? ` of type ${activityType}` : ''}${date ? ` for date ${date}` : ''}`);
15686
+ return this.makeRequest(`/account/activities?${queryParams.toString()}`);
15687
+ }
15688
+ /**
15689
+ * Create a long call spread (buy lower strike call, sell higher strike call)
15690
+ * @param lowerStrikeCallSymbol Symbol of the lower strike call option
15691
+ * @param higherStrikeCallSymbol Symbol of the higher strike call option
15692
+ * @param qty Quantity of spreads to create (must be a whole number)
15693
+ * @param limitPrice Limit price for the spread
15694
+ * @returns The created multi-leg order
15695
+ */
15696
+ async createLongCallSpread(lowerStrikeCallSymbol, higherStrikeCallSymbol, qty, limitPrice) {
15697
+ this.log(`Creating long call spread: Buy ${lowerStrikeCallSymbol}, Sell ${higherStrikeCallSymbol}, Qty: ${qty}, Price: $${limitPrice.toFixed(2)}`, {
15698
+ symbol: `${lowerStrikeCallSymbol},${higherStrikeCallSymbol}`,
15699
+ });
15700
+ const legs = [
15701
+ {
15702
+ symbol: lowerStrikeCallSymbol,
15703
+ ratio_qty: '1',
15704
+ side: 'buy',
15705
+ position_intent: 'buy_to_open',
15706
+ },
15707
+ {
15708
+ symbol: higherStrikeCallSymbol,
15709
+ ratio_qty: '1',
15710
+ side: 'sell',
15711
+ position_intent: 'sell_to_open',
15712
+ },
15713
+ ];
15714
+ return this.createMultiLegOptionOrder(legs, qty, 'limit', limitPrice);
15715
+ }
15716
+ /**
15717
+ * Create a long put spread (buy higher strike put, sell lower strike put)
15718
+ * @param higherStrikePutSymbol Symbol of the higher strike put option
15719
+ * @param lowerStrikePutSymbol Symbol of the lower strike put option
15720
+ * @param qty Quantity of spreads to create (must be a whole number)
15721
+ * @param limitPrice Limit price for the spread
15722
+ * @returns The created multi-leg order
15723
+ */
15724
+ async createLongPutSpread(higherStrikePutSymbol, lowerStrikePutSymbol, qty, limitPrice) {
15725
+ this.log(`Creating long put spread: Buy ${higherStrikePutSymbol}, Sell ${lowerStrikePutSymbol}, Qty: ${qty}, Price: $${limitPrice.toFixed(2)}`, {
15726
+ symbol: `${higherStrikePutSymbol},${lowerStrikePutSymbol}`,
15727
+ });
15728
+ const legs = [
15729
+ {
15730
+ symbol: higherStrikePutSymbol,
15731
+ ratio_qty: '1',
15732
+ side: 'buy',
15733
+ position_intent: 'buy_to_open',
15734
+ },
15735
+ {
15736
+ symbol: lowerStrikePutSymbol,
15737
+ ratio_qty: '1',
15738
+ side: 'sell',
15739
+ position_intent: 'sell_to_open',
15740
+ },
15741
+ ];
15742
+ return this.createMultiLegOptionOrder(legs, qty, 'limit', limitPrice);
15743
+ }
15744
+ /**
15745
+ * Create an iron condor (sell call spread and put spread)
15746
+ * @param longPutSymbol Symbol of the lower strike put (long)
15747
+ * @param shortPutSymbol Symbol of the higher strike put (short)
15748
+ * @param shortCallSymbol Symbol of the lower strike call (short)
15749
+ * @param longCallSymbol Symbol of the higher strike call (long)
15750
+ * @param qty Quantity of iron condors to create (must be a whole number)
15751
+ * @param limitPrice Limit price for the iron condor (credit)
15752
+ * @returns The created multi-leg order
15753
+ */
15754
+ async createIronCondor(longPutSymbol, shortPutSymbol, shortCallSymbol, longCallSymbol, qty, limitPrice) {
15755
+ this.log(`Creating iron condor with ${qty} contracts at $${limitPrice.toFixed(2)}`, {
15756
+ symbol: `${longPutSymbol},${shortPutSymbol},${shortCallSymbol},${longCallSymbol}`,
15757
+ });
15758
+ const legs = [
15759
+ {
15760
+ symbol: longPutSymbol,
15761
+ ratio_qty: '1',
15762
+ side: 'buy',
15763
+ position_intent: 'buy_to_open',
15764
+ },
15765
+ {
15766
+ symbol: shortPutSymbol,
15767
+ ratio_qty: '1',
15768
+ side: 'sell',
15769
+ position_intent: 'sell_to_open',
15770
+ },
15771
+ {
15772
+ symbol: shortCallSymbol,
15773
+ ratio_qty: '1',
15774
+ side: 'sell',
15775
+ position_intent: 'sell_to_open',
15776
+ },
15777
+ {
15778
+ symbol: longCallSymbol,
15779
+ ratio_qty: '1',
15780
+ side: 'buy',
15781
+ position_intent: 'buy_to_open',
15782
+ },
15783
+ ];
15784
+ try {
15785
+ return await this.createMultiLegOptionOrder(legs, qty, 'limit', limitPrice);
15786
+ }
15787
+ catch (error) {
15788
+ this.log(`Error creating iron condor: ${error}`, { type: 'error' });
15789
+ throw error;
15790
+ }
15791
+ }
15792
+ /**
15793
+ * Create a covered call (sell call option against owned stock)
15794
+ * @param stockSymbol Symbol of the underlying stock
15795
+ * @param callOptionSymbol Symbol of the call option to sell
15796
+ * @param qty Quantity of covered calls to create (must be a whole number)
15797
+ * @param limitPrice Limit price for the call option
15798
+ * @returns The created order
15799
+ */
15800
+ async createCoveredCall(stockSymbol, callOptionSymbol, qty, limitPrice) {
15801
+ this.log(`Creating covered call: Sell ${callOptionSymbol} against ${stockSymbol}, Qty: ${qty}, Price: $${limitPrice.toFixed(2)}`, {
15802
+ symbol: `${stockSymbol},${callOptionSymbol}`,
15803
+ });
15804
+ // For covered calls, we don't need to include the stock leg if we already own the shares
15805
+ // We just create a simple sell order for the call option
15806
+ try {
15807
+ return await this.createOptionOrder(callOptionSymbol, qty, 'sell', 'sell_to_open', 'limit', limitPrice);
15808
+ }
15809
+ catch (error) {
15810
+ this.log(`Error creating covered call: ${error}`, { type: 'error' });
15811
+ throw error;
15812
+ }
15813
+ }
15814
+ /**
15815
+ * Roll an option position to a new expiration or strike
15816
+ * @param currentOptionSymbol Symbol of the current option position
15817
+ * @param newOptionSymbol Symbol of the new option to roll to
15818
+ * @param qty Quantity of options to roll (must be a whole number)
15819
+ * @param currentPositionSide Side of the current position ('buy' or 'sell')
15820
+ * @param limitPrice Net limit price for the roll
15821
+ * @returns The created multi-leg order
15822
+ */
15823
+ async rollOptionPosition(currentOptionSymbol, newOptionSymbol, qty, currentPositionSide, limitPrice) {
15824
+ this.log(`Rolling ${qty} ${currentOptionSymbol} to ${newOptionSymbol} at net price $${limitPrice.toFixed(2)}`, {
15825
+ symbol: `${currentOptionSymbol},${newOptionSymbol}`,
15826
+ });
15827
+ // If current position is long, we need to sell to close and buy to open
15828
+ // If current position is short, we need to buy to close and sell to open
15829
+ const closePositionSide = currentPositionSide === 'buy' ? 'sell' : 'buy';
15830
+ const openPositionSide = currentPositionSide;
15831
+ const closePositionIntent = closePositionSide === 'buy' ? 'buy_to_close' : 'sell_to_close';
15832
+ const openPositionIntent = openPositionSide === 'buy' ? 'buy_to_open' : 'sell_to_open';
15833
+ const legs = [
15834
+ {
15835
+ symbol: currentOptionSymbol,
15836
+ ratio_qty: '1',
15837
+ side: closePositionSide,
15838
+ position_intent: closePositionIntent,
15839
+ },
15840
+ {
15841
+ symbol: newOptionSymbol,
15842
+ ratio_qty: '1',
15843
+ side: openPositionSide,
15844
+ position_intent: openPositionIntent,
15845
+ },
15846
+ ];
15847
+ try {
15848
+ return await this.createMultiLegOptionOrder(legs, qty, 'limit', limitPrice);
15849
+ }
15850
+ catch (error) {
15851
+ this.log(`Error rolling option position: ${error}`, { type: 'error' });
15852
+ throw error;
15853
+ }
15854
+ }
15855
+ /**
15856
+ * Get option chain for a specific underlying symbol and expiration date
15857
+ * @param underlyingSymbol The underlying stock symbol
15858
+ * @param expirationDate The expiration date (YYYY-MM-DD format)
15859
+ * @returns Option contracts for the specified symbol and expiration date
15860
+ */
15861
+ async getOptionChain(underlyingSymbol, expirationDate) {
15862
+ this.log(`Fetching option chain for ${underlyingSymbol} with expiration date ${expirationDate}`, {
15863
+ symbol: underlyingSymbol,
15864
+ });
15865
+ try {
15866
+ const params = {
15867
+ underlying_symbols: [underlyingSymbol],
15868
+ expiration_date_gte: expirationDate,
15869
+ expiration_date_lte: expirationDate,
15870
+ status: 'active',
15871
+ limit: 500, // Get a large number to ensure we get all strikes
15872
+ };
15873
+ const response = await this.getOptionContracts(params);
15874
+ return response.option_contracts || [];
15875
+ }
15876
+ catch (error) {
15877
+ this.log(`Failed to fetch option chain for ${underlyingSymbol}: ${error instanceof Error ? error.message : 'Unknown error'}`, {
15878
+ type: 'error',
15879
+ symbol: underlyingSymbol,
15880
+ });
15881
+ return [];
15882
+ }
15883
+ }
15884
+ /**
15885
+ * Get all available expiration dates for a specific underlying symbol
15886
+ * @param underlyingSymbol The underlying stock symbol
15887
+ * @returns Array of available expiration dates
15888
+ */
15889
+ async getOptionExpirationDates(underlyingSymbol) {
15890
+ this.log(`Fetching available expiration dates for ${underlyingSymbol}`, {
15891
+ symbol: underlyingSymbol,
15892
+ });
15893
+ try {
15894
+ const params = {
15895
+ underlying_symbols: [underlyingSymbol],
15896
+ status: 'active',
15897
+ limit: 1000, // Get a large number to ensure we get contracts with all expiration dates
15898
+ };
15899
+ const response = await this.getOptionContracts(params);
15900
+ // Extract unique expiration dates
15901
+ const expirationDates = new Set();
15902
+ if (response.option_contracts) {
15903
+ response.option_contracts.forEach((contract) => {
15904
+ expirationDates.add(contract.expiration_date);
15905
+ });
15906
+ }
15907
+ // Convert to array and sort
15908
+ return Array.from(expirationDates).sort();
15909
+ }
15910
+ catch (error) {
15911
+ this.log(`Failed to fetch expiration dates for ${underlyingSymbol}: ${error instanceof Error ? error.message : 'Unknown error'}`, {
15912
+ type: 'error',
15913
+ symbol: underlyingSymbol,
15914
+ });
15915
+ return [];
15916
+ }
15917
+ }
15918
+ /**
15919
+ * Get the current options trading level for the account
15920
+ * @returns The options trading level (0-3)
15921
+ */
15922
+ async getOptionsTradingLevel() {
15923
+ this.log('Fetching options trading level');
15924
+ const accountDetails = await this.getAccountDetails();
15925
+ return accountDetails.options_trading_level || 0;
15926
+ }
15927
+ /**
15928
+ * Check if the account has options trading enabled
15929
+ * @returns Boolean indicating if options trading is enabled
15930
+ */
15931
+ async isOptionsEnabled() {
15932
+ this.log('Checking if options trading is enabled');
15933
+ const accountDetails = await this.getAccountDetails();
15934
+ // Check if options trading level is 2 or higher (Level 2+ allows buying calls/puts)
15935
+ // Level 0: Options disabled
15936
+ // Level 1: Only covered calls and cash-secured puts
15937
+ // Level 2+: Can buy calls and puts (required for executeOptionsOrder)
15938
+ const optionsLevel = accountDetails.options_trading_level || 0;
15939
+ const isEnabled = optionsLevel >= 2;
15940
+ this.log(`Options trading level: ${optionsLevel}, enabled: ${isEnabled}`);
15941
+ return isEnabled;
15942
+ }
15943
+ /**
15944
+ * Close all option positions
15945
+ * @param cancelOrders Whether to cancel related orders (default: true)
15946
+ * @returns Response from the close positions request
15947
+ */
15948
+ async closeAllOptionPositions(cancelOrders = true) {
15949
+ this.log(`Closing all option positions${cancelOrders ? ' and canceling related orders' : ''}`);
15950
+ const optionPositions = await this.getOptionPositions();
15951
+ if (optionPositions.length === 0) {
15952
+ this.log('No option positions to close');
15953
+ return;
15954
+ }
15955
+ // Create market orders to close each position
15956
+ for (const position of optionPositions) {
15957
+ const side = position.side === 'long' ? 'sell' : 'buy';
15958
+ const positionIntent = side === 'sell' ? 'sell_to_close' : 'buy_to_close';
15959
+ this.log(`Closing ${position.side} position of ${position.qty} contracts for ${position.symbol}`, {
15960
+ symbol: position.symbol,
15961
+ });
15962
+ await this.createOptionOrder(position.symbol, parseInt(position.qty), side, positionIntent, 'market');
15963
+ }
15964
+ if (cancelOrders) {
15965
+ // Get all open option orders
15966
+ const orders = await this.getOrders({ status: 'open' });
15967
+ const optionOrders = orders.filter((order) => order.asset_class === 'us_option');
15968
+ // Cancel each open option order
15969
+ for (const order of optionOrders) {
15970
+ this.log(`Canceling open order for ${order.symbol}`, {
15971
+ symbol: order.symbol,
15972
+ });
15973
+ await this.makeRequest(`/orders/${order.id}`, 'DELETE');
15974
+ }
15975
+ }
15976
+ }
15977
+ /**
15978
+ * Close a specific option position
15979
+ * @param symbol The option contract symbol
15980
+ * @param qty Optional quantity to close (defaults to entire position)
15981
+ * @returns The created order
15982
+ */
15983
+ async closeOptionPosition(symbol, qty) {
15984
+ this.log(`Closing option position for ${symbol}${qty ? ` (${qty} contracts)` : ''}`, {
15985
+ symbol,
15986
+ });
15987
+ // Get the position details
15988
+ const positions = await this.getOptionPositions();
15989
+ const position = positions.find((p) => p.symbol === symbol);
15990
+ if (!position) {
15991
+ throw new Error(`No position found for option contract ${symbol}`);
15992
+ }
15993
+ const quantityToClose = qty || parseInt(position.qty);
15994
+ const side = position.side === 'long' ? 'sell' : 'buy';
15995
+ const positionIntent = side === 'sell' ? 'sell_to_close' : 'buy_to_close';
15996
+ try {
15997
+ return await this.createOptionOrder(symbol, quantityToClose, side, positionIntent, 'market');
15998
+ }
15999
+ catch (error) {
16000
+ this.log(`Error closing option position: ${error}`, { type: 'error' });
16001
+ throw error;
16002
+ }
16003
+ }
16004
+ /**
16005
+ * Create a complete equities trade with optional stop loss and take profit
16006
+ * @param params Trade parameters including symbol, qty, side, and optional referencePrice
16007
+ * @param options Trade options including order type, extended hours, stop loss, and take profit settings
16008
+ * @returns The created order
16009
+ */
16010
+ async createEquitiesTrade(params, options) {
16011
+ const { symbol, qty, side, referencePrice } = params;
16012
+ const { type = 'market', limitPrice, extendedHours = false, useStopLoss = false, stopPrice, stopPercent100, useTakeProfit = false, takeProfitPrice, takeProfitPercent100, clientOrderId, } = options || {};
16013
+ // Validation: Extended hours + market order is not allowed
16014
+ if (extendedHours && type === 'market') {
16015
+ this.log('Cannot create market order with extended hours enabled', {
16016
+ symbol,
16017
+ type: 'error',
16018
+ });
16019
+ throw new Error('Cannot create market order with extended hours enabled');
16020
+ }
16021
+ // Validation: Limit orders require limit price
16022
+ if (type === 'limit' && limitPrice === undefined) {
16023
+ this.log('Limit price is required for limit orders', {
16024
+ symbol,
16025
+ type: 'error',
16026
+ });
16027
+ throw new Error('Limit price is required for limit orders');
16028
+ }
16029
+ let calculatedStopPrice;
16030
+ let calculatedTakeProfitPrice;
16031
+ // Handle stop loss validation and calculation
16032
+ if (useStopLoss) {
16033
+ if (stopPrice === undefined && stopPercent100 === undefined) {
16034
+ this.log('Either stopPrice or stopPercent100 must be provided when useStopLoss is true', {
16035
+ symbol,
16036
+ type: 'error',
16037
+ });
16038
+ throw new Error('Either stopPrice or stopPercent100 must be provided when useStopLoss is true');
16039
+ }
16040
+ if (stopPercent100 !== undefined) {
16041
+ if (referencePrice === undefined) {
16042
+ this.log('referencePrice is required when using stopPercent100', {
16043
+ symbol,
16044
+ type: 'error',
16045
+ });
16046
+ throw new Error('referencePrice is required when using stopPercent100');
16047
+ }
16048
+ // Calculate stop price based on percentage and side
16049
+ const stopPercentDecimal = stopPercent100 / 100;
16050
+ if (side === 'buy') {
16051
+ // For buy orders, stop loss is below the reference price
16052
+ calculatedStopPrice = referencePrice * (1 - stopPercentDecimal);
16053
+ }
16054
+ else {
16055
+ // For sell orders, stop loss is above the reference price
16056
+ calculatedStopPrice = referencePrice * (1 + stopPercentDecimal);
16057
+ }
16058
+ }
16059
+ else {
16060
+ calculatedStopPrice = stopPrice;
16061
+ }
16062
+ }
16063
+ // Handle take profit validation and calculation
16064
+ if (useTakeProfit) {
16065
+ if (takeProfitPrice === undefined && takeProfitPercent100 === undefined) {
16066
+ this.log('Either takeProfitPrice or takeProfitPercent100 must be provided when useTakeProfit is true', {
16067
+ symbol,
16068
+ type: 'error',
16069
+ });
16070
+ throw new Error('Either takeProfitPrice or takeProfitPercent100 must be provided when useTakeProfit is true');
16071
+ }
16072
+ if (takeProfitPercent100 !== undefined) {
16073
+ if (referencePrice === undefined) {
16074
+ this.log('referencePrice is required when using takeProfitPercent100', {
16075
+ symbol,
16076
+ type: 'error',
16077
+ });
16078
+ throw new Error('referencePrice is required when using takeProfitPercent100');
16079
+ }
16080
+ // Calculate take profit price based on percentage and side
16081
+ const takeProfitPercentDecimal = takeProfitPercent100 / 100;
16082
+ if (side === 'buy') {
16083
+ // For buy orders, take profit is above the reference price
16084
+ calculatedTakeProfitPrice = referencePrice * (1 + takeProfitPercentDecimal);
16085
+ }
16086
+ else {
16087
+ // For sell orders, take profit is below the reference price
16088
+ calculatedTakeProfitPrice = referencePrice * (1 - takeProfitPercentDecimal);
16089
+ }
16090
+ }
16091
+ else {
16092
+ calculatedTakeProfitPrice = takeProfitPrice;
16093
+ }
16094
+ }
16095
+ // Determine order class based on what's enabled
16096
+ let orderClass = 'simple';
16097
+ if (useStopLoss && useTakeProfit) {
16098
+ orderClass = 'bracket';
16099
+ }
16100
+ else if (useStopLoss || useTakeProfit) {
16101
+ orderClass = 'oto';
16102
+ }
16103
+ // Build the order request
16104
+ const orderData = {
16105
+ symbol,
16106
+ qty: Math.abs(qty).toString(),
16107
+ side,
16108
+ type,
16109
+ time_in_force: 'day',
16110
+ order_class: orderClass,
16111
+ extended_hours: extendedHours,
16112
+ position_intent: side === 'buy' ? 'buy_to_open' : 'sell_to_open',
16113
+ };
16114
+ if (clientOrderId) {
16115
+ orderData.client_order_id = clientOrderId;
16116
+ }
16117
+ // Add limit price for limit orders
16118
+ if (type === 'limit' && limitPrice !== undefined) {
16119
+ orderData.limit_price = this.roundPriceForAlpaca(limitPrice).toString();
16120
+ }
16121
+ // Add stop loss if enabled
16122
+ if (useStopLoss && calculatedStopPrice !== undefined) {
16123
+ orderData.stop_loss = {
16124
+ stop_price: this.roundPriceForAlpaca(calculatedStopPrice).toString(),
16125
+ };
16126
+ }
16127
+ // Add take profit if enabled
16128
+ if (useTakeProfit && calculatedTakeProfitPrice !== undefined) {
16129
+ orderData.take_profit = {
16130
+ limit_price: this.roundPriceForAlpaca(calculatedTakeProfitPrice).toString(),
16131
+ };
16132
+ }
16133
+ 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)' : ''}`;
16134
+ this.log(logMessage, {
16135
+ symbol,
16136
+ });
16137
+ try {
16138
+ return await this.makeRequest('/orders', 'POST', orderData);
16139
+ }
16140
+ catch (error) {
16141
+ this.log(`Error creating equities trade: ${error}`, {
16142
+ symbol,
16143
+ type: 'error',
16144
+ });
16145
+ throw error;
16146
+ }
16147
+ }
16148
+ }
16149
+
16150
+ // Test file for context functionality
16151
+ // testGetPortfolioDailyHistory();
16152
+ // testWebSocketConnectAndDisconnect();
16153
+ // testGetAssetsShortableFilter();
16154
+ // testLLM();
16155
+ // testImageModelDefaults();
16156
+ // testGetTradingDaysBack();
16157
+ // testMOOAndMOCOrders();
16158
+ // testMarketDataAPI();
16159
+ // Test market data subscription with a real symbol or FAKEPACA
16160
+ // Uncomment one of the following to test:
16161
+ // testMarketDataSubscription('SPY');
16162
+ // testMarketDataSubscription('FAKEPACA');
16163
+ // testGetTradingDate();
16164
+ // Test new order functions
16165
+ testStopAndOCOOrders();
16166
+ /**
16167
+ * Test the createStopOrder and createOCOOrder functions
16168
+ */
16169
+ async function testStopAndOCOOrders() {
16170
+ const log = (message, options = { type: 'info' }) => {
16171
+ log$1(message, { ...options, source: 'Test' });
16172
+ };
16173
+ log('=== Testing Stop and OCO Orders ===');
16174
+ const credentials = {
16175
+ accountName: 'Paper',
16176
+ apiKey: process.env.ALPACA_TRADING_API_KEY || '',
16177
+ apiSecret: process.env.ALPACA_TRADING_SECRET_KEY || '',
16178
+ type: 'PAPER',
16179
+ orderType: 'market',
16180
+ engine: 'brain',
16181
+ };
16182
+ if (!credentials.apiKey || !credentials.apiSecret) {
16183
+ log('Missing Alpaca credentials in environment variables', { type: 'error' });
16184
+ return;
16185
+ }
16186
+ const tradingAPI = AlpacaTradingAPI.getInstance(credentials);
16187
+ try {
16188
+ const timestamp = Date.now();
16189
+ // Test 1: Create a simple stop order
16190
+ log('Test 1: Creating stop order for AAPL');
16191
+ const stopOrder = await tradingAPI.createStopOrder('AAPL', 1, 'sell', 145.00, 'sell_to_open' // Use sell_to_open since we don't have a position
16192
+ );
16193
+ log(`✓ Stop order created: ${stopOrder.id}, type: ${stopOrder.type}, stop_price: ${stopOrder.stop_price}`);
16194
+ // Test 2: Create a stop-limit order
16195
+ log('Test 2: Creating stop-limit order for TSLA');
16196
+ const stopLimitOrder = await tradingAPI.createStopOrder('TSLA', 1, 'sell', 200.00, 'sell_to_open', // Use sell_to_open since we don't have a position
16197
+ 199.50, `test-stop-limit-${timestamp}`);
16198
+ log(`✓ Stop-limit order created: ${stopLimitOrder.id}, type: ${stopLimitOrder.type}, stop_price: ${stopLimitOrder.stop_price}, limit_price: ${stopLimitOrder.limit_price}`);
16199
+ // Test 3: Create an OCO order (must be an exit order, so we first need to buy shares)
16200
+ log('Test 3: Creating position in MSFT and then OCO order to exit');
16201
+ // First check if there's an existing position and close it
16202
+ const existingPositions = await tradingAPI.getPositions();
16203
+ const msftPosition = existingPositions.find(p => p.symbol === 'MSFT');
16204
+ if (msftPosition) {
16205
+ log(' Closing existing MSFT position...');
16206
+ await tradingAPI.closeAllPositions({ cancel_orders: true, useLimitOrders: false });
16207
+ await new Promise(resolve => setTimeout(resolve, 2000));
16208
+ }
16209
+ // Buy shares to create a position
16210
+ log(' 3a: Buying 1 share of MSFT to create position...');
16211
+ const msftEntry = await tradingAPI.createMarketOrder('MSFT', 1, 'buy', 'buy_to_open');
16212
+ log(` ✓ Entry order created: ${msftEntry.id}`);
16213
+ // Wait for order to fill (paper trading should be instant but let's be safe)
16214
+ await new Promise(resolve => setTimeout(resolve, 3000));
16215
+ // Verify position exists
16216
+ const positions = await tradingAPI.getPositions();
16217
+ const newMsftPosition = positions.find(p => p.symbol === 'MSFT');
16218
+ if (!newMsftPosition) {
16219
+ log(' Warning: MSFT position not found, skipping OCO test', { type: 'error' });
16220
+ }
16221
+ else {
16222
+ log(` ✓ MSFT position confirmed: ${newMsftPosition.qty} shares`);
16223
+ // Now create OCO to exit the position
16224
+ log(' 3b: Creating OCO order to exit MSFT position...');
16225
+ const ocoOrder = await tradingAPI.createOCOOrder('MSFT', 1, 'sell', // sell to exit the long position
16226
+ 'sell_to_close', // must be sell_to_close since we're exiting
16227
+ 420.00, // limit price for exit order
16228
+ 425.00, // take profit (sell at or above this)
16229
+ 415.00 // stop loss (sell if price drops to this)
16230
+ );
16231
+ log(`✓ OCO order created: ${ocoOrder.id}, order_class: ${ocoOrder.order_class}`);
16232
+ if (ocoOrder.legs && ocoOrder.legs.length > 0) {
16233
+ log(` - Legs: ${ocoOrder.legs.length} orders`);
16234
+ ocoOrder.legs.forEach((leg, index) => {
16235
+ log(` Leg ${index + 1}: type=${leg.type}, limit_price=${leg.limit_price}, stop_price=${leg.stop_price}`);
16236
+ });
16237
+ }
16238
+ }
16239
+ // Test 4: Create another OCO order with stop-limit
16240
+ log('Test 4: Creating position in QQQ and OCO order with stop-limit');
16241
+ // Check existing QQQ position
16242
+ const qqqExistingPosition = existingPositions.find(p => p.symbol === 'QQQ');
16243
+ if (qqqExistingPosition) {
16244
+ log(' Found existing QQQ position, skipping position creation');
16245
+ }
16246
+ else {
16247
+ log(' 4a: Buying 1 share of QQQ to create position...');
16248
+ const qqqEntry = await tradingAPI.createMarketOrder('QQQ', 1, 'buy', 'buy_to_open');
16249
+ log(` ✓ Entry order created: ${qqqEntry.id}`);
16250
+ // Wait for order to fill
16251
+ await new Promise(resolve => setTimeout(resolve, 3000));
16252
+ }
16253
+ // Verify position exists
16254
+ const updatedPositions = await tradingAPI.getPositions();
16255
+ const qqqPosition = updatedPositions.find(p => p.symbol === 'QQQ');
16256
+ if (!qqqPosition) {
16257
+ log(' Warning: QQQ position not found, skipping OCO test', { type: 'error' });
16258
+ }
16259
+ else {
16260
+ log(` ✓ QQQ position confirmed: ${qqqPosition.qty} shares`);
16261
+ log(' 4b: Creating OCO order with stop-limit for QQQ...');
16262
+ const ocoLimitOrder = await tradingAPI.createOCOOrder('QQQ', 1, 'sell', // sell to exit the long position
16263
+ 'sell_to_close', // must be sell_to_close since we're exiting
16264
+ 480.00, // limit price for exit order
16265
+ 490.00, // take profit (sell at or above this)
16266
+ 470.00, // stop loss
16267
+ 470.50, // stop loss limit
16268
+ `test-oco-limit-${timestamp}`);
16269
+ log(`✓ OCO limit order created: ${ocoLimitOrder.id}, order_class: ${ocoLimitOrder.order_class}`);
16270
+ if (ocoLimitOrder.legs && ocoLimitOrder.legs.length > 0) {
16271
+ log(` - Legs: ${ocoLimitOrder.legs.length} orders`);
16272
+ ocoLimitOrder.legs.forEach((leg, index) => {
16273
+ log(` Leg ${index + 1}: type=${leg.type}, limit_price=${leg.limit_price}, stop_price=${leg.stop_price}`);
16274
+ });
16275
+ }
16276
+ }
16277
+ // Clean up: Cancel all test orders
16278
+ log('Cleaning up: Canceling all test orders...');
16279
+ await tradingAPI.cancelAllOrders();
16280
+ log('✓ All test orders canceled');
16281
+ log('=== All Stop and OCO Order Tests Passed ===');
16282
+ }
16283
+ catch (error) {
16284
+ log(`Error during tests: ${error}`, { type: 'error' });
16285
+ throw error;
16286
+ }
16287
+ }
14585
16288
  //# sourceMappingURL=test.js.map