Browse Source

Fix historical market data gathering at daylight saving time change

pull/7715/head
Thomas Kaul 5 days ago
parent
commit
cd7207873d
  1. 30
      apps/api/jest-environment-tz.js
  2. 83
      apps/api/src/services/queues/data-gathering/data-gathering.service.time-zone.spec.ts
  3. 5
      apps/api/src/services/queues/data-gathering/data-gathering.service.ts

30
apps/api/jest-environment-tz.js

@ -0,0 +1,30 @@
const NodeEnvironment = require('jest-environment-node').TestEnvironment;
// Jest gives each test file a copy of `process`, so a test cannot change the
// time zone at run time. This environment sets `TZ` on the real process of the
// worker, which resets the internal date cache of Node.
class TimeZoneEnvironment extends NodeEnvironment {
constructor(config, context) {
super(config, context);
this.timeZone = config.projectConfig.testEnvironmentOptions?.timeZone;
}
async setup() {
this.previousTimeZone = process.env.TZ;
if (this.timeZone) {
process.env.TZ = this.timeZone;
}
await super.setup();
}
async teardown() {
await super.teardown();
process.env.TZ = this.previousTimeZone;
}
}
module.exports = TimeZoneEnvironment;

83
apps/api/src/services/queues/data-gathering/data-gathering.service.time-zone.spec.ts

@ -0,0 +1,83 @@
/**
* @jest-environment <rootDir>/jest-environment-tz.js
* @jest-environment-options {"timeZone": "Europe/Berlin"}
*/
import { DataGatheringService } from './data-gathering.service';
describe('DataGatheringService in a time zone with daylight saving time', () => {
let dataGatheringQueue: { addBulk: jest.Mock; clean: jest.Mock };
let dataGatheringService: DataGatheringService;
let prismaService: { marketData: { groupBy: jest.Mock; upsert: jest.Mock } };
beforeEach(() => {
dataGatheringQueue = {
addBulk: jest.fn().mockResolvedValue([]),
clean: jest.fn().mockResolvedValue([])
};
prismaService = {
marketData: {
groupBy: jest.fn().mockResolvedValue([]),
upsert: jest.fn().mockResolvedValue({})
}
};
dataGatheringService = new DataGatheringService(
null,
dataGatheringQueue as any,
{ getHistoricalRaw: jest.fn() } as any,
null,
null,
prismaService as any,
null,
null
);
});
afterEach(() => {
jest.useRealTimers();
});
describe('getAssetProfileIdentifiersWithRecentMarketData', () => {
it('keeps a market price of yesterday when the clocks go forward', async () => {
// The clocks in Berlin go forward on 2026-03-29, so a local day is 23 hours
jest.useFakeTimers().setSystemTime(new Date('2026-03-29T23:00:00.000Z'));
prismaService.marketData.groupBy.mockResolvedValue([
{
_max: { date: new Date('2026-03-28T00:00:00.000Z') },
dataSource: 'COINGECKO',
symbol: 'bitcoin'
}
]);
const assetProfileIdentifiers =
await dataGatheringService[
'getAssetProfileIdentifiersWithRecentMarketData'
]();
expect(assetProfileIdentifiers).toEqual([
{ dataSource: 'COINGECKO', symbol: 'bitcoin' }
]);
});
it('drops a market price of two days ago when the clocks go back', async () => {
// The clocks in Berlin go back on 2026-10-25, so a local day is 25 hours
jest.useFakeTimers().setSystemTime(new Date('2026-10-26T00:00:00.000Z'));
prismaService.marketData.groupBy.mockResolvedValue([
{
_max: { date: new Date('2026-10-24T00:00:00.000Z') },
dataSource: 'COINGECKO',
symbol: 'bitcoin'
}
]);
const assetProfileIdentifiers =
await dataGatheringService[
'getAssetProfileIdentifiersWithRecentMarketData'
]();
expect(assetProfileIdentifiers).toEqual([]);
});
});
});

5
apps/api/src/services/queues/data-gathering/data-gathering.service.ts

@ -437,7 +437,10 @@ export class DataGatheringService {
}) })
) )
.filter(({ _max }) => { .filter(({ _max }) => {
return !isBefore(_max.date, getStartOfUtcDate(subDays(new Date(), 1))); return !isBefore(
_max.date,
subMilliseconds(getStartOfUtcDate(new Date()), ms('1 day'))
);
}) })
.map(({ dataSource, symbol }) => { .map(({ dataSource, symbol }) => {
return { dataSource, symbol }; return { dataSource, symbol };

Loading…
Cancel
Save