@adaptic/utils 0.0.1006 → 0.0.1008
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 +586 -177
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +587 -179
- package/dist/index.mjs.map +1 -1
- package/dist/test.js +120 -9
- package/dist/test.js.map +1 -1
- package/dist/types/__tests__/alpaca-client-order-id.test.d.ts +2 -0
- package/dist/types/__tests__/alpaca-client-order-id.test.d.ts.map +1 -0
- package/dist/types/__tests__/alpaca-market-data-retry.test.d.ts +2 -0
- package/dist/types/__tests__/alpaca-market-data-retry.test.d.ts.map +1 -0
- package/dist/types/__tests__/performance-metrics-fees.test.d.ts +2 -0
- package/dist/types/__tests__/performance-metrics-fees.test.d.ts.map +1 -0
- package/dist/types/__tests__/price-utils-fees.test.d.ts +2 -0
- package/dist/types/__tests__/price-utils-fees.test.d.ts.map +1 -0
- package/dist/types/alpaca-market-data-api.d.ts.map +1 -1
- package/dist/types/alpaca-trading-api.d.ts +93 -8
- package/dist/types/alpaca-trading-api.d.ts.map +1 -1
- package/dist/types/cache/stampede-protected-cache.d.ts +19 -1
- package/dist/types/cache/stampede-protected-cache.d.ts.map +1 -1
- package/dist/types/errors/index.d.ts +25 -0
- package/dist/types/errors/index.d.ts.map +1 -1
- package/dist/types/index.d.ts +5 -5
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/performance-metrics.d.ts +31 -1
- package/dist/types/performance-metrics.d.ts.map +1 -1
- package/dist/types/price-utils.d.ts +12 -0
- package/dist/types/price-utils.d.ts.map +1 -1
- package/dist/types/rate-limiter.d.ts.map +1 -1
- package/dist/types/utils/retry.d.ts +14 -0
- package/dist/types/utils/retry.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -2276,6 +2276,80 @@ function isTransientNetworkError(error) {
|
|
|
2276
2276
|
}
|
|
2277
2277
|
return false;
|
|
2278
2278
|
}
|
|
2279
|
+
/**
|
|
2280
|
+
* Error names that indicate the CLIENT's own request deadline expired (an
|
|
2281
|
+
* `AbortSignal` timeout or an undici per-phase timeout) rather than a
|
|
2282
|
+
* connection-phase fault. These faults have already consumed a full request
|
|
2283
|
+
* timeout, so retrying them is expensive by construction.
|
|
2284
|
+
*/
|
|
2285
|
+
const DEADLINE_EXPIRY_ERROR_NAMES = new Set([
|
|
2286
|
+
"AbortError",
|
|
2287
|
+
"TimeoutError",
|
|
2288
|
+
"RequestTimeoutError",
|
|
2289
|
+
"ConnectTimeoutError",
|
|
2290
|
+
"HeadersTimeoutError",
|
|
2291
|
+
"BodyTimeoutError",
|
|
2292
|
+
]);
|
|
2293
|
+
/**
|
|
2294
|
+
* Error codes that indicate an expired request/phase deadline (vs a fast
|
|
2295
|
+
* connection-phase fault such as `ECONNRESET`/`ECONNREFUSED`).
|
|
2296
|
+
*/
|
|
2297
|
+
const DEADLINE_EXPIRY_ERROR_CODES = new Set([
|
|
2298
|
+
"ETIMEDOUT",
|
|
2299
|
+
"ESOCKETTIMEDOUT",
|
|
2300
|
+
"ECONNABORTED",
|
|
2301
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
2302
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
2303
|
+
"UND_ERR_BODY_TIMEOUT",
|
|
2304
|
+
]);
|
|
2305
|
+
/** Message-pattern fallback for deadline-expiry errors that lost name/code. */
|
|
2306
|
+
const DEADLINE_EXPIRY_MESSAGE_PATTERNS = [
|
|
2307
|
+
/timed out/i,
|
|
2308
|
+
/timeout/i,
|
|
2309
|
+
/aborted/i,
|
|
2310
|
+
];
|
|
2311
|
+
/**
|
|
2312
|
+
* Whether an error represents the client's OWN deadline expiring (abort /
|
|
2313
|
+
* timeout) rather than a connection-phase network fault. Both classes are
|
|
2314
|
+
* "transient" per {@link isTransientNetworkError}, but they have very
|
|
2315
|
+
* different retry economics: a connection fault (`ECONNRESET`, `EPIPE`,
|
|
2316
|
+
* refused socket) settles in milliseconds and is cheap to retry, while a
|
|
2317
|
+
* deadline expiry has already consumed the full per-attempt timeout — blindly
|
|
2318
|
+
* retrying it multiplies time-to-failure exactly when the caller most needs
|
|
2319
|
+
* to fail fast. Walks the `error.cause` chain like the transient classifier.
|
|
2320
|
+
*
|
|
2321
|
+
* @param error - The error to classify.
|
|
2322
|
+
* @returns true when the fault is a client deadline/abort expiry.
|
|
2323
|
+
*/
|
|
2324
|
+
function isClientDeadlineExpiry(error) {
|
|
2325
|
+
const MAX_CAUSE_DEPTH = 6;
|
|
2326
|
+
let current = error;
|
|
2327
|
+
for (let depth = 0; depth < MAX_CAUSE_DEPTH && current; depth++) {
|
|
2328
|
+
if (current instanceof Error || typeof current === "object") {
|
|
2329
|
+
const err = current;
|
|
2330
|
+
if (typeof err.name === "string" &&
|
|
2331
|
+
DEADLINE_EXPIRY_ERROR_NAMES.has(err.name)) {
|
|
2332
|
+
return true;
|
|
2333
|
+
}
|
|
2334
|
+
if (typeof err.code === "string" &&
|
|
2335
|
+
DEADLINE_EXPIRY_ERROR_CODES.has(err.code)) {
|
|
2336
|
+
return true;
|
|
2337
|
+
}
|
|
2338
|
+
if (typeof err.message === "string") {
|
|
2339
|
+
for (const pattern of DEADLINE_EXPIRY_MESSAGE_PATTERNS) {
|
|
2340
|
+
if (pattern.test(err.message)) {
|
|
2341
|
+
return true;
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
current = err.cause;
|
|
2346
|
+
}
|
|
2347
|
+
else {
|
|
2348
|
+
break;
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
return false;
|
|
2352
|
+
}
|
|
2279
2353
|
/**
|
|
2280
2354
|
* Analyzes an error and determines if it's retryable.
|
|
2281
2355
|
* @param error - The error to analyze
|
|
@@ -2803,6 +2877,33 @@ class DataFormatError extends AdapticUtilsError {
|
|
|
2803
2877
|
this.service = service;
|
|
2804
2878
|
}
|
|
2805
2879
|
}
|
|
2880
|
+
/**
|
|
2881
|
+
* Broker-side duplicate `client_order_id` rejection (Alpaca HTTP 422,
|
|
2882
|
+
* "client order id must be unique").
|
|
2883
|
+
*
|
|
2884
|
+
* Thrown by the order-creation paths of `AlpacaTradingAPI` so callers can
|
|
2885
|
+
* distinguish "this exact order was already submitted" from a genuine order
|
|
2886
|
+
* rejection. When {@link wasDerived} is `false` the id was caller-supplied and
|
|
2887
|
+
* the caller owns idempotency semantics (a legitimate repeat needs a new
|
|
2888
|
+
* explicit id or an `idempotencyNonce`). When `true`, the wrapper's automatic
|
|
2889
|
+
* recovery (existing-order lookup, then one salted resubmit) was exhausted.
|
|
2890
|
+
*
|
|
2891
|
+
* Never retryable with the same id — resubmitting the identical
|
|
2892
|
+
* `client_order_id` will 422 again.
|
|
2893
|
+
*/
|
|
2894
|
+
class DuplicateClientOrderIdError extends AlpacaApiError {
|
|
2895
|
+
clientOrderId;
|
|
2896
|
+
wasDerived;
|
|
2897
|
+
constructor(message,
|
|
2898
|
+
/** The `client_order_id` that collided broker-side. */
|
|
2899
|
+
clientOrderId,
|
|
2900
|
+
/** Whether the colliding id was derived by the wrapper (vs caller-supplied). */
|
|
2901
|
+
wasDerived, cause) {
|
|
2902
|
+
super(message, "DUPLICATE_CLIENT_ORDER_ID", 422, cause);
|
|
2903
|
+
this.clientOrderId = clientOrderId;
|
|
2904
|
+
this.wasDerived = wasDerived;
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2806
2907
|
|
|
2807
2908
|
/**
|
|
2808
2909
|
* Token bucket rate limiter for external API integrations
|
|
@@ -2900,8 +3001,11 @@ class TokenBucketRateLimiter {
|
|
|
2900
3001
|
async acquire() {
|
|
2901
3002
|
const logger = getLogger();
|
|
2902
3003
|
this.refill();
|
|
2903
|
-
|
|
2904
|
-
|
|
3004
|
+
// Require a WHOLE token: refill() accrues fractionally, and admitting on
|
|
3005
|
+
// any positive fraction would release a full request per accrual tick,
|
|
3006
|
+
// driving the bucket negative and overrunning the configured rate.
|
|
3007
|
+
if (this.tokens >= TOKENS_PER_REQUEST) {
|
|
3008
|
+
this.tokens -= TOKENS_PER_REQUEST;
|
|
2905
3009
|
logger.debug(`Rate limit token acquired for ${this.config.label}`, {
|
|
2906
3010
|
remainingTokens: this.tokens,
|
|
2907
3011
|
queueLength: this.queue.length,
|
|
@@ -2997,8 +3101,9 @@ class TokenBucketRateLimiter {
|
|
|
2997
3101
|
this.processingQueue = true;
|
|
2998
3102
|
const logger = getLogger();
|
|
2999
3103
|
try {
|
|
3000
|
-
|
|
3001
|
-
|
|
3104
|
+
// Whole-token admission — see the matching guard in acquire().
|
|
3105
|
+
while (this.queue.length > 0 && this.tokens >= TOKENS_PER_REQUEST) {
|
|
3106
|
+
this.tokens -= TOKENS_PER_REQUEST;
|
|
3002
3107
|
const next = this.queue.shift();
|
|
3003
3108
|
if (next) {
|
|
3004
3109
|
clearTimeout(next.timeoutHandle);
|
|
@@ -3160,6 +3265,23 @@ function transientRetryDelayMs(attempt) {
|
|
|
3160
3265
|
const ceiling = TRANSIENT_NETWORK_RETRY_BASE_MS * 2 ** attempt;
|
|
3161
3266
|
return Math.floor(Math.random() * ceiling);
|
|
3162
3267
|
}
|
|
3268
|
+
/**
|
|
3269
|
+
* Total-deadline multiple over the per-attempt client timeout for one
|
|
3270
|
+
* {@link AlpacaMarketDataAPI.makeRequest} call INCLUDING transient retries.
|
|
3271
|
+
* The ECONNRESET class this loop targets settles in milliseconds, so the
|
|
3272
|
+
* retries fit comfortably inside 1.5x the single-attempt timeout — while a
|
|
3273
|
+
* request whose attempts each consume the full client timeout is refused
|
|
3274
|
+
* further retries instead of stretching a hot-path read to a multi-minute
|
|
3275
|
+
* stall (worst case before this budget: 3 x (60s limiter wait + 30s fetch)).
|
|
3276
|
+
*/
|
|
3277
|
+
const TRANSIENT_RETRY_BUDGET_TIMEOUT_MULTIPLE = 1.5;
|
|
3278
|
+
/**
|
|
3279
|
+
* Maximum retries for a fault classified as the client's OWN deadline expiry
|
|
3280
|
+
* (see {@link isClientDeadlineExpiry}): such a fault already consumed a full
|
|
3281
|
+
* per-attempt timeout, so it is retried at most once — and then only if the
|
|
3282
|
+
* total budget still allows a further full-length attempt.
|
|
3283
|
+
*/
|
|
3284
|
+
const CLIENT_DEADLINE_EXPIRY_MAX_RETRIES = 1;
|
|
3163
3285
|
const log$l = (message, options = { type: "info" }) => {
|
|
3164
3286
|
log$m(message, { ...options, source: "AlpacaMarketDataAPI" });
|
|
3165
3287
|
};
|
|
@@ -3673,7 +3795,15 @@ class AlpacaMarketDataAPI extends require$$0$1.EventEmitter {
|
|
|
3673
3795
|
// Retry ONLY transient connection faults, and only on GET (every
|
|
3674
3796
|
// market-data read here is idempotent). A non-2xx response is a real
|
|
3675
3797
|
// answer from Alpaca and is never retried — that path still throws on
|
|
3676
|
-
// the first attempt exactly as before.
|
|
3798
|
+
// the first attempt exactly as before. The whole loop is bounded by a
|
|
3799
|
+
// cumulative deadline so retries can never stretch a hot-path read far
|
|
3800
|
+
// beyond a single attempt's timeout: connection-phase faults
|
|
3801
|
+
// (ECONNRESET class, millisecond-scale) retry cheaply, while a fault
|
|
3802
|
+
// that consumed the full client timeout retries at most once and only
|
|
3803
|
+
// when the remaining budget still fits a full-length attempt.
|
|
3804
|
+
const retryLoopStartedAt = Date.now();
|
|
3805
|
+
const totalBudgetMs = Math.round(DEFAULT_TIMEOUTS.ALPACA_API * TRANSIENT_RETRY_BUDGET_TIMEOUT_MULTIPLE);
|
|
3806
|
+
let deadlineExpiryRetries = 0;
|
|
3677
3807
|
let response;
|
|
3678
3808
|
let lastNetworkError;
|
|
3679
3809
|
for (let attempt = 0; attempt < TRANSIENT_NETWORK_RETRY_ATTEMPTS; attempt += 1) {
|
|
@@ -3687,12 +3817,21 @@ class AlpacaMarketDataAPI extends require$$0$1.EventEmitter {
|
|
|
3687
3817
|
}
|
|
3688
3818
|
catch (networkErr) {
|
|
3689
3819
|
lastNetworkError = networkErr;
|
|
3820
|
+
const deadlineExpiry = isClientDeadlineExpiry(networkErr);
|
|
3821
|
+
const nextAttemptFitsBudget = Date.now() - retryLoopStartedAt + DEFAULT_TIMEOUTS.ALPACA_API <=
|
|
3822
|
+
totalBudgetMs;
|
|
3690
3823
|
const retryable = method === "GET" &&
|
|
3691
3824
|
isTransientNetworkError(networkErr) &&
|
|
3692
|
-
attempt < TRANSIENT_NETWORK_RETRY_ATTEMPTS - 1
|
|
3825
|
+
attempt < TRANSIENT_NETWORK_RETRY_ATTEMPTS - 1 &&
|
|
3826
|
+
nextAttemptFitsBudget &&
|
|
3827
|
+
(!deadlineExpiry ||
|
|
3828
|
+
deadlineExpiryRetries < CLIENT_DEADLINE_EXPIRY_MAX_RETRIES);
|
|
3693
3829
|
if (!retryable) {
|
|
3694
3830
|
throw networkErr;
|
|
3695
3831
|
}
|
|
3832
|
+
if (deadlineExpiry) {
|
|
3833
|
+
deadlineExpiryRetries += 1;
|
|
3834
|
+
}
|
|
3696
3835
|
const delayMs = transientRetryDelayMs(attempt);
|
|
3697
3836
|
log$l(`Transient network fault on ${endpoint} (attempt ${attempt + 1}/${TRANSIENT_NETWORK_RETRY_ATTEMPTS}); retrying in ${delayMs}ms`, { type: "warn" });
|
|
3698
3837
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
@@ -3713,8 +3852,7 @@ class AlpacaMarketDataAPI extends require$$0$1.EventEmitter {
|
|
|
3713
3852
|
});
|
|
3714
3853
|
throw new Error(`Market Data API error (${response.status}): ${errorText}`);
|
|
3715
3854
|
}
|
|
3716
|
-
|
|
3717
|
-
return data;
|
|
3855
|
+
return (await response.json());
|
|
3718
3856
|
}
|
|
3719
3857
|
catch (err) {
|
|
3720
3858
|
const error = err;
|
|
@@ -4401,7 +4539,7 @@ class AlpacaMarketDataAPI extends require$$0$1.EventEmitter {
|
|
|
4401
4539
|
});
|
|
4402
4540
|
throw new Error(`Alpaca news API error (${response.status}): ${errorText}`);
|
|
4403
4541
|
}
|
|
4404
|
-
const data = await response.json();
|
|
4542
|
+
const data = (await response.json());
|
|
4405
4543
|
if (!data.news || !Array.isArray(data.news)) {
|
|
4406
4544
|
log$l(`No news data found in Alpaca response for ${symbol}`, {
|
|
4407
4545
|
type: "warn",
|
|
@@ -4484,9 +4622,45 @@ const CLIENT_ORDER_ID_HASH_LENGTH = 32;
|
|
|
4484
4622
|
* This derived default is a best-effort safety net; the guaranteed-idempotent
|
|
4485
4623
|
* path is for the caller to pass an explicit `clientOrderId` tied to the
|
|
4486
4624
|
* originating signal/decision id (which also permits legitimately-repeated
|
|
4487
|
-
* identical orders inside a single window).
|
|
4625
|
+
* identical orders inside a single window). Callers that intentionally repeat
|
|
4626
|
+
* an identical order inside one window without managing explicit ids can pass
|
|
4627
|
+
* an `idempotencyNonce` (e.g. an attempt counter or signal id) instead — the
|
|
4628
|
+
* nonce is folded into the derived id, so each distinct nonce yields a
|
|
4629
|
+
* distinct id while a timeout+retry of the SAME attempt still collides
|
|
4630
|
+
* broker-side as intended.
|
|
4631
|
+
*
|
|
4632
|
+
* When a DERIVED id is 422-rejected as a duplicate, {@link
|
|
4633
|
+
* AlpacaTradingAPI.postOrderWithIdempotencyRecovery} recovers instead of
|
|
4634
|
+
* failing the caller: if the previously-submitted order is still live (or
|
|
4635
|
+
* filled) it is returned as idempotent success; if it is terminally dead
|
|
4636
|
+
* (canceled/expired/rejected) the order is resubmitted exactly once with a
|
|
4637
|
+
* fresh random salt. Caller-SUPPLIED ids are never recovered — they surface a
|
|
4638
|
+
* typed {@link DuplicateClientOrderIdError} so the caller can distinguish a
|
|
4639
|
+
* duplicate from a genuine rejection.
|
|
4488
4640
|
*/
|
|
4489
4641
|
const CLIENT_ORDER_ID_WINDOW_MS = 300_000;
|
|
4642
|
+
/**
|
|
4643
|
+
* Matches Alpaca's 422 duplicate-idempotency-key rejection message. Alpaca has
|
|
4644
|
+
* used both "client_order_id must be unique" and "client order id must be
|
|
4645
|
+
* unique" across API revisions, so separators are matched loosely.
|
|
4646
|
+
*/
|
|
4647
|
+
const DUPLICATE_CLIENT_ORDER_ID_PATTERN = /client[\s_-]?order[\s_-]?id must be unique/i;
|
|
4648
|
+
/** HTTP status Alpaca uses for duplicate `client_order_id` rejections. */
|
|
4649
|
+
const DUPLICATE_CLIENT_ORDER_ID_STATUS = 422;
|
|
4650
|
+
/**
|
|
4651
|
+
* Order statuses in which a previously-submitted order can never execute.
|
|
4652
|
+
* A derived-id duplicate colliding with an order in one of these states is a
|
|
4653
|
+
* legitimate NEW order (e.g. cancel-then-recreate of an identical trailing
|
|
4654
|
+
* stop) and is resubmitted with a fresh salt; any other status means the
|
|
4655
|
+
* original order is live or executed, so it is returned as idempotent success.
|
|
4656
|
+
*/
|
|
4657
|
+
const TERMINAL_DEAD_ORDER_STATUSES = new Set([
|
|
4658
|
+
"canceled",
|
|
4659
|
+
"expired",
|
|
4660
|
+
"rejected",
|
|
4661
|
+
"replaced",
|
|
4662
|
+
"done_for_day",
|
|
4663
|
+
]);
|
|
4490
4664
|
/**
|
|
4491
4665
|
Websocket example
|
|
4492
4666
|
const alpacaAPI = createAlpacaTradingAPI(credentials); // type AlpacaCredentials
|
|
@@ -4594,6 +4768,142 @@ class AlpacaTradingAPI {
|
|
|
4594
4768
|
.slice(0, CLIENT_ORDER_ID_HASH_LENGTH);
|
|
4595
4769
|
return `${CLIENT_ORDER_ID_PREFIX}${digest}`;
|
|
4596
4770
|
}
|
|
4771
|
+
/**
|
|
4772
|
+
* Running count of derived-id duplicate collisions resolved by returning the
|
|
4773
|
+
* already-submitted order. Emitted in log metadata so operators can see the
|
|
4774
|
+
* idempotency net firing.
|
|
4775
|
+
*/
|
|
4776
|
+
idempotentDuplicateReturns = 0;
|
|
4777
|
+
/**
|
|
4778
|
+
* Running count of derived-id duplicate collisions resolved by resubmitting
|
|
4779
|
+
* once with a fresh random salt (the colliding order was terminally dead).
|
|
4780
|
+
*/
|
|
4781
|
+
saltedDuplicateResubmits = 0;
|
|
4782
|
+
/**
|
|
4783
|
+
* Whether an error thrown by {@link makeRequest} is Alpaca's 422
|
|
4784
|
+
* duplicate-`client_order_id` rejection.
|
|
4785
|
+
* @param error - The error thrown by the order POST.
|
|
4786
|
+
* @returns true when the error is a duplicate-idempotency-key rejection.
|
|
4787
|
+
*/
|
|
4788
|
+
isDuplicateClientOrderIdRejection(error) {
|
|
4789
|
+
if (!(error instanceof Error))
|
|
4790
|
+
return false;
|
|
4791
|
+
return (error.message.includes(`(${DUPLICATE_CLIENT_ORDER_ID_STATUS})`) &&
|
|
4792
|
+
DUPLICATE_CLIENT_ORDER_ID_PATTERN.test(error.message));
|
|
4793
|
+
}
|
|
4794
|
+
/**
|
|
4795
|
+
* Look up an order by its `client_order_id` (Alpaca
|
|
4796
|
+
* `GET /orders:by_client_order_id`).
|
|
4797
|
+
* @param clientOrderId - The idempotency key the order was submitted with.
|
|
4798
|
+
* @returns The order, or null when no order exists for the id (404).
|
|
4799
|
+
*/
|
|
4800
|
+
async getOrderByClientOrderId(clientOrderId) {
|
|
4801
|
+
try {
|
|
4802
|
+
return await this.makeRequest("/orders:by_client_order_id", "GET", undefined, `?client_order_id=${encodeURIComponent(clientOrderId)}`);
|
|
4803
|
+
}
|
|
4804
|
+
catch (error) {
|
|
4805
|
+
if (error instanceof Error && error.message.includes("(404)")) {
|
|
4806
|
+
return null;
|
|
4807
|
+
}
|
|
4808
|
+
throw error;
|
|
4809
|
+
}
|
|
4810
|
+
}
|
|
4811
|
+
/**
|
|
4812
|
+
* POST an order body with duplicate-`client_order_id` recovery.
|
|
4813
|
+
*
|
|
4814
|
+
* Sets `client_order_id` (explicit id wins; otherwise derived from
|
|
4815
|
+
* `deriveParts`), submits, and on Alpaca's 422 duplicate rejection:
|
|
4816
|
+
*
|
|
4817
|
+
* - **Caller-supplied id**: throws a typed
|
|
4818
|
+
* {@link DuplicateClientOrderIdError} — the caller owns idempotency
|
|
4819
|
+
* semantics and must decide whether the duplicate is success or a bug.
|
|
4820
|
+
* - **Derived id, colliding order live/filled**: returns the existing order
|
|
4821
|
+
* as idempotent success (this is the timeout+retry case the derived id
|
|
4822
|
+
* exists to de-duplicate).
|
|
4823
|
+
* - **Derived id, colliding order terminally dead** (canceled / expired /
|
|
4824
|
+
* rejected — e.g. cancel-then-recreate of an identical trailing stop):
|
|
4825
|
+
* resubmits exactly once with a fresh random salt so the legitimate new
|
|
4826
|
+
* order is not blocked. A second duplicate rejection throws the typed
|
|
4827
|
+
* error.
|
|
4828
|
+
* - **Derived id, status lookup fails**: fails CLOSED with the typed error —
|
|
4829
|
+
* the colliding order may be live, so resubmitting could double-fill; the
|
|
4830
|
+
* caller's next cycle retries when the lookup can succeed.
|
|
4831
|
+
*
|
|
4832
|
+
* Both recovery outcomes increment log-visible counters.
|
|
4833
|
+
*
|
|
4834
|
+
* @param body - The order payload (its `client_order_id` is set here).
|
|
4835
|
+
* @param options - Idempotency inputs: optional explicit id and the derive
|
|
4836
|
+
* parts used both for the default id and for the salted resubmit.
|
|
4837
|
+
* @returns The created (or pre-existing, on idempotent recovery) order.
|
|
4838
|
+
*/
|
|
4839
|
+
async postOrderWithIdempotencyRecovery(body, options) {
|
|
4840
|
+
const derived = options.explicitClientOrderId === undefined;
|
|
4841
|
+
const clientOrderId = options.explicitClientOrderId ??
|
|
4842
|
+
this.deriveClientOrderId(options.deriveParts);
|
|
4843
|
+
body.client_order_id = clientOrderId;
|
|
4844
|
+
const requestBody = body;
|
|
4845
|
+
try {
|
|
4846
|
+
return await this.makeRequest("/orders", "POST", requestBody);
|
|
4847
|
+
}
|
|
4848
|
+
catch (error) {
|
|
4849
|
+
if (!this.isDuplicateClientOrderIdRejection(error)) {
|
|
4850
|
+
throw error;
|
|
4851
|
+
}
|
|
4852
|
+
if (!derived) {
|
|
4853
|
+
throw new DuplicateClientOrderIdError(`Duplicate client_order_id "${clientOrderId}" rejected by Alpaca (caller-supplied id)`, clientOrderId, false, error);
|
|
4854
|
+
}
|
|
4855
|
+
let existing = null;
|
|
4856
|
+
try {
|
|
4857
|
+
existing = await this.getOrderByClientOrderId(clientOrderId);
|
|
4858
|
+
}
|
|
4859
|
+
catch (lookupError) {
|
|
4860
|
+
// Fail CLOSED: the 422 proves an order with this id exists, but its
|
|
4861
|
+
// status is unverifiable. Resubmitting here could double-fill a live
|
|
4862
|
+
// order (the timeout+retry case), which is strictly worse than the
|
|
4863
|
+
// caller retrying on its next cycle — by then the lookup will resolve.
|
|
4864
|
+
this.log(`Duplicate-order lookup failed for ${clientOrderId}; failing closed (no resubmit) to avoid a possible double order: ${lookupError instanceof Error
|
|
4865
|
+
? lookupError.message
|
|
4866
|
+
: String(lookupError)}`, { symbol: options.logSymbol, type: "error" });
|
|
4867
|
+
throw new DuplicateClientOrderIdError(`Duplicate client_order_id "${clientOrderId}" rejected by Alpaca and the existing-order lookup failed; refusing to resubmit (possible live duplicate)`, clientOrderId, true, lookupError);
|
|
4868
|
+
}
|
|
4869
|
+
if (existing && !TERMINAL_DEAD_ORDER_STATUSES.has(existing.status)) {
|
|
4870
|
+
this.idempotentDuplicateReturns++;
|
|
4871
|
+
this.log(`Derived client_order_id ${clientOrderId} already submitted (status=${existing.status}); returning existing order ${existing.id} as idempotent success`, {
|
|
4872
|
+
symbol: options.logSymbol,
|
|
4873
|
+
type: "warn",
|
|
4874
|
+
metadata: {
|
|
4875
|
+
outcome: "idempotent_return",
|
|
4876
|
+
idempotentDuplicateReturns: this.idempotentDuplicateReturns,
|
|
4877
|
+
},
|
|
4878
|
+
});
|
|
4879
|
+
return existing;
|
|
4880
|
+
}
|
|
4881
|
+
const saltedId = this.deriveClientOrderId([
|
|
4882
|
+
...options.deriveParts,
|
|
4883
|
+
"resubmit",
|
|
4884
|
+
node_crypto.randomUUID(),
|
|
4885
|
+
]);
|
|
4886
|
+
this.saltedDuplicateResubmits++;
|
|
4887
|
+
this.log(`Derived client_order_id ${clientOrderId} collided with a ${existing ? `terminal (${existing.status})` : "missing"} order; resubmitting once with fresh salt ${saltedId}`, {
|
|
4888
|
+
symbol: options.logSymbol,
|
|
4889
|
+
type: "warn",
|
|
4890
|
+
metadata: {
|
|
4891
|
+
outcome: "salted_resubmit",
|
|
4892
|
+
saltedDuplicateResubmits: this.saltedDuplicateResubmits,
|
|
4893
|
+
},
|
|
4894
|
+
});
|
|
4895
|
+
body.client_order_id = saltedId;
|
|
4896
|
+
try {
|
|
4897
|
+
return await this.makeRequest("/orders", "POST", requestBody);
|
|
4898
|
+
}
|
|
4899
|
+
catch (resubmitError) {
|
|
4900
|
+
if (this.isDuplicateClientOrderIdRejection(resubmitError)) {
|
|
4901
|
+
throw new DuplicateClientOrderIdError(`Salted resubmit of duplicate client_order_id "${clientOrderId}" was itself rejected as a duplicate ("${saltedId}")`, saltedId, true, resubmitError);
|
|
4902
|
+
}
|
|
4903
|
+
throw resubmitError;
|
|
4904
|
+
}
|
|
4905
|
+
}
|
|
4906
|
+
}
|
|
4597
4907
|
/**
|
|
4598
4908
|
* Collect the human-readable failure entries from a bulk Multi-Status (207)
|
|
4599
4909
|
* response body (`DELETE /orders`, `DELETE /positions`). Each element carries
|
|
@@ -4896,7 +5206,7 @@ class AlpacaTradingAPI {
|
|
|
4896
5206
|
}
|
|
4897
5207
|
const contentType = response.headers.get("content-type");
|
|
4898
5208
|
if (contentType && contentType.includes("application/json")) {
|
|
4899
|
-
return await response.json();
|
|
5209
|
+
return (await response.json());
|
|
4900
5210
|
}
|
|
4901
5211
|
// For non-JSON responses, return the text content
|
|
4902
5212
|
const textContent = await response.text();
|
|
@@ -5044,9 +5354,15 @@ class AlpacaTradingAPI {
|
|
|
5044
5354
|
* @param side (string) - the side of the order
|
|
5045
5355
|
* @param trailPercent100 (number) - the trail percent of the order (scale 100, i.e. 0.5 = 0.5%)
|
|
5046
5356
|
* @param position_intent (string) - the position intent of the order
|
|
5357
|
+
* @param clientOrderId - Optional explicit idempotency key; when supplied it
|
|
5358
|
+
* is used verbatim and duplicate rejections surface as
|
|
5359
|
+
* {@link DuplicateClientOrderIdError}.
|
|
5360
|
+
* @param idempotencyNonce - Optional attempt/signal discriminator folded into
|
|
5361
|
+
* the derived idempotency key so an intentionally-repeated identical order
|
|
5362
|
+
* inside one derivation window receives a distinct id.
|
|
5047
5363
|
* @returns The created AlpacaOrder with order ID and details
|
|
5048
5364
|
*/
|
|
5049
|
-
async createTrailingStop(symbol, qty, side, trailPercent100, position_intent, clientOrderId) {
|
|
5365
|
+
async createTrailingStop(symbol, qty, side, trailPercent100, position_intent, clientOrderId, idempotencyNonce) {
|
|
5050
5366
|
this.log(`Creating trailing stop ${side.toUpperCase()} ${qty} shares for ${symbol} with trail percent ${trailPercent100}%`, {
|
|
5051
5367
|
symbol,
|
|
5052
5368
|
});
|
|
@@ -5059,18 +5375,21 @@ class AlpacaTradingAPI {
|
|
|
5059
5375
|
type: "trailing_stop",
|
|
5060
5376
|
trail_percent: trailPercent100.toString(), // Already in decimal form (e.g., 4 for 4%)
|
|
5061
5377
|
time_in_force: "gtc",
|
|
5062
|
-
|
|
5063
|
-
|
|
5378
|
+
};
|
|
5379
|
+
try {
|
|
5380
|
+
const order = await this.postOrderWithIdempotencyRecovery(body, {
|
|
5381
|
+
explicitClientOrderId: clientOrderId,
|
|
5382
|
+
deriveParts: [
|
|
5064
5383
|
"trailing_stop",
|
|
5065
5384
|
symbol,
|
|
5066
5385
|
side,
|
|
5067
5386
|
position_intent,
|
|
5068
5387
|
Math.abs(qty),
|
|
5069
5388
|
trailPercent100,
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5389
|
+
...(idempotencyNonce !== undefined ? [String(idempotencyNonce)] : []),
|
|
5390
|
+
],
|
|
5391
|
+
logSymbol: symbol,
|
|
5392
|
+
});
|
|
5074
5393
|
this.log(`Trailing stop order created for ${symbol}: orderId=${order.id}, trailPercent=${trailPercent100}%`, { symbol });
|
|
5075
5394
|
return order;
|
|
5076
5395
|
}
|
|
@@ -5088,8 +5407,14 @@ class AlpacaTradingAPI {
|
|
|
5088
5407
|
* @param qty (number) - the quantity of the order
|
|
5089
5408
|
* @param side (string) - the side of the order
|
|
5090
5409
|
* @param position_intent (string) - the position intent of the order. Important for knowing if a position needs a trailing stop.
|
|
5410
|
+
* @param client_order_id - Optional explicit idempotency key; duplicate
|
|
5411
|
+
* rejections of an explicit id surface as
|
|
5412
|
+
* {@link DuplicateClientOrderIdError}.
|
|
5413
|
+
* @param idempotencyNonce - Optional attempt/signal discriminator folded into
|
|
5414
|
+
* the derived idempotency key so an intentionally-repeated identical order
|
|
5415
|
+
* inside one derivation window receives a distinct id.
|
|
5091
5416
|
*/
|
|
5092
|
-
async createMarketOrder(symbol, qty, side, position_intent, client_order_id) {
|
|
5417
|
+
async createMarketOrder(symbol, qty, side, position_intent, client_order_id, idempotencyNonce) {
|
|
5093
5418
|
this.log(`Creating market order for ${symbol}: ${side} ${qty} shares (${position_intent})`, {
|
|
5094
5419
|
symbol,
|
|
5095
5420
|
});
|
|
@@ -5102,17 +5427,19 @@ class AlpacaTradingAPI {
|
|
|
5102
5427
|
time_in_force: "day",
|
|
5103
5428
|
order_class: "simple",
|
|
5104
5429
|
};
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5430
|
+
try {
|
|
5431
|
+
return await this.postOrderWithIdempotencyRecovery(body, {
|
|
5432
|
+
explicitClientOrderId: client_order_id,
|
|
5433
|
+
deriveParts: [
|
|
5108
5434
|
"market",
|
|
5109
5435
|
symbol,
|
|
5110
5436
|
side,
|
|
5111
5437
|
position_intent,
|
|
5112
5438
|
Math.abs(qty),
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5439
|
+
...(idempotencyNonce !== undefined ? [String(idempotencyNonce)] : []),
|
|
5440
|
+
],
|
|
5441
|
+
logSymbol: symbol,
|
|
5442
|
+
});
|
|
5116
5443
|
}
|
|
5117
5444
|
catch (error) {
|
|
5118
5445
|
this.log(`Error creating market order: ${error}`, { type: "error" });
|
|
@@ -5269,9 +5596,14 @@ class AlpacaTradingAPI {
|
|
|
5269
5596
|
* @param limitPrice (number) - the limit price of the order
|
|
5270
5597
|
* @param position_intent (string) - the position intent of the order
|
|
5271
5598
|
* @param extended_hours (boolean) - whether the order is in extended hours
|
|
5272
|
-
* @param client_order_id
|
|
5599
|
+
* @param client_order_id - Optional explicit idempotency key; duplicate
|
|
5600
|
+
* rejections of an explicit id surface as
|
|
5601
|
+
* {@link DuplicateClientOrderIdError}.
|
|
5602
|
+
* @param idempotencyNonce - Optional attempt/signal discriminator folded into
|
|
5603
|
+
* the derived idempotency key so an intentionally-repeated identical order
|
|
5604
|
+
* inside one derivation window receives a distinct id.
|
|
5273
5605
|
*/
|
|
5274
|
-
async createLimitOrder(symbol, qty, side, limitPrice, position_intent, extended_hours = false, client_order_id) {
|
|
5606
|
+
async createLimitOrder(symbol, qty, side, limitPrice, position_intent, extended_hours = false, client_order_id, idempotencyNonce) {
|
|
5275
5607
|
this.log(`Creating limit order for ${symbol}: ${side} ${qty} shares at $${limitPrice.toFixed(2)} (${position_intent})`, {
|
|
5276
5608
|
symbol,
|
|
5277
5609
|
});
|
|
@@ -5286,9 +5618,10 @@ class AlpacaTradingAPI {
|
|
|
5286
5618
|
order_class: "simple",
|
|
5287
5619
|
extended_hours,
|
|
5288
5620
|
};
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5621
|
+
try {
|
|
5622
|
+
return await this.postOrderWithIdempotencyRecovery(body, {
|
|
5623
|
+
explicitClientOrderId: client_order_id,
|
|
5624
|
+
deriveParts: [
|
|
5292
5625
|
"limit",
|
|
5293
5626
|
symbol,
|
|
5294
5627
|
side,
|
|
@@ -5296,9 +5629,10 @@ class AlpacaTradingAPI {
|
|
|
5296
5629
|
Math.abs(qty),
|
|
5297
5630
|
this.roundPriceForAlpaca(limitPrice),
|
|
5298
5631
|
extended_hours,
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5632
|
+
...(idempotencyNonce !== undefined ? [String(idempotencyNonce)] : []),
|
|
5633
|
+
],
|
|
5634
|
+
logSymbol: symbol,
|
|
5635
|
+
});
|
|
5302
5636
|
}
|
|
5303
5637
|
catch (error) {
|
|
5304
5638
|
this.log(`Error creating limit order: ${error}`, { type: "error" });
|
|
@@ -5451,10 +5785,14 @@ class AlpacaTradingAPI {
|
|
|
5451
5785
|
* @param limitPrice Limit price (required for limit orders)
|
|
5452
5786
|
* @param clientOrderId Optional idempotency key; a deterministic one is
|
|
5453
5787
|
* derived from the order parameters when omitted so a client-timeout retry
|
|
5454
|
-
* is de-duplicated broker-side.
|
|
5788
|
+
* is de-duplicated broker-side. Duplicate rejections of an explicit id
|
|
5789
|
+
* surface as {@link DuplicateClientOrderIdError}.
|
|
5790
|
+
* @param idempotencyNonce Optional attempt/signal discriminator folded into
|
|
5791
|
+
* the derived idempotency key so an intentionally-repeated identical order
|
|
5792
|
+
* inside one derivation window receives a distinct id.
|
|
5455
5793
|
* @returns The created order
|
|
5456
5794
|
*/
|
|
5457
|
-
async createOptionOrder(symbol, qty, side, position_intent, type, limitPrice, clientOrderId) {
|
|
5795
|
+
async createOptionOrder(symbol, qty, side, position_intent, type, limitPrice, clientOrderId, idempotencyNonce) {
|
|
5458
5796
|
if (!Number.isInteger(qty) || qty <= 0) {
|
|
5459
5797
|
this.log("Quantity must be a positive whole number for option orders", {
|
|
5460
5798
|
type: "error",
|
|
@@ -5479,20 +5817,22 @@ class AlpacaTradingAPI {
|
|
|
5479
5817
|
if (type === "limit" && limitPrice !== undefined) {
|
|
5480
5818
|
orderData.limit_price = this.roundPriceForAlpaca(limitPrice).toString();
|
|
5481
5819
|
}
|
|
5482
|
-
orderData
|
|
5483
|
-
clientOrderId
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
])
|
|
5495
|
-
|
|
5820
|
+
return this.postOrderWithIdempotencyRecovery(orderData, {
|
|
5821
|
+
explicitClientOrderId: clientOrderId,
|
|
5822
|
+
deriveParts: [
|
|
5823
|
+
"option",
|
|
5824
|
+
type,
|
|
5825
|
+
symbol,
|
|
5826
|
+
side,
|
|
5827
|
+
position_intent,
|
|
5828
|
+
qty,
|
|
5829
|
+
type === "limit" && limitPrice !== undefined
|
|
5830
|
+
? this.roundPriceForAlpaca(limitPrice)
|
|
5831
|
+
: undefined,
|
|
5832
|
+
...(idempotencyNonce !== undefined ? [String(idempotencyNonce)] : []),
|
|
5833
|
+
],
|
|
5834
|
+
logSymbol: symbol,
|
|
5835
|
+
});
|
|
5496
5836
|
}
|
|
5497
5837
|
/**
|
|
5498
5838
|
* Create a multi-leg option order
|
|
@@ -5502,10 +5842,14 @@ class AlpacaTradingAPI {
|
|
|
5502
5842
|
* @param limitPrice Limit price (required for limit orders)
|
|
5503
5843
|
* @param clientOrderId Optional idempotency key; a deterministic one is
|
|
5504
5844
|
* derived from the legs and order parameters when omitted so a
|
|
5505
|
-
* client-timeout retry is de-duplicated broker-side.
|
|
5845
|
+
* client-timeout retry is de-duplicated broker-side. Duplicate rejections
|
|
5846
|
+
* of an explicit id surface as {@link DuplicateClientOrderIdError}.
|
|
5847
|
+
* @param idempotencyNonce Optional attempt/signal discriminator folded into
|
|
5848
|
+
* the derived idempotency key so an intentionally-repeated identical order
|
|
5849
|
+
* inside one derivation window receives a distinct id.
|
|
5506
5850
|
* @returns The created multi-leg order
|
|
5507
5851
|
*/
|
|
5508
|
-
async createMultiLegOptionOrder(legs, qty, type, limitPrice, clientOrderId) {
|
|
5852
|
+
async createMultiLegOptionOrder(legs, qty, type, limitPrice, clientOrderId, idempotencyNonce) {
|
|
5509
5853
|
if (!Number.isInteger(qty) || qty <= 0) {
|
|
5510
5854
|
this.log("Quantity must be a positive whole number for option orders", {
|
|
5511
5855
|
type: "error",
|
|
@@ -5531,18 +5875,20 @@ class AlpacaTradingAPI {
|
|
|
5531
5875
|
if (type === "limit" && limitPrice !== undefined) {
|
|
5532
5876
|
orderData.limit_price = this.roundPriceForAlpaca(limitPrice).toString();
|
|
5533
5877
|
}
|
|
5534
|
-
orderData
|
|
5535
|
-
clientOrderId
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
])
|
|
5545
|
-
|
|
5878
|
+
return this.postOrderWithIdempotencyRecovery(orderData, {
|
|
5879
|
+
explicitClientOrderId: clientOrderId,
|
|
5880
|
+
deriveParts: [
|
|
5881
|
+
"mleg",
|
|
5882
|
+
type,
|
|
5883
|
+
qty,
|
|
5884
|
+
type === "limit" && limitPrice !== undefined
|
|
5885
|
+
? this.roundPriceForAlpaca(limitPrice)
|
|
5886
|
+
: undefined,
|
|
5887
|
+
...legs.map((leg) => `${leg.symbol}:${leg.side}:${leg.ratio_qty}:${leg.position_intent}`),
|
|
5888
|
+
...(idempotencyNonce !== undefined ? [String(idempotencyNonce)] : []),
|
|
5889
|
+
],
|
|
5890
|
+
logSymbol: legSymbols,
|
|
5891
|
+
});
|
|
5546
5892
|
}
|
|
5547
5893
|
/**
|
|
5548
5894
|
* Exercise an option contract
|
|
@@ -5915,7 +6261,7 @@ class AlpacaTradingAPI {
|
|
|
5915
6261
|
*/
|
|
5916
6262
|
async createEquitiesTrade(params, options) {
|
|
5917
6263
|
const { symbol, qty, side, referencePrice } = params;
|
|
5918
|
-
const { type = "market", limitPrice, extendedHours = false, useStopLoss = false, stopPrice, stopPercent100, useTakeProfit = false, takeProfitPrice, takeProfitPercent100, clientOrderId, } = options || {};
|
|
6264
|
+
const { type = "market", limitPrice, extendedHours = false, useStopLoss = false, stopPrice, stopPercent100, useTakeProfit = false, takeProfitPrice, takeProfitPercent100, clientOrderId, idempotencyNonce, } = options || {};
|
|
5919
6265
|
// Validation: Extended hours + market order is not allowed
|
|
5920
6266
|
if (extendedHours && type === "market") {
|
|
5921
6267
|
this.log("Cannot create market order with extended hours enabled", {
|
|
@@ -6019,26 +6365,25 @@ class AlpacaTradingAPI {
|
|
|
6019
6365
|
extended_hours: extendedHours,
|
|
6020
6366
|
position_intent: side === "buy" ? "buy_to_open" : "sell_to_open",
|
|
6021
6367
|
};
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
]);
|
|
6368
|
+
const deriveParts = [
|
|
6369
|
+
"equities",
|
|
6370
|
+
orderClass,
|
|
6371
|
+
type,
|
|
6372
|
+
symbol,
|
|
6373
|
+
side,
|
|
6374
|
+
Math.abs(qty),
|
|
6375
|
+
type === "limit" && limitPrice !== undefined
|
|
6376
|
+
? this.roundPriceForAlpaca(limitPrice)
|
|
6377
|
+
: undefined,
|
|
6378
|
+
extendedHours,
|
|
6379
|
+
useStopLoss && calculatedStopPrice !== undefined
|
|
6380
|
+
? this.roundPriceForAlpaca(calculatedStopPrice)
|
|
6381
|
+
: undefined,
|
|
6382
|
+
useTakeProfit && calculatedTakeProfitPrice !== undefined
|
|
6383
|
+
? this.roundPriceForAlpaca(calculatedTakeProfitPrice)
|
|
6384
|
+
: undefined,
|
|
6385
|
+
...(idempotencyNonce !== undefined ? [String(idempotencyNonce)] : []),
|
|
6386
|
+
];
|
|
6042
6387
|
// Add limit price for limit orders
|
|
6043
6388
|
if (type === "limit" && limitPrice !== undefined) {
|
|
6044
6389
|
orderData.limit_price = this.roundPriceForAlpaca(limitPrice).toString();
|
|
@@ -6062,7 +6407,11 @@ class AlpacaTradingAPI {
|
|
|
6062
6407
|
symbol,
|
|
6063
6408
|
});
|
|
6064
6409
|
try {
|
|
6065
|
-
return await this.
|
|
6410
|
+
return await this.postOrderWithIdempotencyRecovery(orderData, {
|
|
6411
|
+
explicitClientOrderId: clientOrderId,
|
|
6412
|
+
deriveParts,
|
|
6413
|
+
logSymbol: symbol,
|
|
6414
|
+
});
|
|
6066
6415
|
}
|
|
6067
6416
|
catch (error) {
|
|
6068
6417
|
this.log(`Error creating equities trade: ${error}`, {
|
|
@@ -11045,7 +11394,9 @@ async function calculateTotalReturnYTD(portfolioHistory) {
|
|
|
11045
11394
|
* @param accountId - The ID of the Alpaca account.
|
|
11046
11395
|
* @param client - The Apollo client instance.
|
|
11047
11396
|
* @param alpacaAccount - The Alpaca account object.
|
|
11048
|
-
* @returns A promise that resolves to a string representing the expense ratio
|
|
11397
|
+
* @returns A promise that resolves to a string representing the expense ratio
|
|
11398
|
+
* in percentage format, or "N/A" when the ratio cannot be computed honestly
|
|
11399
|
+
* (missing account, fee-fetch failure, or non-positive/non-finite equity).
|
|
11049
11400
|
*/
|
|
11050
11401
|
async function calculateExpenseRatio({ accountId, client, alpacaAccount, }) {
|
|
11051
11402
|
if (!accountId && !alpacaAccount && !client) {
|
|
@@ -11079,12 +11430,15 @@ async function calculateExpenseRatio({ accountId, client, alpacaAccount, }) {
|
|
|
11079
11430
|
return "N/A";
|
|
11080
11431
|
}
|
|
11081
11432
|
}
|
|
11082
|
-
// Validate equity
|
|
11083
|
-
|
|
11084
|
-
|
|
11433
|
+
// Validate equity. A drained or freshly-funded account reports equity "0"
|
|
11434
|
+
// (and a broken feed can yield NaN or a negative string); dividing by it
|
|
11435
|
+
// would fabricate "Infinity%"/"NaN%" — return the deliberate "N/A"
|
|
11436
|
+
// (unknown) instead, consistent with the fee-fetch failure path below.
|
|
11437
|
+
const equity = parseFloat(accountDetails.equity);
|
|
11438
|
+
if (!Number.isFinite(equity) || equity <= 0) {
|
|
11439
|
+
getLogger().warn("Non-positive or non-finite equity value; cannot compute expense ratio.", { equity: accountDetails.equity });
|
|
11085
11440
|
return "N/A";
|
|
11086
11441
|
}
|
|
11087
|
-
const equity = parseFloat(accountDetails.equity);
|
|
11088
11442
|
// Fetch the account's real trailing fee expenses from Alpaca account
|
|
11089
11443
|
// activities. A genuine data-source failure yields "N/A" (unknown) rather
|
|
11090
11444
|
// than a fabricated 0.00%.
|
|
@@ -11410,8 +11764,13 @@ async function calculateAlphaAndBeta(portfolioHistory, benchmarkBars) {
|
|
|
11410
11764
|
const alignedPortfolioReturns = [];
|
|
11411
11765
|
const alignedBenchmarkReturns = [];
|
|
11412
11766
|
for (const timestamp of commonTimestamps) {
|
|
11767
|
+
// commonTimestamps is the key intersection of both maps, so both lookups
|
|
11768
|
+
// are guaranteed present; the guard replaces a non-null assertion.
|
|
11413
11769
|
const portfolioRet = portfolioReturnsMap.get(timestamp);
|
|
11414
11770
|
const benchmarkRet = benchmarkReturnsMap.get(timestamp);
|
|
11771
|
+
if (portfolioRet === undefined || benchmarkRet === undefined) {
|
|
11772
|
+
continue;
|
|
11773
|
+
}
|
|
11415
11774
|
if (isFinite(portfolioRet) && isFinite(benchmarkRet)) {
|
|
11416
11775
|
alignedPortfolioReturns.push(portfolioRet);
|
|
11417
11776
|
alignedBenchmarkReturns.push(benchmarkRet);
|
|
@@ -11672,8 +12031,13 @@ function alignReturnsByDate(portfolioHistory, benchmarkBars) {
|
|
|
11672
12031
|
const alignedPortfolioReturns = [];
|
|
11673
12032
|
const alignedBenchmarkReturns = [];
|
|
11674
12033
|
for (const timestamp of commonTimestamps) {
|
|
12034
|
+
// commonTimestamps is the key intersection of both maps, so both lookups
|
|
12035
|
+
// are guaranteed present; the guard replaces a non-null assertion.
|
|
11675
12036
|
const portfolioRet = portfolioReturnsMap.get(timestamp);
|
|
11676
12037
|
const benchmarkRet = benchmarkReturnsMap.get(timestamp);
|
|
12038
|
+
if (portfolioRet === undefined || benchmarkRet === undefined) {
|
|
12039
|
+
continue;
|
|
12040
|
+
}
|
|
11677
12041
|
alignedPortfolioReturns.push(portfolioRet);
|
|
11678
12042
|
alignedBenchmarkReturns.push(benchmarkRet);
|
|
11679
12043
|
}
|
|
@@ -11790,8 +12154,13 @@ async function calculateInformationRatio(portfolioHistory, benchmarkBars) {
|
|
|
11790
12154
|
// Extract aligned returns
|
|
11791
12155
|
const activeReturns = [];
|
|
11792
12156
|
for (const timestamp of commonTimestamps) {
|
|
12157
|
+
// commonTimestamps is the key intersection of both maps, so both lookups
|
|
12158
|
+
// are guaranteed present; the guard replaces a non-null assertion.
|
|
11793
12159
|
const portfolioRet = portfolioReturnsMap.get(timestamp);
|
|
11794
12160
|
const benchmarkRet = benchmarkReturnsMap.get(timestamp);
|
|
12161
|
+
if (portfolioRet === undefined || benchmarkRet === undefined) {
|
|
12162
|
+
continue;
|
|
12163
|
+
}
|
|
11795
12164
|
activeReturns.push(portfolioRet - benchmarkRet);
|
|
11796
12165
|
}
|
|
11797
12166
|
const n = activeReturns.length;
|
|
@@ -14620,17 +14989,17 @@ var hasRequiredBrowser;
|
|
|
14620
14989
|
function requireBrowser () {
|
|
14621
14990
|
if (hasRequiredBrowser) return browser.exports;
|
|
14622
14991
|
hasRequiredBrowser = 1;
|
|
14623
|
-
(function (module, exports
|
|
14992
|
+
(function (module, exports) {
|
|
14624
14993
|
/**
|
|
14625
14994
|
* This is the web browser implementation of `debug()`.
|
|
14626
14995
|
*/
|
|
14627
14996
|
|
|
14628
|
-
exports
|
|
14629
|
-
exports
|
|
14630
|
-
exports
|
|
14631
|
-
exports
|
|
14632
|
-
exports
|
|
14633
|
-
exports
|
|
14997
|
+
exports.formatArgs = formatArgs;
|
|
14998
|
+
exports.save = save;
|
|
14999
|
+
exports.load = load;
|
|
15000
|
+
exports.useColors = useColors;
|
|
15001
|
+
exports.storage = localstorage();
|
|
15002
|
+
exports.destroy = (() => {
|
|
14634
15003
|
let warned = false;
|
|
14635
15004
|
|
|
14636
15005
|
return () => {
|
|
@@ -14645,7 +15014,7 @@ function requireBrowser () {
|
|
|
14645
15014
|
* Colors.
|
|
14646
15015
|
*/
|
|
14647
15016
|
|
|
14648
|
-
exports
|
|
15017
|
+
exports.colors = [
|
|
14649
15018
|
'#0000CC',
|
|
14650
15019
|
'#0000FF',
|
|
14651
15020
|
'#0033CC',
|
|
@@ -14810,7 +15179,7 @@ function requireBrowser () {
|
|
|
14810
15179
|
*
|
|
14811
15180
|
* @api public
|
|
14812
15181
|
*/
|
|
14813
|
-
exports
|
|
15182
|
+
exports.log = console.debug || console.log || (() => {});
|
|
14814
15183
|
|
|
14815
15184
|
/**
|
|
14816
15185
|
* Save `namespaces`.
|
|
@@ -14821,9 +15190,9 @@ function requireBrowser () {
|
|
|
14821
15190
|
function save(namespaces) {
|
|
14822
15191
|
try {
|
|
14823
15192
|
if (namespaces) {
|
|
14824
|
-
exports
|
|
15193
|
+
exports.storage.setItem('debug', namespaces);
|
|
14825
15194
|
} else {
|
|
14826
|
-
exports
|
|
15195
|
+
exports.storage.removeItem('debug');
|
|
14827
15196
|
}
|
|
14828
15197
|
} catch (error) {
|
|
14829
15198
|
// Swallow
|
|
@@ -14840,7 +15209,7 @@ function requireBrowser () {
|
|
|
14840
15209
|
function load() {
|
|
14841
15210
|
let r;
|
|
14842
15211
|
try {
|
|
14843
|
-
r = exports
|
|
15212
|
+
r = exports.storage.getItem('debug') || exports.storage.getItem('DEBUG') ;
|
|
14844
15213
|
} catch (error) {
|
|
14845
15214
|
// Swallow
|
|
14846
15215
|
// XXX (@Qix-) should we be logging these?
|
|
@@ -14876,7 +15245,7 @@ function requireBrowser () {
|
|
|
14876
15245
|
}
|
|
14877
15246
|
}
|
|
14878
15247
|
|
|
14879
|
-
module.exports = requireCommon()(exports
|
|
15248
|
+
module.exports = requireCommon()(exports);
|
|
14880
15249
|
|
|
14881
15250
|
const {formatters} = module.exports;
|
|
14882
15251
|
|
|
@@ -15065,7 +15434,7 @@ var hasRequiredNode$1;
|
|
|
15065
15434
|
function requireNode$1 () {
|
|
15066
15435
|
if (hasRequiredNode$1) return node$1.exports;
|
|
15067
15436
|
hasRequiredNode$1 = 1;
|
|
15068
|
-
(function (module, exports
|
|
15437
|
+
(function (module, exports) {
|
|
15069
15438
|
const tty = require$$1$1;
|
|
15070
15439
|
const util = require$$1$2;
|
|
15071
15440
|
|
|
@@ -15073,13 +15442,13 @@ function requireNode$1 () {
|
|
|
15073
15442
|
* This is the Node.js implementation of `debug()`.
|
|
15074
15443
|
*/
|
|
15075
15444
|
|
|
15076
|
-
exports
|
|
15077
|
-
exports
|
|
15078
|
-
exports
|
|
15079
|
-
exports
|
|
15080
|
-
exports
|
|
15081
|
-
exports
|
|
15082
|
-
exports
|
|
15445
|
+
exports.init = init;
|
|
15446
|
+
exports.log = log;
|
|
15447
|
+
exports.formatArgs = formatArgs;
|
|
15448
|
+
exports.save = save;
|
|
15449
|
+
exports.load = load;
|
|
15450
|
+
exports.useColors = useColors;
|
|
15451
|
+
exports.destroy = util.deprecate(
|
|
15083
15452
|
() => {},
|
|
15084
15453
|
'Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.'
|
|
15085
15454
|
);
|
|
@@ -15088,7 +15457,7 @@ function requireNode$1 () {
|
|
|
15088
15457
|
* Colors.
|
|
15089
15458
|
*/
|
|
15090
15459
|
|
|
15091
|
-
exports
|
|
15460
|
+
exports.colors = [6, 2, 3, 4, 5, 1];
|
|
15092
15461
|
|
|
15093
15462
|
try {
|
|
15094
15463
|
// Optional dependency (as in, doesn't need to be installed, NOT like optionalDependencies in package.json)
|
|
@@ -15096,7 +15465,7 @@ function requireNode$1 () {
|
|
|
15096
15465
|
const supportsColor = requireSupportsColor();
|
|
15097
15466
|
|
|
15098
15467
|
if (supportsColor && (supportsColor.stderr || supportsColor).level >= 2) {
|
|
15099
|
-
exports
|
|
15468
|
+
exports.colors = [
|
|
15100
15469
|
20,
|
|
15101
15470
|
21,
|
|
15102
15471
|
26,
|
|
@@ -15185,7 +15554,7 @@ function requireNode$1 () {
|
|
|
15185
15554
|
* $ DEBUG_COLORS=no DEBUG_DEPTH=10 DEBUG_SHOW_HIDDEN=enabled node script.js
|
|
15186
15555
|
*/
|
|
15187
15556
|
|
|
15188
|
-
exports
|
|
15557
|
+
exports.inspectOpts = Object.keys(process.env).filter(key => {
|
|
15189
15558
|
return /^debug_/i.test(key);
|
|
15190
15559
|
}).reduce((obj, key) => {
|
|
15191
15560
|
// Camel-case
|
|
@@ -15217,8 +15586,8 @@ function requireNode$1 () {
|
|
|
15217
15586
|
*/
|
|
15218
15587
|
|
|
15219
15588
|
function useColors() {
|
|
15220
|
-
return 'colors' in exports
|
|
15221
|
-
Boolean(exports
|
|
15589
|
+
return 'colors' in exports.inspectOpts ?
|
|
15590
|
+
Boolean(exports.inspectOpts.colors) :
|
|
15222
15591
|
tty.isatty(process.stderr.fd);
|
|
15223
15592
|
}
|
|
15224
15593
|
|
|
@@ -15244,7 +15613,7 @@ function requireNode$1 () {
|
|
|
15244
15613
|
}
|
|
15245
15614
|
|
|
15246
15615
|
function getDate() {
|
|
15247
|
-
if (exports
|
|
15616
|
+
if (exports.inspectOpts.hideDate) {
|
|
15248
15617
|
return '';
|
|
15249
15618
|
}
|
|
15250
15619
|
return new Date().toISOString() + ' ';
|
|
@@ -15255,7 +15624,7 @@ function requireNode$1 () {
|
|
|
15255
15624
|
*/
|
|
15256
15625
|
|
|
15257
15626
|
function log(...args) {
|
|
15258
|
-
return process.stderr.write(util.formatWithOptions(exports
|
|
15627
|
+
return process.stderr.write(util.formatWithOptions(exports.inspectOpts, ...args) + '\n');
|
|
15259
15628
|
}
|
|
15260
15629
|
|
|
15261
15630
|
/**
|
|
@@ -15295,13 +15664,13 @@ function requireNode$1 () {
|
|
|
15295
15664
|
function init(debug) {
|
|
15296
15665
|
debug.inspectOpts = {};
|
|
15297
15666
|
|
|
15298
|
-
const keys = Object.keys(exports
|
|
15667
|
+
const keys = Object.keys(exports.inspectOpts);
|
|
15299
15668
|
for (let i = 0; i < keys.length; i++) {
|
|
15300
|
-
debug.inspectOpts[keys[i]] = exports
|
|
15669
|
+
debug.inspectOpts[keys[i]] = exports.inspectOpts[keys[i]];
|
|
15301
15670
|
}
|
|
15302
15671
|
}
|
|
15303
15672
|
|
|
15304
|
-
module.exports = requireCommon()(exports
|
|
15673
|
+
module.exports = requireCommon()(exports);
|
|
15305
15674
|
|
|
15306
15675
|
const {formatters} = module.exports;
|
|
15307
15676
|
|
|
@@ -15875,7 +16244,7 @@ function requireFollowRedirects () {
|
|
|
15875
16244
|
// Wraps the key/value object of protocols with redirect functionality
|
|
15876
16245
|
function wrap(protocols) {
|
|
15877
16246
|
// Default settings
|
|
15878
|
-
var exports
|
|
16247
|
+
var exports = {
|
|
15879
16248
|
maxRedirects: 21,
|
|
15880
16249
|
maxBodyLength: 10 * 1024 * 1024,
|
|
15881
16250
|
};
|
|
@@ -15885,7 +16254,7 @@ function requireFollowRedirects () {
|
|
|
15885
16254
|
Object.keys(protocols).forEach(function (scheme) {
|
|
15886
16255
|
var protocol = scheme + ":";
|
|
15887
16256
|
var nativeProtocol = nativeProtocols[protocol] = protocols[scheme];
|
|
15888
|
-
var wrappedProtocol = exports
|
|
16257
|
+
var wrappedProtocol = exports[scheme] = Object.create(nativeProtocol);
|
|
15889
16258
|
|
|
15890
16259
|
// Executes a request, following redirects
|
|
15891
16260
|
function request(input, options, callback) {
|
|
@@ -15908,8 +16277,8 @@ function requireFollowRedirects () {
|
|
|
15908
16277
|
|
|
15909
16278
|
// Set defaults
|
|
15910
16279
|
options = Object.assign({
|
|
15911
|
-
maxRedirects: exports
|
|
15912
|
-
maxBodyLength: exports
|
|
16280
|
+
maxRedirects: exports.maxRedirects,
|
|
16281
|
+
maxBodyLength: exports.maxBodyLength,
|
|
15913
16282
|
}, input, options);
|
|
15914
16283
|
options.nativeProtocols = nativeProtocols;
|
|
15915
16284
|
if (!isString(options.host) && !isString(options.hostname)) {
|
|
@@ -15934,7 +16303,7 @@ function requireFollowRedirects () {
|
|
|
15934
16303
|
get: { value: get, configurable: true, enumerable: true, writable: true },
|
|
15935
16304
|
});
|
|
15936
16305
|
});
|
|
15937
|
-
return exports
|
|
16306
|
+
return exports;
|
|
15938
16307
|
}
|
|
15939
16308
|
|
|
15940
16309
|
function noop() { /* empty */ }
|
|
@@ -17540,7 +17909,7 @@ var hasRequiredLodash;
|
|
|
17540
17909
|
function requireLodash () {
|
|
17541
17910
|
if (hasRequiredLodash) return lodash$1.exports;
|
|
17542
17911
|
hasRequiredLodash = 1;
|
|
17543
|
-
(function (module, exports
|
|
17912
|
+
(function (module, exports) {
|
|
17544
17913
|
(function() {
|
|
17545
17914
|
|
|
17546
17915
|
/** Used as a safe reference for `undefined` in pre-ES5 environments. */
|
|
@@ -17971,7 +18340,7 @@ function requireLodash () {
|
|
|
17971
18340
|
var root = freeGlobal || freeSelf || Function('return this')();
|
|
17972
18341
|
|
|
17973
18342
|
/** Detect free variable `exports`. */
|
|
17974
|
-
var freeExports = exports
|
|
18343
|
+
var freeExports = exports && !exports.nodeType && exports;
|
|
17975
18344
|
|
|
17976
18345
|
/** Detect free variable `module`. */
|
|
17977
18346
|
var freeModule = freeExports && 'object' == 'object' && module && !module.nodeType && module;
|
|
@@ -35801,12 +36170,12 @@ var hasRequiredIsBuffer;
|
|
|
35801
36170
|
function requireIsBuffer () {
|
|
35802
36171
|
if (hasRequiredIsBuffer) return isBuffer.exports;
|
|
35803
36172
|
hasRequiredIsBuffer = 1;
|
|
35804
|
-
(function (module, exports
|
|
36173
|
+
(function (module, exports) {
|
|
35805
36174
|
var root = require_root(),
|
|
35806
36175
|
stubFalse = requireStubFalse();
|
|
35807
36176
|
|
|
35808
36177
|
/** Detect free variable `exports`. */
|
|
35809
|
-
var freeExports = exports
|
|
36178
|
+
var freeExports = exports && !exports.nodeType && exports;
|
|
35810
36179
|
|
|
35811
36180
|
/** Detect free variable `module`. */
|
|
35812
36181
|
var freeModule = freeExports && 'object' == 'object' && module && !module.nodeType && module;
|
|
@@ -36026,11 +36395,11 @@ var hasRequired_nodeUtil;
|
|
|
36026
36395
|
function require_nodeUtil () {
|
|
36027
36396
|
if (hasRequired_nodeUtil) return _nodeUtil.exports;
|
|
36028
36397
|
hasRequired_nodeUtil = 1;
|
|
36029
|
-
(function (module, exports
|
|
36398
|
+
(function (module, exports) {
|
|
36030
36399
|
var freeGlobal = require_freeGlobal();
|
|
36031
36400
|
|
|
36032
36401
|
/** Detect free variable `exports`. */
|
|
36033
|
-
var freeExports = exports
|
|
36402
|
+
var freeExports = exports && !exports.nodeType && exports;
|
|
36034
36403
|
|
|
36035
36404
|
/** Detect free variable `module`. */
|
|
36036
36405
|
var freeModule = freeExports && 'object' == 'object' && module && !module.nodeType && module;
|
|
@@ -44952,7 +45321,7 @@ var hasRequiredSafeBuffer;
|
|
|
44952
45321
|
function requireSafeBuffer () {
|
|
44953
45322
|
if (hasRequiredSafeBuffer) return safeBuffer.exports;
|
|
44954
45323
|
hasRequiredSafeBuffer = 1;
|
|
44955
|
-
(function (module, exports
|
|
45324
|
+
(function (module, exports) {
|
|
44956
45325
|
/* eslint-disable node/no-deprecated-api */
|
|
44957
45326
|
var buffer = require$$0$5;
|
|
44958
45327
|
var Buffer = buffer.Buffer;
|
|
@@ -44967,8 +45336,8 @@ function requireSafeBuffer () {
|
|
|
44967
45336
|
module.exports = buffer;
|
|
44968
45337
|
} else {
|
|
44969
45338
|
// Copy properties from require('buffer')
|
|
44970
|
-
copyProps(buffer, exports
|
|
44971
|
-
exports
|
|
45339
|
+
copyProps(buffer, exports);
|
|
45340
|
+
exports.Buffer = SafeBuffer;
|
|
44972
45341
|
}
|
|
44973
45342
|
|
|
44974
45343
|
function SafeBuffer (arg, encodingOrOffset, length) {
|
|
@@ -48166,22 +48535,22 @@ var hasRequiredReadable;
|
|
|
48166
48535
|
function requireReadable () {
|
|
48167
48536
|
if (hasRequiredReadable) return readable.exports;
|
|
48168
48537
|
hasRequiredReadable = 1;
|
|
48169
|
-
(function (module, exports
|
|
48538
|
+
(function (module, exports) {
|
|
48170
48539
|
var Stream = require$$0$4;
|
|
48171
48540
|
if (process.env.READABLE_STREAM === 'disable' && Stream) {
|
|
48172
48541
|
module.exports = Stream.Readable;
|
|
48173
48542
|
Object.assign(module.exports, Stream);
|
|
48174
48543
|
module.exports.Stream = Stream;
|
|
48175
48544
|
} else {
|
|
48176
|
-
exports
|
|
48177
|
-
exports
|
|
48178
|
-
exports
|
|
48179
|
-
exports
|
|
48180
|
-
exports
|
|
48181
|
-
exports
|
|
48182
|
-
exports
|
|
48183
|
-
exports
|
|
48184
|
-
exports
|
|
48545
|
+
exports = module.exports = require_stream_readable();
|
|
48546
|
+
exports.Stream = Stream || exports;
|
|
48547
|
+
exports.Readable = exports;
|
|
48548
|
+
exports.Writable = require_stream_writable();
|
|
48549
|
+
exports.Duplex = require_stream_duplex();
|
|
48550
|
+
exports.Transform = require_stream_transform();
|
|
48551
|
+
exports.PassThrough = require_stream_passthrough();
|
|
48552
|
+
exports.finished = requireEndOfStream();
|
|
48553
|
+
exports.pipeline = requirePipeline();
|
|
48185
48554
|
}
|
|
48186
48555
|
} (readable, readable.exports));
|
|
48187
48556
|
return readable.exports;
|
|
@@ -49616,12 +49985,12 @@ var hasRequiredWebsocket;
|
|
|
49616
49985
|
function requireWebsocket () {
|
|
49617
49986
|
if (hasRequiredWebsocket) return websocket$1;
|
|
49618
49987
|
hasRequiredWebsocket = 1;
|
|
49619
|
-
(function (exports
|
|
49988
|
+
(function (exports) {
|
|
49620
49989
|
var __importDefault = (websocket$1 && websocket$1.__importDefault) || function (mod) {
|
|
49621
49990
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
49622
49991
|
};
|
|
49623
|
-
Object.defineProperty(exports
|
|
49624
|
-
exports
|
|
49992
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49993
|
+
exports.AlpacaWebsocket = exports.ERROR = exports.CONN_ERROR = exports.EVENT = exports.STATE = void 0;
|
|
49625
49994
|
const events_1 = __importDefault(require$$0$1);
|
|
49626
49995
|
const ws_1 = __importDefault(requireWs());
|
|
49627
49996
|
const msgpack5_1 = __importDefault(requireMsgpack5());
|
|
@@ -49635,7 +50004,7 @@ function requireWebsocket () {
|
|
|
49635
50004
|
STATE["DISCONNECTED"] = "disconnected";
|
|
49636
50005
|
STATE["WAITING_TO_CONNECT"] = "waiting to connect";
|
|
49637
50006
|
STATE["WAITING_TO_RECONNECT"] = "waiting to reconnect";
|
|
49638
|
-
})(STATE || (exports
|
|
50007
|
+
})(STATE || (exports.STATE = STATE = {}));
|
|
49639
50008
|
// Client events
|
|
49640
50009
|
var EVENT;
|
|
49641
50010
|
(function (EVENT) {
|
|
@@ -49654,9 +50023,9 @@ function requireWebsocket () {
|
|
|
49654
50023
|
EVENT["CORRECTIONS"] = "corrections";
|
|
49655
50024
|
EVENT["ORDERBOOKS"] = "orderbooks";
|
|
49656
50025
|
EVENT["NEWS"] = "news";
|
|
49657
|
-
})(EVENT || (exports
|
|
50026
|
+
})(EVENT || (exports.EVENT = EVENT = {}));
|
|
49658
50027
|
// Connection errors by code
|
|
49659
|
-
exports
|
|
50028
|
+
exports.CONN_ERROR = new Map([
|
|
49660
50029
|
[400, "invalid syntax"],
|
|
49661
50030
|
[401, "not authenticated"],
|
|
49662
50031
|
[402, "auth failed"],
|
|
@@ -49675,7 +50044,7 @@ function requireWebsocket () {
|
|
|
49675
50044
|
ERROR["MISSING_SECERT_KEY"] = "missing secret key";
|
|
49676
50045
|
ERROR["MISSING_API_KEY"] = "missing api key";
|
|
49677
50046
|
ERROR["UNEXPECTED_MESSAGE"] = "unexpected message";
|
|
49678
|
-
})(ERROR || (exports
|
|
50047
|
+
})(ERROR || (exports.ERROR = ERROR = {}));
|
|
49679
50048
|
class AlpacaWebsocket extends events_1.default.EventEmitter {
|
|
49680
50049
|
constructor(options) {
|
|
49681
50050
|
super();
|
|
@@ -49832,7 +50201,7 @@ function requireWebsocket () {
|
|
|
49832
50201
|
this.updateSubscriptions(data[0]);
|
|
49833
50202
|
break;
|
|
49834
50203
|
case "error":
|
|
49835
|
-
this.emit(EVENT.CLIENT_ERROR, exports
|
|
50204
|
+
this.emit(EVENT.CLIENT_ERROR, exports.CONN_ERROR.get(data[0].code));
|
|
49836
50205
|
break;
|
|
49837
50206
|
default:
|
|
49838
50207
|
this.dataHandler(data);
|
|
@@ -49861,7 +50230,7 @@ function requireWebsocket () {
|
|
|
49861
50230
|
}
|
|
49862
50231
|
}
|
|
49863
50232
|
}
|
|
49864
|
-
exports
|
|
50233
|
+
exports.AlpacaWebsocket = AlpacaWebsocket;
|
|
49865
50234
|
} (websocket$1));
|
|
49866
50235
|
return websocket$1;
|
|
49867
50236
|
}
|
|
@@ -50495,8 +50864,8 @@ var hasRequiredWebsockets;
|
|
|
50495
50864
|
function requireWebsockets () {
|
|
50496
50865
|
if (hasRequiredWebsockets) return websockets;
|
|
50497
50866
|
hasRequiredWebsockets = 1;
|
|
50498
|
-
(function (exports
|
|
50499
|
-
Object.defineProperty(exports
|
|
50867
|
+
(function (exports) {
|
|
50868
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50500
50869
|
const events = require$$0$1;
|
|
50501
50870
|
const WebSocket = requireWs();
|
|
50502
50871
|
const entity = requireEntity();
|
|
@@ -50511,7 +50880,7 @@ function requireWebsockets () {
|
|
|
50511
50880
|
STATE.DISCONNECTED = "disconnected";
|
|
50512
50881
|
STATE.WAITING_TO_CONNECT = "waiting to connect";
|
|
50513
50882
|
STATE.WAITING_TO_RECONNECT = "waiting to reconnect";
|
|
50514
|
-
})((STATE = exports
|
|
50883
|
+
})((STATE = exports.STATE || (exports.STATE = {})));
|
|
50515
50884
|
// Client events
|
|
50516
50885
|
var EVENT;
|
|
50517
50886
|
(function (EVENT) {
|
|
@@ -50525,7 +50894,7 @@ function requireWebsockets () {
|
|
|
50525
50894
|
EVENT.STOCK_QUOTES = "stock_quotes";
|
|
50526
50895
|
EVENT.STOCK_AGG_SEC = "stock_agg_sec";
|
|
50527
50896
|
EVENT.STOCK_AGG_MIN = "stock_agg_min";
|
|
50528
|
-
})((EVENT = exports
|
|
50897
|
+
})((EVENT = exports.EVENT || (exports.EVENT = {})));
|
|
50529
50898
|
// Connection errors Each of these will also emit EVENT.ERROR
|
|
50530
50899
|
var ERROR;
|
|
50531
50900
|
(function (ERROR) {
|
|
@@ -50534,7 +50903,7 @@ function requireWebsockets () {
|
|
|
50534
50903
|
ERROR.MISSING_API_KEY = "missing api key";
|
|
50535
50904
|
ERROR.MISSING_SECRET_KEY = "missing secret key";
|
|
50536
50905
|
ERROR.UNKNOWN = "unknown error";
|
|
50537
|
-
})((ERROR = exports
|
|
50906
|
+
})((ERROR = exports.ERROR || (exports.ERROR = {})));
|
|
50538
50907
|
/**
|
|
50539
50908
|
* AlpacaStreamClient manages a connection to Alpaca's websocket api
|
|
50540
50909
|
*/
|
|
@@ -50847,7 +51216,7 @@ function requireWebsockets () {
|
|
|
50847
51216
|
}
|
|
50848
51217
|
}
|
|
50849
51218
|
}
|
|
50850
|
-
exports
|
|
51219
|
+
exports.AlpacaStreamClient = AlpacaStreamClient;
|
|
50851
51220
|
} (websockets));
|
|
50852
51221
|
return websockets;
|
|
50853
51222
|
}
|
|
@@ -51099,13 +51468,13 @@ var hasRequiredDist;
|
|
|
51099
51468
|
function requireDist () {
|
|
51100
51469
|
if (hasRequiredDist) return dist$1.exports;
|
|
51101
51470
|
hasRequiredDist = 1;
|
|
51102
|
-
(function (module, exports
|
|
51471
|
+
(function (module, exports) {
|
|
51103
51472
|
var __importDefault = (dist && dist.__importDefault) || function (mod) {
|
|
51104
51473
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
51105
51474
|
};
|
|
51106
|
-
Object.defineProperty(exports
|
|
51475
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51107
51476
|
const alpaca_trade_api_1 = __importDefault(requireAlpacaTradeApi());
|
|
51108
|
-
exports
|
|
51477
|
+
exports.default = alpaca_trade_api_1.default;
|
|
51109
51478
|
module.exports = alpaca_trade_api_1.default;
|
|
51110
51479
|
} (dist$1, dist$1.exports));
|
|
51111
51480
|
return dist$1.exports;
|
|
@@ -63149,6 +63518,14 @@ class LRUCache {
|
|
|
63149
63518
|
}
|
|
63150
63519
|
}
|
|
63151
63520
|
|
|
63521
|
+
/**
|
|
63522
|
+
* Default hard ceiling (ms) on a single loader invocation. Resolved at
|
|
63523
|
+
* construction time into `options.loadTimeoutMs` so the effective value is
|
|
63524
|
+
* always a validated number (see the constructor guard) — mirrors the
|
|
63525
|
+
* engine-local copy's constructor-time resolution ahead of consolidating the
|
|
63526
|
+
* two implementations onto this one.
|
|
63527
|
+
*/
|
|
63528
|
+
const DEFAULT_LOAD_TIMEOUT_MS = 30_000;
|
|
63152
63529
|
/**
|
|
63153
63530
|
* StampedeProtectedCache provides three-layer protection against cache stampedes
|
|
63154
63531
|
*
|
|
@@ -63217,12 +63594,17 @@ class StampedeProtectedCache {
|
|
|
63217
63594
|
loadTimeouts: 0,
|
|
63218
63595
|
};
|
|
63219
63596
|
constructor(options) {
|
|
63597
|
+
if (options.loadTimeoutMs !== undefined &&
|
|
63598
|
+
(!Number.isFinite(options.loadTimeoutMs) || options.loadTimeoutMs <= 0)) {
|
|
63599
|
+
throw new RangeError(`StampedeProtectedCache loadTimeoutMs must be a positive finite number of milliseconds; received ${String(options.loadTimeoutMs)}`);
|
|
63600
|
+
}
|
|
63220
63601
|
this.options = {
|
|
63221
63602
|
...options,
|
|
63222
63603
|
staleWhileRevalidateTtl: options.staleWhileRevalidateTtl ?? options.defaultTtl * 2,
|
|
63223
63604
|
minJitter: options.minJitter ?? 0.9,
|
|
63224
63605
|
maxJitter: options.maxJitter ?? 1.1,
|
|
63225
63606
|
enableBackgroundRefresh: options.enableBackgroundRefresh ?? true,
|
|
63607
|
+
loadTimeoutMs: options.loadTimeoutMs ?? DEFAULT_LOAD_TIMEOUT_MS,
|
|
63226
63608
|
logger: options.logger ?? {
|
|
63227
63609
|
debug: () => { },
|
|
63228
63610
|
info: () => { },
|
|
@@ -63299,7 +63681,7 @@ class StampedeProtectedCache {
|
|
|
63299
63681
|
cached.accessCount++;
|
|
63300
63682
|
cached.lastAccessedAt = now;
|
|
63301
63683
|
// Check if entry is still fresh (considering probabilistic expiration)
|
|
63302
|
-
const jitteredExpiresAt = this.applyJitter(cached.expiresAt);
|
|
63684
|
+
const jitteredExpiresAt = this.applyJitter(cached.expiresAt, cached.ttl);
|
|
63303
63685
|
if (now < jitteredExpiresAt) {
|
|
63304
63686
|
// Fresh hit
|
|
63305
63687
|
this.stats.hits++;
|
|
@@ -63562,12 +63944,16 @@ class StampedeProtectedCache {
|
|
|
63562
63944
|
* is logged rather than surfacing as an unhandled rejection.
|
|
63563
63945
|
*/
|
|
63564
63946
|
async loadWithTimeout(key, loader, ttl) {
|
|
63565
|
-
const timeoutMs = this.options.loadTimeoutMs
|
|
63566
|
-
const
|
|
63947
|
+
const timeoutMs = this.options.loadTimeoutMs;
|
|
63948
|
+
const invocation = { abandoned: false };
|
|
63949
|
+
const loadPromise = this.loadAndCache(key, loader, ttl, invocation);
|
|
63567
63950
|
let timeoutHandle;
|
|
63568
63951
|
const timeoutPromise = new Promise((_, reject) => {
|
|
63569
63952
|
timeoutHandle = setTimeout(() => {
|
|
63570
63953
|
this.stats.loadTimeouts++;
|
|
63954
|
+
// Mark the invocation abandoned BEFORE evicting the pin: a retry that
|
|
63955
|
+
// starts now must never be overwritten by this loader's late result.
|
|
63956
|
+
invocation.abandoned = true;
|
|
63571
63957
|
// Evict the pin so the next caller retries fresh.
|
|
63572
63958
|
this.pendingRefreshes.delete(key);
|
|
63573
63959
|
this.options.logger.warn("Cache loader timed out — pin evicted", {
|
|
@@ -63600,11 +63986,19 @@ class StampedeProtectedCache {
|
|
|
63600
63986
|
/**
|
|
63601
63987
|
* Load data and cache it
|
|
63602
63988
|
*/
|
|
63603
|
-
async loadAndCache(key, loader, ttl) {
|
|
63989
|
+
async loadAndCache(key, loader, ttl, invocation = { abandoned: false }) {
|
|
63604
63990
|
const startTime = Date.now();
|
|
63605
63991
|
try {
|
|
63606
63992
|
this.options.logger.debug("Loading data", { key });
|
|
63607
63993
|
const value = await loader(key);
|
|
63994
|
+
if (invocation.abandoned) {
|
|
63995
|
+
// The pin was evicted at loadTimeoutMs and a retry may have cached
|
|
63996
|
+
// fresher data since; writing this late value would overwrite it with
|
|
63997
|
+
// a snapshot fetched before/through the hang.
|
|
63998
|
+
const loadTime = Date.now() - startTime;
|
|
63999
|
+
this.options.logger.warn("Abandoned cache loader resolved late — result discarded", { key, loadTime });
|
|
64000
|
+
return value;
|
|
64001
|
+
}
|
|
63608
64002
|
// Cache the loaded value
|
|
63609
64003
|
this.set(key, value, ttl);
|
|
63610
64004
|
const loadTime = Date.now() - startTime;
|
|
@@ -63619,11 +64013,15 @@ class StampedeProtectedCache {
|
|
|
63619
64013
|
error,
|
|
63620
64014
|
loadTime,
|
|
63621
64015
|
});
|
|
63622
|
-
// Update cached entry with error if it exists
|
|
63623
|
-
|
|
63624
|
-
|
|
63625
|
-
|
|
63626
|
-
cached
|
|
64016
|
+
// Update cached entry with error if it exists — unless this invocation
|
|
64017
|
+
// was abandoned, in which case the entry may already belong to a
|
|
64018
|
+
// fresher retry and must not be marked with a stale error.
|
|
64019
|
+
if (!invocation.abandoned) {
|
|
64020
|
+
const cached = this.cache.get(key);
|
|
64021
|
+
if (cached) {
|
|
64022
|
+
cached.lastError = error;
|
|
64023
|
+
cached.isRefreshing = false;
|
|
64024
|
+
}
|
|
63627
64025
|
}
|
|
63628
64026
|
throw error;
|
|
63629
64027
|
}
|
|
@@ -63655,13 +64053,23 @@ class StampedeProtectedCache {
|
|
|
63655
64053
|
});
|
|
63656
64054
|
}
|
|
63657
64055
|
/**
|
|
63658
|
-
* Apply probabilistic jitter to expiration time
|
|
64056
|
+
* Apply probabilistic jitter to an entry's expiration time.
|
|
64057
|
+
*
|
|
64058
|
+
* The jitter must scale with the ENTRY's own TTL: using `defaultTtl` here
|
|
64059
|
+
* (as this method originally did) mis-anchored `createdAt` for any entry
|
|
64060
|
+
* cached with a custom TTL, swinging its effective expiry by up to
|
|
64061
|
+
* ±(defaultTtl - ttl) — a 50ms entry under a 5s default could randomly
|
|
64062
|
+
* read as already-expired at write time or fresh for 10x its TTL.
|
|
64063
|
+
*
|
|
64064
|
+
* @param originalExpiresAt - The entry's unjittered expiry timestamp (ms).
|
|
64065
|
+
* @param ttl - The TTL (ms) the entry was cached with.
|
|
64066
|
+
* @returns The jittered expiry timestamp.
|
|
63659
64067
|
*/
|
|
63660
|
-
applyJitter(originalExpiresAt) {
|
|
64068
|
+
applyJitter(originalExpiresAt, ttl) {
|
|
63661
64069
|
const range = this.options.maxJitter - this.options.minJitter;
|
|
63662
64070
|
const jitter = this.options.minJitter + Math.random() * range;
|
|
63663
|
-
const createdAt = originalExpiresAt -
|
|
63664
|
-
const jitteredTtl =
|
|
64071
|
+
const createdAt = originalExpiresAt - ttl;
|
|
64072
|
+
const jitteredTtl = ttl * jitter;
|
|
63665
64073
|
return createdAt + jitteredTtl;
|
|
63666
64074
|
}
|
|
63667
64075
|
/**
|
|
@@ -63746,7 +64154,7 @@ const DEFAULT_CACHE_OPTIONS = {
|
|
|
63746
64154
|
minJitter: 0.9, // 90%
|
|
63747
64155
|
maxJitter: 1.1, // 110%
|
|
63748
64156
|
enableBackgroundRefresh: true,
|
|
63749
|
-
loadTimeoutMs:
|
|
64157
|
+
loadTimeoutMs: DEFAULT_LOAD_TIMEOUT_MS, // hard loader ceiling (anti-pinning)
|
|
63750
64158
|
};
|
|
63751
64159
|
|
|
63752
64160
|
/**
|
|
@@ -70628,6 +71036,7 @@ exports.DEFAULT_RISK_FREE_RATE = DEFAULT_RISK_FREE_RATE;
|
|
|
70628
71036
|
exports.DEFAULT_TIMEOUTS = DEFAULT_TIMEOUTS;
|
|
70629
71037
|
exports.DEFAULT_TRADING_POLICY = DEFAULT_TRADING_POLICY;
|
|
70630
71038
|
exports.DataFormatError = DataFormatError;
|
|
71039
|
+
exports.DuplicateClientOrderIdError = DuplicateClientOrderIdError;
|
|
70631
71040
|
exports.HttpClientError = HttpClientError;
|
|
70632
71041
|
exports.HttpServerError = HttpServerError;
|
|
70633
71042
|
exports.KEEP_ALIVE_DEFAULTS = KEEP_ALIVE_DEFAULTS;
|