@discomedia/utils 1.0.53 → 1.0.55

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,6 +11,7 @@ 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';
14
15
 
15
16
  /**
16
17
  * Logs a message to the console.
@@ -365,21 +366,6 @@ function getLastFullTradingDateImpl(currentDate = new Date()) {
365
366
  function getLastFullTradingDate(currentDate = new Date()) {
366
367
  return getLastFullTradingDateImpl(currentDate);
367
368
  }
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
- }
383
369
 
384
370
  /*
385
371
  How it works:
@@ -815,7 +801,7 @@ const safeJSON = (text) => {
815
801
  // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
816
802
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
817
803
 
818
- const VERSION = '6.15.0'; // x-release-please-version
804
+ const VERSION = '6.16.0'; // x-release-please-version
819
805
 
820
806
  // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
821
807
  const isRunningInBrowser = () => {
@@ -13295,6 +13281,11 @@ class AlpacaMarketDataAPI extends EventEmitter {
13295
13281
  optionWs = null;
13296
13282
  stockSubscriptions = { trades: [], quotes: [], bars: [] };
13297
13283
  optionSubscriptions = { trades: [], quotes: [], bars: [] };
13284
+ autoReconnect = true;
13285
+ stockReconnectAttempts = 0;
13286
+ optionReconnectAttempts = 0;
13287
+ stockReconnectTimeout = null;
13288
+ optionReconnectTimeout = null;
13298
13289
  setMode(mode = 'production') {
13299
13290
  if (mode === 'sandbox') {
13300
13291
  // sandbox mode
@@ -13323,6 +13314,13 @@ class AlpacaMarketDataAPI extends EventEmitter {
13323
13314
  return 'production';
13324
13315
  }
13325
13316
  }
13317
+ setAutoReconnect(enabled) {
13318
+ this.autoReconnect = enabled;
13319
+ log(`Auto-reconnect ${enabled ? 'enabled' : 'disabled'}`);
13320
+ }
13321
+ getAutoReconnect() {
13322
+ return this.autoReconnect;
13323
+ }
13326
13324
  constructor() {
13327
13325
  super();
13328
13326
  this.dataURL = 'https://data.alpaca.markets/v2';
@@ -13345,6 +13343,36 @@ class AlpacaMarketDataAPI extends EventEmitter {
13345
13343
  }
13346
13344
  return AlpacaMarketDataAPI.instance;
13347
13345
  }
13346
+ getReconnectDelay(attempt) {
13347
+ // 0: instant (0ms), 1: 5s, 2: 10s, 3: 15s, 4+: 60s
13348
+ const delays = [0, 5000, 10000, 15000, 60000];
13349
+ return attempt < delays.length ? delays[attempt] : 60000;
13350
+ }
13351
+ scheduleReconnect(streamType) {
13352
+ if (!this.autoReconnect) {
13353
+ log(`Auto-reconnect disabled, not reconnecting ${streamType} stream`);
13354
+ return;
13355
+ }
13356
+ const attempts = streamType === 'stock' ? this.stockReconnectAttempts : this.optionReconnectAttempts;
13357
+ const delay = this.getReconnectDelay(attempts);
13358
+ log(`Scheduling ${streamType} stream reconnect (attempt ${attempts + 1}) in ${delay}ms`);
13359
+ const timeout = setTimeout(() => {
13360
+ log(`Attempting to reconnect ${streamType} stream (attempt ${attempts + 1})`);
13361
+ if (streamType === 'stock') {
13362
+ this.stockReconnectAttempts++;
13363
+ }
13364
+ else {
13365
+ this.optionReconnectAttempts++;
13366
+ }
13367
+ this.connect(streamType);
13368
+ }, delay);
13369
+ if (streamType === 'stock') {
13370
+ this.stockReconnectTimeout = timeout;
13371
+ }
13372
+ else {
13373
+ this.optionReconnectTimeout = timeout;
13374
+ }
13375
+ }
13348
13376
  on(event, listener) {
13349
13377
  return super.on(event, listener);
13350
13378
  }
@@ -13362,6 +13390,21 @@ class AlpacaMarketDataAPI extends EventEmitter {
13362
13390
  }
13363
13391
  ws.on('open', () => {
13364
13392
  log(`${streamType} stream connected`);
13393
+ // Reset reconnect attempts on successful connection
13394
+ if (streamType === 'stock') {
13395
+ this.stockReconnectAttempts = 0;
13396
+ if (this.stockReconnectTimeout) {
13397
+ clearTimeout(this.stockReconnectTimeout);
13398
+ this.stockReconnectTimeout = null;
13399
+ }
13400
+ }
13401
+ else {
13402
+ this.optionReconnectAttempts = 0;
13403
+ if (this.optionReconnectTimeout) {
13404
+ clearTimeout(this.optionReconnectTimeout);
13405
+ this.optionReconnectTimeout = null;
13406
+ }
13407
+ }
13365
13408
  const authMessage = {
13366
13409
  action: 'auth',
13367
13410
  key: process.env.ALPACA_API_KEY,
@@ -13386,20 +13429,42 @@ class AlpacaMarketDataAPI extends EventEmitter {
13386
13429
  }
13387
13430
  }
13388
13431
  });
13389
- ws.on('close', () => {
13390
- log(`${streamType} stream disconnected`, { type: 'warn' });
13432
+ ws.on('close', (code, reason) => {
13433
+ const reasonStr = reason.toString() || 'No reason provided';
13434
+ const codeDesc = this.getCloseCodeDescription(code);
13435
+ log(`${streamType} stream disconnected - Code: ${code} (${codeDesc}), Reason: ${reasonStr}`, { type: 'warn' });
13391
13436
  if (streamType === 'stock') {
13392
13437
  this.stockWs = null;
13393
13438
  }
13394
13439
  else {
13395
13440
  this.optionWs = null;
13396
13441
  }
13397
- // Optional: implement reconnect logic
13442
+ this.scheduleReconnect(streamType);
13398
13443
  });
13399
13444
  ws.on('error', (error) => {
13400
13445
  log(`${streamType} stream error: ${error.message}`, { type: 'error' });
13401
13446
  });
13402
13447
  }
13448
+ getCloseCodeDescription(code) {
13449
+ const codes = {
13450
+ 1000: 'Normal Closure',
13451
+ 1001: 'Going Away',
13452
+ 1002: 'Protocol Error',
13453
+ 1003: 'Unsupported Data',
13454
+ 1005: 'No Status Received',
13455
+ 1006: 'Abnormal Closure',
13456
+ 1007: 'Invalid Frame Payload Data',
13457
+ 1008: 'Policy Violation',
13458
+ 1009: 'Message Too Big',
13459
+ 1010: 'Mandatory Extension',
13460
+ 1011: 'Internal Server Error',
13461
+ 1012: 'Service Restart',
13462
+ 1013: 'Try Again Later',
13463
+ 1014: 'Bad Gateway',
13464
+ 1015: 'TLS Handshake',
13465
+ };
13466
+ return codes[code] || 'Unknown';
13467
+ }
13403
13468
  sendSubscription(streamType) {
13404
13469
  const ws = streamType === 'stock' ? this.stockWs : this.optionWs;
13405
13470
  const subscriptions = streamType === 'stock' ? this.stockSubscriptions : this.optionSubscriptions;
@@ -13434,11 +13499,21 @@ class AlpacaMarketDataAPI extends EventEmitter {
13434
13499
  }
13435
13500
  }
13436
13501
  disconnectStockStream() {
13502
+ if (this.stockReconnectTimeout) {
13503
+ clearTimeout(this.stockReconnectTimeout);
13504
+ this.stockReconnectTimeout = null;
13505
+ }
13506
+ this.stockReconnectAttempts = 0;
13437
13507
  if (this.stockWs) {
13438
13508
  this.stockWs.close();
13439
13509
  }
13440
13510
  }
13441
13511
  disconnectOptionStream() {
13512
+ if (this.optionReconnectTimeout) {
13513
+ clearTimeout(this.optionReconnectTimeout);
13514
+ this.optionReconnectTimeout = null;
13515
+ }
13516
+ this.optionReconnectAttempts = 0;
13442
13517
  if (this.optionWs) {
13443
13518
  this.optionWs.close();
13444
13519
  }
@@ -14429,1603 +14504,9 @@ class AlpacaMarketDataAPI extends EventEmitter {
14429
14504
  }
14430
14505
  }
14431
14506
  // Export the singleton instance
14432
- const marketDataAPI = AlpacaMarketDataAPI.getInstance();
14433
-
14434
- const limitPriceSlippagePercent100 = 0.1; // 0.1%
14435
- /**
14436
- Websocket example
14437
- const alpacaAPI = createAlpacaTradingAPI(credentials); // type AlpacaCredentials
14438
- alpacaAPI.onTradeUpdate((update: TradeUpdate) => {
14439
- this.log(`Received trade update: event ${update.event} for an order to ${update.order.side} ${update.order.qty} of ${update.order.symbol}`);
14440
- });
14441
- alpacaAPI.connectWebsocket(); // necessary to connect to the WebSocket
14442
-
14443
- Portfolio History examples
14444
- // Get standard portfolio history
14445
- const portfolioHistory = await alpacaAPI.getPortfolioHistory({
14446
- timeframe: '1D',
14447
- period: '1M'
14448
- });
14449
-
14450
- // Get daily portfolio history with current day included (if available from hourly data)
14451
- const dailyHistory = await alpacaAPI.getPortfolioDailyHistory({
14452
- period: '1M'
14453
- });
14454
- */
14455
- class AlpacaTradingAPI {
14456
- static new(credentials) {
14457
- return new AlpacaTradingAPI(credentials);
14458
- }
14459
- static getInstance(credentials) {
14460
- return new AlpacaTradingAPI(credentials);
14461
- }
14462
- ws = null;
14463
- headers;
14464
- tradeUpdateCallback = null;
14465
- credentials;
14466
- apiBaseUrl;
14467
- wsUrl;
14468
- authenticated = false;
14469
- connecting = false;
14470
- reconnectDelay = 10000; // 10 seconds between reconnection attempts
14471
- reconnectTimeout = null;
14472
- messageHandlers = new Map();
14473
- debugLogging = false;
14474
- manualDisconnect = false;
14475
- /**
14476
- * Constructor for AlpacaTradingAPI
14477
- * @param credentials - Alpaca credentials,
14478
- * accountName: string; // The account identifier used inthis.logs and tracking
14479
- * apiKey: string; // Alpaca API key
14480
- * apiSecret: string; // Alpaca API secret
14481
- * type: AlpacaAccountType;
14482
- * orderType: AlpacaOrderType;
14483
- * @param options - Optional options
14484
- * debugLogging: boolean; // Whether to log messages of type 'debug'
14485
- */
14486
- constructor(credentials, options) {
14487
- this.credentials = credentials;
14488
- // Set URLs based on account type
14489
- this.apiBaseUrl =
14490
- credentials.type === 'PAPER' ? 'https://paper-api.alpaca.markets/v2' : 'https://api.alpaca.markets/v2';
14491
- this.wsUrl =
14492
- credentials.type === 'PAPER' ? 'wss://paper-api.alpaca.markets/stream' : 'wss://api.alpaca.markets/stream';
14493
- this.headers = {
14494
- 'APCA-API-KEY-ID': credentials.apiKey,
14495
- 'APCA-API-SECRET-KEY': credentials.apiSecret,
14496
- 'Content-Type': 'application/json',
14497
- };
14498
- // Initialize message handlers
14499
- this.messageHandlers.set('authorization', this.handleAuthMessage.bind(this));
14500
- this.messageHandlers.set('listening', this.handleListenMessage.bind(this));
14501
- this.messageHandlers.set('trade_updates', this.handleTradeUpdate.bind(this));
14502
- this.debugLogging = options?.debugLogging || false;
14503
- }
14504
- log(message, options = { type: 'info' }) {
14505
- if (this.debugLogging && options.type === 'debug') {
14506
- return;
14507
- }
14508
- log$1(message, { ...options, source: 'AlpacaTradingAPI', account: this.credentials.accountName });
14509
- }
14510
- /**
14511
- * Round a price to the nearest 2 decimal places for Alpaca, or 4 decimal places for prices less than $1
14512
- * @param price - The price to round
14513
- * @returns The rounded price
14514
- */
14515
- roundPriceForAlpaca = (price) => {
14516
- return price >= 1 ? Math.round(price * 100) / 100 : Math.round(price * 10000) / 10000;
14517
- };
14518
- handleAuthMessage(data) {
14519
- if (data.status === 'authorized') {
14520
- this.authenticated = true;
14521
- this.log('WebSocket authenticated');
14522
- }
14523
- else {
14524
- this.log(`Authentication failed: ${data.message || 'Unknown error'}`, {
14525
- type: 'error',
14526
- });
14527
- }
14528
- }
14529
- handleListenMessage(data) {
14530
- if (data.streams?.includes('trade_updates')) {
14531
- this.log('Successfully subscribed to trade updates');
14532
- }
14533
- }
14534
- handleTradeUpdate(data) {
14535
- if (this.tradeUpdateCallback) {
14536
- this.log(`Trade update: ${data.event} to ${data.order.side} ${data.order.qty} shares, type ${data.order.type}`, {
14537
- symbol: data.order.symbol,
14538
- type: 'debug',
14539
- });
14540
- this.tradeUpdateCallback(data);
14541
- }
14542
- }
14543
- handleMessage(message) {
14544
- try {
14545
- const data = JSON.parse(message);
14546
- const handler = this.messageHandlers.get(data.stream);
14547
- if (handler) {
14548
- handler(data.data);
14549
- }
14550
- else {
14551
- this.log(`Received message for unknown stream: ${data.stream}`, {
14552
- type: 'warn',
14553
- });
14554
- }
14555
- }
14556
- catch (error) {
14557
- this.log('Failed to parse WebSocket message', {
14558
- type: 'error',
14559
- metadata: { error: error instanceof Error ? error.message : 'Unknown error' },
14560
- });
14561
- }
14562
- }
14563
- connectWebsocket() {
14564
- // Reset manual disconnect flag to allow reconnection logic
14565
- this.manualDisconnect = false;
14566
- if (this.connecting) {
14567
- this.log('Connection attempt skipped - already connecting');
14568
- return;
14569
- }
14570
- if (this.ws?.readyState === WebSocket.OPEN) {
14571
- this.log('Connection attempt skipped - already connected');
14572
- return;
14573
- }
14574
- this.connecting = true;
14575
- if (this.ws) {
14576
- this.ws.removeAllListeners();
14577
- this.ws.terminate();
14578
- this.ws = null;
14579
- }
14580
- this.log(`Connecting to WebSocket at ${this.wsUrl}...`);
14581
- this.ws = new WebSocket(this.wsUrl);
14582
- this.ws.on('open', async () => {
14583
- try {
14584
- this.log('WebSocket connected');
14585
- await this.authenticate();
14586
- await this.subscribeToTradeUpdates();
14587
- this.connecting = false;
14588
- }
14589
- catch (error) {
14590
- this.log('Failed to setup WebSocket connection', {
14591
- type: 'error',
14592
- metadata: { error: error instanceof Error ? error.message : 'Unknown error' },
14593
- });
14594
- this.ws?.close();
14595
- }
14596
- });
14597
- this.ws.on('message', (data) => {
14598
- this.handleMessage(data.toString());
14599
- });
14600
- this.ws.on('error', (error) => {
14601
- this.log('WebSocket error', {
14602
- type: 'error',
14603
- metadata: { error: error instanceof Error ? error.message : 'Unknown error' },
14604
- });
14605
- this.connecting = false;
14606
- });
14607
- this.ws.on('close', () => {
14608
- this.log('WebSocket connection closed');
14609
- this.authenticated = false;
14610
- this.connecting = false;
14611
- // Clear any existing reconnect timeout
14612
- if (this.reconnectTimeout) {
14613
- clearTimeout(this.reconnectTimeout);
14614
- this.reconnectTimeout = null;
14615
- }
14616
- // Schedule reconnection unless this was a manual disconnect
14617
- if (!this.manualDisconnect) {
14618
- this.reconnectTimeout = setTimeout(() => {
14619
- this.log('Attempting to reconnect...');
14620
- this.connectWebsocket();
14621
- }, this.reconnectDelay);
14622
- }
14623
- });
14624
- }
14625
- /**
14626
- * Cleanly disconnect from the WebSocket and stop auto-reconnects
14627
- */
14628
- disconnect() {
14629
- // Prevent auto-reconnect scheduling
14630
- this.manualDisconnect = true;
14631
- // Clear any scheduled reconnect
14632
- if (this.reconnectTimeout) {
14633
- clearTimeout(this.reconnectTimeout);
14634
- this.reconnectTimeout = null;
14635
- }
14636
- if (this.ws) {
14637
- this.log('Disconnecting WebSocket...');
14638
- // Remove listeners first to avoid duplicate handlers after reconnects
14639
- this.ws.removeAllListeners();
14640
- try {
14641
- // Attempt graceful close
14642
- if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
14643
- this.ws.close(1000, 'Client disconnect');
14644
- }
14645
- else {
14646
- this.ws.terminate();
14647
- }
14648
- }
14649
- catch {
14650
- // Fallback terminate on any error
14651
- try {
14652
- this.ws.terminate();
14653
- }
14654
- catch { /* no-op */ }
14655
- }
14656
- this.ws = null;
14657
- }
14658
- this.authenticated = false;
14659
- this.connecting = false;
14660
- this.log('WebSocket disconnected');
14661
- }
14662
- async authenticate() {
14663
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
14664
- throw new Error('WebSocket not ready for authentication');
14665
- }
14666
- const authMessage = {
14667
- action: 'auth',
14668
- key: this.credentials.apiKey,
14669
- secret: this.credentials.apiSecret,
14670
- };
14671
- this.ws.send(JSON.stringify(authMessage));
14672
- return new Promise((resolve, reject) => {
14673
- const authTimeout = setTimeout(() => {
14674
- this.log('Authentication timeout', { type: 'error' });
14675
- reject(new Error('Authentication timed out'));
14676
- }, 10000);
14677
- const handleAuthResponse = (data) => {
14678
- try {
14679
- const message = JSON.parse(data.toString());
14680
- if (message.stream === 'authorization') {
14681
- this.ws?.removeListener('message', handleAuthResponse);
14682
- clearTimeout(authTimeout);
14683
- if (message.data?.status === 'authorized') {
14684
- this.authenticated = true;
14685
- resolve();
14686
- }
14687
- else {
14688
- const error = `Authentication failed: ${message.data?.message || 'Unknown error'}`;
14689
- this.log(error, { type: 'error' });
14690
- reject(new Error(error));
14691
- }
14692
- }
14693
- }
14694
- catch (error) {
14695
- this.log('Failed to parse auth response', {
14696
- type: 'error',
14697
- metadata: { error: error instanceof Error ? error.message : 'Unknown error' },
14698
- });
14699
- }
14700
- };
14701
- this.ws?.on('message', handleAuthResponse);
14702
- });
14703
- }
14704
- async subscribeToTradeUpdates() {
14705
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.authenticated) {
14706
- throw new Error('WebSocket not ready for subscription');
14707
- }
14708
- const listenMessage = {
14709
- action: 'listen',
14710
- data: {
14711
- streams: ['trade_updates'],
14712
- },
14713
- };
14714
- this.ws.send(JSON.stringify(listenMessage));
14715
- return new Promise((resolve, reject) => {
14716
- const listenTimeout = setTimeout(() => {
14717
- reject(new Error('Subscribe timeout'));
14718
- }, 10000);
14719
- const handleListenResponse = (data) => {
14720
- try {
14721
- const message = JSON.parse(data.toString());
14722
- if (message.stream === 'listening') {
14723
- this.ws?.removeListener('message', handleListenResponse);
14724
- clearTimeout(listenTimeout);
14725
- if (message.data?.streams?.includes('trade_updates')) {
14726
- resolve();
14727
- }
14728
- else {
14729
- reject(new Error('Failed to subscribe to trade updates'));
14730
- }
14731
- }
14732
- }
14733
- catch (error) {
14734
- this.log('Failed to parse listen response', {
14735
- type: 'error',
14736
- metadata: { error: error instanceof Error ? error.message : 'Unknown error' },
14737
- });
14738
- }
14739
- };
14740
- this.ws?.on('message', handleListenResponse);
14741
- });
14742
- }
14743
- async makeRequest(endpoint, method = 'GET', body, queryString = '') {
14744
- const url = `${this.apiBaseUrl}${endpoint}${queryString}`;
14745
- try {
14746
- const response = await fetch(url, {
14747
- method,
14748
- headers: this.headers,
14749
- body: body ? JSON.stringify(body) : undefined,
14750
- });
14751
- if (!response.ok) {
14752
- const errorText = await response.text();
14753
- this.log(`Alpaca API error (${response.status}): ${errorText}`, { type: 'error' });
14754
- throw new Error(`Alpaca API error (${response.status}): ${errorText}`);
14755
- }
14756
- // Handle responses with no content (e.g., 204 No Content)
14757
- if (response.status === 204 || response.headers.get('content-length') === '0') {
14758
- return null;
14759
- }
14760
- const contentType = response.headers.get('content-type');
14761
- if (contentType && contentType.includes('application/json')) {
14762
- return await response.json();
14763
- }
14764
- // For non-JSON responses, return the text content
14765
- const textContent = await response.text();
14766
- return textContent || null;
14767
- }
14768
- catch (err) {
14769
- const error = err;
14770
- this.log(`Error in makeRequest: ${error.message}. Url: ${url}`, {
14771
- source: 'AlpacaAPI',
14772
- type: 'error',
14773
- });
14774
- throw error;
14775
- }
14776
- }
14777
- async getPositions(assetClass) {
14778
- const positions = (await this.makeRequest('/positions'));
14779
- if (assetClass) {
14780
- return positions.filter((position) => position.asset_class === assetClass);
14781
- }
14782
- return positions;
14783
- }
14784
- /**
14785
- * Get all orders
14786
- * @param params (GetOrdersParams) - optional parameters to filter the orders
14787
- * - status: 'open' | 'closed' | 'all'
14788
- * - limit: number
14789
- * - after: string
14790
- * - until: string
14791
- * - direction: 'asc' | 'desc'
14792
- * - nested: boolean
14793
- * - symbols: string[], an array of all the symbols
14794
- * - side: 'buy' | 'sell'
14795
- * @returns all orders
14796
- */
14797
- async getOrders(params = {}) {
14798
- const queryParams = new URLSearchParams();
14799
- if (params.status)
14800
- queryParams.append('status', params.status);
14801
- if (params.limit)
14802
- queryParams.append('limit', params.limit.toString());
14803
- if (params.after)
14804
- queryParams.append('after', params.after);
14805
- if (params.until)
14806
- queryParams.append('until', params.until);
14807
- if (params.direction)
14808
- queryParams.append('direction', params.direction);
14809
- if (params.nested)
14810
- queryParams.append('nested', params.nested.toString());
14811
- if (params.symbols)
14812
- queryParams.append('symbols', params.symbols.join(','));
14813
- if (params.side)
14814
- queryParams.append('side', params.side);
14815
- const endpoint = `/orders${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
14816
- try {
14817
- return await this.makeRequest(endpoint);
14818
- }
14819
- catch (error) {
14820
- this.log(`Error getting orders: ${error}`, { type: 'error' });
14821
- throw error;
14822
- }
14823
- }
14824
- async getAccountDetails() {
14825
- try {
14826
- return await this.makeRequest('/account');
14827
- }
14828
- catch (error) {
14829
- this.log(`Error getting account details: ${error}`, { type: 'error' });
14830
- throw error;
14831
- }
14832
- }
14833
- /**
14834
- * Create a trailing stop order
14835
- * @param symbol (string) - the symbol of the order
14836
- * @param qty (number) - the quantity of the order
14837
- * @param side (string) - the side of the order
14838
- * @param trailPercent100 (number) - the trail percent of the order (scale 100, i.e. 0.5 = 0.5%)
14839
- * @param position_intent (string) - the position intent of the order
14840
- */
14841
- async createTrailingStop(symbol, qty, side, trailPercent100, position_intent) {
14842
- this.log(`Creating trailing stop ${side.toUpperCase()} ${qty} shares for ${symbol} with trail percent ${trailPercent100}%`, {
14843
- symbol,
14844
- });
14845
- try {
14846
- await this.makeRequest(`/orders`, 'POST', {
14847
- symbol,
14848
- qty: Math.abs(qty),
14849
- side,
14850
- position_intent,
14851
- order_class: 'simple',
14852
- type: 'trailing_stop',
14853
- trail_percent: trailPercent100, // Already in decimal form (e.g., 4 for 4%)
14854
- time_in_force: 'gtc',
14855
- });
14856
- }
14857
- catch (error) {
14858
- this.log(`Error creating trailing stop: ${error}`, {
14859
- symbol,
14860
- type: 'error',
14861
- });
14862
- throw error;
14863
- }
14864
- }
14865
- /**
14866
- * Create a market order
14867
- * @param symbol (string) - the symbol of the order
14868
- * @param qty (number) - the quantity of the order
14869
- * @param side (string) - the side of the order
14870
- * @param position_intent (string) - the position intent of the order. Important for knowing if a position needs a trailing stop.
14871
- * @param client_order_id (string) - optional client order id
14872
- */
14873
- async createMarketOrder(symbol, qty, side, position_intent, client_order_id) {
14874
- this.log(`Creating market order for ${symbol}: ${side} ${qty} shares (${position_intent})`, {
14875
- symbol,
14876
- });
14877
- const body = {
14878
- symbol,
14879
- qty: Math.abs(qty).toString(),
14880
- side,
14881
- position_intent,
14882
- type: 'market',
14883
- time_in_force: 'day',
14884
- order_class: 'simple',
14885
- };
14886
- if (client_order_id !== undefined) {
14887
- body.client_order_id = client_order_id;
14888
- }
14889
- try {
14890
- return await this.makeRequest('/orders', 'POST', body);
14891
- }
14892
- catch (error) {
14893
- this.log(`Error creating market order: ${error}`, { type: 'error' });
14894
- throw error;
14895
- }
14896
- }
14897
- /**
14898
- * Create a Market on Open (MOO) order - executes in the opening auction
14899
- *
14900
- * IMPORTANT TIMING CONSTRAINTS:
14901
- * - Valid submission window: After 7:00pm ET and before 9:28am ET
14902
- * - Orders submitted between 9:28am and 7:00pm ET will be REJECTED
14903
- * - Orders submitted after 7:00pm ET are queued for the next trading day's opening auction
14904
- * - Example: An order at 8:00pm Monday will execute at Tuesday's market open (9:30am)
14905
- *
14906
- * @param symbol - The symbol of the order
14907
- * @param qty - The quantity of shares
14908
- * @param side - Buy or sell
14909
- * @param position_intent - The position intent (buy_to_open, sell_to_close, etc.)
14910
- * @param client_order_id - Optional client order id
14911
- * @returns The created order
14912
- */
14913
- async createMOOOrder(symbol, qty, side, position_intent, client_order_id) {
14914
- this.log(`Creating Market on Open order for ${symbol}: ${side} ${qty} shares (${position_intent})`, {
14915
- symbol,
14916
- });
14917
- const body = {
14918
- symbol,
14919
- qty: Math.abs(qty).toString(),
14920
- side,
14921
- position_intent,
14922
- type: 'market',
14923
- time_in_force: 'opg',
14924
- order_class: 'simple',
14925
- };
14926
- if (client_order_id !== undefined) {
14927
- body.client_order_id = client_order_id;
14928
- }
14929
- try {
14930
- return await this.makeRequest('/orders', 'POST', body);
14931
- }
14932
- catch (error) {
14933
- this.log(`Error creating MOO order: ${error}`, { type: 'error' });
14934
- throw error;
14935
- }
14936
- }
14937
- /**
14938
- * Create a Market on Close (MOC) order - executes in the closing auction
14939
- *
14940
- * IMPORTANT TIMING CONSTRAINTS:
14941
- * - Valid submission window: After 7:00pm ET (previous day) and before 3:50pm ET (same day)
14942
- * - Orders submitted between 3:50pm and 7:00pm ET will be REJECTED
14943
- * - Orders submitted after 7:00pm ET are queued for the next trading day's closing auction
14944
- * - Example: An order at 8:00pm Monday will execute at Tuesday's market close (4:00pm)
14945
- *
14946
- * @param symbol - The symbol of the order
14947
- * @param qty - The quantity of shares
14948
- * @param side - Buy or sell
14949
- * @param position_intent - The position intent (buy_to_open, sell_to_close, etc.)
14950
- * @param client_order_id - Optional client order id
14951
- * @returns The created order
14952
- */
14953
- async createMOCOrder(symbol, qty, side, position_intent, client_order_id) {
14954
- this.log(`Creating Market on Close order for ${symbol}: ${side} ${qty} shares (${position_intent})`, {
14955
- symbol,
14956
- });
14957
- const body = {
14958
- symbol,
14959
- qty: Math.abs(qty).toString(),
14960
- side,
14961
- position_intent,
14962
- type: 'market',
14963
- time_in_force: 'cls',
14964
- order_class: 'simple',
14965
- };
14966
- if (client_order_id !== undefined) {
14967
- body.client_order_id = client_order_id;
14968
- }
14969
- try {
14970
- return await this.makeRequest('/orders', 'POST', body);
14971
- }
14972
- catch (error) {
14973
- this.log(`Error creating MOC order: ${error}`, { type: 'error' });
14974
- throw error;
14975
- }
14976
- }
14977
- /**
14978
- * 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.
14979
- * @param symbol (string) - the symbol of the order
14980
- * @returns the current trail percent
14981
- */
14982
- async getCurrentTrailPercent(symbol) {
14983
- try {
14984
- const orders = await this.getOrders({
14985
- status: 'open',
14986
- symbols: [symbol],
14987
- });
14988
- const trailingStopOrder = orders.find((order) => order.type === 'trailing_stop' &&
14989
- (order.position_intent === 'sell_to_close' || order.position_intent === 'buy_to_close'));
14990
- if (!trailingStopOrder) {
14991
- this.log(`No closing trailing stop order found for ${symbol}`, {
14992
- symbol,
14993
- });
14994
- return null;
14995
- }
14996
- if (!trailingStopOrder.trail_percent) {
14997
- this.log(`Trailing stop order found for ${symbol} but no trail_percent value`, {
14998
- symbol,
14999
- });
15000
- return null;
15001
- }
15002
- const trailPercent = parseFloat(trailingStopOrder.trail_percent);
15003
- return trailPercent;
15004
- }
15005
- catch (error) {
15006
- this.log(`Error getting current trail percent: ${error}`, {
15007
- symbol,
15008
- type: 'error',
15009
- });
15010
- throw error;
15011
- }
15012
- }
15013
- /**
15014
- * Update the trail percent for a trailing stop order
15015
- * @param symbol (string) - the symbol of the order
15016
- * @param trailPercent100 (number) - the trail percent of the order (scale 100, i.e. 0.5 = 0.5%)
15017
- */
15018
- async updateTrailingStop(symbol, trailPercent100) {
15019
- // First get all open orders for this symbol
15020
- const orders = await this.getOrders({
15021
- status: 'open',
15022
- symbols: [symbol],
15023
- });
15024
- // Find the trailing stop order
15025
- const trailingStopOrder = orders.find((order) => order.type === 'trailing_stop');
15026
- if (!trailingStopOrder) {
15027
- this.log(`No open trailing stop order found for ${symbol}`, { type: 'error', symbol });
15028
- return;
15029
- }
15030
- // Check if the trail_percent is already set to the desired value
15031
- const currentTrailPercent = trailingStopOrder.trail_percent ? parseFloat(trailingStopOrder.trail_percent) : null;
15032
- // Compare with a small epsilon to handle floating point precision
15033
- const epsilon = 0.0001;
15034
- if (currentTrailPercent !== null && Math.abs(currentTrailPercent - trailPercent100) < epsilon) {
15035
- this.log(`Trailing stop for ${symbol} already set to ${trailPercent100}% (current: ${currentTrailPercent}%), skipping update`, {
15036
- symbol,
15037
- });
15038
- return;
15039
- }
15040
- this.log(`Updating trailing stop for ${symbol} from ${currentTrailPercent}% to ${trailPercent100}%`, {
15041
- symbol,
15042
- });
15043
- try {
15044
- await this.makeRequest(`/orders/${trailingStopOrder.id}`, 'PATCH', {
15045
- trail: trailPercent100.toString(), // Changed from trail_percent to trail
15046
- });
15047
- }
15048
- catch (error) {
15049
- this.log(`Error updating trailing stop: ${error}`, {
15050
- symbol,
15051
- type: 'error',
15052
- });
15053
- throw error;
15054
- }
15055
- }
15056
- /**
15057
- * Cancel all open orders
15058
- */
15059
- async cancelAllOrders() {
15060
- this.log(`Canceling all open orders`);
15061
- try {
15062
- await this.makeRequest('/orders', 'DELETE');
15063
- }
15064
- catch (error) {
15065
- this.log(`Error canceling all orders: ${error}`, { type: 'error' });
15066
- }
15067
- }
15068
- /**
15069
- * Cancel a specific order by its ID
15070
- * @param orderId The id of the order to cancel
15071
- * @throws Error if the order is not cancelable (status 422) or if the order doesn't exist
15072
- * @returns Promise that resolves when the order is successfully canceled
15073
- */
15074
- async cancelOrder(orderId) {
15075
- this.log(`Attempting to cancel order ${orderId}`);
15076
- try {
15077
- await this.makeRequest(`/orders/${orderId}`, 'DELETE');
15078
- this.log(`Successfully canceled order ${orderId}`);
15079
- }
15080
- catch (error) {
15081
- // If the error is a 422, it means the order is not cancelable
15082
- if (error instanceof Error && error.message.includes('422')) {
15083
- this.log(`Order ${orderId} is not cancelable`, {
15084
- type: 'error',
15085
- });
15086
- throw new Error(`Order ${orderId} is not cancelable`);
15087
- }
15088
- // Re-throw other errors
15089
- throw error;
15090
- }
15091
- }
15092
- /**
15093
- * Create a limit order
15094
- * @param symbol (string) - the symbol of the order
15095
- * @param qty (number) - the quantity of the order
15096
- * @param side (string) - the side of the order
15097
- * @param limitPrice (number) - the limit price of the order
15098
- * @param position_intent (string) - the position intent of the order
15099
- * @param extended_hours (boolean) - whether the order is in extended hours
15100
- * @param client_order_id (string) - the client order id of the order
15101
- */
15102
- async createLimitOrder(symbol, qty, side, limitPrice, position_intent, extended_hours = false, client_order_id) {
15103
- this.log(`Creating limit order for ${symbol}: ${side} ${qty} shares at $${limitPrice.toFixed(2)} (${position_intent})`, {
15104
- symbol,
15105
- });
15106
- const body = {
15107
- symbol,
15108
- qty: Math.abs(qty).toString(),
15109
- side,
15110
- position_intent,
15111
- type: 'limit',
15112
- limit_price: this.roundPriceForAlpaca(limitPrice).toString(),
15113
- time_in_force: 'day',
15114
- order_class: 'simple',
15115
- extended_hours,
15116
- };
15117
- if (client_order_id !== undefined) {
15118
- body.client_order_id = client_order_id;
15119
- }
15120
- try {
15121
- return await this.makeRequest('/orders', 'POST', body);
15122
- }
15123
- catch (error) {
15124
- this.log(`Error creating limit order: ${error}`, { type: 'error' });
15125
- throw error;
15126
- }
15127
- }
15128
- /**
15129
- * Close all equities positions
15130
- * @param options (object) - the options for closing the positions
15131
- * - cancel_orders (boolean) - whether to cancel related orders
15132
- * - useLimitOrders (boolean) - whether to use limit orders to close the positions
15133
- */
15134
- async closeAllPositions(options = { cancel_orders: true, useLimitOrders: false }) {
15135
- this.log(`Closing all positions${options.useLimitOrders ? ' using limit orders' : ''}${options.cancel_orders ? ' and canceling open orders' : ''}`);
15136
- if (options.useLimitOrders) {
15137
- // Get all positions
15138
- const positions = await this.getPositions('us_equity');
15139
- if (positions.length === 0) {
15140
- this.log('No positions to close');
15141
- return;
15142
- }
15143
- this.log(`Found ${positions.length} positions to close`);
15144
- // Get latest quotes for all positions
15145
- const symbols = positions.map((position) => position.symbol);
15146
- const quotesResponse = await marketDataAPI.getLatestQuotes(symbols);
15147
- const lengthOfQuotes = Object.keys(quotesResponse.quotes).length;
15148
- if (lengthOfQuotes === 0) {
15149
- this.log('No quotes available for positions, received 0 quotes', {
15150
- type: 'error',
15151
- });
15152
- return;
15153
- }
15154
- if (lengthOfQuotes !== positions.length) {
15155
- this.log(`Received ${lengthOfQuotes} quotes for ${positions.length} positions, expected ${positions.length} quotes`, { type: 'warn' });
15156
- return;
15157
- }
15158
- // Create limit orders to close each position
15159
- for (const position of positions) {
15160
- const quote = quotesResponse.quotes[position.symbol];
15161
- if (!quote) {
15162
- this.log(`No quote available for ${position.symbol}, skipping limit order`, {
15163
- symbol: position.symbol,
15164
- type: 'warn',
15165
- });
15166
- continue;
15167
- }
15168
- const qty = Math.abs(parseFloat(position.qty));
15169
- const side = position.side === 'long' ? 'sell' : 'buy';
15170
- const positionIntent = side === 'sell' ? 'sell_to_close' : 'buy_to_close';
15171
- // Get the current price from the quote
15172
- const currentPrice = side === 'sell' ? quote.bp : quote.ap; // Use bid for sells, ask for buys
15173
- if (!currentPrice) {
15174
- this.log(`No valid price available for ${position.symbol}, skipping limit order`, {
15175
- symbol: position.symbol,
15176
- type: 'warn',
15177
- });
15178
- continue;
15179
- }
15180
- // Apply slippage from config
15181
- const limitSlippagePercent1 = limitPriceSlippagePercent100 / 100;
15182
- const limitPrice = side === 'sell'
15183
- ? this.roundPriceForAlpaca(currentPrice * (1 - limitSlippagePercent1)) // Sell slightly lower
15184
- : this.roundPriceForAlpaca(currentPrice * (1 + limitSlippagePercent1)); // Buy slightly higher
15185
- this.log(`Creating limit order to close ${position.symbol} position: ${side} ${qty} shares at $${limitPrice.toFixed(2)}`, {
15186
- symbol: position.symbol,
15187
- });
15188
- await this.createLimitOrder(position.symbol, qty, side, limitPrice, positionIntent);
15189
- }
15190
- }
15191
- else {
15192
- await this.makeRequest('/positions', 'DELETE', undefined, options.cancel_orders ? '?cancel_orders=true' : '');
15193
- }
15194
- }
15195
- /**
15196
- * Close all equities positions using limit orders during extended hours trading
15197
- * @param cancelOrders Whether to cancel related orders (default: true)
15198
- * @returns Promise that resolves when all positions are closed
15199
- */
15200
- async closeAllPositionsAfterHours() {
15201
- this.log('Closing all positions using limit orders during extended hours trading');
15202
- // Get all positions
15203
- const positions = await this.getPositions();
15204
- this.log(`Found ${positions.length} positions to close`);
15205
- if (positions.length === 0) {
15206
- this.log('No positions to close');
15207
- return;
15208
- }
15209
- await this.cancelAllOrders();
15210
- this.log(`Cancelled all open orders`);
15211
- // Get latest quotes for all positions
15212
- const symbols = positions.map((position) => position.symbol);
15213
- const quotesResponse = await marketDataAPI.getLatestQuotes(symbols);
15214
- // Create limit orders to close each position
15215
- for (const position of positions) {
15216
- const quote = quotesResponse.quotes[position.symbol];
15217
- if (!quote) {
15218
- this.log(`No quote available for ${position.symbol}, skipping limit order`, {
15219
- symbol: position.symbol,
15220
- type: 'warn',
15221
- });
15222
- continue;
15223
- }
15224
- const qty = Math.abs(parseFloat(position.qty));
15225
- const side = position.side === 'long' ? 'sell' : 'buy';
15226
- const positionIntent = side === 'sell' ? 'sell_to_close' : 'buy_to_close';
15227
- // Get the current price from the quote
15228
- const currentPrice = side === 'sell' ? quote.bp : quote.ap; // Use bid for sells, ask for buys
15229
- if (!currentPrice) {
15230
- this.log(`No valid price available for ${position.symbol}, skipping limit order`, {
15231
- symbol: position.symbol,
15232
- type: 'warn',
15233
- });
15234
- continue;
15235
- }
15236
- // Apply slippage from config
15237
- const limitSlippagePercent1 = limitPriceSlippagePercent100 / 100;
15238
- const limitPrice = side === 'sell'
15239
- ? this.roundPriceForAlpaca(currentPrice * (1 - limitSlippagePercent1)) // Sell slightly lower
15240
- : this.roundPriceForAlpaca(currentPrice * (1 + limitSlippagePercent1)); // Buy slightly higher
15241
- this.log(`Creating extended hours limit order to close ${position.symbol} position: ${side} ${qty} shares at $${limitPrice.toFixed(2)}`, {
15242
- symbol: position.symbol,
15243
- });
15244
- await this.createLimitOrder(position.symbol, qty, side, limitPrice, positionIntent, true // Enable extended hours trading
15245
- );
15246
- }
15247
- this.log(`All positions closed: ${positions.map((p) => p.symbol).join(', ')}`);
15248
- }
15249
- onTradeUpdate(callback) {
15250
- this.tradeUpdateCallback = callback;
15251
- }
15252
- /**
15253
- * Get portfolio history for the account
15254
- * @param params Parameters for the portfolio history request
15255
- * @returns Portfolio history data
15256
- */
15257
- async getPortfolioHistory(params) {
15258
- const queryParams = new URLSearchParams();
15259
- if (params.timeframe)
15260
- queryParams.append('timeframe', params.timeframe);
15261
- if (params.period)
15262
- queryParams.append('period', params.period);
15263
- if (params.extended_hours !== undefined)
15264
- queryParams.append('extended_hours', params.extended_hours.toString());
15265
- if (params.start)
15266
- queryParams.append('start', params.start);
15267
- if (params.end)
15268
- queryParams.append('end', params.end);
15269
- if (params.date_end)
15270
- queryParams.append('date_end', params.date_end);
15271
- const response = await this.makeRequest(`/account/portfolio/history?${queryParams.toString()}`);
15272
- return response;
15273
- }
15274
- /**
15275
- * Get portfolio daily history for the account, ensuring the most recent day is included
15276
- * by combining daily and hourly history if needed.
15277
- *
15278
- * This function performs two API calls:
15279
- * 1. Retrieves daily portfolio history
15280
- * 2. Retrieves hourly portfolio history to check for more recent data
15281
- *
15282
- * If hourly history has timestamps more recent than the last timestamp in daily history,
15283
- * it appends one additional day to the daily history using the most recent hourly values.
15284
- *
15285
- * @param params Parameters for the portfolio history request (same as getPortfolioHistory except timeframe is forced to '1D')
15286
- * @returns Portfolio history data with daily timeframe, including the most recent day if available from hourly data
15287
- */
15288
- async getPortfolioDailyHistory(params) {
15289
- // Get daily and hourly history in parallel
15290
- const dailyParams = { ...params, timeframe: '1D' };
15291
- const hourlyParams = { timeframe: '1Min', period: '1D' };
15292
- const [dailyHistory, hourlyHistory] = await Promise.all([
15293
- this.getPortfolioHistory(dailyParams),
15294
- this.getPortfolioHistory(hourlyParams)
15295
- ]);
15296
- // If no hourly history, return daily as-is
15297
- if (!hourlyHistory.timestamp || hourlyHistory.timestamp.length === 0) {
15298
- return dailyHistory;
15299
- }
15300
- // Get the last timestamp from daily history
15301
- const lastDailyTimestamp = dailyHistory.timestamp[dailyHistory.timestamp.length - 1];
15302
- // Check if hourly history has more recent data
15303
- const recentHourlyData = hourlyHistory.timestamp
15304
- .map((timestamp, index) => ({ timestamp, index }))
15305
- .filter(({ timestamp }) => timestamp > lastDailyTimestamp);
15306
- // If no more recent hourly data, return daily history as-is
15307
- if (recentHourlyData.length === 0) {
15308
- return dailyHistory;
15309
- }
15310
- // Get the most recent hourly data point
15311
- const mostRecentHourly = recentHourlyData[recentHourlyData.length - 1];
15312
- const mostRecentIndex = mostRecentHourly.index;
15313
- // Calculate the timestamp for the new daily entry.
15314
- // Alpaca's daily history timestamps are at 00:00:00Z for the calendar day
15315
- // following the NY trading date. Derive the trading date in NY time from the
15316
- // most recent intraday timestamp, then set the new daily timestamp to
15317
- // midnight UTC of the next calendar day.
15318
- const mostRecentMs = mostRecentHourly.timestamp * 1000; // hourly timestamps are seconds
15319
- const tradingDateStr = getTradingDate(new Date(mostRecentMs)); // e.g., '2025-09-05' (NY trading date)
15320
- const [yearStr, monthStr, dayStr] = tradingDateStr.split('-');
15321
- const year = Number(yearStr);
15322
- const month = Number(monthStr); // 1-based
15323
- const day = Number(dayStr);
15324
- const newDailyTimestamp = Math.floor(Date.UTC(year, month - 1, day + 1, 0, 0, 0, 0) / 1000);
15325
- // Create a new daily history entry with the most recent hourly values
15326
- const updatedDailyHistory = {
15327
- ...dailyHistory,
15328
- timestamp: [...dailyHistory.timestamp, newDailyTimestamp],
15329
- equity: [...dailyHistory.equity, hourlyHistory.equity[mostRecentIndex]],
15330
- profit_loss: [...dailyHistory.profit_loss, hourlyHistory.profit_loss[mostRecentIndex]],
15331
- profit_loss_pct: [...dailyHistory.profit_loss_pct, hourlyHistory.profit_loss_pct[mostRecentIndex]],
15332
- };
15333
- return updatedDailyHistory;
15334
- }
15335
- /**
15336
- * Get option contracts based on specified parameters
15337
- * @param params Parameters to filter option contracts
15338
- * @returns Option contracts matching the criteria
15339
- */
15340
- async getOptionContracts(params) {
15341
- const queryParams = new URLSearchParams();
15342
- queryParams.append('underlying_symbols', params.underlying_symbols.join(','));
15343
- if (params.expiration_date_gte)
15344
- queryParams.append('expiration_date_gte', params.expiration_date_gte);
15345
- if (params.expiration_date_lte)
15346
- queryParams.append('expiration_date_lte', params.expiration_date_lte);
15347
- if (params.strike_price_gte)
15348
- queryParams.append('strike_price_gte', params.strike_price_gte);
15349
- if (params.strike_price_lte)
15350
- queryParams.append('strike_price_lte', params.strike_price_lte);
15351
- if (params.type)
15352
- queryParams.append('type', params.type);
15353
- if (params.status)
15354
- queryParams.append('status', params.status);
15355
- if (params.limit)
15356
- queryParams.append('limit', params.limit.toString());
15357
- if (params.page_token)
15358
- queryParams.append('page_token', params.page_token);
15359
- this.log(`Fetching option contracts for ${params.underlying_symbols.join(', ')}`, {
15360
- symbol: params.underlying_symbols.join(', '),
15361
- });
15362
- const response = (await this.makeRequest(`/options/contracts?${queryParams.toString()}`));
15363
- this.log(`Found ${response.option_contracts.length} option contracts`, {
15364
- symbol: params.underlying_symbols.join(', '),
15365
- });
15366
- return response;
15367
- }
15368
- /**
15369
- * Get a specific option contract by symbol or ID
15370
- * @param symbolOrId The symbol or ID of the option contract
15371
- * @returns The option contract details
15372
- */
15373
- async getOptionContract(symbolOrId) {
15374
- this.log(`Fetching option contract details for ${symbolOrId}`, {
15375
- symbol: symbolOrId,
15376
- });
15377
- const response = (await this.makeRequest(`/options/contracts/${symbolOrId}`));
15378
- this.log(`Found option contract details for ${symbolOrId}: ${response.name}`, {
15379
- symbol: symbolOrId,
15380
- });
15381
- return response;
15382
- }
15383
- /**
15384
- * Create a simple option order (market or limit)
15385
- * @param symbol Option contract symbol
15386
- * @param qty Quantity of contracts (must be a whole number)
15387
- * @param side Buy or sell
15388
- * @param position_intent Position intent (buy_to_open, buy_to_close, sell_to_open, sell_to_close)
15389
- * @param type Order type (market or limit)
15390
- * @param limitPrice Limit price (required for limit orders)
15391
- * @returns The created order
15392
- */
15393
- async createOptionOrder(symbol, qty, side, position_intent, type, limitPrice) {
15394
- if (!Number.isInteger(qty) || qty <= 0) {
15395
- this.log('Quantity must be a positive whole number for option orders', { type: 'error' });
15396
- }
15397
- if (type === 'limit' && limitPrice === undefined) {
15398
- this.log('Limit price is required for limit orders', { type: 'error' });
15399
- }
15400
- this.log(`Creating ${type} option order for ${symbol}: ${side} ${qty} contracts (${position_intent})${type === 'limit' ? ` at $${limitPrice?.toFixed(2)}` : ''}`, {
15401
- symbol,
15402
- });
15403
- const orderData = {
15404
- symbol,
15405
- qty: qty.toString(),
15406
- side,
15407
- position_intent,
15408
- type,
15409
- time_in_force: 'day',
15410
- order_class: 'simple',
15411
- extended_hours: false,
15412
- };
15413
- if (type === 'limit' && limitPrice !== undefined) {
15414
- orderData.limit_price = this.roundPriceForAlpaca(limitPrice).toString();
15415
- }
15416
- return this.makeRequest('/orders', 'POST', orderData);
15417
- }
15418
- /**
15419
- * Create a multi-leg option order
15420
- * @param legs Array of order legs
15421
- * @param qty Quantity of the multi-leg order (must be a whole number)
15422
- * @param type Order type (market or limit)
15423
- * @param limitPrice Limit price (required for limit orders)
15424
- * @returns The created multi-leg order
15425
- */
15426
- async createMultiLegOptionOrder(legs, qty, type, limitPrice) {
15427
- if (!Number.isInteger(qty) || qty <= 0) {
15428
- this.log('Quantity must be a positive whole number for option orders', { type: 'error' });
15429
- }
15430
- if (type === 'limit' && limitPrice === undefined) {
15431
- this.log('Limit price is required for limit orders', { type: 'error' });
15432
- }
15433
- if (legs.length < 2) {
15434
- this.log('Multi-leg orders require at least 2 legs', { type: 'error' });
15435
- }
15436
- const legSymbols = legs.map((leg) => leg.symbol).join(', ');
15437
- this.log(`Creating multi-leg ${type} option order with ${legs.length} legs (${legSymbols})${type === 'limit' ? ` at $${limitPrice?.toFixed(2)}` : ''}`, {
15438
- symbol: legSymbols,
15439
- });
15440
- const orderData = {
15441
- order_class: 'mleg',
15442
- qty: qty.toString(),
15443
- type,
15444
- time_in_force: 'day',
15445
- legs,
15446
- };
15447
- if (type === 'limit' && limitPrice !== undefined) {
15448
- orderData.limit_price = this.roundPriceForAlpaca(limitPrice).toString();
15449
- }
15450
- return this.makeRequest('/orders', 'POST', orderData);
15451
- }
15452
- /**
15453
- * Exercise an option contract
15454
- * @param symbolOrContractId The symbol or ID of the option contract to exercise
15455
- * @returns Response from the exercise request
15456
- */
15457
- async exerciseOption(symbolOrContractId) {
15458
- this.log(`Exercising option contract ${symbolOrContractId}`, {
15459
- symbol: symbolOrContractId,
15460
- });
15461
- return this.makeRequest(`/positions/${symbolOrContractId}/exercise`, 'POST');
15462
- }
15463
- /**
15464
- * Get option positions
15465
- * @returns Array of option positions
15466
- */
15467
- async getOptionPositions() {
15468
- this.log('Fetching option positions');
15469
- const positions = await this.getPositions('us_option');
15470
- return positions;
15471
- }
15472
- async getOptionsOpenSpreadTrades() {
15473
- this.log('Fetching option open trades');
15474
- // this function will get all open positions, extract the symbol and see when they were created.
15475
- // figures out when the earliest date was (should be today)
15476
- // then it pulls all orders after the earliest date that were closed and that were of class 'mleg'
15477
- // Each of these contains two orders. they look like this:
15478
- }
15479
- /**
15480
- * Get option account activities (exercises, assignments, expirations)
15481
- * @param activityType Type of option activity to filter by
15482
- * @param date Date to filter activities (YYYY-MM-DD format)
15483
- * @returns Array of option account activities
15484
- */
15485
- async getOptionActivities(activityType, date) {
15486
- const queryParams = new URLSearchParams();
15487
- if (activityType) {
15488
- queryParams.append('activity_types', activityType);
15489
- }
15490
- else {
15491
- queryParams.append('activity_types', 'OPEXC,OPASN,OPEXP');
15492
- }
15493
- if (date) {
15494
- queryParams.append('date', date);
15495
- }
15496
- this.log(`Fetching option activities${activityType ? ` of type ${activityType}` : ''}${date ? ` for date ${date}` : ''}`);
15497
- return this.makeRequest(`/account/activities?${queryParams.toString()}`);
15498
- }
15499
- /**
15500
- * Create a long call spread (buy lower strike call, sell higher strike call)
15501
- * @param lowerStrikeCallSymbol Symbol of the lower strike call option
15502
- * @param higherStrikeCallSymbol Symbol of the higher strike call option
15503
- * @param qty Quantity of spreads to create (must be a whole number)
15504
- * @param limitPrice Limit price for the spread
15505
- * @returns The created multi-leg order
15506
- */
15507
- async createLongCallSpread(lowerStrikeCallSymbol, higherStrikeCallSymbol, qty, limitPrice) {
15508
- this.log(`Creating long call spread: Buy ${lowerStrikeCallSymbol}, Sell ${higherStrikeCallSymbol}, Qty: ${qty}, Price: $${limitPrice.toFixed(2)}`, {
15509
- symbol: `${lowerStrikeCallSymbol},${higherStrikeCallSymbol}`,
15510
- });
15511
- const legs = [
15512
- {
15513
- symbol: lowerStrikeCallSymbol,
15514
- ratio_qty: '1',
15515
- side: 'buy',
15516
- position_intent: 'buy_to_open',
15517
- },
15518
- {
15519
- symbol: higherStrikeCallSymbol,
15520
- ratio_qty: '1',
15521
- side: 'sell',
15522
- position_intent: 'sell_to_open',
15523
- },
15524
- ];
15525
- return this.createMultiLegOptionOrder(legs, qty, 'limit', limitPrice);
15526
- }
15527
- /**
15528
- * Create a long put spread (buy higher strike put, sell lower strike put)
15529
- * @param higherStrikePutSymbol Symbol of the higher strike put option
15530
- * @param lowerStrikePutSymbol Symbol of the lower strike put option
15531
- * @param qty Quantity of spreads to create (must be a whole number)
15532
- * @param limitPrice Limit price for the spread
15533
- * @returns The created multi-leg order
15534
- */
15535
- async createLongPutSpread(higherStrikePutSymbol, lowerStrikePutSymbol, qty, limitPrice) {
15536
- this.log(`Creating long put spread: Buy ${higherStrikePutSymbol}, Sell ${lowerStrikePutSymbol}, Qty: ${qty}, Price: $${limitPrice.toFixed(2)}`, {
15537
- symbol: `${higherStrikePutSymbol},${lowerStrikePutSymbol}`,
15538
- });
15539
- const legs = [
15540
- {
15541
- symbol: higherStrikePutSymbol,
15542
- ratio_qty: '1',
15543
- side: 'buy',
15544
- position_intent: 'buy_to_open',
15545
- },
15546
- {
15547
- symbol: lowerStrikePutSymbol,
15548
- ratio_qty: '1',
15549
- side: 'sell',
15550
- position_intent: 'sell_to_open',
15551
- },
15552
- ];
15553
- return this.createMultiLegOptionOrder(legs, qty, 'limit', limitPrice);
15554
- }
15555
- /**
15556
- * Create an iron condor (sell call spread and put spread)
15557
- * @param longPutSymbol Symbol of the lower strike put (long)
15558
- * @param shortPutSymbol Symbol of the higher strike put (short)
15559
- * @param shortCallSymbol Symbol of the lower strike call (short)
15560
- * @param longCallSymbol Symbol of the higher strike call (long)
15561
- * @param qty Quantity of iron condors to create (must be a whole number)
15562
- * @param limitPrice Limit price for the iron condor (credit)
15563
- * @returns The created multi-leg order
15564
- */
15565
- async createIronCondor(longPutSymbol, shortPutSymbol, shortCallSymbol, longCallSymbol, qty, limitPrice) {
15566
- this.log(`Creating iron condor with ${qty} contracts at $${limitPrice.toFixed(2)}`, {
15567
- symbol: `${longPutSymbol},${shortPutSymbol},${shortCallSymbol},${longCallSymbol}`,
15568
- });
15569
- const legs = [
15570
- {
15571
- symbol: longPutSymbol,
15572
- ratio_qty: '1',
15573
- side: 'buy',
15574
- position_intent: 'buy_to_open',
15575
- },
15576
- {
15577
- symbol: shortPutSymbol,
15578
- ratio_qty: '1',
15579
- side: 'sell',
15580
- position_intent: 'sell_to_open',
15581
- },
15582
- {
15583
- symbol: shortCallSymbol,
15584
- ratio_qty: '1',
15585
- side: 'sell',
15586
- position_intent: 'sell_to_open',
15587
- },
15588
- {
15589
- symbol: longCallSymbol,
15590
- ratio_qty: '1',
15591
- side: 'buy',
15592
- position_intent: 'buy_to_open',
15593
- },
15594
- ];
15595
- try {
15596
- return await this.createMultiLegOptionOrder(legs, qty, 'limit', limitPrice);
15597
- }
15598
- catch (error) {
15599
- this.log(`Error creating iron condor: ${error}`, { type: 'error' });
15600
- throw error;
15601
- }
15602
- }
15603
- /**
15604
- * Create a covered call (sell call option against owned stock)
15605
- * @param stockSymbol Symbol of the underlying stock
15606
- * @param callOptionSymbol Symbol of the call option to sell
15607
- * @param qty Quantity of covered calls to create (must be a whole number)
15608
- * @param limitPrice Limit price for the call option
15609
- * @returns The created order
15610
- */
15611
- async createCoveredCall(stockSymbol, callOptionSymbol, qty, limitPrice) {
15612
- this.log(`Creating covered call: Sell ${callOptionSymbol} against ${stockSymbol}, Qty: ${qty}, Price: $${limitPrice.toFixed(2)}`, {
15613
- symbol: `${stockSymbol},${callOptionSymbol}`,
15614
- });
15615
- // For covered calls, we don't need to include the stock leg if we already own the shares
15616
- // We just create a simple sell order for the call option
15617
- try {
15618
- return await this.createOptionOrder(callOptionSymbol, qty, 'sell', 'sell_to_open', 'limit', limitPrice);
15619
- }
15620
- catch (error) {
15621
- this.log(`Error creating covered call: ${error}`, { type: 'error' });
15622
- throw error;
15623
- }
15624
- }
15625
- /**
15626
- * Roll an option position to a new expiration or strike
15627
- * @param currentOptionSymbol Symbol of the current option position
15628
- * @param newOptionSymbol Symbol of the new option to roll to
15629
- * @param qty Quantity of options to roll (must be a whole number)
15630
- * @param currentPositionSide Side of the current position ('buy' or 'sell')
15631
- * @param limitPrice Net limit price for the roll
15632
- * @returns The created multi-leg order
15633
- */
15634
- async rollOptionPosition(currentOptionSymbol, newOptionSymbol, qty, currentPositionSide, limitPrice) {
15635
- this.log(`Rolling ${qty} ${currentOptionSymbol} to ${newOptionSymbol} at net price $${limitPrice.toFixed(2)}`, {
15636
- symbol: `${currentOptionSymbol},${newOptionSymbol}`,
15637
- });
15638
- // If current position is long, we need to sell to close and buy to open
15639
- // If current position is short, we need to buy to close and sell to open
15640
- const closePositionSide = currentPositionSide === 'buy' ? 'sell' : 'buy';
15641
- const openPositionSide = currentPositionSide;
15642
- const closePositionIntent = closePositionSide === 'buy' ? 'buy_to_close' : 'sell_to_close';
15643
- const openPositionIntent = openPositionSide === 'buy' ? 'buy_to_open' : 'sell_to_open';
15644
- const legs = [
15645
- {
15646
- symbol: currentOptionSymbol,
15647
- ratio_qty: '1',
15648
- side: closePositionSide,
15649
- position_intent: closePositionIntent,
15650
- },
15651
- {
15652
- symbol: newOptionSymbol,
15653
- ratio_qty: '1',
15654
- side: openPositionSide,
15655
- position_intent: openPositionIntent,
15656
- },
15657
- ];
15658
- try {
15659
- return await this.createMultiLegOptionOrder(legs, qty, 'limit', limitPrice);
15660
- }
15661
- catch (error) {
15662
- this.log(`Error rolling option position: ${error}`, { type: 'error' });
15663
- throw error;
15664
- }
15665
- }
15666
- /**
15667
- * Get option chain for a specific underlying symbol and expiration date
15668
- * @param underlyingSymbol The underlying stock symbol
15669
- * @param expirationDate The expiration date (YYYY-MM-DD format)
15670
- * @returns Option contracts for the specified symbol and expiration date
15671
- */
15672
- async getOptionChain(underlyingSymbol, expirationDate) {
15673
- this.log(`Fetching option chain for ${underlyingSymbol} with expiration date ${expirationDate}`, {
15674
- symbol: underlyingSymbol,
15675
- });
15676
- try {
15677
- const params = {
15678
- underlying_symbols: [underlyingSymbol],
15679
- expiration_date_gte: expirationDate,
15680
- expiration_date_lte: expirationDate,
15681
- status: 'active',
15682
- limit: 500, // Get a large number to ensure we get all strikes
15683
- };
15684
- const response = await this.getOptionContracts(params);
15685
- return response.option_contracts || [];
15686
- }
15687
- catch (error) {
15688
- this.log(`Failed to fetch option chain for ${underlyingSymbol}: ${error instanceof Error ? error.message : 'Unknown error'}`, {
15689
- type: 'error',
15690
- symbol: underlyingSymbol,
15691
- });
15692
- return [];
15693
- }
15694
- }
15695
- /**
15696
- * Get all available expiration dates for a specific underlying symbol
15697
- * @param underlyingSymbol The underlying stock symbol
15698
- * @returns Array of available expiration dates
15699
- */
15700
- async getOptionExpirationDates(underlyingSymbol) {
15701
- this.log(`Fetching available expiration dates for ${underlyingSymbol}`, {
15702
- symbol: underlyingSymbol,
15703
- });
15704
- try {
15705
- const params = {
15706
- underlying_symbols: [underlyingSymbol],
15707
- status: 'active',
15708
- limit: 1000, // Get a large number to ensure we get contracts with all expiration dates
15709
- };
15710
- const response = await this.getOptionContracts(params);
15711
- // Extract unique expiration dates
15712
- const expirationDates = new Set();
15713
- if (response.option_contracts) {
15714
- response.option_contracts.forEach((contract) => {
15715
- expirationDates.add(contract.expiration_date);
15716
- });
15717
- }
15718
- // Convert to array and sort
15719
- return Array.from(expirationDates).sort();
15720
- }
15721
- catch (error) {
15722
- this.log(`Failed to fetch expiration dates for ${underlyingSymbol}: ${error instanceof Error ? error.message : 'Unknown error'}`, {
15723
- type: 'error',
15724
- symbol: underlyingSymbol,
15725
- });
15726
- return [];
15727
- }
15728
- }
15729
- /**
15730
- * Get the current options trading level for the account
15731
- * @returns The options trading level (0-3)
15732
- */
15733
- async getOptionsTradingLevel() {
15734
- this.log('Fetching options trading level');
15735
- const accountDetails = await this.getAccountDetails();
15736
- return accountDetails.options_trading_level || 0;
15737
- }
15738
- /**
15739
- * Check if the account has options trading enabled
15740
- * @returns Boolean indicating if options trading is enabled
15741
- */
15742
- async isOptionsEnabled() {
15743
- this.log('Checking if options trading is enabled');
15744
- const accountDetails = await this.getAccountDetails();
15745
- // Check if options trading level is 2 or higher (Level 2+ allows buying calls/puts)
15746
- // Level 0: Options disabled
15747
- // Level 1: Only covered calls and cash-secured puts
15748
- // Level 2+: Can buy calls and puts (required for executeOptionsOrder)
15749
- const optionsLevel = accountDetails.options_trading_level || 0;
15750
- const isEnabled = optionsLevel >= 2;
15751
- this.log(`Options trading level: ${optionsLevel}, enabled: ${isEnabled}`);
15752
- return isEnabled;
15753
- }
15754
- /**
15755
- * Close all option positions
15756
- * @param cancelOrders Whether to cancel related orders (default: true)
15757
- * @returns Response from the close positions request
15758
- */
15759
- async closeAllOptionPositions(cancelOrders = true) {
15760
- this.log(`Closing all option positions${cancelOrders ? ' and canceling related orders' : ''}`);
15761
- const optionPositions = await this.getOptionPositions();
15762
- if (optionPositions.length === 0) {
15763
- this.log('No option positions to close');
15764
- return;
15765
- }
15766
- // Create market orders to close each position
15767
- for (const position of optionPositions) {
15768
- const side = position.side === 'long' ? 'sell' : 'buy';
15769
- const positionIntent = side === 'sell' ? 'sell_to_close' : 'buy_to_close';
15770
- this.log(`Closing ${position.side} position of ${position.qty} contracts for ${position.symbol}`, {
15771
- symbol: position.symbol,
15772
- });
15773
- await this.createOptionOrder(position.symbol, parseInt(position.qty), side, positionIntent, 'market');
15774
- }
15775
- if (cancelOrders) {
15776
- // Get all open option orders
15777
- const orders = await this.getOrders({ status: 'open' });
15778
- const optionOrders = orders.filter((order) => order.asset_class === 'us_option');
15779
- // Cancel each open option order
15780
- for (const order of optionOrders) {
15781
- this.log(`Canceling open order for ${order.symbol}`, {
15782
- symbol: order.symbol,
15783
- });
15784
- await this.makeRequest(`/orders/${order.id}`, 'DELETE');
15785
- }
15786
- }
15787
- }
15788
- /**
15789
- * Close a specific option position
15790
- * @param symbol The option contract symbol
15791
- * @param qty Optional quantity to close (defaults to entire position)
15792
- * @returns The created order
15793
- */
15794
- async closeOptionPosition(symbol, qty) {
15795
- this.log(`Closing option position for ${symbol}${qty ? ` (${qty} contracts)` : ''}`, {
15796
- symbol,
15797
- });
15798
- // Get the position details
15799
- const positions = await this.getOptionPositions();
15800
- const position = positions.find((p) => p.symbol === symbol);
15801
- if (!position) {
15802
- throw new Error(`No position found for option contract ${symbol}`);
15803
- }
15804
- const quantityToClose = qty || parseInt(position.qty);
15805
- const side = position.side === 'long' ? 'sell' : 'buy';
15806
- const positionIntent = side === 'sell' ? 'sell_to_close' : 'buy_to_close';
15807
- try {
15808
- return await this.createOptionOrder(symbol, quantityToClose, side, positionIntent, 'market');
15809
- }
15810
- catch (error) {
15811
- this.log(`Error closing option position: ${error}`, { type: 'error' });
15812
- throw error;
15813
- }
15814
- }
15815
- /**
15816
- * Create a complete equities trade with optional stop loss and take profit
15817
- * @param params Trade parameters including symbol, qty, side, and optional referencePrice
15818
- * @param options Trade options including order type, extended hours, stop loss, and take profit settings
15819
- * @returns The created order
15820
- */
15821
- async createEquitiesTrade(params, options) {
15822
- const { symbol, qty, side, referencePrice } = params;
15823
- const { type = 'market', limitPrice, extendedHours = false, useStopLoss = false, stopPrice, stopPercent100, useTakeProfit = false, takeProfitPrice, takeProfitPercent100, clientOrderId, } = options || {};
15824
- // Validation: Extended hours + market order is not allowed
15825
- if (extendedHours && type === 'market') {
15826
- this.log('Cannot create market order with extended hours enabled', {
15827
- symbol,
15828
- type: 'error',
15829
- });
15830
- throw new Error('Cannot create market order with extended hours enabled');
15831
- }
15832
- // Validation: Limit orders require limit price
15833
- if (type === 'limit' && limitPrice === undefined) {
15834
- this.log('Limit price is required for limit orders', {
15835
- symbol,
15836
- type: 'error',
15837
- });
15838
- throw new Error('Limit price is required for limit orders');
15839
- }
15840
- let calculatedStopPrice;
15841
- let calculatedTakeProfitPrice;
15842
- // Handle stop loss validation and calculation
15843
- if (useStopLoss) {
15844
- if (stopPrice === undefined && stopPercent100 === undefined) {
15845
- this.log('Either stopPrice or stopPercent100 must be provided when useStopLoss is true', {
15846
- symbol,
15847
- type: 'error',
15848
- });
15849
- throw new Error('Either stopPrice or stopPercent100 must be provided when useStopLoss is true');
15850
- }
15851
- if (stopPercent100 !== undefined) {
15852
- if (referencePrice === undefined) {
15853
- this.log('referencePrice is required when using stopPercent100', {
15854
- symbol,
15855
- type: 'error',
15856
- });
15857
- throw new Error('referencePrice is required when using stopPercent100');
15858
- }
15859
- // Calculate stop price based on percentage and side
15860
- const stopPercentDecimal = stopPercent100 / 100;
15861
- if (side === 'buy') {
15862
- // For buy orders, stop loss is below the reference price
15863
- calculatedStopPrice = referencePrice * (1 - stopPercentDecimal);
15864
- }
15865
- else {
15866
- // For sell orders, stop loss is above the reference price
15867
- calculatedStopPrice = referencePrice * (1 + stopPercentDecimal);
15868
- }
15869
- }
15870
- else {
15871
- calculatedStopPrice = stopPrice;
15872
- }
15873
- }
15874
- // Handle take profit validation and calculation
15875
- if (useTakeProfit) {
15876
- if (takeProfitPrice === undefined && takeProfitPercent100 === undefined) {
15877
- this.log('Either takeProfitPrice or takeProfitPercent100 must be provided when useTakeProfit is true', {
15878
- symbol,
15879
- type: 'error',
15880
- });
15881
- throw new Error('Either takeProfitPrice or takeProfitPercent100 must be provided when useTakeProfit is true');
15882
- }
15883
- if (takeProfitPercent100 !== undefined) {
15884
- if (referencePrice === undefined) {
15885
- this.log('referencePrice is required when using takeProfitPercent100', {
15886
- symbol,
15887
- type: 'error',
15888
- });
15889
- throw new Error('referencePrice is required when using takeProfitPercent100');
15890
- }
15891
- // Calculate take profit price based on percentage and side
15892
- const takeProfitPercentDecimal = takeProfitPercent100 / 100;
15893
- if (side === 'buy') {
15894
- // For buy orders, take profit is above the reference price
15895
- calculatedTakeProfitPrice = referencePrice * (1 + takeProfitPercentDecimal);
15896
- }
15897
- else {
15898
- // For sell orders, take profit is below the reference price
15899
- calculatedTakeProfitPrice = referencePrice * (1 - takeProfitPercentDecimal);
15900
- }
15901
- }
15902
- else {
15903
- calculatedTakeProfitPrice = takeProfitPrice;
15904
- }
15905
- }
15906
- // Determine order class based on what's enabled
15907
- let orderClass = 'simple';
15908
- if (useStopLoss && useTakeProfit) {
15909
- orderClass = 'bracket';
15910
- }
15911
- else if (useStopLoss || useTakeProfit) {
15912
- orderClass = 'oto';
15913
- }
15914
- // Build the order request
15915
- const orderData = {
15916
- symbol,
15917
- qty: Math.abs(qty).toString(),
15918
- side,
15919
- type,
15920
- time_in_force: 'day',
15921
- order_class: orderClass,
15922
- extended_hours: extendedHours,
15923
- position_intent: side === 'buy' ? 'buy_to_open' : 'sell_to_open',
15924
- };
15925
- if (clientOrderId) {
15926
- orderData.client_order_id = clientOrderId;
15927
- }
15928
- // Add limit price for limit orders
15929
- if (type === 'limit' && limitPrice !== undefined) {
15930
- orderData.limit_price = this.roundPriceForAlpaca(limitPrice).toString();
15931
- }
15932
- // Add stop loss if enabled
15933
- if (useStopLoss && calculatedStopPrice !== undefined) {
15934
- orderData.stop_loss = {
15935
- stop_price: this.roundPriceForAlpaca(calculatedStopPrice).toString(),
15936
- };
15937
- }
15938
- // Add take profit if enabled
15939
- if (useTakeProfit && calculatedTakeProfitPrice !== undefined) {
15940
- orderData.take_profit = {
15941
- limit_price: this.roundPriceForAlpaca(calculatedTakeProfitPrice).toString(),
15942
- };
15943
- }
15944
- 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)' : ''}`;
15945
- this.log(logMessage, {
15946
- symbol,
15947
- });
15948
- try {
15949
- return await this.makeRequest('/orders', 'POST', orderData);
15950
- }
15951
- catch (error) {
15952
- this.log(`Error creating equities trade: ${error}`, {
15953
- symbol,
15954
- type: 'error',
15955
- });
15956
- throw error;
15957
- }
15958
- }
15959
- }
14507
+ AlpacaMarketDataAPI.getInstance();
15960
14508
 
