Browse Source

Fix portfolio calculation for holdings with same symbol

pull/7664/head
Thomas Kaul 1 week ago
parent
commit
24655251fc
  1. 46
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  2. 14
      apps/api/src/app/portfolio/portfolio.service.ts
  3. 56
      apps/api/src/helper/object.helper.spec.ts
  4. 27
      apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
  5. 23
      apps/client/src/app/pages/public/public-page.component.ts

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

@ -527,48 +527,51 @@ export abstract class PortfolioCalculator {
} }
} }
const assetProfileIdentifiers = Object.keys(valuesByAssetProfileIdentifier);
for (const dateString of chartDates) { for (const dateString of chartDates) {
for (const assetProfileIdentifier of Object.keys( for (const assetProfileIdentifier of assetProfileIdentifiers) {
valuesByAssetProfileIdentifier const assetProfileValues =
)) {
const symbolValues =
valuesByAssetProfileIdentifier[assetProfileIdentifier]; valuesByAssetProfileIdentifier[assetProfileIdentifier];
const currentValue = const currentValue =
symbolValues.currentValues?.[dateString] ?? new Big(0); assetProfileValues.currentValues?.[dateString] ?? new Big(0);
const currentValueWithCurrencyEffect = const currentValueWithCurrencyEffect =
symbolValues.currentValuesWithCurrencyEffect?.[dateString] ?? assetProfileValues.currentValuesWithCurrencyEffect?.[dateString] ??
new Big(0); new Big(0);
const investmentValueAccumulated = const investmentValueAccumulated =
symbolValues.investmentValuesAccumulated?.[dateString] ?? new Big(0); assetProfileValues.investmentValuesAccumulated?.[dateString] ??
new Big(0);
const investmentValueAccumulatedWithCurrencyEffect = const investmentValueAccumulatedWithCurrencyEffect =
symbolValues.investmentValuesAccumulatedWithCurrencyEffect?.[ assetProfileValues.investmentValuesAccumulatedWithCurrencyEffect?.[
dateString dateString
] ?? new Big(0); ] ?? new Big(0);
const investmentValueWithCurrencyEffect = const investmentValueWithCurrencyEffect =
symbolValues.investmentValuesWithCurrencyEffect?.[dateString] ?? assetProfileValues.investmentValuesWithCurrencyEffect?.[dateString] ??
new Big(0); new Big(0);
const netPerformanceValue = const netPerformanceValue =
symbolValues.netPerformanceValues?.[dateString] ?? new Big(0); assetProfileValues.netPerformanceValues?.[dateString] ?? new Big(0);
const netPerformanceValueWithCurrencyEffect = const netPerformanceValueWithCurrencyEffect =
symbolValues.netPerformanceValuesWithCurrencyEffect?.[dateString] ?? assetProfileValues.netPerformanceValuesWithCurrencyEffect?.[
new Big(0); dateString
] ?? new Big(0);
const netWorthValueWithCurrencyEffect = const netWorthValueWithCurrencyEffect =
symbolValues.netWorthValuesWithCurrencyEffect?.[dateString] ?? assetProfileValues.netWorthValuesWithCurrencyEffect?.[dateString] ??
new Big(0); new Big(0);
const timeWeightedInvestmentValue = const timeWeightedInvestmentValue =
symbolValues.timeWeightedInvestmentValues?.[dateString] ?? new Big(0); assetProfileValues.timeWeightedInvestmentValues?.[dateString] ??
new Big(0);
const timeWeightedInvestmentValueWithCurrencyEffect = const timeWeightedInvestmentValueWithCurrencyEffect =
symbolValues.timeWeightedInvestmentValuesWithCurrencyEffect?.[ assetProfileValues.timeWeightedInvestmentValuesWithCurrencyEffect?.[
dateString dateString
] ?? new Big(0); ] ?? new Big(0);
@ -989,7 +992,7 @@ export abstract class PortfolioCalculator {
@LogPerformance @LogPerformance
private computeTransactionPoints() { private computeTransactionPoints() {
this.transactionPoints = []; this.transactionPoints = [];
const symbols: { const transactionPointSymbols: {
[assetProfileIdentifier: string]: TransactionPointSymbol; [assetProfileIdentifier: string]: TransactionPointSymbol;
} = {}; } = {};
@ -1017,7 +1020,8 @@ export abstract class PortfolioCalculator {
const assetProfileIdentifier = getAssetProfileIdentifier(assetProfile); const assetProfileIdentifier = getAssetProfileIdentifier(assetProfile);
const oldAccumulatedSymbol = symbols[assetProfileIdentifier]; const oldAccumulatedSymbol =
transactionPointSymbols[assetProfileIdentifier];
if (oldAccumulatedSymbol) { if (oldAccumulatedSymbol) {
let investment = oldAccumulatedSymbol.investment; let investment = oldAccumulatedSymbol.investment;
@ -1099,7 +1103,8 @@ export abstract class PortfolioCalculator {
'id' 'id'
); );
symbols[assetProfileIdentifier] = currentTransactionPointItem; transactionPointSymbols[assetProfileIdentifier] =
currentTransactionPointItem;
const items = lastTransactionPoint?.items ?? []; const items = lastTransactionPoint?.items ?? [];
@ -1110,7 +1115,10 @@ export abstract class PortfolioCalculator {
newItems.push(currentTransactionPointItem); newItems.push(currentTransactionPointItem);
newItems.sort((a, b) => { newItems.sort((a, b) => {
return a.symbol?.localeCompare(b.symbol); return (
a.symbol?.localeCompare(b.symbol) ||
a.dataSource?.localeCompare(b.dataSource)
);
}); });
let fees = new Big(0); let fees = new Big(0);

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

@ -737,14 +737,20 @@ export class PortfolioService {
valueInBaseCurrency: emergencyFundInCash valueInBaseCurrency: emergencyFundInCash
}; };
const indexOfHoldingInBaseCurrency = holdings.findIndex( const emergencyFundCashHoldingAssetProfileIdentifier =
getAssetProfileIdentifier(emergencyFundCashHolding.assetProfile);
const indexOfEmergencyFundCashHolding = holdings.findIndex(
({ assetProfile }) => { ({ assetProfile }) => {
return assetProfile.symbol === userCurrency; return (
getAssetProfileIdentifier(assetProfile) ===
emergencyFundCashHoldingAssetProfileIdentifier
);
} }
); );
if (indexOfHoldingInBaseCurrency >= 0) { if (indexOfEmergencyFundCashHolding >= 0) {
holdings[indexOfHoldingInBaseCurrency] = emergencyFundCashHolding; holdings[indexOfEmergencyFundCashHolding] = emergencyFundCashHolding;
} else { } else {
holdings.push(emergencyFundCashHolding); holdings.push(emergencyFundCashHolding);
} }

56
apps/api/src/helper/object.helper.spec.ts

@ -109,8 +109,8 @@ describe('redactAttributes', () => {
} }
}, },
hasError: false, hasError: false,
holdings: { holdings: [
'AAPL.US': { {
activitiesCount: 1, activitiesCount: 1,
currency: 'USD', currency: 'USD',
markets: { markets: {
@ -162,7 +162,7 @@ describe('redactAttributes', () => {
valueInBaseCurrency: 11039.5, valueInBaseCurrency: 11039.5,
valueInPercentage: 0.0694356974830054 valueInPercentage: 0.0694356974830054
}, },
'ALV.DE': { {
activitiesCount: 2, activitiesCount: 2,
currency: 'EUR', currency: 'EUR',
markets: { markets: {
@ -209,7 +209,7 @@ describe('redactAttributes', () => {
valueInBaseCurrency: 6616.826601205088, valueInBaseCurrency: 6616.826601205088,
valueInPercentage: 0.04161818652826481 valueInPercentage: 0.04161818652826481
}, },
AMZN: { {
activitiesCount: 1, activitiesCount: 1,
currency: 'USD', currency: 'USD',
markets: { markets: {
@ -261,7 +261,7 @@ describe('redactAttributes', () => {
valueInBaseCurrency: 18799, valueInBaseCurrency: 18799,
valueInPercentage: 0.11824101426541227 valueInPercentage: 0.11824101426541227
}, },
bitcoin: { {
activitiesCount: 1, activitiesCount: 1,
currency: 'USD', currency: 'USD',
markets: { markets: {
@ -312,7 +312,7 @@ describe('redactAttributes', () => {
valueInBaseCurrency: 36985.0332704, valueInBaseCurrency: 36985.0332704,
valueInPercentage: 0.232626620912395 valueInPercentage: 0.232626620912395
}, },
BONDORA_GO_AND_GROW: { {
activitiesCount: 5, activitiesCount: 5,
currency: 'EUR', currency: 'EUR',
markets: { markets: {
@ -363,7 +363,7 @@ describe('redactAttributes', () => {
valueInBaseCurrency: 2231.644722160232, valueInBaseCurrency: 2231.644722160232,
valueInPercentage: 0.014036487867880205 valueInPercentage: 0.014036487867880205
}, },
FRANKLY95P: { {
activitiesCount: 6, activitiesCount: 6,
currency: 'CHF', currency: 'CHF',
markets: { markets: {
@ -487,7 +487,7 @@ describe('redactAttributes', () => {
valueInBaseCurrency: 22363.19795483481, valueInBaseCurrency: 22363.19795483481,
valueInPercentage: 0.14065892911313693 valueInPercentage: 0.14065892911313693
}, },
MSFT: { {
activitiesCount: 1, activitiesCount: 1,
currency: 'USD', currency: 'USD',
markets: { markets: {
@ -539,7 +539,7 @@ describe('redactAttributes', () => {
valueInBaseCurrency: 12840.6, valueInBaseCurrency: 12840.6,
valueInPercentage: 0.08076416659271518 valueInPercentage: 0.08076416659271518
}, },
TSLA: { {
activitiesCount: 1, activitiesCount: 1,
currency: 'USD', currency: 'USD',
markets: { markets: {
@ -591,7 +591,7 @@ describe('redactAttributes', () => {
valueInBaseCurrency: 39069, valueInBaseCurrency: 39069,
valueInPercentage: 0.2457342510950259 valueInPercentage: 0.2457342510950259
}, },
VTI: { {
activitiesCount: 5, activitiesCount: 5,
currency: 'USD', currency: 'USD',
markets: { markets: {
@ -763,7 +763,7 @@ describe('redactAttributes', () => {
valueInBaseCurrency: 14102.5, valueInBaseCurrency: 14102.5,
valueInPercentage: 0.08870120238725339 valueInPercentage: 0.08870120238725339
}, },
'VWRL.SW': { {
activitiesCount: 5, activitiesCount: 5,
currency: 'CHF', currency: 'CHF',
markets: { markets: {
@ -1171,7 +1171,7 @@ describe('redactAttributes', () => {
valueInBaseCurrency: 23079.20085622547, valueInBaseCurrency: 23079.20085622547,
valueInPercentage: 0.145162408515095 valueInPercentage: 0.145162408515095
}, },
'XDWD.DE': { {
activitiesCount: 1, activitiesCount: 1,
currency: 'EUR', currency: 'EUR',
markets: { markets: {
@ -1449,7 +1449,7 @@ describe('redactAttributes', () => {
valueInBaseCurrency: 8847.35550100424, valueInBaseCurrency: 8847.35550100424,
valueInPercentage: 0.055647656152211074 valueInPercentage: 0.055647656152211074
}, },
USD: { {
activitiesCount: 0, activitiesCount: 0,
currency: 'USD', currency: 'USD',
allocationInPercentage: 0.20291717628620132, allocationInPercentage: 0.20291717628620132,
@ -1476,7 +1476,7 @@ describe('redactAttributes', () => {
valueInBaseCurrency: 49890, valueInBaseCurrency: 49890,
valueInPercentage: 0.3137956381563603 valueInPercentage: 0.3137956381563603
} }
}, ],
platforms: { platforms: {
'a5b14588-49a0-48e4-b9f7-e186b27860b7': { 'a5b14588-49a0-48e4-b9f7-e186b27860b7': {
balance: 0, balance: 0,
@ -1613,8 +1613,8 @@ describe('redactAttributes', () => {
} }
}, },
hasError: false, hasError: false,
holdings: { holdings: [
'AAPL.US': { {
activitiesCount: 1, activitiesCount: 1,
currency: 'USD', currency: 'USD',
markets: { markets: {
@ -1666,7 +1666,7 @@ describe('redactAttributes', () => {
valueInBaseCurrency: null, valueInBaseCurrency: null,
valueInPercentage: 0.0694356974830054 valueInPercentage: 0.0694356974830054
}, },
'ALV.DE': { {
activitiesCount: 2, activitiesCount: 2,
currency: 'EUR', currency: 'EUR',
markets: { markets: {
@ -1713,7 +1713,7 @@ describe('redactAttributes', () => {
valueInBaseCurrency: null, valueInBaseCurrency: null,
valueInPercentage: 0.04161818652826481 valueInPercentage: 0.04161818652826481
}, },
AMZN: { {
activitiesCount: 1, activitiesCount: 1,
currency: 'USD', currency: 'USD',
markets: { markets: {
@ -1765,7 +1765,7 @@ describe('redactAttributes', () => {
valueInBaseCurrency: null, valueInBaseCurrency: null,
valueInPercentage: 0.11824101426541227 valueInPercentage: 0.11824101426541227
}, },
bitcoin: { {
activitiesCount: 1, activitiesCount: 1,
currency: 'USD', currency: 'USD',
markets: { markets: {
@ -1816,7 +1816,7 @@ describe('redactAttributes', () => {
valueInBaseCurrency: null, valueInBaseCurrency: null,
valueInPercentage: 0.232626620912395 valueInPercentage: 0.232626620912395
}, },
BONDORA_GO_AND_GROW: { {
activitiesCount: 5, activitiesCount: 5,
currency: 'EUR', currency: 'EUR',
markets: { markets: {
@ -1867,7 +1867,7 @@ describe('redactAttributes', () => {
valueInBaseCurrency: null, valueInBaseCurrency: null,
valueInPercentage: 0.014036487867880205 valueInPercentage: 0.014036487867880205
}, },
FRANKLY95P: { {
activitiesCount: 6, activitiesCount: 6,
currency: 'CHF', currency: 'CHF',
markets: { markets: {
@ -1971,7 +1971,7 @@ describe('redactAttributes', () => {
valueInBaseCurrency: null, valueInBaseCurrency: null,
valueInPercentage: 0.14065892911313693 valueInPercentage: 0.14065892911313693
}, },
MSFT: { {
activitiesCount: 1, activitiesCount: 1,
currency: 'USD', currency: 'USD',
markets: { markets: {
@ -2023,7 +2023,7 @@ describe('redactAttributes', () => {
valueInBaseCurrency: null, valueInBaseCurrency: null,
valueInPercentage: 0.08076416659271518 valueInPercentage: 0.08076416659271518
}, },
TSLA: { {
activitiesCount: 1, activitiesCount: 1,
currency: 'USD', currency: 'USD',
markets: { markets: {
@ -2075,7 +2075,7 @@ describe('redactAttributes', () => {
valueInBaseCurrency: null, valueInBaseCurrency: null,
valueInPercentage: 0.2457342510950259 valueInPercentage: 0.2457342510950259
}, },
VTI: { {
activitiesCount: 5, activitiesCount: 5,
currency: 'USD', currency: 'USD',
markets: { markets: {
@ -2247,7 +2247,7 @@ describe('redactAttributes', () => {
valueInBaseCurrency: null, valueInBaseCurrency: null,
valueInPercentage: 0.08870120238725339 valueInPercentage: 0.08870120238725339
}, },
'VWRL.SW': { {
activitiesCount: 5, activitiesCount: 5,
currency: 'CHF', currency: 'CHF',
markets: { markets: {
@ -2647,7 +2647,7 @@ describe('redactAttributes', () => {
valueInBaseCurrency: null, valueInBaseCurrency: null,
valueInPercentage: 0.145162408515095 valueInPercentage: 0.145162408515095
}, },
'XDWD.DE': { {
activitiesCount: 1, activitiesCount: 1,
currency: 'EUR', currency: 'EUR',
markets: { markets: {
@ -2925,7 +2925,7 @@ describe('redactAttributes', () => {
valueInBaseCurrency: null, valueInBaseCurrency: null,
valueInPercentage: 0.055647656152211074 valueInPercentage: 0.055647656152211074
}, },
USD: { {
activitiesCount: 0, activitiesCount: 0,
currency: 'USD', currency: 'USD',
allocationInPercentage: 0.20291717628620132, allocationInPercentage: 0.20291717628620132,
@ -2952,7 +2952,7 @@ describe('redactAttributes', () => {
valueInBaseCurrency: null, valueInBaseCurrency: null,
valueInPercentage: 0.3137956381563603 valueInPercentage: 0.3137956381563603
} }
}, ],
platforms: { platforms: {
'a5b14588-49a0-48e4-b9f7-e186b27860b7': { 'a5b14588-49a0-48e4-b9f7-e186b27860b7': {
balance: null, balance: null,

27
apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts

@ -119,7 +119,7 @@ export class GfAllocationsPageComponent implements OnInit {
[name: string]: { name: string; value: number }; [name: string]: { name: string; value: number };
}; };
protected symbols: { protected symbols: {
[name: string]: { [symbol: string]: {
dataSource?: DataSource; dataSource?: DataSource;
isClickable?: boolean; isClickable?: boolean;
name: string; name: string;
@ -505,17 +505,28 @@ export class GfAllocationsPageComponent implements OnInit {
this.totalValueInEtf += this.holdings[assetProfileIdentifier].value; this.totalValueInEtf += this.holdings[assetProfileIdentifier].value;
} }
this.symbols[assetProfileIdentifier] = { const symbol = position.assetProfile.symbol;
dataSource: position.assetProfile.dataSource,
isClickable: canOpenHoldingDetail(position), const value =
name: position.assetProfile.name ?? '',
symbol: position.assetProfile.symbol,
value:
(isNumber(position.valueInBaseCurrency) (isNumber(position.valueInBaseCurrency)
? position.valueInBaseCurrency ? position.valueInBaseCurrency
: position.valueInPercentage) ?? 0 : position.valueInPercentage) ?? 0;
const symbolData = this.symbols[symbol];
if (symbolData) {
// Aggregate holdings with the same symbol from different data sources
symbolData.value += value;
} else {
this.symbols[symbol] = {
symbol,
value,
dataSource: position.assetProfile.dataSource,
isClickable: canOpenHoldingDetail(position),
name: position.assetProfile.name ?? ''
}; };
} }
}
this.markets = this.portfolioDetails.markets; this.markets = this.portfolioDetails.markets;

23
apps/client/src/app/pages/public/public-page.component.ts

@ -93,7 +93,7 @@ export class GfPublicPageComponent implements OnInit {
[name: string]: { name: string; value: number }; [name: string]: { name: string; value: number };
}; };
protected symbols: { protected symbols: {
[name: string]: { name: string; symbol: string; value: number }; [symbol: string]: { name: string; symbol: string; value: number };
}; };
protected readonly UNKNOWN_KEY = UNKNOWN_KEY; protected readonly UNKNOWN_KEY = UNKNOWN_KEY;
@ -245,13 +245,24 @@ export class GfPublicPageComponent implements OnInit {
} }
} }
this.symbols[assetProfileIdentifier] = { const symbol = position.assetProfile.symbol;
name: position.assetProfile.name ?? position.assetProfile.symbol,
symbol: position.assetProfile.symbol, const value = isNumber(position.valueInBaseCurrency)
value: isNumber(position.valueInBaseCurrency)
? position.valueInBaseCurrency ? position.valueInBaseCurrency
: (position.valueInPercentage ?? 0) : (position.valueInPercentage ?? 0);
const symbolData = this.symbols[symbol];
if (symbolData) {
// Aggregate holdings with the same symbol from different data sources
symbolData.value += value;
} else {
this.symbols[symbol] = {
symbol,
value,
name: position.assetProfile.name ?? symbol
}; };
} }
} }
} }
}

Loading…
Cancel
Save