bugfixes; research subproc; higher sandbox limits
This commit is contained in:
@@ -1,6 +1,37 @@
|
||||
// CCXT data fetcher for historical OHLC and realtime ticks
|
||||
import ccxt from 'ccxt';
|
||||
|
||||
/**
|
||||
* Thrown when an exchange returns a 429 rate-limit response.
|
||||
* retryAfterMs is derived from the exchange's Retry-After header when available.
|
||||
*/
|
||||
export class ExchangeRateLimitError extends Error {
|
||||
constructor(exchange, retryAfterMs, originalMessage) {
|
||||
super(`Rate limit on ${exchange}: retry after ${retryAfterMs}ms (${originalMessage})`);
|
||||
this.name = 'ExchangeRateLimitError';
|
||||
this.exchange = exchange.toUpperCase();
|
||||
this.retryAfterMs = retryAfterMs;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract retry-after duration in milliseconds from a CCXT RateLimitExceeded error.
|
||||
* Priority: Retry-After header → error message numeric → 30s fallback.
|
||||
*/
|
||||
function extractRetryAfterMs(exchange, error) {
|
||||
const header = exchange.last_response_headers?.['retry-after'];
|
||||
if (header) {
|
||||
const secs = parseFloat(header);
|
||||
if (!isNaN(secs)) return Math.ceil(secs * 1000);
|
||||
}
|
||||
// Some exchanges embed the delay in the message (e.g. "retry after 5000 ms")
|
||||
const msMatch = error.message?.match(/(\d+)\s*ms/i);
|
||||
if (msMatch) return parseInt(msMatch[1], 10);
|
||||
const secMatch = error.message?.match(/(\d+(?:\.\d+)?)\s*s(?:ec|econds?)?/i);
|
||||
if (secMatch) return Math.ceil(parseFloat(secMatch[1]) * 1000);
|
||||
return 30_000;
|
||||
}
|
||||
|
||||
export class CCXTFetcher {
|
||||
constructor(config, logger, metadataGenerator = null) {
|
||||
this.config = config;
|
||||
@@ -135,9 +166,12 @@ export class CCXTFetcher {
|
||||
break;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
const isRetryable = error.constructor?.name === 'NetworkError' ||
|
||||
const isRateLimit = error.constructor?.name === 'RateLimitExceeded';
|
||||
const isRetryable = !isRateLimit && (
|
||||
error.constructor?.name === 'NetworkError' ||
|
||||
error.constructor?.name === 'RequestTimeout' ||
|
||||
error.constructor?.name === 'ExchangeNotAvailable';
|
||||
error.constructor?.name === 'ExchangeNotAvailable'
|
||||
);
|
||||
this.logger.warn(
|
||||
{
|
||||
errorType: error.constructor?.name,
|
||||
@@ -146,15 +180,21 @@ export class CCXTFetcher {
|
||||
ticker,
|
||||
since,
|
||||
attempt,
|
||||
retryable: isRetryable
|
||||
retryable: isRetryable,
|
||||
rateLimit: isRateLimit
|
||||
},
|
||||
'OHLC fetch attempt failed'
|
||||
);
|
||||
if (!isRetryable || attempt === FETCH_RETRIES) break;
|
||||
if (isRateLimit || !isRetryable || attempt === FETCH_RETRIES) break;
|
||||
await exchange.sleep(FETCH_RETRY_DELAY_MS * attempt);
|
||||
}
|
||||
}
|
||||
if (lastError) {
|
||||
if (lastError.constructor?.name === 'RateLimitExceeded') {
|
||||
const retryAfterMs = extractRetryAfterMs(exchange, lastError);
|
||||
this.logger.warn({ ticker, retryAfterMs }, 'OHLC fetch rate-limited by exchange');
|
||||
throw new ExchangeRateLimitError(exchangeName, retryAfterMs, lastError.message);
|
||||
}
|
||||
this.logger.error(
|
||||
{
|
||||
errorType: lastError.constructor?.name,
|
||||
@@ -278,6 +318,11 @@ export class CCXTFetcher {
|
||||
// Convert to our Tick format
|
||||
return trades.map(trade => this.convertToTick(trade, ticker, metadata));
|
||||
} catch (error) {
|
||||
if (error.constructor?.name === 'RateLimitExceeded') {
|
||||
const retryAfterMs = extractRetryAfterMs(exchange, error);
|
||||
this.logger.warn({ ticker, retryAfterMs }, 'Trades fetch rate-limited by exchange');
|
||||
throw new ExchangeRateLimitError(exchangeName, retryAfterMs, error.message);
|
||||
}
|
||||
this.logger.error(
|
||||
{ error: error.message, ticker },
|
||||
'Error fetching trades'
|
||||
|
||||
Reference in New Issue
Block a user