Browse Source

Simplify recent market data gathering

task/simplify-recent-market-data-gathering
Thomas Kaul 5 days ago
parent
commit
32fedd2a72
  1. 24
      apps/api/src/services/queues/data-gathering/data-gathering.processor.ts
  2. 80
      apps/api/src/services/queues/data-gathering/data-gathering.service.spec.ts
  3. 45
      apps/api/src/services/queues/data-gathering/data-gathering.service.ts

24
apps/api/src/services/queues/data-gathering/data-gathering.processor.ts

@ -21,15 +21,7 @@ import { Process, Processor } from '@nestjs/bull';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { Prisma } from '@prisma/client'; import { Prisma } from '@prisma/client';
import { Job } from 'bull'; import { Job } from 'bull';
import { import { addDays, format, isBefore, parseISO } from 'date-fns';
addDays,
format,
getDate,
getMonth,
getYear,
isBefore,
parseISO
} from 'date-fns';
import { DataGatheringService } from './data-gathering.service'; import { DataGatheringService } from './data-gathering.service';
@ -126,19 +118,7 @@ export class DataGatheringProcessor {
const data: Prisma.MarketDataUpdateInput[] = []; const data: Prisma.MarketDataUpdateInput[] = [];
let lastMarketPrice: number; let lastMarketPrice: number;
while ( while (isBefore(currentDate, getStartOfUtcDate(new Date()))) {
isBefore(
currentDate,
new Date(
Date.UTC(
getYear(new Date()),
getMonth(new Date()),
getDate(new Date()),
0
)
)
)
) {
const marketPriceOfDataProvider = const marketPriceOfDataProvider =
historicalData[assetProfileIdentifier]?.[ historicalData[assetProfileIdentifier]?.[
format(currentDate, DATE_FORMAT) format(currentDate, DATE_FORMAT)

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

@ -42,8 +42,8 @@ describe('DataGatheringService', () => {
}); });
describe('getAssetProfileIdentifiersWithRecentMarketData', () => { describe('getAssetProfileIdentifiersWithRecentMarketData', () => {
it('excludes carried forward market prices from the query', async () => { it('queries real market prices since the start of yesterday (UTC)', async () => {
jest.useFakeTimers().setSystemTime(parseDate('2026-08-23').getTime()); jest.useFakeTimers().setSystemTime(new Date('2026-08-24T14:00:00.000Z'));
await dataGatheringService[ await dataGatheringService[
'getAssetProfileIdentifiersWithRecentMarketData' 'getAssetProfileIdentifiersWithRecentMarketData'
@ -51,80 +51,19 @@ describe('DataGatheringService', () => {
expect(prismaService.marketData.groupBy).toHaveBeenCalledWith( expect(prismaService.marketData.groupBy).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
where: expect.objectContaining({ where: {
date: { gte: new Date('2026-08-23T00:00:00.000Z') },
isCarriedForward: false, isCarriedForward: false,
state: 'CLOSE' state: 'CLOSE'
}) }
}) })
); );
}); });
it('keeps a cryptocurrency with a real market price of yesterday on Sunday', async () => { it('maps the query result to asset profile identifiers', async () => {
jest.useFakeTimers().setSystemTime(parseDate('2026-08-23').getTime());
prismaService.marketData.groupBy.mockResolvedValue([
{
_max: { date: parseDate('2026-08-22') },
dataSource: 'COINGECKO',
symbol: 'bitcoin'
},
{
_max: { date: parseDate('2026-08-21') },
dataSource: 'YAHOO',
symbol: 'AAPL'
}
]);
const assetProfileIdentifiers =
await dataGatheringService[
'getAssetProfileIdentifiersWithRecentMarketData'
]();
expect(assetProfileIdentifiers).toEqual([
{ dataSource: 'COINGECKO', symbol: 'bitcoin' }
]);
});
it('drops a stock with a real market price of Friday on Monday', async () => {
jest.useFakeTimers().setSystemTime(parseDate('2026-08-24').getTime());
prismaService.marketData.groupBy.mockResolvedValue([ prismaService.marketData.groupBy.mockResolvedValue([
{ { dataSource: 'COINGECKO', symbol: 'bitcoin' },
_max: { date: parseDate('2026-08-23') }, { dataSource: 'YAHOO', symbol: 'AAPL' }
dataSource: 'COINGECKO',
symbol: 'bitcoin'
},
{
_max: { date: parseDate('2026-08-21') },
dataSource: 'YAHOO',
symbol: 'AAPL'
}
]);
const assetProfileIdentifiers =
await dataGatheringService[
'getAssetProfileIdentifiersWithRecentMarketData'
]();
expect(assetProfileIdentifiers).toEqual([
{ dataSource: 'COINGECKO', symbol: 'bitcoin' }
]);
});
it('drops a stock with a late Friday close on Saturday', async () => {
jest.useFakeTimers().setSystemTime(parseDate('2026-08-22').getTime());
prismaService.marketData.groupBy.mockResolvedValue([
{
_max: { date: parseDate('2026-08-20') },
dataSource: 'YAHOO',
symbol: 'AAPL'
},
{
_max: { date: parseDate('2026-08-21') },
dataSource: 'YAHOO',
symbol: 'MSFT'
}
]); ]);
const assetProfileIdentifiers = const assetProfileIdentifiers =
@ -133,7 +72,8 @@ describe('DataGatheringService', () => {
](); ]();
expect(assetProfileIdentifiers).toEqual([ expect(assetProfileIdentifiers).toEqual([
{ dataSource: 'YAHOO', symbol: 'MSFT' } { dataSource: 'COINGECKO', symbol: 'bitcoin' },
{ dataSource: 'YAHOO', symbol: 'AAPL' }
]); ]);
}); });
}); });

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

@ -31,14 +31,7 @@ import { InjectQueue } from '@nestjs/bull';
import { Inject, Injectable, Logger } from '@nestjs/common'; import { Inject, Injectable, Logger } from '@nestjs/common';
import { Prisma } from '@prisma/client'; import { Prisma } from '@prisma/client';
import { Job, JobOptions, Queue } from 'bull'; import { Job, JobOptions, Queue } from 'bull';
import { import { format, min, subDays, subMilliseconds, subYears } from 'date-fns';
format,
isBefore,
min,
subDays,
subMilliseconds,
subYears
} from 'date-fns';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import ms, { StringValue } from 'ms'; import ms, { StringValue } from 'ms';
@ -269,15 +262,21 @@ export class DataGatheringService {
age: GATHER_HISTORICAL_MARKET_DATA_COOLDOWN_IN_MS / 1000 age: GATHER_HISTORICAL_MARKET_DATA_COOLDOWN_IN_MS / 1000
}; };
const assetProfileIdentifiersWithRecentMarketData =
await this.getAssetProfileIdentifiersWithRecentMarketData();
await this.gatherSymbols({ await this.gatherSymbols({
removeOnComplete, removeOnComplete,
dataGatheringItems: await this.getCurrencies7D(), dataGatheringItems: await this.getCurrencies7D({
assetProfileIdentifiersWithRecentMarketData
}),
priority: DATA_GATHERING_QUEUE_PRIORITY_HIGH priority: DATA_GATHERING_QUEUE_PRIORITY_HIGH
}); });
await this.gatherSymbols({ await this.gatherSymbols({
removeOnComplete, removeOnComplete,
dataGatheringItems: await this.getSymbols7D({ dataGatheringItems: await this.getSymbols7D({
assetProfileIdentifiersWithRecentMarketData,
withUserSubscription: true withUserSubscription: true
}), }),
priority: DATA_GATHERING_QUEUE_PRIORITY_MEDIUM priority: DATA_GATHERING_QUEUE_PRIORITY_MEDIUM
@ -286,6 +285,7 @@ export class DataGatheringService {
await this.gatherSymbols({ await this.gatherSymbols({
removeOnComplete, removeOnComplete,
dataGatheringItems: await this.getSymbols7D({ dataGatheringItems: await this.getSymbols7D({
assetProfileIdentifiersWithRecentMarketData,
withUserSubscription: false withUserSubscription: false
}), }),
priority: DATA_GATHERING_QUEUE_PRIORITY_LOW priority: DATA_GATHERING_QUEUE_PRIORITY_LOW
@ -426,28 +426,24 @@ export class DataGatheringService {
> { > {
return ( return (
await this.prismaService.marketData.groupBy({ await this.prismaService.marketData.groupBy({
_max: { date: true },
by: ['dataSource', 'symbol'], by: ['dataSource', 'symbol'],
orderBy: [{ symbol: 'asc' }], orderBy: [{ symbol: 'asc' }],
where: { where: {
date: { gt: subDays(resetHours(new Date()), 7) }, date: { gte: getStartOfUtcDate(subDays(new Date(), 1)) },
isCarriedForward: false, isCarriedForward: false,
state: 'CLOSE' state: 'CLOSE'
} }
}) })
) ).map(({ dataSource, symbol }) => {
.filter(({ _max }) => { return { dataSource, symbol };
return !isBefore(_max.date, getStartOfUtcDate(subDays(new Date(), 1))); });
})
.map(({ dataSource, symbol }) => {
return { dataSource, symbol };
});
} }
private async getCurrencies7D(): Promise<DataGatheringItem[]> { private async getCurrencies7D({
const assetProfileIdentifiersWithRecentMarketData = assetProfileIdentifiersWithRecentMarketData
await this.getAssetProfileIdentifiersWithRecentMarketData(); }: {
assetProfileIdentifiersWithRecentMarketData: AssetProfileIdentifier[];
}): Promise<DataGatheringItem[]> {
return this.exchangeRateDataService return this.exchangeRateDataService
.getCurrencyPairs() .getCurrencyPairs()
.filter(({ dataSource, symbol }) => { .filter(({ dataSource, symbol }) => {
@ -499,8 +495,10 @@ export class DataGatheringService {
} }
private async getSymbols7D({ private async getSymbols7D({
assetProfileIdentifiersWithRecentMarketData,
withUserSubscription = false withUserSubscription = false
}: { }: {
assetProfileIdentifiersWithRecentMarketData: AssetProfileIdentifier[];
withUserSubscription?: boolean; withUserSubscription?: boolean;
}): Promise<DataGatheringItem[]> { }): Promise<DataGatheringItem[]> {
const symbolProfiles = const symbolProfiles =
@ -510,9 +508,6 @@ export class DataGatheringService {
} }
); );
const assetProfileIdentifiersWithRecentMarketData =
await this.getAssetProfileIdentifiersWithRecentMarketData();
return symbolProfiles return symbolProfiles
.filter(({ dataSource, scraperConfiguration, symbol }) => { .filter(({ dataSource, scraperConfiguration, symbol }) => {
const manualDataSourceWithScraperConfiguration = const manualDataSourceWithScraperConfiguration =

Loading…
Cancel
Save