Browse Source

Fix portfolio calculation for holdings with same symbol

pull/7664/head
Thomas Kaul 1 week ago
parent
commit
e931543388
  1. 2
      apps/api/src/app/endpoints/ai/ai.service.ts
  2. 10
      apps/api/src/app/endpoints/public/public.service.ts
  3. 2
      apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts
  4. 70
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  5. 25
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts
  6. 2
      apps/api/src/app/portfolio/calculator/roi/portfolio-calculator.ts
  7. 2
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts
  8. 10
      apps/api/src/app/portfolio/portfolio.controller.ts
  9. 60
      apps/api/src/app/portfolio/portfolio.service.spec.ts
  10. 61
      apps/api/src/app/portfolio/portfolio.service.ts
  11. 29
      apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
  12. 50
      apps/client/src/app/pages/public/public-page.component.ts
  13. 2
      libs/common/src/lib/interfaces/portfolio-details.interface.ts
  14. 6
      libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts
  15. 46
      libs/ui/src/lib/services/data.service.ts

2
apps/api/src/app/endpoints/ai/ai.service.ts

@ -118,7 +118,7 @@ export class AiService {
values: Object.values(AssetSubClass) values: Object.values(AssetSubClass)
}); });
const holdingsTableRows = Object.values(holdings) const holdingsTableRows = holdings
.sort((a, b) => { .sort((a, b) => {
return b.allocationInPercentage - a.allocationInPercentage; return b.allocationInPercentage - a.allocationInPercentage;
}) })

10
apps/api/src/app/endpoints/public/public.service.ts

