Browse Source

Refactor fee calculation

pull/3267/head
Thomas Kaul 1 year ago
parent
commit
d7b1033c0f
  1. 22
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  2. 1
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts
  3. 1
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts
  4. 1
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts
  5. 1
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts
  6. 131
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-fee.spec.ts
  7. 1
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts
  8. 1
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts
  9. 1
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-no-orders.spec.ts
  10. 1
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts
  11. 1
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts
  12. 11
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts
  13. 1
      apps/api/src/app/portfolio/interfaces/portfolio-snapshot.interface.ts
  14. 3
      apps/api/src/app/portfolio/interfaces/transaction-point.interface.ts
  15. 5
      apps/api/src/app/portfolio/portfolio.controller.ts
  16. 71
      apps/api/src/app/portfolio/portfolio.service.ts
  17. 1
      libs/common/src/lib/interfaces/symbol-metrics.interface.ts

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

@ -137,6 +137,7 @@ export abstract class PortfolioCalculator {
netPerformancePercentageWithCurrencyEffect: new Big(0),
netPerformanceWithCurrencyEffect: new Big(0),
positions: [],
totalFeesWithCurrencyEffect: new Big(0),
totalInvestment: new Big(0),
totalInvestmentWithCurrencyEffect: new Big(0)
};
@ -259,6 +260,7 @@ export abstract class PortfolioCalculator {
);
const {
feesWithCurrencyEffect,
grossPerformance,
grossPerformancePercentage,
grossPerformancePercentageWithCurrencyEffect,
@ -660,6 +662,12 @@ export abstract class PortfolioCalculator {
);
}
public async getFeesInBaseCurrency() {
await this.snapshotPromise;
return this.snapshot.totalFeesWithCurrencyEffect;
}
public getInvestments(): { date: string; investment: Big }[] {
if (this.transactionPoints.length === 0) {
return [];
@ -752,6 +760,12 @@ export abstract class PortfolioCalculator {
type,
unitPrice
} of this.orders) {
if (
[/*'DIVIDEND', 'FEE',*/ 'INTEREST', 'ITEM', 'LIABILITY'].includes(type)
) {
continue;
}
let currentTransactionPointItem: TransactionPointSymbol;
const oldAccumulatedSymbol = symbols[SymbolProfile.symbol];
@ -824,14 +838,22 @@ export abstract class PortfolioCalculator {
return a.symbol?.localeCompare(b.symbol);
});
let fees = new Big(0);
if (type === 'FEE') {
fees = fee;
}
if (lastDate !== date || lastTransactionPoint === null) {
lastTransactionPoint = {
date,
fees,
items: newItems
};
this.transactionPoints.push(lastTransactionPoint);
} else {
lastTransactionPoint.fees = lastTransactionPoint.fees.plus(fees);
lastTransactionPoint.items = newItems;
}

1
apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts

@ -173,6 +173,7 @@ describe('PortfolioCalculator', () => {
valueInBaseCurrency: new Big('0')
}
],
totalFeesWithCurrencyEffect: new Big('3.2'),
totalInvestment: new Big('0'),
totalInvestmentWithCurrencyEffect: new Big('0')
});

1
apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts

@ -156,6 +156,7 @@ describe('PortfolioCalculator', () => {
valueInBaseCurrency: new Big('0')
}
],
totalFeesWithCurrencyEffect: new Big('3.2'),
totalInvestment: new Big('0'),
totalInvestmentWithCurrencyEffect: new Big('0')
});

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

@ -141,6 +141,7 @@ describe('PortfolioCalculator', () => {
valueInBaseCurrency: new Big('297.8')
}
],
totalFeesWithCurrencyEffect: new Big('1.55'),
totalInvestment: new Big('273.2'),
totalInvestmentWithCurrencyEffect: new Big('273.2')
});

1
apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts

@ -175,6 +175,7 @@ describe('PortfolioCalculator', () => {
valueInBaseCurrency: new Big('13298.425356')
}
],
totalFeesWithCurrencyEffect: new Big('0'),
totalInvestment: new Big('320.43'),
totalInvestmentWithCurrencyEffect: new Big('318.542667299999967957')
});

131
apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-fee.spec.ts

