Browse Source

Fix market price fallback

pull/7813/head
Thomas Kaul 1 week ago
parent
commit
2301276108
  1. 43
      apps/api/src/app/activities/activities.service.spec.ts
  2. 1
      apps/api/src/app/activities/activities.service.ts
  3. 47
      apps/api/src/app/portfolio/current-rate.service.spec.ts
  4. 84
      apps/api/src/app/portfolio/current-rate.service.ts

43
apps/api/src/app/activities/activities.service.spec.ts

@ -3,6 +3,7 @@ import {
activityDummyData,
assetProfileDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { WHERE_ACTIVITY_NOT_DRAFT } from '@ghostfolio/api/helper/activity.helper';
import { AssetProfileSplitService } from '@ghostfolio/api/services/asset-profile-split/asset-profile-split.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
@ -14,7 +15,11 @@ import {
import { parseDate } from '@ghostfolio/common/helper';
import { Activity, Filter } from '@ghostfolio/common/interfaces';
import { AssetProfileSplit, DataSource } from '@prisma/client';
import {
AssetProfileSplit,
DataSource,
Type as ActivityType
} from '@prisma/client';
import { Big } from 'big.js';
import { ActivitiesService } from './activities.service';
@ -381,6 +386,42 @@ describe('ActivitiesService', () => {
return result.activities[0];
}
});
describe('getLatestActivity', () => {
it('filters by type and excludes draft activities', async () => {
const findFirst = jest.fn().mockResolvedValue(null);
const service = new ActivitiesService(
null,
null,
null,
null,
null,
null,
null,
null,
null,
{ order: { findFirst } } as unknown as PrismaService,
null,
null
);
await service.getLatestActivity({
dataSource: DataSource.YAHOO,
symbol: 'AAPL',
types: [ActivityType.BUY, ActivityType.SELL]
});
expect(findFirst).toHaveBeenCalledWith({
orderBy: { date: 'desc' },
where: {
...WHERE_ACTIVITY_NOT_DRAFT,
SymbolProfile: { dataSource: DataSource.YAHOO, symbol: 'AAPL' },
type: { in: [ActivityType.BUY, ActivityType.SELL] }
}
});
});
});
});
function createActivity({

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

@ -589,6 +589,7 @@ export class ActivitiesService {
date: 'desc'
},
where: {
...WHERE_ACTIVITY_NOT_DRAFT,
SymbolProfile: { dataSource, symbol },
...(types?.length > 0 && { type: { in: types } })
}

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

@ -192,53 +192,6 @@ 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')

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

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

Loading…
Cancel
Save