Browse Source

Fix portfolio current value responses

pull/7439/head
Daniel 1 month ago
parent
commit
549d71c693
  1. 4
      apps/api/src/app/portfolio/portfolio.controller.ts
  2. 125
      apps/api/src/app/portfolio/portfolio.service.spec.ts
  3. 24
      apps/api/src/app/portfolio/portfolio.service.ts
  4. 1
      libs/common/src/lib/interfaces/responses/portfolio-holdings-response.interface.ts

4
apps/api/src/app/portfolio/portfolio.controller.ts

@ -446,14 +446,12 @@ export class PortfolioController {
filterByTags
});
const holdings = await this.portfolioService.getHoldings({
return this.portfolioService.getHoldings({
dateRange,
filters,
impersonationId,
userId: this.request.user.id
});
return { holdings };
}
@Get('investments')

125
apps/api/src/app/portfolio/portfolio.service.spec.ts

@ -1,3 +1,4 @@
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details.interface';
import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.service';
@ -20,6 +21,7 @@ import { randomUUID } from 'node:crypto';
import { PortfolioService } from './portfolio.service';
describe('PortfolioService', () => {
let accountBalanceService: AccountBalanceService;
let accountService: AccountService;
let activitiesService: ActivitiesService;
let configurationService: ConfigurationService;
@ -50,6 +52,12 @@ describe('PortfolioService', () => {
null
);
accountBalanceService = new AccountBalanceService(
null,
exchangeRateDataService,
null
);
accountService = new AccountService(
null,
null,
@ -96,7 +104,7 @@ describe('PortfolioService', () => {
);
portfolioService = new PortfolioService(
null,
accountBalanceService,
accountService,
activitiesService,
null,
@ -337,6 +345,121 @@ describe('PortfolioService', () => {
});
});
describe('getHoldings', () => {
it('should return totalValueInBaseCurrency as the sum of returned holdings', async () => {
jest
.spyOn(impersonationService, 'validateImpersonationId')
.mockResolvedValue(null);
jest.spyOn(portfolioService, 'getDetails').mockResolvedValue({
accounts: {},
createdAt: parseDate('2024-01-01'),
hasErrors: false,
holdings: {
AMZN: {
assetProfile: { name: 'Amazon.com, Inc.', symbol: 'AMZN' },
valueInBaseCurrency: 5298.2
},
MSFT: {
assetProfile: { name: 'Microsoft Corporation', symbol: 'MSFT' },
valueInBaseCurrency: 1700.15
}
},
platforms: {}
} as unknown as Awaited<ReturnType<typeof portfolioService.getDetails>>);
const response = await portfolioService.getHoldings({
dateRange: 'max',
filters: [],
impersonationId: userDummyData.id,
userId: userDummyData.id
});
expect(response.holdings).toHaveLength(2);
expect(response.totalValueInBaseCurrency).toBeCloseTo(6998.35);
});
});
describe('getPerformance', () => {
it('should use the snapshot currentValueInBaseCurrency for the current performance value', async () => {
jest
.spyOn(accountBalanceService, 'getAccountBalanceItems')
.mockResolvedValue([
{
date: '2024-01-01',
value: 0
}
]);
jest
.spyOn(activitiesService, 'getActivitiesForPortfolioCalculator')
.mockResolvedValue({ activities: [], count: 0 });
jest
.spyOn(impersonationService, 'validateImpersonationId')
.mockResolvedValue(null);
jest.spyOn(userService, 'user').mockResolvedValue({
accessesGet: [],
accounts: [],
activityCount: 0,
dataProviderGhostfolioDailyRequests: 0,
id: userDummyData.id,
settings: {
settings: {
baseCurrency: 'USD'
}
}
} as unknown as Awaited<ReturnType<typeof userService.user>>);
jest
.spyOn(portfolioCalculatorFactory, 'createCalculator')
.mockReturnValue({
getPerformance: jest.fn().mockResolvedValue({
chart: [
{
date: '2024-01-01',
netPerformance: 0,
netPerformanceInPercentage: 0,
netPerformanceInPercentageWithCurrencyEffect: 0,
netPerformanceWithCurrencyEffect: 0,
netWorth: 0,
totalInvestment: 0,
totalInvestmentValueWithCurrencyEffect: 0,
valueWithCurrencyEffect: 0
}
]
}),
getSnapshot: jest.fn().mockResolvedValue({
createdAt: parseDate('2024-01-01'),
currentValueInBaseCurrency: new Big(231216.02329207),
errors: [],
hasErrors: false,
historicalData: [{ date: '2024-01-01' }],
positions: [],
totalFeesWithCurrencyEffect: new Big(0),
totalInterestWithCurrencyEffect: new Big(0),
totalInvestment: new Big(0),
totalInvestmentWithCurrencyEffect: new Big(0),
totalLiabilitiesWithCurrencyEffect: new Big(0)
})
} as unknown as ReturnType<
typeof portfolioCalculatorFactory.createCalculator
>);
const response = await portfolioService.getPerformance({
dateRange: 'max',
filters: [],
impersonationId: userDummyData.id,
userId: userDummyData.id
});
expect(response.performance.currentValueInBaseCurrency).toBe(
231216.02329207
);
});
});
describe('getValueOfAccountsAndPlatforms', () => {
const getValueOfAccountsAndPlatforms = (args: object) => {
return (

24
apps/api/src/app/portfolio/portfolio.service.ts

@ -54,6 +54,7 @@ import {
InvestmentItem,
PortfolioDetails,
PortfolioHoldingResponse,
PortfolioHoldingsResponse,
PortfolioInvestmentsResponse,
PortfolioPerformanceResponse,
PortfolioPosition,
@ -367,7 +368,7 @@ export class PortfolioService {
filters?: Filter[];
impersonationId: string;
userId: string;
}) {
}): Promise<PortfolioHoldingsResponse> {
userId = await this.getUserId(impersonationId, userId);
const { holdings: holdingsMap } = await this.getDetails({
dateRange,
@ -396,7 +397,14 @@ export class PortfolioService {
});
}
return holdings;
return {
holdings,
totalValueInBaseCurrency: getSum(
holdings.map(({ valueInBaseCurrency }) => {
return new Big(valueInBaseCurrency ?? 0);
})
).toNumber()
};
}
public async getInvestments({
@ -1059,7 +1067,12 @@ export class PortfolioService {
currency: userCurrency
});
const { errors, hasErrors, historicalData } =
const {
currentValueInBaseCurrency,
errors,
hasErrors,
historicalData
} =
await portfolioCalculator.getSnapshot();
const { endDate, startDate } = getIntervalFromDateRange({ dateRange });
@ -1077,7 +1090,6 @@ export class PortfolioService {
netWorth,
totalInvestment,
totalInvestmentValueWithCurrencyEffect,
valueWithCurrencyEffect
} = chart?.at(-1) ?? {
netPerformance: 0,
netPerformanceInPercentage: 0,
@ -1085,7 +1097,7 @@ export class PortfolioService {
netPerformanceWithCurrencyEffect: 0,
netWorth: 0,
totalInvestment: 0,
valueWithCurrencyEffect: 0
totalInvestmentValueWithCurrencyEffect: 0
};
return {
@ -1100,7 +1112,7 @@ export class PortfolioService {
totalInvestment,
totalInvestmentValueWithCurrencyEffect,
currentNetWorth: netWorth,
currentValueInBaseCurrency: valueWithCurrencyEffect,
currentValueInBaseCurrency: currentValueInBaseCurrency.toNumber(),
netPerformancePercentage: netPerformanceInPercentage,
netPerformancePercentageWithCurrencyEffect:
netPerformanceInPercentageWithCurrencyEffect

1
libs/common/src/lib/interfaces/responses/portfolio-holdings-response.interface.ts

@ -2,4 +2,5 @@ import { PortfolioPosition } from '@ghostfolio/common/interfaces';
export interface PortfolioHoldingsResponse {
holdings: PortfolioPosition[];
totalValueInBaseCurrency: number;
}

Loading…
Cancel
Save