15961
14509
  // Test file for context functionality
15962
- async function testMOOAndMOCOrders() {
15963
- console.log('\n--- Testing Market on Open and Market on Close Orders ---');
15964
- console.log('NOTE: MOO orders must be submitted after 7:00pm ET and before 9:28am ET');
15965
- console.log('NOTE: MOC orders must be submitted after 7:00pm ET and before 3:50pm ET');
15966
- console.log('Orders submitted outside these windows will be rejected.\n');
15967
- const log = (message, options = { type: 'info' }) => {
15968
- log$1(message, { ...options, source: 'Test' });
15969
- };
15970
- if (!process.env.ALPACA_TRADING_API_KEY ||
15971
- !process.env.ALPACA_TRADING_SECRET_KEY ||
15972
- !process.env.ALPACA_TRADING_ACCOUNT_TYPE) {
15973
- log('Missing required ALPACA_TRADING_* environment variables', { type: 'error' });
15974
- return;
15975
- }
15976
- const credentials = {
15977
- accountName: 'Test Account',
15978
- apiKey: process.env.ALPACA_TRADING_API_KEY,
15979
- apiSecret: process.env.ALPACA_TRADING_SECRET_KEY,
15980
- type: process.env.ALPACA_TRADING_ACCOUNT_TYPE,
15981
- orderType: 'limit',
15982
- engine: 'quant',
15983
- };
15984
- const tradingAPI = AlpacaTradingAPI.getInstance(credentials);
15985
- try {
15986
- // Test creating a Market on Open order
15987
- log('Creating Market on Open (MOO) order for SPY...');
15988
- const mooOrder = await tradingAPI.createMOOOrder('SPY', 1, 'buy', 'buy_to_open', 'test-moo-order');
15989
- log(`MOO order created successfully: ${mooOrder.id}`);
15990
- log(` Symbol: ${mooOrder.symbol}`);
15991
- log(` Qty: ${mooOrder.qty}`);
15992
- log(` Side: ${mooOrder.side}`);
15993
- log(` Type: ${mooOrder.type}`);
15994
- log(` Time in Force: ${mooOrder.time_in_force}`);
15995
- log(` Status: ${mooOrder.status}`);
15996
- // Wait a moment before canceling
15997
- await new Promise((resolve) => setTimeout(resolve, 1000));
15998
- // Cancel the MOO order
15999
- log(`Canceling MOO order ${mooOrder.id}...`);
16000
- await tradingAPI.cancelOrder(mooOrder.id);
16001
- log(`MOO order canceled successfully`);
16002
- // Wait a moment before next order
16003
- await new Promise((resolve) => setTimeout(resolve, 1000));
16004
- // Test creating a Market on Close order
16005
- log('Creating Market on Close (MOC) order for SPY...');
16006
- const mocOrder = await tradingAPI.createMOCOrder('SPY', 1, 'sell', 'sell_to_open', 'test-moc-order');
16007
- log(`MOC order created successfully: ${mocOrder.id}`);
16008
- log(` Symbol: ${mocOrder.symbol}`);
16009
- log(` Qty: ${mocOrder.qty}`);
16010
- log(` Side: ${mocOrder.side}`);
16011
- log(` Type: ${mocOrder.type}`);
16012
- log(` Time in Force: ${mocOrder.time_in_force}`);
16013
- log(` Status: ${mocOrder.status}`);
16014
- // Wait a moment before canceling
16015
- await new Promise((resolve) => setTimeout(resolve, 1000));
16016
- // Cancel the MOC order
16017
- log(`Canceling MOC order ${mocOrder.id}...`);
16018
- await tradingAPI.cancelOrder(mocOrder.id);
16019
- log(`MOC order canceled successfully`);
16020
- log('\nMOO/MOC order test completed successfully');
16021
- }
16022
- catch (error) {
16023
- log(`Error during MOO/MOC order test: ${error instanceof Error ? error.message : 'Unknown error'}`, {
16024
- type: 'error',
16025
- });
16026
- throw error;
16027
- }
16028
- }
16029
14510
  // testGetTradingDate();
16030
14511
  // testGetTradingStartAndEndDates();
16031
14512
  // testGetLastFullTradingDate();
@@ -16037,12 +14518,68 @@ async function testMOOAndMOCOrders() {
16037
14518
  // testOpenRouter();
16038
14519
  // testGetMarketStatus();
16039
14520
  // testCryptoMarketData();
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
+ }
16040
14573
  // testGetPortfolioDailyHistory();
16041
14574
  // testWebSocketConnectAndDisconnect();
16042
14575
  // testGetAssetsShortableFilter();
16043
- // testMarketDataAPI();
16044
14576
  // testLLM();
16045
14577
  // testImageModelDefaults();
16046
14578
  // testGetTradingDaysBack();
16047
- testMOOAndMOCOrders();
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');
16048
14585
  //# sourceMappingURL=test.js.map