@adaptic/utils 0.0.979 → 0.0.981
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 +478 -398
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +478 -399
- package/dist/index.mjs.map +1 -1
- package/dist/types/alpaca/legacy/orders.d.ts.map +1 -1
- package/dist/types/alpaca-trading-api.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/massive.d.ts.map +1 -1
- package/dist/types/utils/retry.d.ts +14 -0
- package/dist/types/utils/retry.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -3799,6 +3799,419 @@ class AlpacaMarketDataAPI extends EventEmitter {
|
|
|
3799
3799
|
// Export the singleton instance
|
|
3800
3800
|
const marketDataAPI = AlpacaMarketDataAPI.getInstance();
|
|
3801
3801
|
|
|
3802
|
+
const DEFAULT_RETRY_CONFIG = {
|
|
3803
|
+
maxRetries: 3,
|
|
3804
|
+
baseDelayMs: 1000,
|
|
3805
|
+
maxDelayMs: 30000,
|
|
3806
|
+
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
3807
|
+
retryOnNetworkError: true,
|
|
3808
|
+
};
|
|
3809
|
+
/**
|
|
3810
|
+
* Node.js / undici / system error codes that represent transient network
|
|
3811
|
+
* conditions. Present on `error.code` for net/http/dns/undici errors.
|
|
3812
|
+
*/
|
|
3813
|
+
const RETRYABLE_ERROR_CODES = new Set([
|
|
3814
|
+
"ETIMEDOUT",
|
|
3815
|
+
"ESOCKETTIMEDOUT",
|
|
3816
|
+
"ECONNRESET",
|
|
3817
|
+
"ECONNREFUSED",
|
|
3818
|
+
"EHOSTUNREACH",
|
|
3819
|
+
"ENETUNREACH",
|
|
3820
|
+
"EAI_AGAIN",
|
|
3821
|
+
"EPIPE",
|
|
3822
|
+
"ECONNABORTED",
|
|
3823
|
+
"ENOTFOUND",
|
|
3824
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
3825
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
3826
|
+
"UND_ERR_BODY_TIMEOUT",
|
|
3827
|
+
"UND_ERR_SOCKET",
|
|
3828
|
+
"UND_ERR_CLOSED",
|
|
3829
|
+
"UND_ERR_REQ_CONTENT_LENGTH_MISMATCH",
|
|
3830
|
+
]);
|
|
3831
|
+
/**
|
|
3832
|
+
* Error constructor names / `error.name` values that indicate transient
|
|
3833
|
+
* abort / timeout conditions.
|
|
3834
|
+
*/
|
|
3835
|
+
const RETRYABLE_ERROR_NAMES = new Set([
|
|
3836
|
+
"AbortError",
|
|
3837
|
+
"TimeoutError",
|
|
3838
|
+
"FetchError",
|
|
3839
|
+
"RequestTimeoutError",
|
|
3840
|
+
"ConnectTimeoutError",
|
|
3841
|
+
"HeadersTimeoutError",
|
|
3842
|
+
"BodyTimeoutError",
|
|
3843
|
+
]);
|
|
3844
|
+
/**
|
|
3845
|
+
* Message-pattern fallback for libraries that discard error codes/names but
|
|
3846
|
+
* preserve text (e.g., some Apollo/axios wrappers).
|
|
3847
|
+
*/
|
|
3848
|
+
const RETRYABLE_MESSAGE_PATTERNS = [
|
|
3849
|
+
/aborted/i,
|
|
3850
|
+
/timeout/i,
|
|
3851
|
+
/timed out/i,
|
|
3852
|
+
/network error/i,
|
|
3853
|
+
/socket hang up/i,
|
|
3854
|
+
/connection (reset|refused|closed)/i,
|
|
3855
|
+
/ECONNRESET/,
|
|
3856
|
+
/ETIMEDOUT/,
|
|
3857
|
+
/ECONNREFUSED/,
|
|
3858
|
+
/EAI_AGAIN/,
|
|
3859
|
+
/UND_ERR_/,
|
|
3860
|
+
];
|
|
3861
|
+
/**
|
|
3862
|
+
* Walks the `error.cause` chain (capped to avoid cycles) and tests whether
|
|
3863
|
+
* any link along the chain looks like a transient network error. Modern APIs
|
|
3864
|
+
* (undici, fetch, Apollo Client 3.8+) wrap the root network failure as a
|
|
3865
|
+
* `.cause`, so the surface `Error` may report a generic message while the
|
|
3866
|
+
* actionable signal lives one or more levels deeper.
|
|
3867
|
+
*
|
|
3868
|
+
* Exported for use by downstream consumers (engine services, per-call catch
|
|
3869
|
+
* blocks, application-level loggers) that need to demote recoverable
|
|
3870
|
+
* transient errors from ERROR to WARN. Aligns the whole stack on a single
|
|
3871
|
+
* canonical classifier so MassiveAPI, AlpacaAPI, and application-layer
|
|
3872
|
+
* retry handlers all treat the same network blips identically.
|
|
3873
|
+
*/
|
|
3874
|
+
function isTransientNetworkError(error) {
|
|
3875
|
+
const MAX_CAUSE_DEPTH = 6;
|
|
3876
|
+
let current = error;
|
|
3877
|
+
for (let depth = 0; depth < MAX_CAUSE_DEPTH && current; depth++) {
|
|
3878
|
+
if (current instanceof Error || typeof current === "object") {
|
|
3879
|
+
const err = current;
|
|
3880
|
+
if (typeof err.name === "string" && RETRYABLE_ERROR_NAMES.has(err.name)) {
|
|
3881
|
+
return true;
|
|
3882
|
+
}
|
|
3883
|
+
if (typeof err.code === "string" && RETRYABLE_ERROR_CODES.has(err.code)) {
|
|
3884
|
+
return true;
|
|
3885
|
+
}
|
|
3886
|
+
if (typeof err.message === "string") {
|
|
3887
|
+
for (const pattern of RETRYABLE_MESSAGE_PATTERNS) {
|
|
3888
|
+
if (pattern.test(err.message)) {
|
|
3889
|
+
return true;
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3892
|
+
}
|
|
3893
|
+
current = err.cause;
|
|
3894
|
+
}
|
|
3895
|
+
else {
|
|
3896
|
+
break;
|
|
3897
|
+
}
|
|
3898
|
+
}
|
|
3899
|
+
return false;
|
|
3900
|
+
}
|
|
3901
|
+
/**
|
|
3902
|
+
* Analyzes an error and determines if it's retryable.
|
|
3903
|
+
* @param error - The error to analyze
|
|
3904
|
+
* @param response - Optional Response object for HTTP errors
|
|
3905
|
+
* @param config - Retry configuration
|
|
3906
|
+
* @returns Structured error details
|
|
3907
|
+
*/
|
|
3908
|
+
function analyzeError(error, response, config) {
|
|
3909
|
+
// Handle Response objects with error status codes
|
|
3910
|
+
if (response && !response.ok) {
|
|
3911
|
+
const status = response.status;
|
|
3912
|
+
// Rate limit errors - always retryable
|
|
3913
|
+
if (status === 429) {
|
|
3914
|
+
const retryAfterHeader = response.headers.get("Retry-After");
|
|
3915
|
+
const retryAfter = retryAfterHeader
|
|
3916
|
+
? parseInt(retryAfterHeader, 10) * 1000
|
|
3917
|
+
: undefined;
|
|
3918
|
+
return {
|
|
3919
|
+
type: "RATE_LIMIT",
|
|
3920
|
+
reason: "Rate limit exceeded",
|
|
3921
|
+
status,
|
|
3922
|
+
retryAfter,
|
|
3923
|
+
isRetryable: true,
|
|
3924
|
+
};
|
|
3925
|
+
}
|
|
3926
|
+
// Authentication errors - never retry
|
|
3927
|
+
if (status === 401 || status === 403) {
|
|
3928
|
+
return {
|
|
3929
|
+
type: "AUTH_ERROR",
|
|
3930
|
+
reason: status === 401
|
|
3931
|
+
? "Authentication failed - invalid credentials"
|
|
3932
|
+
: "Access forbidden - insufficient permissions",
|
|
3933
|
+
status,
|
|
3934
|
+
isRetryable: false,
|
|
3935
|
+
};
|
|
3936
|
+
}
|
|
3937
|
+
// Server errors - check if in retryable list
|
|
3938
|
+
if (status >= 500 && status < 600) {
|
|
3939
|
+
return {
|
|
3940
|
+
type: "SERVER_ERROR",
|
|
3941
|
+
reason: `Server error (${status})`,
|
|
3942
|
+
status,
|
|
3943
|
+
isRetryable: config.retryableStatusCodes.includes(status),
|
|
3944
|
+
};
|
|
3945
|
+
}
|
|
3946
|
+
// Other client errors - never retry
|
|
3947
|
+
if (status >= 400 && status < 500) {
|
|
3948
|
+
return {
|
|
3949
|
+
type: "CLIENT_ERROR",
|
|
3950
|
+
reason: `Client error (${status})`,
|
|
3951
|
+
status,
|
|
3952
|
+
isRetryable: false,
|
|
3953
|
+
};
|
|
3954
|
+
}
|
|
3955
|
+
}
|
|
3956
|
+
// Handle network errors (TypeError from fetch API)
|
|
3957
|
+
if (error instanceof TypeError && error.message.includes("fetch")) {
|
|
3958
|
+
return {
|
|
3959
|
+
type: "NETWORK_ERROR",
|
|
3960
|
+
reason: "Network connectivity issue",
|
|
3961
|
+
status: null,
|
|
3962
|
+
isRetryable: config.retryOnNetworkError,
|
|
3963
|
+
};
|
|
3964
|
+
}
|
|
3965
|
+
// Handle transient network conditions: AbortError, TimeoutError,
|
|
3966
|
+
// Node/undici error codes (ETIMEDOUT, ECONNRESET, UND_ERR_*), and
|
|
3967
|
+
// wrapped failures exposed via error.cause. This catches the broad class
|
|
3968
|
+
// of infrastructure flakes that the TypeError-only check above misses.
|
|
3969
|
+
if (isTransientNetworkError(error)) {
|
|
3970
|
+
const reason = error instanceof Error ? error.message : "Transient network error";
|
|
3971
|
+
return {
|
|
3972
|
+
type: "NETWORK_ERROR",
|
|
3973
|
+
reason,
|
|
3974
|
+
status: null,
|
|
3975
|
+
isRetryable: config.retryOnNetworkError,
|
|
3976
|
+
};
|
|
3977
|
+
}
|
|
3978
|
+
// Handle error objects with messages
|
|
3979
|
+
if (error instanceof Error) {
|
|
3980
|
+
// Parse error messages that might contain status information
|
|
3981
|
+
if (error.message.includes("429") || error.message.includes("RATE_LIMIT")) {
|
|
3982
|
+
const match = error.message.match(/RATE_LIMIT: 429:(\d+)/);
|
|
3983
|
+
const retryAfter = match ? parseInt(match[1], 10) : undefined;
|
|
3984
|
+
return {
|
|
3985
|
+
type: "RATE_LIMIT",
|
|
3986
|
+
reason: "Rate limit exceeded",
|
|
3987
|
+
status: 429,
|
|
3988
|
+
retryAfter,
|
|
3989
|
+
isRetryable: true,
|
|
3990
|
+
};
|
|
3991
|
+
}
|
|
3992
|
+
if (error.message.includes("401") ||
|
|
3993
|
+
error.message.includes("403") ||
|
|
3994
|
+
error.message.includes("AUTH_ERROR")) {
|
|
3995
|
+
const status = error.message.includes("401") ? 401 : 403;
|
|
3996
|
+
return {
|
|
3997
|
+
type: "AUTH_ERROR",
|
|
3998
|
+
reason: `Authentication error (${status})`,
|
|
3999
|
+
status,
|
|
4000
|
+
isRetryable: false,
|
|
4001
|
+
};
|
|
4002
|
+
}
|
|
4003
|
+
if (error.message.includes("SERVER_ERROR") ||
|
|
4004
|
+
error.message.match(/50[0-9]/)) {
|
|
4005
|
+
const statusMatch = error.message.match(/50[0-9]/);
|
|
4006
|
+
const status = statusMatch ? parseInt(statusMatch[0], 10) : 500;
|
|
4007
|
+
return {
|
|
4008
|
+
type: "SERVER_ERROR",
|
|
4009
|
+
reason: `Server error (${status})`,
|
|
4010
|
+
status,
|
|
4011
|
+
isRetryable: config.retryableStatusCodes.includes(status),
|
|
4012
|
+
};
|
|
4013
|
+
}
|
|
4014
|
+
if (error.message.includes("network") ||
|
|
4015
|
+
error.message.includes("NETWORK_ERROR")) {
|
|
4016
|
+
return {
|
|
4017
|
+
type: "NETWORK_ERROR",
|
|
4018
|
+
reason: error.message,
|
|
4019
|
+
status: null,
|
|
4020
|
+
isRetryable: config.retryOnNetworkError,
|
|
4021
|
+
};
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
4024
|
+
// Unknown error - not retryable by default for safety
|
|
4025
|
+
return {
|
|
4026
|
+
type: "UNKNOWN",
|
|
4027
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
4028
|
+
status: null,
|
|
4029
|
+
isRetryable: false,
|
|
4030
|
+
};
|
|
4031
|
+
}
|
|
4032
|
+
/**
|
|
4033
|
+
* Calculates the delay before the next retry attempt using exponential backoff with jitter.
|
|
4034
|
+
* @param attempt - Current attempt number (1-indexed)
|
|
4035
|
+
* @param baseDelay - Base delay in milliseconds
|
|
4036
|
+
* @param maxDelay - Maximum delay in milliseconds
|
|
4037
|
+
* @returns Delay in milliseconds
|
|
4038
|
+
*/
|
|
4039
|
+
function calculateBackoff(attempt, baseDelay, maxDelay) {
|
|
4040
|
+
// Exponential backoff: baseDelay * 2^(attempt-1)
|
|
4041
|
+
const exponentialDelay = baseDelay * Math.pow(2, attempt - 1);
|
|
4042
|
+
// Cap at maxDelay
|
|
4043
|
+
const cappedDelay = Math.min(exponentialDelay, maxDelay);
|
|
4044
|
+
// Add jitter (random value between 0% and 25% of the delay)
|
|
4045
|
+
const jitter = Math.random() * cappedDelay * 0.25;
|
|
4046
|
+
return Math.floor(cappedDelay + jitter);
|
|
4047
|
+
}
|
|
4048
|
+
/**
|
|
4049
|
+
* Wraps an async function with retry logic and exponential backoff.
|
|
4050
|
+
*
|
|
4051
|
+
* This utility handles transient errors in external API calls by automatically retrying
|
|
4052
|
+
* failed requests with intelligent backoff strategies. It respects rate limit headers,
|
|
4053
|
+
* fails fast on non-retryable errors, and provides detailed logging.
|
|
4054
|
+
*
|
|
4055
|
+
* @template T - The return type of the wrapped function
|
|
4056
|
+
* @param fn - The async function to wrap with retry logic
|
|
4057
|
+
* @param config - Retry configuration (merged with defaults)
|
|
4058
|
+
* @param label - A descriptive label for logging (e.g., 'Massive.fetchTickerInfo')
|
|
4059
|
+
* @returns A promise that resolves to the function's return value
|
|
4060
|
+
* @throws The last error encountered if all retries are exhausted
|
|
4061
|
+
*
|
|
4062
|
+
* @example
|
|
4063
|
+
* ```typescript
|
|
4064
|
+
* // Basic usage with defaults
|
|
4065
|
+
* const data = await withRetry(
|
|
4066
|
+
* async () => fetch('https://api.example.com/data'),
|
|
4067
|
+
* {},
|
|
4068
|
+
* 'ExampleAPI.fetchData'
|
|
4069
|
+
* );
|
|
4070
|
+
*
|
|
4071
|
+
* // Custom configuration for rate-limited API
|
|
4072
|
+
* const result = await withRetry(
|
|
4073
|
+
* async () => alphaVantageAPI.getQuote(symbol),
|
|
4074
|
+
* {
|
|
4075
|
+
* maxRetries: 5,
|
|
4076
|
+
* baseDelayMs: 5000,
|
|
4077
|
+
* maxDelayMs: 60000,
|
|
4078
|
+
* onRetry: (attempt, error) => {
|
|
4079
|
+
* getLogger().info(`Retry ${attempt} after error:`, error);
|
|
4080
|
+
* }
|
|
4081
|
+
* },
|
|
4082
|
+
* 'AlphaVantage.getQuote'
|
|
4083
|
+
* );
|
|
4084
|
+
* ```
|
|
4085
|
+
*/
|
|
4086
|
+
async function withRetry(fn, config = {}, label = "unknown") {
|
|
4087
|
+
const fullConfig = { ...DEFAULT_RETRY_CONFIG, ...config };
|
|
4088
|
+
let lastError;
|
|
4089
|
+
for (let attempt = 1; attempt <= fullConfig.maxRetries; attempt++) {
|
|
4090
|
+
try {
|
|
4091
|
+
const result = await fn();
|
|
4092
|
+
// If we succeeded after retries, log it
|
|
4093
|
+
if (attempt > 1) {
|
|
4094
|
+
getLogger().info(`[${label}] Succeeded on attempt ${attempt}/${fullConfig.maxRetries}`);
|
|
4095
|
+
}
|
|
4096
|
+
return result;
|
|
4097
|
+
}
|
|
4098
|
+
catch (error) {
|
|
4099
|
+
lastError = error;
|
|
4100
|
+
// If this is the last attempt, throw the error.
|
|
4101
|
+
// Transient network classes (undici/fetch timeouts, ECONNRESET,
|
|
4102
|
+
// AbortError, etc.) are self-healing at the upstream retry layer —
|
|
4103
|
+
// the caller re-invokes on the next refresh/poll tick. Logging them
|
|
4104
|
+
// at ERROR produces alert noise that does not represent actionable
|
|
4105
|
+
// failures. Demote the transient class to WARN with a recovery hint;
|
|
4106
|
+
// reserve ERROR for non-transient final failures (auth, schema,
|
|
4107
|
+
// contract violations, unknown classes).
|
|
4108
|
+
if (attempt === fullConfig.maxRetries) {
|
|
4109
|
+
const isTransient = isTransientNetworkError(error);
|
|
4110
|
+
const logMeta = {
|
|
4111
|
+
error: error instanceof Error ? error.message : String(error),
|
|
4112
|
+
attempts: fullConfig.maxRetries,
|
|
4113
|
+
timestamp: new Date().toISOString(),
|
|
4114
|
+
...(isTransient
|
|
4115
|
+
? {
|
|
4116
|
+
transient: true,
|
|
4117
|
+
recoveryHint: "Upstream caller should retry on next cycle",
|
|
4118
|
+
}
|
|
4119
|
+
: {}),
|
|
4120
|
+
};
|
|
4121
|
+
if (isTransient) {
|
|
4122
|
+
getLogger().warn(`[${label}] Failed after ${fullConfig.maxRetries} attempts (transient)`, logMeta);
|
|
4123
|
+
}
|
|
4124
|
+
else {
|
|
4125
|
+
getLogger().error(`[${label}] Failed after ${fullConfig.maxRetries} attempts`, logMeta);
|
|
4126
|
+
}
|
|
4127
|
+
throw error;
|
|
4128
|
+
}
|
|
4129
|
+
// Analyze the error to determine if we should retry
|
|
4130
|
+
const response = error instanceof Response ? error : null;
|
|
4131
|
+
const errorDetails = analyzeError(error, response, fullConfig);
|
|
4132
|
+
// If error is not retryable, fail immediately
|
|
4133
|
+
if (!errorDetails.isRetryable) {
|
|
4134
|
+
getLogger().error(`[${label}] Non-retryable error (${errorDetails.type})`, {
|
|
4135
|
+
reason: errorDetails.reason,
|
|
4136
|
+
status: errorDetails.status,
|
|
4137
|
+
timestamp: new Date().toISOString(),
|
|
4138
|
+
});
|
|
4139
|
+
throw error;
|
|
4140
|
+
}
|
|
4141
|
+
// Calculate delay for next retry
|
|
4142
|
+
let delayMs;
|
|
4143
|
+
if (errorDetails.type === "RATE_LIMIT" && errorDetails.retryAfter) {
|
|
4144
|
+
// Use Retry-After header if available
|
|
4145
|
+
delayMs = errorDetails.retryAfter;
|
|
4146
|
+
}
|
|
4147
|
+
else if (errorDetails.type === "RATE_LIMIT") {
|
|
4148
|
+
// For rate limits without Retry-After, use a longer minimum delay
|
|
4149
|
+
delayMs = Math.max(calculateBackoff(attempt, fullConfig.baseDelayMs, fullConfig.maxDelayMs), 5000);
|
|
4150
|
+
}
|
|
4151
|
+
else {
|
|
4152
|
+
// Standard exponential backoff with jitter
|
|
4153
|
+
delayMs = calculateBackoff(attempt, fullConfig.baseDelayMs, fullConfig.maxDelayMs);
|
|
4154
|
+
}
|
|
4155
|
+
// Log the retry attempt
|
|
4156
|
+
getLogger().warn(`[${label}] Attempt ${attempt}/${fullConfig.maxRetries} failed: ${errorDetails.reason}. Retrying in ${delayMs}ms...`, {
|
|
4157
|
+
attemptNumber: attempt,
|
|
4158
|
+
totalRetries: fullConfig.maxRetries,
|
|
4159
|
+
errorType: errorDetails.type,
|
|
4160
|
+
httpStatus: errorDetails.status,
|
|
4161
|
+
retryDelay: delayMs,
|
|
4162
|
+
timestamp: new Date().toISOString(),
|
|
4163
|
+
});
|
|
4164
|
+
// Call the optional retry callback
|
|
4165
|
+
if (fullConfig.onRetry) {
|
|
4166
|
+
fullConfig.onRetry(attempt, error);
|
|
4167
|
+
}
|
|
4168
|
+
// Wait before retrying
|
|
4169
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
4170
|
+
}
|
|
4171
|
+
}
|
|
4172
|
+
// This should never be reached due to the throw in the last attempt,
|
|
4173
|
+
// but TypeScript needs this to satisfy the return type
|
|
4174
|
+
throw lastError;
|
|
4175
|
+
}
|
|
4176
|
+
/**
|
|
4177
|
+
* API-specific retry configurations for different external services.
|
|
4178
|
+
* These configurations are tuned based on each API's rate limits and characteristics.
|
|
4179
|
+
*/
|
|
4180
|
+
const API_RETRY_CONFIGS = {
|
|
4181
|
+
/** Massive.com API - 5 requests/second rate limit */
|
|
4182
|
+
MASSIVE: {
|
|
4183
|
+
maxRetries: 3,
|
|
4184
|
+
baseDelayMs: 1000,
|
|
4185
|
+
maxDelayMs: 30000,
|
|
4186
|
+
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
4187
|
+
retryOnNetworkError: true,
|
|
4188
|
+
},
|
|
4189
|
+
/** Alpha Vantage API - 5 requests/minute rate limit (more strict) */
|
|
4190
|
+
ALPHA_VANTAGE: {
|
|
4191
|
+
maxRetries: 5,
|
|
4192
|
+
baseDelayMs: 5000,
|
|
4193
|
+
maxDelayMs: 60000,
|
|
4194
|
+
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
4195
|
+
retryOnNetworkError: true,
|
|
4196
|
+
},
|
|
4197
|
+
/** Alpaca API - generally reliable, shorter retry window */
|
|
4198
|
+
ALPACA: {
|
|
4199
|
+
maxRetries: 3,
|
|
4200
|
+
baseDelayMs: 1000,
|
|
4201
|
+
maxDelayMs: 30000,
|
|
4202
|
+
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
4203
|
+
retryOnNetworkError: true,
|
|
4204
|
+
},
|
|
4205
|
+
/** Generic crypto API configuration */
|
|
4206
|
+
CRYPTO: {
|
|
4207
|
+
maxRetries: 3,
|
|
4208
|
+
baseDelayMs: 1000,
|
|
4209
|
+
maxDelayMs: 30000,
|
|
4210
|
+
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
4211
|
+
retryOnNetworkError: true,
|
|
4212
|
+
},
|
|
4213
|
+
};
|
|
4214
|
+
|
|
3802
4215
|
const limitPriceSlippagePercent100 = 0.1; // 0.1%
|
|
3803
4216
|
/**
|
|
3804
4217
|
Websocket example
|
|
@@ -4109,9 +4522,20 @@ class AlpacaTradingAPI {
|
|
|
4109
4522
|
}
|
|
4110
4523
|
catch (err) {
|
|
4111
4524
|
const error = err;
|
|
4525
|
+
const isTransient = isTransientNetworkError(err);
|
|
4526
|
+
// Transient fetch failures (timeouts, connection resets, undici
|
|
4527
|
+
// aborts) are recoverable at the caller's next cycle; log at WARN
|
|
4528
|
+
// and annotate for observability filters. Non-transient errors
|
|
4529
|
+
// (4xx/auth/schema) stay at ERROR for operator attention.
|
|
4112
4530
|
this.log(`Error in makeRequest: ${error.message}. Url: ${url}`, {
|
|
4113
4531
|
source: "AlpacaAPI",
|
|
4114
|
-
type: "error",
|
|
4532
|
+
type: isTransient ? "warn" : "error",
|
|
4533
|
+
metadata: isTransient
|
|
4534
|
+
? {
|
|
4535
|
+
transient: true,
|
|
4536
|
+
recoveryHint: "Upstream caller should retry on next cycle",
|
|
4537
|
+
}
|
|
4538
|
+
: undefined,
|
|
4115
4539
|
});
|
|
4116
4540
|
throw error;
|
|
4117
4541
|
}
|
|
@@ -5420,7 +5844,30 @@ async function getOrders$1(auth, params = {}) {
|
|
|
5420
5844
|
return allOrders;
|
|
5421
5845
|
}
|
|
5422
5846
|
catch (error) {
|
|
5423
|
-
|
|
5847
|
+
const isTransient = isTransientNetworkError(error);
|
|
5848
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
5849
|
+
const logMeta = {
|
|
5850
|
+
source: "AlpacaLegacy.getOrders",
|
|
5851
|
+
error: errMsg,
|
|
5852
|
+
...(isTransient
|
|
5853
|
+
? {
|
|
5854
|
+
transient: true,
|
|
5855
|
+
recoveryHint: "Upstream caller should retry on next cycle",
|
|
5856
|
+
}
|
|
5857
|
+
: {}),
|
|
5858
|
+
};
|
|
5859
|
+
// Transient network errors (fetch timeouts, ECONNRESET) are
|
|
5860
|
+
// recoverable; log at WARN. Non-transient failures (4xx/auth)
|
|
5861
|
+
// stay at ERROR. Previous call also passed the raw Error as the
|
|
5862
|
+
// second argument, which Pino treats as context not as `err`,
|
|
5863
|
+
// producing empty `err` fields in production logs — fix by
|
|
5864
|
+
// serializing to an explicit string message.
|
|
5865
|
+
if (isTransient) {
|
|
5866
|
+
getLogger().warn(`Error in getOrders: ${errMsg}`, logMeta);
|
|
5867
|
+
}
|
|
5868
|
+
else {
|
|
5869
|
+
getLogger().error(`Error in getOrders: ${errMsg}`, logMeta);
|
|
5870
|
+
}
|
|
5424
5871
|
throw error;
|
|
5425
5872
|
}
|
|
5426
5873
|
}
|
|
@@ -5595,392 +6042,6 @@ async function createLimitOrder(auth, params = {
|
|
|
5595
6042
|
});
|
|
5596
6043
|
}
|
|
5597
6044
|
|
|
5598
|
-
const DEFAULT_RETRY_CONFIG = {
|
|
5599
|
-
maxRetries: 3,
|
|
5600
|
-
baseDelayMs: 1000,
|
|
5601
|
-
maxDelayMs: 30000,
|
|
5602
|
-
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
5603
|
-
retryOnNetworkError: true,
|
|
5604
|
-
};
|
|
5605
|
-
/**
|
|
5606
|
-
* Node.js / undici / system error codes that represent transient network
|
|
5607
|
-
* conditions. Present on `error.code` for net/http/dns/undici errors.
|
|
5608
|
-
*/
|
|
5609
|
-
const RETRYABLE_ERROR_CODES = new Set([
|
|
5610
|
-
"ETIMEDOUT",
|
|
5611
|
-
"ESOCKETTIMEDOUT",
|
|
5612
|
-
"ECONNRESET",
|
|
5613
|
-
"ECONNREFUSED",
|
|
5614
|
-
"EHOSTUNREACH",
|
|
5615
|
-
"ENETUNREACH",
|
|
5616
|
-
"EAI_AGAIN",
|
|
5617
|
-
"EPIPE",
|
|
5618
|
-
"ECONNABORTED",
|
|
5619
|
-
"ENOTFOUND",
|
|
5620
|
-
"UND_ERR_CONNECT_TIMEOUT",
|
|
5621
|
-
"UND_ERR_HEADERS_TIMEOUT",
|
|
5622
|
-
"UND_ERR_BODY_TIMEOUT",
|
|
5623
|
-
"UND_ERR_SOCKET",
|
|
5624
|
-
"UND_ERR_CLOSED",
|
|
5625
|
-
"UND_ERR_REQ_CONTENT_LENGTH_MISMATCH",
|
|
5626
|
-
]);
|
|
5627
|
-
/**
|
|
5628
|
-
* Error constructor names / `error.name` values that indicate transient
|
|
5629
|
-
* abort / timeout conditions.
|
|
5630
|
-
*/
|
|
5631
|
-
const RETRYABLE_ERROR_NAMES = new Set([
|
|
5632
|
-
"AbortError",
|
|
5633
|
-
"TimeoutError",
|
|
5634
|
-
"FetchError",
|
|
5635
|
-
"RequestTimeoutError",
|
|
5636
|
-
"ConnectTimeoutError",
|
|
5637
|
-
"HeadersTimeoutError",
|
|
5638
|
-
"BodyTimeoutError",
|
|
5639
|
-
]);
|
|
5640
|
-
/**
|
|
5641
|
-
* Message-pattern fallback for libraries that discard error codes/names but
|
|
5642
|
-
* preserve text (e.g., some Apollo/axios wrappers).
|
|
5643
|
-
*/
|
|
5644
|
-
const RETRYABLE_MESSAGE_PATTERNS = [
|
|
5645
|
-
/aborted/i,
|
|
5646
|
-
/timeout/i,
|
|
5647
|
-
/timed out/i,
|
|
5648
|
-
/network error/i,
|
|
5649
|
-
/socket hang up/i,
|
|
5650
|
-
/connection (reset|refused|closed)/i,
|
|
5651
|
-
/ECONNRESET/,
|
|
5652
|
-
/ETIMEDOUT/,
|
|
5653
|
-
/ECONNREFUSED/,
|
|
5654
|
-
/EAI_AGAIN/,
|
|
5655
|
-
/UND_ERR_/,
|
|
5656
|
-
];
|
|
5657
|
-
/**
|
|
5658
|
-
* Walks the `error.cause` chain (capped to avoid cycles) and tests whether
|
|
5659
|
-
* any link along the chain looks like a transient network error. Modern APIs
|
|
5660
|
-
* (undici, fetch, Apollo Client 3.8+) wrap the root network failure as a
|
|
5661
|
-
* `.cause`, so the surface `Error` may report a generic message while the
|
|
5662
|
-
* actionable signal lives one or more levels deeper.
|
|
5663
|
-
*/
|
|
5664
|
-
function isTransientNetworkError(error) {
|
|
5665
|
-
const MAX_CAUSE_DEPTH = 6;
|
|
5666
|
-
let current = error;
|
|
5667
|
-
for (let depth = 0; depth < MAX_CAUSE_DEPTH && current; depth++) {
|
|
5668
|
-
if (current instanceof Error || typeof current === "object") {
|
|
5669
|
-
const err = current;
|
|
5670
|
-
if (typeof err.name === "string" && RETRYABLE_ERROR_NAMES.has(err.name)) {
|
|
5671
|
-
return true;
|
|
5672
|
-
}
|
|
5673
|
-
if (typeof err.code === "string" && RETRYABLE_ERROR_CODES.has(err.code)) {
|
|
5674
|
-
return true;
|
|
5675
|
-
}
|
|
5676
|
-
if (typeof err.message === "string") {
|
|
5677
|
-
for (const pattern of RETRYABLE_MESSAGE_PATTERNS) {
|
|
5678
|
-
if (pattern.test(err.message)) {
|
|
5679
|
-
return true;
|
|
5680
|
-
}
|
|
5681
|
-
}
|
|
5682
|
-
}
|
|
5683
|
-
current = err.cause;
|
|
5684
|
-
}
|
|
5685
|
-
else {
|
|
5686
|
-
break;
|
|
5687
|
-
}
|
|
5688
|
-
}
|
|
5689
|
-
return false;
|
|
5690
|
-
}
|
|
5691
|
-
/**
|
|
5692
|
-
* Analyzes an error and determines if it's retryable.
|
|
5693
|
-
* @param error - The error to analyze
|
|
5694
|
-
* @param response - Optional Response object for HTTP errors
|
|
5695
|
-
* @param config - Retry configuration
|
|
5696
|
-
* @returns Structured error details
|
|
5697
|
-
*/
|
|
5698
|
-
function analyzeError(error, response, config) {
|
|
5699
|
-
// Handle Response objects with error status codes
|
|
5700
|
-
if (response && !response.ok) {
|
|
5701
|
-
const status = response.status;
|
|
5702
|
-
// Rate limit errors - always retryable
|
|
5703
|
-
if (status === 429) {
|
|
5704
|
-
const retryAfterHeader = response.headers.get("Retry-After");
|
|
5705
|
-
const retryAfter = retryAfterHeader
|
|
5706
|
-
? parseInt(retryAfterHeader, 10) * 1000
|
|
5707
|
-
: undefined;
|
|
5708
|
-
return {
|
|
5709
|
-
type: "RATE_LIMIT",
|
|
5710
|
-
reason: "Rate limit exceeded",
|
|
5711
|
-
status,
|
|
5712
|
-
retryAfter,
|
|
5713
|
-
isRetryable: true,
|
|
5714
|
-
};
|
|
5715
|
-
}
|
|
5716
|
-
// Authentication errors - never retry
|
|
5717
|
-
if (status === 401 || status === 403) {
|
|
5718
|
-
return {
|
|
5719
|
-
type: "AUTH_ERROR",
|
|
5720
|
-
reason: status === 401
|
|
5721
|
-
? "Authentication failed - invalid credentials"
|
|
5722
|
-
: "Access forbidden - insufficient permissions",
|
|
5723
|
-
status,
|
|
5724
|
-
isRetryable: false,
|
|
5725
|
-
};
|
|
5726
|
-
}
|
|
5727
|
-
// Server errors - check if in retryable list
|
|
5728
|
-
if (status >= 500 && status < 600) {
|
|
5729
|
-
return {
|
|
5730
|
-
type: "SERVER_ERROR",
|
|
5731
|
-
reason: `Server error (${status})`,
|
|
5732
|
-
status,
|
|
5733
|
-
isRetryable: config.retryableStatusCodes.includes(status),
|
|
5734
|
-
};
|
|
5735
|
-
}
|
|
5736
|
-
// Other client errors - never retry
|
|
5737
|
-
if (status >= 400 && status < 500) {
|
|
5738
|
-
return {
|
|
5739
|
-
type: "CLIENT_ERROR",
|
|
5740
|
-
reason: `Client error (${status})`,
|
|
5741
|
-
status,
|
|
5742
|
-
isRetryable: false,
|
|
5743
|
-
};
|
|
5744
|
-
}
|
|
5745
|
-
}
|
|
5746
|
-
// Handle network errors (TypeError from fetch API)
|
|
5747
|
-
if (error instanceof TypeError && error.message.includes("fetch")) {
|
|
5748
|
-
return {
|
|
5749
|
-
type: "NETWORK_ERROR",
|
|
5750
|
-
reason: "Network connectivity issue",
|
|
5751
|
-
status: null,
|
|
5752
|
-
isRetryable: config.retryOnNetworkError,
|
|
5753
|
-
};
|
|
5754
|
-
}
|
|
5755
|
-
// Handle transient network conditions: AbortError, TimeoutError,
|
|
5756
|
-
// Node/undici error codes (ETIMEDOUT, ECONNRESET, UND_ERR_*), and
|
|
5757
|
-
// wrapped failures exposed via error.cause. This catches the broad class
|
|
5758
|
-
// of infrastructure flakes that the TypeError-only check above misses.
|
|
5759
|
-
if (isTransientNetworkError(error)) {
|
|
5760
|
-
const reason = error instanceof Error ? error.message : "Transient network error";
|
|
5761
|
-
return {
|
|
5762
|
-
type: "NETWORK_ERROR",
|
|
5763
|
-
reason,
|
|
5764
|
-
status: null,
|
|
5765
|
-
isRetryable: config.retryOnNetworkError,
|
|
5766
|
-
};
|
|
5767
|
-
}
|
|
5768
|
-
// Handle error objects with messages
|
|
5769
|
-
if (error instanceof Error) {
|
|
5770
|
-
// Parse error messages that might contain status information
|
|
5771
|
-
if (error.message.includes("429") || error.message.includes("RATE_LIMIT")) {
|
|
5772
|
-
const match = error.message.match(/RATE_LIMIT: 429:(\d+)/);
|
|
5773
|
-
const retryAfter = match ? parseInt(match[1], 10) : undefined;
|
|
5774
|
-
return {
|
|
5775
|
-
type: "RATE_LIMIT",
|
|
5776
|
-
reason: "Rate limit exceeded",
|
|
5777
|
-
status: 429,
|
|
5778
|
-
retryAfter,
|
|
5779
|
-
isRetryable: true,
|
|
5780
|
-
};
|
|
5781
|
-
}
|
|
5782
|
-
if (error.message.includes("401") ||
|
|
5783
|
-
error.message.includes("403") ||
|
|
5784
|
-
error.message.includes("AUTH_ERROR")) {
|
|
5785
|
-
const status = error.message.includes("401") ? 401 : 403;
|
|
5786
|
-
return {
|
|
5787
|
-
type: "AUTH_ERROR",
|
|
5788
|
-
reason: `Authentication error (${status})`,
|
|
5789
|
-
status,
|
|
5790
|
-
isRetryable: false,
|
|
5791
|
-
};
|
|
5792
|
-
}
|
|
5793
|
-
if (error.message.includes("SERVER_ERROR") ||
|
|
5794
|
-
error.message.match(/50[0-9]/)) {
|
|
5795
|
-
const statusMatch = error.message.match(/50[0-9]/);
|
|
5796
|
-
const status = statusMatch ? parseInt(statusMatch[0], 10) : 500;
|
|
5797
|
-
return {
|
|
5798
|
-
type: "SERVER_ERROR",
|
|
5799
|
-
reason: `Server error (${status})`,
|
|
5800
|
-
status,
|
|
5801
|
-
isRetryable: config.retryableStatusCodes.includes(status),
|
|
5802
|
-
};
|
|
5803
|
-
}
|
|
5804
|
-
if (error.message.includes("network") ||
|
|
5805
|
-
error.message.includes("NETWORK_ERROR")) {
|
|
5806
|
-
return {
|
|
5807
|
-
type: "NETWORK_ERROR",
|
|
5808
|
-
reason: error.message,
|
|
5809
|
-
status: null,
|
|
5810
|
-
isRetryable: config.retryOnNetworkError,
|
|
5811
|
-
};
|
|
5812
|
-
}
|
|
5813
|
-
}
|
|
5814
|
-
// Unknown error - not retryable by default for safety
|
|
5815
|
-
return {
|
|
5816
|
-
type: "UNKNOWN",
|
|
5817
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
5818
|
-
status: null,
|
|
5819
|
-
isRetryable: false,
|
|
5820
|
-
};
|
|
5821
|
-
}
|
|
5822
|
-
/**
|
|
5823
|
-
* Calculates the delay before the next retry attempt using exponential backoff with jitter.
|
|
5824
|
-
* @param attempt - Current attempt number (1-indexed)
|
|
5825
|
-
* @param baseDelay - Base delay in milliseconds
|
|
5826
|
-
* @param maxDelay - Maximum delay in milliseconds
|
|
5827
|
-
* @returns Delay in milliseconds
|
|
5828
|
-
*/
|
|
5829
|
-
function calculateBackoff(attempt, baseDelay, maxDelay) {
|
|
5830
|
-
// Exponential backoff: baseDelay * 2^(attempt-1)
|
|
5831
|
-
const exponentialDelay = baseDelay * Math.pow(2, attempt - 1);
|
|
5832
|
-
// Cap at maxDelay
|
|
5833
|
-
const cappedDelay = Math.min(exponentialDelay, maxDelay);
|
|
5834
|
-
// Add jitter (random value between 0% and 25% of the delay)
|
|
5835
|
-
const jitter = Math.random() * cappedDelay * 0.25;
|
|
5836
|
-
return Math.floor(cappedDelay + jitter);
|
|
5837
|
-
}
|
|
5838
|
-
/**
|
|
5839
|
-
* Wraps an async function with retry logic and exponential backoff.
|
|
5840
|
-
*
|
|
5841
|
-
* This utility handles transient errors in external API calls by automatically retrying
|
|
5842
|
-
* failed requests with intelligent backoff strategies. It respects rate limit headers,
|
|
5843
|
-
* fails fast on non-retryable errors, and provides detailed logging.
|
|
5844
|
-
*
|
|
5845
|
-
* @template T - The return type of the wrapped function
|
|
5846
|
-
* @param fn - The async function to wrap with retry logic
|
|
5847
|
-
* @param config - Retry configuration (merged with defaults)
|
|
5848
|
-
* @param label - A descriptive label for logging (e.g., 'Massive.fetchTickerInfo')
|
|
5849
|
-
* @returns A promise that resolves to the function's return value
|
|
5850
|
-
* @throws The last error encountered if all retries are exhausted
|
|
5851
|
-
*
|
|
5852
|
-
* @example
|
|
5853
|
-
* ```typescript
|
|
5854
|
-
* // Basic usage with defaults
|
|
5855
|
-
* const data = await withRetry(
|
|
5856
|
-
* async () => fetch('https://api.example.com/data'),
|
|
5857
|
-
* {},
|
|
5858
|
-
* 'ExampleAPI.fetchData'
|
|
5859
|
-
* );
|
|
5860
|
-
*
|
|
5861
|
-
* // Custom configuration for rate-limited API
|
|
5862
|
-
* const result = await withRetry(
|
|
5863
|
-
* async () => alphaVantageAPI.getQuote(symbol),
|
|
5864
|
-
* {
|
|
5865
|
-
* maxRetries: 5,
|
|
5866
|
-
* baseDelayMs: 5000,
|
|
5867
|
-
* maxDelayMs: 60000,
|
|
5868
|
-
* onRetry: (attempt, error) => {
|
|
5869
|
-
* getLogger().info(`Retry ${attempt} after error:`, error);
|
|
5870
|
-
* }
|
|
5871
|
-
* },
|
|
5872
|
-
* 'AlphaVantage.getQuote'
|
|
5873
|
-
* );
|
|
5874
|
-
* ```
|
|
5875
|
-
*/
|
|
5876
|
-
async function withRetry(fn, config = {}, label = "unknown") {
|
|
5877
|
-
const fullConfig = { ...DEFAULT_RETRY_CONFIG, ...config };
|
|
5878
|
-
let lastError;
|
|
5879
|
-
for (let attempt = 1; attempt <= fullConfig.maxRetries; attempt++) {
|
|
5880
|
-
try {
|
|
5881
|
-
const result = await fn();
|
|
5882
|
-
// If we succeeded after retries, log it
|
|
5883
|
-
if (attempt > 1) {
|
|
5884
|
-
getLogger().info(`[${label}] Succeeded on attempt ${attempt}/${fullConfig.maxRetries}`);
|
|
5885
|
-
}
|
|
5886
|
-
return result;
|
|
5887
|
-
}
|
|
5888
|
-
catch (error) {
|
|
5889
|
-
lastError = error;
|
|
5890
|
-
// If this is the last attempt, throw the error
|
|
5891
|
-
if (attempt === fullConfig.maxRetries) {
|
|
5892
|
-
getLogger().error(`[${label}] Failed after ${fullConfig.maxRetries} attempts`, {
|
|
5893
|
-
error: error instanceof Error ? error.message : String(error),
|
|
5894
|
-
timestamp: new Date().toISOString(),
|
|
5895
|
-
});
|
|
5896
|
-
throw error;
|
|
5897
|
-
}
|
|
5898
|
-
// Analyze the error to determine if we should retry
|
|
5899
|
-
const response = error instanceof Response ? error : null;
|
|
5900
|
-
const errorDetails = analyzeError(error, response, fullConfig);
|
|
5901
|
-
// If error is not retryable, fail immediately
|
|
5902
|
-
if (!errorDetails.isRetryable) {
|
|
5903
|
-
getLogger().error(`[${label}] Non-retryable error (${errorDetails.type})`, {
|
|
5904
|
-
reason: errorDetails.reason,
|
|
5905
|
-
status: errorDetails.status,
|
|
5906
|
-
timestamp: new Date().toISOString(),
|
|
5907
|
-
});
|
|
5908
|
-
throw error;
|
|
5909
|
-
}
|
|
5910
|
-
// Calculate delay for next retry
|
|
5911
|
-
let delayMs;
|
|
5912
|
-
if (errorDetails.type === "RATE_LIMIT" && errorDetails.retryAfter) {
|
|
5913
|
-
// Use Retry-After header if available
|
|
5914
|
-
delayMs = errorDetails.retryAfter;
|
|
5915
|
-
}
|
|
5916
|
-
else if (errorDetails.type === "RATE_LIMIT") {
|
|
5917
|
-
// For rate limits without Retry-After, use a longer minimum delay
|
|
5918
|
-
delayMs = Math.max(calculateBackoff(attempt, fullConfig.baseDelayMs, fullConfig.maxDelayMs), 5000);
|
|
5919
|
-
}
|
|
5920
|
-
else {
|
|
5921
|
-
// Standard exponential backoff with jitter
|
|
5922
|
-
delayMs = calculateBackoff(attempt, fullConfig.baseDelayMs, fullConfig.maxDelayMs);
|
|
5923
|
-
}
|
|
5924
|
-
// Log the retry attempt
|
|
5925
|
-
getLogger().warn(`[${label}] Attempt ${attempt}/${fullConfig.maxRetries} failed: ${errorDetails.reason}. Retrying in ${delayMs}ms...`, {
|
|
5926
|
-
attemptNumber: attempt,
|
|
5927
|
-
totalRetries: fullConfig.maxRetries,
|
|
5928
|
-
errorType: errorDetails.type,
|
|
5929
|
-
httpStatus: errorDetails.status,
|
|
5930
|
-
retryDelay: delayMs,
|
|
5931
|
-
timestamp: new Date().toISOString(),
|
|
5932
|
-
});
|
|
5933
|
-
// Call the optional retry callback
|
|
5934
|
-
if (fullConfig.onRetry) {
|
|
5935
|
-
fullConfig.onRetry(attempt, error);
|
|
5936
|
-
}
|
|
5937
|
-
// Wait before retrying
|
|
5938
|
-
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
5939
|
-
}
|
|
5940
|
-
}
|
|
5941
|
-
// This should never be reached due to the throw in the last attempt,
|
|
5942
|
-
// but TypeScript needs this to satisfy the return type
|
|
5943
|
-
throw lastError;
|
|
5944
|
-
}
|
|
5945
|
-
/**
|
|
5946
|
-
* API-specific retry configurations for different external services.
|
|
5947
|
-
* These configurations are tuned based on each API's rate limits and characteristics.
|
|
5948
|
-
*/
|
|
5949
|
-
const API_RETRY_CONFIGS = {
|
|
5950
|
-
/** Massive.com API - 5 requests/second rate limit */
|
|
5951
|
-
MASSIVE: {
|
|
5952
|
-
maxRetries: 3,
|
|
5953
|
-
baseDelayMs: 1000,
|
|
5954
|
-
maxDelayMs: 30000,
|
|
5955
|
-
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
5956
|
-
retryOnNetworkError: true,
|
|
5957
|
-
},
|
|
5958
|
-
/** Alpha Vantage API - 5 requests/minute rate limit (more strict) */
|
|
5959
|
-
ALPHA_VANTAGE: {
|
|
5960
|
-
maxRetries: 5,
|
|
5961
|
-
baseDelayMs: 5000,
|
|
5962
|
-
maxDelayMs: 60000,
|
|
5963
|
-
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
5964
|
-
retryOnNetworkError: true,
|
|
5965
|
-
},
|
|
5966
|
-
/** Alpaca API - generally reliable, shorter retry window */
|
|
5967
|
-
ALPACA: {
|
|
5968
|
-
maxRetries: 3,
|
|
5969
|
-
baseDelayMs: 1000,
|
|
5970
|
-
maxDelayMs: 30000,
|
|
5971
|
-
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
5972
|
-
retryOnNetworkError: true,
|
|
5973
|
-
},
|
|
5974
|
-
/** Generic crypto API configuration */
|
|
5975
|
-
CRYPTO: {
|
|
5976
|
-
maxRetries: 3,
|
|
5977
|
-
baseDelayMs: 1000,
|
|
5978
|
-
maxDelayMs: 30000,
|
|
5979
|
-
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
5980
|
-
retryOnNetworkError: true,
|
|
5981
|
-
},
|
|
5982
|
-
};
|
|
5983
|
-
|
|
5984
6045
|
// Utility function for debug logging
|
|
5985
6046
|
/**
|
|
5986
6047
|
* Debug logging utility that respects environment debug flags.
|
|
@@ -8507,19 +8568,37 @@ const fetchPrices = async (params, options) => {
|
|
|
8507
8568
|
catch (error) {
|
|
8508
8569
|
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
8509
8570
|
const contextualMessage = `Error fetching price data for ${ticker}`;
|
|
8510
|
-
|
|
8571
|
+
const isTransient = isTransientNetworkError(error);
|
|
8572
|
+
const errorType = error instanceof Error && error.message.includes("AUTH_ERROR")
|
|
8573
|
+
? "AUTH_ERROR"
|
|
8574
|
+
: error instanceof Error && error.message.includes("RATE_LIMIT")
|
|
8575
|
+
? "RATE_LIMIT"
|
|
8576
|
+
: isTransient
|
|
8577
|
+
? "NETWORK_ERROR"
|
|
8578
|
+
: "UNKNOWN";
|
|
8579
|
+
// Transient network failures (undici timeouts, socket resets, DNS
|
|
8580
|
+
// blips) are recoverable at the caller's next cycle — demote to
|
|
8581
|
+
// WARN to avoid alert noise. Non-transient classes (auth, rate
|
|
8582
|
+
// limits that have exhausted retries, unknown schema errors) stay
|
|
8583
|
+
// at ERROR because they require operator attention.
|
|
8584
|
+
const logMeta = {
|
|
8511
8585
|
ticker,
|
|
8512
|
-
errorType
|
|
8513
|
-
? "AUTH_ERROR"
|
|
8514
|
-
: error instanceof Error && error.message.includes("RATE_LIMIT")
|
|
8515
|
-
? "RATE_LIMIT"
|
|
8516
|
-
: error instanceof Error &&
|
|
8517
|
-
error.message.includes("NETWORK_ERROR")
|
|
8518
|
-
? "NETWORK_ERROR"
|
|
8519
|
-
: "UNKNOWN",
|
|
8586
|
+
errorType,
|
|
8520
8587
|
source: "MassiveAPI.fetchPrices",
|
|
8521
8588
|
timestamp: new Date().toISOString(),
|
|
8522
|
-
|
|
8589
|
+
...(isTransient
|
|
8590
|
+
? {
|
|
8591
|
+
transient: true,
|
|
8592
|
+
recoveryHint: "Upstream caller should retry on next cycle",
|
|
8593
|
+
}
|
|
8594
|
+
: {}),
|
|
8595
|
+
};
|
|
8596
|
+
if (isTransient) {
|
|
8597
|
+
getLogger().warn(`${contextualMessage}: ${errorMessage}`, logMeta);
|
|
8598
|
+
}
|
|
8599
|
+
else {
|
|
8600
|
+
getLogger().error(`${contextualMessage}: ${errorMessage}`, logMeta);
|
|
8601
|
+
}
|
|
8523
8602
|
throw new Error(`${contextualMessage}: ${errorMessage}`);
|
|
8524
8603
|
}
|
|
8525
8604
|
});
|
|
@@ -68384,5 +68463,5 @@ const adaptic = {
|
|
|
68384
68463
|
};
|
|
68385
68464
|
const adptc = adaptic;
|
|
68386
68465
|
|
|
68387
|
-
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, CryptoDataError, CryptoOrderError, DEFAULT_CACHE_OPTIONS, 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, 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, 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, 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, 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, index$1 as legacyApi, limitBuyWithTakeProfit, ocoOrders, orderUtils, otoOrders, paginate, paginateAll, parseOCCSymbol, protectLongPosition, protectShortPosition, rateLimiters, resetLogger, rollOptionPosition, roundPriceForAlpaca$3 as roundPriceForAlpaca, roundPriceForAlpacaNumber, safeValidateResponse, searchNews, sellAllCrypto, sellCryptoNotional, sellToClose, sellToOpen, setLogger, shortWithStopLoss, sortOrdersByDate, index as tradingPolicy, trailingStops, updateAccountConfiguration, updateTrailingStop, validateAlpacaCredentials, validateAlphaVantageApiKey, validateMassiveApiKey$1 as validateMassiveApiKey, validateMultiLegOrder, validateResponse, verifyFetchKeepAlive, waitForOrderFill, withRetry, withTimeout };
|
|
68466
|
+
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, CryptoDataError, CryptoOrderError, DEFAULT_CACHE_OPTIONS, 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, 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, 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, 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, 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, rollOptionPosition, roundPriceForAlpaca$3 as roundPriceForAlpaca, roundPriceForAlpacaNumber, safeValidateResponse, searchNews, sellAllCrypto, sellCryptoNotional, sellToClose, sellToOpen, setLogger, shortWithStopLoss, sortOrdersByDate, index as tradingPolicy, trailingStops, updateAccountConfiguration, updateTrailingStop, validateAlpacaCredentials, validateAlphaVantageApiKey, validateMassiveApiKey$1 as validateMassiveApiKey, validateMultiLegOrder, validateResponse, verifyFetchKeepAlive, waitForOrderFill, withRetry, withTimeout };
|
|
68388
68467
|
//# sourceMappingURL=index.mjs.map
|