Browse Source

Fix market price fallback

bugfix/market-price-fallback
Thomas Kaul 1 month ago
parent
commit
1d640f83c3
  1. 8
      apps/api/src/app/activities/activities.service.ts
  2. 63
      apps/api/src/app/portfolio/current-rate.service.spec.ts
  3. 84
      apps/api/src/app/portfolio/current-rate.service.ts

8
apps/api/src/app/activities/activities.service.ts

@ -581,14 +581,16 @@ export class ActivitiesService {
public async getLatestActivity({
dataSource,
symbol
}: AssetProfileIdentifier) {
symbol,
types
}: AssetProfileIdentifier & { types?: ActivityType[] }) {
return this.prismaService.order.findFirst({
orderBy: {
date: 'desc'
},
where: {
SymbolProfile: { dataSource, symbol }
SymbolProfile: { dataSource, symbol },
...(types?.length > 0 && { type: { in: types } })
}
});
}

63
apps/api/src/app/portfolio/current-rate.service.spec.ts

@ -5,7 +5,12 @@ import { PropertyService } from '@ghostfolio/api/services/property/property.serv
import { resetHours } from '@ghostfolio/common/helper';
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import { DataSource, MarketData, Order } from '@prisma/client';
import {
DataSource,
MarketData,
Order,
Type as ActivityType
} from '@prisma/client';
import { addDays, subDays } from 'date-fns';
import { CurrentRateService } from './current-rate.service';
@ -187,6 +192,53 @@ describe('CurrentRateService', () => {
jest.restoreAllMocks();
});
it('should fall back to the latest market price in the date range', async () => {
const getLatestActivity = jest.spyOn(
activitiesService,
'getLatestActivity'
);
const getLatestMarketData = jest.spyOn(marketDataService, 'getLatest');
jest.spyOn(marketDataService, 'getRangeCount').mockResolvedValue(1);
jest.spyOn(marketDataService, 'getRange').mockResolvedValue([
{
createdAt: yesterday,
dataSource: DataSource.YAHOO,
date: yesterday,
id: '3b1a3f4c-3d2b-4a19-9f5a-5c0f5b4a2e11',
isCarriedForward: false,
marketPrice: 1841.823902,
state: 'CLOSE',
symbol: 'AMZN'
}
]);
const { errors, values } = await currentRateService.getValues({
dataGatheringItems,
dateQuery
});
expect(getLatestActivity).not.toHaveBeenCalled();
expect(getLatestMarketData).not.toHaveBeenCalled();
expect(errors).toEqual(dataGatheringItems);
expect(values).toEqual([
{
dataSource: DataSource.YAHOO,
date: yesterday,
marketPrice: 1841.823902,
symbol: 'AMZN'
},
{
dataSource: DataSource.YAHOO,
date: today,
marketPrice: 1841.823902,
symbol: 'AMZN'
}
]);
});
it('should fall back to the latest market price', async () => {
const getLatestActivity = jest
.spyOn(activitiesService, 'getLatestActivity')
@ -220,8 +272,8 @@ describe('CurrentRateService', () => {
]);
});
it('should fall back to the unit price of the latest activity without market data', async () => {
jest
it('should fall back to the unit price of the latest buy or sell activity without market data', async () => {
const getLatestActivity = jest
.spyOn(activitiesService, 'getLatestActivity')
.mockResolvedValue({ unitPrice: 1000 } as Order);
@ -232,6 +284,11 @@ describe('CurrentRateService', () => {
dateQuery
});
expect(getLatestActivity).toHaveBeenCalledWith({
dataSource: DataSource.YAHOO,
symbol: 'AMZN',
types: [ActivityType.BUY, ActivityType.SELL]
});
expect(errors).toEqual(dataGatheringItems);
expect(values).toEqual([
{

84
apps/api/src/app/portfolio/current-rate.service.ts

@ -12,8 +12,10 @@ import {
ResponseError
} from '@ghostfolio/common/interfaces';
import { utc } from '@date-fns/utc';
import { Injectable } from '@nestjs/common';
import { isBefore, isToday } from 'date-fns';
import { Type as ActivityType } from '@prisma/client';
import { compareDesc, isBefore, isSameDay } from 'date-fns';
import { isEmpty, uniqBy } from 'lodash-es';
import { GetValueObject } from './interfaces/get-value-object.interface';
@ -122,44 +124,19 @@ export class CurrentRateService {
if (!isEmpty(quoteErrors)) {
for (const { dataSource, symbol } of quoteErrors) {
try {
// If missing quote, fallback to the latest available historical market price
let value: GetValueObject = response.values.find((currentValue) => {
const valueOfToday = response.values.find((currentValue) => {
return (
currentValue.dataSource === dataSource &&
currentValue.symbol === symbol &&
isToday(currentValue.date)
isSameDay(currentValue.date, today, { in: utc })
);
});
if (!value) {
const latestMarketData = await this.marketDataService.getLatest({
dataSource,
symbol
});
let marketPrice = latestMarketData?.marketPrice;
if (!marketPrice) {
// Fallback to unit price of latest activity
const latestActivity =
await this.activitiesService.getLatestActivity({
dataSource,
symbol
});
marketPrice = latestActivity?.unitPrice ?? 0;
}
value = {
dataSource,
marketPrice,
symbol,
date: today
};
response.values.push(value);
if (valueOfToday?.marketPrice) {
continue;
}
// If missing quote, fallback to the latest available historical market price
const [latestValue] = response.values
.filter((currentValue) => {
return (
@ -169,18 +146,43 @@ export class CurrentRateService {
);
})
.sort((a, b) => {
if (a.date < b.date) {
return 1;
}
return compareDesc(a.date, b.date);
});
if (a.date > b.date) {
return -1;
}
let marketPrice = latestValue?.marketPrice;
return 0;
if (!marketPrice) {
// Fallback to latest market price outside of the date range
const latestMarketData = await this.marketDataService.getLatest({
dataSource,
symbol
});
value.marketPrice = latestValue.marketPrice;
marketPrice = latestMarketData?.marketPrice;
}
if (!marketPrice) {
// Fallback to unit price of latest buy or sell activity
const latestActivity =
await this.activitiesService.getLatestActivity({
dataSource,
symbol,
types: [ActivityType.BUY, ActivityType.SELL]
});
marketPrice = latestActivity?.unitPrice ?? 0;
}
if (valueOfToday) {
valueOfToday.marketPrice = marketPrice;
} else {
response.values.push({
dataSource,
marketPrice,
symbol,
date: today
});
}
} catch {}
}
}
@ -189,8 +191,10 @@ export class CurrentRateService {
}
private containsToday(dates: Date[]): boolean {
const today = resetHours(new Date());
for (const date of dates) {
if (isToday(date)) {
if (isSameDay(date, today, { in: utc })) {
return true;
}
}

Loading…
Cancel
Save