@adaptic/utils 0.0.1007 → 0.0.1009
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 +548 -104
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +549 -106
- 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 +47 -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 +2 -2
- 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.mjs
CHANGED
|
@@ -3,7 +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
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
7
7
|
import ms from 'ms';
|
|
8
8
|
import require$$0$1 from 'fs';
|
|
9
9
|
import require$$1 from 'path';
|
|
@@ -2274,6 +2274,80 @@ function isTransientNetworkError(error) {
|
|
|
2274
2274
|
}
|
|
2275
2275
|
return false;
|
|
2276
2276
|
}
|
|
2277
|
+
/**
|
|
2278
|
+
* Error names that indicate the CLIENT's own request deadline expired (an
|
|
2279
|
+
* `AbortSignal` timeout or an undici per-phase timeout) rather than a
|
|
2280
|
+
* connection-phase fault. These faults have already consumed a full request
|
|
2281
|
+
* timeout, so retrying them is expensive by construction.
|
|
2282
|
+
*/
|
|
2283
|
+
const DEADLINE_EXPIRY_ERROR_NAMES = new Set([
|
|
2284
|
+
"AbortError",
|
|
2285
|
+
"TimeoutError",
|
|
2286
|
+
"RequestTimeoutError",
|
|
2287
|
+
"ConnectTimeoutError",
|
|
2288
|
+
"HeadersTimeoutError",
|
|
2289
|
+
"BodyTimeoutError",
|
|
2290
|
+
]);
|
|
2291
|
+
/**
|
|
2292
|
+
* Error codes that indicate an expired request/phase deadline (vs a fast
|
|
2293
|
+
* connection-phase fault such as `ECONNRESET`/`ECONNREFUSED`).
|
|
2294
|
+
*/
|
|
2295
|
+
const DEADLINE_EXPIRY_ERROR_CODES = new Set([
|
|
2296
|
+
"ETIMEDOUT",
|
|
2297
|
+
"ESOCKETTIMEDOUT",
|
|
2298
|
+
"ECONNABORTED",
|
|
2299
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
2300
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
2301
|
+
"UND_ERR_BODY_TIMEOUT",
|
|
2302
|
+
]);
|
|
2303
|
+
/** Message-pattern fallback for deadline-expiry errors that lost name/code. */
|
|
2304
|
+
const DEADLINE_EXPIRY_MESSAGE_PATTERNS = [
|
|
2305
|
+
/timed out/i,
|
|
2306
|
+
/timeout/i,
|
|
2307
|
+
/aborted/i,
|
|
2308
|
+
];
|
|
2309
|
+
/**
|
|
2310
|
+
* Whether an error represents the client's OWN deadline expiring (abort /
|
|
2311
|
+
* timeout) rather than a connection-phase network fault. Both classes are
|
|
2312
|
+
* "transient" per {@link isTransientNetworkError}, but they have very
|
|
2313
|
+
* different retry economics: a connection fault (`ECONNRESET`, `EPIPE`,
|
|
2314
|
+
* refused socket) settles in milliseconds and is cheap to retry, while a
|
|
2315
|
+
* deadline expiry has already consumed the full per-attempt timeout — blindly
|
|
2316
|
+
* retrying it multiplies time-to-failure exactly when the caller most needs
|
|
2317
|
+
* to fail fast. Walks the `error.cause` chain like the transient classifier.
|
|
2318
|
+
*
|
|
2319
|
+
* @param error - The error to classify.
|
|
2320
|
+
* @returns true when the fault is a client deadline/abort expiry.
|
|
2321
|
+
*/
|
|
2322
|
+
function isClientDeadlineExpiry(error) {
|
|
2323
|
+
const MAX_CAUSE_DEPTH = 6;
|
|
2324
|
+
let current = error;
|
|
2325
|
+
for (let depth = 0; depth < MAX_CAUSE_DEPTH && current; depth++) {
|
|
2326
|
+
if (current instanceof Error || typeof current === "object") {
|
|
2327
|
+
const err = current;
|
|
2328
|
+
if (typeof err.name === "string" &&
|
|
2329
|
+
DEADLINE_EXPIRY_ERROR_NAMES.has(err.name)) {
|
|
2330
|
+
return true;
|
|
2331
|
+
}
|
|
2332
|
+
if (typeof err.code === "string" &&
|
|
2333
|
+
DEADLINE_EXPIRY_ERROR_CODES.has(err.code)) {
|
|
2334
|
+
return true;
|
|
2335
|
+
}
|
|
2336
|
+
if (typeof err.message === "string") {
|
|
2337
|
+
for (const pattern of DEADLINE_EXPIRY_MESSAGE_PATTERNS) {
|
|
2338
|
+
if (pattern.test(err.message)) {
|
|
2339
|
+
return true;
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
current = err.cause;
|
|
2344
|
+
}
|
|
2345
|
+
else {
|
|
2346
|
+
break;
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
return false;
|
|
2350
|
+
}
|
|
2277
2351
|
/**
|
|
2278
2352
|
* Analyzes an error and determines if it's retryable.
|
|
2279
2353
|
* @param error - The error to analyze
|
|
@@ -2801,6 +2875,33 @@ class DataFormatError extends AdapticUtilsError {
|
|
|
2801
2875
|
this.service = service;
|
|
2802
2876
|
}
|
|
2803
2877
|
}
|
|
2878
|
+
/**
|
|
2879
|
+
* Broker-side duplicate `client_order_id` rejection (Alpaca HTTP 422,
|
|
2880
|
+
* "client order id must be unique").
|
|
2881
|
+
*
|
|
2882
|
+
* Thrown by the order-creation paths of `AlpacaTradingAPI` so callers can
|
|
2883
|
+
* distinguish "this exact order was already submitted" from a genuine order
|
|
2884
|
+
* rejection. When {@link wasDerived} is `false` the id was caller-supplied and
|
|
2885
|
+
* the caller owns idempotency semantics (a legitimate repeat needs a new
|
|
2886
|
+
* explicit id or an `idempotencyNonce`). When `true`, the wrapper's automatic
|
|
2887
|
+
* recovery (existing-order lookup, then one salted resubmit) was exhausted.
|
|
2888
|
+
*
|
|
2889
|
+
* Never retryable with the same id — resubmitting the identical
|
|
2890
|
+
* `client_order_id` will 422 again.
|
|
2891
|
+
*/
|
|
2892
|
+
class DuplicateClientOrderIdError extends AlpacaApiError {
|
|
2893
|
+
clientOrderId;
|
|
2894
|
+
wasDerived;
|
|
2895
|
+
constructor(message,
|
|
2896
|
+
/** The `client_order_id` that collided broker-side. */
|
|
2897
|
+
clientOrderId,
|
|
2898
|
+
/** Whether the colliding id was derived by the wrapper (vs caller-supplied). */
|
|
2899
|
+
wasDerived, cause) {
|
|
2900
|
+
super(message, "DUPLICATE_CLIENT_ORDER_ID", 422, cause);
|
|
2901
|
+
this.clientOrderId = clientOrderId;
|
|
2902
|
+
this.wasDerived = wasDerived;
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2804
2905
|
|
|
2805
2906
|
/**
|
|
2806
2907
|
* Token bucket rate limiter for external API integrations
|
|
@@ -2898,8 +2999,11 @@ class TokenBucketRateLimiter {
|
|
|
2898
2999
|
async acquire() {
|
|
2899
3000
|
const logger = getLogger();
|
|
2900
3001
|
this.refill();
|
|
2901
|
-
|
|
2902
|
-
|
|
3002
|
+
// Require a WHOLE token: refill() accrues fractionally, and admitting on
|
|
3003
|
+
// any positive fraction would release a full request per accrual tick,
|
|
3004
|
+
// driving the bucket negative and overrunning the configured rate.
|
|
3005
|
+
if (this.tokens >= TOKENS_PER_REQUEST) {
|
|
3006
|
+
this.tokens -= TOKENS_PER_REQUEST;
|
|
2903
3007
|
logger.debug(`Rate limit token acquired for ${this.config.label}`, {
|
|
2904
3008
|
remainingTokens: this.tokens,
|
|
2905
3009
|
queueLength: this.queue.length,
|
|
@@ -2995,8 +3099,9 @@ class TokenBucketRateLimiter {
|
|
|
2995
3099
|
this.processingQueue = true;
|
|
2996
3100
|
const logger = getLogger();
|
|
2997
3101
|
try {
|
|
2998
|
-
|
|
2999
|
-
|
|
3102
|
+
// Whole-token admission — see the matching guard in acquire().
|
|
3103
|
+
while (this.queue.length > 0 && this.tokens >= TOKENS_PER_REQUEST) {
|
|
3104
|
+
this.tokens -= TOKENS_PER_REQUEST;
|
|
3000
3105
|
const next = this.queue.shift();
|
|
3001
3106
|
if (next) {
|
|
3002
3107
|
clearTimeout(next.timeoutHandle);
|
|
@@ -3158,6 +3263,23 @@ function transientRetryDelayMs(attempt) {
|
|
|
3158
3263
|
const ceiling = TRANSIENT_NETWORK_RETRY_BASE_MS * 2 ** attempt;
|
|
3159
3264
|
return Math.floor(Math.random() * ceiling);
|
|
3160
3265
|
}
|
|
3266
|
+
/**
|
|
3267
|
+
* Total-deadline multiple over the per-attempt client timeout for one
|
|
3268
|
+
* {@link AlpacaMarketDataAPI.makeRequest} call INCLUDING transient retries.
|
|
3269
|
+
* The ECONNRESET class this loop targets settles in milliseconds, so the
|
|
3270
|
+
* retries fit comfortably inside 1.5x the single-attempt timeout — while a
|
|
3271
|
+
* request whose attempts each consume the full client timeout is refused
|
|
3272
|
+
* further retries instead of stretching a hot-path read to a multi-minute
|
|
3273
|
+
* stall (worst case before this budget: 3 x (60s limiter wait + 30s fetch)).
|
|
3274
|
+
*/
|
|
3275
|
+
const TRANSIENT_RETRY_BUDGET_TIMEOUT_MULTIPLE = 1.5;
|
|
3276
|
+
/**
|
|
3277
|
+
* Maximum retries for a fault classified as the client's OWN deadline expiry
|
|
3278
|
+
* (see {@link isClientDeadlineExpiry}): such a fault already consumed a full
|
|
3279
|
+
* per-attempt timeout, so it is retried at most once — and then only if the
|
|
3280
|
+
* total budget still allows a further full-length attempt.
|
|
3281
|
+
*/
|
|
3282
|
+
const CLIENT_DEADLINE_EXPIRY_MAX_RETRIES = 1;
|
|
3161
3283
|
const log$l = (message, options = { type: "info" }) => {
|
|
3162
3284
|
log$m(message, { ...options, source: "AlpacaMarketDataAPI" });
|
|
3163
3285
|
};
|
|
@@ -3671,7 +3793,15 @@ class AlpacaMarketDataAPI extends EventEmitter {
|
|
|
3671
3793
|
// Retry ONLY transient connection faults, and only on GET (every
|
|
3672
3794
|
// market-data read here is idempotent). A non-2xx response is a real
|
|
3673
3795
|
// answer from Alpaca and is never retried — that path still throws on
|
|
3674
|
-
// the first attempt exactly as before.
|
|
3796
|
+
// the first attempt exactly as before. The whole loop is bounded by a
|
|
3797
|
+
// cumulative deadline so retries can never stretch a hot-path read far
|
|
3798
|
+
// beyond a single attempt's timeout: connection-phase faults
|
|
3799
|
+
// (ECONNRESET class, millisecond-scale) retry cheaply, while a fault
|
|
3800
|
+
// that consumed the full client timeout retries at most once and only
|
|
3801
|
+
// when the remaining budget still fits a full-length attempt.
|
|
3802
|
+
const retryLoopStartedAt = Date.now();
|
|
3803
|
+
const totalBudgetMs = Math.round(DEFAULT_TIMEOUTS.ALPACA_API * TRANSIENT_RETRY_BUDGET_TIMEOUT_MULTIPLE);
|
|
3804
|
+
let deadlineExpiryRetries = 0;
|
|
3675
3805
|
let response;
|
|
3676
3806
|
let lastNetworkError;
|
|
3677
3807
|
for (let attempt = 0; attempt < TRANSIENT_NETWORK_RETRY_ATTEMPTS; attempt += 1) {
|
|
@@ -3685,12 +3815,21 @@ class AlpacaMarketDataAPI extends EventEmitter {
|
|
|
3685
3815
|
}
|
|
3686
3816
|
catch (networkErr) {
|
|
3687
3817
|
lastNetworkError = networkErr;
|
|
3818
|
+
const deadlineExpiry = isClientDeadlineExpiry(networkErr);
|
|
3819
|
+
const nextAttemptFitsBudget = Date.now() - retryLoopStartedAt + DEFAULT_TIMEOUTS.ALPACA_API <=
|
|
3820
|
+
totalBudgetMs;
|
|
3688
3821
|
const retryable = method === "GET" &&
|
|
3689
3822
|
isTransientNetworkError(networkErr) &&
|
|
3690
|
-
attempt < TRANSIENT_NETWORK_RETRY_ATTEMPTS - 1
|
|
3823
|
+
attempt < TRANSIENT_NETWORK_RETRY_ATTEMPTS - 1 &&
|
|
3824
|
+
nextAttemptFitsBudget &&
|
|
3825
|
+
(!deadlineExpiry ||
|
|
3826
|
+
deadlineExpiryRetries < CLIENT_DEADLINE_EXPIRY_MAX_RETRIES);
|
|
3691
3827
|
if (!retryable) {
|
|
3692
3828
|
throw networkErr;
|
|
3693
3829
|
}
|
|
3830
|
+
if (deadlineExpiry) {
|
|
3831
|
+
deadlineExpiryRetries += 1;
|
|
3832
|
+
}
|
|
3694
3833
|
const delayMs = transientRetryDelayMs(attempt);
|
|
3695
3834
|
log$l(`Transient network fault on ${endpoint} (attempt ${attempt + 1}/${TRANSIENT_NETWORK_RETRY_ATTEMPTS}); retrying in ${delayMs}ms`, { type: "warn" });
|
|
3696
3835
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
@@ -3711,8 +3850,7 @@ class AlpacaMarketDataAPI extends EventEmitter {
|
|
|
3711
3850
|
});
|
|
3712
3851
|
throw new Error(`Market Data API error (${response.status}): ${errorText}`);
|
|
3713
3852
|
}
|
|
3714
|
-
|
|
3715
|
-
return data;
|
|
3853
|
+
return (await response.json());
|
|
3716
3854
|
}
|
|
3717
3855
|
catch (err) {
|
|
3718
3856
|
const error = err;
|
|
@@ -4399,7 +4537,7 @@ class AlpacaMarketDataAPI extends EventEmitter {
|
|
|
4399
4537
|
});
|
|
4400
4538
|
throw new Error(`Alpaca news API error (${response.status}): ${errorText}`);
|
|
4401
4539
|
}
|
|
4402
|
-
const data = await response.json();
|
|
4540
|
+
const data = (await response.json());
|
|
4403
4541
|
if (!data.news || !Array.isArray(data.news)) {
|
|
4404
4542
|
log$l(`No news data found in Alpaca response for ${symbol}`, {
|
|
4405
4543
|
type: "warn",
|
|
@@ -4482,9 +4620,45 @@ const CLIENT_ORDER_ID_HASH_LENGTH = 32;
|
|
|
4482
4620
|
* This derived default is a best-effort safety net; the guaranteed-idempotent
|
|
4483
4621
|
* path is for the caller to pass an explicit `clientOrderId` tied to the
|
|
4484
4622
|
* originating signal/decision id (which also permits legitimately-repeated
|
|
4485
|
-
* identical orders inside a single window).
|
|
4623
|
+
* identical orders inside a single window). Callers that intentionally repeat
|
|
4624
|
+
* an identical order inside one window without managing explicit ids can pass
|
|
4625
|
+
* an `idempotencyNonce` (e.g. an attempt counter or signal id) instead — the
|
|
4626
|
+
* nonce is folded into the derived id, so each distinct nonce yields a
|
|
4627
|
+
* distinct id while a timeout+retry of the SAME attempt still collides
|
|
4628
|
+
* broker-side as intended.
|
|
4629
|
+
*
|
|
4630
|
+
* When a DERIVED id is 422-rejected as a duplicate, {@link
|
|
4631
|
+
* AlpacaTradingAPI.postOrderWithIdempotencyRecovery} recovers instead of
|
|
4632
|
+
* failing the caller: if the previously-submitted order is still live (or
|
|
4633
|
+
* filled) it is returned as idempotent success; if it is terminally dead
|
|
4634
|
+
* (canceled/expired/rejected) the order is resubmitted exactly once with a
|
|
4635
|
+
* fresh random salt. Caller-SUPPLIED ids are never recovered — they surface a
|
|
4636
|
+
* typed {@link DuplicateClientOrderIdError} so the caller can distinguish a
|
|
4637
|
+
* duplicate from a genuine rejection.
|
|
4486
4638
|
*/
|
|
4487
4639
|
const CLIENT_ORDER_ID_WINDOW_MS = 300_000;
|
|
4640
|
+
/**
|
|
4641
|
+
* Matches Alpaca's 422 duplicate-idempotency-key rejection message. Alpaca has
|
|
4642
|
+
* used both "client_order_id must be unique" and "client order id must be
|
|
4643
|
+
* unique" across API revisions, so separators are matched loosely.
|
|
4644
|
+
*/
|
|
4645
|
+
const DUPLICATE_CLIENT_ORDER_ID_PATTERN = /client[\s_-]?order[\s_-]?id must be unique/i;
|
|
4646
|
+
/** HTTP status Alpaca uses for duplicate `client_order_id` rejections. */
|
|
4647
|
+
const DUPLICATE_CLIENT_ORDER_ID_STATUS = 422;
|
|
4648
|
+
/**
|
|
4649
|
+
* Order statuses in which a previously-submitted order can never execute.
|
|
4650
|
+
* A derived-id duplicate colliding with an order in one of these states is a
|
|
4651
|
+
* legitimate NEW order (e.g. cancel-then-recreate of an identical trailing
|
|
4652
|
+
* stop) and is resubmitted with a fresh salt; any other status means the
|
|
4653
|
+
* original order is live or executed, so it is returned as idempotent success.
|
|
4654
|
+
*/
|
|
4655
|
+
const TERMINAL_DEAD_ORDER_STATUSES = new Set([
|
|
4656
|
+
"canceled",
|
|
4657
|
+
"expired",
|
|
4658
|
+
"rejected",
|
|
4659
|
+
"replaced",
|
|
4660
|
+
"done_for_day",
|
|
4661
|
+
]);
|
|
4488
4662
|
/**
|
|
4489
4663
|
Websocket example
|
|
4490
4664
|
const alpacaAPI = createAlpacaTradingAPI(credentials); // type AlpacaCredentials
|
|
@@ -4592,6 +4766,142 @@ class AlpacaTradingAPI {
|
|
|
4592
4766
|
.slice(0, CLIENT_ORDER_ID_HASH_LENGTH);
|
|
4593
4767
|
return `${CLIENT_ORDER_ID_PREFIX}${digest}`;
|
|
4594
4768
|
}
|
|
4769
|
+
/**
|
|
4770
|
+
* Running count of derived-id duplicate collisions resolved by returning the
|
|
4771
|
+
* already-submitted order. Emitted in log metadata so operators can see the
|
|
4772
|
+
* idempotency net firing.
|
|
4773
|
+
*/
|
|
4774
|
+
idempotentDuplicateReturns = 0;
|
|
4775
|
+
/**
|
|
4776
|
+
* Running count of derived-id duplicate collisions resolved by resubmitting
|
|
4777
|
+
* once with a fresh random salt (the colliding order was terminally dead).
|
|
4778
|
+
*/
|
|
4779
|
+
saltedDuplicateResubmits = 0;
|
|
4780
|
+
/**
|
|
4781
|
+
* Whether an error thrown by {@link makeRequest} is Alpaca's 422
|
|
4782
|
+
* duplicate-`client_order_id` rejection.
|
|
4783
|
+
* @param error - The error thrown by the order POST.
|
|
4784
|
+
* @returns true when the error is a duplicate-idempotency-key rejection.
|
|
4785
|
+
*/
|
|
4786
|
+
isDuplicateClientOrderIdRejection(error) {
|
|
4787
|
+
if (!(error instanceof Error))
|
|
4788
|
+
return false;
|
|
4789
|
+
return (error.message.includes(`(${DUPLICATE_CLIENT_ORDER_ID_STATUS})`) &&
|
|
4790
|
+
DUPLICATE_CLIENT_ORDER_ID_PATTERN.test(error.message));
|
|
4791
|
+
}
|
|
4792
|
+
/**
|
|
4793
|
+
* Look up an order by its `client_order_id` (Alpaca
|
|
4794
|
+
* `GET /orders:by_client_order_id`).
|
|
4795
|
+
* @param clientOrderId - The idempotency key the order was submitted with.
|
|
4796
|
+
* @returns The order, or null when no order exists for the id (404).
|
|
4797
|
+
*/
|
|
4798
|
+
async getOrderByClientOrderId(clientOrderId) {
|
|
4799
|
+
try {
|
|
4800
|
+
return await this.makeRequest("/orders:by_client_order_id", "GET", undefined, `?client_order_id=${encodeURIComponent(clientOrderId)}`);
|
|
4801
|
+
}
|
|
4802
|
+
catch (error) {
|
|
4803
|
+
if (error instanceof Error && error.message.includes("(404)")) {
|
|
4804
|
+
return null;
|
|
4805
|
+
}
|
|
4806
|
+
throw error;
|
|
4807
|
+
}
|
|
4808
|
+
}
|
|
4809
|
+
/**
|
|
4810
|
+
* POST an order body with duplicate-`client_order_id` recovery.
|
|
4811
|
+
*
|
|
4812
|
+
* Sets `client_order_id` (explicit id wins; otherwise derived from
|
|
4813
|
+
* `deriveParts`), submits, and on Alpaca's 422 duplicate rejection:
|
|
4814
|
+
*
|
|
4815
|
+
* - **Caller-supplied id**: throws a typed
|
|
4816
|
+
* {@link DuplicateClientOrderIdError} — the caller owns idempotency
|
|
4817
|
+
* semantics and must decide whether the duplicate is success or a bug.
|
|
4818
|
+
* - **Derived id, colliding order live/filled**: returns the existing order
|
|
4819
|
+
* as idempotent success (this is the timeout+retry case the derived id
|
|
4820
|
+
* exists to de-duplicate).
|
|
4821
|
+
* - **Derived id, colliding order terminally dead** (canceled / expired /
|
|
4822
|
+
* rejected — e.g. cancel-then-recreate of an identical trailing stop):
|
|
4823
|
+
* resubmits exactly once with a fresh random salt so the legitimate new
|
|
4824
|
+
* order is not blocked. A second duplicate rejection throws the typed
|
|
4825
|
+
* error.
|
|
4826
|
+
* - **Derived id, status lookup fails**: fails CLOSED with the typed error —
|
|
4827
|
+
* the colliding order may be live, so resubmitting could double-fill; the
|
|
4828
|
+
* caller's next cycle retries when the lookup can succeed.
|
|
4829
|
+
*
|
|
4830
|
+
* Both recovery outcomes increment log-visible counters.
|
|
4831
|
+
*
|
|
4832
|
+
* @param body - The order payload (its `client_order_id` is set here).
|
|
4833
|
+
* @param options - Idempotency inputs: optional explicit id and the derive
|
|
4834
|
+
* parts used both for the default id and for the salted resubmit.
|
|
4835
|
+
* @returns The created (or pre-existing, on idempotent recovery) order.
|
|
4836
|
+
*/
|
|
4837
|
+
async postOrderWithIdempotencyRecovery(body, options) {
|
|
4838
|
+
const derived = options.explicitClientOrderId === undefined;
|
|
4839
|
+
const clientOrderId = options.explicitClientOrderId ??
|
|
4840
|
+
this.deriveClientOrderId(options.deriveParts);
|
|
4841
|
+
body.client_order_id = clientOrderId;
|
|
4842
|
+
const requestBody = body;
|
|
4843
|
+
try {
|
|
4844
|
+
return await this.makeRequest("/orders", "POST", requestBody);
|
|
4845
|
+
}
|
|
4846
|
+
catch (error) {
|
|
4847
|
+
if (!this.isDuplicateClientOrderIdRejection(error)) {
|
|
4848
|
+
throw error;
|
|
4849
|
+
}
|
|
4850
|
+
if (!derived) {
|
|
4851
|
+
throw new DuplicateClientOrderIdError(`Duplicate client_order_id "${clientOrderId}" rejected by Alpaca (caller-supplied id)`, clientOrderId, false, error);
|
|
4852
|
+
}
|
|
4853
|
+
let existing = null;
|
|
4854
|
+
try {
|
|
4855
|
+
existing = await this.getOrderByClientOrderId(clientOrderId);
|
|
4856
|
+
}
|
|
4857
|
+
catch (lookupError) {
|
|
4858
|
+
// Fail CLOSED: the 422 proves an order with this id exists, but its
|
|
4859
|
+
// status is unverifiable. Resubmitting here could double-fill a live
|
|
4860
|
+
// order (the timeout+retry case), which is strictly worse than the
|
|
4861
|
+
// caller retrying on its next cycle — by then the lookup will resolve.
|
|
4862
|
+
this.log(`Duplicate-order lookup failed for ${clientOrderId}; failing closed (no resubmit) to avoid a possible double order: ${lookupError instanceof Error
|
|
4863
|
+
? lookupError.message
|
|
4864
|
+
: String(lookupError)}`, { symbol: options.logSymbol, type: "error" });
|
|
4865
|
+
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);
|
|
4866
|
+
}
|
|
4867
|
+
if (existing && !TERMINAL_DEAD_ORDER_STATUSES.has(existing.status)) {
|
|
4868
|
+
this.idempotentDuplicateReturns++;
|
|
4869
|
+
this.log(`Derived client_order_id ${clientOrderId} already submitted (status=${existing.status}); returning existing order ${existing.id} as idempotent success`, {
|
|
4870
|
+
symbol: options.logSymbol,
|
|
4871
|
+
type: "warn",
|
|
4872
|
+
metadata: {
|
|
4873
|
+
outcome: "idempotent_return",
|
|
4874
|
+
idempotentDuplicateReturns: this.idempotentDuplicateReturns,
|
|
4875
|
+
},
|
|
4876
|
+
});
|
|
4877
|
+
return existing;
|
|
4878
|
+
}
|
|
4879
|
+
const saltedId = this.deriveClientOrderId([
|
|
4880
|
+
...options.deriveParts,
|
|
4881
|
+
"resubmit",
|
|
4882
|
+
randomUUID(),
|
|
4883
|
+
]);
|
|
4884
|
+
this.saltedDuplicateResubmits++;
|
|
4885
|
+
this.log(`Derived client_order_id ${clientOrderId} collided with a ${existing ? `terminal (${existing.status})` : "missing"} order; resubmitting once with fresh salt ${saltedId}`, {
|
|
4886
|
+
symbol: options.logSymbol,
|
|
4887
|
+
type: "warn",
|
|
4888
|
+
metadata: {
|
|
4889
|
+
outcome: "salted_resubmit",
|
|
4890
|
+
saltedDuplicateResubmits: this.saltedDuplicateResubmits,
|
|
4891
|
+
},
|
|
4892
|
+
});
|
|
4893
|
+
body.client_order_id = saltedId;
|
|
4894
|
+
try {
|
|
4895
|
+
return await this.makeRequest("/orders", "POST", requestBody);
|
|
4896
|
+
}
|
|
4897
|
+
catch (resubmitError) {
|
|
4898
|
+
if (this.isDuplicateClientOrderIdRejection(resubmitError)) {
|
|
4899
|
+
throw new DuplicateClientOrderIdError(`Salted resubmit of duplicate client_order_id "${clientOrderId}" was itself rejected as a duplicate ("${saltedId}")`, saltedId, true, resubmitError);
|
|
4900
|
+
}
|
|
4901
|
+
throw resubmitError;
|
|
4902
|
+
}
|
|
4903
|
+
}
|
|
4904
|
+
}
|
|
4595
4905
|
/**
|
|
4596
4906
|
* Collect the human-readable failure entries from a bulk Multi-Status (207)
|
|
4597
4907
|
* response body (`DELETE /orders`, `DELETE /positions`). Each element carries
|
|
@@ -4894,7 +5204,7 @@ class AlpacaTradingAPI {
|
|
|
4894
5204
|
}
|
|
4895
5205
|
const contentType = response.headers.get("content-type");
|
|
4896
5206
|
if (contentType && contentType.includes("application/json")) {
|
|
4897
|
-
return await response.json();
|
|
5207
|
+
return (await response.json());
|
|
4898
5208
|
}
|
|
4899
5209
|
// For non-JSON responses, return the text content
|
|
4900
5210
|
const textContent = await response.text();
|
|
@@ -5042,9 +5352,15 @@ class AlpacaTradingAPI {
|
|
|
5042
5352
|
* @param side (string) - the side of the order
|
|
5043
5353
|
* @param trailPercent100 (number) - the trail percent of the order (scale 100, i.e. 0.5 = 0.5%)
|
|
5044
5354
|
* @param position_intent (string) - the position intent of the order
|
|
5355
|
+
* @param clientOrderId - Optional explicit idempotency key; when supplied it
|
|
5356
|
+
* is used verbatim and duplicate rejections surface as
|
|
5357
|
+
* {@link DuplicateClientOrderIdError}.
|
|
5358
|
+
* @param idempotencyNonce - Optional attempt/signal discriminator folded into
|
|
5359
|
+
* the derived idempotency key so an intentionally-repeated identical order
|
|
5360
|
+
* inside one derivation window receives a distinct id.
|
|
5045
5361
|
* @returns The created AlpacaOrder with order ID and details
|
|
5046
5362
|
*/
|
|
5047
|
-
async createTrailingStop(symbol, qty, side, trailPercent100, position_intent, clientOrderId) {
|
|
5363
|
+
async createTrailingStop(symbol, qty, side, trailPercent100, position_intent, clientOrderId, idempotencyNonce) {
|
|
5048
5364
|
this.log(`Creating trailing stop ${side.toUpperCase()} ${qty} shares for ${symbol} with trail percent ${trailPercent100}%`, {
|
|
5049
5365
|
symbol,
|
|
5050
5366
|
});
|
|
@@ -5057,18 +5373,21 @@ class AlpacaTradingAPI {
|
|
|
5057
5373
|
type: "trailing_stop",
|
|
5058
5374
|
trail_percent: trailPercent100.toString(), // Already in decimal form (e.g., 4 for 4%)
|
|
5059
5375
|
time_in_force: "gtc",
|
|
5060
|
-
|
|
5061
|
-
|
|
5376
|
+
};
|
|
5377
|
+
try {
|
|
5378
|
+
const order = await this.postOrderWithIdempotencyRecovery(body, {
|
|
5379
|
+
explicitClientOrderId: clientOrderId,
|
|
5380
|
+
deriveParts: [
|
|
5062
5381
|
"trailing_stop",
|
|
5063
5382
|
symbol,
|
|
5064
5383
|
side,
|
|
5065
5384
|
position_intent,
|
|
5066
5385
|
Math.abs(qty),
|
|
5067
5386
|
trailPercent100,
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5387
|
+
...(idempotencyNonce !== undefined ? [String(idempotencyNonce)] : []),
|
|
5388
|
+
],
|
|
5389
|
+
logSymbol: symbol,
|
|
5390
|
+
});
|
|
5072
5391
|
this.log(`Trailing stop order created for ${symbol}: orderId=${order.id}, trailPercent=${trailPercent100}%`, { symbol });
|
|
5073
5392
|
return order;
|
|
5074
5393
|
}
|
|
@@ -5086,8 +5405,14 @@ class AlpacaTradingAPI {
|
|
|
5086
5405
|
* @param qty (number) - the quantity of the order
|
|
5087
5406
|
* @param side (string) - the side of the order
|
|
5088
5407
|
* @param position_intent (string) - the position intent of the order. Important for knowing if a position needs a trailing stop.
|
|
5408
|
+
* @param client_order_id - Optional explicit idempotency key; duplicate
|
|
5409
|
+
* rejections of an explicit id surface as
|
|
5410
|
+
* {@link DuplicateClientOrderIdError}.
|
|
5411
|
+
* @param idempotencyNonce - Optional attempt/signal discriminator folded into
|
|
5412
|
+
* the derived idempotency key so an intentionally-repeated identical order
|
|
5413
|
+
* inside one derivation window receives a distinct id.
|
|
5089
5414
|
*/
|
|
5090
|
-
async createMarketOrder(symbol, qty, side, position_intent, client_order_id) {
|
|
5415
|
+
async createMarketOrder(symbol, qty, side, position_intent, client_order_id, idempotencyNonce) {
|
|
5091
5416
|
this.log(`Creating market order for ${symbol}: ${side} ${qty} shares (${position_intent})`, {
|
|
5092
5417
|
symbol,
|
|
5093
5418
|
});
|
|
@@ -5100,17 +5425,19 @@ class AlpacaTradingAPI {
|
|
|
5100
5425
|
time_in_force: "day",
|
|
5101
5426
|
order_class: "simple",
|
|
5102
5427
|
};
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5428
|
+
try {
|
|
5429
|
+
return await this.postOrderWithIdempotencyRecovery(body, {
|
|
5430
|
+
explicitClientOrderId: client_order_id,
|
|
5431
|
+
deriveParts: [
|
|
5106
5432
|
"market",
|
|
5107
5433
|
symbol,
|
|
5108
5434
|
side,
|
|
5109
5435
|
position_intent,
|
|
5110
5436
|
Math.abs(qty),
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5437
|
+
...(idempotencyNonce !== undefined ? [String(idempotencyNonce)] : []),
|
|
5438
|
+
],
|
|
5439
|
+
logSymbol: symbol,
|
|
5440
|
+
});
|
|
5114
5441
|
}
|
|
5115
5442
|
catch (error) {
|
|
5116
5443
|
this.log(`Error creating market order: ${error}`, { type: "error" });
|
|
@@ -5267,9 +5594,14 @@ class AlpacaTradingAPI {
|
|
|
5267
5594
|
* @param limitPrice (number) - the limit price of the order
|
|
5268
5595
|
* @param position_intent (string) - the position intent of the order
|
|
5269
5596
|
* @param extended_hours (boolean) - whether the order is in extended hours
|
|
5270
|
-
* @param client_order_id
|
|
5597
|
+
* @param client_order_id - Optional explicit idempotency key; duplicate
|
|
5598
|
+
* rejections of an explicit id surface as
|
|
5599
|
+
* {@link DuplicateClientOrderIdError}.
|
|
5600
|
+
* @param idempotencyNonce - Optional attempt/signal discriminator folded into
|
|
5601
|
+
* the derived idempotency key so an intentionally-repeated identical order
|
|
5602
|
+
* inside one derivation window receives a distinct id.
|
|
5271
5603
|
*/
|
|
5272
|
-
async createLimitOrder(symbol, qty, side, limitPrice, position_intent, extended_hours = false, client_order_id) {
|
|
5604
|
+
async createLimitOrder(symbol, qty, side, limitPrice, position_intent, extended_hours = false, client_order_id, idempotencyNonce) {
|
|
5273
5605
|
this.log(`Creating limit order for ${symbol}: ${side} ${qty} shares at $${limitPrice.toFixed(2)} (${position_intent})`, {
|
|
5274
5606
|
symbol,
|
|
5275
5607
|
});
|
|
@@ -5284,9 +5616,10 @@ class AlpacaTradingAPI {
|
|
|
5284
5616
|
order_class: "simple",
|
|
5285
5617
|
extended_hours,
|
|
5286
5618
|
};
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5619
|
+
try {
|
|
5620
|
+
return await this.postOrderWithIdempotencyRecovery(body, {
|
|
5621
|
+
explicitClientOrderId: client_order_id,
|
|
5622
|
+
deriveParts: [
|
|
5290
5623
|
"limit",
|
|
5291
5624
|
symbol,
|
|
5292
5625
|
side,
|
|
@@ -5294,9 +5627,10 @@ class AlpacaTradingAPI {
|
|
|
5294
5627
|
Math.abs(qty),
|
|
5295
5628
|
this.roundPriceForAlpaca(limitPrice),
|
|
5296
5629
|
extended_hours,
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5630
|
+
...(idempotencyNonce !== undefined ? [String(idempotencyNonce)] : []),
|
|
5631
|
+
],
|
|
5632
|
+
logSymbol: symbol,
|
|
5633
|
+
});
|
|
5300
5634
|
}
|
|
5301
5635
|
catch (error) {
|
|
5302
5636
|
this.log(`Error creating limit order: ${error}`, { type: "error" });
|
|
@@ -5449,10 +5783,14 @@ class AlpacaTradingAPI {
|
|
|
5449
5783
|
* @param limitPrice Limit price (required for limit orders)
|
|
5450
5784
|
* @param clientOrderId Optional idempotency key; a deterministic one is
|
|
5451
5785
|
* derived from the order parameters when omitted so a client-timeout retry
|
|
5452
|
-
* is de-duplicated broker-side.
|
|
5786
|
+
* is de-duplicated broker-side. Duplicate rejections of an explicit id
|
|
5787
|
+
* surface as {@link DuplicateClientOrderIdError}.
|
|
5788
|
+
* @param idempotencyNonce Optional attempt/signal discriminator folded into
|
|
5789
|
+
* the derived idempotency key so an intentionally-repeated identical order
|
|
5790
|
+
* inside one derivation window receives a distinct id.
|
|
5453
5791
|
* @returns The created order
|
|
5454
5792
|
*/
|
|
5455
|
-
async createOptionOrder(symbol, qty, side, position_intent, type, limitPrice, clientOrderId) {
|
|
5793
|
+
async createOptionOrder(symbol, qty, side, position_intent, type, limitPrice, clientOrderId, idempotencyNonce) {
|
|
5456
5794
|
if (!Number.isInteger(qty) || qty <= 0) {
|
|
5457
5795
|
this.log("Quantity must be a positive whole number for option orders", {
|
|
5458
5796
|
type: "error",
|
|
@@ -5477,20 +5815,22 @@ class AlpacaTradingAPI {
|
|
|
5477
5815
|
if (type === "limit" && limitPrice !== undefined) {
|
|
5478
5816
|
orderData.limit_price = this.roundPriceForAlpaca(limitPrice).toString();
|
|
5479
5817
|
}
|
|
5480
|
-
orderData
|
|
5481
|
-
clientOrderId
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
])
|
|
5493
|
-
|
|
5818
|
+
return this.postOrderWithIdempotencyRecovery(orderData, {
|
|
5819
|
+
explicitClientOrderId: clientOrderId,
|
|
5820
|
+
deriveParts: [
|
|
5821
|
+
"option",
|
|
5822
|
+
type,
|
|
5823
|
+
symbol,
|
|
5824
|
+
side,
|
|
5825
|
+
position_intent,
|
|
5826
|
+
qty,
|
|
5827
|
+
type === "limit" && limitPrice !== undefined
|
|
5828
|
+
? this.roundPriceForAlpaca(limitPrice)
|
|
5829
|
+
: undefined,
|
|
5830
|
+
...(idempotencyNonce !== undefined ? [String(idempotencyNonce)] : []),
|
|
5831
|
+
],
|
|
5832
|
+
logSymbol: symbol,
|
|
5833
|
+
});
|
|
5494
5834
|
}
|
|
5495
5835
|
/**
|
|
5496
5836
|
* Create a multi-leg option order
|
|
@@ -5500,10 +5840,14 @@ class AlpacaTradingAPI {
|
|
|
5500
5840
|
* @param limitPrice Limit price (required for limit orders)
|
|
5501
5841
|
* @param clientOrderId Optional idempotency key; a deterministic one is
|
|
5502
5842
|
* derived from the legs and order parameters when omitted so a
|
|
5503
|
-
* client-timeout retry is de-duplicated broker-side.
|
|
5843
|
+
* client-timeout retry is de-duplicated broker-side. Duplicate rejections
|
|
5844
|
+
* of an explicit id surface as {@link DuplicateClientOrderIdError}.
|
|
5845
|
+
* @param idempotencyNonce Optional attempt/signal discriminator folded into
|
|
5846
|
+
* the derived idempotency key so an intentionally-repeated identical order
|
|
5847
|
+
* inside one derivation window receives a distinct id.
|
|
5504
5848
|
* @returns The created multi-leg order
|
|
5505
5849
|
*/
|
|
5506
|
-
async createMultiLegOptionOrder(legs, qty, type, limitPrice, clientOrderId) {
|
|
5850
|
+
async createMultiLegOptionOrder(legs, qty, type, limitPrice, clientOrderId, idempotencyNonce) {
|
|
5507
5851
|
if (!Number.isInteger(qty) || qty <= 0) {
|
|
5508
5852
|
this.log("Quantity must be a positive whole number for option orders", {
|
|
5509
5853
|
type: "error",
|
|
@@ -5529,18 +5873,20 @@ class AlpacaTradingAPI {
|
|
|
5529
5873
|
if (type === "limit" && limitPrice !== undefined) {
|
|
5530
5874
|
orderData.limit_price = this.roundPriceForAlpaca(limitPrice).toString();
|
|
5531
5875
|
}
|
|
5532
|
-
orderData
|
|
5533
|
-
clientOrderId
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
])
|
|
5543
|
-
|
|
5876
|
+
return this.postOrderWithIdempotencyRecovery(orderData, {
|
|
5877
|
+
explicitClientOrderId: clientOrderId,
|
|
5878
|
+
deriveParts: [
|
|
5879
|
+
"mleg",
|
|
5880
|
+
type,
|
|
5881
|
+
qty,
|
|
5882
|
+
type === "limit" && limitPrice !== undefined
|
|
5883
|
+
? this.roundPriceForAlpaca(limitPrice)
|
|
5884
|
+
: undefined,
|
|
5885
|
+
...legs.map((leg) => `${leg.symbol}:${leg.side}:${leg.ratio_qty}:${leg.position_intent}`),
|
|
5886
|
+
...(idempotencyNonce !== undefined ? [String(idempotencyNonce)] : []),
|
|
5887
|
+
],
|
|
5888
|
+
logSymbol: legSymbols,
|
|
5889
|
+
});
|
|
5544
5890
|
}
|
|
5545
5891
|
/**
|
|
5546
5892
|
* Exercise an option contract
|
|
@@ -5913,7 +6259,7 @@ class AlpacaTradingAPI {
|
|
|
5913
6259
|
*/
|
|
5914
6260
|
async createEquitiesTrade(params, options) {
|
|
5915
6261
|
const { symbol, qty, side, referencePrice } = params;
|
|
5916
|
-
const { type = "market", limitPrice, extendedHours = false, useStopLoss = false, stopPrice, stopPercent100, useTakeProfit = false, takeProfitPrice, takeProfitPercent100, clientOrderId, } = options || {};
|
|
6262
|
+
const { type = "market", limitPrice, extendedHours = false, useStopLoss = false, stopPrice, stopPercent100, useTakeProfit = false, takeProfitPrice, takeProfitPercent100, clientOrderId, idempotencyNonce, } = options || {};
|
|
5917
6263
|
// Validation: Extended hours + market order is not allowed
|
|
5918
6264
|
if (extendedHours && type === "market") {
|
|
5919
6265
|
this.log("Cannot create market order with extended hours enabled", {
|
|
@@ -6017,26 +6363,25 @@ class AlpacaTradingAPI {
|
|
|
6017
6363
|
extended_hours: extendedHours,
|
|
6018
6364
|
position_intent: side === "buy" ? "buy_to_open" : "sell_to_open",
|
|
6019
6365
|
};
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
]);
|
|
6366
|
+
const deriveParts = [
|
|
6367
|
+
"equities",
|
|
6368
|
+
orderClass,
|
|
6369
|
+
type,
|
|
6370
|
+
symbol,
|
|
6371
|
+
side,
|
|
6372
|
+
Math.abs(qty),
|
|
6373
|
+
type === "limit" && limitPrice !== undefined
|
|
6374
|
+
? this.roundPriceForAlpaca(limitPrice)
|
|
6375
|
+
: undefined,
|
|
6376
|
+
extendedHours,
|
|
6377
|
+
useStopLoss && calculatedStopPrice !== undefined
|
|
6378
|
+
? this.roundPriceForAlpaca(calculatedStopPrice)
|
|
6379
|
+
: undefined,
|
|
6380
|
+
useTakeProfit && calculatedTakeProfitPrice !== undefined
|
|
6381
|
+
? this.roundPriceForAlpaca(calculatedTakeProfitPrice)
|
|
6382
|
+
: undefined,
|
|
6383
|
+
...(idempotencyNonce !== undefined ? [String(idempotencyNonce)] : []),
|
|
6384
|
+
];
|
|
6040
6385
|
// Add limit price for limit orders
|
|
6041
6386
|
if (type === "limit" && limitPrice !== undefined) {
|
|
6042
6387
|
orderData.limit_price = this.roundPriceForAlpaca(limitPrice).toString();
|
|
@@ -6060,7 +6405,11 @@ class AlpacaTradingAPI {
|
|
|
6060
6405
|
symbol,
|
|
6061
6406
|
});
|
|
6062
6407
|
try {
|
|
6063
|
-
return await this.
|
|
6408
|
+
return await this.postOrderWithIdempotencyRecovery(orderData, {
|
|
6409
|
+
explicitClientOrderId: clientOrderId,
|
|
6410
|
+
deriveParts,
|
|
6411
|
+
logSymbol: symbol,
|
|
6412
|
+
});
|
|
6064
6413
|
}
|
|
6065
6414
|
catch (error) {
|
|
6066
6415
|
this.log(`Error creating equities trade: ${error}`, {
|
|
@@ -11043,7 +11392,9 @@ async function calculateTotalReturnYTD(portfolioHistory) {
|
|
|
11043
11392
|
* @param accountId - The ID of the Alpaca account.
|
|
11044
11393
|
* @param client - The Apollo client instance.
|
|
11045
11394
|
* @param alpacaAccount - The Alpaca account object.
|
|
11046
|
-
* @returns A promise that resolves to a string representing the expense ratio
|
|
11395
|
+
* @returns A promise that resolves to a string representing the expense ratio
|
|
11396
|
+
* in percentage format, or "N/A" when the ratio cannot be computed honestly
|
|
11397
|
+
* (missing account, fee-fetch failure, or non-positive/non-finite equity).
|
|
11047
11398
|
*/
|
|
11048
11399
|
async function calculateExpenseRatio({ accountId, client, alpacaAccount, }) {
|
|
11049
11400
|
if (!accountId && !alpacaAccount && !client) {
|
|
@@ -11077,12 +11428,15 @@ async function calculateExpenseRatio({ accountId, client, alpacaAccount, }) {
|
|
|
11077
11428
|
return "N/A";
|
|
11078
11429
|
}
|
|
11079
11430
|
}
|
|
11080
|
-
// Validate equity
|
|
11081
|
-
|
|
11082
|
-
|
|
11431
|
+
// Validate equity. A drained or freshly-funded account reports equity "0"
|
|
11432
|
+
// (and a broken feed can yield NaN or a negative string); dividing by it
|
|
11433
|
+
// would fabricate "Infinity%"/"NaN%" — return the deliberate "N/A"
|
|
11434
|
+
// (unknown) instead, consistent with the fee-fetch failure path below.
|
|
11435
|
+
const equity = parseFloat(accountDetails.equity);
|
|
11436
|
+
if (!Number.isFinite(equity) || equity <= 0) {
|
|
11437
|
+
getLogger().warn("Non-positive or non-finite equity value; cannot compute expense ratio.", { equity: accountDetails.equity });
|
|
11083
11438
|
return "N/A";
|
|
11084
11439
|
}
|
|
11085
|
-
const equity = parseFloat(accountDetails.equity);
|
|
11086
11440
|
// Fetch the account's real trailing fee expenses from Alpaca account
|
|
11087
11441
|
// activities. A genuine data-source failure yields "N/A" (unknown) rather
|
|
11088
11442
|
// than a fabricated 0.00%.
|
|
@@ -11408,8 +11762,13 @@ async function calculateAlphaAndBeta(portfolioHistory, benchmarkBars) {
|
|
|
11408
11762
|
const alignedPortfolioReturns = [];
|
|
11409
11763
|
const alignedBenchmarkReturns = [];
|
|
11410
11764
|
for (const timestamp of commonTimestamps) {
|
|
11765
|
+
// commonTimestamps is the key intersection of both maps, so both lookups
|
|
11766
|
+
// are guaranteed present; the guard replaces a non-null assertion.
|
|
11411
11767
|
const portfolioRet = portfolioReturnsMap.get(timestamp);
|
|
11412
11768
|
const benchmarkRet = benchmarkReturnsMap.get(timestamp);
|
|
11769
|
+
if (portfolioRet === undefined || benchmarkRet === undefined) {
|
|
11770
|
+
continue;
|
|
11771
|
+
}
|
|
11413
11772
|
if (isFinite(portfolioRet) && isFinite(benchmarkRet)) {
|
|
11414
11773
|
alignedPortfolioReturns.push(portfolioRet);
|
|
11415
11774
|
alignedBenchmarkReturns.push(benchmarkRet);
|
|
@@ -11670,8 +12029,13 @@ function alignReturnsByDate(portfolioHistory, benchmarkBars) {
|
|
|
11670
12029
|
const alignedPortfolioReturns = [];
|
|
11671
12030
|
const alignedBenchmarkReturns = [];
|
|
11672
12031
|
for (const timestamp of commonTimestamps) {
|
|
12032
|
+
// commonTimestamps is the key intersection of both maps, so both lookups
|
|
12033
|
+
// are guaranteed present; the guard replaces a non-null assertion.
|
|
11673
12034
|
const portfolioRet = portfolioReturnsMap.get(timestamp);
|
|
11674
12035
|
const benchmarkRet = benchmarkReturnsMap.get(timestamp);
|
|
12036
|
+
if (portfolioRet === undefined || benchmarkRet === undefined) {
|
|
12037
|
+
continue;
|
|
12038
|
+
}
|
|
11675
12039
|
alignedPortfolioReturns.push(portfolioRet);
|
|
11676
12040
|
alignedBenchmarkReturns.push(benchmarkRet);
|
|
11677
12041
|
}
|
|
@@ -11788,8 +12152,13 @@ async function calculateInformationRatio(portfolioHistory, benchmarkBars) {
|
|
|
11788
12152
|
// Extract aligned returns
|
|
11789
12153
|
const activeReturns = [];
|
|
11790
12154
|
for (const timestamp of commonTimestamps) {
|
|
12155
|
+
// commonTimestamps is the key intersection of both maps, so both lookups
|
|
12156
|
+
// are guaranteed present; the guard replaces a non-null assertion.
|
|
11791
12157
|
const portfolioRet = portfolioReturnsMap.get(timestamp);
|
|
11792
12158
|
const benchmarkRet = benchmarkReturnsMap.get(timestamp);
|
|
12159
|
+
if (portfolioRet === undefined || benchmarkRet === undefined) {
|
|
12160
|
+
continue;
|
|
12161
|
+
}
|
|
11793
12162
|
activeReturns.push(portfolioRet - benchmarkRet);
|
|
11794
12163
|
}
|
|
11795
12164
|
const n = activeReturns.length;
|
|
@@ -63147,6 +63516,14 @@ class LRUCache {
|
|
|
63147
63516
|
}
|
|
63148
63517
|
}
|
|
63149
63518
|
|
|
63519
|
+
/**
|
|
63520
|
+
* Default hard ceiling (ms) on a single loader invocation. Resolved at
|
|
63521
|
+
* construction time into `options.loadTimeoutMs` so the effective value is
|
|
63522
|
+
* always a validated number (see the constructor guard) — mirrors the
|
|
63523
|
+
* engine-local copy's constructor-time resolution ahead of consolidating the
|
|
63524
|
+
* two implementations onto this one.
|
|
63525
|
+
*/
|
|
63526
|
+
const DEFAULT_LOAD_TIMEOUT_MS = 30_000;
|
|
63150
63527
|
/**
|
|
63151
63528
|
* StampedeProtectedCache provides three-layer protection against cache stampedes
|
|
63152
63529
|
*
|
|
@@ -63215,12 +63592,17 @@ class StampedeProtectedCache {
|
|
|
63215
63592
|
loadTimeouts: 0,
|
|
63216
63593
|
};
|
|
63217
63594
|
constructor(options) {
|
|
63595
|
+
if (options.loadTimeoutMs !== undefined &&
|
|
63596
|
+
(!Number.isFinite(options.loadTimeoutMs) || options.loadTimeoutMs <= 0)) {
|
|
63597
|
+
throw new RangeError(`StampedeProtectedCache loadTimeoutMs must be a positive finite number of milliseconds; received ${String(options.loadTimeoutMs)}`);
|
|
63598
|
+
}
|
|
63218
63599
|
this.options = {
|
|
63219
63600
|
...options,
|
|
63220
63601
|
staleWhileRevalidateTtl: options.staleWhileRevalidateTtl ?? options.defaultTtl * 2,
|
|
63221
63602
|
minJitter: options.minJitter ?? 0.9,
|
|
63222
63603
|
maxJitter: options.maxJitter ?? 1.1,
|
|
63223
63604
|
enableBackgroundRefresh: options.enableBackgroundRefresh ?? true,
|
|
63605
|
+
loadTimeoutMs: options.loadTimeoutMs ?? DEFAULT_LOAD_TIMEOUT_MS,
|
|
63224
63606
|
logger: options.logger ?? {
|
|
63225
63607
|
debug: () => { },
|
|
63226
63608
|
info: () => { },
|
|
@@ -63234,6 +63616,15 @@ class StampedeProtectedCache {
|
|
|
63234
63616
|
allowStale: true,
|
|
63235
63617
|
updateAgeOnGet: false,
|
|
63236
63618
|
updateAgeOnHas: false,
|
|
63619
|
+
// LRU discard visibility for the onEvent observability hook. lru-cache
|
|
63620
|
+
// invokes dispose for every removal; only capacity discards ("evict")
|
|
63621
|
+
// are reported as evictions so deliberate delete()/invalidate() calls
|
|
63622
|
+
// do not inflate the eviction signal.
|
|
63623
|
+
dispose: (_value, key, reason) => {
|
|
63624
|
+
if (reason === "evict") {
|
|
63625
|
+
this.emitEvent("eviction", key);
|
|
63626
|
+
}
|
|
63627
|
+
},
|
|
63237
63628
|
});
|
|
63238
63629
|
this.options.logger.info("StampedeProtectedCache initialized", {
|
|
63239
63630
|
maxSize: this.options.maxSize,
|
|
@@ -63297,10 +63688,11 @@ class StampedeProtectedCache {
|
|
|
63297
63688
|
cached.accessCount++;
|
|
63298
63689
|
cached.lastAccessedAt = now;
|
|
63299
63690
|
// Check if entry is still fresh (considering probabilistic expiration)
|
|
63300
|
-
const jitteredExpiresAt = this.applyJitter(cached.expiresAt);
|
|
63691
|
+
const jitteredExpiresAt = this.applyJitter(cached.expiresAt, cached.ttl);
|
|
63301
63692
|
if (now < jitteredExpiresAt) {
|
|
63302
63693
|
// Fresh hit
|
|
63303
63694
|
this.stats.hits++;
|
|
63695
|
+
this.emitEvent("hit", key);
|
|
63304
63696
|
this.options.logger.debug("Cache hit (fresh)", {
|
|
63305
63697
|
key,
|
|
63306
63698
|
age: now - cached.createdAt,
|
|
@@ -63312,6 +63704,7 @@ class StampedeProtectedCache {
|
|
|
63312
63704
|
if (now < staleExpiresAt && !cached.isRefreshing) {
|
|
63313
63705
|
// Serve stale and trigger background refresh
|
|
63314
63706
|
this.stats.staleHits++;
|
|
63707
|
+
this.emitEvent("stale_hit", key);
|
|
63315
63708
|
this.options.logger.debug("Cache hit (stale-while-revalidate)", {
|
|
63316
63709
|
key,
|
|
63317
63710
|
age: now - cached.createdAt,
|
|
@@ -63325,6 +63718,7 @@ class StampedeProtectedCache {
|
|
|
63325
63718
|
}
|
|
63326
63719
|
// Cache miss or expired - need to load
|
|
63327
63720
|
this.stats.misses++;
|
|
63721
|
+
this.emitEvent("miss", key);
|
|
63328
63722
|
this.options.logger.debug("Cache miss", { key, hadCached: !!cached });
|
|
63329
63723
|
return this.loadWithCoalescing(key, loader, effectiveTtl);
|
|
63330
63724
|
}
|
|
@@ -63402,6 +63796,25 @@ class StampedeProtectedCache {
|
|
|
63402
63796
|
* cache.delete(`positions:${accountId}`);
|
|
63403
63797
|
* ```
|
|
63404
63798
|
*/
|
|
63799
|
+
/**
|
|
63800
|
+
* Fire the {@link StampedeProtectedCacheOptions.onEvent} hook, never
|
|
63801
|
+
* letting an observer failure propagate into the cache path.
|
|
63802
|
+
*/
|
|
63803
|
+
emitEvent(event, key) {
|
|
63804
|
+
const hook = this.options.onEvent;
|
|
63805
|
+
if (!hook)
|
|
63806
|
+
return;
|
|
63807
|
+
try {
|
|
63808
|
+
hook(event, key);
|
|
63809
|
+
}
|
|
63810
|
+
catch (err) {
|
|
63811
|
+
this.options.logger.warn("cache onEvent observer threw — ignored", {
|
|
63812
|
+
event,
|
|
63813
|
+
key,
|
|
63814
|
+
error: err instanceof Error ? err.message : String(err),
|
|
63815
|
+
});
|
|
63816
|
+
}
|
|
63817
|
+
}
|
|
63405
63818
|
delete(key) {
|
|
63406
63819
|
const deleted = this.cache.delete(key);
|
|
63407
63820
|
if (deleted) {
|
|
@@ -63534,6 +63947,7 @@ class StampedeProtectedCache {
|
|
|
63534
63947
|
const existingPromise = this.pendingRefreshes.get(key);
|
|
63535
63948
|
if (existingPromise) {
|
|
63536
63949
|
this.stats.coalescedRequests++;
|
|
63950
|
+
this.emitEvent("coalesced", key);
|
|
63537
63951
|
this.options.logger.debug("Request coalesced", { key });
|
|
63538
63952
|
return existingPromise;
|
|
63539
63953
|
}
|
|
@@ -63560,12 +63974,17 @@ class StampedeProtectedCache {
|
|
|
63560
63974
|
* is logged rather than surfacing as an unhandled rejection.
|
|
63561
63975
|
*/
|
|
63562
63976
|
async loadWithTimeout(key, loader, ttl) {
|
|
63563
|
-
const timeoutMs = this.options.loadTimeoutMs
|
|
63564
|
-
const
|
|
63977
|
+
const timeoutMs = this.options.loadTimeoutMs;
|
|
63978
|
+
const invocation = { abandoned: false };
|
|
63979
|
+
const loadPromise = this.loadAndCache(key, loader, ttl, invocation);
|
|
63565
63980
|
let timeoutHandle;
|
|
63566
63981
|
const timeoutPromise = new Promise((_, reject) => {
|
|
63567
63982
|
timeoutHandle = setTimeout(() => {
|
|
63568
63983
|
this.stats.loadTimeouts++;
|
|
63984
|
+
this.emitEvent("load_timeout", key);
|
|
63985
|
+
// Mark the invocation abandoned BEFORE evicting the pin: a retry that
|
|
63986
|
+
// starts now must never be overwritten by this loader's late result.
|
|
63987
|
+
invocation.abandoned = true;
|
|
63569
63988
|
// Evict the pin so the next caller retries fresh.
|
|
63570
63989
|
this.pendingRefreshes.delete(key);
|
|
63571
63990
|
this.options.logger.warn("Cache loader timed out — pin evicted", {
|
|
@@ -63598,11 +64017,19 @@ class StampedeProtectedCache {
|
|
|
63598
64017
|
/**
|
|
63599
64018
|
* Load data and cache it
|
|
63600
64019
|
*/
|
|
63601
|
-
async loadAndCache(key, loader, ttl) {
|
|
64020
|
+
async loadAndCache(key, loader, ttl, invocation = { abandoned: false }) {
|
|
63602
64021
|
const startTime = Date.now();
|
|
63603
64022
|
try {
|
|
63604
64023
|
this.options.logger.debug("Loading data", { key });
|
|
63605
64024
|
const value = await loader(key);
|
|
64025
|
+
if (invocation.abandoned) {
|
|
64026
|
+
// The pin was evicted at loadTimeoutMs and a retry may have cached
|
|
64027
|
+
// fresher data since; writing this late value would overwrite it with
|
|
64028
|
+
// a snapshot fetched before/through the hang.
|
|
64029
|
+
const loadTime = Date.now() - startTime;
|
|
64030
|
+
this.options.logger.warn("Abandoned cache loader resolved late — result discarded", { key, loadTime });
|
|
64031
|
+
return value;
|
|
64032
|
+
}
|
|
63606
64033
|
// Cache the loaded value
|
|
63607
64034
|
this.set(key, value, ttl);
|
|
63608
64035
|
const loadTime = Date.now() - startTime;
|
|
@@ -63611,17 +64038,22 @@ class StampedeProtectedCache {
|
|
|
63611
64038
|
}
|
|
63612
64039
|
catch (error) {
|
|
63613
64040
|
this.stats.refreshErrors++;
|
|
64041
|
+
this.emitEvent("refresh_error", key);
|
|
63614
64042
|
const loadTime = Date.now() - startTime;
|
|
63615
64043
|
this.options.logger.error("Failed to load data", {
|
|
63616
64044
|
key,
|
|
63617
64045
|
error,
|
|
63618
64046
|
loadTime,
|
|
63619
64047
|
});
|
|
63620
|
-
// Update cached entry with error if it exists
|
|
63621
|
-
|
|
63622
|
-
|
|
63623
|
-
|
|
63624
|
-
cached
|
|
64048
|
+
// Update cached entry with error if it exists — unless this invocation
|
|
64049
|
+
// was abandoned, in which case the entry may already belong to a
|
|
64050
|
+
// fresher retry and must not be marked with a stale error.
|
|
64051
|
+
if (!invocation.abandoned) {
|
|
64052
|
+
const cached = this.cache.get(key);
|
|
64053
|
+
if (cached) {
|
|
64054
|
+
cached.lastError = error;
|
|
64055
|
+
cached.isRefreshing = false;
|
|
64056
|
+
}
|
|
63625
64057
|
}
|
|
63626
64058
|
throw error;
|
|
63627
64059
|
}
|
|
@@ -63639,6 +64071,7 @@ class StampedeProtectedCache {
|
|
|
63639
64071
|
this.loadWithCoalescing(key, loader, ttl)
|
|
63640
64072
|
.then(() => {
|
|
63641
64073
|
this.stats.backgroundRefreshes++;
|
|
64074
|
+
this.emitEvent("background_refresh", key);
|
|
63642
64075
|
this.options.logger.debug("Background refresh completed", { key });
|
|
63643
64076
|
})
|
|
63644
64077
|
.catch((error) => {
|
|
@@ -63653,13 +64086,23 @@ class StampedeProtectedCache {
|
|
|
63653
64086
|
});
|
|
63654
64087
|
}
|
|
63655
64088
|
/**
|
|
63656
|
-
* Apply probabilistic jitter to expiration time
|
|
64089
|
+
* Apply probabilistic jitter to an entry's expiration time.
|
|
64090
|
+
*
|
|
64091
|
+
* The jitter must scale with the ENTRY's own TTL: using `defaultTtl` here
|
|
64092
|
+
* (as this method originally did) mis-anchored `createdAt` for any entry
|
|
64093
|
+
* cached with a custom TTL, swinging its effective expiry by up to
|
|
64094
|
+
* ±(defaultTtl - ttl) — a 50ms entry under a 5s default could randomly
|
|
64095
|
+
* read as already-expired at write time or fresh for 10x its TTL.
|
|
64096
|
+
*
|
|
64097
|
+
* @param originalExpiresAt - The entry's unjittered expiry timestamp (ms).
|
|
64098
|
+
* @param ttl - The TTL (ms) the entry was cached with.
|
|
64099
|
+
* @returns The jittered expiry timestamp.
|
|
63657
64100
|
*/
|
|
63658
|
-
applyJitter(originalExpiresAt) {
|
|
64101
|
+
applyJitter(originalExpiresAt, ttl) {
|
|
63659
64102
|
const range = this.options.maxJitter - this.options.minJitter;
|
|
63660
64103
|
const jitter = this.options.minJitter + Math.random() * range;
|
|
63661
|
-
const createdAt = originalExpiresAt -
|
|
63662
|
-
const jitteredTtl =
|
|
64104
|
+
const createdAt = originalExpiresAt - ttl;
|
|
64105
|
+
const jitteredTtl = ttl * jitter;
|
|
63663
64106
|
return createdAt + jitteredTtl;
|
|
63664
64107
|
}
|
|
63665
64108
|
/**
|
|
@@ -63744,7 +64187,7 @@ const DEFAULT_CACHE_OPTIONS = {
|
|
|
63744
64187
|
minJitter: 0.9, // 90%
|
|
63745
64188
|
maxJitter: 1.1, // 110%
|
|
63746
64189
|
enableBackgroundRefresh: true,
|
|
63747
|
-
loadTimeoutMs:
|
|
64190
|
+
loadTimeoutMs: DEFAULT_LOAD_TIMEOUT_MS, // hard loader ceiling (anti-pinning)
|
|
63748
64191
|
};
|
|
63749
64192
|
|
|
63750
64193
|
/**
|
|
@@ -70588,5 +71031,5 @@ const adaptic = {
|
|
|
70588
71031
|
};
|
|
70589
71032
|
const adptc = adaptic;
|
|
70590
71033
|
|
|
70591
|
-
export { API_RETRY_CONFIGS, AVNewsArticleSchema, AVNewsResponseSchema, AdapticUtilsError, AlpacaAccountDetailsSchema, AlpacaApiError, AlpacaBarSchema, AlpacaClient, AlpacaCryptoBarsResponseSchema, AlpacaHistoricalBarsResponseSchema, AlpacaLatestBarsResponseSchema, AlpacaLatestQuotesResponseSchema, AlpacaLatestTradesResponseSchema, AlpacaMarketDataAPI, AlpacaNewsArticleSchema, AlpacaNewsResponseSchema, AlpacaOrderSchema, AlpacaOrdersArraySchema, AlpacaPortfolioHistoryResponseSchema, AlpacaPositionSchema, AlpacaPositionsArraySchema, AlpacaQuoteSchema, AlpacaTradeSchema, AlpacaTradingAPI, AlphaVantageError, AlphaVantageQuoteResponseSchema, AssetAllocationEngine, AuthenticationError, AutonomyMode, BTC_PAIRS, BarError, CircuitOpenError, CryptoDataError, CryptoOrderError, DEFAULT_CACHE_OPTIONS, DEFAULT_RISK_FREE_RATE, DEFAULT_TIMEOUTS, DEFAULT_TRADING_POLICY, DataFormatError, DecisionMemoryOutcome, DecisionOutcome, DecisionRecordStatus, HttpClientError, HttpServerError, KEEP_ALIVE_DEFAULTS, LlmProvider, MARKET_DATA_API, MassiveAggregatesResponseSchema, MassiveApiError, MassiveDailyOpenCloseSchema, MassiveErrorResponseSchema, MassiveGroupedDailyResponseSchema, MassiveLastTradeResponseSchema, MassiveTickerDetailsResponseSchema, MassiveTickerInfoSchema, MassiveTradeSchema as MassiveTradeZodSchema, MassiveTradesResponseSchema, NetworkError, NewsError, OptionStrategyError, OptionsDataError, OverlaySeverity, OverlayStatus, OverlayType, QuoteError, RISK_FREE_RATE_TTL_MS, RateLimitError, RawMassivePriceDataSchema, StampedeProtectedCache, TRADING_API, TimeoutError, TokenBucketRateLimiter, TradeError, TrailingStopValidationError, USDC_PAIRS, USDT_PAIRS, USD_PAIRS, UnsupportedBrokerError, ValidationError, ValidationResponseError, WEBSOCKET_STREAMS, WebSocketError, account, adaptic, adptc, alpaca, analyzeBars, approximateImpliedVolatility, atrNs as atr, bracketOrders, buildOCCSymbol, buildOptionSymbol, buyCryptoNotional, buyToClose, buyToOpen, buyWithStopLoss, buyWithTrailingStop, calculateMoneyness, calculateOrderValue, calculatePeriodPerformance, calculatePutCallRatio, calculateTotalFilledValue, cancelAllCryptoOrders, cancelOCOOrder, cancelOTOOrder, cancelTrailingStop, cancelTrailingStopsForSymbol, checkTradingEligibility, clearClientCache, clock, closeAllOptionPositions, closeOptionPosition, createAlpacaClient, createAlpacaMarketDataAPI, createAlpacaTradingAPI, createBracketOrder, createBrokerClient, createButterflySpread, createClientFromEnv, createCoveredCall, createCryptoLimitOrder, createCryptoMarketOrder, createCryptoOrder, createCryptoStopLimitOrder, createCryptoStopOrder, createExecutorFromTradingAPI, createIronCondor$1 as createIronCondor, createIronCondor as createIronCondorAdvanced, createMultiLegOptionOrder, createOCOOrder, createOTOOrder, createOptionOrder, createPortfolioTrailingStops, createProtectiveBracket, createStampedeProtectedCache, createStraddle$1 as createStraddle, createStraddle as createStraddleAdvanced, createStrangle$1 as createStrangle, createStrangle as createStrangleAdvanced, createStreamManager, createTimeoutSignal, createTrailingStop, createVerticalSpread$1 as createVerticalSpread, createVerticalSpread as createVerticalSpreadAdvanced, entryWithPercentStopLoss, exerciseOption, extractGreeks, filterByExpiration, filterByStrike, filterByType, filterOrdersByDateRange, findATMOptions, findATMStrikes, findNearestExpiration, findOptionsByDelta, formatOrderForLog, formatOrderSummary, generateOptimalAllocation, getAccountConfiguration, getAccountDetails, getAccountSummary, getAgentPoolStatus, getAllOrders, getAlpacaCalendar, getAlpacaClock, getAverageDailyVolume, getBars, getBuyingPower, getCachedRiskFreeRateSync, getCachedRiskFreeRateSyncWithProvenance, getCrypto24HourChange, getCryptoBars, getCryptoDailyPrices, getCryptoPairsByQuote, getCryptoPrice, getCryptoSnapshots, getCryptoSpread, getCryptoStreamUrl, getCryptoTrades, getCurrentPrice, getCurrentPrices, getDailyPrices, getDailyReturns, getDaysToExpiration, getDefaultRiskProfile, getEquityCurve, getExpirationDates, getFilledOrders, getGroupedOptionChain, getHistoricalOptionsBars, getHistoricalTrades, getIntradayPrices, getLatestBars, getLatestCryptoQuotes, getLatestCryptoTrades, getLatestNews, getLatestOptionsQuotes, getLatestOptionsTrades, getLatestQuote, getLatestQuotes, getLatestTrade, getLatestTrades, getLogger, getMarginInfo, getNews, getNewsForSymbols, getOCOOrderStatus, getOTOOrderStatus, getOpenCryptoOrders, getOpenOrders$1 as getOpenOrdersQuery, getOpenTrailingStops, getOptionChain, getOptionContract, getOptionContracts, getOptionSpread, getOptionsChain, getOptionsSnapshots, getOptionsStreamUrl, getOptionsTradingLevel, getOrderHistory, getOrdersBySymbol, getPDTStatus, getPopularCryptoPairs, getPortfolioHistory, getPreviousClose, getPriceRange, getRiskFreeRate, getRiskFreeRateWithProvenance, getSpread, getSpreads, getStockStreamUrl, getStrikePrices, getSupportedCryptoPairs, getSymbolSentiment, getTimeout, getTradeVolume, getTradingApiUrl, getTradingWebSocketUrl, getTrailingStopHWM, groupOrdersByStatus, groupOrdersBySymbol, hasActiveTrailingStop, hasGoodLiquidity as hasOptionLiquidity, hasGoodLiquidity$1 as hasStockLiquidity, hasSufficientVolume, httpAgent, httpsAgent, isAlpacaBrokerCredentials, isContractTradable, isCryptoPair, isExpiringWithin, isMarginAccount, isOptionOrderCancelable, isOptionOrderTerminal, isOrderFillable, isOrderFilled, isOrderOpen, isOrderTerminal$1 as isOrderTerminalStatus, isSupportedCryptoPair, isTransientNetworkError, index$1 as legacyApi, limitBuyWithTakeProfit, ocoOrders, orderUtils, otoOrders, paginate, paginateAll, parseOCCSymbol, protectLongPosition, protectShortPosition, rateLimiters, resetLogger, resetRiskFreeRateCache, riskNs as risk, rollOptionPosition, roundPriceForAlpaca$3 as roundPriceForAlpaca, roundPriceForAlpacaNumber, safeValidateResponse, searchNews, sellAllCrypto, sellCryptoNotional, sellToClose, sellToOpen, setLogger, setRiskFreeRate, shortWithStopLoss, sortOrdersByDate, strategyNs as strategy, index as tradingPolicy, trailingStops, updateAccountConfiguration, updateTrailingStop, validateAlpacaCredentials, validateAlphaVantageApiKey, validateMassiveApiKey$1 as validateMassiveApiKey, validateMultiLegOrder, validateResponse, verifyFetchKeepAlive, volatilityNs as volatility, waitForOrderFill, withRetry, withTimeout };
|
|
71034
|
+
export { API_RETRY_CONFIGS, AVNewsArticleSchema, AVNewsResponseSchema, AdapticUtilsError, AlpacaAccountDetailsSchema, AlpacaApiError, AlpacaBarSchema, AlpacaClient, AlpacaCryptoBarsResponseSchema, AlpacaHistoricalBarsResponseSchema, AlpacaLatestBarsResponseSchema, AlpacaLatestQuotesResponseSchema, AlpacaLatestTradesResponseSchema, AlpacaMarketDataAPI, AlpacaNewsArticleSchema, AlpacaNewsResponseSchema, AlpacaOrderSchema, AlpacaOrdersArraySchema, AlpacaPortfolioHistoryResponseSchema, AlpacaPositionSchema, AlpacaPositionsArraySchema, AlpacaQuoteSchema, AlpacaTradeSchema, AlpacaTradingAPI, AlphaVantageError, AlphaVantageQuoteResponseSchema, AssetAllocationEngine, AuthenticationError, AutonomyMode, BTC_PAIRS, BarError, CircuitOpenError, CryptoDataError, CryptoOrderError, DEFAULT_CACHE_OPTIONS, DEFAULT_RISK_FREE_RATE, DEFAULT_TIMEOUTS, DEFAULT_TRADING_POLICY, DataFormatError, DecisionMemoryOutcome, DecisionOutcome, DecisionRecordStatus, DuplicateClientOrderIdError, HttpClientError, HttpServerError, KEEP_ALIVE_DEFAULTS, LlmProvider, MARKET_DATA_API, MassiveAggregatesResponseSchema, MassiveApiError, MassiveDailyOpenCloseSchema, MassiveErrorResponseSchema, MassiveGroupedDailyResponseSchema, MassiveLastTradeResponseSchema, MassiveTickerDetailsResponseSchema, MassiveTickerInfoSchema, MassiveTradeSchema as MassiveTradeZodSchema, MassiveTradesResponseSchema, NetworkError, NewsError, OptionStrategyError, OptionsDataError, OverlaySeverity, OverlayStatus, OverlayType, QuoteError, RISK_FREE_RATE_TTL_MS, RateLimitError, RawMassivePriceDataSchema, StampedeProtectedCache, TRADING_API, TimeoutError, TokenBucketRateLimiter, TradeError, TrailingStopValidationError, USDC_PAIRS, USDT_PAIRS, USD_PAIRS, UnsupportedBrokerError, ValidationError, ValidationResponseError, WEBSOCKET_STREAMS, WebSocketError, account, adaptic, adptc, alpaca, analyzeBars, approximateImpliedVolatility, atrNs as atr, bracketOrders, buildOCCSymbol, buildOptionSymbol, buyCryptoNotional, buyToClose, buyToOpen, buyWithStopLoss, buyWithTrailingStop, calculateMoneyness, calculateOrderValue, calculatePeriodPerformance, calculatePutCallRatio, calculateTotalFilledValue, cancelAllCryptoOrders, cancelOCOOrder, cancelOTOOrder, cancelTrailingStop, cancelTrailingStopsForSymbol, checkTradingEligibility, clearClientCache, clock, closeAllOptionPositions, closeOptionPosition, createAlpacaClient, createAlpacaMarketDataAPI, createAlpacaTradingAPI, createBracketOrder, createBrokerClient, createButterflySpread, createClientFromEnv, createCoveredCall, createCryptoLimitOrder, createCryptoMarketOrder, createCryptoOrder, createCryptoStopLimitOrder, createCryptoStopOrder, createExecutorFromTradingAPI, createIronCondor$1 as createIronCondor, createIronCondor as createIronCondorAdvanced, createMultiLegOptionOrder, createOCOOrder, createOTOOrder, createOptionOrder, createPortfolioTrailingStops, createProtectiveBracket, createStampedeProtectedCache, createStraddle$1 as createStraddle, createStraddle as createStraddleAdvanced, createStrangle$1 as createStrangle, createStrangle as createStrangleAdvanced, createStreamManager, createTimeoutSignal, createTrailingStop, createVerticalSpread$1 as createVerticalSpread, createVerticalSpread as createVerticalSpreadAdvanced, entryWithPercentStopLoss, exerciseOption, extractGreeks, filterByExpiration, filterByStrike, filterByType, filterOrdersByDateRange, findATMOptions, findATMStrikes, findNearestExpiration, findOptionsByDelta, formatOrderForLog, formatOrderSummary, generateOptimalAllocation, getAccountConfiguration, getAccountDetails, getAccountSummary, getAgentPoolStatus, getAllOrders, getAlpacaCalendar, getAlpacaClock, getAverageDailyVolume, getBars, getBuyingPower, getCachedRiskFreeRateSync, getCachedRiskFreeRateSyncWithProvenance, getCrypto24HourChange, getCryptoBars, getCryptoDailyPrices, getCryptoPairsByQuote, getCryptoPrice, getCryptoSnapshots, getCryptoSpread, getCryptoStreamUrl, getCryptoTrades, getCurrentPrice, getCurrentPrices, getDailyPrices, getDailyReturns, getDaysToExpiration, getDefaultRiskProfile, getEquityCurve, getExpirationDates, getFilledOrders, getGroupedOptionChain, getHistoricalOptionsBars, getHistoricalTrades, getIntradayPrices, getLatestBars, getLatestCryptoQuotes, getLatestCryptoTrades, getLatestNews, getLatestOptionsQuotes, getLatestOptionsTrades, getLatestQuote, getLatestQuotes, getLatestTrade, getLatestTrades, getLogger, getMarginInfo, getNews, getNewsForSymbols, getOCOOrderStatus, getOTOOrderStatus, getOpenCryptoOrders, getOpenOrders$1 as getOpenOrdersQuery, getOpenTrailingStops, getOptionChain, getOptionContract, getOptionContracts, getOptionSpread, getOptionsChain, getOptionsSnapshots, getOptionsStreamUrl, getOptionsTradingLevel, getOrderHistory, getOrdersBySymbol, getPDTStatus, getPopularCryptoPairs, getPortfolioHistory, getPreviousClose, getPriceRange, getRiskFreeRate, getRiskFreeRateWithProvenance, getSpread, getSpreads, getStockStreamUrl, getStrikePrices, getSupportedCryptoPairs, getSymbolSentiment, getTimeout, getTradeVolume, getTradingApiUrl, getTradingWebSocketUrl, getTrailingStopHWM, groupOrdersByStatus, groupOrdersBySymbol, hasActiveTrailingStop, hasGoodLiquidity as hasOptionLiquidity, hasGoodLiquidity$1 as hasStockLiquidity, hasSufficientVolume, httpAgent, httpsAgent, isAlpacaBrokerCredentials, isContractTradable, isCryptoPair, isExpiringWithin, isMarginAccount, isOptionOrderCancelable, isOptionOrderTerminal, isOrderFillable, isOrderFilled, isOrderOpen, isOrderTerminal$1 as isOrderTerminalStatus, isSupportedCryptoPair, isTransientNetworkError, index$1 as legacyApi, limitBuyWithTakeProfit, ocoOrders, orderUtils, otoOrders, paginate, paginateAll, parseOCCSymbol, protectLongPosition, protectShortPosition, rateLimiters, resetLogger, resetRiskFreeRateCache, riskNs as risk, rollOptionPosition, roundPriceForAlpaca$3 as roundPriceForAlpaca, roundPriceForAlpacaNumber, safeValidateResponse, searchNews, sellAllCrypto, sellCryptoNotional, sellToClose, sellToOpen, setLogger, setRiskFreeRate, shortWithStopLoss, sortOrdersByDate, strategyNs as strategy, index as tradingPolicy, trailingStops, updateAccountConfiguration, updateTrailingStop, validateAlpacaCredentials, validateAlphaVantageApiKey, validateMassiveApiKey$1 as validateMassiveApiKey, validateMultiLegOrder, validateResponse, verifyFetchKeepAlive, volatilityNs as volatility, waitForOrderFill, withRetry, withTimeout };
|
|
70592
71035
|
//# sourceMappingURL=index.mjs.map
|