@discomedia/utils 1.0.56 → 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/index.cjs +95 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +95 -0
- package/dist/index.mjs.map +1 -1
- package/dist/package.json +1 -1
- package/dist/test.js +1768 -801
- package/dist/test.js.map +1 -1
- package/dist/types/alpaca-trading-api.d.ts +26 -0
- package/dist/types/alpaca-trading-api.d.ts.map +1 -1
- package/dist/types-frontend/alpaca-trading-api.d.ts +26 -0
- package/dist/types-frontend/alpaca-trading-api.d.ts.map +1 -1
- package/package.json +1 -1
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 'console';
|
|
15
14
|
|
|
16
15
|
/**
|
|
17
16
|
* Logs a message to the console.
|
|
@@ -147,18 +146,12 @@ const marketEarlyCloses = {
|
|
|
147
146
|
|
|
148
147
|
// Constants for NY market times (Eastern Time)
|
|
149
148
|
const MARKET_CONFIG = {
|
|
150
|
-
TIMEZONE: 'America/New_York',
|
|
151
149
|
UTC_OFFSET_STANDARD: -5, // EST
|
|
152
150
|
UTC_OFFSET_DST: -4, // EDT
|
|
153
151
|
TIMES: {
|
|
154
|
-
EXTENDED_START: { hour: 4, minute: 0 },
|
|
155
152
|
MARKET_OPEN: { hour: 9, minute: 30 },
|
|
156
|
-
EARLY_MARKET_END: { hour: 10, minute: 0 },
|
|
157
153
|
MARKET_CLOSE: { hour: 16, minute: 0 },
|
|
158
|
-
EARLY_CLOSE: { hour: 13, minute: 0 },
|
|
159
|
-
EXTENDED_END: { hour: 20, minute: 0 },
|
|
160
|
-
EARLY_EXTENDED_END: { hour: 17, minute: 0 },
|
|
161
|
-
},
|
|
154
|
+
EARLY_CLOSE: { hour: 13, minute: 0 }},
|
|
162
155
|
};
|
|
163
156
|
// Helper: Get NY offset for a given UTC date (DST rules for US)
|
|
164
157
|
/**
|
|
@@ -226,33 +219,6 @@ function fromNYTime(date) {
|
|
|
226
219
|
const utcMillis = nyMillis - offset * 60 * 60 * 1000;
|
|
227
220
|
return new Date(utcMillis);
|
|
228
221
|
}
|
|
229
|
-
// Helper: Format date in ISO, unix, etc.
|
|
230
|
-
/**
|
|
231
|
-
* Formats a date in ISO, unix-seconds, or unix-ms format.
|
|
232
|
-
* @param date - Date object
|
|
233
|
-
* @param outputFormat - Output format ('iso', 'unix-seconds', 'unix-ms')
|
|
234
|
-
* @returns Formatted date string or number
|
|
235
|
-
*/
|
|
236
|
-
function formatDate(date, outputFormat = 'iso') {
|
|
237
|
-
switch (outputFormat) {
|
|
238
|
-
case 'unix-seconds':
|
|
239
|
-
return Math.floor(date.getTime() / 1000);
|
|
240
|
-
case 'unix-ms':
|
|
241
|
-
return date.getTime();
|
|
242
|
-
case 'iso':
|
|
243
|
-
default:
|
|
244
|
-
return date.toISOString();
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
// Helper: Format date in NY locale string
|
|
248
|
-
/**
|
|
249
|
-
* Formats a date in NY locale string.
|
|
250
|
-
* @param date - Date object
|
|
251
|
-
* @returns NY locale string
|
|
252
|
-
*/
|
|
253
|
-
function formatNYLocale(date) {
|
|
254
|
-
return date.toLocaleString('en-US', { timeZone: 'America/New_York' });
|
|
255
|
-
}
|
|
256
222
|
// Market calendar logic
|
|
257
223
|
/**
|
|
258
224
|
* Market calendar logic for holidays, weekends, and market days.
|
|
@@ -348,82 +314,6 @@ class MarketCalendar {
|
|
|
348
314
|
return prevDay;
|
|
349
315
|
}
|
|
350
316
|
}
|
|
351
|
-
// Market open/close times
|
|
352
|
-
/**
|
|
353
|
-
* Returns market open/close times for a given date, including extended and early closes.
|
|
354
|
-
* @param date - Date object
|
|
355
|
-
* @returns MarketOpenCloseResult
|
|
356
|
-
*/
|
|
357
|
-
function getMarketTimes(date) {
|
|
358
|
-
const calendar = new MarketCalendar();
|
|
359
|
-
const nyDate = toNYTime(date);
|
|
360
|
-
if (!calendar.isMarketDay(date)) {
|
|
361
|
-
return {
|
|
362
|
-
marketOpen: false,
|
|
363
|
-
open: null,
|
|
364
|
-
close: null,
|
|
365
|
-
openExt: null,
|
|
366
|
-
closeExt: null,
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
const year = nyDate.getUTCFullYear();
|
|
370
|
-
const month = nyDate.getUTCMonth();
|
|
371
|
-
const day = nyDate.getUTCDate();
|
|
372
|
-
// Helper to build NY time for a given hour/minute
|
|
373
|
-
function buildNYTime(hour, minute) {
|
|
374
|
-
const d = new Date(Date.UTC(year, month, day, hour, minute, 0, 0));
|
|
375
|
-
return fromNYTime(d);
|
|
376
|
-
}
|
|
377
|
-
let open = buildNYTime(MARKET_CONFIG.TIMES.MARKET_OPEN.hour, MARKET_CONFIG.TIMES.MARKET_OPEN.minute);
|
|
378
|
-
let close = buildNYTime(MARKET_CONFIG.TIMES.MARKET_CLOSE.hour, MARKET_CONFIG.TIMES.MARKET_CLOSE.minute);
|
|
379
|
-
let openExt = buildNYTime(MARKET_CONFIG.TIMES.EXTENDED_START.hour, MARKET_CONFIG.TIMES.EXTENDED_START.minute);
|
|
380
|
-
let closeExt = buildNYTime(MARKET_CONFIG.TIMES.EXTENDED_END.hour, MARKET_CONFIG.TIMES.EXTENDED_END.minute);
|
|
381
|
-
if (calendar.isEarlyCloseDay(date)) {
|
|
382
|
-
close = buildNYTime(MARKET_CONFIG.TIMES.EARLY_CLOSE.hour, MARKET_CONFIG.TIMES.EARLY_CLOSE.minute);
|
|
383
|
-
closeExt = buildNYTime(MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour, MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute);
|
|
384
|
-
}
|
|
385
|
-
return {
|
|
386
|
-
marketOpen: true,
|
|
387
|
-
open,
|
|
388
|
-
close,
|
|
389
|
-
openExt,
|
|
390
|
-
closeExt,
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
// Is within market hours
|
|
394
|
-
/**
|
|
395
|
-
* Checks if a date/time is within market hours, extended hours, or continuous.
|
|
396
|
-
* @param date - Date object
|
|
397
|
-
* @param intradayReporting - 'market_hours', 'extended_hours', or 'continuous'
|
|
398
|
-
* @returns true if within hours, false otherwise
|
|
399
|
-
*/
|
|
400
|
-
function isWithinMarketHours(date, intradayReporting = 'market_hours') {
|
|
401
|
-
const calendar = new MarketCalendar();
|
|
402
|
-
if (!calendar.isMarketDay(date))
|
|
403
|
-
return false;
|
|
404
|
-
const nyDate = toNYTime(date);
|
|
405
|
-
const minutes = nyDate.getUTCHours() * 60 + nyDate.getUTCMinutes();
|
|
406
|
-
switch (intradayReporting) {
|
|
407
|
-
case 'extended_hours': {
|
|
408
|
-
let endMinutes = MARKET_CONFIG.TIMES.EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_END.minute;
|
|
409
|
-
if (calendar.isEarlyCloseDay(date)) {
|
|
410
|
-
endMinutes = MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute;
|
|
411
|
-
}
|
|
412
|
-
const startMinutes = MARKET_CONFIG.TIMES.EXTENDED_START.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_START.minute;
|
|
413
|
-
return minutes >= startMinutes && minutes <= endMinutes;
|
|
414
|
-
}
|
|
415
|
-
case 'continuous':
|
|
416
|
-
return true;
|
|
417
|
-
default: {
|
|
418
|
-
let endMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
|
|
419
|
-
if (calendar.isEarlyCloseDay(date)) {
|
|
420
|
-
endMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
|
|
421
|
-
}
|
|
422
|
-
const startMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
|
|
423
|
-
return minutes >= startMinutes && minutes <= endMinutes;
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
317
|
// Get last full trading date
|
|
428
318
|
/**
|
|
429
319
|
* Returns the last full trading date (market close) for a given date.
|
|
@@ -464,244 +354,6 @@ function getLastFullTradingDateImpl(currentDate = new Date()) {
|
|
|
464
354
|
const closeMinute = marketCloseMinutes % 60;
|
|
465
355
|
return fromNYTime(new Date(Date.UTC(year, month, day, closeHour, closeMinute, 0, 0)));
|
|
466
356
|
}
|
|
467
|
-
// Get day boundaries
|
|
468
|
-
/**
|
|
469
|
-
* Returns the start and end boundaries for a market day, extended hours, or continuous.
|
|
470
|
-
* @param date - Date object
|
|
471
|
-
* @param intradayReporting - 'market_hours', 'extended_hours', or 'continuous'
|
|
472
|
-
* @returns Object with start and end Date
|
|
473
|
-
*/
|
|
474
|
-
function getDayBoundaries(date, intradayReporting = 'market_hours') {
|
|
475
|
-
const calendar = new MarketCalendar();
|
|
476
|
-
const nyDate = toNYTime(date);
|
|
477
|
-
const year = nyDate.getUTCFullYear();
|
|
478
|
-
const month = nyDate.getUTCMonth();
|
|
479
|
-
const day = nyDate.getUTCDate();
|
|
480
|
-
function buildNYTime(hour, minute, sec = 0, ms = 0) {
|
|
481
|
-
const d = new Date(Date.UTC(year, month, day, hour, minute, sec, ms));
|
|
482
|
-
return fromNYTime(d);
|
|
483
|
-
}
|
|
484
|
-
let start;
|
|
485
|
-
let end;
|
|
486
|
-
switch (intradayReporting) {
|
|
487
|
-
case 'extended_hours':
|
|
488
|
-
start = buildNYTime(MARKET_CONFIG.TIMES.EXTENDED_START.hour, MARKET_CONFIG.TIMES.EXTENDED_START.minute, 0, 0);
|
|
489
|
-
end = buildNYTime(MARKET_CONFIG.TIMES.EXTENDED_END.hour, MARKET_CONFIG.TIMES.EXTENDED_END.minute, 59, 999);
|
|
490
|
-
if (calendar.isEarlyCloseDay(date)) {
|
|
491
|
-
end = buildNYTime(MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour, MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute, 59, 999);
|
|
492
|
-
}
|
|
493
|
-
break;
|
|
494
|
-
case 'continuous':
|
|
495
|
-
start = new Date(Date.UTC(year, month, day, 0, 0, 0, 0));
|
|
496
|
-
end = new Date(Date.UTC(year, month, day, 23, 59, 59, 999));
|
|
497
|
-
break;
|
|
498
|
-
default:
|
|
499
|
-
start = buildNYTime(MARKET_CONFIG.TIMES.MARKET_OPEN.hour, MARKET_CONFIG.TIMES.MARKET_OPEN.minute, 0, 0);
|
|
500
|
-
end = buildNYTime(MARKET_CONFIG.TIMES.MARKET_CLOSE.hour, MARKET_CONFIG.TIMES.MARKET_CLOSE.minute, 59, 999);
|
|
501
|
-
if (calendar.isEarlyCloseDay(date)) {
|
|
502
|
-
end = buildNYTime(MARKET_CONFIG.TIMES.EARLY_CLOSE.hour, MARKET_CONFIG.TIMES.EARLY_CLOSE.minute, 59, 999);
|
|
503
|
-
}
|
|
504
|
-
break;
|
|
505
|
-
}
|
|
506
|
-
return { start, end };
|
|
507
|
-
}
|
|
508
|
-
// Period calculator
|
|
509
|
-
/**
|
|
510
|
-
* Calculates the start date for a given period ending at endDate.
|
|
511
|
-
* @param endDate - Date object
|
|
512
|
-
* @param period - Period string
|
|
513
|
-
* @returns Date object for period start
|
|
514
|
-
*/
|
|
515
|
-
function calculatePeriodStartDate(endDate, period) {
|
|
516
|
-
const calendar = new MarketCalendar();
|
|
517
|
-
let startDate;
|
|
518
|
-
switch (period) {
|
|
519
|
-
case 'YTD':
|
|
520
|
-
startDate = new Date(Date.UTC(endDate.getUTCFullYear(), 0, 1));
|
|
521
|
-
break;
|
|
522
|
-
case '1D':
|
|
523
|
-
startDate = calendar.getPreviousMarketDay(endDate);
|
|
524
|
-
break;
|
|
525
|
-
case '3D':
|
|
526
|
-
startDate = new Date(endDate.getTime() - 3 * 24 * 60 * 60 * 1000);
|
|
527
|
-
break;
|
|
528
|
-
case '1W':
|
|
529
|
-
startDate = new Date(endDate.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
530
|
-
break;
|
|
531
|
-
case '2W':
|
|
532
|
-
startDate = new Date(endDate.getTime() - 14 * 24 * 60 * 60 * 1000);
|
|
533
|
-
break;
|
|
534
|
-
case '1M':
|
|
535
|
-
startDate = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth() - 1, endDate.getUTCDate()));
|
|
536
|
-
break;
|
|
537
|
-
case '3M':
|
|
538
|
-
startDate = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth() - 3, endDate.getUTCDate()));
|
|
539
|
-
break;
|
|
540
|
-
case '6M':
|
|
541
|
-
startDate = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth() - 6, endDate.getUTCDate()));
|
|
542
|
-
break;
|
|
543
|
-
case '1Y':
|
|
544
|
-
startDate = new Date(Date.UTC(endDate.getUTCFullYear() - 1, endDate.getUTCMonth(), endDate.getUTCDate()));
|
|
545
|
-
break;
|
|
546
|
-
default:
|
|
547
|
-
throw new Error(`Invalid period: ${period}`);
|
|
548
|
-
}
|
|
549
|
-
// Ensure start date is a market day
|
|
550
|
-
while (!calendar.isMarketDay(startDate)) {
|
|
551
|
-
startDate = calendar.getNextMarketDay(startDate);
|
|
552
|
-
}
|
|
553
|
-
return startDate;
|
|
554
|
-
}
|
|
555
|
-
// Get market time period
|
|
556
|
-
/**
|
|
557
|
-
* Returns the start and end dates for a market time period.
|
|
558
|
-
* @param params - MarketTimeParams
|
|
559
|
-
* @returns PeriodDates object
|
|
560
|
-
*/
|
|
561
|
-
function getMarketTimePeriod(params) {
|
|
562
|
-
const { period, end = new Date(), intraday_reporting = 'market_hours', outputFormat = 'iso' } = params;
|
|
563
|
-
if (!period)
|
|
564
|
-
throw new Error('Period is required');
|
|
565
|
-
const calendar = new MarketCalendar();
|
|
566
|
-
const nyEndDate = toNYTime(end);
|
|
567
|
-
let endDate;
|
|
568
|
-
const isCurrentMarketDay = calendar.isMarketDay(end);
|
|
569
|
-
const isWithinHours = isWithinMarketHours(end, intraday_reporting);
|
|
570
|
-
const minutes = nyEndDate.getUTCHours() * 60 + nyEndDate.getUTCMinutes();
|
|
571
|
-
const marketStartMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
|
|
572
|
-
if (isCurrentMarketDay) {
|
|
573
|
-
if (minutes < marketStartMinutes) {
|
|
574
|
-
// Before market open - use previous day's close
|
|
575
|
-
const lastMarketDay = calendar.getPreviousMarketDay(end);
|
|
576
|
-
const { end: dayEnd } = getDayBoundaries(lastMarketDay, intraday_reporting);
|
|
577
|
-
endDate = dayEnd;
|
|
578
|
-
}
|
|
579
|
-
else if (isWithinHours) {
|
|
580
|
-
// During market hours - use current time
|
|
581
|
-
endDate = end;
|
|
582
|
-
}
|
|
583
|
-
else {
|
|
584
|
-
// After market close - use today's close
|
|
585
|
-
const { end: dayEnd } = getDayBoundaries(end, intraday_reporting);
|
|
586
|
-
endDate = dayEnd;
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
else {
|
|
590
|
-
// Not a market day - use previous market day's close
|
|
591
|
-
const lastMarketDay = calendar.getPreviousMarketDay(end);
|
|
592
|
-
const { end: dayEnd } = getDayBoundaries(lastMarketDay, intraday_reporting);
|
|
593
|
-
endDate = dayEnd;
|
|
594
|
-
}
|
|
595
|
-
// Calculate start date
|
|
596
|
-
const periodStartDate = calculatePeriodStartDate(endDate, period);
|
|
597
|
-
const { start: dayStart } = getDayBoundaries(periodStartDate, intraday_reporting);
|
|
598
|
-
if (endDate.getTime() < dayStart.getTime()) {
|
|
599
|
-
throw new Error('Start date cannot be after end date');
|
|
600
|
-
}
|
|
601
|
-
return {
|
|
602
|
-
start: formatDate(dayStart, outputFormat),
|
|
603
|
-
end: formatDate(endDate, outputFormat),
|
|
604
|
-
};
|
|
605
|
-
}
|
|
606
|
-
// Market status
|
|
607
|
-
/**
|
|
608
|
-
* Returns the current market status for a given date.
|
|
609
|
-
* @param date - Date object (default: now)
|
|
610
|
-
* @returns MarketStatus object
|
|
611
|
-
*/
|
|
612
|
-
function getMarketStatusImpl(date = new Date()) {
|
|
613
|
-
const calendar = new MarketCalendar();
|
|
614
|
-
const nyDate = toNYTime(date);
|
|
615
|
-
const minutes = nyDate.getUTCHours() * 60 + nyDate.getUTCMinutes();
|
|
616
|
-
const isMarketDay = calendar.isMarketDay(date);
|
|
617
|
-
const isEarlyCloseDay = calendar.isEarlyCloseDay(date);
|
|
618
|
-
const extendedStartMinutes = MARKET_CONFIG.TIMES.EXTENDED_START.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_START.minute;
|
|
619
|
-
const marketStartMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
|
|
620
|
-
const earlyMarketEndMinutes = MARKET_CONFIG.TIMES.EARLY_MARKET_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_MARKET_END.minute;
|
|
621
|
-
let marketCloseMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
|
|
622
|
-
let extendedEndMinutes = MARKET_CONFIG.TIMES.EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_END.minute;
|
|
623
|
-
if (isEarlyCloseDay) {
|
|
624
|
-
marketCloseMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
|
|
625
|
-
extendedEndMinutes =
|
|
626
|
-
MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute;
|
|
627
|
-
}
|
|
628
|
-
let status;
|
|
629
|
-
let nextStatus;
|
|
630
|
-
let nextStatusTime;
|
|
631
|
-
let marketPeriod;
|
|
632
|
-
if (!isMarketDay) {
|
|
633
|
-
status = 'closed';
|
|
634
|
-
nextStatus = 'extended hours';
|
|
635
|
-
marketPeriod = 'closed';
|
|
636
|
-
const nextMarketDay = calendar.getNextMarketDay(date);
|
|
637
|
-
nextStatusTime = getDayBoundaries(nextMarketDay, 'extended_hours').start;
|
|
638
|
-
}
|
|
639
|
-
else if (minutes < extendedStartMinutes) {
|
|
640
|
-
status = 'closed';
|
|
641
|
-
nextStatus = 'extended hours';
|
|
642
|
-
marketPeriod = 'closed';
|
|
643
|
-
nextStatusTime = getDayBoundaries(date, 'extended_hours').start;
|
|
644
|
-
}
|
|
645
|
-
else if (minutes < marketStartMinutes) {
|
|
646
|
-
status = 'extended hours';
|
|
647
|
-
nextStatus = 'open';
|
|
648
|
-
marketPeriod = 'preMarket';
|
|
649
|
-
nextStatusTime = getDayBoundaries(date, 'market_hours').start;
|
|
650
|
-
}
|
|
651
|
-
else if (minutes < marketCloseMinutes) {
|
|
652
|
-
status = 'open';
|
|
653
|
-
nextStatus = 'extended hours';
|
|
654
|
-
marketPeriod = minutes < earlyMarketEndMinutes ? 'earlyMarket' : 'regularMarket';
|
|
655
|
-
nextStatusTime = getDayBoundaries(date, 'market_hours').end;
|
|
656
|
-
}
|
|
657
|
-
else if (minutes < extendedEndMinutes) {
|
|
658
|
-
status = 'extended hours';
|
|
659
|
-
nextStatus = 'closed';
|
|
660
|
-
marketPeriod = 'afterMarket';
|
|
661
|
-
nextStatusTime = getDayBoundaries(date, 'extended_hours').end;
|
|
662
|
-
}
|
|
663
|
-
else {
|
|
664
|
-
status = 'closed';
|
|
665
|
-
nextStatus = 'extended hours';
|
|
666
|
-
marketPeriod = 'closed';
|
|
667
|
-
const nextMarketDay = calendar.getNextMarketDay(date);
|
|
668
|
-
nextStatusTime = getDayBoundaries(nextMarketDay, 'extended_hours').start;
|
|
669
|
-
}
|
|
670
|
-
// I think using nyDate here may be wrong - should use current time? i.e. date.getTime()
|
|
671
|
-
const nextStatusTimeDifference = nextStatusTime.getTime() - date.getTime();
|
|
672
|
-
return {
|
|
673
|
-
time: date,
|
|
674
|
-
timeString: formatNYLocale(nyDate),
|
|
675
|
-
status,
|
|
676
|
-
nextStatus,
|
|
677
|
-
marketPeriod,
|
|
678
|
-
nextStatusTime,
|
|
679
|
-
nextStatusTimeDifference,
|
|
680
|
-
nextStatusTimeString: formatNYLocale(nextStatusTime),
|
|
681
|
-
};
|
|
682
|
-
}
|
|
683
|
-
// API exports
|
|
684
|
-
/**
|
|
685
|
-
* Returns market open/close times for a given date.
|
|
686
|
-
* @param options - { date?: Date }
|
|
687
|
-
* @returns MarketOpenCloseResult
|
|
688
|
-
*/
|
|
689
|
-
function getMarketOpenClose(options = {}) {
|
|
690
|
-
const { date = new Date() } = options;
|
|
691
|
-
return getMarketTimes(date);
|
|
692
|
-
}
|
|
693
|
-
/**
|
|
694
|
-
* Returns the start and end dates for a market time period as Date objects.
|
|
695
|
-
* @param params - MarketTimeParams
|
|
696
|
-
* @returns Object with start and end Date
|
|
697
|
-
*/
|
|
698
|
-
function getStartAndEndDates(params = {}) {
|
|
699
|
-
const { start, end } = getMarketTimePeriod(params);
|
|
700
|
-
return {
|
|
701
|
-
start: typeof start === 'string' || typeof start === 'number' ? new Date(start) : start,
|
|
702
|
-
end: typeof end === 'string' || typeof end === 'number' ? new Date(end) : end,
|
|
703
|
-
};
|
|
704
|
-
}
|
|
705
357
|
/**
|
|
706
358
|
* Returns the last full trading date as a Date object.
|
|
707
359
|
*/
|
|
@@ -713,43 +365,6 @@ function getStartAndEndDates(params = {}) {
|
|
|
713
365
|
function getLastFullTradingDate(currentDate = new Date()) {
|
|
714
366
|
return getLastFullTradingDateImpl(currentDate);
|
|
715
367
|
}
|
|
716
|
-
/**
|
|
717
|
-
* Returns the next market day after the reference date.
|
|
718
|
-
* @param referenceDate - Date object (default: now)
|
|
719
|
-
* @returns Object with date, yyyymmdd string, and ISO string
|
|
720
|
-
*/
|
|
721
|
-
function getNextMarketDay({ referenceDate } = {}) {
|
|
722
|
-
const calendar = new MarketCalendar();
|
|
723
|
-
const startDate = referenceDate ?? new Date();
|
|
724
|
-
// Find the next trading day (UTC Date object)
|
|
725
|
-
const nextDate = calendar.getNextMarketDay(startDate);
|
|
726
|
-
// Convert to NY time before extracting Y-M-D parts
|
|
727
|
-
const nyNext = toNYTime(nextDate);
|
|
728
|
-
const yyyymmdd = `${nyNext.getUTCFullYear()}-${String(nyNext.getUTCMonth() + 1).padStart(2, '0')}-${String(nyNext.getUTCDate()).padStart(2, '0')}`;
|
|
729
|
-
return {
|
|
730
|
-
date: nextDate, // raw Date, unchanged
|
|
731
|
-
yyyymmdd, // correct trading date string
|
|
732
|
-
dateISOString: nextDate.toISOString(),
|
|
733
|
-
};
|
|
734
|
-
}
|
|
735
|
-
/**
|
|
736
|
-
* Returns the previous market day before the reference date.
|
|
737
|
-
* @param referenceDate - Date object (default: now)
|
|
738
|
-
* @returns Object with date, yyyymmdd string, and ISO string
|
|
739
|
-
*/
|
|
740
|
-
function getPreviousMarketDay({ referenceDate } = {}) {
|
|
741
|
-
const calendar = new MarketCalendar();
|
|
742
|
-
const startDate = referenceDate || new Date();
|
|
743
|
-
const prevDate = calendar.getPreviousMarketDay(startDate);
|
|
744
|
-
// convert to NY time first
|
|
745
|
-
const nyPrev = toNYTime(prevDate); // ← already in this file
|
|
746
|
-
const yyyymmdd = `${nyPrev.getUTCFullYear()}-${String(nyPrev.getUTCMonth() + 1).padStart(2, '0')}-${String(nyPrev.getUTCDate()).padStart(2, '0')}`;
|
|
747
|
-
return {
|
|
748
|
-
date: prevDate,
|
|
749
|
-
yyyymmdd,
|
|
750
|
-
dateISOString: prevDate.toISOString(),
|
|
751
|
-
};
|
|
752
|
-
}
|
|
753
368
|
/**
|
|
754
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.
|
|
755
370
|
* @param time - a string, number (unix timestamp), or Date object representing the time
|
|
@@ -765,296 +380,6 @@ function getTradingDate(time) {
|
|
|
765
380
|
const nyDate = toNYTime(date);
|
|
766
381
|
return `${nyDate.getUTCFullYear()}-${String(nyDate.getUTCMonth() + 1).padStart(2, '0')}-${String(nyDate.getUTCDate()).padStart(2, '0')}`;
|
|
767
382
|
}
|
|
768
|
-
/**
|
|
769
|
-
* Returns the NY timezone offset string for a given date.
|
|
770
|
-
* @param date - Date object (default: now)
|
|
771
|
-
* @returns '-04:00' for EDT, '-05:00' for EST
|
|
772
|
-
*/
|
|
773
|
-
function getNYTimeZone(date) {
|
|
774
|
-
const offset = getNYOffset(date || new Date());
|
|
775
|
-
return offset === -4 ? '-04:00' : '-05:00';
|
|
776
|
-
}
|
|
777
|
-
/**
|
|
778
|
-
* Returns the regular market open and close Date objects for a given trading day string in the
|
|
779
|
-
* America/New_York timezone (NYSE/NASDAQ calendar).
|
|
780
|
-
*
|
|
781
|
-
* This helper is convenient when you have a calendar date like '2025-10-03' and want the precise
|
|
782
|
-
* open and close Date values for that day. It internally:
|
|
783
|
-
* - Determines the NY offset for the day using `getNYTimeZone()`.
|
|
784
|
-
* - Anchors a noon-time Date on that day in NY time to avoid DST edge cases.
|
|
785
|
-
* - Verifies the day is a market day via `isMarketDay()`.
|
|
786
|
-
* - Fetches the open/close times via `getMarketOpenClose()`.
|
|
787
|
-
*
|
|
788
|
-
* Throws if the provided day is not a market day or if open/close times are unavailable.
|
|
789
|
-
*
|
|
790
|
-
* See also:
|
|
791
|
-
* - `getNYTimeZone(date?: Date)`
|
|
792
|
-
* - `isMarketDay(date: Date)`
|
|
793
|
-
* - `getMarketOpenClose(options?: { date?: Date })`
|
|
794
|
-
*
|
|
795
|
-
* @param dateStr - Trading day string in 'YYYY-MM-DD' format (Eastern Time date)
|
|
796
|
-
* @returns An object containing `{ open: Date; close: Date }`
|
|
797
|
-
* @example
|
|
798
|
-
* ```ts
|
|
799
|
-
* const { open, close } = disco.time.getOpenCloseForTradingDay('2025-10-03');
|
|
800
|
-
* ```
|
|
801
|
-
*/
|
|
802
|
-
function getOpenCloseForTradingDay(dateStr) {
|
|
803
|
-
// Build a UTC midnight anchor for the date, then derive the NY offset for that day.
|
|
804
|
-
const utcAnchor = new Date(`${dateStr}T00:00:00Z`);
|
|
805
|
-
const nyOffset = getNYTimeZone(utcAnchor); // '-04:00' | '-05:00'
|
|
806
|
-
// Create a NY-local noon date to avoid DST midnight transitions.
|
|
807
|
-
const nyNoon = new Date(`${dateStr}T12:00:00${nyOffset}`);
|
|
808
|
-
if (!isMarketDay(nyNoon)) {
|
|
809
|
-
throw new Error(`Not a market day in ET: ${dateStr}`);
|
|
810
|
-
}
|
|
811
|
-
const { open, close } = getMarketOpenClose({ date: nyNoon });
|
|
812
|
-
if (!open || !close) {
|
|
813
|
-
throw new Error(`No market times available for ${dateStr}`);
|
|
814
|
-
}
|
|
815
|
-
return { open, close };
|
|
816
|
-
}
|
|
817
|
-
/**
|
|
818
|
-
* Converts any date to the market time zone (America/New_York, Eastern Time).
|
|
819
|
-
* Returns a new Date object representing the same moment in time but adjusted to NY/Eastern timezone.
|
|
820
|
-
* Automatically handles daylight saving time transitions (EST/EDT).
|
|
821
|
-
*
|
|
822
|
-
* @param date - Date object to convert to market time zone
|
|
823
|
-
* @returns Date object in NY/Eastern time zone
|
|
824
|
-
* @example
|
|
825
|
-
* ```typescript
|
|
826
|
-
* const utcDate = new Date('2024-01-15T15:30:00Z'); // 3:30 PM UTC
|
|
827
|
-
* const nyDate = convertDateToMarketTimeZone(utcDate); // 10:30 AM EST (winter) or 11:30 AM EDT (summer)
|
|
828
|
-
* ```
|
|
829
|
-
*/
|
|
830
|
-
function convertDateToMarketTimeZone(date) {
|
|
831
|
-
return toNYTime(date);
|
|
832
|
-
}
|
|
833
|
-
/**
|
|
834
|
-
* Returns the current market status for a given date.
|
|
835
|
-
* @param options - { date?: Date }
|
|
836
|
-
* @returns MarketStatus object
|
|
837
|
-
*/
|
|
838
|
-
function getMarketStatus(options = {}) {
|
|
839
|
-
const { date = new Date() } = options;
|
|
840
|
-
return getMarketStatusImpl(date);
|
|
841
|
-
}
|
|
842
|
-
/**
|
|
843
|
-
* Checks if a date is a market day.
|
|
844
|
-
* @param date - Date object
|
|
845
|
-
* @returns true if market day, false otherwise
|
|
846
|
-
*/
|
|
847
|
-
function isMarketDay(date) {
|
|
848
|
-
const calendar = new MarketCalendar();
|
|
849
|
-
return calendar.isMarketDay(date);
|
|
850
|
-
}
|
|
851
|
-
/**
|
|
852
|
-
* Returns full trading days from market open to market close.
|
|
853
|
-
* endDate is always the most recent market close (previous day's close if before open, today's close if after open).
|
|
854
|
-
* days: 1 or not specified = that day's open; 2 = previous market day's open, etc.
|
|
855
|
-
*/
|
|
856
|
-
/**
|
|
857
|
-
* Returns full trading days from market open to market close.
|
|
858
|
-
* @param options - { endDate?: Date, days?: number }
|
|
859
|
-
* @returns Object with startDate and endDate
|
|
860
|
-
*/
|
|
861
|
-
function getTradingStartAndEndDates(options = {}) {
|
|
862
|
-
const { endDate = new Date(), days = 1 } = options;
|
|
863
|
-
const calendar = new MarketCalendar();
|
|
864
|
-
// Find the most recent market close
|
|
865
|
-
let endMarketDay = endDate;
|
|
866
|
-
const nyEnd = toNYTime(endDate);
|
|
867
|
-
const marketOpenMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
|
|
868
|
-
const marketCloseMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
|
|
869
|
-
const minutes = nyEnd.getUTCHours() * 60 + nyEnd.getUTCMinutes();
|
|
870
|
-
if (!calendar.isMarketDay(endDate) ||
|
|
871
|
-
minutes < marketOpenMinutes ||
|
|
872
|
-
(minutes >= marketOpenMinutes && minutes < marketCloseMinutes)) {
|
|
873
|
-
// Before market open, not a market day, or during market hours: use previous market day
|
|
874
|
-
endMarketDay = calendar.getPreviousMarketDay(endDate);
|
|
875
|
-
}
|
|
876
|
-
else {
|
|
877
|
-
// After market close: use today
|
|
878
|
-
endMarketDay = endDate;
|
|
879
|
-
}
|
|
880
|
-
// Get market close for endMarketDay
|
|
881
|
-
const endClose = getMarketOpenClose({ date: endMarketDay }).close;
|
|
882
|
-
// Find start market day by iterating back over market days
|
|
883
|
-
let startMarketDay = endMarketDay;
|
|
884
|
-
let count = Math.max(1, days);
|
|
885
|
-
for (let i = 1; i < count; i++) {
|
|
886
|
-
startMarketDay = calendar.getPreviousMarketDay(startMarketDay);
|
|
887
|
-
}
|
|
888
|
-
// If days > 1, we need to go back (days-1) market days from endMarketDay
|
|
889
|
-
if (days > 1) {
|
|
890
|
-
startMarketDay = endMarketDay;
|
|
891
|
-
for (let i = 1; i < days; i++) {
|
|
892
|
-
startMarketDay = calendar.getPreviousMarketDay(startMarketDay);
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
const startOpen = getMarketOpenClose({ date: startMarketDay }).open;
|
|
896
|
-
return { startDate: startOpen, endDate: endClose };
|
|
897
|
-
}
|
|
898
|
-
/**
|
|
899
|
-
* Counts trading time between two dates (passed as standard Date objects), excluding weekends and holidays, and closed market hours, using other functions in this library.
|
|
900
|
-
*
|
|
901
|
-
* This function calculates the actual trading time between two dates by:
|
|
902
|
-
* 1. Iterating through each calendar day between startDate and endDate (inclusive)
|
|
903
|
-
* 2. For each day that is a market day (not weekend/holiday), getting market open/close times
|
|
904
|
-
* 3. Calculating the overlap between the time range and market hours for that day
|
|
905
|
-
* 4. Summing up all the trading minutes across all days
|
|
906
|
-
*
|
|
907
|
-
* The function automatically handles:
|
|
908
|
-
* - Weekends (Saturday/Sunday) - skipped entirely
|
|
909
|
-
* - Market holidays - skipped entirely
|
|
910
|
-
* - Early close days (e.g. day before holidays) - uses early close time
|
|
911
|
-
* - Times outside market hours - only counts time within 9:30am-4pm ET (or early close)
|
|
912
|
-
*
|
|
913
|
-
* Examples:
|
|
914
|
-
* - 12pm to 3:30pm same day = 3.5 hours = 210 minutes = 0.54 days
|
|
915
|
-
* - 9:30am to 4pm same day = 6.5 hours = 390 minutes = 1 day
|
|
916
|
-
* - Friday 2pm to Monday 2pm = 6.5 hours (Friday 2pm-4pm + Monday 9:30am-2pm)
|
|
917
|
-
*
|
|
918
|
-
* @param startDate - Start date/time
|
|
919
|
-
* @param endDate - End date/time (default: now)
|
|
920
|
-
* @returns Object containing:
|
|
921
|
-
* - days: Trading time as fraction of full trading days (6.5 hours = 1 day)
|
|
922
|
-
* - hours: Trading time in hours
|
|
923
|
-
* - minutes: Trading time in minutes
|
|
924
|
-
*/
|
|
925
|
-
function countTradingDays(startDate, endDate = new Date()) {
|
|
926
|
-
const calendar = new MarketCalendar();
|
|
927
|
-
// Ensure start is before end
|
|
928
|
-
if (startDate.getTime() > endDate.getTime()) {
|
|
929
|
-
throw new Error('Start date must be before end date');
|
|
930
|
-
}
|
|
931
|
-
let totalMinutes = 0;
|
|
932
|
-
// Get the NY dates for iteration
|
|
933
|
-
const startNY = toNYTime(startDate);
|
|
934
|
-
const endNY = toNYTime(endDate);
|
|
935
|
-
// Create date at start of first day (in NY time)
|
|
936
|
-
const currentNY = new Date(Date.UTC(startNY.getUTCFullYear(), startNY.getUTCMonth(), startNY.getUTCDate(), 0, 0, 0, 0));
|
|
937
|
-
// Iterate through each calendar day
|
|
938
|
-
while (currentNY.getTime() <= endNY.getTime()) {
|
|
939
|
-
const currentUTC = fromNYTime(currentNY);
|
|
940
|
-
// Check if this is a market day
|
|
941
|
-
if (calendar.isMarketDay(currentUTC)) {
|
|
942
|
-
// Get market hours for this day
|
|
943
|
-
const marketTimes = getMarketTimes(currentUTC);
|
|
944
|
-
if (marketTimes.marketOpen && marketTimes.open && marketTimes.close) {
|
|
945
|
-
// Calculate the overlap between our time range and market hours
|
|
946
|
-
const dayStart = Math.max(startDate.getTime(), marketTimes.open.getTime());
|
|
947
|
-
const dayEnd = Math.min(endDate.getTime(), marketTimes.close.getTime());
|
|
948
|
-
// Only count if there's actual overlap
|
|
949
|
-
if (dayStart < dayEnd) {
|
|
950
|
-
totalMinutes += (dayEnd - dayStart) / (1000 * 60);
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
// Move to next day
|
|
955
|
-
currentNY.setUTCDate(currentNY.getUTCDate() + 1);
|
|
956
|
-
}
|
|
957
|
-
// Convert to days, hours, minutes
|
|
958
|
-
const MINUTES_PER_TRADING_DAY = 390; // 6.5 hours
|
|
959
|
-
const days = totalMinutes / MINUTES_PER_TRADING_DAY;
|
|
960
|
-
const hours = totalMinutes / 60;
|
|
961
|
-
const minutes = totalMinutes;
|
|
962
|
-
return {
|
|
963
|
-
days: Math.round(days * 1000) / 1000, // Round to 3 decimal places
|
|
964
|
-
hours: Math.round(hours * 100) / 100, // Round to 2 decimal places
|
|
965
|
-
minutes: Math.round(minutes),
|
|
966
|
-
};
|
|
967
|
-
}
|
|
968
|
-
/**
|
|
969
|
-
* Returns the trading day N days back from a reference date, along with its market open time.
|
|
970
|
-
* Trading days are counted as full or half trading days (days that end count as 1 full trading day).
|
|
971
|
-
* By default, the most recent completed trading day counts as day 1.
|
|
972
|
-
* Set includeMostRecentFullDay to false to count strictly before that day.
|
|
973
|
-
*
|
|
974
|
-
* @param options - Object with:
|
|
975
|
-
* - referenceDate: Date to count back from (default: now)
|
|
976
|
-
* - days: Number of trading days to go back (must be an integer >= 1)
|
|
977
|
-
* - includeMostRecentFullDay: Whether to include the most recent completed trading day (default: true)
|
|
978
|
-
* @returns Object containing:
|
|
979
|
-
* - date: Trading date in YYYY-MM-DD format
|
|
980
|
-
* - marketOpenISO: Market open time as ISO string (e.g., "2025-11-15T13:30:00.000Z")
|
|
981
|
-
* - unixTimestamp: Market open time as Unix timestamp in seconds
|
|
982
|
-
* @example
|
|
983
|
-
* ```typescript
|
|
984
|
-
* // Get the trading day 1 day back (most recent full trading day)
|
|
985
|
-
* const result = getTradingDaysBack({ days: 1 });
|
|
986
|
-
* console.log(result.date); // "2025-11-01"
|
|
987
|
-
* console.log(result.marketOpenISO); // "2025-11-01T13:30:00.000Z"
|
|
988
|
-
* console.log(result.unixTimestamp); // 1730466600
|
|
989
|
-
*
|
|
990
|
-
* // Get the trading day 5 days back from a specific date
|
|
991
|
-
* const result2 = getTradingDaysBack({
|
|
992
|
-
* referenceDate: new Date('2025-11-15T12:00:00-05:00'),
|
|
993
|
-
* days: 5
|
|
994
|
-
* });
|
|
995
|
-
* ```
|
|
996
|
-
*/
|
|
997
|
-
function getTradingDaysBack(options) {
|
|
998
|
-
const calendar = new MarketCalendar();
|
|
999
|
-
const { referenceDate, days, includeMostRecentFullDay = true } = options;
|
|
1000
|
-
const refDate = referenceDate || new Date();
|
|
1001
|
-
const daysBack = days;
|
|
1002
|
-
if (!Number.isInteger(daysBack) || daysBack < 1) {
|
|
1003
|
-
throw new Error('days must be an integer >= 1');
|
|
1004
|
-
}
|
|
1005
|
-
// Start from the last full trading date relative to reference
|
|
1006
|
-
let targetDate = getLastFullTradingDateImpl(refDate);
|
|
1007
|
-
if (!includeMostRecentFullDay) {
|
|
1008
|
-
targetDate = calendar.getPreviousMarketDay(targetDate);
|
|
1009
|
-
}
|
|
1010
|
-
// Go back the specified number of days (we're already at day 1, so go back days-1 more)
|
|
1011
|
-
for (let i = 1; i < daysBack; i++) {
|
|
1012
|
-
targetDate = calendar.getPreviousMarketDay(targetDate);
|
|
1013
|
-
}
|
|
1014
|
-
// Get market open time for this date
|
|
1015
|
-
const marketTimes = getMarketTimes(targetDate);
|
|
1016
|
-
if (!marketTimes.open) {
|
|
1017
|
-
throw new Error(`No market open time for target date`);
|
|
1018
|
-
}
|
|
1019
|
-
// Format the date string (YYYY-MM-DD) in NY time
|
|
1020
|
-
const nyDate = toNYTime(marketTimes.open);
|
|
1021
|
-
const dateStr = `${nyDate.getUTCFullYear()}-${String(nyDate.getUTCMonth() + 1).padStart(2, '0')}-${String(nyDate.getUTCDate()).padStart(2, '0')}`;
|
|
1022
|
-
const marketOpenISO = marketTimes.open.toISOString();
|
|
1023
|
-
const unixTimestamp = Math.floor(marketTimes.open.getTime() / 1000);
|
|
1024
|
-
return {
|
|
1025
|
-
date: dateStr,
|
|
1026
|
-
marketOpenISO,
|
|
1027
|
-
unixTimestamp,
|
|
1028
|
-
};
|
|
1029
|
-
}
|
|
1030
|
-
// Export MARKET_TIMES for compatibility
|
|
1031
|
-
const MARKET_TIMES = {
|
|
1032
|
-
TIMEZONE: MARKET_CONFIG.TIMEZONE,
|
|
1033
|
-
PRE: {
|
|
1034
|
-
START: { HOUR: 4, MINUTE: 0, MINUTES: 240 },
|
|
1035
|
-
END: { HOUR: 9, MINUTE: 30, MINUTES: 570 },
|
|
1036
|
-
},
|
|
1037
|
-
EARLY_MORNING: {
|
|
1038
|
-
START: { HOUR: 9, MINUTE: 30, MINUTES: 570 },
|
|
1039
|
-
END: { HOUR: 10, MINUTE: 0, MINUTES: 600 },
|
|
1040
|
-
},
|
|
1041
|
-
EARLY_CLOSE_BEFORE_HOLIDAY: {
|
|
1042
|
-
START: { HOUR: 9, MINUTE: 30, MINUTES: 570 },
|
|
1043
|
-
END: { HOUR: 13, MINUTE: 0, MINUTES: 780 },
|
|
1044
|
-
},
|
|
1045
|
-
EARLY_EXTENDED_BEFORE_HOLIDAY: {
|
|
1046
|
-
START: { HOUR: 13, MINUTE: 0, MINUTES: 780 },
|
|
1047
|
-
END: { HOUR: 17, MINUTE: 0, MINUTES: 1020 },
|
|
1048
|
-
},
|
|
1049
|
-
REGULAR: {
|
|
1050
|
-
START: { HOUR: 9, MINUTE: 30, MINUTES: 570 },
|
|
1051
|
-
END: { HOUR: 16, MINUTE: 0, MINUTES: 960 },
|
|
1052
|
-
},
|
|
1053
|
-
EXTENDED: {
|
|
1054
|
-
START: { HOUR: 4, MINUTE: 0, MINUTES: 240 },
|
|
1055
|
-
END: { HOUR: 20, MINUTE: 0, MINUTES: 1200 },
|
|
1056
|
-
},
|
|
1057
|
-
};
|
|
1058
383
|
|
|
1059
384
|
/*
|
|
1060
385
|
How it works:
|
|
@@ -15190,132 +14515,1774 @@ class AlpacaMarketDataAPI extends EventEmitter {
|
|
|
15190
14515
|
}
|
|
15191
14516
|
}
|
|
15192
14517
|
// Export the singleton instance
|
|
15193
|
-
AlpacaMarketDataAPI.getInstance();
|
|
15194
|
-
|
|
15195
|
-
const disco = {
|
|
15196
|
-
time: {
|
|
15197
|
-
convertDateToMarketTimeZone: convertDateToMarketTimeZone,
|
|
15198
|
-
getStartAndEndDates: getStartAndEndDates,
|
|
15199
|
-
getMarketOpenClose: getMarketOpenClose,
|
|
15200
|
-
getOpenCloseForTradingDay: getOpenCloseForTradingDay,
|
|
15201
|
-
getLastFullTradingDate: getLastFullTradingDate,
|
|
15202
|
-
getNextMarketDay: getNextMarketDay,
|
|
15203
|
-
getPreviousMarketDay: getPreviousMarketDay,
|
|
15204
|
-
getMarketTimePeriod: getMarketTimePeriod,
|
|
15205
|
-
getMarketStatus: getMarketStatus,
|
|
15206
|
-
getNYTimeZone: getNYTimeZone,
|
|
15207
|
-
getTradingDate: getTradingDate,
|
|
15208
|
-
getTradingStartAndEndDates: getTradingStartAndEndDates,
|
|
15209
|
-
getTradingDaysBack: getTradingDaysBack,
|
|
15210
|
-
isMarketDay: isMarketDay,
|
|
15211
|
-
isWithinMarketHours: isWithinMarketHours,
|
|
15212
|
-
countTradingDays: countTradingDays,
|
|
15213
|
-
MARKET_TIMES: MARKET_TIMES,
|
|
15214
|
-
}};
|
|
14518
|
+
const marketDataAPI = AlpacaMarketDataAPI.getInstance();
|
|
15215
14519
|
|
|
15216
|
-
|
|
15217
|
-
|
|
15218
|
-
|
|
15219
|
-
|
|
15220
|
-
|
|
15221
|
-
|
|
15222
|
-
|
|
15223
|
-
|
|
15224
|
-
|
|
15225
|
-
|
|
15226
|
-
|
|
15227
|
-
|
|
15228
|
-
|
|
15229
|
-
|
|
15230
|
-
|
|
15231
|
-
|
|
15232
|
-
|
|
15233
|
-
|
|
15234
|
-
|
|
15235
|
-
|
|
15236
|
-
|
|
15237
|
-
|
|
15238
|
-
|
|
15239
|
-
|
|
15240
|
-
|
|
15241
|
-
|
|
15242
|
-
|
|
15243
|
-
|
|
15244
|
-
|
|
15245
|
-
|
|
15246
|
-
|
|
15247
|
-
|
|
15248
|
-
|
|
15249
|
-
|
|
15250
|
-
|
|
15251
|
-
|
|
15252
|
-
|
|
15253
|
-
|
|
15254
|
-
|
|
15255
|
-
|
|
15256
|
-
|
|
15257
|
-
|
|
15258
|
-
|
|
15259
|
-
|
|
15260
|
-
|
|
15261
|
-
|
|
15262
|
-
|
|
15263
|
-
|
|
15264
|
-
|
|
15265
|
-
|
|
15266
|
-
|
|
15267
|
-
|
|
15268
|
-
|
|
15269
|
-
|
|
15270
|
-
|
|
15271
|
-
|
|
15272
|
-
|
|
15273
|
-
|
|
15274
|
-
|
|
15275
|
-
|
|
15276
|
-
|
|
15277
|
-
|
|
15278
|
-
|
|
15279
|
-
|
|
15280
|
-
|
|
15281
|
-
|
|
15282
|
-
|
|
15283
|
-
|
|
15284
|
-
|
|
15285
|
-
|
|
15286
|
-
|
|
15287
|
-
|
|
15288
|
-
|
|
15289
|
-
expected: '2026-01-26',
|
|
15290
|
-
},
|
|
15291
|
-
{
|
|
15292
|
-
label: 'Edge case (Jan 24, 2026, timestamp input EST)',
|
|
15293
|
-
input: new Date('2026-01-24T12:00:00-05:00').getTime(),
|
|
15294
|
-
expected: '2026-01-24',
|
|
15295
|
-
},
|
|
15296
|
-
];
|
|
15297
|
-
for (const { label, input, expected } of testCases) {
|
|
15298
|
-
const result = disco.time.getTradingDate(input);
|
|
15299
|
-
const pass = result === expected;
|
|
15300
|
-
console.log(`\nTest: ${label}`);
|
|
15301
|
-
console.log(` Input: ${input instanceof Date ? input.toISOString() : input}`);
|
|
15302
|
-
console.log(` Result: ${result} ${pass ? '✓' : `✗ (expected ${expected})`}`);
|
|
15303
|
-
if (!pass) {
|
|
15304
|
-
console.error(` FAILED: Expected ${expected}, got ${result}`);
|
|
14520
|
+
const limitPriceSlippagePercent100 = 0.1; // 0.1%
|
|
14521
|
+
/**
|
|
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;
|
|
15305
14593
|
}
|
|
14594
|
+
log$1(message, { ...options, source: 'AlpacaTradingAPI', account: this.credentials.accountName });
|
|
15306
14595
|
}
|
|
15307
|
-
|
|
15308
|
-
|
|
15309
|
-
|
|
15310
|
-
|
|
15311
|
-
|
|
15312
|
-
|
|
15313
|
-
|
|
15314
|
-
|
|
15315
|
-
|
|
15316
|
-
|
|
15317
|
-
|
|
15318
|
-
|
|
15319
|
-
|
|
15320
|
-
|
|
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
|
+
}
|
|
15321
16288
|
//# sourceMappingURL=test.js.map
|