@adaptic/utils 0.0.1002 → 0.0.1004
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 +766 -235
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +766 -235
- package/dist/index.mjs.map +1 -1
- package/dist/test.js +70 -0
- package/dist/test.js.map +1 -1
- package/dist/types/__tests__/stampede-load-timeout.test.d.ts +2 -0
- package/dist/types/__tests__/stampede-load-timeout.test.d.ts.map +1 -0
- package/dist/types/alpaca-trading-api.d.ts +68 -4
- package/dist/types/alpaca-trading-api.d.ts.map +1 -1
- package/dist/types/cache/stampede-protected-cache.d.ts +28 -0
- package/dist/types/cache/stampede-protected-cache.d.ts.map +1 -1
- package/dist/types/crypto.d.ts.map +1 -1
- package/dist/types/index.d.ts +4 -4
- package/dist/types/performance-metrics.d.ts.map +1 -1
- package/dist/types/price-utils.d.ts.map +1 -1
- package/dist/types/rate-limiter.d.ts +21 -0
- package/dist/types/rate-limiter.d.ts.map +1 -1
- package/dist/types/types/alpaca-types.d.ts +1 -0
- package/dist/types/types/alpaca-types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { format, sub, set, add, startOfDay, endOfDay, isBefore, differenceInMill
|
|
|
3
3
|
import { formatInTimeZone, toZonedTime, fromZonedTime } from 'date-fns-tz';
|
|
4
4
|
import require$$0$4, { EventEmitter } from 'events';
|
|
5
5
|
import WebSocket from 'ws';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
6
7
|
import ms from 'ms';
|
|
7
8
|
import require$$0$1 from 'fs';
|
|
8
9
|
import require$$1 from 'path';
|
|
@@ -2403,6 +2404,19 @@ class DataFormatError extends AdapticUtilsError {
|
|
|
2403
2404
|
* const result = await makeAlpacaApiCall();
|
|
2404
2405
|
* ```
|
|
2405
2406
|
*/
|
|
2407
|
+
/** Number of milliseconds in one second, used for token-refill timing math. */
|
|
2408
|
+
const MS_PER_SECOND$1 = 1000;
|
|
2409
|
+
/**
|
|
2410
|
+
* Minimum delay (ms) for a scheduled queue wake-up. Guards against a `0`/`NaN`
|
|
2411
|
+
* delay when the token deficit rounds down, ensuring the timer always makes
|
|
2412
|
+
* forward progress rather than busy-looping on the event loop.
|
|
2413
|
+
*/
|
|
2414
|
+
const MIN_WAKE_DELAY_MS = 1;
|
|
2415
|
+
/**
|
|
2416
|
+
* Number of whole tokens required to release a single queued request. The token
|
|
2417
|
+
* bucket consumes exactly one token per admitted request.
|
|
2418
|
+
*/
|
|
2419
|
+
const TOKENS_PER_REQUEST = 1;
|
|
2406
2420
|
/**
|
|
2407
2421
|
* Token bucket rate limiter implementation
|
|
2408
2422
|
*
|
|
@@ -2418,6 +2432,13 @@ class TokenBucketRateLimiter {
|
|
|
2418
2432
|
queue = [];
|
|
2419
2433
|
timeoutMs;
|
|
2420
2434
|
processingQueue = false;
|
|
2435
|
+
/**
|
|
2436
|
+
* Single pending timer that wakes the limiter to refill tokens and drain the
|
|
2437
|
+
* queue. Without this, a queued request would only be released by a
|
|
2438
|
+
* subsequent {@link acquire} call and would otherwise stall until its own
|
|
2439
|
+
* timeout fired. `null` means no wake-up is currently scheduled.
|
|
2440
|
+
*/
|
|
2441
|
+
wakeTimer = null;
|
|
2421
2442
|
/**
|
|
2422
2443
|
* Creates a new rate limiter instance
|
|
2423
2444
|
*
|
|
@@ -2491,8 +2512,48 @@ class TokenBucketRateLimiter {
|
|
|
2491
2512
|
reject(error);
|
|
2492
2513
|
}, this.timeoutMs);
|
|
2493
2514
|
this.queue.push({ resolve, reject, timeoutHandle });
|
|
2515
|
+
// Ensure the queue is actively drained even if no further acquire() calls
|
|
2516
|
+
// arrive: schedule a wake-up to refill tokens and release this request.
|
|
2517
|
+
this.scheduleQueueWake();
|
|
2494
2518
|
});
|
|
2495
2519
|
}
|
|
2520
|
+
/**
|
|
2521
|
+
* Schedules a single wake-up timer that refills tokens and drains the queue.
|
|
2522
|
+
*
|
|
2523
|
+
* The delay is the time required to accrue the tokens still needed to release
|
|
2524
|
+
* the next queued request at the configured refill rate. Only one timer is
|
|
2525
|
+
* ever outstanding (guarded by {@link wakeTimer}); the timer is `unref`'d so
|
|
2526
|
+
* it never keeps the Node.js process alive on its own. When it fires it
|
|
2527
|
+
* refills, drains what it can, and re-arms itself if work remains.
|
|
2528
|
+
*/
|
|
2529
|
+
scheduleQueueWake() {
|
|
2530
|
+
// A wake-up is already pending, or there is nothing to wake for.
|
|
2531
|
+
if (this.wakeTimer !== null || this.queue.length === 0) {
|
|
2532
|
+
return;
|
|
2533
|
+
}
|
|
2534
|
+
const tokensNeeded = Math.max(0, TOKENS_PER_REQUEST - this.tokens);
|
|
2535
|
+
const deficitMs = Math.max(MIN_WAKE_DELAY_MS, Math.ceil((tokensNeeded / this.config.refillRate) * MS_PER_SECOND$1));
|
|
2536
|
+
const timer = setTimeout(() => {
|
|
2537
|
+
this.wakeTimer = null;
|
|
2538
|
+
// refill() drains the queue via processQueue(); if requests remain
|
|
2539
|
+
// afterwards, processQueue() re-arms the wake-up.
|
|
2540
|
+
this.refill();
|
|
2541
|
+
}, deficitMs);
|
|
2542
|
+
// Do not let a pending rate-limiter wake-up keep the process alive.
|
|
2543
|
+
if (typeof timer.unref === "function") {
|
|
2544
|
+
timer.unref();
|
|
2545
|
+
}
|
|
2546
|
+
this.wakeTimer = timer;
|
|
2547
|
+
}
|
|
2548
|
+
/**
|
|
2549
|
+
* Clears any pending wake-up timer.
|
|
2550
|
+
*/
|
|
2551
|
+
clearWakeTimer() {
|
|
2552
|
+
if (this.wakeTimer !== null) {
|
|
2553
|
+
clearTimeout(this.wakeTimer);
|
|
2554
|
+
this.wakeTimer = null;
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2496
2557
|
/**
|
|
2497
2558
|
* Refills tokens based on elapsed time and processes queued requests
|
|
2498
2559
|
*
|
|
@@ -2537,6 +2598,15 @@ class TokenBucketRateLimiter {
|
|
|
2537
2598
|
finally {
|
|
2538
2599
|
this.processingQueue = false;
|
|
2539
2600
|
}
|
|
2601
|
+
// Keep the wake-up state consistent with the queue: if requests are still
|
|
2602
|
+
// waiting (tokens ran out mid-drain), ensure a wake-up is armed; otherwise
|
|
2603
|
+
// release any pending timer so it cannot fire needlessly.
|
|
2604
|
+
if (this.queue.length > 0) {
|
|
2605
|
+
this.scheduleQueueWake();
|
|
2606
|
+
}
|
|
2607
|
+
else {
|
|
2608
|
+
this.clearWakeTimer();
|
|
2609
|
+
}
|
|
2540
2610
|
}
|
|
2541
2611
|
/**
|
|
2542
2612
|
* Gets the current number of available tokens
|
|
@@ -2568,6 +2638,7 @@ class TokenBucketRateLimiter {
|
|
|
2568
2638
|
clearTimeout(request.timeoutHandle);
|
|
2569
2639
|
request.reject(new RateLimitError(`Rate limiter reset for ${this.config.label}`, this.config.label, undefined));
|
|
2570
2640
|
}
|
|
2641
|
+
this.clearWakeTimer();
|
|
2571
2642
|
this.queue = [];
|
|
2572
2643
|
this.tokens = this.config.maxTokens;
|
|
2573
2644
|
this.lastRefill = Date.now();
|
|
@@ -4317,6 +4388,46 @@ const limitPriceSlippagePercent100 = 0.1; // 0.1%
|
|
|
4317
4388
|
const ORDER_PAGE_LIMIT = 500;
|
|
4318
4389
|
/** Delay between order pagination pages to stay clear of rate limits. */
|
|
4319
4390
|
const ORDER_PAGINATION_DELAY_MS = 300;
|
|
4391
|
+
/**
|
|
4392
|
+
* HTTP status at or above which a Multi-Status (207) sub-result is a failure.
|
|
4393
|
+
* Alpaca's bulk `DELETE /orders` and `DELETE /positions` endpoints return a 207
|
|
4394
|
+
* envelope whose top-level status is 2xx even when individual orders/positions
|
|
4395
|
+
* failed to cancel or close; each element carries its own per-item HTTP status.
|
|
4396
|
+
* Treating >= 300 as a failure lets the engine failsafe see partial failures
|
|
4397
|
+
* instead of recording a false success.
|
|
4398
|
+
*/
|
|
4399
|
+
const HTTP_STATUS_MIN_ERROR = 300;
|
|
4400
|
+
/**
|
|
4401
|
+
* Prefix applied to engine-derived `client_order_id` idempotency keys so they
|
|
4402
|
+
* are visibly attributable in Alpaca's dashboard and can never collide with a
|
|
4403
|
+
* caller-supplied identifier.
|
|
4404
|
+
*/
|
|
4405
|
+
const CLIENT_ORDER_ID_PREFIX = "adaptic-";
|
|
4406
|
+
/**
|
|
4407
|
+
* Number of leading hex characters of the SHA-256 digest retained in a derived
|
|
4408
|
+
* `client_order_id`. 32 hex chars = 128 bits of entropy (collision-negligible),
|
|
4409
|
+
* and keeps the full id (prefix + digest = 40 chars) within Alpaca's identifier
|
|
4410
|
+
* length limit.
|
|
4411
|
+
*/
|
|
4412
|
+
const CLIENT_ORDER_ID_HASH_LENGTH = 32;
|
|
4413
|
+
/**
|
|
4414
|
+
* Idempotency window (ms) used when deriving a default `client_order_id`.
|
|
4415
|
+
*
|
|
4416
|
+
* A client request timeout followed by an automatic retry re-submits the SAME
|
|
4417
|
+
* logical order. Deriving the id from the order's semantic parameters plus the
|
|
4418
|
+
* current time-window bucket makes Alpaca reject the retried duplicate
|
|
4419
|
+
* (`client_order_id must be unique`) instead of double-filling. The window is
|
|
4420
|
+
* deliberately much larger than the 30s Alpaca request timeout so a full
|
|
4421
|
+
* timeout+retry sequence lands in the same bucket, while a genuinely new but
|
|
4422
|
+
* otherwise-identical order placed in a later window still receives a distinct
|
|
4423
|
+
* id.
|
|
4424
|
+
*
|
|
4425
|
+
* This derived default is a best-effort safety net; the guaranteed-idempotent
|
|
4426
|
+
* path is for the caller to pass an explicit `clientOrderId` tied to the
|
|
4427
|
+
* originating signal/decision id (which also permits legitimately-repeated
|
|
4428
|
+
* identical orders inside a single window).
|
|
4429
|
+
*/
|
|
4430
|
+
const CLIENT_ORDER_ID_WINDOW_MS = 300_000;
|
|
4320
4431
|
/**
|
|
4321
4432
|
Websocket example
|
|
4322
4433
|
const alpacaAPI = createAlpacaTradingAPI(credentials); // type AlpacaCredentials
|
|
@@ -4397,6 +4508,114 @@ class AlpacaTradingAPI {
|
|
|
4397
4508
|
? Math.round(price * 100) / 100
|
|
4398
4509
|
: Math.round(price * 10000) / 10000;
|
|
4399
4510
|
};
|
|
4511
|
+
/**
|
|
4512
|
+
* Derive a deterministic `client_order_id` from an order's semantic
|
|
4513
|
+
* parameters so that a client-timeout-triggered retry re-submits the SAME id
|
|
4514
|
+
* and Alpaca rejects the duplicate broker-side instead of double-filling.
|
|
4515
|
+
*
|
|
4516
|
+
* The id is stable for identical parameters within a single
|
|
4517
|
+
* {@link CLIENT_ORDER_ID_WINDOW_MS} bucket and scoped per account. Callers
|
|
4518
|
+
* that must place genuinely distinct yet otherwise-identical orders should
|
|
4519
|
+
* pass an explicit `clientOrderId` rather than relying on this default.
|
|
4520
|
+
*
|
|
4521
|
+
* @param parts - Ordered, stringifiable components uniquely describing the
|
|
4522
|
+
* order (e.g. order kind, symbol, side, quantity, price, intent).
|
|
4523
|
+
* @returns An Alpaca-safe `client_order_id` (prefix + truncated SHA-256 hex).
|
|
4524
|
+
*/
|
|
4525
|
+
deriveClientOrderId(parts) {
|
|
4526
|
+
const windowBucket = Math.floor(Date.now() / CLIENT_ORDER_ID_WINDOW_MS);
|
|
4527
|
+
const material = [
|
|
4528
|
+
this.credentials.accountName,
|
|
4529
|
+
windowBucket,
|
|
4530
|
+
...parts.map((part) => (part === undefined ? "" : String(part))),
|
|
4531
|
+
].join("|");
|
|
4532
|
+
const digest = createHash("sha256")
|
|
4533
|
+
.update(material)
|
|
4534
|
+
.digest("hex")
|
|
4535
|
+
.slice(0, CLIENT_ORDER_ID_HASH_LENGTH);
|
|
4536
|
+
return `${CLIENT_ORDER_ID_PREFIX}${digest}`;
|
|
4537
|
+
}
|
|
4538
|
+
/**
|
|
4539
|
+
* Collect the human-readable failure entries from a bulk Multi-Status (207)
|
|
4540
|
+
* response body (`DELETE /orders`, `DELETE /positions`). Each element carries
|
|
4541
|
+
* its own per-item HTTP status; any element with status >=
|
|
4542
|
+
* {@link HTTP_STATUS_MIN_ERROR} is a failure the caller must be able to see.
|
|
4543
|
+
*
|
|
4544
|
+
* @param entries - Parsed 207 response array.
|
|
4545
|
+
* @returns One `"<identifier>:<status>"` string per failed entry.
|
|
4546
|
+
*/
|
|
4547
|
+
collectMultiStatusFailures(entries) {
|
|
4548
|
+
return entries
|
|
4549
|
+
.filter((entry) => typeof entry.status === "number" &&
|
|
4550
|
+
entry.status >= HTTP_STATUS_MIN_ERROR)
|
|
4551
|
+
.map((entry) => `${entry.symbol ?? entry.id ?? "unknown"}:${entry.status}`);
|
|
4552
|
+
}
|
|
4553
|
+
/**
|
|
4554
|
+
* Flatten a single position with a marketable limit order, deriving the
|
|
4555
|
+
* closing side, intent, and slippage-adjusted limit price from the latest
|
|
4556
|
+
* quote. Throws when no usable quote/price is available for the symbol so the
|
|
4557
|
+
* caller can record a per-position failure via {@link Promise.allSettled}
|
|
4558
|
+
* without aborting the flatten of the remaining positions.
|
|
4559
|
+
*
|
|
4560
|
+
* @param position - The position to close.
|
|
4561
|
+
* @param quotesResponse - Latest quotes keyed by symbol.
|
|
4562
|
+
* @param extendedHours - Whether the closing order is an extended-hours order.
|
|
4563
|
+
*/
|
|
4564
|
+
async closePositionWithLimitOrder(position, quotesResponse, extendedHours) {
|
|
4565
|
+
const quote = quotesResponse.quotes[position.symbol];
|
|
4566
|
+
if (!quote) {
|
|
4567
|
+
throw new Error(`No quote available for ${position.symbol}`);
|
|
4568
|
+
}
|
|
4569
|
+
const qty = Math.abs(parseFloat(position.qty));
|
|
4570
|
+
const side = position.side === "long" ? "sell" : "buy";
|
|
4571
|
+
const positionIntent = side === "sell" ? "sell_to_close" : "buy_to_close";
|
|
4572
|
+
// Use bid for sells, ask for buys.
|
|
4573
|
+
const currentPrice = side === "sell" ? quote.bp : quote.ap;
|
|
4574
|
+
if (!currentPrice) {
|
|
4575
|
+
throw new Error(`No valid price available for ${position.symbol}`);
|
|
4576
|
+
}
|
|
4577
|
+
const limitSlippagePercent1 = limitPriceSlippagePercent100 / 100;
|
|
4578
|
+
const limitPrice = side === "sell"
|
|
4579
|
+
? this.roundPriceForAlpaca(currentPrice * (1 - limitSlippagePercent1)) // Sell slightly lower
|
|
4580
|
+
: this.roundPriceForAlpaca(currentPrice * (1 + limitSlippagePercent1)); // Buy slightly higher
|
|
4581
|
+
this.log(`Creating ${extendedHours ? "extended hours " : ""}limit order to close ${position.symbol} position: ${side} ${qty} shares at $${limitPrice.toFixed(2)}`, {
|
|
4582
|
+
symbol: position.symbol,
|
|
4583
|
+
});
|
|
4584
|
+
await this.createLimitOrder(position.symbol, qty, side, limitPrice, positionIntent, extendedHours);
|
|
4585
|
+
}
|
|
4586
|
+
/**
|
|
4587
|
+
* Flatten every supplied position independently and surface an aggregate
|
|
4588
|
+
* failure if any could not be closed. Positions are attempted concurrently
|
|
4589
|
+
* with {@link Promise.allSettled} so a data gap or broker rejection on one
|
|
4590
|
+
* symbol never silently prevents the others from being flattened.
|
|
4591
|
+
*
|
|
4592
|
+
* @param positions - Positions to flatten.
|
|
4593
|
+
* @param extendedHours - Whether the closing orders are extended-hours orders.
|
|
4594
|
+
* @throws Error listing every symbol that failed to flatten.
|
|
4595
|
+
*/
|
|
4596
|
+
async flattenPositionsWithLimitOrders(positions, extendedHours) {
|
|
4597
|
+
const symbols = positions.map((position) => position.symbol);
|
|
4598
|
+
const quotesResponse = await marketDataAPI.getLatestQuotes(symbols);
|
|
4599
|
+
const results = await Promise.allSettled(positions.map((position) => this.closePositionWithLimitOrder(position, quotesResponse, extendedHours)));
|
|
4600
|
+
const failures = [];
|
|
4601
|
+
results.forEach((result, index) => {
|
|
4602
|
+
if (result.status === "rejected") {
|
|
4603
|
+
const symbol = positions[index]?.symbol ?? "unknown";
|
|
4604
|
+
const reason = result.reason instanceof Error
|
|
4605
|
+
? result.reason.message
|
|
4606
|
+
: String(result.reason);
|
|
4607
|
+
failures.push(`${symbol}: ${reason}`);
|
|
4608
|
+
this.log(`Failed to close position ${symbol}: ${reason}`, {
|
|
4609
|
+
symbol,
|
|
4610
|
+
type: "error",
|
|
4611
|
+
});
|
|
4612
|
+
}
|
|
4613
|
+
});
|
|
4614
|
+
if (failures.length > 0) {
|
|
4615
|
+
throw new Error(`Failed to close ${failures.length} of ${positions.length} positions: ${failures.join("; ")}`);
|
|
4616
|
+
}
|
|
4617
|
+
this.log(`All positions closed: ${symbols.join(", ")}`);
|
|
4618
|
+
}
|
|
4400
4619
|
handleAuthMessage(data) {
|
|
4401
4620
|
if (data.status === "authorized") {
|
|
4402
4621
|
this.authenticated = true;
|
|
@@ -4768,21 +4987,31 @@ class AlpacaTradingAPI {
|
|
|
4768
4987
|
* @param position_intent (string) - the position intent of the order
|
|
4769
4988
|
* @returns The created AlpacaOrder with order ID and details
|
|
4770
4989
|
*/
|
|
4771
|
-
async createTrailingStop(symbol, qty, side, trailPercent100, position_intent) {
|
|
4990
|
+
async createTrailingStop(symbol, qty, side, trailPercent100, position_intent, clientOrderId) {
|
|
4772
4991
|
this.log(`Creating trailing stop ${side.toUpperCase()} ${qty} shares for ${symbol} with trail percent ${trailPercent100}%`, {
|
|
4773
4992
|
symbol,
|
|
4774
4993
|
});
|
|
4994
|
+
const body = {
|
|
4995
|
+
symbol,
|
|
4996
|
+
qty: Math.abs(qty).toString(),
|
|
4997
|
+
side,
|
|
4998
|
+
position_intent,
|
|
4999
|
+
order_class: "simple",
|
|
5000
|
+
type: "trailing_stop",
|
|
5001
|
+
trail_percent: trailPercent100.toString(), // Already in decimal form (e.g., 4 for 4%)
|
|
5002
|
+
time_in_force: "gtc",
|
|
5003
|
+
client_order_id: clientOrderId ??
|
|
5004
|
+
this.deriveClientOrderId([
|
|
5005
|
+
"trailing_stop",
|
|
5006
|
+
symbol,
|
|
5007
|
+
side,
|
|
5008
|
+
position_intent,
|
|
5009
|
+
Math.abs(qty),
|
|
5010
|
+
trailPercent100,
|
|
5011
|
+
]),
|
|
5012
|
+
};
|
|
4775
5013
|
try {
|
|
4776
|
-
const order = await this.makeRequest(`/orders`, "POST",
|
|
4777
|
-
symbol,
|
|
4778
|
-
qty: Math.abs(qty),
|
|
4779
|
-
side,
|
|
4780
|
-
position_intent,
|
|
4781
|
-
order_class: "simple",
|
|
4782
|
-
type: "trailing_stop",
|
|
4783
|
-
trail_percent: trailPercent100, // Already in decimal form (e.g., 4 for 4%)
|
|
4784
|
-
time_in_force: "gtc",
|
|
4785
|
-
});
|
|
5014
|
+
const order = await this.makeRequest(`/orders`, "POST", body);
|
|
4786
5015
|
this.log(`Trailing stop order created for ${symbol}: orderId=${order.id}, trailPercent=${trailPercent100}%`, { symbol });
|
|
4787
5016
|
return order;
|
|
4788
5017
|
}
|
|
@@ -4814,9 +5043,15 @@ class AlpacaTradingAPI {
|
|
|
4814
5043
|
time_in_force: "day",
|
|
4815
5044
|
order_class: "simple",
|
|
4816
5045
|
};
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
5046
|
+
body.client_order_id =
|
|
5047
|
+
client_order_id ??
|
|
5048
|
+
this.deriveClientOrderId([
|
|
5049
|
+
"market",
|
|
5050
|
+
symbol,
|
|
5051
|
+
side,
|
|
5052
|
+
position_intent,
|
|
5053
|
+
Math.abs(qty),
|
|
5054
|
+
]);
|
|
4820
5055
|
try {
|
|
4821
5056
|
return await this.makeRequest("/orders", "POST", body);
|
|
4822
5057
|
}
|
|
@@ -4917,16 +5152,31 @@ class AlpacaTradingAPI {
|
|
|
4917
5152
|
}
|
|
4918
5153
|
}
|
|
4919
5154
|
/**
|
|
4920
|
-
* Cancel all open orders
|
|
5155
|
+
* Cancel all open orders.
|
|
5156
|
+
*
|
|
5157
|
+
* Alpaca's bulk cancel returns a 207 Multi-Status body whose top-level status
|
|
5158
|
+
* is 2xx even when individual orders failed to cancel; this method inspects
|
|
5159
|
+
* the per-order statuses and throws if any order could not be canceled, so a
|
|
5160
|
+
* caller acting as a live-stop failsafe cannot record success while orders
|
|
5161
|
+
* remain live. Transport/HTTP errors propagate unchanged (matching the
|
|
5162
|
+
* throw-on-failure contract of {@link cancelOrder}).
|
|
5163
|
+
*
|
|
5164
|
+
* @throws Error if the bulk cancel request fails or any individual order
|
|
5165
|
+
* could not be canceled.
|
|
4921
5166
|
*/
|
|
4922
5167
|
async cancelAllOrders() {
|
|
4923
5168
|
this.log(`Canceling all open orders`);
|
|
4924
|
-
|
|
4925
|
-
|
|
5169
|
+
const results = await this.makeRequest("/orders", "DELETE");
|
|
5170
|
+
if (!Array.isArray(results)) {
|
|
5171
|
+
return;
|
|
4926
5172
|
}
|
|
4927
|
-
|
|
4928
|
-
|
|
5173
|
+
const failures = this.collectMultiStatusFailures(results);
|
|
5174
|
+
if (failures.length > 0) {
|
|
5175
|
+
const detail = failures.join(", ");
|
|
5176
|
+
this.log(`Error canceling all orders: ${failures.length}/${results.length} orders failed to cancel (${detail})`, { type: "error" });
|
|
5177
|
+
throw new Error(`Failed to cancel ${failures.length} of ${results.length} orders: ${detail}`);
|
|
4929
5178
|
}
|
|
5179
|
+
this.log(`Successfully canceled ${results.length} open orders`);
|
|
4930
5180
|
}
|
|
4931
5181
|
/**
|
|
4932
5182
|
* Cancel a specific order by its ID
|
|
@@ -4977,9 +5227,17 @@ class AlpacaTradingAPI {
|
|
|
4977
5227
|
order_class: "simple",
|
|
4978
5228
|
extended_hours,
|
|
4979
5229
|
};
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
5230
|
+
body.client_order_id =
|
|
5231
|
+
client_order_id ??
|
|
5232
|
+
this.deriveClientOrderId([
|
|
5233
|
+
"limit",
|
|
5234
|
+
symbol,
|
|
5235
|
+
side,
|
|
5236
|
+
position_intent,
|
|
5237
|
+
Math.abs(qty),
|
|
5238
|
+
this.roundPriceForAlpaca(limitPrice),
|
|
5239
|
+
extended_hours,
|
|
5240
|
+
]);
|
|
4983
5241
|
try {
|
|
4984
5242
|
return await this.makeRequest("/orders", "POST", body);
|
|
4985
5243
|
}
|
|
@@ -5007,55 +5265,21 @@ class AlpacaTradingAPI {
|
|
|
5007
5265
|
return;
|
|
5008
5266
|
}
|
|
5009
5267
|
this.log(`Found ${positions.length} positions to close`);
|
|
5010
|
-
//
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
if (lengthOfQuotes === 0) {
|
|
5015
|
-
this.log("No quotes available for positions, received 0 quotes", {
|
|
5016
|
-
type: "error",
|
|
5017
|
-
});
|
|
5018
|
-
return;
|
|
5019
|
-
}
|
|
5020
|
-
if (lengthOfQuotes !== positions.length) {
|
|
5021
|
-
this.log(`Received ${lengthOfQuotes} quotes for ${positions.length} positions, expected ${positions.length} quotes`, { type: "warn" });
|
|
5022
|
-
return;
|
|
5023
|
-
}
|
|
5024
|
-
// Create limit orders to close each position
|
|
5025
|
-
for (const position of positions) {
|
|
5026
|
-
const quote = quotesResponse.quotes[position.symbol];
|
|
5027
|
-
if (!quote) {
|
|
5028
|
-
this.log(`No quote available for ${position.symbol}, skipping limit order`, {
|
|
5029
|
-
symbol: position.symbol,
|
|
5030
|
-
type: "warn",
|
|
5031
|
-
});
|
|
5032
|
-
continue;
|
|
5033
|
-
}
|
|
5034
|
-
const qty = Math.abs(parseFloat(position.qty));
|
|
5035
|
-
const side = position.side === "long" ? "sell" : "buy";
|
|
5036
|
-
const positionIntent = side === "sell" ? "sell_to_close" : "buy_to_close";
|
|
5037
|
-
// Get the current price from the quote
|
|
5038
|
-
const currentPrice = side === "sell" ? quote.bp : quote.ap; // Use bid for sells, ask for buys
|
|
5039
|
-
if (!currentPrice) {
|
|
5040
|
-
this.log(`No valid price available for ${position.symbol}, skipping limit order`, {
|
|
5041
|
-
symbol: position.symbol,
|
|
5042
|
-
type: "warn",
|
|
5043
|
-
});
|
|
5044
|
-
continue;
|
|
5045
|
-
}
|
|
5046
|
-
// Apply slippage from config
|
|
5047
|
-
const limitSlippagePercent1 = limitPriceSlippagePercent100 / 100;
|
|
5048
|
-
const limitPrice = side === "sell"
|
|
5049
|
-
? this.roundPriceForAlpaca(currentPrice * (1 - limitSlippagePercent1)) // Sell slightly lower
|
|
5050
|
-
: this.roundPriceForAlpaca(currentPrice * (1 + limitSlippagePercent1)); // Buy slightly higher
|
|
5051
|
-
this.log(`Creating limit order to close ${position.symbol} position: ${side} ${qty} shares at $${limitPrice.toFixed(2)}`, {
|
|
5052
|
-
symbol: position.symbol,
|
|
5053
|
-
});
|
|
5054
|
-
await this.createLimitOrder(position.symbol, qty, side, limitPrice, positionIntent);
|
|
5055
|
-
}
|
|
5268
|
+
// Flatten each position independently. A missing quote or broker
|
|
5269
|
+
// rejection on one symbol must never abort the flatten of the others; any
|
|
5270
|
+
// per-position failure is surfaced as an aggregate error.
|
|
5271
|
+
await this.flattenPositionsWithLimitOrders(positions, false);
|
|
5056
5272
|
}
|
|
5057
5273
|
else {
|
|
5058
|
-
await this.makeRequest("/positions", "DELETE", undefined, options.cancel_orders ? "?cancel_orders=true" : "");
|
|
5274
|
+
const results = await this.makeRequest("/positions", "DELETE", undefined, options.cancel_orders ? "?cancel_orders=true" : "");
|
|
5275
|
+
if (Array.isArray(results)) {
|
|
5276
|
+
const failures = this.collectMultiStatusFailures(results);
|
|
5277
|
+
if (failures.length > 0) {
|
|
5278
|
+
const detail = failures.join(", ");
|
|
5279
|
+
this.log(`Error closing all positions: ${failures.length}/${results.length} positions failed to close (${detail})`, { type: "error" });
|
|
5280
|
+
throw new Error(`Failed to close ${failures.length} of ${results.length} positions: ${detail}`);
|
|
5281
|
+
}
|
|
5282
|
+
}
|
|
5059
5283
|
}
|
|
5060
5284
|
}
|
|
5061
5285
|
/**
|
|
@@ -5072,44 +5296,22 @@ class AlpacaTradingAPI {
|
|
|
5072
5296
|
this.log("No positions to close");
|
|
5073
5297
|
return;
|
|
5074
5298
|
}
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
//
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
const quote = quotesResponse.quotes[position.symbol];
|
|
5083
|
-
if (!quote) {
|
|
5084
|
-
this.log(`No quote available for ${position.symbol}, skipping limit order`, {
|
|
5085
|
-
symbol: position.symbol,
|
|
5086
|
-
type: "warn",
|
|
5087
|
-
});
|
|
5088
|
-
continue;
|
|
5089
|
-
}
|
|
5090
|
-
const qty = Math.abs(parseFloat(position.qty));
|
|
5091
|
-
const side = position.side === "long" ? "sell" : "buy";
|
|
5092
|
-
const positionIntent = side === "sell" ? "sell_to_close" : "buy_to_close";
|
|
5093
|
-
// Get the current price from the quote
|
|
5094
|
-
const currentPrice = side === "sell" ? quote.bp : quote.ap; // Use bid for sells, ask for buys
|
|
5095
|
-
if (!currentPrice) {
|
|
5096
|
-
this.log(`No valid price available for ${position.symbol}, skipping limit order`, {
|
|
5097
|
-
symbol: position.symbol,
|
|
5098
|
-
type: "warn",
|
|
5099
|
-
});
|
|
5100
|
-
continue;
|
|
5101
|
-
}
|
|
5102
|
-
// Apply slippage from config
|
|
5103
|
-
const limitSlippagePercent1 = limitPriceSlippagePercent100 / 100;
|
|
5104
|
-
const limitPrice = side === "sell"
|
|
5105
|
-
? this.roundPriceForAlpaca(currentPrice * (1 - limitSlippagePercent1)) // Sell slightly lower
|
|
5106
|
-
: this.roundPriceForAlpaca(currentPrice * (1 + limitSlippagePercent1)); // Buy slightly higher
|
|
5107
|
-
this.log(`Creating extended hours limit order to close ${position.symbol} position: ${side} ${qty} shares at $${limitPrice.toFixed(2)}`, {
|
|
5108
|
-
symbol: position.symbol,
|
|
5109
|
-
});
|
|
5110
|
-
await this.createLimitOrder(position.symbol, qty, side, limitPrice, positionIntent, true);
|
|
5299
|
+
// Cancelling stale open orders is secondary to the primary failsafe goal of
|
|
5300
|
+
// flattening positions. A cancel failure is logged but must not abort the
|
|
5301
|
+
// flatten, otherwise a single un-cancelable order would leave every position
|
|
5302
|
+
// open. The flatten step below surfaces its own aggregate failure.
|
|
5303
|
+
try {
|
|
5304
|
+
await this.cancelAllOrders();
|
|
5305
|
+
this.log(`Cancelled all open orders`);
|
|
5111
5306
|
}
|
|
5112
|
-
|
|
5307
|
+
catch (error) {
|
|
5308
|
+
this.log(`Proceeding to flatten despite cancelAllOrders failure: ${error instanceof Error ? error.message : String(error)}`, { type: "error" });
|
|
5309
|
+
}
|
|
5310
|
+
// Flatten each position independently with extended-hours limit orders. A
|
|
5311
|
+
// missing quote or broker rejection on one symbol must never silently leave
|
|
5312
|
+
// the remaining positions open; per-position failures are surfaced as an
|
|
5313
|
+
// aggregate error.
|
|
5314
|
+
await this.flattenPositionsWithLimitOrders(positions, true);
|
|
5113
5315
|
}
|
|
5114
5316
|
onTradeUpdate(callback) {
|
|
5115
5317
|
this.tradeUpdateCallback = callback;
|
|
@@ -5188,9 +5390,12 @@ class AlpacaTradingAPI {
|
|
|
5188
5390
|
* @param position_intent Position intent (buy_to_open, buy_to_close, sell_to_open, sell_to_close)
|
|
5189
5391
|
* @param type Order type (market or limit)
|
|
5190
5392
|
* @param limitPrice Limit price (required for limit orders)
|
|
5393
|
+
* @param clientOrderId Optional idempotency key; a deterministic one is
|
|
5394
|
+
* derived from the order parameters when omitted so a client-timeout retry
|
|
5395
|
+
* is de-duplicated broker-side.
|
|
5191
5396
|
* @returns The created order
|
|
5192
5397
|
*/
|
|
5193
|
-
async createOptionOrder(symbol, qty, side, position_intent, type, limitPrice) {
|
|
5398
|
+
async createOptionOrder(symbol, qty, side, position_intent, type, limitPrice, clientOrderId) {
|
|
5194
5399
|
if (!Number.isInteger(qty) || qty <= 0) {
|
|
5195
5400
|
this.log("Quantity must be a positive whole number for option orders", {
|
|
5196
5401
|
type: "error",
|
|
@@ -5215,6 +5420,19 @@ class AlpacaTradingAPI {
|
|
|
5215
5420
|
if (type === "limit" && limitPrice !== undefined) {
|
|
5216
5421
|
orderData.limit_price = this.roundPriceForAlpaca(limitPrice).toString();
|
|
5217
5422
|
}
|
|
5423
|
+
orderData.client_order_id =
|
|
5424
|
+
clientOrderId ??
|
|
5425
|
+
this.deriveClientOrderId([
|
|
5426
|
+
"option",
|
|
5427
|
+
type,
|
|
5428
|
+
symbol,
|
|
5429
|
+
side,
|
|
5430
|
+
position_intent,
|
|
5431
|
+
qty,
|
|
5432
|
+
type === "limit" && limitPrice !== undefined
|
|
5433
|
+
? this.roundPriceForAlpaca(limitPrice)
|
|
5434
|
+
: undefined,
|
|
5435
|
+
]);
|
|
5218
5436
|
return this.makeRequest("/orders", "POST", orderData);
|
|
5219
5437
|
}
|
|
5220
5438
|
/**
|
|
@@ -5223,9 +5441,12 @@ class AlpacaTradingAPI {
|
|
|
5223
5441
|
* @param qty Quantity of the multi-leg order (must be a whole number)
|
|
5224
5442
|
* @param type Order type (market or limit)
|
|
5225
5443
|
* @param limitPrice Limit price (required for limit orders)
|
|
5444
|
+
* @param clientOrderId Optional idempotency key; a deterministic one is
|
|
5445
|
+
* derived from the legs and order parameters when omitted so a
|
|
5446
|
+
* client-timeout retry is de-duplicated broker-side.
|
|
5226
5447
|
* @returns The created multi-leg order
|
|
5227
5448
|
*/
|
|
5228
|
-
async createMultiLegOptionOrder(legs, qty, type, limitPrice) {
|
|
5449
|
+
async createMultiLegOptionOrder(legs, qty, type, limitPrice, clientOrderId) {
|
|
5229
5450
|
if (!Number.isInteger(qty) || qty <= 0) {
|
|
5230
5451
|
this.log("Quantity must be a positive whole number for option orders", {
|
|
5231
5452
|
type: "error",
|
|
@@ -5251,6 +5472,17 @@ class AlpacaTradingAPI {
|
|
|
5251
5472
|
if (type === "limit" && limitPrice !== undefined) {
|
|
5252
5473
|
orderData.limit_price = this.roundPriceForAlpaca(limitPrice).toString();
|
|
5253
5474
|
}
|
|
5475
|
+
orderData.client_order_id =
|
|
5476
|
+
clientOrderId ??
|
|
5477
|
+
this.deriveClientOrderId([
|
|
5478
|
+
"mleg",
|
|
5479
|
+
type,
|
|
5480
|
+
qty,
|
|
5481
|
+
type === "limit" && limitPrice !== undefined
|
|
5482
|
+
? this.roundPriceForAlpaca(limitPrice)
|
|
5483
|
+
: undefined,
|
|
5484
|
+
...legs.map((leg) => `${leg.symbol}:${leg.side}:${leg.ratio_qty}:${leg.position_intent}`),
|
|
5485
|
+
]);
|
|
5254
5486
|
return this.makeRequest("/orders", "POST", orderData);
|
|
5255
5487
|
}
|
|
5256
5488
|
/**
|
|
@@ -5728,9 +5960,26 @@ class AlpacaTradingAPI {
|
|
|
5728
5960
|
extended_hours: extendedHours,
|
|
5729
5961
|
position_intent: side === "buy" ? "buy_to_open" : "sell_to_open",
|
|
5730
5962
|
};
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5963
|
+
orderData.client_order_id =
|
|
5964
|
+
clientOrderId ??
|
|
5965
|
+
this.deriveClientOrderId([
|
|
5966
|
+
"equities",
|
|
5967
|
+
orderClass,
|
|
5968
|
+
type,
|
|
5969
|
+
symbol,
|
|
5970
|
+
side,
|
|
5971
|
+
Math.abs(qty),
|
|
5972
|
+
type === "limit" && limitPrice !== undefined
|
|
5973
|
+
? this.roundPriceForAlpaca(limitPrice)
|
|
5974
|
+
: undefined,
|
|
5975
|
+
extendedHours,
|
|
5976
|
+
useStopLoss && calculatedStopPrice !== undefined
|
|
5977
|
+
? this.roundPriceForAlpaca(calculatedStopPrice)
|
|
5978
|
+
: undefined,
|
|
5979
|
+
useTakeProfit && calculatedTakeProfitPrice !== undefined
|
|
5980
|
+
? this.roundPriceForAlpaca(calculatedTakeProfitPrice)
|
|
5981
|
+
: undefined,
|
|
5982
|
+
]);
|
|
5734
5983
|
// Add limit price for limit orders
|
|
5735
5984
|
if (type === "limit" && limitPrice !== undefined) {
|
|
5736
5985
|
orderData.limit_price = this.roundPriceForAlpaca(limitPrice).toString();
|
|
@@ -8406,6 +8655,13 @@ var atrNs = /*#__PURE__*/Object.freeze({
|
|
|
8406
8655
|
});
|
|
8407
8656
|
|
|
8408
8657
|
const ALPACA_API_BASE = MARKET_DATA_API.CRYPTO;
|
|
8658
|
+
/**
|
|
8659
|
+
* Hard upper bound on the number of paginated news pages fetched in a single
|
|
8660
|
+
* {@link fetchNews} call. Acts as a runaway-loop backstop that is independent of
|
|
8661
|
+
* the caller-supplied `limit`, mirroring the max-page guard the equities
|
|
8662
|
+
* paginator already enforces.
|
|
8663
|
+
*/
|
|
8664
|
+
const MAX_NEWS_PAGES = 100;
|
|
8409
8665
|
/**
|
|
8410
8666
|
* Fetches cryptocurrency bars for the specified parameters.
|
|
8411
8667
|
* This function retrieves historical price data for multiple cryptocurrencies.
|
|
@@ -8505,17 +8761,25 @@ async function fetchNews(params, auth) {
|
|
|
8505
8761
|
include_content: includeContent.toString(),
|
|
8506
8762
|
limit: limit.toString(),
|
|
8507
8763
|
});
|
|
8508
|
-
const
|
|
8509
|
-
|
|
8510
|
-
|
|
8764
|
+
const authHeaders = {
|
|
8765
|
+
"APCA-API-KEY-ID": auth.APIKey,
|
|
8766
|
+
"APCA-API-SECRET-KEY": auth.APISecret,
|
|
8767
|
+
};
|
|
8768
|
+
const newsArticles = [];
|
|
8511
8769
|
let pageToken = null;
|
|
8512
|
-
let
|
|
8513
|
-
while (
|
|
8770
|
+
let pageCount = 0;
|
|
8771
|
+
while (pageCount < MAX_NEWS_PAGES) {
|
|
8772
|
+
// Rebuild the request URL on every iteration so the pagination cursor is
|
|
8773
|
+
// actually applied. Using `set` (not `append`) overwrites the previous
|
|
8774
|
+
// cursor instead of accumulating stale `page_token` values across pages.
|
|
8514
8775
|
if (pageToken) {
|
|
8515
|
-
queryParams.
|
|
8776
|
+
queryParams.set("page_token", pageToken);
|
|
8516
8777
|
}
|
|
8778
|
+
const url = `${ALPACA_API_BASE}/news?${queryParams.toString()}`;
|
|
8779
|
+
logIfDebug(`Fetching news from: ${url}`);
|
|
8517
8780
|
await withRetry(async () => {
|
|
8518
8781
|
const response = await fetch(url, {
|
|
8782
|
+
headers: authHeaders,
|
|
8519
8783
|
signal: createTimeoutSignal(DEFAULT_TIMEOUTS.ALPACA_API),
|
|
8520
8784
|
});
|
|
8521
8785
|
if (!response.ok) {
|
|
@@ -8523,7 +8787,7 @@ async function fetchNews(params, auth) {
|
|
|
8523
8787
|
throw new Error(`Alpaca API error (${response.status}): ${errorText}`);
|
|
8524
8788
|
}
|
|
8525
8789
|
const data = await response.json();
|
|
8526
|
-
|
|
8790
|
+
const pageArticles = (data.news ?? []).map((article) => ({
|
|
8527
8791
|
id: article.id,
|
|
8528
8792
|
author: article.author,
|
|
8529
8793
|
content: article.content,
|
|
@@ -8535,11 +8799,17 @@ async function fetchNews(params, auth) {
|
|
|
8535
8799
|
url: article.url,
|
|
8536
8800
|
symbols: article.symbols,
|
|
8537
8801
|
images: article.images,
|
|
8538
|
-
}))
|
|
8802
|
+
}));
|
|
8803
|
+
newsArticles.push(...pageArticles);
|
|
8539
8804
|
pageToken = data.next_page_token ?? null;
|
|
8540
|
-
|
|
8541
|
-
logIfDebug(`Received ${data.news.length} news articles. More pages: ${hasMorePages}`);
|
|
8805
|
+
logIfDebug(`Received ${pageArticles.length} news articles. Next page token: ${pageToken ? "present" : "none"}`);
|
|
8542
8806
|
}, API_RETRY_CONFIGS.CRYPTO, `Crypto.fetchNews(${symbol})`);
|
|
8807
|
+
pageCount++;
|
|
8808
|
+
// Terminate once the API reports no further pages or once we have
|
|
8809
|
+
// accumulated at least the requested number of articles.
|
|
8810
|
+
if (!pageToken || newsArticles.length >= limit) {
|
|
8811
|
+
break;
|
|
8812
|
+
}
|
|
8543
8813
|
}
|
|
8544
8814
|
// If sort is "asc" and limit is 10, return only the 10 most recent articles
|
|
8545
8815
|
if (sort === "asc" && limit === 10) {
|
|
@@ -9756,33 +10026,138 @@ const formatIndicesBarData = (data) => {
|
|
|
9756
10026
|
};
|
|
9757
10027
|
|
|
9758
10028
|
// price-utils.ts
|
|
10029
|
+
// ---------------------------------------------------------------------------
|
|
10030
|
+
// Transaction-cost (fee) model
|
|
10031
|
+
//
|
|
10032
|
+
// Alpaca's REST order object does not expose the realized per-order fee, so the
|
|
10033
|
+
// transaction cost is reconstructed from the published fee schedules, branching
|
|
10034
|
+
// on the order's ACTUAL asset class (never a hardcoded STOCK). Every rate is a
|
|
10035
|
+
// named constant sourced from Alpaca / SEC / FINRA public schedules (2024-2025)
|
|
10036
|
+
// so it can be audited and updated in one place.
|
|
10037
|
+
// ---------------------------------------------------------------------------
|
|
10038
|
+
/** Basis points in one whole unit (1 = 10,000 bps). */
|
|
10039
|
+
const BPS_PER_UNIT = 10_000;
|
|
10040
|
+
/** Shares represented by one US listed option contract. */
|
|
10041
|
+
const OPTIONS_CONTRACT_MULTIPLIER = 100;
|
|
10042
|
+
/**
|
|
10043
|
+
* SEC Section 31 fee, charged on the principal of SELL orders for equities and
|
|
10044
|
+
* options. FY2024+ rate: USD 8.00 per USD 1,000,000 of principal.
|
|
10045
|
+
*/
|
|
10046
|
+
const SEC_SECTION31_FEE_PER_USD = 8.0 / 1_000_000;
|
|
10047
|
+
/** FINRA Trading Activity Fee (TAF) for equity sells: USD per share sold. */
|
|
10048
|
+
const FINRA_TAF_EQUITY_PER_SHARE = 0.000166;
|
|
10049
|
+
/** FINRA TAF for option sells: USD per contract sold. */
|
|
10050
|
+
const FINRA_TAF_OPTIONS_PER_CONTRACT = 0.00279;
|
|
10051
|
+
/** FINRA TAF is capped per trade regardless of size. */
|
|
10052
|
+
const FINRA_TAF_MAX_PER_TRADE = 8.3;
|
|
10053
|
+
/** OCC clearing fee per option contract, capped per trade. */
|
|
10054
|
+
const OCC_CLEARING_FEE_PER_CONTRACT = 0.02;
|
|
10055
|
+
const OCC_CLEARING_FEE_MAX_PER_TRADE = 55.0;
|
|
10056
|
+
/**
|
|
10057
|
+
* Options Regulatory Fee (ORF) pass-through, charged on both sides, USD per
|
|
10058
|
+
* contract. Published, exchange-set pass-through rate.
|
|
10059
|
+
*/
|
|
10060
|
+
const OPTIONS_REGULATORY_FEE_PER_CONTRACT = 0.02685;
|
|
10061
|
+
/**
|
|
10062
|
+
* Alpaca crypto TAKER fee schedule as `[minTrailing30dVolumeUsd, takerBps]`,
|
|
10063
|
+
* ordered ascending by volume threshold. Market orders are takers; absent a
|
|
10064
|
+
* known trailing-30-day volume we conservatively select the tier-1 (highest)
|
|
10065
|
+
* taker rate. Source: Alpaca Crypto fee schedule.
|
|
10066
|
+
*/
|
|
10067
|
+
const ALPACA_CRYPTO_TAKER_FEE_TIERS_BPS = [
|
|
10068
|
+
[0, 25],
|
|
10069
|
+
[100_000, 22],
|
|
10070
|
+
[500_000, 20],
|
|
10071
|
+
[1_000_000, 18],
|
|
10072
|
+
[10_000_000, 15],
|
|
10073
|
+
[25_000_000, 13],
|
|
10074
|
+
[50_000_000, 12],
|
|
10075
|
+
[100_000_000, 10],
|
|
10076
|
+
];
|
|
10077
|
+
/**
|
|
10078
|
+
* Resolve the applicable Alpaca crypto taker fee (in bps) for a trailing
|
|
10079
|
+
* 30-day USD volume. Defaults to the tier-1 rate when the volume is unknown.
|
|
10080
|
+
* @param trailing30dVolumeUsd - Trailing 30-day traded notional in USD.
|
|
10081
|
+
* @returns The taker fee in basis points.
|
|
10082
|
+
*/
|
|
10083
|
+
function resolveCryptoTakerBps(trailing30dVolumeUsd) {
|
|
10084
|
+
let bps = ALPACA_CRYPTO_TAKER_FEE_TIERS_BPS[0][1];
|
|
10085
|
+
for (const [threshold, tierBps] of ALPACA_CRYPTO_TAKER_FEE_TIERS_BPS) {
|
|
10086
|
+
if (trailing30dVolumeUsd >= threshold) {
|
|
10087
|
+
bps = tierBps;
|
|
10088
|
+
}
|
|
10089
|
+
else {
|
|
10090
|
+
break;
|
|
10091
|
+
}
|
|
10092
|
+
}
|
|
10093
|
+
return bps;
|
|
10094
|
+
}
|
|
10095
|
+
/**
|
|
10096
|
+
* Computes the realized transaction cost (fees + regulatory charges) for the
|
|
10097
|
+
* Alpaca order backing a single {@link types.Action}, branching on the order's
|
|
10098
|
+
* actual asset class. Returns 0 only when there is genuinely no order to price
|
|
10099
|
+
* (no linked order id, order not found, or nothing filled) — never as a
|
|
10100
|
+
* fabricated success.
|
|
10101
|
+
* @param action - The action whose linked Alpaca order should be priced.
|
|
10102
|
+
* @param trade - The parent trade (supplies the Alpaca account id).
|
|
10103
|
+
* @param alpacaAccount - The Alpaca account supplying broker credentials.
|
|
10104
|
+
* @returns The total fee in account currency (USD).
|
|
10105
|
+
*/
|
|
9759
10106
|
const calculateFees = async (action, trade, alpacaAccount) => {
|
|
9760
|
-
let fee = 0;
|
|
9761
10107
|
const alpacaOrderId = action.alpacaOrderId;
|
|
9762
10108
|
if (!alpacaOrderId)
|
|
9763
|
-
return
|
|
10109
|
+
return 0;
|
|
9764
10110
|
const order = await getOrder$1({
|
|
9765
10111
|
adapticAccountId: trade.alpacaAccountId,
|
|
9766
10112
|
alpacaApiKey: alpacaAccount.APIKey,
|
|
9767
10113
|
alpacaApiSecret: alpacaAccount.APISecret,
|
|
9768
10114
|
}, alpacaOrderId);
|
|
9769
10115
|
if (!order)
|
|
9770
|
-
return
|
|
9771
|
-
const
|
|
9772
|
-
Number(order.
|
|
9773
|
-
order.notional || 0;
|
|
9774
|
-
Number(order.filled_avg_price || order.limit_price || order.stop_price) ||
|
|
10116
|
+
return 0;
|
|
10117
|
+
const filledQty = Number(order.filled_qty) || 0;
|
|
10118
|
+
const filledPrice = Number(order.filled_avg_price ?? order.limit_price ?? order.stop_price) ||
|
|
9775
10119
|
0;
|
|
9776
|
-
|
|
9777
|
-
|
|
9778
|
-
|
|
9779
|
-
|
|
9780
|
-
|
|
9781
|
-
|
|
9782
|
-
|
|
9783
|
-
|
|
10120
|
+
// Realized notional prefers the actual fill (qty * avg price); it falls back
|
|
10121
|
+
// to the order's notional field for dollar-notional (fractional) orders.
|
|
10122
|
+
const notional = filledQty > 0 && filledPrice > 0
|
|
10123
|
+
? filledQty * filledPrice
|
|
10124
|
+
: Number(order.notional ?? 0) || 0;
|
|
10125
|
+
if (notional <= 0)
|
|
10126
|
+
return 0;
|
|
10127
|
+
const isSell = order.side === "sell";
|
|
10128
|
+
switch (order.asset_class) {
|
|
10129
|
+
case "crypto": {
|
|
10130
|
+
// Crypto fees are bps of notional. Without a known 30-day volume we use
|
|
10131
|
+
// the conservative tier-1 taker rate.
|
|
10132
|
+
const takerBps = resolveCryptoTakerBps(0);
|
|
10133
|
+
return (notional * takerBps) / BPS_PER_UNIT;
|
|
10134
|
+
}
|
|
10135
|
+
case "us_option": {
|
|
10136
|
+
const contracts = filledQty > 0 ? filledQty : Number(order.qty) || 0;
|
|
10137
|
+
const occFee = Math.min(contracts * OCC_CLEARING_FEE_PER_CONTRACT, OCC_CLEARING_FEE_MAX_PER_TRADE);
|
|
10138
|
+
const orfFee = contracts * OPTIONS_REGULATORY_FEE_PER_CONTRACT;
|
|
10139
|
+
let fee = occFee + orfFee;
|
|
10140
|
+
if (isSell) {
|
|
10141
|
+
// Option premium is quoted per share; SEC fee applies to the full
|
|
10142
|
+
// principal (premium * contract multiplier).
|
|
10143
|
+
const optionPrincipal = notional * OPTIONS_CONTRACT_MULTIPLIER;
|
|
10144
|
+
const secFee = optionPrincipal * SEC_SECTION31_FEE_PER_USD;
|
|
10145
|
+
const taf = Math.min(contracts * FINRA_TAF_OPTIONS_PER_CONTRACT, FINRA_TAF_MAX_PER_TRADE);
|
|
10146
|
+
fee += secFee + taf;
|
|
10147
|
+
}
|
|
10148
|
+
return fee;
|
|
10149
|
+
}
|
|
10150
|
+
case "us_equity":
|
|
10151
|
+
default: {
|
|
10152
|
+
// Alpaca charges USD 0 commission on US equities; only sell-side
|
|
10153
|
+
// regulatory charges (SEC Section 31 + FINRA TAF) apply.
|
|
10154
|
+
if (!isSell)
|
|
10155
|
+
return 0;
|
|
10156
|
+
const secFee = notional * SEC_SECTION31_FEE_PER_USD;
|
|
10157
|
+
const taf = Math.min(filledQty * FINRA_TAF_EQUITY_PER_SHARE, FINRA_TAF_MAX_PER_TRADE);
|
|
10158
|
+
return secFee + taf;
|
|
10159
|
+
}
|
|
9784
10160
|
}
|
|
9785
|
-
return fee;
|
|
9786
10161
|
};
|
|
9787
10162
|
const computeTotalFees = async (trade) => {
|
|
9788
10163
|
let totalFees = 0;
|
|
@@ -10651,16 +11026,76 @@ async function calculateExpenseRatio({ accountId, client, alpacaAccount, }) {
|
|
|
10651
11026
|
return "N/A";
|
|
10652
11027
|
}
|
|
10653
11028
|
const equity = parseFloat(accountDetails.equity);
|
|
10654
|
-
// Fetch
|
|
10655
|
-
|
|
10656
|
-
//
|
|
11029
|
+
// Fetch the account's real trailing fee expenses from Alpaca account
|
|
11030
|
+
// activities. A genuine data-source failure yields "N/A" (unknown) rather
|
|
11031
|
+
// than a fabricated 0.00%.
|
|
11032
|
+
const auth = {
|
|
11033
|
+
adapticAccountId: alpacaAccountId,
|
|
11034
|
+
alpacaApiKey: alpacaAccount?.APIKey,
|
|
11035
|
+
alpacaApiSecret: alpacaAccount?.APISecret,
|
|
11036
|
+
};
|
|
11037
|
+
let expenses;
|
|
11038
|
+
try {
|
|
11039
|
+
expenses = await fetchTrailingFeeExpenses(auth);
|
|
11040
|
+
}
|
|
11041
|
+
catch (error) {
|
|
11042
|
+
getLogger().warn("Failed to fetch Alpaca account fee activities for expense ratio.", { error });
|
|
11043
|
+
return "N/A";
|
|
11044
|
+
}
|
|
11045
|
+
// Calculate expense ratio (trailing fees as a percentage of current equity).
|
|
10657
11046
|
const expenseRatio = (expenses / equity) * 100;
|
|
10658
11047
|
return `${expenseRatio.toFixed(2)}%`;
|
|
10659
11048
|
}
|
|
10660
|
-
|
|
10661
|
-
|
|
10662
|
-
|
|
10663
|
-
|
|
11049
|
+
/** Trailing window over which account fees are aggregated for the expense ratio. */
|
|
11050
|
+
const EXPENSE_TRAILING_WINDOW_DAYS = 365;
|
|
11051
|
+
/** Milliseconds in one day. */
|
|
11052
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
11053
|
+
/** Alpaca account-activity types that represent fees/regulatory charges. */
|
|
11054
|
+
const FEE_ACTIVITY_TYPES = "FEE,REG,CFEE";
|
|
11055
|
+
/** Page size for the paginated Alpaca account-activities endpoint. */
|
|
11056
|
+
const ACTIVITIES_PAGE_SIZE = 100;
|
|
11057
|
+
/** Hard cap on activity pages to bound pagination on unexpected responses. */
|
|
11058
|
+
const ACTIVITIES_MAX_PAGES = 1000;
|
|
11059
|
+
/**
|
|
11060
|
+
* Aggregates the account's fee/regulatory charges over the trailing window from
|
|
11061
|
+
* the Alpaca account-activities endpoint, following id-based pagination.
|
|
11062
|
+
* @param auth - Alpaca authentication (account id and/or direct API keys).
|
|
11063
|
+
* @returns Total fees in account currency (USD) as a positive number.
|
|
11064
|
+
*/
|
|
11065
|
+
async function fetchTrailingFeeExpenses(auth) {
|
|
11066
|
+
const after = new Date(Date.now() - EXPENSE_TRAILING_WINDOW_DAYS * MS_PER_DAY).toISOString();
|
|
11067
|
+
let total = 0;
|
|
11068
|
+
let pageToken;
|
|
11069
|
+
for (let page = 0; page < ACTIVITIES_MAX_PAGES; page++) {
|
|
11070
|
+
const queryParams = new URLSearchParams({
|
|
11071
|
+
activity_types: FEE_ACTIVITY_TYPES,
|
|
11072
|
+
after,
|
|
11073
|
+
page_size: String(ACTIVITIES_PAGE_SIZE),
|
|
11074
|
+
});
|
|
11075
|
+
if (pageToken) {
|
|
11076
|
+
queryParams.append("page_token", pageToken);
|
|
11077
|
+
}
|
|
11078
|
+
const activities = await makeRequest(auth, {
|
|
11079
|
+
endpoint: "/account/activities",
|
|
11080
|
+
method: "GET",
|
|
11081
|
+
queryString: `?${queryParams.toString()}`,
|
|
11082
|
+
});
|
|
11083
|
+
if (!Array.isArray(activities) || activities.length === 0) {
|
|
11084
|
+
break;
|
|
11085
|
+
}
|
|
11086
|
+
for (const activity of activities) {
|
|
11087
|
+
const amount = parseFloat(activity.net_amount ?? "");
|
|
11088
|
+
if (Number.isFinite(amount)) {
|
|
11089
|
+
// Fee entries are debits (negative net_amount); accumulate magnitude.
|
|
11090
|
+
total += Math.abs(amount);
|
|
11091
|
+
}
|
|
11092
|
+
}
|
|
11093
|
+
if (activities.length < ACTIVITIES_PAGE_SIZE) {
|
|
11094
|
+
break;
|
|
11095
|
+
}
|
|
11096
|
+
pageToken = activities[activities.length - 1].id;
|
|
11097
|
+
}
|
|
11098
|
+
return total;
|
|
10664
11099
|
}
|
|
10665
11100
|
/**
|
|
10666
11101
|
* Calculates the liquidity ratio for a given Alpaca account.
|
|
@@ -11320,6 +11755,58 @@ async function calculateInformationRatio(portfolioHistory, benchmarkBars) {
|
|
|
11320
11755
|
}
|
|
11321
11756
|
return informationRatio.toFixed(4);
|
|
11322
11757
|
}
|
|
11758
|
+
/**
|
|
11759
|
+
* Maps a portfolio-history timeframe token to the Alpaca market-data
|
|
11760
|
+
* {@link TimeFrame} accepted by the historical-bars endpoint. Benchmark
|
|
11761
|
+
* comparison is daily by default when no timeframe is supplied.
|
|
11762
|
+
* @param timeframe - The portfolio-history timeframe token.
|
|
11763
|
+
* @returns The equivalent Alpaca market-data timeframe.
|
|
11764
|
+
*/
|
|
11765
|
+
function toAlpacaTimeFrame(timeframe) {
|
|
11766
|
+
switch (timeframe) {
|
|
11767
|
+
case "1Min":
|
|
11768
|
+
return "1Min";
|
|
11769
|
+
case "5Min":
|
|
11770
|
+
return "5Min";
|
|
11771
|
+
case "15Min":
|
|
11772
|
+
return "15Min";
|
|
11773
|
+
case "1H":
|
|
11774
|
+
return "1Hour";
|
|
11775
|
+
case "1D":
|
|
11776
|
+
return "1Day";
|
|
11777
|
+
default:
|
|
11778
|
+
return "1Day";
|
|
11779
|
+
}
|
|
11780
|
+
}
|
|
11781
|
+
/** Milliseconds per second, for RFC-3339 → Unix-second conversion. */
|
|
11782
|
+
const MS_PER_SECOND = 1000;
|
|
11783
|
+
/**
|
|
11784
|
+
* Fetches benchmark OHLCV bars from the wrapped Alpaca market-data vendor and
|
|
11785
|
+
* maps them into {@link BenchmarkBar}s (Unix-second timestamp + close price)
|
|
11786
|
+
* expected by the alpha/beta/information-ratio calculators.
|
|
11787
|
+
* @param request - Benchmark symbol, RFC-3339 start/end, and timeframe token.
|
|
11788
|
+
* @returns The benchmark bars, sorted ascending by time; empty if none.
|
|
11789
|
+
*/
|
|
11790
|
+
async function fetchBenchmarkBars(request) {
|
|
11791
|
+
const { symbol, start, end, timeframe } = request;
|
|
11792
|
+
const response = await marketDataAPI.getHistoricalBars({
|
|
11793
|
+
symbols: [symbol],
|
|
11794
|
+
timeframe: toAlpacaTimeFrame(timeframe),
|
|
11795
|
+
start,
|
|
11796
|
+
end,
|
|
11797
|
+
sort: "asc",
|
|
11798
|
+
});
|
|
11799
|
+
const bars = response.bars[symbol];
|
|
11800
|
+
if (!Array.isArray(bars) || bars.length === 0) {
|
|
11801
|
+
return [];
|
|
11802
|
+
}
|
|
11803
|
+
return bars
|
|
11804
|
+
.map((bar) => ({
|
|
11805
|
+
t: Math.floor(new Date(bar.t).getTime() / MS_PER_SECOND),
|
|
11806
|
+
c: bar.c,
|
|
11807
|
+
}))
|
|
11808
|
+
.filter((bar) => Number.isFinite(bar.t) && Number.isFinite(bar.c));
|
|
11809
|
+
}
|
|
11323
11810
|
/**
|
|
11324
11811
|
* Fetches performance metrics for a given Alpaca account.
|
|
11325
11812
|
* @param params - The parameters for fetching performance metrics.
|
|
@@ -11384,7 +11871,10 @@ async function fetchPerformanceMetrics({ params, client, accountId, alpacaAccoun
|
|
|
11384
11871
|
getLogger().error("[fetchPerformanceMetrics] Error fetching portfolio history:", error);
|
|
11385
11872
|
throw new Error("Failed to retrieve portfolio history data");
|
|
11386
11873
|
}
|
|
11387
|
-
// Fetch benchmark data
|
|
11874
|
+
// Fetch benchmark data directly from the wrapped Alpaca market-data vendor.
|
|
11875
|
+
// (Previously this hit a relative "/api/market-data/historical-prices"
|
|
11876
|
+
// Next.js route that only resolves in a browser; in a Node/engine runtime
|
|
11877
|
+
// the relative fetch always threw, silently zeroing out alpha/beta/IR.)
|
|
11388
11878
|
const benchmarkSymbol = "SPY";
|
|
11389
11879
|
let benchmarkBars = [];
|
|
11390
11880
|
try {
|
|
@@ -11395,24 +11885,16 @@ async function fetchPerformanceMetrics({ params, client, accountId, alpacaAccoun
|
|
|
11395
11885
|
: params?.period
|
|
11396
11886
|
? params?.period
|
|
11397
11887
|
: "1Y",
|
|
11398
|
-
outputFormat: "
|
|
11888
|
+
outputFormat: "iso",
|
|
11399
11889
|
intraday_reporting: params?.intraday_reporting,
|
|
11400
11890
|
});
|
|
11401
|
-
|
|
11402
|
-
|
|
11403
|
-
|
|
11404
|
-
|
|
11405
|
-
|
|
11406
|
-
signal: createTimeoutSignal(DEFAULT_TIMEOUTS.GENERAL),
|
|
11891
|
+
benchmarkBars = await fetchBenchmarkBars({
|
|
11892
|
+
symbol: benchmarkSymbol,
|
|
11893
|
+
start: String(start),
|
|
11894
|
+
end: String(end),
|
|
11895
|
+
timeframe: params.timeframe,
|
|
11407
11896
|
});
|
|
11408
|
-
if (
|
|
11409
|
-
const errorText = await response.text();
|
|
11410
|
-
throw new Error(`Failed to fetch benchmark data: ${response.statusText} - ${errorText}`);
|
|
11411
|
-
}
|
|
11412
|
-
benchmarkBars = await response.json();
|
|
11413
|
-
if (!benchmarkBars ||
|
|
11414
|
-
!Array.isArray(benchmarkBars) ||
|
|
11415
|
-
benchmarkBars.length === 0) {
|
|
11897
|
+
if (benchmarkBars.length === 0) {
|
|
11416
11898
|
throw new Error("Received empty or invalid benchmark data");
|
|
11417
11899
|
}
|
|
11418
11900
|
}
|
|
@@ -14079,17 +14561,17 @@ var hasRequiredBrowser;
|
|
|
14079
14561
|
function requireBrowser () {
|
|
14080
14562
|
if (hasRequiredBrowser) return browser.exports;
|
|
14081
14563
|
hasRequiredBrowser = 1;
|
|
14082
|
-
(function (module, exports) {
|
|
14564
|
+
(function (module, exports$1) {
|
|
14083
14565
|
/**
|
|
14084
14566
|
* This is the web browser implementation of `debug()`.
|
|
14085
14567
|
*/
|
|
14086
14568
|
|
|
14087
|
-
exports.formatArgs = formatArgs;
|
|
14088
|
-
exports.save = save;
|
|
14089
|
-
exports.load = load;
|
|
14090
|
-
exports.useColors = useColors;
|
|
14091
|
-
exports.storage = localstorage();
|
|
14092
|
-
exports.destroy = (() => {
|
|
14569
|
+
exports$1.formatArgs = formatArgs;
|
|
14570
|
+
exports$1.save = save;
|
|
14571
|
+
exports$1.load = load;
|
|
14572
|
+
exports$1.useColors = useColors;
|
|
14573
|
+
exports$1.storage = localstorage();
|
|
14574
|
+
exports$1.destroy = (() => {
|
|
14093
14575
|
let warned = false;
|
|
14094
14576
|
|
|
14095
14577
|
return () => {
|
|
@@ -14104,7 +14586,7 @@ function requireBrowser () {
|
|
|
14104
14586
|
* Colors.
|
|
14105
14587
|
*/
|
|
14106
14588
|
|
|
14107
|
-
exports.colors = [
|
|
14589
|
+
exports$1.colors = [
|
|
14108
14590
|
'#0000CC',
|
|
14109
14591
|
'#0000FF',
|
|
14110
14592
|
'#0033CC',
|
|
@@ -14269,7 +14751,7 @@ function requireBrowser () {
|
|
|
14269
14751
|
*
|
|
14270
14752
|
* @api public
|
|
14271
14753
|
*/
|
|
14272
|
-
exports.log = console.debug || console.log || (() => {});
|
|
14754
|
+
exports$1.log = console.debug || console.log || (() => {});
|
|
14273
14755
|
|
|
14274
14756
|
/**
|
|
14275
14757
|
* Save `namespaces`.
|
|
@@ -14280,9 +14762,9 @@ function requireBrowser () {
|
|
|
14280
14762
|
function save(namespaces) {
|
|
14281
14763
|
try {
|
|
14282
14764
|
if (namespaces) {
|
|
14283
|
-
exports.storage.setItem('debug', namespaces);
|
|
14765
|
+
exports$1.storage.setItem('debug', namespaces);
|
|
14284
14766
|
} else {
|
|
14285
|
-
exports.storage.removeItem('debug');
|
|
14767
|
+
exports$1.storage.removeItem('debug');
|
|
14286
14768
|
}
|
|
14287
14769
|
} catch (error) {
|
|
14288
14770
|
// Swallow
|
|
@@ -14299,7 +14781,7 @@ function requireBrowser () {
|
|
|
14299
14781
|
function load() {
|
|
14300
14782
|
let r;
|
|
14301
14783
|
try {
|
|
14302
|
-
r = exports.storage.getItem('debug') || exports.storage.getItem('DEBUG') ;
|
|
14784
|
+
r = exports$1.storage.getItem('debug') || exports$1.storage.getItem('DEBUG') ;
|
|
14303
14785
|
} catch (error) {
|
|
14304
14786
|
// Swallow
|
|
14305
14787
|
// XXX (@Qix-) should we be logging these?
|
|
@@ -14335,7 +14817,7 @@ function requireBrowser () {
|
|
|
14335
14817
|
}
|
|
14336
14818
|
}
|
|
14337
14819
|
|
|
14338
|
-
module.exports = requireCommon()(exports);
|
|
14820
|
+
module.exports = requireCommon()(exports$1);
|
|
14339
14821
|
|
|
14340
14822
|
const {formatters} = module.exports;
|
|
14341
14823
|
|
|
@@ -14524,7 +15006,7 @@ var hasRequiredNode$1;
|
|
|
14524
15006
|
function requireNode$1 () {
|
|
14525
15007
|
if (hasRequiredNode$1) return node$1.exports;
|
|
14526
15008
|
hasRequiredNode$1 = 1;
|
|
14527
|
-
(function (module, exports) {
|
|
15009
|
+
(function (module, exports$1) {
|
|
14528
15010
|
const tty = require$$1$1;
|
|
14529
15011
|
const util = require$$1$2;
|
|
14530
15012
|
|
|
@@ -14532,13 +15014,13 @@ function requireNode$1 () {
|
|
|
14532
15014
|
* This is the Node.js implementation of `debug()`.
|
|
14533
15015
|
*/
|
|
14534
15016
|
|
|
14535
|
-
exports.init = init;
|
|
14536
|
-
exports.log = log;
|
|
14537
|
-
exports.formatArgs = formatArgs;
|
|
14538
|
-
exports.save = save;
|
|
14539
|
-
exports.load = load;
|
|
14540
|
-
exports.useColors = useColors;
|
|
14541
|
-
exports.destroy = util.deprecate(
|
|
15017
|
+
exports$1.init = init;
|
|
15018
|
+
exports$1.log = log;
|
|
15019
|
+
exports$1.formatArgs = formatArgs;
|
|
15020
|
+
exports$1.save = save;
|
|
15021
|
+
exports$1.load = load;
|
|
15022
|
+
exports$1.useColors = useColors;
|
|
15023
|
+
exports$1.destroy = util.deprecate(
|
|
14542
15024
|
() => {},
|
|
14543
15025
|
'Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.'
|
|
14544
15026
|
);
|
|
@@ -14547,7 +15029,7 @@ function requireNode$1 () {
|
|
|
14547
15029
|
* Colors.
|
|
14548
15030
|
*/
|
|
14549
15031
|
|
|
14550
|
-
exports.colors = [6, 2, 3, 4, 5, 1];
|
|
15032
|
+
exports$1.colors = [6, 2, 3, 4, 5, 1];
|
|
14551
15033
|
|
|
14552
15034
|
try {
|
|
14553
15035
|
// Optional dependency (as in, doesn't need to be installed, NOT like optionalDependencies in package.json)
|
|
@@ -14555,7 +15037,7 @@ function requireNode$1 () {
|
|
|
14555
15037
|
const supportsColor = requireSupportsColor();
|
|
14556
15038
|
|
|
14557
15039
|
if (supportsColor && (supportsColor.stderr || supportsColor).level >= 2) {
|
|
14558
|
-
exports.colors = [
|
|
15040
|
+
exports$1.colors = [
|
|
14559
15041
|
20,
|
|
14560
15042
|
21,
|
|
14561
15043
|
26,
|
|
@@ -14644,7 +15126,7 @@ function requireNode$1 () {
|
|
|
14644
15126
|
* $ DEBUG_COLORS=no DEBUG_DEPTH=10 DEBUG_SHOW_HIDDEN=enabled node script.js
|
|
14645
15127
|
*/
|
|
14646
15128
|
|
|
14647
|
-
exports.inspectOpts = Object.keys(process.env).filter(key => {
|
|
15129
|
+
exports$1.inspectOpts = Object.keys(process.env).filter(key => {
|
|
14648
15130
|
return /^debug_/i.test(key);
|
|
14649
15131
|
}).reduce((obj, key) => {
|
|
14650
15132
|
// Camel-case
|
|
@@ -14676,8 +15158,8 @@ function requireNode$1 () {
|
|
|
14676
15158
|
*/
|
|
14677
15159
|
|
|
14678
15160
|
function useColors() {
|
|
14679
|
-
return 'colors' in exports.inspectOpts ?
|
|
14680
|
-
Boolean(exports.inspectOpts.colors) :
|
|
15161
|
+
return 'colors' in exports$1.inspectOpts ?
|
|
15162
|
+
Boolean(exports$1.inspectOpts.colors) :
|
|
14681
15163
|
tty.isatty(process.stderr.fd);
|
|
14682
15164
|
}
|
|
14683
15165
|
|
|
@@ -14703,7 +15185,7 @@ function requireNode$1 () {
|
|
|
14703
15185
|
}
|
|
14704
15186
|
|
|
14705
15187
|
function getDate() {
|
|
14706
|
-
if (exports.inspectOpts.hideDate) {
|
|
15188
|
+
if (exports$1.inspectOpts.hideDate) {
|
|
14707
15189
|
return '';
|
|
14708
15190
|
}
|
|
14709
15191
|
return new Date().toISOString() + ' ';
|
|
@@ -14714,7 +15196,7 @@ function requireNode$1 () {
|
|
|
14714
15196
|
*/
|
|
14715
15197
|
|
|
14716
15198
|
function log(...args) {
|
|
14717
|
-
return process.stderr.write(util.formatWithOptions(exports.inspectOpts, ...args) + '\n');
|
|
15199
|
+
return process.stderr.write(util.formatWithOptions(exports$1.inspectOpts, ...args) + '\n');
|
|
14718
15200
|
}
|
|
14719
15201
|
|
|
14720
15202
|
/**
|
|
@@ -14754,13 +15236,13 @@ function requireNode$1 () {
|
|
|
14754
15236
|
function init(debug) {
|
|
14755
15237
|
debug.inspectOpts = {};
|
|
14756
15238
|
|
|
14757
|
-
const keys = Object.keys(exports.inspectOpts);
|
|
15239
|
+
const keys = Object.keys(exports$1.inspectOpts);
|
|
14758
15240
|
for (let i = 0; i < keys.length; i++) {
|
|
14759
|
-
debug.inspectOpts[keys[i]] = exports.inspectOpts[keys[i]];
|
|
15241
|
+
debug.inspectOpts[keys[i]] = exports$1.inspectOpts[keys[i]];
|
|
14760
15242
|
}
|
|
14761
15243
|
}
|
|
14762
15244
|
|
|
14763
|
-
module.exports = requireCommon()(exports);
|
|
15245
|
+
module.exports = requireCommon()(exports$1);
|
|
14764
15246
|
|
|
14765
15247
|
const {formatters} = module.exports;
|
|
14766
15248
|
|
|
@@ -15334,7 +15816,7 @@ function requireFollowRedirects () {
|
|
|
15334
15816
|
// Wraps the key/value object of protocols with redirect functionality
|
|
15335
15817
|
function wrap(protocols) {
|
|
15336
15818
|
// Default settings
|
|
15337
|
-
var exports = {
|
|
15819
|
+
var exports$1 = {
|
|
15338
15820
|
maxRedirects: 21,
|
|
15339
15821
|
maxBodyLength: 10 * 1024 * 1024,
|
|
15340
15822
|
};
|
|
@@ -15344,7 +15826,7 @@ function requireFollowRedirects () {
|
|
|
15344
15826
|
Object.keys(protocols).forEach(function (scheme) {
|
|
15345
15827
|
var protocol = scheme + ":";
|
|
15346
15828
|
var nativeProtocol = nativeProtocols[protocol] = protocols[scheme];
|
|
15347
|
-
var wrappedProtocol = exports[scheme] = Object.create(nativeProtocol);
|
|
15829
|
+
var wrappedProtocol = exports$1[scheme] = Object.create(nativeProtocol);
|
|
15348
15830
|
|
|
15349
15831
|
// Executes a request, following redirects
|
|
15350
15832
|
function request(input, options, callback) {
|
|
@@ -15367,8 +15849,8 @@ function requireFollowRedirects () {
|
|
|
15367
15849
|
|
|
15368
15850
|
// Set defaults
|
|
15369
15851
|
options = Object.assign({
|
|
15370
|
-
maxRedirects: exports.maxRedirects,
|
|
15371
|
-
maxBodyLength: exports.maxBodyLength,
|
|
15852
|
+
maxRedirects: exports$1.maxRedirects,
|
|
15853
|
+
maxBodyLength: exports$1.maxBodyLength,
|
|
15372
15854
|
}, input, options);
|
|
15373
15855
|
options.nativeProtocols = nativeProtocols;
|
|
15374
15856
|
if (!isString(options.host) && !isString(options.hostname)) {
|
|
@@ -15393,7 +15875,7 @@ function requireFollowRedirects () {
|
|
|
15393
15875
|
get: { value: get, configurable: true, enumerable: true, writable: true },
|
|
15394
15876
|
});
|
|
15395
15877
|
});
|
|
15396
|
-
return exports;
|
|
15878
|
+
return exports$1;
|
|
15397
15879
|
}
|
|
15398
15880
|
|
|
15399
15881
|
function noop() { /* empty */ }
|
|
@@ -16999,7 +17481,7 @@ var hasRequiredLodash;
|
|
|
16999
17481
|
function requireLodash () {
|
|
17000
17482
|
if (hasRequiredLodash) return lodash$1.exports;
|
|
17001
17483
|
hasRequiredLodash = 1;
|
|
17002
|
-
(function (module, exports) {
|
|
17484
|
+
(function (module, exports$1) {
|
|
17003
17485
|
(function() {
|
|
17004
17486
|
|
|
17005
17487
|
/** Used as a safe reference for `undefined` in pre-ES5 environments. */
|
|
@@ -17430,7 +17912,7 @@ function requireLodash () {
|
|
|
17430
17912
|
var root = freeGlobal || freeSelf || Function('return this')();
|
|
17431
17913
|
|
|
17432
17914
|
/** Detect free variable `exports`. */
|
|
17433
|
-
var freeExports = exports && !exports.nodeType && exports;
|
|
17915
|
+
var freeExports = exports$1 && !exports$1.nodeType && exports$1;
|
|
17434
17916
|
|
|
17435
17917
|
/** Detect free variable `module`. */
|
|
17436
17918
|
var freeModule = freeExports && 'object' == 'object' && module && !module.nodeType && module;
|
|
@@ -35260,12 +35742,12 @@ var hasRequiredIsBuffer;
|
|
|
35260
35742
|
function requireIsBuffer () {
|
|
35261
35743
|
if (hasRequiredIsBuffer) return isBuffer.exports;
|
|
35262
35744
|
hasRequiredIsBuffer = 1;
|
|
35263
|
-
(function (module, exports) {
|
|
35745
|
+
(function (module, exports$1) {
|
|
35264
35746
|
var root = require_root(),
|
|
35265
35747
|
stubFalse = requireStubFalse();
|
|
35266
35748
|
|
|
35267
35749
|
/** Detect free variable `exports`. */
|
|
35268
|
-
var freeExports = exports && !exports.nodeType && exports;
|
|
35750
|
+
var freeExports = exports$1 && !exports$1.nodeType && exports$1;
|
|
35269
35751
|
|
|
35270
35752
|
/** Detect free variable `module`. */
|
|
35271
35753
|
var freeModule = freeExports && 'object' == 'object' && module && !module.nodeType && module;
|
|
@@ -35485,11 +35967,11 @@ var hasRequired_nodeUtil;
|
|
|
35485
35967
|
function require_nodeUtil () {
|
|
35486
35968
|
if (hasRequired_nodeUtil) return _nodeUtil.exports;
|
|
35487
35969
|
hasRequired_nodeUtil = 1;
|
|
35488
|
-
(function (module, exports) {
|
|
35970
|
+
(function (module, exports$1) {
|
|
35489
35971
|
var freeGlobal = require_freeGlobal();
|
|
35490
35972
|
|
|
35491
35973
|
/** Detect free variable `exports`. */
|
|
35492
|
-
var freeExports = exports && !exports.nodeType && exports;
|
|
35974
|
+
var freeExports = exports$1 && !exports$1.nodeType && exports$1;
|
|
35493
35975
|
|
|
35494
35976
|
/** Detect free variable `module`. */
|
|
35495
35977
|
var freeModule = freeExports && 'object' == 'object' && module && !module.nodeType && module;
|
|
@@ -44411,7 +44893,7 @@ var hasRequiredSafeBuffer;
|
|
|
44411
44893
|
function requireSafeBuffer () {
|
|
44412
44894
|
if (hasRequiredSafeBuffer) return safeBuffer.exports;
|
|
44413
44895
|
hasRequiredSafeBuffer = 1;
|
|
44414
|
-
(function (module, exports) {
|
|
44896
|
+
(function (module, exports$1) {
|
|
44415
44897
|
/* eslint-disable node/no-deprecated-api */
|
|
44416
44898
|
var buffer = require$$0$5;
|
|
44417
44899
|
var Buffer = buffer.Buffer;
|
|
@@ -44426,8 +44908,8 @@ function requireSafeBuffer () {
|
|
|
44426
44908
|
module.exports = buffer;
|
|
44427
44909
|
} else {
|
|
44428
44910
|
// Copy properties from require('buffer')
|
|
44429
|
-
copyProps(buffer, exports);
|
|
44430
|
-
exports.Buffer = SafeBuffer;
|
|
44911
|
+
copyProps(buffer, exports$1);
|
|
44912
|
+
exports$1.Buffer = SafeBuffer;
|
|
44431
44913
|
}
|
|
44432
44914
|
|
|
44433
44915
|
function SafeBuffer (arg, encodingOrOffset, length) {
|
|
@@ -47625,22 +48107,22 @@ var hasRequiredReadable;
|
|
|
47625
48107
|
function requireReadable () {
|
|
47626
48108
|
if (hasRequiredReadable) return readable.exports;
|
|
47627
48109
|
hasRequiredReadable = 1;
|
|
47628
|
-
(function (module, exports) {
|
|
48110
|
+
(function (module, exports$1) {
|
|
47629
48111
|
var Stream = require$$0$3;
|
|
47630
48112
|
if (process.env.READABLE_STREAM === 'disable' && Stream) {
|
|
47631
48113
|
module.exports = Stream.Readable;
|
|
47632
48114
|
Object.assign(module.exports, Stream);
|
|
47633
48115
|
module.exports.Stream = Stream;
|
|
47634
48116
|
} else {
|
|
47635
|
-
exports = module.exports = require_stream_readable();
|
|
47636
|
-
exports.Stream = Stream || exports;
|
|
47637
|
-
exports.Readable = exports;
|
|
47638
|
-
exports.Writable = require_stream_writable();
|
|
47639
|
-
exports.Duplex = require_stream_duplex();
|
|
47640
|
-
exports.Transform = require_stream_transform();
|
|
47641
|
-
exports.PassThrough = require_stream_passthrough();
|
|
47642
|
-
exports.finished = requireEndOfStream();
|
|
47643
|
-
exports.pipeline = requirePipeline();
|
|
48117
|
+
exports$1 = module.exports = require_stream_readable();
|
|
48118
|
+
exports$1.Stream = Stream || exports$1;
|
|
48119
|
+
exports$1.Readable = exports$1;
|
|
48120
|
+
exports$1.Writable = require_stream_writable();
|
|
48121
|
+
exports$1.Duplex = require_stream_duplex();
|
|
48122
|
+
exports$1.Transform = require_stream_transform();
|
|
48123
|
+
exports$1.PassThrough = require_stream_passthrough();
|
|
48124
|
+
exports$1.finished = requireEndOfStream();
|
|
48125
|
+
exports$1.pipeline = requirePipeline();
|
|
47644
48126
|
}
|
|
47645
48127
|
} (readable, readable.exports));
|
|
47646
48128
|
return readable.exports;
|
|
@@ -49075,12 +49557,12 @@ var hasRequiredWebsocket;
|
|
|
49075
49557
|
function requireWebsocket () {
|
|
49076
49558
|
if (hasRequiredWebsocket) return websocket$1;
|
|
49077
49559
|
hasRequiredWebsocket = 1;
|
|
49078
|
-
(function (exports) {
|
|
49560
|
+
(function (exports$1) {
|
|
49079
49561
|
var __importDefault = (websocket$1 && websocket$1.__importDefault) || function (mod) {
|
|
49080
49562
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
49081
49563
|
};
|
|
49082
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49083
|
-
exports.AlpacaWebsocket = exports.ERROR = exports.CONN_ERROR = exports.EVENT = exports.STATE = void 0;
|
|
49564
|
+
Object.defineProperty(exports$1, "__esModule", { value: true });
|
|
49565
|
+
exports$1.AlpacaWebsocket = exports$1.ERROR = exports$1.CONN_ERROR = exports$1.EVENT = exports$1.STATE = void 0;
|
|
49084
49566
|
const events_1 = __importDefault(require$$0$4);
|
|
49085
49567
|
const ws_1 = __importDefault(requireWs());
|
|
49086
49568
|
const msgpack5_1 = __importDefault(requireMsgpack5());
|
|
@@ -49094,7 +49576,7 @@ function requireWebsocket () {
|
|
|
49094
49576
|
STATE["DISCONNECTED"] = "disconnected";
|
|
49095
49577
|
STATE["WAITING_TO_CONNECT"] = "waiting to connect";
|
|
49096
49578
|
STATE["WAITING_TO_RECONNECT"] = "waiting to reconnect";
|
|
49097
|
-
})(STATE || (exports.STATE = STATE = {}));
|
|
49579
|
+
})(STATE || (exports$1.STATE = STATE = {}));
|
|
49098
49580
|
// Client events
|
|
49099
49581
|
var EVENT;
|
|
49100
49582
|
(function (EVENT) {
|
|
@@ -49113,9 +49595,9 @@ function requireWebsocket () {
|
|
|
49113
49595
|
EVENT["CORRECTIONS"] = "corrections";
|
|
49114
49596
|
EVENT["ORDERBOOKS"] = "orderbooks";
|
|
49115
49597
|
EVENT["NEWS"] = "news";
|
|
49116
|
-
})(EVENT || (exports.EVENT = EVENT = {}));
|
|
49598
|
+
})(EVENT || (exports$1.EVENT = EVENT = {}));
|
|
49117
49599
|
// Connection errors by code
|
|
49118
|
-
exports.CONN_ERROR = new Map([
|
|
49600
|
+
exports$1.CONN_ERROR = new Map([
|
|
49119
49601
|
[400, "invalid syntax"],
|
|
49120
49602
|
[401, "not authenticated"],
|
|
49121
49603
|
[402, "auth failed"],
|
|
@@ -49134,7 +49616,7 @@ function requireWebsocket () {
|
|
|
49134
49616
|
ERROR["MISSING_SECERT_KEY"] = "missing secret key";
|
|
49135
49617
|
ERROR["MISSING_API_KEY"] = "missing api key";
|
|
49136
49618
|
ERROR["UNEXPECTED_MESSAGE"] = "unexpected message";
|
|
49137
|
-
})(ERROR || (exports.ERROR = ERROR = {}));
|
|
49619
|
+
})(ERROR || (exports$1.ERROR = ERROR = {}));
|
|
49138
49620
|
class AlpacaWebsocket extends events_1.default.EventEmitter {
|
|
49139
49621
|
constructor(options) {
|
|
49140
49622
|
super();
|
|
@@ -49291,7 +49773,7 @@ function requireWebsocket () {
|
|
|
49291
49773
|
this.updateSubscriptions(data[0]);
|
|
49292
49774
|
break;
|
|
49293
49775
|
case "error":
|
|
49294
|
-
this.emit(EVENT.CLIENT_ERROR, exports.CONN_ERROR.get(data[0].code));
|
|
49776
|
+
this.emit(EVENT.CLIENT_ERROR, exports$1.CONN_ERROR.get(data[0].code));
|
|
49295
49777
|
break;
|
|
49296
49778
|
default:
|
|
49297
49779
|
this.dataHandler(data);
|
|
@@ -49320,7 +49802,7 @@ function requireWebsocket () {
|
|
|
49320
49802
|
}
|
|
49321
49803
|
}
|
|
49322
49804
|
}
|
|
49323
|
-
exports.AlpacaWebsocket = AlpacaWebsocket;
|
|
49805
|
+
exports$1.AlpacaWebsocket = AlpacaWebsocket;
|
|
49324
49806
|
} (websocket$1));
|
|
49325
49807
|
return websocket$1;
|
|
49326
49808
|
}
|
|
@@ -49954,8 +50436,8 @@ var hasRequiredWebsockets;
|
|
|
49954
50436
|
function requireWebsockets () {
|
|
49955
50437
|
if (hasRequiredWebsockets) return websockets;
|
|
49956
50438
|
hasRequiredWebsockets = 1;
|
|
49957
|
-
(function (exports) {
|
|
49958
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50439
|
+
(function (exports$1) {
|
|
50440
|
+
Object.defineProperty(exports$1, "__esModule", { value: true });
|
|
49959
50441
|
const events = require$$0$4;
|
|
49960
50442
|
const WebSocket = requireWs();
|
|
49961
50443
|
const entity = requireEntity();
|
|
@@ -49970,7 +50452,7 @@ function requireWebsockets () {
|
|
|
49970
50452
|
STATE.DISCONNECTED = "disconnected";
|
|
49971
50453
|
STATE.WAITING_TO_CONNECT = "waiting to connect";
|
|
49972
50454
|
STATE.WAITING_TO_RECONNECT = "waiting to reconnect";
|
|
49973
|
-
})((STATE = exports.STATE || (exports.STATE = {})));
|
|
50455
|
+
})((STATE = exports$1.STATE || (exports$1.STATE = {})));
|
|
49974
50456
|
// Client events
|
|
49975
50457
|
var EVENT;
|
|
49976
50458
|
(function (EVENT) {
|
|
@@ -49984,7 +50466,7 @@ function requireWebsockets () {
|
|
|
49984
50466
|
EVENT.STOCK_QUOTES = "stock_quotes";
|
|
49985
50467
|
EVENT.STOCK_AGG_SEC = "stock_agg_sec";
|
|
49986
50468
|
EVENT.STOCK_AGG_MIN = "stock_agg_min";
|
|
49987
|
-
})((EVENT = exports.EVENT || (exports.EVENT = {})));
|
|
50469
|
+
})((EVENT = exports$1.EVENT || (exports$1.EVENT = {})));
|
|
49988
50470
|
// Connection errors Each of these will also emit EVENT.ERROR
|
|
49989
50471
|
var ERROR;
|
|
49990
50472
|
(function (ERROR) {
|
|
@@ -49993,7 +50475,7 @@ function requireWebsockets () {
|
|
|
49993
50475
|
ERROR.MISSING_API_KEY = "missing api key";
|
|
49994
50476
|
ERROR.MISSING_SECRET_KEY = "missing secret key";
|
|
49995
50477
|
ERROR.UNKNOWN = "unknown error";
|
|
49996
|
-
})((ERROR = exports.ERROR || (exports.ERROR = {})));
|
|
50478
|
+
})((ERROR = exports$1.ERROR || (exports$1.ERROR = {})));
|
|
49997
50479
|
/**
|
|
49998
50480
|
* AlpacaStreamClient manages a connection to Alpaca's websocket api
|
|
49999
50481
|
*/
|
|
@@ -50306,7 +50788,7 @@ function requireWebsockets () {
|
|
|
50306
50788
|
}
|
|
50307
50789
|
}
|
|
50308
50790
|
}
|
|
50309
|
-
exports.AlpacaStreamClient = AlpacaStreamClient;
|
|
50791
|
+
exports$1.AlpacaStreamClient = AlpacaStreamClient;
|
|
50310
50792
|
} (websockets));
|
|
50311
50793
|
return websockets;
|
|
50312
50794
|
}
|
|
@@ -50558,13 +51040,13 @@ var hasRequiredDist;
|
|
|
50558
51040
|
function requireDist () {
|
|
50559
51041
|
if (hasRequiredDist) return dist$1.exports;
|
|
50560
51042
|
hasRequiredDist = 1;
|
|
50561
|
-
(function (module, exports) {
|
|
51043
|
+
(function (module, exports$1) {
|
|
50562
51044
|
var __importDefault = (dist && dist.__importDefault) || function (mod) {
|
|
50563
51045
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
50564
51046
|
};
|
|
50565
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51047
|
+
Object.defineProperty(exports$1, "__esModule", { value: true });
|
|
50566
51048
|
const alpaca_trade_api_1 = __importDefault(requireAlpacaTradeApi());
|
|
50567
|
-
exports.default = alpaca_trade_api_1.default;
|
|
51049
|
+
exports$1.default = alpaca_trade_api_1.default;
|
|
50568
51050
|
module.exports = alpaca_trade_api_1.default;
|
|
50569
51051
|
} (dist$1, dist$1.exports));
|
|
50570
51052
|
return dist$1.exports;
|
|
@@ -62673,6 +63155,7 @@ class StampedeProtectedCache {
|
|
|
62673
63155
|
coalescedRequests: 0,
|
|
62674
63156
|
backgroundRefreshes: 0,
|
|
62675
63157
|
refreshErrors: 0,
|
|
63158
|
+
loadTimeouts: 0,
|
|
62676
63159
|
};
|
|
62677
63160
|
constructor(options) {
|
|
62678
63161
|
this.options = {
|
|
@@ -62946,6 +63429,7 @@ class StampedeProtectedCache {
|
|
|
62946
63429
|
coalescedRequests: this.stats.coalescedRequests,
|
|
62947
63430
|
backgroundRefreshes: this.stats.backgroundRefreshes,
|
|
62948
63431
|
refreshErrors: this.stats.refreshErrors,
|
|
63432
|
+
loadTimeouts: this.stats.loadTimeouts,
|
|
62949
63433
|
};
|
|
62950
63434
|
}
|
|
62951
63435
|
/**
|
|
@@ -62996,8 +63480,9 @@ class StampedeProtectedCache {
|
|
|
62996
63480
|
this.options.logger.debug("Request coalesced", { key });
|
|
62997
63481
|
return existingPromise;
|
|
62998
63482
|
}
|
|
62999
|
-
// Create new promise and store it
|
|
63000
|
-
|
|
63483
|
+
// Create new promise and store it — bounded by loadTimeoutMs so a
|
|
63484
|
+
// never-settling loader cannot pin the key (see loadWithTimeout).
|
|
63485
|
+
const promise = this.loadWithTimeout(key, loader, ttl);
|
|
63001
63486
|
this.pendingRefreshes.set(key, promise);
|
|
63002
63487
|
try {
|
|
63003
63488
|
const result = await promise;
|
|
@@ -63008,6 +63493,51 @@ class StampedeProtectedCache {
|
|
|
63008
63493
|
this.pendingRefreshes.delete(key);
|
|
63009
63494
|
}
|
|
63010
63495
|
}
|
|
63496
|
+
/**
|
|
63497
|
+
* Race {@link loadAndCache} against the configured load timeout.
|
|
63498
|
+
*
|
|
63499
|
+
* On timeout: rejects with a typed timeout error, evicts the pending
|
|
63500
|
+
* single-flight entry so the NEXT caller retries with a fresh loader
|
|
63501
|
+
* (instead of coalescing onto the hung one), and attaches a settlement
|
|
63502
|
+
* observer to the abandoned loader so its eventual resolution/rejection
|
|
63503
|
+
* is logged rather than surfacing as an unhandled rejection.
|
|
63504
|
+
*/
|
|
63505
|
+
async loadWithTimeout(key, loader, ttl) {
|
|
63506
|
+
const timeoutMs = this.options.loadTimeoutMs ?? 30000;
|
|
63507
|
+
const loadPromise = this.loadAndCache(key, loader, ttl);
|
|
63508
|
+
let timeoutHandle;
|
|
63509
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
63510
|
+
timeoutHandle = setTimeout(() => {
|
|
63511
|
+
this.stats.loadTimeouts++;
|
|
63512
|
+
// Evict the pin so the next caller retries fresh.
|
|
63513
|
+
this.pendingRefreshes.delete(key);
|
|
63514
|
+
this.options.logger.warn("Cache loader timed out — pin evicted", {
|
|
63515
|
+
key,
|
|
63516
|
+
timeoutMs,
|
|
63517
|
+
});
|
|
63518
|
+
// Observe the abandoned loader; never let it become unhandled.
|
|
63519
|
+
loadPromise
|
|
63520
|
+
.then(() => {
|
|
63521
|
+
this.options.logger.warn("Abandoned cache loader eventually resolved", { key });
|
|
63522
|
+
})
|
|
63523
|
+
.catch((error) => {
|
|
63524
|
+
this.options.logger.warn("Abandoned cache loader eventually rejected", {
|
|
63525
|
+
key,
|
|
63526
|
+
error: error instanceof Error ? error.message : String(error),
|
|
63527
|
+
});
|
|
63528
|
+
});
|
|
63529
|
+
reject(new Error(`StampedeProtectedCache loader timed out after ${timeoutMs}ms for key "${key}"`));
|
|
63530
|
+
}, timeoutMs);
|
|
63531
|
+
});
|
|
63532
|
+
try {
|
|
63533
|
+
return await Promise.race([loadPromise, timeoutPromise]);
|
|
63534
|
+
}
|
|
63535
|
+
finally {
|
|
63536
|
+
if (timeoutHandle !== undefined) {
|
|
63537
|
+
clearTimeout(timeoutHandle);
|
|
63538
|
+
}
|
|
63539
|
+
}
|
|
63540
|
+
}
|
|
63011
63541
|
/**
|
|
63012
63542
|
* Load data and cache it
|
|
63013
63543
|
*/
|
|
@@ -63157,6 +63687,7 @@ const DEFAULT_CACHE_OPTIONS = {
|
|
|
63157
63687
|
minJitter: 0.9, // 90%
|
|
63158
63688
|
maxJitter: 1.1, // 110%
|
|
63159
63689
|
enableBackgroundRefresh: true,
|
|
63690
|
+
loadTimeoutMs: 30000, // 30s hard loader ceiling (anti-pinning)
|
|
63160
63691
|
};
|
|
63161
63692
|
|
|
63162
63693
|
/**
|