Browse Source

Improve chart date calculation and tests

pull/8074/head
Thomas Kaul 1 week ago
parent
commit
70ec5f277f
  1. 1
      apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts
  2. 17
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  3. 31
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-fee-and-dividend.spec.ts
  4. 19
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts
  5. 185
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.spec.ts
  6. 4
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts
  7. 1
      apps/api/src/app/portfolio/calculator/roi/portfolio-calculator.ts
  8. 1
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts

1
apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts

@ -24,7 +24,6 @@ export class MwrPortfolioCalculator extends PortfolioCalculator {
protected getHoldingPerformance({}: { protected getHoldingPerformance({}: {
chartDates: string[]; chartDates: string[];
daysUntilNextChartDate: { [date: string]: number };
end: Date; end: Date;
exchangeRates: { [dateString: string]: number }; exchangeRates: { [dateString: string]: number };
marketSymbolMap: { marketSymbolMap: {

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

@ -364,14 +364,15 @@ export abstract class PortfolioCalculator {
// The chart dates are the same for each holding and each date range. Thus // The chart dates are the same for each holding and each date range. Thus
// calculate the number of days until the next chart date one time only. // calculate the number of days until the next chart date one time only.
const daysUntilNextChartDate: { [date: string]: number } = {}; const parsedChartDates = chartDates.map((chartDate) => {
return parseDate(chartDate);
});
for (let i = 0; i < chartDates.length - 1; i += 1) { const daysUntilNextChartDate = parsedChartDates
daysUntilNextChartDate[chartDates[i]] = differenceInDays( .slice(1)
parseDate(chartDates[i + 1]), .map((nextChartDate, index) => {
parseDate(chartDates[i]) return differenceInDays(nextChartDate, parsedChartDates[index]);
); });
}
const errors: ResponseError['errors'] = []; const errors: ResponseError['errors'] = [];
let hasAnyHoldingPerformanceErrors = false; let hasAnyHoldingPerformanceErrors = false;
@ -955,7 +956,7 @@ export abstract class PortfolioCalculator {
symbol symbol
}: { }: {
chartDates: string[]; chartDates: string[];
daysUntilNextChartDate: { [date: string]: number }; daysUntilNextChartDate: number[];
end: Date; end: Date;
exchangeRates: { [dateString: string]: number }; exchangeRates: { [dateString: string]: number };
marketSymbolMap: { marketSymbolMap: {

31
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-fee-and-dividend.spec.ts

@ -190,9 +190,12 @@ describe('PortfolioCalculator', () => {
expect(performanceByDateRange).toMatchObject({ expect(performanceByDateRange).toMatchObject({
max: { max: {
dividendInBaseCurrency: 5.62, dividendInBaseCurrency: 5.62,
dividendInPercentageWithCurrencyEffect: 0.01882242615044543, dividendInPercentageWithCurrencyEffect: expect.closeTo(
0.01882242615044535,
10
),
netPerformance: 18.87, netPerformance: 18.87,
netPerformanceInPercentage: 0.06319914260834614 netPerformanceInPercentage: expect.closeTo(0.06319914260834586, 10)
} }
}); });
}); });
@ -282,9 +285,12 @@ describe('PortfolioCalculator', () => {
expect(performanceByDateRange).toMatchObject({ expect(performanceByDateRange).toMatchObject({
max: { max: {
dividendInBaseCurrency: 5.62, dividendInBaseCurrency: 5.62,
dividendInPercentageWithCurrencyEffect: 0.01882242615044543, dividendInPercentageWithCurrencyEffect: expect.closeTo(
0.01882242615044535,
10
),
netPerformance: 19.87, netPerformance: 19.87,
netPerformanceInPercentage: 0.06654832875611222 netPerformanceInPercentage: expect.closeTo(0.06654832875611194, 10)
} }
}); });
}); });
@ -373,9 +379,12 @@ describe('PortfolioCalculator', () => {
expect(performanceByDateRange).toMatchObject({ expect(performanceByDateRange).toMatchObject({
max: { max: {
dividendInBaseCurrency: 0.62, dividendInBaseCurrency: 0.62,
dividendInPercentageWithCurrencyEffect: 0.002076495411614976, dividendInPercentageWithCurrencyEffect: expect.closeTo(
0.002076495411614967,
10
),
netPerformance: 14.87, netPerformance: 14.87,
netPerformanceInPercentage: 0.049802398017281764 netPerformanceInPercentage: expect.closeTo(0.049802398017281556, 10)
} }
}); });
}); });
@ -458,11 +467,17 @@ describe('PortfolioCalculator', () => {
}, },
max: { max: {
dividendInBaseCurrency: 1.3, dividendInBaseCurrency: 1.3,
dividendInPercentageWithCurrencyEffect: 0.004353941992095918 dividendInPercentageWithCurrencyEffect: expect.closeTo(
0.004353941992095899,
10
)
}, },
ytd: { ytd: {
dividendInBaseCurrency: 0.68, dividendInBaseCurrency: 0.68,
dividendInPercentageWithCurrencyEffect: 0.002002886512915671 dividendInPercentageWithCurrencyEffect: expect.closeTo(
0.002002886512915673,
10
)
} }
}); });
}); });

