Browse Source

Track accumulated buy values in holding valuation of portfolio calculator

pull/8188/head
Pedro Moreira 1 week ago
parent
commit
5a69ce761e
  1. 154
      apps/api/src/app/portfolio/calculator/portfolio-calculator.spec.ts
  2. 53
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  3. 4
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts
  4. 2
      apps/api/src/app/portfolio/interfaces/accumulated-values.interface.ts
  5. 6
      apps/api/src/app/portfolio/interfaces/holding-performance.interface.ts
  6. 2
      apps/api/src/app/portfolio/interfaces/holding-valuation.interface.ts

154
apps/api/src/app/portfolio/calculator/portfolio-calculator.spec.ts

@ -0,0 +1,154 @@
import { userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { PortfolioCalculatorActivityItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator-activity-item.interface';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Big } from 'big.js';
import { PortfolioCalculator } from './portfolio-calculator';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return {
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
};
});
jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => {
return {
PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock;
})
};
}
);
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
};
});
function getActivityItem({
date,
itemType,
quantity,
type,
unitPrice
}: {
date: string;
itemType?: PortfolioCalculatorActivityItem['itemType'];
quantity: number;
type: PortfolioCalculatorActivityItem['type'];
unitPrice: number;
}): PortfolioCalculatorActivityItem {
return {
date,
itemType,
type,
assetProfile: {
assetSubClass: 'ETF',
currency: 'CHF',
dataSource: 'YAHOO',
name: 'Test',
symbol: 'TEST'
},
fee: new Big(0),
feeInBaseCurrency: new Big(0),
quantity: new Big(quantity),
unitPrice: new Big(unitPrice),
unitPriceFromMarketData: new Big(unitPrice)
};
}
describe('PortfolioCalculator', () => {
let portfolioCalculator: PortfolioCalculator;
beforeEach(() => {
PortfolioSnapshotServiceMock.reset();
RedisCacheServiceMock.reset();
const portfolioCalculatorFactory = new PortfolioCalculatorFactory(
new ConfigurationService(),
new CurrentRateService(null, null, null),
new ExchangeRateDataService(null, null, null, null),
new PortfolioSnapshotService(null, null),
new RedisCacheService(null, null)
);
portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities: [],
calculationType: PerformanceCalculationType.ROAI,
currency: 'CHF',
userId: userDummyData.id
});
});
describe('get holding valuation', () => {
it('accumulates the buy values without deducting sells', () => {
const { buyValuesAccumulated, buyValuesAccumulatedWithCurrencyEffect } =
portfolioCalculator['getHoldingValuation']({
activities: [
getActivityItem({
date: '2023-12-31',
itemType: 'start',
quantity: 0,
type: 'BUY',
unitPrice: 10
}),
getActivityItem({
date: '2024-01-01',
quantity: 100,
type: 'BUY',
unitPrice: 10
}),
getActivityItem({
date: '2024-06-01',
quantity: 100,
type: 'SELL',
unitPrice: 10
}),
getActivityItem({
date: '2024-06-02',
quantity: 100,
type: 'BUY',
unitPrice: 10
}),
getActivityItem({
date: '2024-12-31',
itemType: 'end',
quantity: 0,
type: 'BUY',
unitPrice: 11
})
],
exchangeRates: {},
unitPriceAtStartDate: undefined
});
expect(buyValuesAccumulated).toEqual({
'2024-01-01': new Big(1000),
'2024-06-01': new Big(1000),
'2024-06-02': new Big(2000),
'2024-12-31': new Big(2000)
});
expect(buyValuesAccumulatedWithCurrencyEffect).toEqual(
buyValuesAccumulated
);
});
});
});

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

