@adaptic/utils 0.0.991 → 0.0.993
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/README.md +15 -3
- package/dist/index.cjs +85 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +85 -6
- package/dist/index.mjs.map +1 -1
- package/dist/test.js +85 -220
- package/dist/test.js.map +1 -1
- package/dist/types/alpaca-market-data-api.d.ts +42 -0
- package/dist/types/alpaca-market-data-api.d.ts.map +1 -1
- package/dist/types/technical-analysis.d.ts.map +1 -1
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/types/index.d.ts.map +1 -1
- package/package.json +1 -2
package/dist/test.js
CHANGED
|
@@ -1900,6 +1900,48 @@ class AlpacaMarketDataAPI extends EventEmitter {
|
|
|
1900
1900
|
};
|
|
1901
1901
|
reconnectAttempts = {};
|
|
1902
1902
|
reconnectTimers = {};
|
|
1903
|
+
/**
|
|
1904
|
+
* Wall-clock timestamp of the most recent Alpaca app-level error code
|
|
1905
|
+
* 406 ("connection limit exceeded") received on each stream. Used by
|
|
1906
|
+
* {@link scheduleReconnect} to apply a long backoff with jitter rather
|
|
1907
|
+
* than the normal sub-second exponential ramp — without this, hitting
|
|
1908
|
+
* Alpaca's account-wide concurrent-connection cap (typical for blue/green
|
|
1909
|
+
* deploy rollovers where the old pod's WS slots haven't been released
|
|
1910
|
+
* yet) produced a 10-attempt retry storm that compounded the slot
|
|
1911
|
+
* pressure and consumed the per-account connection quota across the
|
|
1912
|
+
* organisation.
|
|
1913
|
+
*
|
|
1914
|
+
* Cleared once the long-backoff retry is scheduled so that subsequent
|
|
1915
|
+
* normal failures fall back to the standard sub-second exponential.
|
|
1916
|
+
*
|
|
1917
|
+
* @see CONNECTION_LIMIT_BACKOFF_MS / CONNECTION_LIMIT_BACKOFF_JITTER_MS
|
|
1918
|
+
*/
|
|
1919
|
+
lastConnectionLimitAt = {};
|
|
1920
|
+
/**
|
|
1921
|
+
* Five-minute base backoff after Alpaca's app-level 406. Long enough
|
|
1922
|
+
* for Alpaca's server-side cleanup to release stale slots in typical
|
|
1923
|
+
* rollover scenarios; short enough that an operator doesn't need to
|
|
1924
|
+
* intervene. Mirrors the equivalent MassiveClient
|
|
1925
|
+
* `MAX_CONNECTIONS_RETRY_DELAY_MS` (engine v1.0.59) so both providers
|
|
1926
|
+
* behave identically under the same failure mode.
|
|
1927
|
+
*/
|
|
1928
|
+
CONNECTION_LIMIT_BACKOFF_MS = 5 * 60_000;
|
|
1929
|
+
/**
|
|
1930
|
+
* ±30 s of uniform jitter on the connection-limit backoff. Prevents
|
|
1931
|
+
* a thundering-herd retry when all three streams (stock / option /
|
|
1932
|
+
* crypto) hit 406 simultaneously during a deploy rollover — without
|
|
1933
|
+
* jitter they'd all retry at the same wall-clock instant and could
|
|
1934
|
+
* re-trip the account cap together.
|
|
1935
|
+
*/
|
|
1936
|
+
CONNECTION_LIMIT_BACKOFF_JITTER_MS = 30_000;
|
|
1937
|
+
/**
|
|
1938
|
+
* Recency window within which a 406 is considered "still applicable"
|
|
1939
|
+
* to a subsequent reconnect-schedule call. The 406 message handler
|
|
1940
|
+
* stamps {@link lastConnectionLimitAt} and the `close` handler fires
|
|
1941
|
+
* shortly afterwards (sub-second typically) — the window is wide
|
|
1942
|
+
* enough to absorb scheduling delays without false-positives.
|
|
1943
|
+
*/
|
|
1944
|
+
CONNECTION_LIMIT_RECENCY_MS = 30_000;
|
|
1903
1945
|
setMode(mode = "production") {
|
|
1904
1946
|
if (mode === "sandbox") {
|
|
1905
1947
|
// sandbox mode
|
|
@@ -2031,6 +2073,18 @@ class AlpacaMarketDataAPI extends EventEmitter {
|
|
|
2031
2073
|
}
|
|
2032
2074
|
else if (message.T === "error") {
|
|
2033
2075
|
log$1(`${streamType} stream error: ${message.msg} (code: ${message.code}, raw: ${JSON.stringify(message)})`, { type: "error" });
|
|
2076
|
+
// Alpaca code 406: "connection limit exceeded" — account-wide
|
|
2077
|
+
// concurrent-WS cap reached. The Alpaca server will close the
|
|
2078
|
+
// socket immediately after this frame, which would normally
|
|
2079
|
+
// trigger our standard sub-second exponential reconnect chain
|
|
2080
|
+
// (1 s, 2 s, 4 s, 8 s, 16 s, 30 s × 5) — exactly the wrong
|
|
2081
|
+
// behaviour against a rate-limit response. Stamp the recency
|
|
2082
|
+
// marker so {@link scheduleReconnect} switches to the
|
|
2083
|
+
// 5-minute jittered backoff instead.
|
|
2084
|
+
if (typeof message.code === "number" &&
|
|
2085
|
+
message.code === 406) {
|
|
2086
|
+
this.lastConnectionLimitAt[streamType] = Date.now();
|
|
2087
|
+
}
|
|
2034
2088
|
}
|
|
2035
2089
|
else if (message.S) {
|
|
2036
2090
|
super.emit(`${streamType}-${message.T}`, message);
|
|
@@ -2064,6 +2118,37 @@ class AlpacaMarketDataAPI extends EventEmitter {
|
|
|
2064
2118
|
});
|
|
2065
2119
|
}
|
|
2066
2120
|
scheduleReconnect(streamType) {
|
|
2121
|
+
// 406-recovery fast path. When the most recent close was preceded
|
|
2122
|
+
// by an Alpaca app-level 406 ("connection limit exceeded"), the
|
|
2123
|
+
// standard sub-second exponential ramp is exactly wrong — it
|
|
2124
|
+
// hammers the rate-limit endpoint and prolongs the slot pressure.
|
|
2125
|
+
// Use a 5-minute jittered backoff instead and reset the normal
|
|
2126
|
+
// attempt counter so we don't fall off the end of maxAttempts
|
|
2127
|
+
// and permanently give up on a transient rollover blip.
|
|
2128
|
+
const connectionLimitAt = this.lastConnectionLimitAt[streamType];
|
|
2129
|
+
const isRecentConnectionLimit = typeof connectionLimitAt === "number" &&
|
|
2130
|
+
Date.now() - connectionLimitAt <= this.CONNECTION_LIMIT_RECENCY_MS;
|
|
2131
|
+
if (isRecentConnectionLimit) {
|
|
2132
|
+
const jitter = Math.floor((Math.random() - 0.5) *
|
|
2133
|
+
2 *
|
|
2134
|
+
this.CONNECTION_LIMIT_BACKOFF_JITTER_MS);
|
|
2135
|
+
const delayMs = this.CONNECTION_LIMIT_BACKOFF_MS + jitter;
|
|
2136
|
+
// Reset normal attempt counter so the next 406 retry doesn't
|
|
2137
|
+
// inherit a stale exponential cap.
|
|
2138
|
+
this.reconnectAttempts[streamType] = 0;
|
|
2139
|
+
// Consume the recency marker — subsequent reconnects fall back
|
|
2140
|
+
// to the standard exponential path unless a new 406 arrives.
|
|
2141
|
+
delete this.lastConnectionLimitAt[streamType];
|
|
2142
|
+
log$1(`${streamType} stream: Alpaca 406 connection-limit recovery — backing off ${Math.round(delayMs / 1000)}s before retry to allow account-wide slot release`, { type: "warn" });
|
|
2143
|
+
if (this.reconnectTimers[streamType]) {
|
|
2144
|
+
clearTimeout(this.reconnectTimers[streamType]);
|
|
2145
|
+
}
|
|
2146
|
+
this.reconnectTimers[streamType] = setTimeout(() => {
|
|
2147
|
+
log$1(`${streamType} stream: attempting reconnect after 406-recovery backoff`, { type: "info" });
|
|
2148
|
+
this.connect(streamType);
|
|
2149
|
+
}, delayMs);
|
|
2150
|
+
return;
|
|
2151
|
+
}
|
|
2067
2152
|
const attempts = this.reconnectAttempts[streamType] ?? 0;
|
|
2068
2153
|
const maxAttempts = 10;
|
|
2069
2154
|
if (attempts >= maxAttempts) {
|
|
@@ -7914,226 +7999,6 @@ function testScalpingDefaultsLockedIn() {
|
|
|
7914
7999
|
log(`Scalping defaults locked in (${expectations.length} fields verified). Full policy:`);
|
|
7915
8000
|
console.log(JSON.stringify(policy, null, 2));
|
|
7916
8001
|
}
|
|
7917
|
-
// async function testCreateEquitiesTrade() {
|
|
7918
|
-
// try {
|
|
7919
|
-
// log('Starting createEquitiesTrade test...');
|
|
7920
|
-
// // Create credentials using environment variables
|
|
7921
|
-
// const credentials: AlpacaCredentials = {
|
|
7922
|
-
// accountName: 'test',
|
|
7923
|
-
// apiKey: process.env.ALPACA_API_KEY || '',
|
|
7924
|
-
// apiSecret: process.env.ALPACA_SECRET_KEY || '',
|
|
7925
|
-
// type: 'PAPER',
|
|
7926
|
-
// orderType: 'market',
|
|
7927
|
-
// engine: 'quant'
|
|
7928
|
-
// };
|
|
7929
|
-
// if (!credentials.apiKey || !credentials.apiSecret) {
|
|
7930
|
-
// throw new Error('ALPACA_API_KEY and ALPACA_SECRET_KEY environment variables must be set');
|
|
7931
|
-
// }
|
|
7932
|
-
// // Create a new instance of the trading API
|
|
7933
|
-
// const alpacaAPI = createAlpacaTradingAPI(credentials);
|
|
7934
|
-
// // Get current account details
|
|
7935
|
-
// const accountDetails = await alpacaAPI.getAccountDetails();
|
|
7936
|
-
// log(`Account equity: $${parseFloat(accountDetails.equity).toFixed(2)}`);
|
|
7937
|
-
// log(`Buying power: $${parseFloat(accountDetails.buying_power).toFixed(2)}`);
|
|
7938
|
-
// const testSymbols = ['AAPL', 'MSFT', 'GOOGL', 'TSLA', 'NVDA', 'META', 'AMZN']; // Multiple symbols to avoid wash trade detection
|
|
7939
|
-
// const referencePrice = 150.00; // Mock reference price for testing
|
|
7940
|
-
// log('=== Testing createEquitiesTrade function ===');
|
|
7941
|
-
// // Test 1: Simple market order (long position)
|
|
7942
|
-
// log('\n1. Testing simple market order (long)...');
|
|
7943
|
-
// try {
|
|
7944
|
-
// const order1 = await alpacaAPI.createEquitiesTrade(
|
|
7945
|
-
// { symbol: testSymbols[0], qty: 1, side: 'buy' }
|
|
7946
|
-
// );
|
|
7947
|
-
// log(`✅ Created simple market buy order: ${order1.id}`);
|
|
7948
|
-
// } catch (error) {
|
|
7949
|
-
// log(`❌ Failed to create simple market order: ${error}`);
|
|
7950
|
-
// }
|
|
7951
|
-
// // Test 2: Simple market order (short position)
|
|
7952
|
-
// log('\n2. Testing simple market order (short)...');
|
|
7953
|
-
// try {
|
|
7954
|
-
// const order2 = await alpacaAPI.createEquitiesTrade(
|
|
7955
|
-
// { symbol: testSymbols[1], qty: 1, side: 'sell' }
|
|
7956
|
-
// );
|
|
7957
|
-
// log(`✅ Created simple market sell order: ${order2.id}`);
|
|
7958
|
-
// } catch (error) {
|
|
7959
|
-
// log(`❌ Failed to create simple market sell order: ${error}`);
|
|
7960
|
-
// }
|
|
7961
|
-
// // Test 3: Limit order with percentage-based stop loss (long)
|
|
7962
|
-
// log('\n3. Testing limit order with percentage-based stop loss (long)...');
|
|
7963
|
-
// try {
|
|
7964
|
-
// const order3 = await alpacaAPI.createEquitiesTrade(
|
|
7965
|
-
// { symbol: testSymbols[2], qty: 1, side: 'buy', referencePrice },
|
|
7966
|
-
// {
|
|
7967
|
-
// type: 'limit',
|
|
7968
|
-
// limitPrice: 149.50,
|
|
7969
|
-
// useStopLoss: true,
|
|
7970
|
-
// stopPercent100: 3.0 // 3% stop loss
|
|
7971
|
-
// }
|
|
7972
|
-
// );
|
|
7973
|
-
// log(`✅ Created limit buy order with stop loss: ${order3.id}`);
|
|
7974
|
-
// } catch (error) {
|
|
7975
|
-
// log(`❌ Failed to create limit order with stop loss: ${error}`);
|
|
7976
|
-
// }
|
|
7977
|
-
// // Test 4: Limit order with percentage-based take profit (short)
|
|
7978
|
-
// log('\n4. Testing limit order with percentage-based take profit (short)...');
|
|
7979
|
-
// try {
|
|
7980
|
-
// const order4 = await alpacaAPI.createEquitiesTrade(
|
|
7981
|
-
// { symbol: testSymbols[3], qty: 1, side: 'sell', referencePrice },
|
|
7982
|
-
// {
|
|
7983
|
-
// type: 'limit',
|
|
7984
|
-
// limitPrice: 150.50,
|
|
7985
|
-
// useTakeProfit: true,
|
|
7986
|
-
// takeProfitPercent100: 2.5 // 2.5% take profit
|
|
7987
|
-
// }
|
|
7988
|
-
// );
|
|
7989
|
-
// log(`✅ Created limit sell order with take profit: ${order4.id}`);
|
|
7990
|
-
// } catch (error) {
|
|
7991
|
-
// log(`❌ Failed to create limit order with take profit: ${error}`);
|
|
7992
|
-
// }
|
|
7993
|
-
// // Test 5: Bracket order with both stop loss and take profit (long)
|
|
7994
|
-
// log('\n5. Testing bracket order with both stop loss and take profit (long)...');
|
|
7995
|
-
// try {
|
|
7996
|
-
// const order5 = await alpacaAPI.createEquitiesTrade(
|
|
7997
|
-
// { symbol: testSymbols[4], qty: 1, side: 'buy', referencePrice },
|
|
7998
|
-
// {
|
|
7999
|
-
// type: 'limit',
|
|
8000
|
-
// limitPrice: 149.00,
|
|
8001
|
-
// useStopLoss: true,
|
|
8002
|
-
// stopPercent100: 4.0, // 4% stop loss
|
|
8003
|
-
// useTakeProfit: true,
|
|
8004
|
-
// takeProfitPercent100: 6.0 // 6% take profit
|
|
8005
|
-
// }
|
|
8006
|
-
// );
|
|
8007
|
-
// log(`✅ Created bracket order (long): ${order5.id}`);
|
|
8008
|
-
// } catch (error) {
|
|
8009
|
-
// log(`❌ Failed to create bracket order: ${error}`);
|
|
8010
|
-
// }
|
|
8011
|
-
// // Test 6: Bracket order with fixed prices (short)
|
|
8012
|
-
// log('\n6. Testing bracket order with fixed prices (short)...');
|
|
8013
|
-
// try {
|
|
8014
|
-
// const order6 = await alpacaAPI.createEquitiesTrade(
|
|
8015
|
-
// { symbol: testSymbols[5], qty: 1, side: 'sell' },
|
|
8016
|
-
// {
|
|
8017
|
-
// type: 'limit',
|
|
8018
|
-
// limitPrice: 151.00,
|
|
8019
|
-
// useStopLoss: true,
|
|
8020
|
-
// stopPrice: 155.00, // Fixed stop price
|
|
8021
|
-
// useTakeProfit: true,
|
|
8022
|
-
// takeProfitPrice: 145.00 // Fixed take profit price
|
|
8023
|
-
// }
|
|
8024
|
-
// );
|
|
8025
|
-
// log(`✅ Created bracket order with fixed prices (short): ${order6.id}`);
|
|
8026
|
-
// } catch (error) {
|
|
8027
|
-
// log(`❌ Failed to create bracket order with fixed prices: ${error}`);
|
|
8028
|
-
// }
|
|
8029
|
-
// // Test 7: Extended hours limit order
|
|
8030
|
-
// log('\n7. Testing extended hours limit order...');
|
|
8031
|
-
// try {
|
|
8032
|
-
// const order7 = await alpacaAPI.createEquitiesTrade(
|
|
8033
|
-
// { symbol: testSymbols[6], qty: 1, side: 'buy' },
|
|
8034
|
-
// {
|
|
8035
|
-
// type: 'limit',
|
|
8036
|
-
// limitPrice: 148.00,
|
|
8037
|
-
// extendedHours: true
|
|
8038
|
-
// }
|
|
8039
|
-
// );
|
|
8040
|
-
// log(`✅ Created extended hours limit order: ${order7.id}`);
|
|
8041
|
-
// } catch (error) {
|
|
8042
|
-
// log(`❌ Failed to create extended hours order: ${error}`);
|
|
8043
|
-
// }
|
|
8044
|
-
// // Test 8: Error condition - market order with extended hours (should fail)
|
|
8045
|
-
// log('\n8. Testing error condition - market order with extended hours (should fail)...');
|
|
8046
|
-
// try {
|
|
8047
|
-
// await alpacaAPI.createEquitiesTrade(
|
|
8048
|
-
// { symbol: testSymbols[0], qty: 1, side: 'buy' },
|
|
8049
|
-
// {
|
|
8050
|
-
// type: 'market',
|
|
8051
|
-
// extendedHours: true // This should trigger an error
|
|
8052
|
-
// }
|
|
8053
|
-
// );
|
|
8054
|
-
// log(`❌ Unexpected success - should have failed!`);
|
|
8055
|
-
// } catch (error) {
|
|
8056
|
-
// log(`✅ Correctly caught error: ${error instanceof Error ? error.message : error}`);
|
|
8057
|
-
// }
|
|
8058
|
-
// // Test 9: Error condition - missing referencePrice for percentage (should fail)
|
|
8059
|
-
// log('\n9. Testing error condition - missing referencePrice for percentage (should fail)...');
|
|
8060
|
-
// try {
|
|
8061
|
-
// await alpacaAPI.createEquitiesTrade(
|
|
8062
|
-
// { symbol: testSymbols[0], qty: 1, side: 'buy' }, // No referencePrice
|
|
8063
|
-
// {
|
|
8064
|
-
// useStopLoss: true,
|
|
8065
|
-
// stopPercent100: 3.0 // Requires referencePrice
|
|
8066
|
-
// }
|
|
8067
|
-
// );
|
|
8068
|
-
// log(`❌ Unexpected success - should have failed!`);
|
|
8069
|
-
// } catch (error) {
|
|
8070
|
-
// log(`✅ Correctly caught error: ${error instanceof Error ? error.message : error}`);
|
|
8071
|
-
// }
|
|
8072
|
-
// // Wait a moment for all orders to process
|
|
8073
|
-
// await new Promise(resolve => setTimeout(resolve, 2000));
|
|
8074
|
-
// // Get all orders to see what was created
|
|
8075
|
-
// log('\n=== Checking created orders ===');
|
|
8076
|
-
// const orders = await alpacaAPI.getOrders({ status: 'open', limit: 20 });
|
|
8077
|
-
// log(`Found ${orders.length} open orders`);
|
|
8078
|
-
// orders.forEach((order, index) => {
|
|
8079
|
-
// log(`Order ${index + 1}: ${order.order_class} ${order.type} ${order.side} ${order.qty} ${order.symbol} @ $${order.limit_price || 'market'}`);
|
|
8080
|
-
// if (order.legs && order.legs.length > 0) {
|
|
8081
|
-
// log(` Stop Loss: $${order.legs.find(leg => leg.type === 'stop')?.stop_price || 'N/A'}`);
|
|
8082
|
-
// log(` Take Profit: $${order.legs.find(leg => leg.type === 'limit')?.limit_price || 'N/A'}`);
|
|
8083
|
-
// }
|
|
8084
|
-
// });
|
|
8085
|
-
// // Cancel all orders to clean up
|
|
8086
|
-
// log('\n=== Cleaning up - canceling all orders ===');
|
|
8087
|
-
// await alpacaAPI.cancelAllOrders();
|
|
8088
|
-
// log('✅ All orders canceled');
|
|
8089
|
-
// log('\n=== Test completed successfully! ===');
|
|
8090
|
-
// } catch (error) {
|
|
8091
|
-
// log(`❌ Error in createEquitiesTrade test: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
8092
|
-
// process.exit(1);
|
|
8093
|
-
// }
|
|
8094
|
-
// }
|
|
8095
|
-
// async function testAlpacaWebSocket() {
|
|
8096
|
-
// try {
|
|
8097
|
-
// log('Starting Alpaca WebSocket test...');
|
|
8098
|
-
// // Create credentials using environment variables
|
|
8099
|
-
// const credentials: AlpacaCredentials = {
|
|
8100
|
-
// accountName: 'test',
|
|
8101
|
-
// apiKey: process.env.ALPACA_API_KEY || '',
|
|
8102
|
-
// apiSecret: process.env.ALPACA_SECRET_KEY || '',
|
|
8103
|
-
// type: 'PAPER',
|
|
8104
|
-
// orderType: 'market',
|
|
8105
|
-
// engine: 'quant'
|
|
8106
|
-
// };
|
|
8107
|
-
// if (!credentials.apiKey || !credentials.apiSecret) {
|
|
8108
|
-
// throw new Error('ALPACA_API_KEY and ALPACA_SECRET_KEY environment variables must be set');
|
|
8109
|
-
// }
|
|
8110
|
-
// // Create a new instance of the trading API
|
|
8111
|
-
// const alpacaAPI = createAlpacaTradingAPI(credentials); // type AlpacaCredentials
|
|
8112
|
-
// // Set up trade update callback
|
|
8113
|
-
// alpacaAPI.onTradeUpdate((update: TradeUpdate) => {
|
|
8114
|
-
// log(`Received trade update: event ${update.event} for an order to ${update.order.side} ${update.order.qty} of ${update.order.symbol}`);
|
|
8115
|
-
// });
|
|
8116
|
-
// // Connect to WebSocket
|
|
8117
|
-
// alpacaAPI.connectWebsocket(); // necessary to connect to the WebSocket
|
|
8118
|
-
// // create an order
|
|
8119
|
-
// const order = await alpacaAPI.createMarketOrder('AAPL', 1, 'buy', 'buy_to_open');
|
|
8120
|
-
// // cancel the order
|
|
8121
|
-
// await alpacaAPI.cancelAllOrders();
|
|
8122
|
-
// // Keep the process running
|
|
8123
|
-
// log('WebSocket connected and listening for trade updates...');
|
|
8124
|
-
// log('Press Ctrl+C to exit');
|
|
8125
|
-
// // Keep the process running
|
|
8126
|
-
// await new Promise(() => {});
|
|
8127
|
-
// } catch (error) {
|
|
8128
|
-
// log(`Error in WebSocket test: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
8129
|
-
// process.exit(1);
|
|
8130
|
-
// }
|
|
8131
|
-
// }
|
|
8132
|
-
// // Run the test
|
|
8133
|
-
// testAlpacaWebSocket();
|
|
8134
|
-
// Run the createEquitiesTrade test
|
|
8135
|
-
//testCreateEquitiesTrade();
|
|
8136
|
-
// testing retrieving pre-market data (just 9:00am to 9:30am on 1 july 2025 for SPY) using the market data api
|
|
8137
8002
|
async function testPreMarketData() {
|
|
8138
8003
|
try {
|
|
8139
8004
|
log("Starting pre-market data test for SPY (9:00am-9:30am, July 1, 2025)...");
|