19
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts

@ -177,10 +177,16 @@ describe('PortfolioCalculator', () => {
max: { max: {
date: '2023-07-10', date: '2023-07-10',
dividendInBaseCurrency: 0.62, dividendInBaseCurrency: 0.62,
dividendInPercentageWithCurrencyEffect: 0.002076495411614976, dividendInPercentageWithCurrencyEffect: expect.closeTo(
0.002076495411614967,
10
),
netPerformance: 14.87, netPerformance: 14.87,
netPerformanceInPercentage: 0.049802398017281764, netPerformanceInPercentage: expect.closeTo(0.049802398017281556, 10),
netPerformanceInPercentageWithCurrencyEffect: 0.049802398017281764, netPerformanceInPercentageWithCurrencyEffect: expect.closeTo(
0.049802398017281556,
10
),
netPerformanceWithCurrencyEffect: 14.87, netPerformanceWithCurrencyEffect: 14.87,
totalInvestmentValueWithCurrencyEffect: 298.58, totalInvestmentValueWithCurrencyEffect: 298.58,
valueWithCurrencyEffect: 331.83 valueWithCurrencyEffect: 331.83
@ -190,8 +196,11 @@ describe('PortfolioCalculator', () => {
dividendInBaseCurrency: 0, dividendInBaseCurrency: 0,
dividendInPercentageWithCurrencyEffect: 0, dividendInPercentageWithCurrencyEffect: 0,
netPerformance: -7.6800000000000015, netPerformance: -7.6800000000000015,
netPerformanceInPercentage: -0.022620835910576995, netPerformanceInPercentage: expect.closeTo(-0.022620835910577015, 10),
netPerformanceInPercentageWithCurrencyEffect: -0.022620835910576995, netPerformanceInPercentageWithCurrencyEffect: expect.closeTo(
-0.022620835910577015,
10
),
netPerformanceWithCurrencyEffect: -7.6800000000000015, netPerformanceWithCurrencyEffect: -7.6800000000000015,
totalInvestmentValueWithCurrencyEffect: 298.58, totalInvestmentValueWithCurrencyEffect: 298.58,
valueWithCurrencyEffect: 331.83 valueWithCurrencyEffect: 331.83

185
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.spec.ts

@ -1,3 +1,186 @@
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
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 { 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 { HistoricalDataItem } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
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 getHistoricalDataItem({
date,
investment,
netPerformance
}: {
date: string;
investment: number;
netPerformance: number;
}): HistoricalDataItem {
return {
date,
netPerformance,
dividendInBaseCurrency: 0,
netPerformanceWithCurrencyEffect: netPerformance,
totalInvestment: investment,
totalInvestmentValueWithCurrencyEffect: investment,
value: investment + netPerformance,
valueWithCurrencyEffect: investment + netPerformance
};
}
describe('PortfolioCalculator', () => { describe('PortfolioCalculator', () => {
test.skip('Skip empty test', () => 1); let configurationService: ConfigurationService;
let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService;
let portfolioCalculator: PortfolioCalculator;
let portfolioCalculatorFactory: PortfolioCalculatorFactory;
let portfolioSnapshotService: PortfolioSnapshotService;
let redisCacheService: RedisCacheService;
beforeEach(() => {
PortfolioSnapshotServiceMock.reset();
RedisCacheServiceMock.reset();
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,
null,
null,
null
);
portfolioSnapshotService = new PortfolioSnapshotService(null, null);
redisCacheService = new RedisCacheService(null, null);
portfolioCalculatorFactory = new PortfolioCalculatorFactory(
configurationService,
currentRateService,
exchangeRateDataService,
portfolioSnapshotService,
redisCacheService
);
portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities: [],
calculationType: PerformanceCalculationType.ROAI,
currency: 'USD',
userId: userDummyData.id
});
});
describe('calculate performance percentages for date range', () => {
it('with a buy followed by a gap between chart dates', () => {
const performancePercentagesByDate = portfolioCalculator[
'calculatePerformancePercentagesForDateRange'
]({
historicalDataItems: [
getHistoricalDataItem({
date: '2017-01-01',
investment: 100,
netPerformance: 0
}),
// Buy, followed by a gap of 21 days until the next chart date
getHistoricalDataItem({
date: '2017-01-02',
investment: 200,
netPerformance: 0
}),
// Sell
getHistoricalDataItem({
date: '2017-01-23',
investment: 100,
netPerformance: 44
})
]
});
// The investment of each chart date applies to each day until the next
// chart date. The investment of the last chart date counts for this day
// only: (1 * 100 + 21 * 200 + 1 * 100) / 23 = 4400 / 23
expect(
performancePercentagesByDate['2017-01-23'].netPerformanceInPercentage
).toBeCloseTo(0.23, 10);
expect(
performancePercentagesByDate['2017-01-23']
.netPerformanceInPercentageWithCurrencyEffect
).toBeCloseTo(0.23, 10);
});
it('with an additional chart date of the same investment', () => {
const performancePercentagesByDate = portfolioCalculator[
'calculatePerformancePercentagesForDateRange'
]({
historicalDataItems: [
getHistoricalDataItem({
date: '2017-01-01',
investment: 100,
netPerformance: 0
}),
getHistoricalDataItem({
date: '2017-01-02',
investment: 200,
netPerformance: 0
}),
// An additional chart date splits the gap but must not change the
// average investment
getHistoricalDataItem({
date: '2017-01-12',
investment: 200,
netPerformance: 20
}),
getHistoricalDataItem({
date: '2017-01-23',
investment: 100,
netPerformance: 44
})
]
});
expect(
performancePercentagesByDate['2017-01-23'].netPerformanceInPercentage
).toBeCloseTo(0.23, 10);
expect(
performancePercentagesByDate['2017-01-23']
.netPerformanceInPercentageWithCurrencyEffect
).toBeCloseTo(0.23, 10);
});
});
}); });

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

@ -315,7 +315,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
symbol symbol
}: { }: {
chartDates: string[]; chartDates: string[];
daysUntilNextChartDate: { [date: string]: number }; daysUntilNextChartDate: number[];
end: Date; end: Date;
exchangeRates: { [dateString: string]: number }; exchangeRates: { [dateString: string]: number };
marketSymbolMap: { marketSymbolMap: {
@ -678,7 +678,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
// date of the range counts for this day only. // date of the range counts for this day only.
const investmentDays = isLastChartDateOfRange const investmentDays = isLastChartDateOfRange
? 1 ? 1
: daysUntilNextChartDate[date]; : daysUntilNextChartDate[i];
if ( if (
investmentValuesAccumulatedWithCurrencyEffect[date] instanceof Big && investmentValuesAccumulatedWithCurrencyEffect[date] instanceof Big &&

1
apps/api/src/app/portfolio/calculator/roi/portfolio-calculator.ts

@ -24,7 +24,6 @@ export class RoiPortfolioCalculator extends PortfolioCalculator {
protected getHoldingPerformance({}: { protected getHoldingPerformance({}: {
chartDates: string[]; chartDates: string[];
daysUntilNextChartDate: { [date: string]: number };
end: Date; end: Date;
exchangeRates: { [dateString: string]: number }; exchangeRates: { [dateString: string]: number };
marketSymbolMap: { marketSymbolMap: {

1
apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts

@ -24,7 +24,6 @@ export class TwrPortfolioCalculator extends PortfolioCalculator {
protected getHoldingPerformance({}: { protected getHoldingPerformance({}: {
chartDates: string[]; chartDates: string[];
daysUntilNextChartDate: { [date: string]: number };
end: Date; end: Date;
exchangeRates: { [dateString: string]: number }; exchangeRates: { [dateString: string]: number };
marketSymbolMap: { marketSymbolMap: {

Loading…
Cancel
Save