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)