From 3da96fe73af9af5680dd366fb5d83620b9683e9e Mon Sep 17 00:00:00 2001 From: Brandon Li Date: Sun, 3 May 2026 17:01:48 -0500 Subject: [PATCH] Retry requestReport on 5xx/network errors with backoff Tradovate's report server occasionally returns 502, causing accounts like PAAPEX5776400000019 to end up with empty data when both Cash History and Fills fail. Now retries up to 3 times with exponential backoff (500ms, 1s, 2s) on transient errors before falling through to the existing 5-min outer retry. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/tradovate-class.ts | 73 +++++++++++++++++++++++++++--------------- 1 file changed, 48 insertions(+), 25 deletions(-) diff --git a/lib/tradovate-class.ts b/lib/tradovate-class.ts index 0587f8e..a5e17e7 100644 --- a/lib/tradovate-class.ts +++ b/lib/tradovate-class.ts @@ -457,33 +457,56 @@ export class TradovateClient { if (!this.lastFetchRaw) this.lastFetchRaw = {}; if (!this.lastFetchErrors) this.lastFetchErrors = {}; + // Treat 5xx and network-level errors as retryable (Tradovate report server is flaky). + const isTransient = (err: unknown): boolean => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const status = (err as any)?.response?.status; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const code = (err as any)?.code; + return (typeof status === 'number' && status >= 500 && status < 600) + || code === 'ECONNRESET' || code === 'ETIMEDOUT' || code === 'ECONNABORTED'; + }; + const requestReport = async (name: string, accountName: string) => { - let reportData = (await axios.post( - 'https://rpt-demo.tradovateapi.com/v1/reports/requestreport', - { - name, representationType: 'json', timezone: -300, - params: [ - { name: 'startDate', value: fmtDate(start) }, - { name: 'endDate', value: fmtDate(now) }, - { name: 'startTime', value: '00:00:00' }, - { name: 'endTime', value: '00:00:00' }, - { name: 'account', value: accountName }, - ], - }, - { headers: authHeaders } - )).data; - let pollAttempts = 0; - while (reportData?.['p-ticket'] && pollAttempts < 30) { - const pTicket: string = reportData['p-ticket']; - const pTime: number = Math.max(1, reportData['p-time'] ?? 1); - await new Promise((r) => setTimeout(r, pTime * 1000)); - reportData = (await axios.get( - 'https://rpt-demo.tradovateapi.com/v1/reports/getreport', - { params: { 'p-ticket': pTicket }, headers: authHeaders } - )).data; - pollAttempts++; + const maxAttempts = 3; + let lastErr: unknown = null; + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + let reportData = (await axios.post( + 'https://rpt-demo.tradovateapi.com/v1/reports/requestreport', + { + name, representationType: 'json', timezone: -300, + params: [ + { name: 'startDate', value: fmtDate(start) }, + { name: 'endDate', value: fmtDate(now) }, + { name: 'startTime', value: '00:00:00' }, + { name: 'endTime', value: '00:00:00' }, + { name: 'account', value: accountName }, + ], + }, + { headers: authHeaders } + )).data; + let pollAttempts = 0; + while (reportData?.['p-ticket'] && pollAttempts < 30) { + const pTicket: string = reportData['p-ticket']; + const pTime: number = Math.max(1, reportData['p-time'] ?? 1); + await new Promise((r) => setTimeout(r, pTime * 1000)); + reportData = (await axios.get( + 'https://rpt-demo.tradovateapi.com/v1/reports/getreport', + { params: { 'p-ticket': pTicket }, headers: authHeaders } + )).data; + pollAttempts++; + } + return typeof reportData?.data === 'string' ? reportData.data : '[]'; + } catch (err) { + lastErr = err; + if (!isTransient(err) || attempt === maxAttempts) throw err; + const backoffMs = 500 * Math.pow(2, attempt - 1); // 500ms, 1s, 2s + console.warn(`[requestReport] ${accountName} ${name}: transient error (attempt ${attempt}/${maxAttempts}), retrying in ${backoffMs}ms`); + await new Promise((r) => setTimeout(r, backoffMs)); + } } - return typeof reportData?.data === 'string' ? reportData.data : '[]'; + throw lastErr; }; // Merge fresh API entries on top of cached historical entries (fresh takes precedence for overlapping dates)