@adaptic/utils 0.0.1001 → 0.0.1003
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 +802 -183
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +800 -184
- package/dist/index.mjs.map +1 -1
- package/dist/test.js +70 -0
- package/dist/test.js.map +1 -1
- package/dist/types/__tests__/broker-factory.test.d.ts +2 -0
- package/dist/types/__tests__/broker-factory.test.d.ts.map +1 -0
- package/dist/types/__tests__/broker-types.test.d.ts +2 -0
- package/dist/types/__tests__/broker-types.test.d.ts.map +1 -0
- package/dist/types/alpaca/client.d.ts +1 -1
- package/dist/types/alpaca/client.d.ts.map +1 -1
- package/dist/types/alpaca/legacy/auth.d.ts +27 -0
- package/dist/types/alpaca/legacy/auth.d.ts.map +1 -1
- package/dist/types/alpaca/legacy/index.d.ts +1 -1
- package/dist/types/alpaca/legacy/index.d.ts.map +1 -1
- package/dist/types/alpaca-trading-api.d.ts +68 -4
- package/dist/types/alpaca-trading-api.d.ts.map +1 -1
- package/dist/types/broker/factory.d.ts +70 -0
- package/dist/types/broker/factory.d.ts.map +1 -0
- package/dist/types/broker/index.d.ts +9 -0
- package/dist/types/broker/index.d.ts.map +1 -0
- package/dist/types/crypto.d.ts.map +1 -1
- package/dist/types/errors/index.d.ts +14 -0
- package/dist/types/errors/index.d.ts.map +1 -1
- package/dist/types/index.d.ts +4 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/performance-metrics.d.ts.map +1 -1
- package/dist/types/price-utils.d.ts.map +1 -1
- package/dist/types/rate-limiter.d.ts +21 -0
- package/dist/types/rate-limiter.d.ts.map +1 -1
- package/dist/types/types/alpaca-types.d.ts +2 -0
- package/dist/types/types/alpaca-types.d.ts.map +1 -1
- package/dist/types/types/broker-types.d.ts +112 -0
- package/dist/types/types/broker-types.d.ts.map +1 -0
- package/dist/types/types/index.d.ts +1 -0
- package/dist/types/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { format, sub, set, add, startOfDay, endOfDay, isBefore, differenceInMill
|
|
|
3
3
|
import { formatInTimeZone, toZonedTime, fromZonedTime } from 'date-fns-tz';
|
|
4
4
|
import require$$0$4, { EventEmitter } from 'events';
|
|
5
5
|
import WebSocket from 'ws';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
6
7
|
import ms from 'ms';
|
|
7
8
|
import require$$0$1 from 'fs';
|
|
8
9
|
import require$$1 from 'path';
|
|
@@ -2357,6 +2358,23 @@ class NetworkError extends AdapticUtilsError {
|
|
|
2357
2358
|
this.service = service;
|
|
2358
2359
|
}
|
|
2359
2360
|
}
|
|
2361
|
+
/**
|
|
2362
|
+
* Unsupported brokerage provider errors
|
|
2363
|
+
* Thrown when a broker operation is requested for a provider that has no
|
|
2364
|
+
* implemented integration (e.g. IBKR or COINBASE before their adapters land,
|
|
2365
|
+
* or an unrecognised provider string from an untyped caller).
|
|
2366
|
+
* Never retryable — the caller must route to a supported provider.
|
|
2367
|
+
*/
|
|
2368
|
+
class UnsupportedBrokerError extends AdapticUtilsError {
|
|
2369
|
+
provider;
|
|
2370
|
+
constructor(
|
|
2371
|
+
/** The provider that was requested but is not supported. */
|
|
2372
|
+
provider, cause) {
|
|
2373
|
+
super(`Brokerage provider "${provider}" is not supported. Supported providers: ALPACA`, "UNSUPPORTED_BROKER", "broker", false, // Unsupported providers are never retryable
|
|
2374
|
+
cause);
|
|
2375
|
+
this.provider = provider;
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2360
2378
|
/**
|
|
2361
2379
|
* Data parsing and format errors
|
|
2362
2380
|
* Used when API responses cannot be parsed or are in unexpected format
|
|
@@ -2386,6 +2404,19 @@ class DataFormatError extends AdapticUtilsError {
|
|
|
2386
2404
|
* const result = await makeAlpacaApiCall();
|
|
2387
2405
|
* ```
|
|
2388
2406
|
*/
|
|
2407
|
+
/** Number of milliseconds in one second, used for token-refill timing math. */
|
|
2408
|
+
const MS_PER_SECOND$1 = 1000;
|
|
2409
|
+
/**
|
|
2410
|
+
* Minimum delay (ms) for a scheduled queue wake-up. Guards against a `0`/`NaN`
|
|
2411
|
+
* delay when the token deficit rounds down, ensuring the timer always makes
|
|
2412
|
+
* forward progress rather than busy-looping on the event loop.
|
|
2413
|
+
*/
|
|
2414
|
+
const MIN_WAKE_DELAY_MS = 1;
|
|
2415
|
+
/**
|
|
2416
|
+
* Number of whole tokens required to release a single queued request. The token
|
|
2417
|
+
* bucket consumes exactly one token per admitted request.
|
|
2418
|
+
*/
|
|
2419
|
+
const TOKENS_PER_REQUEST = 1;
|
|
2389
2420
|
/**
|
|
2390
2421
|
* Token bucket rate limiter implementation
|
|
2391
2422
|
*
|
|
@@ -2401,6 +2432,13 @@ class TokenBucketRateLimiter {
|
|
|
2401
2432
|
queue = [];
|
|
2402
2433
|
timeoutMs;
|
|
2403
2434
|
processingQueue = false;
|
|
2435
|
+
/**
|
|
2436
|
+
* Single pending timer that wakes the limiter to refill tokens and drain the
|
|
2437
|
+
* queue. Without this, a queued request would only be released by a
|
|
2438
|
+
* subsequent {@link acquire} call and would otherwise stall until its own
|
|
2439
|
+
* timeout fired. `null` means no wake-up is currently scheduled.
|
|
2440
|
+
*/
|
|
2441
|
+
wakeTimer = null;
|
|
2404
2442
|
/**
|
|
2405
2443
|
* Creates a new rate limiter instance
|
|
2406
2444
|
*
|
|
@@ -2474,8 +2512,48 @@ class TokenBucketRateLimiter {
|
|
|
2474
2512
|
reject(error);
|
|
2475
2513
|
}, this.timeoutMs);
|
|
2476
2514
|
this.queue.push({ resolve, reject, timeoutHandle });
|
|
2515
|
+
// Ensure the queue is actively drained even if no further acquire() calls
|
|
2516
|
+
// arrive: schedule a wake-up to refill tokens and release this request.
|
|
2517
|
+
this.scheduleQueueWake();
|
|
2477
2518
|
});
|
|
2478
2519
|
}
|
|
2520
|
+
/**
|
|
2521
|
+
* Schedules a single wake-up timer that refills tokens and drains the queue.
|
|
2522
|
+
*
|
|
2523
|
+
* The delay is the time required to accrue the tokens still needed to release
|
|
2524
|
+
* the next queued request at the configured refill rate. Only one timer is
|
|
2525
|
+
* ever outstanding (guarded by {@link wakeTimer}); the timer is `unref`'d so
|
|
2526
|
+
* it never keeps the Node.js process alive on its own. When it fires it
|
|
2527
|
+
* refills, drains what it can, and re-arms itself if work remains.
|
|
2528
|
+
*/
|
|
2529
|
+
scheduleQueueWake() {
|
|
2530
|
+
// A wake-up is already pending, or there is nothing to wake for.
|
|
2531
|
+
if (this.wakeTimer !== null || this.queue.length === 0) {
|
|
2532
|
+
return;
|
|
2533
|
+
}
|
|
2534
|
+
const tokensNeeded = Math.max(0, TOKENS_PER_REQUEST - this.tokens);
|
|
2535
|
+
const deficitMs = Math.max(MIN_WAKE_DELAY_MS, Math.ceil((tokensNeeded / this.config.refillRate) * MS_PER_SECOND$1));
|
|
2536
|
+
const timer = setTimeout(() => {
|
|
2537
|
+
this.wakeTimer = null;
|
|
2538
|
+
// refill() drains the queue via processQueue(); if requests remain
|
|
2539
|
+
// afterwards, processQueue() re-arms the wake-up.
|
|
2540
|
+
this.refill();
|
|
2541
|
+
}, deficitMs);
|
|
2542
|
+
// Do not let a pending rate-limiter wake-up keep the process alive.
|
|
2543
|
+
if (typeof timer.unref === "function") {
|
|
2544
|
+
timer.unref();
|
|
2545
|
+
}
|
|
2546
|
+
this.wakeTimer = timer;
|
|
2547
|
+
}
|
|
2548
|
+
/**
|
|
2549
|
+
* Clears any pending wake-up timer.
|
|
2550
|
+
*/
|
|
2551
|
+
clearWakeTimer() {
|
|
2552
|
+
if (this.wakeTimer !== null) {
|
|
2553
|
+
clearTimeout(this.wakeTimer);
|
|
2554
|
+
this.wakeTimer = null;
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2479
2557
|
/**
|
|
2480
2558
|
* Refills tokens based on elapsed time and processes queued requests
|
|
2481
2559
|
*
|
|
@@ -2520,6 +2598,15 @@ class TokenBucketRateLimiter {
|
|
|
2520
2598
|
finally {
|
|
2521
2599
|
this.processingQueue = false;
|
|
2522
2600
|
}
|
|
2601
|
+
// Keep the wake-up state consistent with the queue: if requests are still
|
|
2602
|
+
// waiting (tokens ran out mid-drain), ensure a wake-up is armed; otherwise
|
|
2603
|
+
// release any pending timer so it cannot fire needlessly.
|
|
2604
|
+
if (this.queue.length > 0) {
|
|
2605
|
+
this.scheduleQueueWake();
|
|
2606
|
+
}
|
|
2607
|
+
else {
|
|
2608
|
+
this.clearWakeTimer();
|
|
2609
|
+
}
|
|
2523
2610
|
}
|
|
2524
2611
|
/**
|
|
2525
2612
|
* Gets the current number of available tokens
|
|
@@ -2551,6 +2638,7 @@ class TokenBucketRateLimiter {
|
|
|
2551
2638
|
clearTimeout(request.timeoutHandle);
|
|
2552
2639
|
request.reject(new RateLimitError(`Rate limiter reset for ${this.config.label}`, this.config.label, undefined));
|
|
2553
2640
|
}
|
|
2641
|
+
this.clearWakeTimer();
|
|
2554
2642
|
this.queue = [];
|
|
2555
2643
|
this.tokens = this.config.maxTokens;
|
|
2556
2644
|
this.lastRefill = Date.now();
|
|
@@ -4300,6 +4388,46 @@ const limitPriceSlippagePercent100 = 0.1; // 0.1%
|
|
|
4300
4388
|
const ORDER_PAGE_LIMIT = 500;
|
|
4301
4389
|
/** Delay between order pagination pages to stay clear of rate limits. */
|
|
4302
4390
|
const ORDER_PAGINATION_DELAY_MS = 300;
|
|
4391
|
+
/**
|
|
4392
|
+
* HTTP status at or above which a Multi-Status (207) sub-result is a failure.
|
|
4393
|
+
* Alpaca's bulk `DELETE /orders` and `DELETE /positions` endpoints return a 207
|
|
4394
|
+
* envelope whose top-level status is 2xx even when individual orders/positions
|
|
4395
|
+
* failed to cancel or close; each element carries its own per-item HTTP status.
|
|
4396
|
+
* Treating >= 300 as a failure lets the engine failsafe see partial failures
|
|
4397
|
+
* instead of recording a false success.
|
|
4398
|
+
*/
|
|
4399
|
+
const HTTP_STATUS_MIN_ERROR = 300;
|
|
4400
|
+
/**
|
|
4401
|
+
* Prefix applied to engine-derived `client_order_id` idempotency keys so they
|
|
4402
|
+
* are visibly attributable in Alpaca's dashboard and can never collide with a
|
|
4403
|
+
* caller-supplied identifier.
|
|
4404
|
+
*/
|
|
4405
|
+
const CLIENT_ORDER_ID_PREFIX = "adaptic-";
|
|
4406
|
+
/**
|
|
4407
|
+
* Number of leading hex characters of the SHA-256 digest retained in a derived
|
|
4408
|
+
* `client_order_id`. 32 hex chars = 128 bits of entropy (collision-negligible),
|
|
4409
|
+
* and keeps the full id (prefix + digest = 40 chars) within Alpaca's identifier
|
|
4410
|
+
* length limit.
|
|
4411
|
+
*/
|
|
4412
|
+
const CLIENT_ORDER_ID_HASH_LENGTH = 32;
|
|
4413
|
+
/**
|
|
4414
|
+
* Idempotency window (ms) used when deriving a default `client_order_id`.
|
|
4415
|
+
*
|
|
4416
|
+
* A client request timeout followed by an automatic retry re-submits the SAME
|
|
4417
|
+
* logical order. Deriving the id from the order's semantic parameters plus the
|
|
4418
|
+
* current time-window bucket makes Alpaca reject the retried duplicate
|
|
4419
|
+
* (`client_order_id must be unique`) instead of double-filling. The window is
|
|
4420
|
+
* deliberately much larger than the 30s Alpaca request timeout so a full
|
|
4421
|
+
* timeout+retry sequence lands in the same bucket, while a genuinely new but
|
|
4422
|
+
* otherwise-identical order placed in a later window still receives a distinct
|
|
4423
|
+
* id.
|
|
4424
|
+
*
|
|
4425
|
+
* This derived default is a best-effort safety net; the guaranteed-idempotent
|
|
4426
|
+
* path is for the caller to pass an explicit `clientOrderId` tied to the
|
|
4427
|
+
* originating signal/decision id (which also permits legitimately-repeated
|
|
4428
|
+
* identical orders inside a single window).
|
|
4429
|
+
*/
|
|
4430
|
+
const CLIENT_ORDER_ID_WINDOW_MS = 300_000;
|
|
4303
4431
|
/**
|
|
4304
4432
|
Websocket example
|
|
4305
4433
|
const alpacaAPI = createAlpacaTradingAPI(credentials); // type AlpacaCredentials
|
|
@@ -4380,6 +4508,114 @@ class AlpacaTradingAPI {
|
|
|
4380
4508
|
? Math.round(price * 100) / 100
|
|
4381
4509
|
: Math.round(price * 10000) / 10000;
|
|
4382
4510
|
};
|
|
4511
|
+
/**
|
|
4512
|
+
* Derive a deterministic `client_order_id` from an order's semantic
|
|
4513
|
+
* parameters so that a client-timeout-triggered retry re-submits the SAME id
|
|
4514
|
+
* and Alpaca rejects the duplicate broker-side instead of double-filling.
|
|
4515
|
+
*
|
|
4516
|
+
* The id is stable for identical parameters within a single
|
|
4517
|
+
* {@link CLIENT_ORDER_ID_WINDOW_MS} bucket and scoped per account. Callers
|
|
4518
|
+
* that must place genuinely distinct yet otherwise-identical orders should
|
|
4519
|
+
* pass an explicit `clientOrderId` rather than relying on this default.
|
|
4520
|
+
*
|
|
4521
|
+
* @param parts - Ordered, stringifiable components uniquely describing the
|
|
4522
|
+
* order (e.g. order kind, symbol, side, quantity, price, intent).
|
|
4523
|
+
* @returns An Alpaca-safe `client_order_id` (prefix + truncated SHA-256 hex).
|
|
4524
|
+
*/
|
|
4525
|
+
deriveClientOrderId(parts) {
|
|
4526
|
+
const windowBucket = Math.floor(Date.now() / CLIENT_ORDER_ID_WINDOW_MS);
|
|
4527
|
+
const material = [
|
|
4528
|
+
this.credentials.accountName,
|
|
4529
|
+
windowBucket,
|
|
4530
|
+
...parts.map((part) => (part === undefined ? "" : String(part))),
|
|
4531
|
+
].join("|");
|
|
4532
|
+
const digest = createHash("sha256")
|
|
4533
|
+
.update(material)
|
|
4534
|
+
.digest("hex")
|
|
4535
|
+
.slice(0, CLIENT_ORDER_ID_HASH_LENGTH);
|
|
4536
|
+
return `${CLIENT_ORDER_ID_PREFIX}${digest}`;
|
|
4537
|
+
}
|
|
4538
|
+
/**
|
|
4539
|
+
* Collect the human-readable failure entries from a bulk Multi-Status (207)
|
|
4540
|
+
* response body (`DELETE /orders`, `DELETE /positions`). Each element carries
|
|
4541
|
+
* its own per-item HTTP status; any element with status >=
|
|
4542
|
+
* {@link HTTP_STATUS_MIN_ERROR} is a failure the caller must be able to see.
|
|
4543
|
+
*
|
|
4544
|
+
* @param entries - Parsed 207 response array.
|
|
4545
|
+
* @returns One `"<identifier>:<status>"` string per failed entry.
|
|
4546
|
+
*/
|
|
4547
|
+
collectMultiStatusFailures(entries) {
|
|
4548
|
+
return entries
|
|
4549
|
+
.filter((entry) => typeof entry.status === "number" &&
|
|
4550
|
+
entry.status >= HTTP_STATUS_MIN_ERROR)
|
|
4551
|
+
.map((entry) => `${entry.symbol ?? entry.id ?? "unknown"}:${entry.status}`);
|
|
4552
|
+
}
|
|
4553
|
+
/**
|
|
4554
|
+
* Flatten a single position with a marketable limit order, deriving the
|
|
4555
|
+
* closing side, intent, and slippage-adjusted limit price from the latest
|
|
4556
|
+
* quote. Throws when no usable quote/price is available for the symbol so the
|
|
4557
|
+
* caller can record a per-position failure via {@link Promise.allSettled}
|
|
4558
|
+
* without aborting the flatten of the remaining positions.
|
|
4559
|
+
*
|
|
4560
|
+
* @param position - The position to close.
|
|
4561
|
+
* @param quotesResponse - Latest quotes keyed by symbol.
|
|
4562
|
+
* @param extendedHours - Whether the closing order is an extended-hours order.
|
|
4563
|
+
*/
|
|
4564
|
+
async closePositionWithLimitOrder(position, quotesResponse, extendedHours) {
|
|
4565
|
+
const quote = quotesResponse.quotes[position.symbol];
|
|
4566
|
+
if (!quote) {
|
|
4567
|
+
throw new Error(`No quote available for ${position.symbol}`);
|
|
4568
|
+
}
|
|
4569
|
+
const qty = Math.abs(parseFloat(position.qty));
|
|
4570
|
+
const side = position.side === "long" ? "sell" : "buy";
|
|
4571
|
+
const positionIntent = side === "sell" ? "sell_to_close" : "buy_to_close";
|
|
4572
|
+
// Use bid for sells, ask for buys.
|
|
4573
|
+
const currentPrice = side === "sell" ? quote.bp : quote.ap;
|
|
4574
|
+
if (!currentPrice) {
|
|
4575
|
+
throw new Error(`No valid price available for ${position.symbol}`);
|
|
4576
|
+
}
|
|
4577
|
+
const limitSlippagePercent1 = limitPriceSlippagePercent100 / 100;
|
|
4578
|
+
const limitPrice = side === "sell"
|
|
4579
|
+
? this.roundPriceForAlpaca(currentPrice * (1 - limitSlippagePercent1)) // Sell slightly lower
|
|
4580
|
+
: this.roundPriceForAlpaca(currentPrice * (1 + limitSlippagePercent1)); // Buy slightly higher
|
|
4581
|
+
this.log(`Creating ${extendedHours ? "extended hours " : ""}limit order to close ${position.symbol} position: ${side} ${qty} shares at $${limitPrice.toFixed(2)}`, {
|
|
4582
|
+
symbol: position.symbol,
|
|
4583
|
+
});
|
|
4584
|
+
await this.createLimitOrder(position.symbol, qty, side, limitPrice, positionIntent, extendedHours);
|
|
4585
|
+
}
|
|
4586
|
+
/**
|
|
4587
|
+
* Flatten every supplied position independently and surface an aggregate
|
|
4588
|
+
* failure if any could not be closed. Positions are attempted concurrently
|
|
4589
|
+
* with {@link Promise.allSettled} so a data gap or broker rejection on one
|
|
4590
|
+
* symbol never silently prevents the others from being flattened.
|
|
4591
|
+
*
|
|
4592
|
+
* @param positions - Positions to flatten.
|
|
4593
|
+
* @param extendedHours - Whether the closing orders are extended-hours orders.
|
|
4594
|
+
* @throws Error listing every symbol that failed to flatten.
|
|
4595
|
+
*/
|
|
4596
|
+
async flattenPositionsWithLimitOrders(positions, extendedHours) {
|
|
4597
|
+
const symbols = positions.map((position) => position.symbol);
|
|
4598
|
+
const quotesResponse = await marketDataAPI.getLatestQuotes(symbols);
|
|
4599
|
+
const results = await Promise.allSettled(positions.map((position) => this.closePositionWithLimitOrder(position, quotesResponse, extendedHours)));
|
|
4600
|
+
const failures = [];
|
|
4601
|
+
results.forEach((result, index) => {
|
|
4602
|
+
if (result.status === "rejected") {
|
|
4603
|
+
const symbol = positions[index]?.symbol ?? "unknown";
|
|
4604
|
+
const reason = result.reason instanceof Error
|
|
4605
|
+
? result.reason.message
|
|
4606
|
+
: String(result.reason);
|
|
4607
|
+
failures.push(`${symbol}: ${reason}`);
|
|
4608
|
+
this.log(`Failed to close position ${symbol}: ${reason}`, {
|
|
4609
|
+
symbol,
|
|
4610
|
+
type: "error",
|
|
4611
|
+
});
|
|
4612
|
+
}
|
|
4613
|
+
});
|
|
4614
|
+
if (failures.length > 0) {
|
|
4615
|
+
throw new Error(`Failed to close ${failures.length} of ${positions.length} positions: ${failures.join("; ")}`);
|
|
4616
|
+
}
|
|
4617
|
+
this.log(`All positions closed: ${symbols.join(", ")}`);
|
|
4618
|
+
}
|
|
4383
4619
|
handleAuthMessage(data) {
|
|
4384
4620
|
if (data.status === "authorized") {
|
|
4385
4621
|
this.authenticated = true;
|
|
@@ -4751,21 +4987,31 @@ class AlpacaTradingAPI {
|
|
|
4751
4987
|
* @param position_intent (string) - the position intent of the order
|
|
4752
4988
|
* @returns The created AlpacaOrder with order ID and details
|
|
4753
4989
|
*/
|
|
4754
|
-
async createTrailingStop(symbol, qty, side, trailPercent100, position_intent) {
|
|
4990
|
+
async createTrailingStop(symbol, qty, side, trailPercent100, position_intent, clientOrderId) {
|
|
4755
4991
|
this.log(`Creating trailing stop ${side.toUpperCase()} ${qty} shares for ${symbol} with trail percent ${trailPercent100}%`, {
|
|
4756
4992
|
symbol,
|
|
4757
4993
|
});
|
|
4994
|
+
const body = {
|
|
4995
|
+
symbol,
|
|
4996
|
+
qty: Math.abs(qty).toString(),
|
|
4997
|
+
side,
|
|
4998
|
+
position_intent,
|
|
4999
|
+
order_class: "simple",
|
|
5000
|
+
type: "trailing_stop",
|
|
5001
|
+
trail_percent: trailPercent100.toString(), // Already in decimal form (e.g., 4 for 4%)
|
|
5002
|
+
time_in_force: "gtc",
|
|
5003
|
+
client_order_id: clientOrderId ??
|
|
5004
|
+
this.deriveClientOrderId([
|
|
5005
|
+
"trailing_stop",
|
|
5006
|
+
symbol,
|
|
5007
|
+
side,
|
|
5008
|
+
position_intent,
|
|
5009
|
+
Math.abs(qty),
|
|
5010
|
+
trailPercent100,
|
|
5011
|
+
]),
|
|
5012
|
+
};
|
|
4758
5013
|
try {
|
|
4759
|
-
const order = await this.makeRequest(`/orders`, "POST",
|
|
4760
|
-
symbol,
|
|
4761
|
-
qty: Math.abs(qty),
|
|
4762
|
-
side,
|
|
4763
|
-
position_intent,
|
|
4764
|
-
order_class: "simple",
|
|
4765
|
-
type: "trailing_stop",
|
|
4766
|
-
trail_percent: trailPercent100, // Already in decimal form (e.g., 4 for 4%)
|
|
4767
|
-
time_in_force: "gtc",
|
|
4768
|
-
});
|
|
5014
|
+
const order = await this.makeRequest(`/orders`, "POST", body);
|
|
4769
5015
|
this.log(`Trailing stop order created for ${symbol}: orderId=${order.id}, trailPercent=${trailPercent100}%`, { symbol });
|
|
4770
5016
|
return order;
|
|
4771
5017
|
}
|
|
@@ -4797,9 +5043,15 @@ class AlpacaTradingAPI {
|
|
|
4797
5043
|
time_in_force: "day",
|
|
4798
5044
|
order_class: "simple",
|
|
4799
5045
|
};
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
5046
|
+
body.client_order_id =
|
|
5047
|
+
client_order_id ??
|
|
5048
|
+
this.deriveClientOrderId([
|
|
5049
|
+
"market",
|
|
5050
|
+
symbol,
|
|
5051
|
+
side,
|
|
5052
|
+
position_intent,
|
|
5053
|
+
Math.abs(qty),
|
|
5054
|
+
]);
|
|
4803
5055
|
try {
|
|
4804
5056
|
return await this.makeRequest("/orders", "POST", body);
|
|
4805
5057
|
}
|
|
@@ -4900,16 +5152,31 @@ class AlpacaTradingAPI {
|
|
|
4900
5152
|
}
|
|
4901
5153
|
}
|
|
4902
5154
|
/**
|
|
4903
|
-
* Cancel all open orders
|
|
5155
|
+
* Cancel all open orders.
|
|
5156
|
+
*
|
|
5157
|
+
* Alpaca's bulk cancel returns a 207 Multi-Status body whose top-level status
|
|
5158
|
+
* is 2xx even when individual orders failed to cancel; this method inspects
|
|
5159
|
+
* the per-order statuses and throws if any order could not be canceled, so a
|
|
5160
|
+
* caller acting as a live-stop failsafe cannot record success while orders
|
|
5161
|
+
* remain live. Transport/HTTP errors propagate unchanged (matching the
|
|
5162
|
+
* throw-on-failure contract of {@link cancelOrder}).
|
|
5163
|
+
*
|
|
5164
|
+
* @throws Error if the bulk cancel request fails or any individual order
|
|
5165
|
+
* could not be canceled.
|
|
4904
5166
|
*/
|
|
4905
5167
|
async cancelAllOrders() {
|
|
4906
5168
|
this.log(`Canceling all open orders`);
|
|
4907
|
-
|
|
4908
|
-
|
|
5169
|
+
const results = await this.makeRequest("/orders", "DELETE");
|
|
5170
|
+
if (!Array.isArray(results)) {
|
|
5171
|
+
return;
|
|
4909
5172
|
}
|
|
4910
|
-
|
|
4911
|
-
|
|
5173
|
+
const failures = this.collectMultiStatusFailures(results);
|
|
5174
|
+
if (failures.length > 0) {
|
|
5175
|
+
const detail = failures.join(", ");
|
|
5176
|
+
this.log(`Error canceling all orders: ${failures.length}/${results.length} orders failed to cancel (${detail})`, { type: "error" });
|
|
5177
|
+
throw new Error(`Failed to cancel ${failures.length} of ${results.length} orders: ${detail}`);
|
|
4912
5178
|
}
|
|
5179
|
+
this.log(`Successfully canceled ${results.length} open orders`);
|
|
4913
5180
|
}
|
|
4914
5181
|
/**
|
|
4915
5182
|
* Cancel a specific order by its ID
|
|
@@ -4960,9 +5227,17 @@ class AlpacaTradingAPI {
|
|
|
4960
5227
|
order_class: "simple",
|
|
4961
5228
|
extended_hours,
|
|
4962
5229
|
};
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
5230
|
+
body.client_order_id =
|
|
5231
|
+
client_order_id ??
|
|
5232
|
+
this.deriveClientOrderId([
|
|
5233
|
+
"limit",
|
|
5234
|
+
symbol,
|
|
5235
|
+
side,
|
|
5236
|
+
position_intent,
|
|
5237
|
+
Math.abs(qty),
|
|
5238
|
+
this.roundPriceForAlpaca(limitPrice),
|
|
5239
|
+
extended_hours,
|
|
5240
|
+
]);
|
|
4966
5241
|
try {
|
|
4967
5242
|
return await this.makeRequest("/orders", "POST", body);
|
|
4968
5243
|
}
|
|
@@ -4990,55 +5265,21 @@ class AlpacaTradingAPI {
|
|
|
4990
5265
|
return;
|
|
4991
5266
|
}
|
|
4992
5267
|
this.log(`Found ${positions.length} positions to close`);
|
|
4993
|
-
//
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
if (lengthOfQuotes === 0) {
|
|
4998
|
-
this.log("No quotes available for positions, received 0 quotes", {
|
|
4999
|
-
type: "error",
|
|
5000
|
-
});
|
|
5001
|
-
return;
|
|
5002
|
-
}
|
|
5003
|
-
if (lengthOfQuotes !== positions.length) {
|
|
5004
|
-
this.log(`Received ${lengthOfQuotes} quotes for ${positions.length} positions, expected ${positions.length} quotes`, { type: "warn" });
|
|
5005
|
-
return;
|
|
5006
|
-
}
|
|
5007
|
-
// Create limit orders to close each position
|
|
5008
|
-
for (const position of positions) {
|
|
5009
|
-
const quote = quotesResponse.quotes[position.symbol];
|
|
5010
|
-
if (!quote) {
|
|
5011
|
-
this.log(`No quote available for ${position.symbol}, skipping limit order`, {
|
|
5012
|
-
symbol: position.symbol,
|
|
5013
|
-
type: "warn",
|
|
5014
|
-
});
|
|
5015
|
-
continue;
|
|
5016
|
-
}
|
|
5017
|
-
const qty = Math.abs(parseFloat(position.qty));
|
|
5018
|
-
const side = position.side === "long" ? "sell" : "buy";
|
|
5019
|
-
const positionIntent = side === "sell" ? "sell_to_close" : "buy_to_close";
|
|
5020
|
-
// Get the current price from the quote
|
|
5021
|
-
const currentPrice = side === "sell" ? quote.bp : quote.ap; // Use bid for sells, ask for buys
|
|
5022
|
-
if (!currentPrice) {
|
|
5023
|
-
this.log(`No valid price available for ${position.symbol}, skipping limit order`, {
|
|
5024
|
-
symbol: position.symbol,
|
|
5025
|
-
type: "warn",
|
|
5026
|
-
});
|
|
5027
|
-
continue;
|
|
5028
|
-
}
|
|
5029
|
-
// Apply slippage from config
|
|
5030
|
-
const limitSlippagePercent1 = limitPriceSlippagePercent100 / 100;
|
|
5031
|
-
const limitPrice = side === "sell"
|
|
5032
|
-
? this.roundPriceForAlpaca(currentPrice * (1 - limitSlippagePercent1)) // Sell slightly lower
|
|
5033
|
-
: this.roundPriceForAlpaca(currentPrice * (1 + limitSlippagePercent1)); // Buy slightly higher
|
|
5034
|
-
this.log(`Creating limit order to close ${position.symbol} position: ${side} ${qty} shares at $${limitPrice.toFixed(2)}`, {
|
|
5035
|
-
symbol: position.symbol,
|
|
5036
|
-
});
|
|
5037
|
-
await this.createLimitOrder(position.symbol, qty, side, limitPrice, positionIntent);
|
|
5038
|
-
}
|
|
5268
|
+
// Flatten each position independently. A missing quote or broker
|
|
5269
|
+
// rejection on one symbol must never abort the flatten of the others; any
|
|
5270
|
+
// per-position failure is surfaced as an aggregate error.
|
|
5271
|
+
await this.flattenPositionsWithLimitOrders(positions, false);
|
|
5039
5272
|
}
|
|
5040
5273
|
else {
|
|
5041
|
-
await this.makeRequest("/positions", "DELETE", undefined, options.cancel_orders ? "?cancel_orders=true" : "");
|
|
5274
|
+
const results = await this.makeRequest("/positions", "DELETE", undefined, options.cancel_orders ? "?cancel_orders=true" : "");
|
|
5275
|
+
if (Array.isArray(results)) {
|
|
5276
|
+
const failures = this.collectMultiStatusFailures(results);
|
|
5277
|
+
if (failures.length > 0) {
|
|
5278
|
+
const detail = failures.join(", ");
|
|
5279
|
+
this.log(`Error closing all positions: ${failures.length}/${results.length} positions failed to close (${detail})`, { type: "error" });
|
|
5280
|
+
throw new Error(`Failed to close ${failures.length} of ${results.length} positions: ${detail}`);
|
|
5281
|
+
}
|
|
5282
|
+
}
|
|
5042
5283
|
}
|
|
5043
5284
|
}
|
|
5044
5285
|
/**
|
|
@@ -5055,44 +5296,22 @@ class AlpacaTradingAPI {
|
|
|
5055
5296
|
this.log("No positions to close");
|
|
5056
5297
|
return;
|
|
5057
5298
|
}
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
//
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
symbol: position.symbol,
|
|
5069
|
-
type: "warn",
|
|
5070
|
-
});
|
|
5071
|
-
continue;
|
|
5072
|
-
}
|
|
5073
|
-
const qty = Math.abs(parseFloat(position.qty));
|
|
5074
|
-
const side = position.side === "long" ? "sell" : "buy";
|
|
5075
|
-
const positionIntent = side === "sell" ? "sell_to_close" : "buy_to_close";
|
|
5076
|
-
// Get the current price from the quote
|
|
5077
|
-
const currentPrice = side === "sell" ? quote.bp : quote.ap; // Use bid for sells, ask for buys
|
|
5078
|
-
if (!currentPrice) {
|
|
5079
|
-
this.log(`No valid price available for ${position.symbol}, skipping limit order`, {
|
|
5080
|
-
symbol: position.symbol,
|
|
5081
|
-
type: "warn",
|
|
5082
|
-
});
|
|
5083
|
-
continue;
|
|
5084
|
-
}
|
|
5085
|
-
// Apply slippage from config
|
|
5086
|
-
const limitSlippagePercent1 = limitPriceSlippagePercent100 / 100;
|
|
5087
|
-
const limitPrice = side === "sell"
|
|
5088
|
-
? this.roundPriceForAlpaca(currentPrice * (1 - limitSlippagePercent1)) // Sell slightly lower
|
|
5089
|
-
: this.roundPriceForAlpaca(currentPrice * (1 + limitSlippagePercent1)); // Buy slightly higher
|
|
5090
|
-
this.log(`Creating extended hours limit order to close ${position.symbol} position: ${side} ${qty} shares at $${limitPrice.toFixed(2)}`, {
|
|
5091
|
-
symbol: position.symbol,
|
|
5092
|
-
});
|
|
5093
|
-
await this.createLimitOrder(position.symbol, qty, side, limitPrice, positionIntent, true);
|
|
5299
|
+
// Cancelling stale open orders is secondary to the primary failsafe goal of
|
|
5300
|
+
// flattening positions. A cancel failure is logged but must not abort the
|
|
5301
|
+
// flatten, otherwise a single un-cancelable order would leave every position
|
|
5302
|
+
// open. The flatten step below surfaces its own aggregate failure.
|
|
5303
|
+
try {
|
|
5304
|
+
await this.cancelAllOrders();
|
|
5305
|
+
this.log(`Cancelled all open orders`);
|
|
5306
|
+
}
|
|
5307
|
+
catch (error) {
|
|
5308
|
+
this.log(`Proceeding to flatten despite cancelAllOrders failure: ${error instanceof Error ? error.message : String(error)}`, { type: "error" });
|
|
5094
5309
|
}
|
|
5095
|
-
|
|
5310
|
+
// Flatten each position independently with extended-hours limit orders. A
|
|
5311
|
+
// missing quote or broker rejection on one symbol must never silently leave
|
|
5312
|
+
// the remaining positions open; per-position failures are surfaced as an
|
|
5313
|
+
// aggregate error.
|
|
5314
|
+
await this.flattenPositionsWithLimitOrders(positions, true);
|
|
5096
5315
|
}
|
|
5097
5316
|
onTradeUpdate(callback) {
|
|
5098
5317
|
this.tradeUpdateCallback = callback;
|
|
@@ -5171,9 +5390,12 @@ class AlpacaTradingAPI {
|
|
|
5171
5390
|
* @param position_intent Position intent (buy_to_open, buy_to_close, sell_to_open, sell_to_close)
|
|
5172
5391
|
* @param type Order type (market or limit)
|
|
5173
5392
|
* @param limitPrice Limit price (required for limit orders)
|
|
5393
|
+
* @param clientOrderId Optional idempotency key; a deterministic one is
|
|
5394
|
+
* derived from the order parameters when omitted so a client-timeout retry
|
|
5395
|
+
* is de-duplicated broker-side.
|
|
5174
5396
|
* @returns The created order
|
|
5175
5397
|
*/
|
|
5176
|
-
async createOptionOrder(symbol, qty, side, position_intent, type, limitPrice) {
|
|
5398
|
+
async createOptionOrder(symbol, qty, side, position_intent, type, limitPrice, clientOrderId) {
|
|
5177
5399
|
if (!Number.isInteger(qty) || qty <= 0) {
|
|
5178
5400
|
this.log("Quantity must be a positive whole number for option orders", {
|
|
5179
5401
|
type: "error",
|
|
@@ -5198,6 +5420,19 @@ class AlpacaTradingAPI {
|
|
|
5198
5420
|
if (type === "limit" && limitPrice !== undefined) {
|
|
5199
5421
|
orderData.limit_price = this.roundPriceForAlpaca(limitPrice).toString();
|
|
5200
5422
|
}
|
|
5423
|
+
orderData.client_order_id =
|
|
5424
|
+
clientOrderId ??
|
|
5425
|
+
this.deriveClientOrderId([
|
|
5426
|
+
"option",
|
|
5427
|
+
type,
|
|
5428
|
+
symbol,
|
|
5429
|
+
side,
|
|
5430
|
+
position_intent,
|
|
5431
|
+
qty,
|
|
5432
|
+
type === "limit" && limitPrice !== undefined
|
|
5433
|
+
? this.roundPriceForAlpaca(limitPrice)
|
|
5434
|
+
: undefined,
|
|
5435
|
+
]);
|
|
5201
5436
|
return this.makeRequest("/orders", "POST", orderData);
|
|
5202
5437
|
}
|
|
5203
5438
|
/**
|
|
@@ -5206,9 +5441,12 @@ class AlpacaTradingAPI {
|
|
|
5206
5441
|
* @param qty Quantity of the multi-leg order (must be a whole number)
|
|
5207
5442
|
* @param type Order type (market or limit)
|
|
5208
5443
|
* @param limitPrice Limit price (required for limit orders)
|
|
5444
|
+
* @param clientOrderId Optional idempotency key; a deterministic one is
|
|
5445
|
+
* derived from the legs and order parameters when omitted so a
|
|
5446
|
+
* client-timeout retry is de-duplicated broker-side.
|
|
5209
5447
|
* @returns The created multi-leg order
|
|
5210
5448
|
*/
|
|
5211
|
-
async createMultiLegOptionOrder(legs, qty, type, limitPrice) {
|
|
5449
|
+
async createMultiLegOptionOrder(legs, qty, type, limitPrice, clientOrderId) {
|
|
5212
5450
|
if (!Number.isInteger(qty) || qty <= 0) {
|
|
5213
5451
|
this.log("Quantity must be a positive whole number for option orders", {
|
|
5214
5452
|
type: "error",
|
|
@@ -5234,6 +5472,17 @@ class AlpacaTradingAPI {
|
|
|
5234
5472
|
if (type === "limit" && limitPrice !== undefined) {
|
|
5235
5473
|
orderData.limit_price = this.roundPriceForAlpaca(limitPrice).toString();
|
|
5236
5474
|
}
|
|
5475
|
+
orderData.client_order_id =
|
|
5476
|
+
clientOrderId ??
|
|
5477
|
+
this.deriveClientOrderId([
|
|
5478
|
+
"mleg",
|
|
5479
|
+
type,
|
|
5480
|
+
qty,
|
|
5481
|
+
type === "limit" && limitPrice !== undefined
|
|
5482
|
+
? this.roundPriceForAlpaca(limitPrice)
|
|
5483
|
+
: undefined,
|
|
5484
|
+
...legs.map((leg) => `${leg.symbol}:${leg.side}:${leg.ratio_qty}:${leg.position_intent}`),
|
|
5485
|
+
]);
|
|
5237
5486
|
return this.makeRequest("/orders", "POST", orderData);
|
|
5238
5487
|
}
|
|
5239
5488
|
/**
|
|
@@ -5711,9 +5960,26 @@ class AlpacaTradingAPI {
|
|
|
5711
5960
|
extended_hours: extendedHours,
|
|
5712
5961
|
position_intent: side === "buy" ? "buy_to_open" : "sell_to_open",
|
|
5713
5962
|
};
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5963
|
+
orderData.client_order_id =
|
|
5964
|
+
clientOrderId ??
|
|
5965
|
+
this.deriveClientOrderId([
|
|
5966
|
+
"equities",
|
|
5967
|
+
orderClass,
|
|
5968
|
+
type,
|
|
5969
|
+
symbol,
|
|
5970
|
+
side,
|
|
5971
|
+
Math.abs(qty),
|
|
5972
|
+
type === "limit" && limitPrice !== undefined
|
|
5973
|
+
? this.roundPriceForAlpaca(limitPrice)
|
|
5974
|
+
: undefined,
|
|
5975
|
+
extendedHours,
|
|
5976
|
+
useStopLoss && calculatedStopPrice !== undefined
|
|
5977
|
+
? this.roundPriceForAlpaca(calculatedStopPrice)
|
|
5978
|
+
: undefined,
|
|
5979
|
+
useTakeProfit && calculatedTakeProfitPrice !== undefined
|
|
5980
|
+
? this.roundPriceForAlpaca(calculatedTakeProfitPrice)
|
|
5981
|
+
: undefined,
|
|
5982
|
+
]);
|
|
5717
5983
|
// Add limit price for limit orders
|
|
5718
5984
|
if (type === "limit" && limitPrice !== undefined) {
|
|
5719
5985
|
orderData.limit_price = this.roundPriceForAlpaca(limitPrice).toString();
|
|
@@ -5771,9 +6037,18 @@ class AlpacaTradingAPI {
|
|
|
5771
6037
|
*
|
|
5772
6038
|
* @param auth - The authentication details for Alpaca
|
|
5773
6039
|
* @returns Validated authentication credentials
|
|
6040
|
+
* @throws UnsupportedBrokerError if `auth.provider` is set to a non-ALPACA provider
|
|
5774
6041
|
* @throws Error if authentication details are missing or invalid
|
|
5775
6042
|
*/
|
|
5776
6043
|
async function validateAuth(auth) {
|
|
6044
|
+
// Multi-broker guard (SP2): this seam only resolves Alpaca credentials.
|
|
6045
|
+
// `auth.provider` is typed as "ALPACA" on AlpacaAuth, but untyped callers
|
|
6046
|
+
// (or future BrokerAuth adapters) may pass other providers at runtime —
|
|
6047
|
+
// fail fast with a typed error instead of silently hitting Alpaca hosts.
|
|
6048
|
+
const requestedProvider = auth.provider;
|
|
6049
|
+
if (requestedProvider !== undefined && requestedProvider !== "ALPACA") {
|
|
6050
|
+
throw new UnsupportedBrokerError(requestedProvider);
|
|
6051
|
+
}
|
|
5777
6052
|
const inlineKey = auth.alpacaApiKey && auth.alpacaApiKey.trim().length > 0
|
|
5778
6053
|
? auth.alpacaApiKey
|
|
5779
6054
|
: undefined;
|
|
@@ -5795,26 +6070,54 @@ async function validateAuth(auth) {
|
|
|
5795
6070
|
};
|
|
5796
6071
|
}
|
|
5797
6072
|
if (auth.adapticAccountId) {
|
|
5798
|
-
|
|
5799
|
-
const alpacaAccount = (await adaptic$1.alpacaAccount.get({
|
|
5800
|
-
id: auth.adapticAccountId,
|
|
5801
|
-
}, client));
|
|
5802
|
-
if (!alpacaAccount || !alpacaAccount.APIKey || !alpacaAccount.APISecret) {
|
|
5803
|
-
throw new Error("Alpaca account not found or incomplete");
|
|
5804
|
-
}
|
|
5805
|
-
validateAlpacaCredentials({
|
|
5806
|
-
apiKey: alpacaAccount.APIKey,
|
|
5807
|
-
apiSecret: alpacaAccount.APISecret,
|
|
5808
|
-
isPaper: alpacaAccount.type === "PAPER",
|
|
5809
|
-
});
|
|
5810
|
-
return {
|
|
5811
|
-
APIKey: alpacaAccount.APIKey,
|
|
5812
|
-
APISecret: alpacaAccount.APISecret,
|
|
5813
|
-
type: alpacaAccount.type,
|
|
5814
|
-
};
|
|
6073
|
+
return resolveBrokerCredentials(auth.adapticAccountId);
|
|
5815
6074
|
}
|
|
5816
6075
|
throw new Error("Either adapticAccountId or both alpacaApiKey and alpacaApiSecret must be provided");
|
|
5817
6076
|
}
|
|
6077
|
+
/**
|
|
6078
|
+
* Resolves broker credentials for a backend brokerage-account id.
|
|
6079
|
+
*
|
|
6080
|
+
* This is the SINGLE backend-coupled credential lookup in this package —
|
|
6081
|
+
* every account-id-based credential resolution must flow through here so
|
|
6082
|
+
* that backend model changes touch exactly one function.
|
|
6083
|
+
*
|
|
6084
|
+
* SP2 transition note: today the id is an `AlpacaAccount.id` resolved via
|
|
6085
|
+
* `adaptic.alpacaAccount.get`. When backend-legacy publishes the
|
|
6086
|
+
* `BrokerageAccount` model (backfilled with `id = AlpacaAccount.id`, so the
|
|
6087
|
+
* id space is identical), the switch to `adaptic.brokerageAccount.get`
|
|
6088
|
+
* happens INSIDE this function only, following the sequencing rule in
|
|
6089
|
+
* CLAUDE.md ("Multi-Broker Sequencing Rule"): backend-legacy publishes →
|
|
6090
|
+
* utils bumps the dependency and switches this helper → utils publishes →
|
|
6091
|
+
* engine bumps its pin. Do not reference `brokerageAccount` anywhere in
|
|
6092
|
+
* this package before the pinned backend-legacy version exports it.
|
|
6093
|
+
*
|
|
6094
|
+
* The lookup is a no-cache GraphQL round trip to backend-legacy; callers
|
|
6095
|
+
* holding inline credentials should never reach it (see `validateAuth`
|
|
6096
|
+
* precedence).
|
|
6097
|
+
*
|
|
6098
|
+
* @param brokerageAccountId - Backend brokerage-account id (currently the AlpacaAccount id)
|
|
6099
|
+
* @returns Validated authentication credentials
|
|
6100
|
+
* @throws Error if the account is not found or its credentials are incomplete
|
|
6101
|
+
*/
|
|
6102
|
+
async function resolveBrokerCredentials(brokerageAccountId) {
|
|
6103
|
+
const client = await getSharedApolloClient();
|
|
6104
|
+
const alpacaAccount = (await adaptic$1.alpacaAccount.get({
|
|
6105
|
+
id: brokerageAccountId,
|
|
6106
|
+
}, client));
|
|
6107
|
+
if (!alpacaAccount || !alpacaAccount.APIKey || !alpacaAccount.APISecret) {
|
|
6108
|
+
throw new Error("Alpaca account not found or incomplete");
|
|
6109
|
+
}
|
|
6110
|
+
validateAlpacaCredentials({
|
|
6111
|
+
apiKey: alpacaAccount.APIKey,
|
|
6112
|
+
apiSecret: alpacaAccount.APISecret,
|
|
6113
|
+
isPaper: alpacaAccount.type === "PAPER",
|
|
6114
|
+
});
|
|
6115
|
+
return {
|
|
6116
|
+
APIKey: alpacaAccount.APIKey,
|
|
6117
|
+
APISecret: alpacaAccount.APISecret,
|
|
6118
|
+
type: alpacaAccount.type,
|
|
6119
|
+
};
|
|
6120
|
+
}
|
|
5818
6121
|
|
|
5819
6122
|
/**
|
|
5820
6123
|
* Legacy Alpaca Utility Functions
|
|
@@ -7944,6 +8247,7 @@ var index$1 = /*#__PURE__*/Object.freeze({
|
|
|
7944
8247
|
getOrders: getOrders$1,
|
|
7945
8248
|
makeRequest: makeRequest,
|
|
7946
8249
|
replaceOrder: replaceOrder$1,
|
|
8250
|
+
resolveBrokerCredentials: resolveBrokerCredentials,
|
|
7947
8251
|
roundPriceForAlpaca: roundPriceForAlpaca$5,
|
|
7948
8252
|
updateConfiguration: updateConfiguration,
|
|
7949
8253
|
validateAuth: validateAuth
|
|
@@ -8351,6 +8655,13 @@ var atrNs = /*#__PURE__*/Object.freeze({
|
|
|
8351
8655
|
});
|
|
8352
8656
|
|
|
8353
8657
|
const ALPACA_API_BASE = MARKET_DATA_API.CRYPTO;
|
|
8658
|
+
/**
|
|
8659
|
+
* Hard upper bound on the number of paginated news pages fetched in a single
|
|
8660
|
+
* {@link fetchNews} call. Acts as a runaway-loop backstop that is independent of
|
|
8661
|
+
* the caller-supplied `limit`, mirroring the max-page guard the equities
|
|
8662
|
+
* paginator already enforces.
|
|
8663
|
+
*/
|
|
8664
|
+
const MAX_NEWS_PAGES = 100;
|
|
8354
8665
|
/**
|
|
8355
8666
|
* Fetches cryptocurrency bars for the specified parameters.
|
|
8356
8667
|
* This function retrieves historical price data for multiple cryptocurrencies.
|
|
@@ -8450,17 +8761,25 @@ async function fetchNews(params, auth) {
|
|
|
8450
8761
|
include_content: includeContent.toString(),
|
|
8451
8762
|
limit: limit.toString(),
|
|
8452
8763
|
});
|
|
8453
|
-
const
|
|
8454
|
-
|
|
8455
|
-
|
|
8764
|
+
const authHeaders = {
|
|
8765
|
+
"APCA-API-KEY-ID": auth.APIKey,
|
|
8766
|
+
"APCA-API-SECRET-KEY": auth.APISecret,
|
|
8767
|
+
};
|
|
8768
|
+
const newsArticles = [];
|
|
8456
8769
|
let pageToken = null;
|
|
8457
|
-
let
|
|
8458
|
-
while (
|
|
8770
|
+
let pageCount = 0;
|
|
8771
|
+
while (pageCount < MAX_NEWS_PAGES) {
|
|
8772
|
+
// Rebuild the request URL on every iteration so the pagination cursor is
|
|
8773
|
+
// actually applied. Using `set` (not `append`) overwrites the previous
|
|
8774
|
+
// cursor instead of accumulating stale `page_token` values across pages.
|
|
8459
8775
|
if (pageToken) {
|
|
8460
|
-
queryParams.
|
|
8776
|
+
queryParams.set("page_token", pageToken);
|
|
8461
8777
|
}
|
|
8778
|
+
const url = `${ALPACA_API_BASE}/news?${queryParams.toString()}`;
|
|
8779
|
+
logIfDebug(`Fetching news from: ${url}`);
|
|
8462
8780
|
await withRetry(async () => {
|
|
8463
8781
|
const response = await fetch(url, {
|
|
8782
|
+
headers: authHeaders,
|
|
8464
8783
|
signal: createTimeoutSignal(DEFAULT_TIMEOUTS.ALPACA_API),
|
|
8465
8784
|
});
|
|
8466
8785
|
if (!response.ok) {
|
|
@@ -8468,7 +8787,7 @@ async function fetchNews(params, auth) {
|
|
|
8468
8787
|
throw new Error(`Alpaca API error (${response.status}): ${errorText}`);
|
|
8469
8788
|
}
|
|
8470
8789
|
const data = await response.json();
|
|
8471
|
-
|
|
8790
|
+
const pageArticles = (data.news ?? []).map((article) => ({
|
|
8472
8791
|
id: article.id,
|
|
8473
8792
|
author: article.author,
|
|
8474
8793
|
content: article.content,
|
|
@@ -8480,11 +8799,17 @@ async function fetchNews(params, auth) {
|
|
|
8480
8799
|
url: article.url,
|
|
8481
8800
|
symbols: article.symbols,
|
|
8482
8801
|
images: article.images,
|
|
8483
|
-
}))
|
|
8802
|
+
}));
|
|
8803
|
+
newsArticles.push(...pageArticles);
|
|
8484
8804
|
pageToken = data.next_page_token ?? null;
|
|
8485
|
-
|
|
8486
|
-
logIfDebug(`Received ${data.news.length} news articles. More pages: ${hasMorePages}`);
|
|
8805
|
+
logIfDebug(`Received ${pageArticles.length} news articles. Next page token: ${pageToken ? "present" : "none"}`);
|
|
8487
8806
|
}, API_RETRY_CONFIGS.CRYPTO, `Crypto.fetchNews(${symbol})`);
|
|
8807
|
+
pageCount++;
|
|
8808
|
+
// Terminate once the API reports no further pages or once we have
|
|
8809
|
+
// accumulated at least the requested number of articles.
|
|
8810
|
+
if (!pageToken || newsArticles.length >= limit) {
|
|
8811
|
+
break;
|
|
8812
|
+
}
|
|
8488
8813
|
}
|
|
8489
8814
|
// If sort is "asc" and limit is 10, return only the 10 most recent articles
|
|
8490
8815
|
if (sort === "asc" && limit === 10) {
|
|
@@ -9701,33 +10026,138 @@ const formatIndicesBarData = (data) => {
|
|
|
9701
10026
|
};
|
|
9702
10027
|
|
|
9703
10028
|
// price-utils.ts
|
|
10029
|
+
// ---------------------------------------------------------------------------
|
|
10030
|
+
// Transaction-cost (fee) model
|
|
10031
|
+
//
|
|
10032
|
+
// Alpaca's REST order object does not expose the realized per-order fee, so the
|
|
10033
|
+
// transaction cost is reconstructed from the published fee schedules, branching
|
|
10034
|
+
// on the order's ACTUAL asset class (never a hardcoded STOCK). Every rate is a
|
|
10035
|
+
// named constant sourced from Alpaca / SEC / FINRA public schedules (2024-2025)
|
|
10036
|
+
// so it can be audited and updated in one place.
|
|
10037
|
+
// ---------------------------------------------------------------------------
|
|
10038
|
+
/** Basis points in one whole unit (1 = 10,000 bps). */
|
|
10039
|
+
const BPS_PER_UNIT = 10_000;
|
|
10040
|
+
/** Shares represented by one US listed option contract. */
|
|
10041
|
+
const OPTIONS_CONTRACT_MULTIPLIER = 100;
|
|
10042
|
+
/**
|
|
10043
|
+
* SEC Section 31 fee, charged on the principal of SELL orders for equities and
|
|
10044
|
+
* options. FY2024+ rate: USD 8.00 per USD 1,000,000 of principal.
|
|
10045
|
+
*/
|
|
10046
|
+
const SEC_SECTION31_FEE_PER_USD = 8.0 / 1_000_000;
|
|
10047
|
+
/** FINRA Trading Activity Fee (TAF) for equity sells: USD per share sold. */
|
|
10048
|
+
const FINRA_TAF_EQUITY_PER_SHARE = 0.000166;
|
|
10049
|
+
/** FINRA TAF for option sells: USD per contract sold. */
|
|
10050
|
+
const FINRA_TAF_OPTIONS_PER_CONTRACT = 0.00279;
|
|
10051
|
+
/** FINRA TAF is capped per trade regardless of size. */
|
|
10052
|
+
const FINRA_TAF_MAX_PER_TRADE = 8.3;
|
|
10053
|
+
/** OCC clearing fee per option contract, capped per trade. */
|
|
10054
|
+
const OCC_CLEARING_FEE_PER_CONTRACT = 0.02;
|
|
10055
|
+
const OCC_CLEARING_FEE_MAX_PER_TRADE = 55.0;
|
|
10056
|
+
/**
|
|
10057
|
+
* Options Regulatory Fee (ORF) pass-through, charged on both sides, USD per
|
|
10058
|
+
* contract. Published, exchange-set pass-through rate.
|
|
10059
|
+
*/
|
|
10060
|
+
const OPTIONS_REGULATORY_FEE_PER_CONTRACT = 0.02685;
|
|
10061
|
+
/**
|
|
10062
|
+
* Alpaca crypto TAKER fee schedule as `[minTrailing30dVolumeUsd, takerBps]`,
|
|
10063
|
+
* ordered ascending by volume threshold. Market orders are takers; absent a
|
|
10064
|
+
* known trailing-30-day volume we conservatively select the tier-1 (highest)
|
|
10065
|
+
* taker rate. Source: Alpaca Crypto fee schedule.
|
|
10066
|
+
*/
|
|
10067
|
+
const ALPACA_CRYPTO_TAKER_FEE_TIERS_BPS = [
|
|
10068
|
+
[0, 25],
|
|
10069
|
+
[100_000, 22],
|
|
10070
|
+
[500_000, 20],
|
|
10071
|
+
[1_000_000, 18],
|
|
10072
|
+
[10_000_000, 15],
|
|
10073
|
+
[25_000_000, 13],
|
|
10074
|
+
[50_000_000, 12],
|
|
10075
|
+
[100_000_000, 10],
|
|
10076
|
+
];
|
|
10077
|
+
/**
|
|
10078
|
+
* Resolve the applicable Alpaca crypto taker fee (in bps) for a trailing
|
|
10079
|
+
* 30-day USD volume. Defaults to the tier-1 rate when the volume is unknown.
|
|
10080
|
+
* @param trailing30dVolumeUsd - Trailing 30-day traded notional in USD.
|
|
10081
|
+
* @returns The taker fee in basis points.
|
|
10082
|
+
*/
|
|
10083
|
+
function resolveCryptoTakerBps(trailing30dVolumeUsd) {
|
|
10084
|
+
let bps = ALPACA_CRYPTO_TAKER_FEE_TIERS_BPS[0][1];
|
|
10085
|
+
for (const [threshold, tierBps] of ALPACA_CRYPTO_TAKER_FEE_TIERS_BPS) {
|
|
10086
|
+
if (trailing30dVolumeUsd >= threshold) {
|
|
10087
|
+
bps = tierBps;
|
|
10088
|
+
}
|
|
10089
|
+
else {
|
|
10090
|
+
break;
|
|
10091
|
+
}
|
|
10092
|
+
}
|
|
10093
|
+
return bps;
|
|
10094
|
+
}
|
|
10095
|
+
/**
|
|
10096
|
+
* Computes the realized transaction cost (fees + regulatory charges) for the
|
|
10097
|
+
* Alpaca order backing a single {@link types.Action}, branching on the order's
|
|
10098
|
+
* actual asset class. Returns 0 only when there is genuinely no order to price
|
|
10099
|
+
* (no linked order id, order not found, or nothing filled) — never as a
|
|
10100
|
+
* fabricated success.
|
|
10101
|
+
* @param action - The action whose linked Alpaca order should be priced.
|
|
10102
|
+
* @param trade - The parent trade (supplies the Alpaca account id).
|
|
10103
|
+
* @param alpacaAccount - The Alpaca account supplying broker credentials.
|
|
10104
|
+
* @returns The total fee in account currency (USD).
|
|
10105
|
+
*/
|
|
9704
10106
|
const calculateFees = async (action, trade, alpacaAccount) => {
|
|
9705
|
-
let fee = 0;
|
|
9706
10107
|
const alpacaOrderId = action.alpacaOrderId;
|
|
9707
10108
|
if (!alpacaOrderId)
|
|
9708
|
-
return
|
|
10109
|
+
return 0;
|
|
9709
10110
|
const order = await getOrder$1({
|
|
9710
10111
|
adapticAccountId: trade.alpacaAccountId,
|
|
9711
10112
|
alpacaApiKey: alpacaAccount.APIKey,
|
|
9712
10113
|
alpacaApiSecret: alpacaAccount.APISecret,
|
|
9713
10114
|
}, alpacaOrderId);
|
|
9714
10115
|
if (!order)
|
|
9715
|
-
return
|
|
9716
|
-
const
|
|
9717
|
-
Number(order.
|
|
9718
|
-
order.notional || 0;
|
|
9719
|
-
Number(order.filled_avg_price || order.limit_price || order.stop_price) ||
|
|
10116
|
+
return 0;
|
|
10117
|
+
const filledQty = Number(order.filled_qty) || 0;
|
|
10118
|
+
const filledPrice = Number(order.filled_avg_price ?? order.limit_price ?? order.stop_price) ||
|
|
9720
10119
|
0;
|
|
9721
|
-
|
|
9722
|
-
|
|
9723
|
-
|
|
9724
|
-
|
|
9725
|
-
|
|
9726
|
-
|
|
9727
|
-
|
|
9728
|
-
|
|
10120
|
+
// Realized notional prefers the actual fill (qty * avg price); it falls back
|
|
10121
|
+
// to the order's notional field for dollar-notional (fractional) orders.
|
|
10122
|
+
const notional = filledQty > 0 && filledPrice > 0
|
|
10123
|
+
? filledQty * filledPrice
|
|
10124
|
+
: Number(order.notional ?? 0) || 0;
|
|
10125
|
+
if (notional <= 0)
|
|
10126
|
+
return 0;
|
|
10127
|
+
const isSell = order.side === "sell";
|
|
10128
|
+
switch (order.asset_class) {
|
|
10129
|
+
case "crypto": {
|
|
10130
|
+
// Crypto fees are bps of notional. Without a known 30-day volume we use
|
|
10131
|
+
// the conservative tier-1 taker rate.
|
|
10132
|
+
const takerBps = resolveCryptoTakerBps(0);
|
|
10133
|
+
return (notional * takerBps) / BPS_PER_UNIT;
|
|
10134
|
+
}
|
|
10135
|
+
case "us_option": {
|
|
10136
|
+
const contracts = filledQty > 0 ? filledQty : Number(order.qty) || 0;
|
|
10137
|
+
const occFee = Math.min(contracts * OCC_CLEARING_FEE_PER_CONTRACT, OCC_CLEARING_FEE_MAX_PER_TRADE);
|
|
10138
|
+
const orfFee = contracts * OPTIONS_REGULATORY_FEE_PER_CONTRACT;
|
|
10139
|
+
let fee = occFee + orfFee;
|
|
10140
|
+
if (isSell) {
|
|
10141
|
+
// Option premium is quoted per share; SEC fee applies to the full
|
|
10142
|
+
// principal (premium * contract multiplier).
|
|
10143
|
+
const optionPrincipal = notional * OPTIONS_CONTRACT_MULTIPLIER;
|
|
10144
|
+
const secFee = optionPrincipal * SEC_SECTION31_FEE_PER_USD;
|
|
10145
|
+
const taf = Math.min(contracts * FINRA_TAF_OPTIONS_PER_CONTRACT, FINRA_TAF_MAX_PER_TRADE);
|
|
10146
|
+
fee += secFee + taf;
|
|
10147
|
+
}
|
|
10148
|
+
return fee;
|
|
10149
|
+
}
|
|
10150
|
+
case "us_equity":
|
|
10151
|
+
default: {
|
|
10152
|
+
// Alpaca charges USD 0 commission on US equities; only sell-side
|
|
10153
|
+
// regulatory charges (SEC Section 31 + FINRA TAF) apply.
|
|
10154
|
+
if (!isSell)
|
|
10155
|
+
return 0;
|
|
10156
|
+
const secFee = notional * SEC_SECTION31_FEE_PER_USD;
|
|
10157
|
+
const taf = Math.min(filledQty * FINRA_TAF_EQUITY_PER_SHARE, FINRA_TAF_MAX_PER_TRADE);
|
|
10158
|
+
return secFee + taf;
|
|
10159
|
+
}
|
|
9729
10160
|
}
|
|
9730
|
-
return fee;
|
|
9731
10161
|
};
|
|
9732
10162
|
const computeTotalFees = async (trade) => {
|
|
9733
10163
|
let totalFees = 0;
|
|
@@ -10596,16 +11026,76 @@ async function calculateExpenseRatio({ accountId, client, alpacaAccount, }) {
|
|
|
10596
11026
|
return "N/A";
|
|
10597
11027
|
}
|
|
10598
11028
|
const equity = parseFloat(accountDetails.equity);
|
|
10599
|
-
// Fetch
|
|
10600
|
-
|
|
10601
|
-
//
|
|
11029
|
+
// Fetch the account's real trailing fee expenses from Alpaca account
|
|
11030
|
+
// activities. A genuine data-source failure yields "N/A" (unknown) rather
|
|
11031
|
+
// than a fabricated 0.00%.
|
|
11032
|
+
const auth = {
|
|
11033
|
+
adapticAccountId: alpacaAccountId,
|
|
11034
|
+
alpacaApiKey: alpacaAccount?.APIKey,
|
|
11035
|
+
alpacaApiSecret: alpacaAccount?.APISecret,
|
|
11036
|
+
};
|
|
11037
|
+
let expenses;
|
|
11038
|
+
try {
|
|
11039
|
+
expenses = await fetchTrailingFeeExpenses(auth);
|
|
11040
|
+
}
|
|
11041
|
+
catch (error) {
|
|
11042
|
+
getLogger().warn("Failed to fetch Alpaca account fee activities for expense ratio.", { error });
|
|
11043
|
+
return "N/A";
|
|
11044
|
+
}
|
|
11045
|
+
// Calculate expense ratio (trailing fees as a percentage of current equity).
|
|
10602
11046
|
const expenseRatio = (expenses / equity) * 100;
|
|
10603
11047
|
return `${expenseRatio.toFixed(2)}%`;
|
|
10604
11048
|
}
|
|
10605
|
-
|
|
10606
|
-
|
|
10607
|
-
|
|
10608
|
-
|
|
11049
|
+
/** Trailing window over which account fees are aggregated for the expense ratio. */
|
|
11050
|
+
const EXPENSE_TRAILING_WINDOW_DAYS = 365;
|
|
11051
|
+
/** Milliseconds in one day. */
|
|
11052
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
11053
|
+
/** Alpaca account-activity types that represent fees/regulatory charges. */
|
|
11054
|
+
const FEE_ACTIVITY_TYPES = "FEE,REG,CFEE";
|
|
11055
|
+
/** Page size for the paginated Alpaca account-activities endpoint. */
|
|
11056
|
+
const ACTIVITIES_PAGE_SIZE = 100;
|
|
11057
|
+
/** Hard cap on activity pages to bound pagination on unexpected responses. */
|
|
11058
|
+
const ACTIVITIES_MAX_PAGES = 1000;
|
|
11059
|
+
/**
|
|
11060
|
+
* Aggregates the account's fee/regulatory charges over the trailing window from
|
|
11061
|
+
* the Alpaca account-activities endpoint, following id-based pagination.
|
|
11062
|
+
* @param auth - Alpaca authentication (account id and/or direct API keys).
|
|
11063
|
+
* @returns Total fees in account currency (USD) as a positive number.
|
|
11064
|
+
*/
|
|
11065
|
+
async function fetchTrailingFeeExpenses(auth) {
|
|
11066
|
+
const after = new Date(Date.now() - EXPENSE_TRAILING_WINDOW_DAYS * MS_PER_DAY).toISOString();
|
|
11067
|
+
let total = 0;
|
|
11068
|
+
let pageToken;
|
|
11069
|
+
for (let page = 0; page < ACTIVITIES_MAX_PAGES; page++) {
|
|
11070
|
+
const queryParams = new URLSearchParams({
|
|
11071
|
+
activity_types: FEE_ACTIVITY_TYPES,
|
|
11072
|
+
after,
|
|
11073
|
+
page_size: String(ACTIVITIES_PAGE_SIZE),
|
|
11074
|
+
});
|
|
11075
|
+
if (pageToken) {
|
|
11076
|
+
queryParams.append("page_token", pageToken);
|
|
11077
|
+
}
|
|
11078
|
+
const activities = await makeRequest(auth, {
|
|
11079
|
+
endpoint: "/account/activities",
|
|
11080
|
+
method: "GET",
|
|
11081
|
+
queryString: `?${queryParams.toString()}`,
|
|
11082
|
+
});
|
|
11083
|
+
if (!Array.isArray(activities) || activities.length === 0) {
|
|
11084
|
+
break;
|
|
11085
|
+
}
|
|
11086
|
+
for (const activity of activities) {
|
|
11087
|
+
const amount = parseFloat(activity.net_amount ?? "");
|
|
11088
|
+
if (Number.isFinite(amount)) {
|
|
11089
|
+
// Fee entries are debits (negative net_amount); accumulate magnitude.
|
|
11090
|
+
total += Math.abs(amount);
|
|
11091
|
+
}
|
|
11092
|
+
}
|
|
11093
|
+
if (activities.length < ACTIVITIES_PAGE_SIZE) {
|
|
11094
|
+
break;
|
|
11095
|
+
}
|
|
11096
|
+
pageToken = activities[activities.length - 1].id;
|
|
11097
|
+
}
|
|
11098
|
+
return total;
|
|
10609
11099
|
}
|
|
10610
11100
|
/**
|
|
10611
11101
|
* Calculates the liquidity ratio for a given Alpaca account.
|
|
@@ -11166,9 +11656,16 @@ function calculateBetaFromReturns(portfolioReturns, benchmarkReturns) {
|
|
|
11166
11656
|
const denom = n > 1 ? n - 1 : 1;
|
|
11167
11657
|
covariance /= denom;
|
|
11168
11658
|
variance /= denom;
|
|
11169
|
-
// Handle zero variance
|
|
11170
|
-
|
|
11171
|
-
|
|
11659
|
+
// Handle zero (or numerically-degenerate) variance. A constant benchmark
|
|
11660
|
+
// series can still produce a tiny nonzero variance because the computed
|
|
11661
|
+
// mean differs from the constant by an ulp; dividing covariance by that
|
|
11662
|
+
// rounding noise yields a meaningless beta. Treat any variance at or
|
|
11663
|
+
// below the summation noise floor — (n * eps * |mean|)^2, the square of
|
|
11664
|
+
// the worst-case naive-summation error — as zero. When the mean is
|
|
11665
|
+
// exactly 0 this reduces to the exact zero check.
|
|
11666
|
+
const varianceNoiseFloor = (n * Number.EPSILON * Math.abs(averageBenchmarkReturn)) ** 2;
|
|
11667
|
+
if (variance <= varianceNoiseFloor) {
|
|
11668
|
+
getLogger().warn("Benchmark variance is zero or below the floating-point noise floor. Setting beta to 0.");
|
|
11172
11669
|
return {
|
|
11173
11670
|
beta: 0,
|
|
11174
11671
|
covariance,
|
|
@@ -11258,6 +11755,58 @@ async function calculateInformationRatio(portfolioHistory, benchmarkBars) {
|
|
|
11258
11755
|
}
|
|
11259
11756
|
return informationRatio.toFixed(4);
|
|
11260
11757
|
}
|
|
11758
|
+
/**
|
|
11759
|
+
* Maps a portfolio-history timeframe token to the Alpaca market-data
|
|
11760
|
+
* {@link TimeFrame} accepted by the historical-bars endpoint. Benchmark
|
|
11761
|
+
* comparison is daily by default when no timeframe is supplied.
|
|
11762
|
+
* @param timeframe - The portfolio-history timeframe token.
|
|
11763
|
+
* @returns The equivalent Alpaca market-data timeframe.
|
|
11764
|
+
*/
|
|
11765
|
+
function toAlpacaTimeFrame(timeframe) {
|
|
11766
|
+
switch (timeframe) {
|
|
11767
|
+
case "1Min":
|
|
11768
|
+
return "1Min";
|
|
11769
|
+
case "5Min":
|
|
11770
|
+
return "5Min";
|
|
11771
|
+
case "15Min":
|
|
11772
|
+
return "15Min";
|
|
11773
|
+
case "1H":
|
|
11774
|
+
return "1Hour";
|
|
11775
|
+
case "1D":
|
|
11776
|
+
return "1Day";
|
|
11777
|
+
default:
|
|
11778
|
+
return "1Day";
|
|
11779
|
+
}
|
|
11780
|
+
}
|
|
11781
|
+
/** Milliseconds per second, for RFC-3339 → Unix-second conversion. */
|
|
11782
|
+
const MS_PER_SECOND = 1000;
|
|
11783
|
+
/**
|
|
11784
|
+
* Fetches benchmark OHLCV bars from the wrapped Alpaca market-data vendor and
|
|
11785
|
+
* maps them into {@link BenchmarkBar}s (Unix-second timestamp + close price)
|
|
11786
|
+
* expected by the alpha/beta/information-ratio calculators.
|
|
11787
|
+
* @param request - Benchmark symbol, RFC-3339 start/end, and timeframe token.
|
|
11788
|
+
* @returns The benchmark bars, sorted ascending by time; empty if none.
|
|
11789
|
+
*/
|
|
11790
|
+
async function fetchBenchmarkBars(request) {
|
|
11791
|
+
const { symbol, start, end, timeframe } = request;
|
|
11792
|
+
const response = await marketDataAPI.getHistoricalBars({
|
|
11793
|
+
symbols: [symbol],
|
|
11794
|
+
timeframe: toAlpacaTimeFrame(timeframe),
|
|
11795
|
+
start,
|
|
11796
|
+
end,
|
|
11797
|
+
sort: "asc",
|
|
11798
|
+
});
|
|
11799
|
+
const bars = response.bars[symbol];
|
|
11800
|
+
if (!Array.isArray(bars) || bars.length === 0) {
|
|
11801
|
+
return [];
|
|
11802
|
+
}
|
|
11803
|
+
return bars
|
|
11804
|
+
.map((bar) => ({
|
|
11805
|
+
t: Math.floor(new Date(bar.t).getTime() / MS_PER_SECOND),
|
|
11806
|
+
c: bar.c,
|
|
11807
|
+
}))
|
|
11808
|
+
.filter((bar) => Number.isFinite(bar.t) && Number.isFinite(bar.c));
|
|
11809
|
+
}
|
|
11261
11810
|
/**
|
|
11262
11811
|
* Fetches performance metrics for a given Alpaca account.
|
|
11263
11812
|
* @param params - The parameters for fetching performance metrics.
|
|
@@ -11322,7 +11871,10 @@ async function fetchPerformanceMetrics({ params, client, accountId, alpacaAccoun
|
|
|
11322
11871
|
getLogger().error("[fetchPerformanceMetrics] Error fetching portfolio history:", error);
|
|
11323
11872
|
throw new Error("Failed to retrieve portfolio history data");
|
|
11324
11873
|
}
|
|
11325
|
-
// Fetch benchmark data
|
|
11874
|
+
// Fetch benchmark data directly from the wrapped Alpaca market-data vendor.
|
|
11875
|
+
// (Previously this hit a relative "/api/market-data/historical-prices"
|
|
11876
|
+
// Next.js route that only resolves in a browser; in a Node/engine runtime
|
|
11877
|
+
// the relative fetch always threw, silently zeroing out alpha/beta/IR.)
|
|
11326
11878
|
const benchmarkSymbol = "SPY";
|
|
11327
11879
|
let benchmarkBars = [];
|
|
11328
11880
|
try {
|
|
@@ -11333,24 +11885,16 @@ async function fetchPerformanceMetrics({ params, client, accountId, alpacaAccoun
|
|
|
11333
11885
|
: params?.period
|
|
11334
11886
|
? params?.period
|
|
11335
11887
|
: "1Y",
|
|
11336
|
-
outputFormat: "
|
|
11888
|
+
outputFormat: "iso",
|
|
11337
11889
|
intraday_reporting: params?.intraday_reporting,
|
|
11338
11890
|
});
|
|
11339
|
-
|
|
11340
|
-
|
|
11341
|
-
|
|
11342
|
-
|
|
11343
|
-
|
|
11344
|
-
signal: createTimeoutSignal(DEFAULT_TIMEOUTS.GENERAL),
|
|
11891
|
+
benchmarkBars = await fetchBenchmarkBars({
|
|
11892
|
+
symbol: benchmarkSymbol,
|
|
11893
|
+
start: String(start),
|
|
11894
|
+
end: String(end),
|
|
11895
|
+
timeframe: params.timeframe,
|
|
11345
11896
|
});
|
|
11346
|
-
if (
|
|
11347
|
-
const errorText = await response.text();
|
|
11348
|
-
throw new Error(`Failed to fetch benchmark data: ${response.statusText} - ${errorText}`);
|
|
11349
|
-
}
|
|
11350
|
-
benchmarkBars = await response.json();
|
|
11351
|
-
if (!benchmarkBars ||
|
|
11352
|
-
!Array.isArray(benchmarkBars) ||
|
|
11353
|
-
benchmarkBars.length === 0) {
|
|
11897
|
+
if (benchmarkBars.length === 0) {
|
|
11354
11898
|
throw new Error("Received empty or invalid benchmark data");
|
|
11355
11899
|
}
|
|
11356
11900
|
}
|
|
@@ -12347,8 +12891,33 @@ const timeDiffString = (milliseconds) => {
|
|
|
12347
12891
|
return parts.join(", ");
|
|
12348
12892
|
};
|
|
12349
12893
|
|
|
12894
|
+
/**
|
|
12895
|
+
* Multi-broker foundation types
|
|
12896
|
+
*
|
|
12897
|
+
* Provider-agnostic brokerage types for the org → fund → brokerageAccount →
|
|
12898
|
+
* broker alignment (SP2). These are strictly ADDITIVE: the existing
|
|
12899
|
+
* Alpaca-specific types (`AlpacaAuth`, `AlpacaCredentials`,
|
|
12900
|
+
* `AlpacaClientConfig`) remain the canonical shapes consumed by the engine
|
|
12901
|
+
* and are unchanged. New provider-aware call sites should prefer these
|
|
12902
|
+
* types; only ALPACA is implemented today — IBKR and COINBASE arms are
|
|
12903
|
+
* typed placeholders that resolve to `UnsupportedBrokerError` at runtime.
|
|
12904
|
+
*
|
|
12905
|
+
* @module @adaptic/utils/types/broker-types
|
|
12906
|
+
*/
|
|
12907
|
+
/**
|
|
12908
|
+
* Type guard narrowing {@link BrokerCredentials} to the implemented
|
|
12909
|
+
* ALPACA arm.
|
|
12910
|
+
*
|
|
12911
|
+
* @param credentials - Any broker credentials union member
|
|
12912
|
+
* @returns True when the credentials belong to the ALPACA provider
|
|
12913
|
+
*/
|
|
12914
|
+
function isAlpacaBrokerCredentials(credentials) {
|
|
12915
|
+
return credentials.provider === "ALPACA";
|
|
12916
|
+
}
|
|
12917
|
+
|
|
12350
12918
|
var Types = /*#__PURE__*/Object.freeze({
|
|
12351
|
-
__proto__: null
|
|
12919
|
+
__proto__: null,
|
|
12920
|
+
isAlpacaBrokerCredentials: isAlpacaBrokerCredentials
|
|
12352
12921
|
});
|
|
12353
12922
|
|
|
12354
12923
|
/**
|
|
@@ -50661,12 +51230,16 @@ class AlpacaClient {
|
|
|
50661
51230
|
}
|
|
50662
51231
|
// Client cache for connection pooling
|
|
50663
51232
|
const clientCache = new Map();
|
|
51233
|
+
// Provider discriminant for cache-key scoping (multi-broker SP2 seam):
|
|
51234
|
+
// keeps Alpaca pool entries disjoint from future providers that might
|
|
51235
|
+
// reuse an identical apiKey string.
|
|
51236
|
+
const ALPACA_PROVIDER = "ALPACA";
|
|
50664
51237
|
/**
|
|
50665
51238
|
* Create or get a cached Alpaca client
|
|
50666
|
-
* Uses apiKey as cache key for connection pooling
|
|
51239
|
+
* Uses provider + apiKey + accountType as cache key for connection pooling
|
|
50667
51240
|
*/
|
|
50668
51241
|
function createAlpacaClient(config) {
|
|
50669
|
-
const cacheKey = `${config.apiKey}-${config.accountType}`;
|
|
51242
|
+
const cacheKey = `${ALPACA_PROVIDER}-${config.apiKey}-${config.accountType}`;
|
|
50670
51243
|
if (clientCache.has(cacheKey)) {
|
|
50671
51244
|
log$k(`Returning cached client for ${config.accountType}`, { type: "debug" });
|
|
50672
51245
|
return clientCache.get(cacheKey);
|
|
@@ -68786,6 +69359,49 @@ function verifyFetchKeepAlive() {
|
|
|
68786
69359
|
};
|
|
68787
69360
|
}
|
|
68788
69361
|
|
|
69362
|
+
/**
|
|
69363
|
+
* Broker Client Factory
|
|
69364
|
+
*
|
|
69365
|
+
* Provider-agnostic entry point for broker trading clients (SP2 multi-broker
|
|
69366
|
+
* seam). Strictly ADDITIVE: `createAlpacaClient`, `createAlpacaTradingAPI`,
|
|
69367
|
+
* and `createAlpacaMarketDataAPI` remain the canonical Alpaca factories and
|
|
69368
|
+
* are unchanged. Only ALPACA is implemented — all other providers throw a
|
|
69369
|
+
* typed {@link UnsupportedBrokerError}.
|
|
69370
|
+
*
|
|
69371
|
+
* @module @adaptic/utils/broker
|
|
69372
|
+
*/
|
|
69373
|
+
/**
|
|
69374
|
+
* Create (or reuse from cache) a broker trading client for the given
|
|
69375
|
+
* credentials.
|
|
69376
|
+
*
|
|
69377
|
+
* ALPACA delegates to `createAlpacaClient`, whose connection-pool cache key
|
|
69378
|
+
* is provider-scoped (`ALPACA-<apiKey>-<accountType>`), so a future
|
|
69379
|
+
* provider reusing an identical apiKey string can never collide with an
|
|
69380
|
+
* Alpaca client. All other providers — including unknown provider strings
|
|
69381
|
+
* from untyped callers — throw {@link UnsupportedBrokerError}.
|
|
69382
|
+
*
|
|
69383
|
+
* @param credentials - Discriminated broker credentials union
|
|
69384
|
+
* @returns A provider-appropriate {@link BrokerTradingClient}
|
|
69385
|
+
* @throws UnsupportedBrokerError for any provider other than ALPACA
|
|
69386
|
+
*/
|
|
69387
|
+
function createBrokerClient(credentials) {
|
|
69388
|
+
switch (credentials.provider) {
|
|
69389
|
+
case "ALPACA":
|
|
69390
|
+
return createAlpacaClient({
|
|
69391
|
+
apiKey: credentials.apiKey,
|
|
69392
|
+
apiSecret: credentials.apiSecret,
|
|
69393
|
+
accountType: credentials.type,
|
|
69394
|
+
});
|
|
69395
|
+
case "IBKR":
|
|
69396
|
+
case "COINBASE":
|
|
69397
|
+
throw new UnsupportedBrokerError(credentials.provider);
|
|
69398
|
+
}
|
|
69399
|
+
// Unreachable for typed callers (the switch above is exhaustive), but
|
|
69400
|
+
// untyped runtime callers may pass an unrecognised provider string —
|
|
69401
|
+
// fail fast with the same typed error rather than undefined behaviour.
|
|
69402
|
+
throw new UnsupportedBrokerError(String(credentials.provider));
|
|
69403
|
+
}
|
|
69404
|
+
|
|
68789
69405
|
/**
|
|
68790
69406
|
* Mirror enums for the trading policy preference system.
|
|
68791
69407
|
* These enums are used by both the trading engine and the frontend app
|
|
@@ -69866,5 +70482,5 @@ const adaptic = {
|
|
|
69866
70482
|
};
|
|
69867
70483
|
const adptc = adaptic;
|
|
69868
70484
|
|
|
69869
|
-
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, 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, 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, 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 };
|
|
70485
|
+
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 };
|
|
69870
70486
|
//# sourceMappingURL=index.mjs.map
|