Browse Source

Merge 549d71c693 into 543818c358

pull/7439/merge
Daniel 1 month ago
committed by GitHub
parent
commit
4eedff3940
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  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

@ -448,14 +448,12 @@ export class PortfolioController {
filterByTags filterByTags
}); });
const holdings = await this.portfolioService.getHoldings({ return this.portfolioService.getHoldings({
dateRange, dateRange,
filters, filters,
impersonationId, impersonationId,
userId: this.request.user.id userId: this.request.user.id
}); });
return { holdings };
} }
@Get('investments') @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 { AccountService } from '@ghostfolio/api/app/account/account.service';
import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details.interface'; import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details.interface';
import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.service'; import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.service';
@ -20,6 +21,7 @@ import { randomUUID } from 'node:crypto';
import { PortfolioService } from './portfolio.service'; import { PortfolioService } from './portfolio.service';
describe('PortfolioService', () => { describe('PortfolioService', () => {
let accountBalanceService: AccountBalanceService;
let accountService: AccountService; let accountService: AccountService;
let activitiesService: ActivitiesService; let activitiesService: ActivitiesService;
let configurationService: ConfigurationService; let configurationService: ConfigurationService;
@ -50,6 +52,12 @@ describe('PortfolioService', () => {
null null
); );
accountBalanceService = new AccountBalanceService(
null,
exchangeRateDataService,
null
);
accountService = new AccountService( accountService = new AccountService(
null, null,
null, null,
@ -96,7 +104,7 @@ describe('PortfolioService', () => {
); );
portfolioService = new PortfolioService( portfolioService = new PortfolioService(
null, accountBalanceService,
accountService, accountService,
activitiesService, activitiesService,
null, 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', () => { describe('getValueOfAccountsAndPlatforms', () => {
const getValueOfAccountsAndPlatforms = (args: object) => { const getValueOfAccountsAndPlatforms = (args: object) => {
return ( return (

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

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

Loading…
Cancel
Save