@ -133,7 +133,7 @@ export class PublicService {
latestActivities, latestActivities,
markets, markets,
alias: access.alias, alias: access.alias,
holdings: {}, holdings: [],
performance: { performance: {
'1d': { '1d': {
relativeChange: relativeChange:
@ -151,7 +151,7 @@ export class PublicService {
}; };
const totalValue = getSum( const totalValue = getSum(
Object.values(holdings).map(({ assetProfile, marketPrice, quantity }) => { holdings.map(({ assetProfile, marketPrice, quantity }) => {
return new Big( return new Big(
this.exchangeRateDataService.toCurrency( this.exchangeRateDataService.toCurrency(
quantity * marketPrice, quantity * marketPrice,
@ -162,8 +162,8 @@ export class PublicService {
}) })
).toNumber(); ).toNumber();
for (const [symbol, portfolioPosition] of Object.entries(holdings)) { for (const portfolioPosition of holdings) {
publicPortfolioResponse.holdings[symbol] = { publicPortfolioResponse.holdings.push({
allocationInPercentage: allocationInPercentage:
portfolioPosition.valueInBaseCurrency / totalValue, portfolioPosition.valueInBaseCurrency / totalValue,
assetProfile: { assetProfile: {
@ -207,7 +207,7 @@ export class PublicService {
netPerformancePercentWithCurrencyEffect: netPerformancePercentWithCurrencyEffect:
portfolioPosition.netPerformancePercentWithCurrencyEffect, portfolioPosition.netPerformancePercentWithCurrencyEffect,
valueInPercentage: portfolioPosition.valueInBaseCurrency / totalValue valueInPercentage: portfolioPosition.valueInBaseCurrency / totalValue
}; });
} }
return publicPortfolioResponse; return publicPortfolioResponse;

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

@ -19,7 +19,7 @@ export class MwrPortfolioCalculator extends PortfolioCalculator {
end: Date; end: Date;
exchangeRates: { [dateString: string]: number }; exchangeRates: { [dateString: string]: number };
marketSymbolMap: { marketSymbolMap: {
[date: string]: { [symbol: string]: Big }; [date: string]: { [assetProfileIdentifier: string]: Big };
}; };
start: Date; start: Date;
step?: number; step?: number;

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

@ -22,6 +22,7 @@ import {
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { import {
DATE_FORMAT, DATE_FORMAT,
getAssetProfileIdentifier,
getSum, getSum,
parseDate, parseDate,
resetHours resetHours
@ -72,8 +73,8 @@ export abstract class PortfolioCalculator {
protected accountBalanceItems: HistoricalDataItem[]; protected accountBalanceItems: HistoricalDataItem[];
protected activities: PortfolioOrder[]; protected activities: PortfolioOrder[];
protected activitiesBySymbol: { protected activitiesByAssetProfileIdentifier: {
[symbol: string]: PortfolioOrder[]; [assetProfileIdentifier: string]: PortfolioOrder[];
}; };
private configurationService: ConfigurationService; private configurationService: ConfigurationService;
@ -165,9 +166,12 @@ export abstract class PortfolioCalculator {
return a.date?.localeCompare(b.date); return a.date?.localeCompare(b.date);
}); });
this.activitiesBySymbol = groupBy(this.activities, ({ assetProfile }) => { this.activitiesByAssetProfileIdentifier = groupBy(
return assetProfile.symbol; this.activities,
}); ({ assetProfile }) => {
return getAssetProfileIdentifier(assetProfile);
}
);
this.portfolioSnapshotService = portfolioSnapshotService; this.portfolioSnapshotService = portfolioSnapshotService;
this.redisCacheService = redisCacheService; this.redisCacheService = redisCacheService;
@ -221,8 +225,8 @@ export abstract class PortfolioCalculator {
}; };
} }
const cashSymbols = new Set<string>(); const cashAssetProfileIdentifiers = new Set<string>();
const currencies: { [symbol: string]: string } = {}; const currencies: { [assetProfileIdentifier: string]: string } = {};
const dataGatheringItems: DataGatheringItem[] = []; const dataGatheringItems: DataGatheringItem[] = [];
let firstIndex = transactionPoints.length; let firstIndex = transactionPoints.length;
let firstTransactionPoint: TransactionPoint = null; let firstTransactionPoint: TransactionPoint = null;
@ -244,7 +248,7 @@ export abstract class PortfolioCalculator {
}); });
} }
currencies[symbol] = currency; currencies[getAssetProfileIdentifier({ dataSource, symbol })] = currency;
} }
for (let i = 0; i < transactionPoints.length; i++) { for (let i = 0; i < transactionPoints.length; i++) {
@ -280,7 +284,7 @@ export abstract class PortfolioCalculator {
this.dataProviderInfos = dataProviderInfos; this.dataProviderInfos = dataProviderInfos;
const marketSymbolMap: { const marketSymbolMap: {
[date: string]: { [symbol: string]: Big }; [date: string]: { [assetProfileIdentifier: string]: Big };
} = {}; } = {};
for (const marketSymbol of marketSymbols) { for (const marketSymbol of marketSymbols) {
@ -291,9 +295,8 @@ export abstract class PortfolioCalculator {
} }
if (marketSymbol.marketPrice) { if (marketSymbol.marketPrice) {
marketSymbolMap[date][marketSymbol.symbol] = new Big( marketSymbolMap[date][getAssetProfileIdentifier(marketSymbol)] =
marketSymbol.marketPrice new Big(marketSymbol.marketPrice);
);
} }
} }
@ -346,8 +349,8 @@ export abstract class PortfolioCalculator {
}; };
} = {}; } = {};
const valuesBySymbol: { const valuesByAssetProfileIdentifier: {
[symbol: string]: { [assetProfileIdentifier: string]: {
currentValues: { [date: string]: Big }; currentValues: { [date: string]: Big };
currentValuesWithCurrencyEffect: { [date: string]: Big }; currentValuesWithCurrencyEffect: { [date: string]: Big };
investmentValuesAccumulated: { [date: string]: Big }; investmentValuesAccumulated: { [date: string]: Big };
@ -362,8 +365,11 @@ export abstract class PortfolioCalculator {
} = {}; } = {};
for (const item of lastTransactionPoint.items) { for (const item of lastTransactionPoint.items) {
const assetProfileIdentifier = getAssetProfileIdentifier(item);
const marketPriceInBaseCurrency = ( const marketPriceInBaseCurrency = (
marketSymbolMap[endDateString]?.[item.symbol] ?? item.averagePrice marketSymbolMap[endDateString]?.[assetProfileIdentifier] ??
item.averagePrice
).mul( ).mul(
exchangeRatesByCurrency[`${item.currency}${this.currency}`]?.[ exchangeRatesByCurrency[`${item.currency}${this.currency}`]?.[
endDateString endDateString
@ -421,7 +427,8 @@ export abstract class PortfolioCalculator {
// contributes nothing but its balance to the performance calculation. It // contributes nothing but its balance to the performance calculation. It
// is therefore excluded from the value and the investment, while still // is therefore excluded from the value and the investment, while still
// contributing to the net worth. // contributing to the net worth.
valuesBySymbol[item.symbol] = isCashInBaseCurrency valuesByAssetProfileIdentifier[assetProfileIdentifier] =
isCashInBaseCurrency
? { ? {
currentValues: {}, currentValues: {},
currentValuesWithCurrencyEffect: {}, currentValuesWithCurrencyEffect: {},
@ -474,7 +481,9 @@ export abstract class PortfolioCalculator {
investment: totalInvestment, investment: totalInvestment,
investmentWithCurrencyEffect: totalInvestmentWithCurrencyEffect, investmentWithCurrencyEffect: totalInvestmentWithCurrencyEffect,
marketPrice: marketPrice:
marketSymbolMap[endDateString]?.[item.symbol]?.toNumber() ?? 1, marketSymbolMap[endDateString]?.[
assetProfileIdentifier
]?.toNumber() ?? 1,
marketPriceInBaseCurrency: marketPriceInBaseCurrency?.toNumber() ?? 1, marketPriceInBaseCurrency: marketPriceInBaseCurrency?.toNumber() ?? 1,
netPerformance: !hasErrors ? (netPerformance ?? null) : null, netPerformance: !hasErrors ? (netPerformance ?? null) : null,
netPerformancePercentage: !hasErrors netPerformancePercentage: !hasErrors
@ -493,7 +502,7 @@ export abstract class PortfolioCalculator {
}); });
if (item.assetSubClass === AssetSubClass.CASH) { if (item.assetSubClass === AssetSubClass.CASH) {
cashSymbols.add(item.symbol); cashAssetProfileIdentifiers.add(assetProfileIdentifier);
totalCashInBaseCurrency = totalCashInBaseCurrency =
totalCashInBaseCurrency.plus(valueInBaseCurrency); totalCashInBaseCurrency.plus(valueInBaseCurrency);
@ -519,8 +528,11 @@ export abstract class PortfolioCalculator {
} }
for (const dateString of chartDates) { for (const dateString of chartDates) {
for (const symbol of Object.keys(valuesBySymbol)) { for (const assetProfileIdentifier of Object.keys(
const symbolValues = valuesBySymbol[symbol]; valuesByAssetProfileIdentifier
)) {
const symbolValues =
valuesByAssetProfileIdentifier[assetProfileIdentifier];
const currentValue = const currentValue =
symbolValues.currentValues?.[dateString] ?? new Big(0); symbolValues.currentValues?.[dateString] ?? new Big(0);
@ -569,7 +581,7 @@ export abstract class PortfolioCalculator {
accumulatedValuesByDate[dateString] accumulatedValuesByDate[dateString]
?.totalCashValueWithCurrencyEffect ?? new Big(0) ?.totalCashValueWithCurrencyEffect ?? new Big(0)
).add( ).add(
cashSymbols.has(symbol) cashAssetProfileIdentifiers.has(assetProfileIdentifier)
? netWorthValueWithCurrencyEffect ? netWorthValueWithCurrencyEffect
: new Big(0) : new Big(0)
), ),
@ -879,7 +891,7 @@ export abstract class PortfolioCalculator {
end: Date; end: Date;
exchangeRates: { [dateString: string]: number }; exchangeRates: { [dateString: string]: number };
marketSymbolMap: { marketSymbolMap: {
[date: string]: { [symbol: string]: Big }; [date: string]: { [assetProfileIdentifier: string]: Big };
}; };
start: Date; start: Date;
} & AssetProfileIdentifier): SymbolMetrics; } & AssetProfileIdentifier): SymbolMetrics;
@ -977,7 +989,9 @@ export abstract class PortfolioCalculator {
@LogPerformance @LogPerformance
private computeTransactionPoints() { private computeTransactionPoints() {
this.transactionPoints = []; this.transactionPoints = [];
const symbols: { [symbol: string]: TransactionPointSymbol } = {}; const symbols: {
[assetProfileIdentifier: string]: TransactionPointSymbol;
} = {};
let lastDate: string = null; let lastDate: string = null;
let lastTransactionPoint: TransactionPoint = null; let lastTransactionPoint: TransactionPoint = null;
@ -1001,7 +1015,9 @@ export abstract class PortfolioCalculator {
const skipErrors = !!assetProfile.userId; // Skip errors for custom asset profiles const skipErrors = !!assetProfile.userId; // Skip errors for custom asset profiles
const symbol = assetProfile.symbol; const symbol = assetProfile.symbol;
const oldAccumulatedSymbol = symbols[symbol]; const assetProfileIdentifier = getAssetProfileIdentifier(assetProfile);
const oldAccumulatedSymbol = symbols[assetProfileIdentifier];
if (oldAccumulatedSymbol) { if (oldAccumulatedSymbol) {
let investment = oldAccumulatedSymbol.investment; let investment = oldAccumulatedSymbol.investment;
@ -1083,12 +1099,12 @@ export abstract class PortfolioCalculator {
'id' 'id'
); );
symbols[symbol] = currentTransactionPointItem; symbols[assetProfileIdentifier] = currentTransactionPointItem;
const items = lastTransactionPoint?.items ?? []; const items = lastTransactionPoint?.items ?? [];
const newItems = items.filter(({ symbol }) => { const newItems = items.filter((item) => {
return symbol !== assetProfile.symbol; return getAssetProfileIdentifier(item) !== assetProfileIdentifier;
}); });
newItems.push(currentTransactionPointItem); newItems.push(currentTransactionPointItem);

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

@ -3,7 +3,11 @@ import { PortfolioCalculatorPosition } from '@ghostfolio/api/app/portfolio/inter
import { PortfolioOrderItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order-item.interface'; import { PortfolioOrderItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order-item.interface';
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper'; import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper';
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper'; import {
DATE_FORMAT,
getAssetProfileIdentifier,
parseDate
} from '@ghostfolio/common/helper';
import { import {
AssetProfileIdentifier, AssetProfileIdentifier,
SymbolMetrics SymbolMetrics
@ -140,7 +144,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
end: Date; end: Date;
exchangeRates: { [dateString: string]: number }; exchangeRates: { [dateString: string]: number };
marketSymbolMap: { marketSymbolMap: {
[date: string]: { [symbol: string]: Big }; [date: string]: { [assetProfileIdentifier: string]: Big };
}; };
start: Date; start: Date;
} & AssetProfileIdentifier): SymbolMetrics { } & AssetProfileIdentifier): SymbolMetrics {
@ -192,10 +196,15 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
let valueAtStartDate: Big; let valueAtStartDate: Big;
let valueAtStartDateWithCurrencyEffect: Big; let valueAtStartDateWithCurrencyEffect: Big;
const assetProfileIdentifier = getAssetProfileIdentifier({
dataSource,
symbol
});
// Copy the items as they are enriched below. A shallow copy is sufficient // Copy the items as they are enriched below. A shallow copy is sufficient
// because only top-level properties are written. // because only top-level properties are written.
let orders: PortfolioOrderItem[] = ( let orders: PortfolioOrderItem[] = (
this.activitiesBySymbol[symbol] ?? [] this.activitiesByAssetProfileIdentifier[assetProfileIdentifier] ?? []
).map((activity) => { ).map((activity) => {
return { ...activity }; return { ...activity };
}); });
@ -275,8 +284,11 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
const endDateString = format(end, DATE_FORMAT); const endDateString = format(end, DATE_FORMAT);
const startDateString = format(start, DATE_FORMAT); const startDateString = format(start, DATE_FORMAT);
const unitPriceAtStartDate = marketSymbolMap[startDateString]?.[symbol]; const unitPriceAtStartDate =
let unitPriceAtEndDate = marketSymbolMap[endDateString]?.[symbol]; marketSymbolMap[startDateString]?.[assetProfileIdentifier];
let unitPriceAtEndDate =
marketSymbolMap[endDateString]?.[assetProfileIdentifier];
const latestActivity = orders.at(-1); const latestActivity = orders.at(-1);
@ -391,7 +403,8 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
break; break;
} }
const unitPrice = marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice; const unitPrice =
marketSymbolMap[dateString]?.[assetProfileIdentifier] ?? lastUnitPrice;
if (ordersByDate[dateString]?.length > 0) { if (ordersByDate[dateString]?.length > 0) {
for (const order of ordersByDate[dateString]) { for (const order of ordersByDate[dateString]) {

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

@ -19,7 +19,7 @@ export class RoiPortfolioCalculator extends PortfolioCalculator {
end: Date; end: Date;
exchangeRates: { [dateString: string]: number }; exchangeRates: { [dateString: string]: number };
marketSymbolMap: { marketSymbolMap: {
[date: string]: { [symbol: string]: Big }; [date: string]: { [assetProfileIdentifier: string]: Big };
}; };
start: Date; start: Date;
step?: number; step?: number;

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

@ -19,7 +19,7 @@ export class TwrPortfolioCalculator extends PortfolioCalculator {
end: Date; end: Date;
exchangeRates: { [dateString: string]: number }; exchangeRates: { [dateString: string]: number };
marketSymbolMap: { marketSymbolMap: {
[date: string]: { [symbol: string]: Big }; [date: string]: { [assetProfileIdentifier: string]: Big };
}; };
start: Date; start: Date;
step?: number; step?: number;

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

@ -128,13 +128,13 @@ export class PortfolioController {
!hasScope(impersonationScopes, scopes.portfolioReadValues) || !hasScope(impersonationScopes, scopes.portfolioReadValues) ||
isRestrictedView(this.request.user) isRestrictedView(this.request.user)
) { ) {
const totalInvestment = Object.values(holdings) const totalInvestment = holdings
.map(({ investment }) => { .map(({ investment }) => {
return investment; return investment;
}) })
.reduce((a, b) => a + b, 0); .reduce((a, b) => a + b, 0);
const totalValue = Object.values(holdings) const totalValue = holdings
.filter(({ assetProfile }) => { .filter(({ assetProfile }) => {
return ( return (
assetProfile.assetClass !== AssetClass.LIQUIDITY && assetProfile.assetClass !== AssetClass.LIQUIDITY &&
@ -148,7 +148,7 @@ export class PortfolioController {
return a + b; return a + b;
}, 0); }, 0);
for (const [, portfolioPosition] of Object.entries(holdings)) { for (const portfolioPosition of holdings) {
portfolioPosition.investment = portfolioPosition.investment =
portfolioPosition.investment / totalInvestment; portfolioPosition.investment / totalInvestment;
portfolioPosition.valueInPercentage = portfolioPosition.valueInPercentage =
@ -204,8 +204,8 @@ export class PortfolioController {
]); ]);
} }
for (const [symbol, portfolioPosition] of Object.entries(holdings)) { for (const [index, portfolioPosition] of holdings.entries()) {
holdings[symbol] = { holdings[index] = {
...portfolioPosition, ...portfolioPosition,
assetProfile: { assetProfile: {
...portfolioPosition.assetProfile, ...portfolioPosition.assetProfile,

60
apps/api/src/app/portfolio/portfolio.service.spec.ts

@ -115,10 +115,10 @@ describe('PortfolioService', () => {
}); });
describe('getAggregatedMarkets', () => { describe('getAggregatedMarkets', () => {
const getAggregatedMarkets = (holdings: object) => { const getAggregatedMarkets = (holdings: object[]) => {
return ( return (
portfolioService as unknown as { portfolioService as unknown as {
getAggregatedMarkets: (aHoldings: object) => { getAggregatedMarkets: (aHoldings: object[]) => {
markets: Record< markets: Record<
string, string,
{ valueInBaseCurrency: number; valueInPercentage: number } { valueInBaseCurrency: number; valueInPercentage: number }
@ -130,9 +130,9 @@ describe('PortfolioService', () => {
}; };
it('should distribute holdings with countries to their market and route holdings without countries (e.g. commodities, cryptocurrencies) to the unknown bucket', () => { it('should distribute holdings with countries to their market and route holdings without countries (e.g. commodities, cryptocurrencies) to the unknown bucket', () => {
const holdings = { const holdings = [
'GC=F': { {
// Gold // Gold (GC=F)
assetProfile: { countries: [] }, assetProfile: { countries: [] },
markets: { developedMarkets: 0, emergingMarkets: 0, otherMarkets: 0 }, markets: { developedMarkets: 0, emergingMarkets: 0, otherMarkets: 0 },
marketsAdvanced: { marketsAdvanced: {
@ -145,7 +145,8 @@ describe('PortfolioService', () => {
}, },
valueInBaseCurrency: 500 valueInBaseCurrency: 500
}, },
MSFT: { {
// MSFT
assetProfile: { countries: [{ code: 'US', weight: 1 }] }, assetProfile: { countries: [{ code: 'US', weight: 1 }] },
markets: { developedMarkets: 1, emergingMarkets: 0, otherMarkets: 0 }, markets: { developedMarkets: 1, emergingMarkets: 0, otherMarkets: 0 },
marketsAdvanced: { marketsAdvanced: {
@ -158,7 +159,7 @@ describe('PortfolioService', () => {
}, },
valueInBaseCurrency: 1000 valueInBaseCurrency: 1000
} }
}; ];
const { markets, marketsAdvanced } = getAggregatedMarkets(holdings); const { markets, marketsAdvanced } = getAggregatedMarkets(holdings);
@ -325,9 +326,14 @@ describe('PortfolioService', () => {
userId: userDummyData.id userId: userDummyData.id
}); });
expect(holdings['USD']).toBeDefined(); expect(holdings).toEqual([
expect(holdings['USD'].assetProfile.dataSource).toBe(DataSource.YAHOO); expect.objectContaining({
expect(holdings['USD'].assetProfile.symbol).toBe('USD'); assetProfile: expect.objectContaining({
dataSource: DataSource.YAHOO,
symbol: 'USD'
})
})
]);
}); });
}); });
@ -450,22 +456,22 @@ describe('PortfolioService', () => {
{ {
account, account,
accountId: account.id, accountId: account.id,
assetProfile: { symbol: 'AAPL' }, assetProfile: { dataSource: DataSource.YAHOO, symbol: 'AAPL' },
quantity: 1, quantity: 1,
type: 'BUY' type: 'BUY'
}, },
{ {
account: null, account: null,
accountId: null, accountId: null,
assetProfile: { symbol: 'BABA' }, assetProfile: { dataSource: DataSource.YAHOO, symbol: 'BABA' },
quantity: 2, quantity: 2,
type: 'BUY' type: 'BUY'
} }
], ],
filters: [], filters: [],
portfolioItemsNow: { portfolioItemsNow: {
AAPL: { marketPriceInBaseCurrency: 10 }, 'YAHOO-AAPL': { marketPriceInBaseCurrency: 10 },
BABA: { marketPriceInBaseCurrency: 20 } 'YAHOO-BABA': { marketPriceInBaseCurrency: 20 }
}, },
userCurrency: 'USD', userCurrency: 'USD',
userId: userDummyData.id userId: userDummyData.id
@ -486,14 +492,14 @@ describe('PortfolioService', () => {
{ {
account, account,
accountId: account.id, accountId: account.id,
assetProfile: { symbol: 'AAPL' }, assetProfile: { dataSource: DataSource.YAHOO, symbol: 'AAPL' },
quantity: 1, quantity: 1,
type: 'BUY' type: 'BUY'
} }
], ],
filters: [], filters: [],
portfolioItemsNow: { portfolioItemsNow: {
AAPL: { marketPriceInBaseCurrency: 10 } 'YAHOO-AAPL': { marketPriceInBaseCurrency: 10 }
}, },
userCurrency: 'USD', userCurrency: 'USD',
userId: userDummyData.id userId: userDummyData.id
@ -509,28 +515,28 @@ describe('PortfolioService', () => {
{ {
account, account,
accountId: account.id, accountId: account.id,
assetProfile: { symbol: 'AAPL' }, assetProfile: { dataSource: DataSource.YAHOO, symbol: 'AAPL' },
quantity: 0.1, quantity: 0.1,
type: 'BUY' type: 'BUY'
}, },
{ {
account, account,
accountId: account.id, accountId: account.id,
assetProfile: { symbol: 'AAPL' }, assetProfile: { dataSource: DataSource.YAHOO, symbol: 'AAPL' },
quantity: 0.2, quantity: 0.2,
type: 'BUY' type: 'BUY'
}, },
{ {
account, account,
accountId: account.id, accountId: account.id,
assetProfile: { symbol: 'AAPL' }, assetProfile: { dataSource: DataSource.YAHOO, symbol: 'AAPL' },
quantity: 0.3, quantity: 0.3,
type: 'SELL' type: 'SELL'
} }
], ],
filters: [], filters: [],
portfolioItemsNow: { portfolioItemsNow: {
AAPL: { marketPriceInBaseCurrency: 1234.5678 } 'YAHOO-AAPL': { marketPriceInBaseCurrency: 1234.5678 }
}, },
userCurrency: 'USD', userCurrency: 'USD',
userId: userDummyData.id userId: userDummyData.id
@ -547,21 +553,21 @@ describe('PortfolioService', () => {
{ {
account, account,
accountId: account.id, accountId: account.id,
assetProfile: { symbol: 'AAPL' }, assetProfile: { dataSource: DataSource.YAHOO, symbol: 'AAPL' },
quantity: 0.1, quantity: 0.1,
type: 'BUY' type: 'BUY'
}, },
{ {
account, account,
accountId: account.id, accountId: account.id,
assetProfile: { symbol: 'AAPL' }, assetProfile: { dataSource: DataSource.YAHOO, symbol: 'AAPL' },
quantity: 0.2, quantity: 0.2,
type: 'BUY' type: 'BUY'
} }
], ],
filters: [{ id: 'AAPL', type: 'SYMBOL' }], filters: [{ id: 'AAPL', type: 'SYMBOL' }],
portfolioItemsNow: { portfolioItemsNow: {
AAPL: { marketPriceInBaseCurrency: 10 } 'YAHOO-AAPL': { marketPriceInBaseCurrency: 10 }
}, },
userCurrency: 'USD', userCurrency: 'USD',
userId: userDummyData.id userId: userDummyData.id
@ -576,14 +582,14 @@ describe('PortfolioService', () => {
{ {
account, account,
accountId: account.id, accountId: account.id,
assetProfile: { symbol: 'AAPL' }, assetProfile: { dataSource: DataSource.YAHOO, symbol: 'AAPL' },
quantity: 1, quantity: 1,
type: 'BUY' type: 'BUY'
} }
], ],
filters: [], filters: [],
portfolioItemsNow: { portfolioItemsNow: {
AAPL: { marketPriceInBaseCurrency: 10 } 'YAHOO-AAPL': { marketPriceInBaseCurrency: 10 }
}, },
userCurrency: 'USD', userCurrency: 'USD',
userId: userDummyData.id userId: userDummyData.id
@ -616,14 +622,14 @@ describe('PortfolioService', () => {
{ {
account, account,
accountId: account.id, accountId: account.id,
assetProfile: { symbol: 'AAPL' }, assetProfile: { dataSource: DataSource.YAHOO, symbol: 'AAPL' },
quantity: 1, quantity: 1,
type: 'BUY' type: 'BUY'
} }
], ],
filters: [{ id: 'AAPL', type: 'SYMBOL' }], filters: [{ id: 'AAPL', type: 'SYMBOL' }],
portfolioItemsNow: { portfolioItemsNow: {
AAPL: { marketPriceInBaseCurrency: 10 } 'YAHOO-AAPL': { marketPriceInBaseCurrency: 10 }
}, },
userCurrency: 'USD', userCurrency: 'USD',
userId: userDummyData.id userId: userDummyData.id

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

@ -397,14 +397,12 @@ export class PortfolioService {
return type !== 'SEARCH_QUERY'; return type !== 'SEARCH_QUERY';
}); });
const { holdings: holdingsMap } = await this.getDetails({ let { holdings } = await this.getDetails({
dateRange, dateRange,
userId, userId,
filters: filtersWithoutSearchQueryFilter filters: filtersWithoutSearchQueryFilter
}); });
let holdings = Object.values(holdingsMap);
if (filterBySearchQuery) { if (filterBySearchQuery) {
const fuse = new Fuse(holdings, { const fuse = new Fuse(holdings, {
keys: ['assetProfile.isin', 'assetProfile.name', 'assetProfile.symbol'], keys: ['assetProfile.isin', 'assetProfile.name', 'assetProfile.symbol'],
@ -546,7 +544,7 @@ export class PortfolioService {
currency: userCurrency currency: userCurrency
}); });
const holdings: PortfolioDetails['holdings'] = {}; const holdings: PortfolioDetails['holdings'] = [];
const { const {
HOLDING_TYPE: [filterByHoldingType] = [], HOLDING_TYPE: [filterByHoldingType] = [],
@ -586,9 +584,12 @@ export class PortfolioService {
] = symbolProfile; ] = symbolProfile;
} }
const portfolioItemsNow: { [symbol: string]: TimelinePosition } = {}; const portfolioItemsNow: {
[assetProfileIdentifier: string]: TimelinePosition;
} = {};
for (const position of positions) { for (const position of positions) {
portfolioItemsNow[position.symbol] = position; portfolioItemsNow[getAssetProfileIdentifier(position)] = position;
} }
for (const { for (const {
@ -643,7 +644,7 @@ export class PortfolioService {
})); }));
} }
holdings[symbol] = { holdings.push({
activitiesCount, activitiesCount,
markets, markets,
marketsAdvanced, marketsAdvanced,
@ -694,7 +695,7 @@ export class PortfolioService {
netPerformanceWithCurrencyEffectMap?.[dateRange]?.toNumber() ?? 0, netPerformanceWithCurrencyEffectMap?.[dateRange]?.toNumber() ?? 0,
quantity: quantity.toNumber(), quantity: quantity.toNumber(),
valueInBaseCurrency: valueInBaseCurrency.toNumber() valueInBaseCurrency: valueInBaseCurrency.toNumber()
}; });
} }
const { accounts, platforms } = await this.getValueOfAccountsAndPlatforms({ const { accounts, platforms } = await this.getValueOfAccountsAndPlatforms({
@ -730,11 +731,23 @@ export class PortfolioService {
valueInBaseCurrency: emergencyFundInCash valueInBaseCurrency: emergencyFundInCash
}; };
holdings[userCurrency] = { const emergencyFundCashHolding = {
...emergencyFundCashPositions[userCurrency], ...emergencyFundCashPositions[userCurrency],
investment: emergencyFundInCash, investment: emergencyFundInCash,
valueInBaseCurrency: emergencyFundInCash valueInBaseCurrency: emergencyFundInCash
}; };
const indexOfHoldingInBaseCurrency = holdings.findIndex(
({ assetProfile }) => {
return assetProfile.symbol === userCurrency;
}
);
if (indexOfHoldingInBaseCurrency >= 0) {
holdings[indexOfHoldingInBaseCurrency] = emergencyFundCashHolding;
} else {
holdings.push(emergencyFundCashHolding);
}
} }
let markets: PortfolioDetails['markets']; let markets: PortfolioDetails['markets'];
@ -1157,7 +1170,7 @@ export class PortfolioService {
userSettings userSettings
}).toNumber(); }).toNumber();
const hasOpenHoldings = Object.keys(holdings).length > 0; const hasOpenHoldings = holdings.length > 0;
const marketsAdvancedTotalInBaseCurrency = getSum( const marketsAdvancedTotalInBaseCurrency = getSum(
Object.values(marketsAdvanced).map(({ valueInBaseCurrency }) => { Object.values(marketsAdvanced).map(({ valueInBaseCurrency }) => {
@ -1234,13 +1247,13 @@ export class PortfolioService {
new CurrencyClusterRiskBaseCurrencyCurrentInvestment( new CurrencyClusterRiskBaseCurrencyCurrentInvestment(
this.exchangeRateDataService, this.exchangeRateDataService,
this.i18nService, this.i18nService,
Object.values(holdings), holdings,
userSettings.language userSettings.language
), ),
new CurrencyClusterRiskCurrentInvestment( new CurrencyClusterRiskCurrentInvestment(
this.exchangeRateDataService, this.exchangeRateDataService,
this.i18nService, this.i18nService,
Object.values(holdings), holdings,
userSettings.language userSettings.language
) )
], ],
@ -1261,13 +1274,13 @@ export class PortfolioService {
this.exchangeRateDataService, this.exchangeRateDataService,
this.i18nService, this.i18nService,
userSettings.language, userSettings.language,
Object.values(holdings) holdings
), ),
new AssetClassClusterRiskFixedIncome( new AssetClassClusterRiskFixedIncome(
this.exchangeRateDataService, this.exchangeRateDataService,
this.i18nService, this.i18nService,
userSettings.language, userSettings.language,
Object.values(holdings) holdings
) )
], ],
userSettings userSettings
@ -1428,7 +1441,7 @@ export class PortfolioService {
}); });
} }
private getAggregatedMarkets(holdings: Record<string, PortfolioPosition>): { private getAggregatedMarkets(holdings: PortfolioPosition[]): {
markets: PortfolioDetails['markets']; markets: PortfolioDetails['markets'];
marketsAdvanced: PortfolioDetails['marketsAdvanced']; marketsAdvanced: PortfolioDetails['marketsAdvanced'];
} { } {
@ -1493,7 +1506,7 @@ export class PortfolioService {
} }
}; };
for (const [, position] of Object.entries(holdings)) { for (const position of holdings) {
const value = position.valueInBaseCurrency; const value = position.valueInBaseCurrency;
if (position.assetProfile.countries.length > 0) { if (position.assetProfile.countries.length > 0) {
@ -1574,7 +1587,7 @@ export class PortfolioService {
userCurrency: string; userCurrency: string;
value: Big; value: Big;
}) { }) {
const cashPositions: PortfolioDetails['holdings'] = { const cashPositions: { [currency: string]: PortfolioPosition } = {
[userCurrency]: this.getInitialCashPosition({ [userCurrency]: this.getInitialCashPosition({
balance: 0, balance: 0,
currency: userCurrency currency: userCurrency
@ -1603,12 +1616,10 @@ export class PortfolioService {
} }
} }
for (const symbol of Object.keys(cashPositions)) { for (const cashPosition of Object.values(cashPositions)) {
// Calculate allocations for each currency // Calculate allocations for each currency
cashPositions[symbol].allocationInPercentage = value.gt(0) cashPosition.allocationInPercentage = value.gt(0)
? new Big(cashPositions[symbol].valueInBaseCurrency) ? new Big(cashPosition.valueInBaseCurrency).div(value).toNumber()
.div(value)
.toNumber()
: 0; : 0;
} }
@ -1715,7 +1726,7 @@ export class PortfolioService {
}) { }) {
// TODO: Use current value of activities instead of holdings // TODO: Use current value of activities instead of holdings
// tagged with EMERGENCY_FUND_TAG_ID // tagged with EMERGENCY_FUND_TAG_ID
const emergencyFundHoldings = Object.values(holdings).filter(({ tags }) => { const emergencyFundHoldings = holdings.filter(({ tags }) => {
return ( return (
tags?.some(({ id }) => { tags?.some(({ id }) => {
return id === TAG_ID_EMERGENCY_FUND; return id === TAG_ID_EMERGENCY_FUND;
@ -2258,8 +2269,8 @@ export class PortfolioService {
valueOfAccountInBaseCurrency = valueOfAccountInBaseCurrency.plus( valueOfAccountInBaseCurrency = valueOfAccountInBaseCurrency.plus(
currentQuantityOfSymbol.mul( currentQuantityOfSymbol.mul(
portfolioItemsNow[assetProfile.symbol]?.marketPriceInBaseCurrency ?? portfolioItemsNow[getAssetProfileIdentifier(assetProfile)]
0 ?.marketPriceInBaseCurrency ?? 0
) )
); );
} }

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

@ -8,6 +8,7 @@ import { UserService } from '@ghostfolio/client/services/user/user.service';
import { MAX_TOP_HOLDINGS, UNKNOWN_KEY } from '@ghostfolio/common/config'; import { MAX_TOP_HOLDINGS, UNKNOWN_KEY } from '@ghostfolio/common/config';
import { import {
canOpenHoldingDetail, canOpenHoldingDetail,
getAssetProfileIdentifier,
getCountryName getCountryName
} from '@ghostfolio/common/helper'; } from '@ghostfolio/common/helper';
import { import {
@ -87,7 +88,7 @@ export class GfAllocationsPageComponent implements OnInit {
() => this.deviceDetectorService.deviceInfo().deviceType () => this.deviceDetectorService.deviceInfo().deviceType
); );
protected holdings: { protected holdings: {
[symbol: string]: Pick< [assetProfileIdentifier: string]: Pick<
PortfolioPosition['assetProfile'], PortfolioPosition['assetProfile'],
| 'assetClass' | 'assetClass'
| 'assetClassLabel' | 'assetClassLabel'
@ -329,7 +330,7 @@ export class GfAllocationsPageComponent implements OnInit {
this.portfolioDetails = { this.portfolioDetails = {
accounts: {}, accounts: {},
createdAt: new Date(), createdAt: new Date(),
holdings: {}, holdings: [],
platforms: {}, platforms: {},
summary: undefined summary: undefined
}; };
@ -369,10 +370,12 @@ export class GfAllocationsPageComponent implements OnInit {
}; };
} }
for (const [symbol, position] of Object.entries( for (const position of this.portfolioDetails.holdings) {
this.portfolioDetails.holdings const assetProfileIdentifier = getAssetProfileIdentifier(
)) { position.assetProfile
this.holdings[symbol] = { );
this.holdings[assetProfileIdentifier] = {
assetClass: assetClass:
position.assetProfile.assetClass || (UNKNOWN_KEY as AssetClass), position.assetProfile.assetClass || (UNKNOWN_KEY as AssetClass),
assetClassLabel: position.assetProfile.assetClassLabel ?? UNKNOWN_KEY, assetClassLabel: position.assetProfile.assetClassLabel ?? UNKNOWN_KEY,
@ -498,15 +501,15 @@ export class GfAllocationsPageComponent implements OnInit {
} }
} }
if (this.holdings[symbol].assetSubClass === 'ETF') { if (this.holdings[assetProfileIdentifier].assetSubClass === 'ETF') {
this.totalValueInEtf += this.holdings[symbol].value; this.totalValueInEtf += this.holdings[assetProfileIdentifier].value;
} }
this.symbols[symbol] = { this.symbols[assetProfileIdentifier] = {
symbol,
dataSource: position.assetProfile.dataSource, dataSource: position.assetProfile.dataSource,
isClickable: canOpenHoldingDetail(position), isClickable: canOpenHoldingDetail(position),
name: position.assetProfile.name ?? '', name: position.assetProfile.name ?? '',
symbol: position.assetProfile.symbol,
value: value:
(isNumber(position.valueInBaseCurrency) (isNumber(position.valueInBaseCurrency)
? position.valueInBaseCurrency ? position.valueInBaseCurrency
@ -558,8 +561,8 @@ export class GfAllocationsPageComponent implements OnInit {
name, name,
allocationInPercentage: allocationInPercentage:
this.totalValueInEtf > 0 ? value / this.totalValueInEtf : 0, this.totalValueInEtf > 0 ? value / this.totalValueInEtf : 0,
parents: Object.entries(this.portfolioDetails.holdings) parents: this.portfolioDetails.holdings
.map(([symbol, holding]) => { .map((holding) => {
if (holding.assetProfile.holdings.length > 0) { if (holding.assetProfile.holdings.length > 0) {
const currentParentHolding = holding.assetProfile.holdings.find( const currentParentHolding = holding.assetProfile.holdings.find(
(parentHolding) => { (parentHolding) => {
@ -573,11 +576,11 @@ export class GfAllocationsPageComponent implements OnInit {
return currentParentHolding && return currentParentHolding &&
isNumber(currentParentHolding.valueInBaseCurrency) isNumber(currentParentHolding.valueInBaseCurrency)
? { ? {
symbol,
allocationInPercentage: allocationInPercentage:
currentParentHolding.valueInBaseCurrency / value, currentParentHolding.valueInBaseCurrency / value,
name: holding.assetProfile.name ?? '', name: holding.assetProfile.name ?? '',
position: holding, position: holding,
symbol: holding.assetProfile.symbol,
valueInBaseCurrency: valueInBaseCurrency:
currentParentHolding.valueInBaseCurrency currentParentHolding.valueInBaseCurrency
} }

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

@ -1,5 +1,8 @@
import { UNKNOWN_KEY } from '@ghostfolio/common/config'; import { UNKNOWN_KEY } from '@ghostfolio/common/config';
import { getCountryName } from '@ghostfolio/common/helper'; import {
getAssetProfileIdentifier,
getCountryName
} from '@ghostfolio/common/helper';
import { import {
InfoItem, InfoItem,
PortfolioPosition, PortfolioPosition,
@ -67,7 +70,7 @@ export class GfPublicPageComponent implements OnInit {
() => this.deviceDetectorService.deviceInfo().deviceType () => this.deviceDetectorService.deviceInfo().deviceType
); );
protected hasPermissionForSubscription: boolean; protected hasPermissionForSubscription: boolean;
protected holdings: PublicPortfolioResponse['holdings'][string][]; protected holdings: PublicPortfolioResponse['holdings'];
protected info: InfoItem; protected info: InfoItem;
protected isLoading = true; protected isLoading = true;
protected latestActivitiesDataSource: MatTableDataSource< protected latestActivitiesDataSource: MatTableDataSource<
@ -78,7 +81,7 @@ export class GfPublicPageComponent implements OnInit {
}; };
protected readonly pageSize = Number.MAX_SAFE_INTEGER; protected readonly pageSize = Number.MAX_SAFE_INTEGER;
protected positions: { protected positions: {
[symbol: string]: Pick< [assetProfileIdentifier: string]: Pick<
PortfolioPosition['assetProfile'], PortfolioPosition['assetProfile'],
'currency' | 'name' 'currency' | 'name'
> & { > & {
@ -175,12 +178,14 @@ export class GfPublicPageComponent implements OnInit {
} }
}; };
for (const [symbol, position] of Object.entries( for (const position of this.publicPortfolioDetails.holdings) {
this.publicPortfolioDetails.holdings const assetProfileIdentifier = getAssetProfileIdentifier(
)) { position.assetProfile
);
this.holdings.push(position); this.holdings.push(position);
this.positions[symbol] = { this.positions[assetProfileIdentifier] = {
currency: position.assetProfile.currency, currency: position.assetProfile.currency,
name: position.assetProfile.name, name: position.assetProfile.name,
value: position.allocationInPercentage value: position.allocationInPercentage
@ -199,10 +204,7 @@ export class GfPublicPageComponent implements OnInit {
} else { } else {
this.continents[continent] = { this.continents[continent] = {
name: translate(continent), name: translate(continent),
value: value: weight * (position.valueInBaseCurrency ?? 0)
weight *
(this.publicPortfolioDetails.holdings[symbol]
.valueInBaseCurrency ?? 0)
}; };
} }
@ -212,21 +214,16 @@ export class GfPublicPageComponent implements OnInit {
} else { } else {
this.countries[code] = { this.countries[code] = {
name: getCountryName({ code }), name: getCountryName({ code }),
value: value: weight * (position.valueInBaseCurrency ?? 0)
weight *
(this.publicPortfolioDetails.holdings[symbol]
.valueInBaseCurrency ?? 0)
}; };
} }
} }
} else { } else {
this.continents[UNKNOWN_KEY].value += this.continents[UNKNOWN_KEY].value +=
this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency ?? position.valueInBaseCurrency ?? 0;
0;
this.countries[UNKNOWN_KEY].value += this.countries[UNKNOWN_KEY].value +=
this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency ?? position.valueInBaseCurrency ?? 0;
0;
} }
if (position.assetProfile.sectors.length > 0) { if (position.assetProfile.sectors.length > 0) {
@ -239,23 +236,18 @@ export class GfPublicPageComponent implements OnInit {
} else { } else {
this.sectors[name] = { this.sectors[name] = {
name: translate(name), name: translate(name),
value: value: weight * (position.valueInBaseCurrency ?? 0)
weight *
(this.publicPortfolioDetails.holdings[symbol]
.valueInBaseCurrency ?? 0)
}; };
} }
} }
} else { } else {
this.sectors[UNKNOWN_KEY].value += this.sectors[UNKNOWN_KEY].value += position.valueInBaseCurrency ?? 0;
this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency ??
0;
} }
} }
this.symbols[symbol] = { this.symbols[assetProfileIdentifier] = {
symbol, name: position.assetProfile.name ?? position.assetProfile.symbol,
name: position.assetProfile.name ?? symbol, symbol: position.assetProfile.symbol,
value: isNumber(position.valueInBaseCurrency) value: isNumber(position.valueInBaseCurrency)
? position.valueInBaseCurrency ? position.valueInBaseCurrency
: (position.valueInPercentage ?? 0) : (position.valueInPercentage ?? 0)

2
libs/common/src/lib/interfaces/portfolio-details.interface.ts

@ -17,7 +17,7 @@ export interface PortfolioDetails {
}; };
}; };
createdAt: Date; createdAt: Date;
holdings: { [symbol: string]: PortfolioPosition }; holdings: PortfolioPosition[];
markets?: { markets?: {
[key in Market]: { [key in Market]: {
id: Market; id: Market;

6
libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts

@ -10,8 +10,7 @@ import { Order } from '@prisma/client';
export interface PublicPortfolioResponse extends PublicPortfolioResponseV1 { export interface PublicPortfolioResponse extends PublicPortfolioResponseV1 {
alias?: string; alias?: string;
hasDetails: boolean; hasDetails: boolean;
holdings: { holdings: Pick<
[symbol: string]: Pick<
PortfolioPosition, PortfolioPosition,
| 'allocationInPercentage' | 'allocationInPercentage'
| 'assetProfile' | 'assetProfile'
@ -20,8 +19,7 @@ export interface PublicPortfolioResponse extends PublicPortfolioResponseV1 {
| 'netPerformancePercentWithCurrencyEffect' | 'netPerformancePercentWithCurrencyEffect'
| 'valueInBaseCurrency' | 'valueInBaseCurrency'
| 'valueInPercentage' | 'valueInPercentage'
>; >[];
};
latestActivities: (Pick< latestActivities: (Pick<
Order, Order,
'currency' | 'date' | 'fee' | 'quantity' | 'type' | 'unitPrice' 'currency' | 'date' | 'fee' | 'quantity' | 'type' | 'unitPrice'

46
libs/ui/src/lib/services/data.service.ts

@ -647,24 +647,22 @@ export class DataService {
.pipe( .pipe(
map((response) => { map((response) => {
if (response.holdings) { if (response.holdings) {
for (const symbol of Object.keys(response.holdings)) { for (const holding of response.holdings) {
response.holdings[symbol].assetProfile.assetClassLabel = holding.assetProfile.assetClassLabel = translate(
translate(response.holdings[symbol].assetProfile.assetClass); holding.assetProfile.assetClass
);
response.holdings[symbol].assetProfile.assetSubClassLabel = holding.assetProfile.assetSubClassLabel = translate(
translate(response.holdings[symbol].assetProfile.assetSubClass); holding.assetProfile.assetSubClass
);
response.holdings[symbol].dateOfFirstActivity = response.holdings[ holding.dateOfFirstActivity = holding.dateOfFirstActivity
symbol ? parseISO(holding.dateOfFirstActivity)
].dateOfFirstActivity
? parseISO(response.holdings[symbol].dateOfFirstActivity)
: undefined; : undefined;
response.holdings[symbol].value = isNumber( holding.value = isNumber(holding.value)
response.holdings[symbol].value ? holding.value
) : holding.valueInPercentage;
? response.holdings[symbol].value
: response.holdings[symbol].valueInPercentage;
} }
} }
@ -782,18 +780,20 @@ export class DataService {
.pipe( .pipe(
map((response) => { map((response) => {
if (response.holdings) { if (response.holdings) {
for (const symbol of Object.keys(response.holdings)) { for (const holding of response.holdings) {
response.holdings[symbol].assetProfile.assetClassLabel = holding.assetProfile.assetClassLabel = translate(
translate(response.holdings[symbol].assetProfile.assetClass); holding.assetProfile.assetClass
);
response.holdings[symbol].assetProfile.assetSubClassLabel = holding.assetProfile.assetSubClassLabel = translate(
translate(response.holdings[symbol].assetProfile.assetSubClass); holding.assetProfile.assetSubClass
);
response.holdings[symbol].valueInBaseCurrency = isNumber( holding.valueInBaseCurrency = isNumber(
response.holdings[symbol].valueInBaseCurrency holding.valueInBaseCurrency
) )
? response.holdings[symbol].valueInBaseCurrency ? holding.valueInBaseCurrency
: response.holdings[symbol].valueInPercentage; : holding.valueInPercentage;
} }
} }

Loading…
Cancel
Save