@adaptic/utils 0.0.997 → 0.0.999
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 +145 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +145 -23
- package/dist/index.mjs.map +1 -1
- package/dist/types/__tests__/alpaca-trading-api.test.d.ts +2 -0
- package/dist/types/__tests__/alpaca-trading-api.test.d.ts.map +1 -0
- package/dist/types/__tests__/legacy-auth.test.d.ts +2 -0
- package/dist/types/__tests__/legacy-auth.test.d.ts.map +1 -0
- package/dist/types/alpaca/legacy/auth.d.ts +18 -1
- package/dist/types/alpaca/legacy/auth.d.ts.map +1 -1
- package/dist/types/alpaca/legacy/positions.d.ts.map +1 -1
- package/dist/types/alpaca-trading-api.d.ts +25 -2
- package/dist/types/alpaca-trading-api.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -4297,6 +4297,15 @@ const API_RETRY_CONFIGS = {
|
|
|
4297
4297
|
};
|
|
4298
4298
|
|
|
4299
4299
|
const limitPriceSlippagePercent100 = 0.1; // 0.1%
|
|
4300
|
+
/**
|
|
4301
|
+
* Alpaca's maximum page size for GET /orders — also our explicit default.
|
|
4302
|
+
* Alpaca's silent server-side default is 50, which truncates order
|
|
4303
|
+
* visibility on protective-stop / qty-reservation paths; we never let it
|
|
4304
|
+
* apply (mirrors ORDER_CHUNK_SIZE in src/alpaca/legacy/orders.ts).
|
|
4305
|
+
*/
|
|
4306
|
+
const ORDER_PAGE_LIMIT = 500;
|
|
4307
|
+
/** Delay between order pagination pages to stay clear of rate limits. */
|
|
4308
|
+
const ORDER_PAGINATION_DELAY_MS = 300;
|
|
4300
4309
|
/**
|
|
4301
4310
|
Websocket example
|
|
4302
4311
|
const alpacaAPI = createAlpacaTradingAPI(credentials); // type AlpacaCredentials
|
|
@@ -4632,10 +4641,25 @@ class AlpacaTradingAPI {
|
|
|
4632
4641
|
return positions;
|
|
4633
4642
|
}
|
|
4634
4643
|
/**
|
|
4635
|
-
* Get all orders
|
|
4644
|
+
* Get all orders, with explicit paging.
|
|
4645
|
+
*
|
|
4646
|
+
* When `params.limit` is provided it is treated as a caller-controlled cap
|
|
4647
|
+
* and a single request is made (preserves the previous contract for
|
|
4648
|
+
* callers that run their own page walks, e.g. phantom-order lookup).
|
|
4649
|
+
*
|
|
4650
|
+
* When `limit` is omitted, Alpaca's silent server-side default of 50
|
|
4651
|
+
* records is NOT allowed to apply — that default truncated open-order
|
|
4652
|
+
* visibility on protective-stop and qty-reservation paths. Instead the
|
|
4653
|
+
* API maximum (500) is requested explicitly and, whenever a full page
|
|
4654
|
+
* comes back, the result is paginated with a `submitted_at` cursor walk
|
|
4655
|
+
* (`until` for desc — the default — or `after` for asc) until a short
|
|
4656
|
+
* page indicates the window is exhausted. Pages are deduplicated by
|
|
4657
|
+
* order id so same-timestamp boundary orders can never be double
|
|
4658
|
+
* counted, and the walk terminates if a full page yields no new orders.
|
|
4659
|
+
*
|
|
4636
4660
|
* @param params (GetOrdersParams) - optional parameters to filter the orders
|
|
4637
4661
|
* - status: 'open' | 'closed' | 'all'
|
|
4638
|
-
* - limit: number
|
|
4662
|
+
* - limit: number — caller-controlled cap; disables pagination
|
|
4639
4663
|
* - after: string
|
|
4640
4664
|
* - until: string
|
|
4641
4665
|
* - direction: 'asc' | 'desc'
|
|
@@ -4645,11 +4669,54 @@ class AlpacaTradingAPI {
|
|
|
4645
4669
|
* @returns all orders
|
|
4646
4670
|
*/
|
|
4647
4671
|
async getOrders(params = {}) {
|
|
4672
|
+
if (params.limit !== undefined) {
|
|
4673
|
+
return this.fetchOrdersPage(params, params.limit);
|
|
4674
|
+
}
|
|
4675
|
+
const isAsc = params.direction === "asc";
|
|
4676
|
+
const allOrders = [];
|
|
4677
|
+
const seenOrderIds = new Set();
|
|
4678
|
+
// Moving cursor: `until` walks backwards for desc (the default),
|
|
4679
|
+
// `after` walks forwards for asc. Both are exclusive on Alpaca's side.
|
|
4680
|
+
let cursor = isAsc ? params.after : params.until;
|
|
4681
|
+
while (true) {
|
|
4682
|
+
const pageParams = {
|
|
4683
|
+
...params,
|
|
4684
|
+
...(isAsc ? { after: cursor } : { until: cursor }),
|
|
4685
|
+
};
|
|
4686
|
+
const page = await this.fetchOrdersPage(pageParams, ORDER_PAGE_LIMIT);
|
|
4687
|
+
let addedCount = 0;
|
|
4688
|
+
for (const order of page) {
|
|
4689
|
+
if (!seenOrderIds.has(order.id)) {
|
|
4690
|
+
seenOrderIds.add(order.id);
|
|
4691
|
+
allOrders.push(order);
|
|
4692
|
+
addedCount++;
|
|
4693
|
+
}
|
|
4694
|
+
}
|
|
4695
|
+
// Short page = window exhausted (same termination as legacy getOrders).
|
|
4696
|
+
if (page.length < ORDER_PAGE_LIMIT)
|
|
4697
|
+
break;
|
|
4698
|
+
const lastOrder = page[page.length - 1];
|
|
4699
|
+
// No usable cursor, or a full page of already-seen orders (cursor not
|
|
4700
|
+
// advancing): stop rather than risk an unbounded walk.
|
|
4701
|
+
if (!lastOrder.submitted_at || addedCount === 0)
|
|
4702
|
+
break;
|
|
4703
|
+
cursor = lastOrder.submitted_at;
|
|
4704
|
+
await new Promise((resolve) => setTimeout(resolve, ORDER_PAGINATION_DELAY_MS));
|
|
4705
|
+
}
|
|
4706
|
+
return allOrders;
|
|
4707
|
+
}
|
|
4708
|
+
/**
|
|
4709
|
+
* Issues a single GET /orders request with an explicit `limit` so
|
|
4710
|
+
* Alpaca's silent 50-record server default can never apply.
|
|
4711
|
+
* @param params - order filter parameters (limit is supplied separately)
|
|
4712
|
+
* @param limit - explicit page size to request
|
|
4713
|
+
* @returns one page of orders
|
|
4714
|
+
*/
|
|
4715
|
+
async fetchOrdersPage(params, limit) {
|
|
4648
4716
|
const queryParams = new URLSearchParams();
|
|
4649
4717
|
if (params.status)
|
|
4650
4718
|
queryParams.append("status", params.status);
|
|
4651
|
-
|
|
4652
|
-
queryParams.append("limit", params.limit.toString());
|
|
4719
|
+
queryParams.append("limit", limit.toString());
|
|
4653
4720
|
if (params.after)
|
|
4654
4721
|
queryParams.append("after", params.after);
|
|
4655
4722
|
if (params.until)
|
|
@@ -4662,9 +4729,10 @@ class AlpacaTradingAPI {
|
|
|
4662
4729
|
queryParams.append("symbols", params.symbols.join(","));
|
|
4663
4730
|
if (params.side)
|
|
4664
4731
|
queryParams.append("side", params.side);
|
|
4665
|
-
const endpoint = `/orders
|
|
4732
|
+
const endpoint = `/orders?${queryParams.toString()}`;
|
|
4666
4733
|
try {
|
|
4667
|
-
|
|
4734
|
+
const orders = await this.makeRequest(endpoint);
|
|
4735
|
+
return orders ?? [];
|
|
4668
4736
|
}
|
|
4669
4737
|
catch (error) {
|
|
4670
4738
|
this.log(`Error getting orders: ${error}`, { type: "error" });
|
|
@@ -5689,12 +5757,49 @@ class AlpacaTradingAPI {
|
|
|
5689
5757
|
|
|
5690
5758
|
/**
|
|
5691
5759
|
* Resolves AlpacaAuth into validated API credentials.
|
|
5692
|
-
*
|
|
5760
|
+
*
|
|
5761
|
+
* Credential precedence (broker connectivity must never depend on the CRUD
|
|
5762
|
+
* backend when the caller already holds valid broker credentials):
|
|
5763
|
+
*
|
|
5764
|
+
* 1. **Inline credentials** — when BOTH `alpacaApiKey` and `alpacaApiSecret`
|
|
5765
|
+
* are present and non-empty AND the account `type` is known (either
|
|
5766
|
+
* `auth.type` is set, or there is no `adapticAccountId` to resolve it
|
|
5767
|
+
* from, in which case `type` defaults to `"PAPER"`), the inline values
|
|
5768
|
+
* are used directly with NO backend round trip. This keeps broker
|
|
5769
|
+
* exits/cancels possible when backend-legacy is degraded and removes
|
|
5770
|
+
* the per-call GraphQL credential refetch from the exit path.
|
|
5771
|
+
* 2. **adapticAccountId lookup** — used only when inline credentials are
|
|
5772
|
+
* absent/empty, or when inline credentials lack an explicit `type` and
|
|
5773
|
+
* an `adapticAccountId` is available to resolve the authoritative
|
|
5774
|
+
* PAPER/LIVE type (a wrong type would route requests to the wrong
|
|
5775
|
+
* Alpaca host). The lookup is a no-cache GraphQL round trip to
|
|
5776
|
+
* backend-legacy via `adaptic.alpacaAccount.get`.
|
|
5777
|
+
*
|
|
5693
5778
|
* @param auth - The authentication details for Alpaca
|
|
5694
5779
|
* @returns Validated authentication credentials
|
|
5695
5780
|
* @throws Error if authentication details are missing or invalid
|
|
5696
5781
|
*/
|
|
5697
5782
|
async function validateAuth(auth) {
|
|
5783
|
+
const inlineKey = auth.alpacaApiKey && auth.alpacaApiKey.trim().length > 0
|
|
5784
|
+
? auth.alpacaApiKey
|
|
5785
|
+
: undefined;
|
|
5786
|
+
const inlineSecret = auth.alpacaApiSecret && auth.alpacaApiSecret.trim().length > 0
|
|
5787
|
+
? auth.alpacaApiSecret
|
|
5788
|
+
: undefined;
|
|
5789
|
+
// Prefer inline credentials whenever the account type is unambiguous:
|
|
5790
|
+
// either the caller supplied it, or there is no adapticAccountId to
|
|
5791
|
+
// resolve the authoritative type from anyway.
|
|
5792
|
+
if (inlineKey && inlineSecret && (auth.type || !auth.adapticAccountId)) {
|
|
5793
|
+
const accountType = auth.type || "PAPER";
|
|
5794
|
+
validateAlpacaCredentials({
|
|
5795
|
+
apiKey: inlineKey,
|
|
5796
|
+
apiSecret: inlineSecret});
|
|
5797
|
+
return {
|
|
5798
|
+
APIKey: inlineKey,
|
|
5799
|
+
APISecret: inlineSecret,
|
|
5800
|
+
type: accountType,
|
|
5801
|
+
};
|
|
5802
|
+
}
|
|
5698
5803
|
if (auth.adapticAccountId) {
|
|
5699
5804
|
const client = await getSharedApolloClient();
|
|
5700
5805
|
const alpacaAccount = (await adaptic$1.alpacaAccount.get({
|
|
@@ -5714,17 +5819,6 @@ async function validateAuth(auth) {
|
|
|
5714
5819
|
type: alpacaAccount.type,
|
|
5715
5820
|
};
|
|
5716
5821
|
}
|
|
5717
|
-
else if (auth.alpacaApiKey && auth.alpacaApiSecret) {
|
|
5718
|
-
const accountType = auth.type || "PAPER";
|
|
5719
|
-
validateAlpacaCredentials({
|
|
5720
|
-
apiKey: auth.alpacaApiKey,
|
|
5721
|
-
apiSecret: auth.alpacaApiSecret});
|
|
5722
|
-
return {
|
|
5723
|
-
APIKey: auth.alpacaApiKey,
|
|
5724
|
-
APISecret: auth.alpacaApiSecret,
|
|
5725
|
-
type: accountType,
|
|
5726
|
-
};
|
|
5727
|
-
}
|
|
5728
5822
|
throw new Error("Either adapticAccountId or both alpacaApiKey and alpacaApiSecret must be provided");
|
|
5729
5823
|
}
|
|
5730
5824
|
|
|
@@ -6998,11 +7092,33 @@ async function closePosition$1(auth, symbolOrAssetId, params) {
|
|
|
6998
7092
|
status: "open",
|
|
6999
7093
|
symbols: [normalizedSymbol],
|
|
7000
7094
|
});
|
|
7001
|
-
//
|
|
7002
|
-
//
|
|
7003
|
-
//
|
|
7004
|
-
//
|
|
7005
|
-
|
|
7095
|
+
// pending_cancel / pending_replace handling needs an age split
|
|
7096
|
+
// (2026-06-10 live evidence, both directions):
|
|
7097
|
+
// - FRESH pending (entered the state < 30s ago): the broker is
|
|
7098
|
+
// mid-terminalisation and STILL HOLDS the order's quantity.
|
|
7099
|
+
// Treating it as done made the close fire early and bounce
|
|
7100
|
+
// with 403 40310000 "insufficient qty available" (META/WFC
|
|
7101
|
+
// closes at 17:07Z), so it must stay in `remainingOrders`
|
|
7102
|
+
// and keep the verification loop waiting.
|
|
7103
|
+
// - STALE pending (>= 30s): a wedged zombie (observed 8 days
|
|
7104
|
+
// on one order) that will never terminalise — waiting spins
|
|
7105
|
+
// the loop to exhaustion on every close attempt, so it is
|
|
7106
|
+
// excluded and the close proceeds; the broker arbitrates the
|
|
7107
|
+
// held quantity.
|
|
7108
|
+
const WEDGED_PENDING_AGE_MS = 30_000;
|
|
7109
|
+
const remainingOrders = allRemainingOrders.filter((o) => {
|
|
7110
|
+
if (o.status !== "pending_cancel" &&
|
|
7111
|
+
o.status !== "pending_replace") {
|
|
7112
|
+
return true;
|
|
7113
|
+
}
|
|
7114
|
+
const updatedAtMs = Date.parse(o.updated_at ?? "");
|
|
7115
|
+
if (!Number.isFinite(updatedAtMs)) {
|
|
7116
|
+
// No usable timestamp — conservatively treat as fresh so the
|
|
7117
|
+
// loop waits rather than racing the broker's qty release.
|
|
7118
|
+
return true;
|
|
7119
|
+
}
|
|
7120
|
+
return Date.now() - updatedAtMs < WEDGED_PENDING_AGE_MS;
|
|
7121
|
+
});
|
|
7006
7122
|
if (remainingOrders.length === 0) {
|
|
7007
7123
|
getLogger().info(`Cancel verification passed for ${normalizedSymbol} (attempt ${attempt}/${maxVerifyAttempts})`, {
|
|
7008
7124
|
account: auth.adapticAccountId || "direct",
|
|
@@ -7107,6 +7223,10 @@ async function closePosition$1(auth, symbolOrAssetId, params) {
|
|
|
7107
7223
|
"APCA-API-KEY-ID": APIKey,
|
|
7108
7224
|
"APCA-API-SECRET-KEY": APISecret,
|
|
7109
7225
|
},
|
|
7226
|
+
// The close submission must never hang indefinitely (hung sockets on
|
|
7227
|
+
// NAT idle-reap stall the exit in exactly the fast-tape scenario
|
|
7228
|
+
// where it matters) — bound it like every other call in this file.
|
|
7229
|
+
signal: createTimeoutSignal(DEFAULT_TIMEOUTS.ALPACA_API),
|
|
7110
7230
|
});
|
|
7111
7231
|
if (!response.ok) {
|
|
7112
7232
|
const errorText = await response.text();
|
|
@@ -7181,6 +7301,7 @@ async function closeAllPositions$1(auth, params = { cancel_orders: true, useLimi
|
|
|
7181
7301
|
"APCA-API-KEY-ID": APIKey,
|
|
7182
7302
|
"APCA-API-SECRET-KEY": APISecret,
|
|
7183
7303
|
},
|
|
7304
|
+
signal: createTimeoutSignal(DEFAULT_TIMEOUTS.ALPACA_API),
|
|
7184
7305
|
});
|
|
7185
7306
|
if (response.ok) {
|
|
7186
7307
|
getLogger().info(`Closed crypto position ${position.symbol} via market order`, {
|
|
@@ -7314,6 +7435,7 @@ async function closeAllPositionsAfterHours$1(auth, params = { cancel_orders: tru
|
|
|
7314
7435
|
"APCA-API-KEY-ID": APIKey,
|
|
7315
7436
|
"APCA-API-SECRET-KEY": APISecret,
|
|
7316
7437
|
},
|
|
7438
|
+
signal: createTimeoutSignal(DEFAULT_TIMEOUTS.ALPACA_API),
|
|
7317
7439
|
});
|
|
7318
7440
|
if (response.ok) {
|
|
7319
7441
|
getLogger().info(`Closed crypto position ${position.symbol} via market order`, {
|