|
|
@ -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'; |
|
|
@ -26,11 +27,13 @@ import { AccountWithBalance } from '@ghostfolio/common/types'; |
|
|
|
|
|
|
|
|
import { AssetClass, DataSource, Prisma } from '@prisma/client'; |
|
|
import { AssetClass, DataSource, Prisma } from '@prisma/client'; |
|
|
import { Big } from 'big.js'; |
|
|
import { Big } from 'big.js'; |
|
|
|
|
|
import { endOfDay } from 'date-fns'; |
|
|
import { randomUUID } from 'node:crypto'; |
|
|
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; |
|
|
@ -61,6 +64,12 @@ describe('PortfolioService', () => { |
|
|
null |
|
|
null |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
accountBalanceService = new AccountBalanceService( |
|
|
|
|
|
null, |
|
|
|
|
|
exchangeRateDataService, |
|
|
|
|
|
null |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
accountService = new AccountService( |
|
|
accountService = new AccountService( |
|
|
null, |
|
|
null, |
|
|
null, |
|
|
null, |
|
|
@ -113,7 +122,7 @@ describe('PortfolioService', () => { |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
portfolioService = new PortfolioService( |
|
|
portfolioService = new PortfolioService( |
|
|
null, |
|
|
accountBalanceService, |
|
|
accountService, |
|
|
accountService, |
|
|
activitiesService, |
|
|
activitiesService, |
|
|
null, |
|
|
null, |
|
|
@ -833,6 +842,170 @@ describe('PortfolioService', () => { |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
describe('getPerformance', () => { |
|
|
|
|
|
let getPerformanceOfCalculator: jest.Mock; |
|
|
|
|
|
|
|
|
|
|
|
beforeEach(() => { |
|
|
|
|
|
jest.useFakeTimers().setSystemTime(parseDate('2025-06-15').getTime()); |
|
|
|
|
|
|
|
|
|
|
|
getPerformanceOfCalculator = jest.fn(); |
|
|
|
|
|
|
|
|
|
|
|
jest |
|
|
|
|
|
.spyOn(accountBalanceService, 'getAccountBalanceItems') |
|
|
|
|
|
.mockResolvedValue([]); |
|
|
|
|
|
|
|
|
|
|
|
jest |
|
|
|
|
|
.spyOn(activitiesService, 'getActivitiesForPortfolioCalculator') |
|
|
|
|
|
.mockResolvedValue({ activities: [{} as Activity], count: 1 }); |
|
|
|
|
|
|
|
|
|
|
|
jest |
|
|
|
|
|
.spyOn(portfolioCalculatorFactory, 'createCalculator') |
|
|
|
|
|
.mockReturnValue({ |
|
|
|
|
|
getPerformance: getPerformanceOfCalculator, |
|
|
|
|
|
getSnapshot: jest.fn().mockResolvedValue({ |
|
|
|
|
|
errors: [], |
|
|
|
|
|
hasErrors: false, |
|
|
|
|
|
historicalData: [{ date: '2023-03-01' }] |
|
|
|
|
|
}) |
|
|
|
|
|
} as unknown as PortfolioCalculator); |
|
|
|
|
|
|
|
|
|
|
|
jest.spyOn(userService, 'user').mockResolvedValue({ |
|
|
|
|
|
id: userDummyData.id, |
|
|
|
|
|
settings: { |
|
|
|
|
|
settings: { |
|
|
|
|
|
baseCurrency: 'CHF' |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} as unknown as Awaited<ReturnType<typeof userService.user>>); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
afterEach(() => { |
|
|
|
|
|
jest.useRealTimers(); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
it('should return the chart of the date range without a group', async () => { |
|
|
|
|
|
getPerformanceOfCalculator.mockResolvedValueOnce({ |
|
|
|
|
|
chart: [ |
|
|
|
|
|
{ date: '2023-03-01', netPerformance: 0 }, |
|
|
|
|
|
{ date: '2025-06-15', netPerformance: 600 } |
|
|
|
|
|
] |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
const { chart, dateOfFirstActivity, performance } = |
|
|
|
|
|
await portfolioService.getPerformance({ |
|
|
|
|
|
dateRange: 'max', |
|
|
|
|
|
userId: userDummyData.id |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
expect(getPerformanceOfCalculator).toHaveBeenCalledTimes(1); |
|
|
|
|
|
expect(getPerformanceOfCalculator).toHaveBeenCalledWith({ |
|
|
|
|
|
end: endOfDay(parseDate('2025-06-15')), |
|
|
|
|
|
start: new Date(0) |
|
|
|
|
|
}); |
|
|
|
|
|
expect(chart).toEqual([ |
|
|
|
|
|
{ date: '2023-03-01', netPerformance: 0 }, |
|
|
|
|
|
{ date: '2025-06-15', netPerformance: 600 } |
|
|
|
|
|
]); |
|
|
|
|
|
expect(dateOfFirstActivity).toEqual(parseDate('2023-03-01')); |
|
|
|
|
|
expect(performance.netPerformance).toBe(600); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
it('should return one chart item per calendar year since the first activity when grouped by year', async () => { |
|
|
|
|
|
getPerformanceOfCalculator |
|
|
|
|
|
.mockResolvedValueOnce({ |
|
|
|
|
|
chart: [ |
|
|
|
|
|
{ date: '2023-03-01', netPerformance: 0 }, |
|
|
|
|
|
{ date: '2023-12-31', netPerformance: 100 }, |
|
|
|
|
|
{ date: '2024-12-31', netPerformance: 300 }, |
|
|
|
|
|
{ date: '2025-06-15', netPerformance: 600 } |
|
|
|
|
|
] |
|
|
|
|
|
}) |
|
|
|
|
|
.mockResolvedValueOnce({ |
|
|
|
|
|
chart: [ |
|
|
|
|
|
{ date: '2023-03-01', netPerformance: 0 }, |
|
|
|
|
|
{ date: '2023-12-31', netPerformance: 100 } |
|
|
|
|
|
] |
|
|
|
|
|
}) |
|
|
|
|
|
.mockResolvedValueOnce({ |
|
|
|
|
|
chart: [ |
|
|
|
|
|
{ date: '2023-12-31', netPerformance: 0 }, |
|
|
|
|
|
{ date: '2024-12-31', netPerformance: 200 } |
|
|
|
|
|
] |
|
|
|
|
|
}) |
|
|
|
|
|
.mockResolvedValueOnce({ |
|
|
|
|
|
chart: [ |
|
|
|
|
|
{ date: '2024-12-31', netPerformance: 0 }, |
|
|
|
|
|
{ date: '2025-06-15', netPerformance: 300 } |
|
|
|
|
|
] |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
const { chart, performance } = await portfolioService.getPerformance({ |
|
|
|
|
|
dateRange: 'max', |
|
|
|
|
|
groupBy: 'year', |
|
|
|
|
|
userId: userDummyData.id |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// A year runs from the end of 31 December of the previous year, like
|
|
|
|
|
|
// the date range of the year. The first year is clipped to the date
|
|
|
|
|
|
// range, and the current year ends today.
|
|
|
|
|
|
expect(getPerformanceOfCalculator).toHaveBeenCalledTimes(4); |
|
|
|
|
|
expect(getPerformanceOfCalculator).toHaveBeenNthCalledWith(2, { |
|
|
|
|
|
end: endOfDay(parseDate('2023-12-31')), |
|
|
|
|
|
start: endOfDay(parseDate('2022-12-31')) |
|
|
|
|
|
}); |
|
|
|
|
|
expect(getPerformanceOfCalculator).toHaveBeenNthCalledWith(3, { |
|
|
|
|
|
end: endOfDay(parseDate('2024-12-31')), |
|
|
|
|
|
start: endOfDay(parseDate('2023-12-31')) |
|
|
|
|
|
}); |
|
|
|
|
|
expect(getPerformanceOfCalculator).toHaveBeenNthCalledWith(4, { |
|
|
|
|
|
end: endOfDay(parseDate('2025-06-15')), |
|
|
|
|
|
start: endOfDay(parseDate('2024-12-31')) |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// Each year stands alone and is dated on 1 January
|
|
|
|
|
|
expect(chart).toEqual([ |
|
|
|
|
|
{ date: '2023-01-01', netPerformance: 100 }, |
|
|
|
|
|
{ date: '2024-01-01', netPerformance: 200 }, |
|
|
|
|
|
{ date: '2025-01-01', netPerformance: 300 } |
|
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
// The performance is the one of the whole date range
|
|
|
|
|
|
expect(performance.netPerformance).toBe(600); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
it('should return the same chart item for a calendar year as for its date range when grouped by year', async () => { |
|
|
|
|
|
getPerformanceOfCalculator |
|
|
|
|
|
.mockResolvedValueOnce({ |
|
|
|
|
|
chart: [ |
|
|
|
|
|
{ date: '2023-12-31', netPerformance: 0 }, |
|
|
|
|
|
{ date: '2024-12-31', netPerformance: 200 } |
|
|
|
|
|
] |
|
|
|
|
|
}) |
|
|
|
|
|
.mockResolvedValueOnce({ |
|
|
|
|
|
chart: [ |
|
|
|
|
|
{ date: '2023-12-31', netPerformance: 0 }, |
|
|
|
|
|
{ date: '2024-12-31', netPerformance: 200 } |
|
|
|
|
|
] |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
const { chart, performance } = await portfolioService.getPerformance({ |
|
|
|
|
|
dateRange: '2024', |
|
|
|
|
|
groupBy: 'year', |
|
|
|
|
|
userId: userDummyData.id |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// The chart item at the start date of the date range belongs to the
|
|
|
|
|
|
// previous year and gets no chart item of its own
|
|
|
|
|
|
expect(getPerformanceOfCalculator).toHaveBeenCalledTimes(2); |
|
|
|
|
|
expect(getPerformanceOfCalculator).toHaveBeenNthCalledWith( |
|
|
|
|
|
2, |
|
|
|
|
|
getPerformanceOfCalculator.mock.calls[0][0] |
|
|
|
|
|
); |
|
|
|
|
|
expect(chart).toEqual([{ date: '2024-01-01', netPerformance: 200 }]); |
|
|
|
|
|
expect(performance.netPerformance).toBe(200); |
|
|
|
|
|
}); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
describe('getSummary', () => { |
|
|
describe('getSummary', () => { |
|
|
const getSummary = (args: object) => { |
|
|
const getSummary = (args: object) => { |
|
|
return ( |
|
|
return ( |
|
|
|