@ -0,0 +1,131 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import {
activityDummyData,
symbolProfileDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import {
PortfolioCalculatorFactory,
PerformanceCalculationType
} 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 { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { parseDate } from '@ghostfolio/common/helper';
import { Big } from 'big.js';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
};
});
describe('PortfolioCalculator', () => {
let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService;
let factory: PortfolioCalculatorFactory;
beforeEach(() => {
currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,
null,
null,
null
);
factory = new PortfolioCalculatorFactory(
currentRateService,
exchangeRateDataService
);
});
describe('compute portfolio snapshot', () => {
it.only('with fee activity', async () => {
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2021-12-18').getTime());
const activities: Activity[] = [
{
...activityDummyData,
date: new Date('2021-09-01'),
fee: 49,
quantity: 0,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD',
dataSource: 'MANUAL',
name: 'Account Opening Fee',
symbol: '2c463fb3-af07-486e-adb0-8301b3d72141'
},
type: 'FEE',
unitPrice: 0
}
];
const portfolioCalculator = factory.createCalculator({
activities,
calculationType: PerformanceCalculationType.TWR,
currency: 'USD'
});
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
parseDate('2021-11-30')
);
spy.mockRestore();
expect(portfolioSnapshot).toEqual({
currentValueInBaseCurrency: new Big('0'),
errors: [],
grossPerformance: new Big('0'),
grossPerformancePercentage: new Big('0'),
grossPerformancePercentageWithCurrencyEffect: new Big('0'),
grossPerformanceWithCurrencyEffect: new Big('0'),
hasErrors: true,
netPerformance: new Big('0'),
netPerformancePercentage: new Big('0'),
netPerformancePercentageWithCurrencyEffect: new Big('0'),
netPerformanceWithCurrencyEffect: new Big('0'),
positions: [
{
averagePrice: new Big('0'),
currency: 'USD',
dataSource: 'MANUAL',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('49'),
firstBuyDate: '2021-09-01',
grossPerformance: null,
grossPerformancePercentage: null,
grossPerformancePercentageWithCurrencyEffect: null,
grossPerformanceWithCurrencyEffect: null,
investment: new Big('0'),
investmentWithCurrencyEffect: new Big('0'),
marketPrice: null,
marketPriceInBaseCurrency: 0,
netPerformance: null,
netPerformancePercentage: null,
netPerformancePercentageWithCurrencyEffect: null,
netPerformanceWithCurrencyEffect: null,
quantity: new Big('0'),
symbol: '2c463fb3-af07-486e-adb0-8301b3d72141',
tags: [],
timeWeightedInvestment: new Big('0'),
timeWeightedInvestmentWithCurrencyEffect: new Big('0'),
transactionCount: 1,
valueInBaseCurrency: new Big('0')
}
],
totalFeesWithCurrencyEffect: new Big('49'),
totalInvestment: new Big('0'),
totalInvestmentWithCurrencyEffect: new Big('0')
});
});
});
});

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

@ -154,6 +154,7 @@ describe('PortfolioCalculator', () => {
valueInBaseCurrency: new Big('103.10483')
}
],
totalFeesWithCurrencyEffect: new Big('1'),
totalInvestment: new Big('89.12'),
totalInvestmentWithCurrencyEffect: new Big('82.329056')
});

1
apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts

@ -130,6 +130,7 @@ describe('PortfolioCalculator', () => {
transactionCount: 2
}
],
totalFeesWithCurrencyEffect: new Big('19'),
totalInvestment: new Big('298.58'),
totalInvestmentWithCurrencyEffect: new Big('298.58')
});

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

@ -80,6 +80,7 @@ describe('PortfolioCalculator', () => {
netPerformancePercentageWithCurrencyEffect: new Big(0),
netPerformanceWithCurrencyEffect: new Big(0),
positions: [],
totalFeesWithCurrencyEffect: new Big('0'),
totalInvestment: new Big(0),
totalInvestmentWithCurrencyEffect: new Big(0)
});

1
apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts

@ -158,6 +158,7 @@ describe('PortfolioCalculator', () => {
valueInBaseCurrency: new Big('87.8')
}
],
totalFeesWithCurrencyEffect: new Big('4.25'),
totalInvestment: new Big('75.80'),
totalInvestmentWithCurrencyEffect: new Big('75.80')
});

1
apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts

@ -182,6 +182,7 @@ describe('PortfolioCalculator', () => {
valueInBaseCurrency: new Big('0')
}
],
totalFeesWithCurrencyEffect: new Big('0'),
totalInvestment: new Big('0'),
totalInvestmentWithCurrencyEffect: new Big('0')
});

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

