Browse Source

Merge 604cf03eb9 into 208a2d2e1d

pull/7816/merge
Thomas Kaul 3 days ago
committed by GitHub
parent
commit
9f863aae44
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 11
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  3. 50
      apps/api/src/app/portfolio/current-rate.service.spec.ts
  4. 30
      apps/api/src/app/portfolio/current-rate.service.ts
  5. 2
      apps/api/src/app/portfolio/interfaces/get-values-params.interface.ts

1
CHANGELOG.md

@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Introduced a maximum length for the comment in the API endpoints
- Introduced a maximum length for the search query and the symbol in the API endpoints
- Hardened the validation of the query parameters (`accounts`, `assetClasses`, `dataSource` and `tags`) in the API endpoints with filters
- Improved the portfolio snapshot calculation to get the quotes of active holdings only
### Fixed

11
apps/api/src/app/portfolio/calculator/portfolio-calculator.ts

@ -225,6 +225,7 @@ export abstract class PortfolioCalculator {
};
}
const assetProfileIdentifiersWithQuotes: AssetProfileIdentifier[] = [];
const cashAssetProfileIdentifiers = new Set<string>();
const currencies: { [assetProfileIdentifier: string]: string } = {};
const dataGatheringItems: DataGatheringItem[] = [];
@ -238,6 +239,7 @@ export abstract class PortfolioCalculator {
assetSubClass,
currency,
dataSource,
quantity,
symbol
} of transactionPoints[firstIndex - 1].items) {
// Gather data for all assets except CASH
@ -246,6 +248,14 @@ export abstract class PortfolioCalculator {
dataSource,
symbol
});
if (!quantity.eq(0)) {
// Get a quote for active holdings only
assetProfileIdentifiersWithQuotes.push({
dataSource,
symbol
});
}
}
currencies[getAssetProfileIdentifier({ dataSource, symbol })] = currency;
@ -274,6 +284,7 @@ export abstract class PortfolioCalculator {
errors: currentRateErrors,
values: marketSymbols
} = await this.currentRateService.getValues({
assetProfileIdentifiersWithQuotes,
dataGatheringItems,
dateQuery: {
gte: this.startDate,

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

@ -145,6 +145,9 @@ describe('CurrentRateService', () => {
it('getValues', async () => {
expect(
await currentRateService.getValues({
assetProfileIdentifiersWithQuotes: [
{ dataSource: DataSource.YAHOO, symbol: 'AMZN' }
],
dataGatheringItems: [{ dataSource: DataSource.YAHOO, symbol: 'AMZN' }],
dateQuery: {
lt: new Date(Date.UTC(2020, 0, 2, 0, 0, 0)),
@ -198,6 +201,9 @@ describe('CurrentRateService', () => {
);
const { errors, values } = await currentRateService.getValues({
assetProfileIdentifiersWithQuotes: [
{ dataSource: DataSource.YAHOO, symbol: 'AMZN' }
],
dataGatheringItems: [{ dataSource: DataSource.YAHOO, symbol: 'AMZN' }],
dateQuery: { gte: yesterday, lt: endOfDay(new Date()) }
});
@ -226,6 +232,9 @@ describe('CurrentRateService', () => {
.mockResolvedValue({ unitPrice: 1847.839966 } as Order);
const { values } = await currentRateService.getValues({
assetProfileIdentifiersWithQuotes: [
{ dataSource: DataSource.YAHOO, symbol: 'AMZN' }
],
dataGatheringItems: [{ dataSource: DataSource.YAHOO, symbol: 'AMZN' }],
dateQuery: {
gte: subDays(today, 1, { in: utc }),
@ -248,4 +257,45 @@ describe('CurrentRateService', () => {
}
]);
});
it('getValues without a quote request carries the latest market price forward', async () => {
const today = resetHours(new Date());
const yesterday = subDays(today, 1, { in: utc });
const getQuotesSpy = jest.spyOn(dataProviderService, 'getQuotes');
jest.spyOn(marketDataService, 'getRangeCount').mockResolvedValue(1);
jest.spyOn(marketDataService, 'getRange').mockResolvedValue([
{
createdAt: yesterday,
dataSource: DataSource.YAHOO,
date: yesterday,
id: 'd51d4e0b-9d1f-4d2e-8a0c-9a0f5c6b1d22',
isCarriedForward: false,
marketPrice: 1841.823902,
state: 'CLOSE',
symbol: 'AMZN'
}
]);
const { errors, values } = await currentRateService.getValues({
assetProfileIdentifiersWithQuotes: [],
dataGatheringItems: [{ dataSource: DataSource.YAHOO, symbol: 'AMZN' }],
dateQuery: { gte: yesterday, lt: endOfDay(new Date()) }
});
expect(getQuotesSpy).toHaveBeenCalledWith(
expect.objectContaining({ items: [] })
);
expect(errors).toEqual([]);
expect(values).toContainEqual({
dataSource: DataSource.YAHOO,
date: today,
marketPrice: 1841.823902,
symbol: 'AMZN'
});
});
});

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

@ -18,7 +18,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Type as ActivityType } from '@prisma/client';
import { compareDesc, isBefore, isSameDay } from 'date-fns';
import { isEmpty, uniqBy } from 'lodash';
import { uniqBy } from 'lodash';
import { GetValueObject } from './interfaces/get-value-object.interface';
import { GetValuesObject } from './interfaces/get-values-object.interface';
@ -38,6 +38,7 @@ export class CurrentRateService {
@LogPerformance
// TODO: Pass user instead of using this.request.user
public async getValues({
assetProfileIdentifiersWithQuotes,
dataGatheringItems,
dateQuery
}: GetValuesParams): Promise<GetValuesObject> {
@ -54,11 +55,11 @@ export class CurrentRateService {
if (includesToday) {
const quotes = await this.dataProviderService.getQuotes({
items: dataGatheringItems,
items: assetProfileIdentifiersWithQuotes,
user: this.request?.user
});
for (const { dataSource, symbol } of dataGatheringItems) {
for (const { dataSource, symbol } of assetProfileIdentifiersWithQuotes) {
const quote = quotes[getAssetProfileIdentifier({ dataSource, symbol })];
if (quote?.dataProviderInfo) {
@ -124,8 +125,27 @@ export class CurrentRateService {
})
};
if (!isEmpty(quoteErrors)) {
for (const { dataSource, symbol } of quoteErrors) {
if (includesToday) {
// A holding without a quote request also needs a market price of today,
// but it is not in error
const assetProfileIdentifiersWithoutQuotes = [
...quoteErrors,
...dataGatheringItems.filter(({ dataSource, symbol }) => {
return !assetProfileIdentifiersWithQuotes.some(
(assetProfileIdentifier) => {
return (
assetProfileIdentifier.dataSource === dataSource &&
assetProfileIdentifier.symbol === symbol
);
}
);
})
];
for (const {
dataSource,
symbol
} of assetProfileIdentifiersWithoutQuotes) {
try {
const valueOfToday = response.values.find((currentValue) => {
return (

2
apps/api/src/app/portfolio/interfaces/get-values-params.interface.ts

@ -1,8 +1,10 @@
import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import { DateQuery } from './date-query.interface';
export interface GetValuesParams {
assetProfileIdentifiersWithQuotes: AssetProfileIdentifier[];
dataGatheringItems: DataGatheringItem[];
dateQuery: DateQuery;
}

Loading…
Cancel
Save