mirror of https://github.com/ghostfolio/ghostfolio
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
266 lines
6.5 KiB
266 lines
6.5 KiB
import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
|
import {
|
|
getAssetProfileIdentifier,
|
|
parseDate
|
|
} from '@ghostfolio/common/helper';
|
|
|
|
import { DataSource } from '@prisma/client';
|
|
import { Job } from 'bull';
|
|
|
|
import { DataGatheringProcessor } from './data-gathering.processor';
|
|
|
|
describe('DataGatheringProcessor', () => {
|
|
let dataGatheringProcessor: DataGatheringProcessor;
|
|
let dataProviderService: { getHistoricalRaw: jest.Mock };
|
|
let marketDataService: { replaceForSymbol: jest.Mock; updateMany: jest.Mock };
|
|
|
|
const createJob = ({
|
|
dataSource,
|
|
date,
|
|
symbol
|
|
}: {
|
|
dataSource: DataSource;
|
|
date: string;
|
|
symbol: string;
|
|
}) => {
|
|
return {
|
|
data: {
|
|
dataSource,
|
|
symbol,
|
|
date: parseDate(date).toISOString()
|
|
}
|
|
} as unknown as Job<DataGatheringItem>;
|
|
};
|
|
|
|
const mockHistoricalData = ({
|
|
dataSource,
|
|
prices,
|
|
symbol
|
|
}: {
|
|
dataSource: DataSource;
|
|
prices: { [date: string]: number };
|
|
symbol: string;
|
|
}) => {
|
|
const assetProfileIdentifier = getAssetProfileIdentifier({
|
|
dataSource,
|
|
symbol
|
|
});
|
|
|
|
const historicalData: {
|
|
[symbol: string]: { [date: string]: { marketPrice: number } };
|
|
} = { [assetProfileIdentifier]: {} };
|
|
|
|
for (const [date, marketPrice] of Object.entries(prices)) {
|
|
historicalData[assetProfileIdentifier][date] = { marketPrice };
|
|
}
|
|
|
|
dataProviderService.getHistoricalRaw.mockResolvedValue(historicalData);
|
|
};
|
|
|
|
beforeAll(() => {
|
|
jest.useFakeTimers().setSystemTime(parseDate('2026-08-24').getTime());
|
|
});
|
|
|
|
beforeEach(() => {
|
|
dataProviderService = { getHistoricalRaw: jest.fn() };
|
|
marketDataService = {
|
|
replaceForSymbol: jest.fn(),
|
|
updateMany: jest.fn()
|
|
};
|
|
|
|
dataGatheringProcessor = new DataGatheringProcessor(
|
|
null,
|
|
dataProviderService as any,
|
|
marketDataService as any,
|
|
null
|
|
);
|
|
});
|
|
|
|
afterAll(() => {
|
|
jest.useRealTimers();
|
|
});
|
|
|
|
it('writes an all-real series without carried forward market prices', async () => {
|
|
mockHistoricalData({
|
|
dataSource: 'COINGECKO',
|
|
symbol: 'bitcoin',
|
|
prices: {
|
|
'2026-08-17': 1,
|
|
'2026-08-18': 2,
|
|
'2026-08-19': 3,
|
|
'2026-08-20': 4,
|
|
'2026-08-21': 5,
|
|
'2026-08-22': 6,
|
|
'2026-08-23': 7
|
|
}
|
|
});
|
|
|
|
await dataGatheringProcessor.gatherHistoricalMarketData(
|
|
createJob({
|
|
dataSource: 'COINGECKO',
|
|
date: '2026-08-17',
|
|
symbol: 'bitcoin'
|
|
})
|
|
);
|
|
|
|
const { data } = marketDataService.updateMany.mock.calls[0][0];
|
|
|
|
expect(data).toHaveLength(7);
|
|
expect(
|
|
data.every(({ isCarriedForward }) => {
|
|
return isCarriedForward === false;
|
|
})
|
|
).toBe(true);
|
|
});
|
|
|
|
it('fills an interior gap with carried forward market prices', async () => {
|
|
mockHistoricalData({
|
|
dataSource: 'YAHOO',
|
|
symbol: 'AAPL',
|
|
prices: {
|
|
'2026-08-17': 1,
|
|
'2026-08-18': 2,
|
|
'2026-08-21': 5,
|
|
'2026-08-22': 6,
|
|
'2026-08-23': 7
|
|
}
|
|
});
|
|
|
|
await dataGatheringProcessor.gatherHistoricalMarketData(
|
|
createJob({ dataSource: 'YAHOO', date: '2026-08-17', symbol: 'AAPL' })
|
|
);
|
|
|
|
const { data } = marketDataService.updateMany.mock.calls[0][0];
|
|
|
|
expect(data).toEqual([
|
|
expect.objectContaining({
|
|
date: parseDate('2026-08-17'),
|
|
isCarriedForward: false,
|
|
marketPrice: 1
|
|
}),
|
|
expect.objectContaining({
|
|
date: parseDate('2026-08-18'),
|
|
isCarriedForward: false,
|
|
marketPrice: 2
|
|
}),
|
|
expect.objectContaining({
|
|
date: parseDate('2026-08-19'),
|
|
isCarriedForward: true,
|
|
marketPrice: 2
|
|
}),
|
|
expect.objectContaining({
|
|
date: parseDate('2026-08-20'),
|
|
isCarriedForward: true,
|
|
marketPrice: 2
|
|
}),
|
|
expect.objectContaining({
|
|
date: parseDate('2026-08-21'),
|
|
isCarriedForward: false,
|
|
marketPrice: 5
|
|
}),
|
|
expect.objectContaining({
|
|
date: parseDate('2026-08-22'),
|
|
isCarriedForward: false,
|
|
marketPrice: 6
|
|
}),
|
|
expect.objectContaining({
|
|
date: parseDate('2026-08-23'),
|
|
isCarriedForward: false,
|
|
marketPrice: 7
|
|
})
|
|
]);
|
|
});
|
|
|
|
it('fills a trailing gap with carried forward market prices', async () => {
|
|
mockHistoricalData({
|
|
dataSource: 'YAHOO',
|
|
symbol: 'AAPL',
|
|
prices: {
|
|
'2026-08-17': 1,
|
|
'2026-08-18': 2,
|
|
'2026-08-19': 3,
|
|
'2026-08-20': 4,
|
|
'2026-08-21': 5
|
|
}
|
|
});
|
|
|
|
await dataGatheringProcessor.gatherHistoricalMarketData(
|
|
createJob({ dataSource: 'YAHOO', date: '2026-08-17', symbol: 'AAPL' })
|
|
);
|
|
|
|
const { data } = marketDataService.updateMany.mock.calls[0][0];
|
|
|
|
expect(data).toHaveLength(7);
|
|
expect(data.slice(5)).toEqual([
|
|
expect.objectContaining({
|
|
date: parseDate('2026-08-22'),
|
|
isCarriedForward: true,
|
|
marketPrice: 5
|
|
}),
|
|
expect.objectContaining({
|
|
date: parseDate('2026-08-23'),
|
|
isCarriedForward: true,
|
|
marketPrice: 5
|
|
})
|
|
]);
|
|
});
|
|
|
|
it('does not fill a leading gap', async () => {
|
|
mockHistoricalData({
|
|
dataSource: 'YAHOO',
|
|
symbol: 'AAPL',
|
|
prices: {
|
|
'2026-08-19': 3,
|
|
'2026-08-20': 4,
|
|
'2026-08-21': 5,
|
|
'2026-08-22': 6,
|
|
'2026-08-23': 7
|
|
}
|
|
});
|
|
|
|
await dataGatheringProcessor.gatherHistoricalMarketData(
|
|
createJob({ dataSource: 'YAHOO', date: '2026-08-17', symbol: 'AAPL' })
|
|
);
|
|
|
|
const { data } = marketDataService.updateMany.mock.calls[0][0];
|
|
|
|
expect(data).toHaveLength(5);
|
|
expect(data[0]).toEqual(
|
|
expect.objectContaining({
|
|
date: parseDate('2026-08-19'),
|
|
isCarriedForward: false,
|
|
marketPrice: 3
|
|
})
|
|
);
|
|
});
|
|
|
|
it('labels a market price of 0 from the data provider as carried forward', async () => {
|
|
mockHistoricalData({
|
|
dataSource: 'YAHOO',
|
|
symbol: 'AAPL',
|
|
prices: {
|
|
'2026-08-17': 1,
|
|
'2026-08-18': 2,
|
|
'2026-08-19': 0,
|
|
'2026-08-20': 4,
|
|
'2026-08-21': 5,
|
|
'2026-08-22': 6,
|
|
'2026-08-23': 7
|
|
}
|
|
});
|
|
|
|
await dataGatheringProcessor.gatherHistoricalMarketData(
|
|
createJob({ dataSource: 'YAHOO', date: '2026-08-17', symbol: 'AAPL' })
|
|
);
|
|
|
|
const { data } = marketDataService.updateMany.mock.calls[0][0];
|
|
|
|
expect(data[2]).toEqual(
|
|
expect.objectContaining({
|
|
date: parseDate('2026-08-19'),
|
|
isCarriedForward: true,
|
|
marketPrice: 2
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|