@ -30,12 +30,19 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
let hasErrors = false;
let netPerformance = new Big(0);
let netPerformanceWithCurrencyEffect = new Big(0);
let totalFeesWithCurrencyEffect = new Big(0);
let totalInvestment = new Big(0);
let totalInvestmentWithCurrencyEffect = new Big(0);
let totalTimeWeightedInvestment = new Big(0);
let totalTimeWeightedInvestmentWithCurrencyEffect = new Big(0);
for (const currentPosition of positions) {
if (currentPosition.fee) {
totalFeesWithCurrencyEffect = totalFeesWithCurrencyEffect.plus(
currentPosition.fee
);
}
if (currentPosition.valueInBaseCurrency) {
currentValueInBaseCurrency = currentValueInBaseCurrency.plus(
currentPosition.valueInBaseCurrency
@ -101,6 +108,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
hasErrors,
netPerformance,
netPerformanceWithCurrencyEffect,
totalFeesWithCurrencyEffect,
totalInvestment,
totalInvestmentWithCurrencyEffect,
netPerformancePercentage: totalTimeWeightedInvestment.eq(0)
@ -198,6 +206,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
return {
currentValues: {},
currentValuesWithCurrencyEffect: {},
feesWithCurrencyEffect: new Big(0),
grossPerformance: new Big(0),
grossPerformancePercentage: new Big(0),
grossPerformancePercentageWithCurrencyEffect: new Big(0),
@ -240,6 +249,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
return {
currentValues: {},
currentValuesWithCurrencyEffect: {},
feesWithCurrencyEffect: new Big(0),
grossPerformance: new Big(0),
grossPerformancePercentage: new Big(0),
grossPerformancePercentageWithCurrencyEffect: new Big(0),
@ -808,6 +818,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
return {
currentValues,
currentValuesWithCurrencyEffect,
feesWithCurrencyEffect,
grossPerformancePercentage,
grossPerformancePercentageWithCurrencyEffect,
initialValue,

1
apps/api/src/app/portfolio/interfaces/portfolio-snapshot.interface.ts

@ -15,6 +15,7 @@ export interface PortfolioSnapshot extends ResponseError {
netPerformancePercentage: Big;
netPerformancePercentageWithCurrencyEffect: Big;
positions: TimelinePosition[];
totalFeesWithCurrencyEffect: Big;
totalInvestment: Big;
totalInvestmentWithCurrencyEffect: Big;
}

3
apps/api/src/app/portfolio/interfaces/transaction-point.interface.ts

@ -1,6 +1,9 @@
import { Big } from 'big.js';
import { TransactionPointSymbol } from './transaction-point-symbol.interface';
export interface TransactionPoint {
date: string;
fees: Big;
items: TransactionPointSymbol[];
}

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

@ -390,11 +390,9 @@ export class PortfolioController {
@Query('assetClasses') filterByAssetClasses?: string,
@Query('range') dateRange: DateRange = 'max',
@Query('tags') filterByTags?: string,
@Query('withExcludedAccounts') withExcludedAccountsParam = 'false',
@Query('withItems') withItemsParam = 'false'
@Query('withExcludedAccounts') withExcludedAccountsParam = 'false'
): Promise<PortfolioPerformanceResponse> {
const withExcludedAccounts = withExcludedAccountsParam === 'true';
const withItems = withItemsParam === 'true';
const hasReadRestrictedAccessPermission =
this.userService.hasReadRestrictedAccessPermission({
@ -413,7 +411,6 @@ export class PortfolioController {
filters,
impersonationId,
withExcludedAccounts,
withItems,
userId: this.request.user.id
});

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

@ -23,12 +23,7 @@ import {
EMERGENCY_FUND_TAG_ID,
UNKNOWN_KEY
} from '@ghostfolio/common/config';
import {
DATE_FORMAT,
getAllActivityTypes,
getSum,
parseDate
} from '@ghostfolio/common/helper';
import { DATE_FORMAT, getSum, parseDate } from '@ghostfolio/common/helper';
import {
Accounts,
EnhancedSymbolProfile,
@ -350,19 +345,8 @@ export class PortfolioService {
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
);
let types = getAllActivityTypes().filter((activityType) => {
return activityType !== 'FEE';
});
if (withLiabilities === false) {
types = types.filter((activityType) => {
return activityType !== 'LIABILITY';
});
}
const { activities } = await this.orderService.getOrders({
filters,
types,
userCurrency,
userId,
withExcludedAccounts
@ -917,7 +901,6 @@ export class PortfolioService {
endDate,
filters,
userId,
types: ['BUY', 'SELL'],
userCurrency: this.getUserCurrency()
});
@ -1043,15 +1026,13 @@ export class PortfolioService {
filters,
impersonationId,
userId,
withExcludedAccounts = false,
withItems = false
withExcludedAccounts = false
}: {
dateRange?: DateRange;
filters?: Filter[];
impersonationId: string;
userId: string;
withExcludedAccounts?: boolean;
withItems?: boolean;
}): Promise<PortfolioPerformanceResponse> {
userId = await this.getUserId(impersonationId, userId);
const user = await this.userService.user({ id: userId });
@ -1089,10 +1070,7 @@ export class PortfolioService {
filters,
userCurrency,
userId,
withExcludedAccounts,
types: withItems
? ['BUY', 'DIVIDEND', 'ITEM', 'SELL']
: ['BUY', 'DIVIDEND', 'SELL']
withExcludedAccounts
});
if (accountBalanceItems?.length <= 0 && activities?.length <= 0) {
@ -1227,8 +1205,7 @@ export class PortfolioService {
const { activities } = await this.orderService.getOrders({
userCurrency,
userId,
types: ['BUY', 'SELL']
userId
});
const portfolioCalculator = this.calculatorFactory.createCalculator({
@ -1237,7 +1214,7 @@ export class PortfolioService {
currency: this.request.user.Settings.settings.baseCurrency
});
let { positions, totalInvestment } =
let { totalFeesWithCurrencyEffect, positions, totalInvestment } =
await portfolioCalculator.getSnapshot();
positions = positions.filter((item) => !item.quantity.eq(0));
@ -1303,7 +1280,7 @@ export class PortfolioService {
new FeeRatioInitialInvestment(
this.exchangeRateDataService,
totalInvestment.toNumber(),
this.getFees({ activities, userCurrency }).toNumber()
totalFeesWithCurrencyEffect.toNumber()
)
],
userSettings
@ -1447,30 +1424,6 @@ export class PortfolioService {
return valueInBaseCurrencyOfEmergencyFundPositions.toNumber();
}
private getFees({
activities,
userCurrency
}: {
activities: Activity[];
userCurrency: string;
}) {
return getSum(
activities
.filter(({ isDraft }) => {
return isDraft === false;
})
.map(({ fee, SymbolProfile }) => {
return new Big(
this.exchangeRateDataService.toCurrency(
fee,
SymbolProfile.currency,
userCurrency
)
);
})
);
}
private getInitialCashPosition({
balance,
currency
@ -1664,15 +1617,20 @@ export class PortfolioService {
)
);
const fees = this.getFees({ activities, userCurrency }).toNumber();
const firstOrderDate = activities[0]?.date;
const fees = await portfolioCalculator.getFeesInBaseCurrency();
const firstOrderDate = portfolioCalculator.getStartDate();
// TODO
const interest = this.getSumOfActivityType({
activities,
userCurrency,
activityType: 'INTEREST'
}).toNumber();
console.log(interest);
// TODO
const items = getSum(
Object.keys(holdings)
.filter((symbol) => {
@ -1687,6 +1645,7 @@ export class PortfolioService {
})
).toNumber();
// TODO
const liabilities = getSum(
Object.keys(holdings)
.filter((symbol) => {
@ -1777,7 +1736,6 @@ export class PortfolioService {
annualizedPerformancePercentWithCurrencyEffect,
cash,
excludedAccountsAndActivities,
fees,
firstOrderDate,
interest,
items,
@ -1793,6 +1751,7 @@ export class PortfolioService {
.toNumber(),
total: emergencyFund.toNumber()
},
fees: fees.toNumber(),
filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(),
filteredValueInPercentage: netWorth
? filteredValueInBaseCurrency.div(netWorth).toNumber()

1
libs/common/src/lib/interfaces/symbol-metrics.interface.ts

@ -7,6 +7,7 @@ export interface SymbolMetrics {
currentValuesWithCurrencyEffect: {
[date: string]: Big;
};
feesWithCurrencyEffect: Big;
grossPerformance: Big;
grossPerformancePercentage: Big;
grossPerformancePercentageWithCurrencyEffect: Big;

Loading…
Cancel
Save