@ -389,6 +389,8 @@ export abstract class PortfolioCalculator {
[assetProfileIdentifier: string]: {
averageInvestmentValues: { [date: string]: Big };
averageInvestmentValuesWithCurrencyEffect: { [date: string]: Big };
buyValuesAccumulated: { [date: string]: Big };
buyValuesAccumulatedWithCurrencyEffect: { [date: string]: Big };
currentValues: { [date: string]: Big };
currentValuesWithCurrencyEffect: { [date: string]: Big };
dividendValuesWithCurrencyEffect: { [date: string]: Big };
@ -425,6 +427,8 @@ export abstract class PortfolioCalculator {
averageInvestmentValues,
averageInvestmentValuesWithCurrencyEffect,
averageInvestmentWithCurrencyEffect,
buyValuesAccumulated,
buyValuesAccumulatedWithCurrencyEffect,
currentValues,
currentValuesWithCurrencyEffect,
dividendValuesWithCurrencyEffect,
@ -474,6 +478,8 @@ export abstract class PortfolioCalculator {
? {
averageInvestmentValues: {},
averageInvestmentValuesWithCurrencyEffect: {},
buyValuesAccumulated: {},
buyValuesAccumulatedWithCurrencyEffect: {},
currentValues: {},
currentValuesWithCurrencyEffect: {},
dividendValuesWithCurrencyEffect: {},
@ -487,6 +493,8 @@ export abstract class PortfolioCalculator {
: {
averageInvestmentValues,
averageInvestmentValuesWithCurrencyEffect,
buyValuesAccumulated,
buyValuesAccumulatedWithCurrencyEffect,
currentValues,
currentValuesWithCurrencyEffect,
dividendValuesWithCurrencyEffect,
@ -625,6 +633,14 @@ export abstract class PortfolioCalculator {
dateString
] ?? new Big(0);
const buyValueAccumulated =
assetProfileValues.buyValuesAccumulated?.[dateString] ?? new Big(0);
const buyValueAccumulatedWithCurrencyEffect =
assetProfileValues.buyValuesAccumulatedWithCurrencyEffect?.[
dateString
] ?? new Big(0);
accumulatedValuesByDate[dateString] = {
investmentValueWithCurrencyEffect: (
accumulatedValuesByDate[dateString]
@ -638,6 +654,13 @@ export abstract class PortfolioCalculator {
accumulatedValuesByDate[dateString]
?.totalAverageInvestmentValueWithCurrencyEffect ?? new Big(0)
).add(averageInvestmentValueWithCurrencyEffect),
totalBuyValue: (
accumulatedValuesByDate[dateString]?.totalBuyValue ?? new Big(0)
).add(buyValueAccumulated),
totalBuyValueWithCurrencyEffect: (
accumulatedValuesByDate[dateString]
?.totalBuyValueWithCurrencyEffect ?? new Big(0)
).add(buyValueAccumulatedWithCurrencyEffect),
totalCashValueWithCurrencyEffect: (
accumulatedValuesByDate[dateString]
?.totalCashValueWithCurrencyEffect ?? new Big(0)
@ -932,6 +955,8 @@ export abstract class PortfolioCalculator {
averageInvestmentValues: {},
averageInvestmentValuesWithCurrencyEffect: {},
averageInvestmentWithCurrencyEffect: new Big(0),
buyValuesAccumulated: {},
buyValuesAccumulatedWithCurrencyEffect: {},
currentValues: {},
currentValuesWithCurrencyEffect: {},
dividendValuesWithCurrencyEffect: {},
@ -1000,6 +1025,8 @@ export abstract class PortfolioCalculator {
unitPriceAtStartDate: Big | undefined;
}): HoldingValuation {
const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)];
const buyValuesAccumulated: { [date: string]: Big } = {};
const buyValuesAccumulatedWithCurrencyEffect: { [date: string]: Big } = {};
const currentValues: { [date: string]: Big } = {};
const currentValuesWithCurrencyEffect: { [date: string]: Big } = {};
const dividendValuesWithCurrencyEffect: { [date: string]: Big } = {};
@ -1033,6 +1060,10 @@ export abstract class PortfolioCalculator {
let totalInvestmentFromBuyTransactions = new Big(0);
let totalInvestmentFromBuyTransactionsWithCurrencyEffect = new Big(0);
let totalInvestmentWithCurrencyEffect = new Big(0);
let totalBuyValue = new Big(0);
let totalBuyValueAtStartDate = new Big(0);
let totalBuyValueAtStartDateWithCurrencyEffect = new Big(0);
let totalBuyValueWithCurrencyEffect = new Big(0);
let totalQuantity = new Big(0);
let totalQuantityFromBuyTransactions = new Big(0);
@ -1127,6 +1158,12 @@ export abstract class PortfolioCalculator {
totalInvestmentFromBuyTransactionsWithCurrencyEffect.plus(
transactionInvestmentWithCurrencyEffect
);
totalBuyValue = totalBuyValue.plus(transactionInvestment);
totalBuyValueWithCurrencyEffect = totalBuyValueWithCurrencyEffect.plus(
transactionInvestmentWithCurrencyEffect
);
} else if (activity.type === 'SELL') {
if (totalQuantity.gt(0)) {
const remainingQuantity = totalQuantity.minus(activity.quantity);
@ -1277,6 +1314,11 @@ export abstract class PortfolioCalculator {
if (activity.itemType === 'start') {
dividendsAtStartDateWithCurrencyEffect = dividendsWithCurrencyEffect;
feesAtStartDate = fees;
totalBuyValueAtStartDate = totalBuyValue;
totalBuyValueAtStartDateWithCurrencyEffect =
totalBuyValueWithCurrencyEffect;
feesAtStartDateWithCurrencyEffect = feesWithCurrencyEffect;
grossPerformanceAtStartDate = grossPerformance;
@ -1285,6 +1327,15 @@ export abstract class PortfolioCalculator {
}
if (i > indexOfStartActivity) {
buyValuesAccumulated[activity.date] = totalBuyValue.minus(
totalBuyValueAtStartDate
);
buyValuesAccumulatedWithCurrencyEffect[activity.date] =
totalBuyValueWithCurrencyEffect.minus(
totalBuyValueAtStartDateWithCurrencyEffect
);
currentValues[activity.date] = valueOfInvestment;
currentValuesWithCurrencyEffect[activity.date] =
@ -1366,6 +1417,8 @@ export abstract class PortfolioCalculator {
}
return {
buyValuesAccumulated,
buyValuesAccumulatedWithCurrencyEffect,
currentValues,
currentValuesWithCurrencyEffect,
dividendValuesWithCurrencyEffect,

4
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts

@ -298,6 +298,8 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
activities,
dateOfFirstActivity,
holdingValuation: {
buyValuesAccumulated,
buyValuesAccumulatedWithCurrencyEffect,
currentValues,
currentValuesWithCurrencyEffect,
dividendValuesWithCurrencyEffect,
@ -619,6 +621,8 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
return {
averageInvestmentValues,
averageInvestmentValuesWithCurrencyEffect,
buyValuesAccumulated,
buyValuesAccumulatedWithCurrencyEffect,
currentValues,
currentValuesWithCurrencyEffect,
dividendValuesWithCurrencyEffect,

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

@ -4,6 +4,8 @@ export interface AccumulatedValues {
investmentValueWithCurrencyEffect: Big;
totalAverageInvestmentValue: Big;
totalAverageInvestmentValueWithCurrencyEffect: Big;
totalBuyValue: Big;
totalBuyValueWithCurrencyEffect: Big;
totalCashValueWithCurrencyEffect: Big;
totalCurrentValue: Big;
totalCurrentValueWithCurrencyEffect: Big;

6
apps/api/src/app/portfolio/interfaces/holding-performance.interface.ts

@ -11,6 +11,12 @@ export interface HoldingPerformance {
[date: string]: Big;
};
averageInvestmentWithCurrencyEffect: Big;
buyValuesAccumulated: {
[date: string]: Big;
};
buyValuesAccumulatedWithCurrencyEffect: {
[date: string]: Big;
};
currentValues: {
[date: string]: Big;
};

2
apps/api/src/app/portfolio/interfaces/holding-valuation.interface.ts

@ -3,6 +3,8 @@ import { Big } from 'big.js';
import { HoldingValuationItem } from './holding-valuation-item.interface';
export interface HoldingValuation {
buyValuesAccumulated: { [date: string]: Big };
buyValuesAccumulatedWithCurrencyEffect: { [date: string]: Big };
currentValues: { [date: string]: Big };
currentValuesWithCurrencyEffect: { [date: string]: Big };
dividendValuesWithCurrencyEffect: { [date: string]: Big };

Loading…
Cancel
Save