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. 68
      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, activityDummyData,
assetProfileDummyData assetProfileDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } 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 { 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 { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
@ -14,7 +15,11 @@ import {
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
import { Activity, Filter } from '@ghostfolio/common/interfaces'; 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 { Big } from 'big.js';
import { ActivitiesService } from './activities.service'; import { ActivitiesService } from './activities.service';
@ -381,6 +386,42 @@ describe('ActivitiesService', () => {
return result.activities[0]; 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({ function createActivity({

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

@ -589,6 +589,7 @@ export class ActivitiesService {
date: 'desc' date: 'desc'
}, },
where: { where: {
...WHERE_ACTIVITY_NOT_DRAFT,
SymbolProfile: { dataSource, symbol }, SymbolProfile: { dataSource, symbol },
...(types?.length > 0 && { type: { in: types } }) ...(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(); 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 () => { it('should fall back to the latest market price', async () => {
const getLatestActivity = jest const getLatestActivity = jest
.spyOn(activitiesService, 'getLatestActivity') .spyOn(activitiesService, 'getLatestActivity')

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

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

Loading…
Cancel
Save