diff --git a/apps/api/src/services/data-provider/fxmacrodata/fxmacrodata.service.spec.ts b/apps/api/src/services/data-provider/fxmacrodata/fxmacrodata.service.spec.ts index a7914e127e..20537cec51 100644 --- a/apps/api/src/services/data-provider/fxmacrodata/fxmacrodata.service.spec.ts +++ b/apps/api/src/services/data-provider/fxmacrodata/fxmacrodata.service.spec.ts @@ -207,6 +207,40 @@ describe('FXMacroDataService', () => { }); }); + it('follows the pagination offsets until the last page', async () => { + responses['offset=0'] = { + data: [ + { date: '2026-09-10', val: 1.16 }, + { date: '2026-09-09', val: 1.15 } + ], + pagination: { has_more: true, next_offset: 2 } + }; + responses['offset=2'] = { + data: [{ date: '2026-09-08', val: 1.14 }], + pagination: { has_more: false, next_offset: null } + }; + + const historical = await fxMacroDataService.getHistorical({ + from: new Date('2026-09-08'), + symbol: 'EURUSD', + to: new Date('2026-09-10') + }); + + expect(historical).toEqual({ + '2026-09-08': { marketPrice: 1.14 }, + '2026-09-09': { marketPrice: 1.15 }, + '2026-09-10': { marketPrice: 1.16 } + }); + + const urls = (fetchService.fetch as jest.Mock).mock.calls.map(([url]) => { + return url as string; + }); + + expect(urls).toHaveLength(2); + expect(urls[0]).toContain('limit=100'); + expect(urls[1]).toContain('offset=2'); + }); + it('skips a date whose rate is null', async () => { responses['forex/eur/usd'] = { data: [ diff --git a/apps/api/src/services/data-provider/fxmacrodata/fxmacrodata.service.ts b/apps/api/src/services/data-provider/fxmacrodata/fxmacrodata.service.ts index 443c4032a4..b987b282f4 100644 --- a/apps/api/src/services/data-provider/fxmacrodata/fxmacrodata.service.ts +++ b/apps/api/src/services/data-provider/fxmacrodata/fxmacrodata.service.ts @@ -26,6 +26,9 @@ import { FXMacroDataSourcesResponse } from './interfaces/interfaces'; +const PAGE_SIZE = 100; +const MAX_PAGES = 1000; + @Injectable() export class FXMacroDataService implements DataProviderInterface { private readonly baseUrl = 'https://api.fxmacrodata.com/v1'; @@ -100,14 +103,16 @@ export class FXMacroDataService implements DataProviderInterface { // The endpoint caps a page at 100 rows and orders most-recent-first, so // longer ranges are walked page by page rather than silently truncated. - for (let page = 1; ; page++) { - const { data } = await this.get({ + let offset = 0; + + for (let page = 0; page < MAX_PAGES; page++) { + const { data, pagination } = await this.get({ requestTimeout, path: `forex/${base.toLowerCase()}/${quote.toLowerCase()}`, searchParams: { end_date: format(to, DATE_FORMAT), - limit: '100', - page: page.toString(), + limit: PAGE_SIZE.toString(), + offset: offset.toString(), start_date: format(from, DATE_FORMAT) } }); @@ -124,9 +129,11 @@ export class FXMacroDataService implements DataProviderInterface { } } - if (data.length < 100) { + if (!pagination?.has_more) { break; } + + offset = pagination.next_offset ?? offset + data.length; } return response; diff --git a/apps/api/src/services/data-provider/fxmacrodata/interfaces/interfaces.ts b/apps/api/src/services/data-provider/fxmacrodata/interfaces/interfaces.ts index da851dfc7b..88b42a8bf4 100644 --- a/apps/api/src/services/data-provider/fxmacrodata/interfaces/interfaces.ts +++ b/apps/api/src/services/data-provider/fxmacrodata/interfaces/interfaces.ts @@ -5,6 +5,10 @@ export interface FXMacroDataForexResponse { // The rate is documented as anyOf[number, null]. val: number | null; }[]; + pagination?: { + has_more: boolean; + next_offset: number | null; + }; quote: string; }