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) <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-05-03 17:01:48 -05:00
co-authored by Claude Opus 4.6
parent e6a83eed2f
commit 3da96fe73a
+48 -25
View File
@@ -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)