Browse Source

Merge 28287c5dc5 into 90c90e179e

pull/8218/merge
Rene Turcios 15 hours ago
committed by GitHub
parent
commit
bac2381156
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 141
      apps/api/src/app/portfolio/portfolio.service.spec.ts
  2. 82
      apps/api/src/app/portfolio/portfolio.service.ts
  3. 19
      apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts

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

@ -25,7 +25,7 @@ import {
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { AccountWithBalance, DateRange } from '@ghostfolio/common/types'; import { AccountWithBalance, DateRange } from '@ghostfolio/common/types';
import { AssetClass, DataSource, Prisma } from '@prisma/client'; import { AssetClass, AssetSubClass, DataSource, Prisma } from '@prisma/client';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { endOfDay } from 'date-fns'; import { endOfDay } from 'date-fns';
import { randomUUID } from 'node:crypto'; import { randomUUID } from 'node:crypto';
@ -346,7 +346,7 @@ describe('PortfolioService', () => {
).getAggregatedMarkets(holdings); ).getAggregatedMarkets(holdings);
}; };
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) to the unknown bucket', () => {
const holdings = [ const holdings = [
{ {
// Gold (GC=F) // Gold (GC=F)
@ -391,6 +391,143 @@ describe('PortfolioService', () => {
expect(marketsAdvanced.northAmerica.valueInBaseCurrency).toBe(1000); expect(marketsAdvanced.northAmerica.valueInBaseCurrency).toBe(1000);
expect(marketsAdvanced[UNKNOWN_KEY].valueInBaseCurrency).toBe(500); expect(marketsAdvanced[UNKNOWN_KEY].valueInBaseCurrency).toBe(500);
}); });
it('should exclude cash and cryptocurrency holdings from the market distribution', () => {
const holdings = [
{
// BTC
assetProfile: {
assetSubClass: AssetSubClass.CRYPTOCURRENCY,
countries: []
},
markets: {
developedMarkets: 0,
emergingMarkets: 0,
otherMarkets: 0
},
marketsAdvanced: {
asiaPacific: 0,
emergingMarkets: 0,
europe: 0,
japan: 0,
northAmerica: 0,
otherMarkets: 0
},
valueInBaseCurrency: 500
},
{
// Cash (USD)
assetProfile: {
assetSubClass: AssetSubClass.CASH,
countries: []
},
markets: {
developedMarkets: 0,
emergingMarkets: 0,
otherMarkets: 0
},
marketsAdvanced: {
asiaPacific: 0,
emergingMarkets: 0,
europe: 0,
japan: 0,
northAmerica: 0,
otherMarkets: 0
},
valueInBaseCurrency: 250
},
{
// MSFT
assetProfile: { countries: [{ code: 'US', weight: 1 }] },
markets: {
developedMarkets: 1,
emergingMarkets: 0,
otherMarkets: 0
},
marketsAdvanced: {
asiaPacific: 0,
emergingMarkets: 0,
europe: 0,
japan: 0,
northAmerica: 1,
otherMarkets: 0
},
valueInBaseCurrency: 1000
}
];
const { markets, marketsAdvanced } = getAggregatedMarkets(holdings);
expect(markets.developedMarkets.valueInBaseCurrency).toBe(1000);
expect(markets[UNKNOWN_KEY].valueInBaseCurrency).toBe(0);
expect(markets.developedMarkets.valueInPercentage).toBeCloseTo(1);
expect(markets[UNKNOWN_KEY].valueInPercentage).toBe(0);
expect(marketsAdvanced.northAmerica.valueInBaseCurrency).toBe(1000);
expect(marketsAdvanced[UNKNOWN_KEY].valueInBaseCurrency).toBe(0);
expect(marketsAdvanced.northAmerica.valueInPercentage).toBeCloseTo(1);
expect(marketsAdvanced[UNKNOWN_KEY].valueInPercentage).toBe(0);
});
it('should not produce NaN percentages when the portfolio only holds cash and cryptocurrencies', () => {
const holdings = [
{
// BTC
assetProfile: {
assetSubClass: AssetSubClass.CRYPTOCURRENCY,
countries: []
},
markets: {
developedMarkets: 0,
emergingMarkets: 0,
otherMarkets: 0
},
marketsAdvanced: {
asiaPacific: 0,
emergingMarkets: 0,
europe: 0,
japan: 0,
northAmerica: 0,
otherMarkets: 0
},
valueInBaseCurrency: 500
},
{
// Cash (USD)
assetProfile: {
assetSubClass: AssetSubClass.CASH,
countries: []
},
markets: {
developedMarkets: 0,
emergingMarkets: 0,
otherMarkets: 0
},
marketsAdvanced: {
asiaPacific: 0,
emergingMarkets: 0,
europe: 0,
japan: 0,
northAmerica: 0,
otherMarkets: 0
},
valueInBaseCurrency: 250
}
];
const { markets, marketsAdvanced } = getAggregatedMarkets(holdings);
for (const bucket of Object.values(markets)) {
expect(bucket.valueInBaseCurrency).toBe(0);
expect(bucket.valueInPercentage).toBe(0);
}
for (const bucket of Object.values(marketsAdvanced)) {
expect(bucket.valueInBaseCurrency).toBe(0);
expect(bucket.valueInPercentage).toBe(0);
}
});
}); });
describe('getCashSymbolProfiles', () => { describe('getCashSymbolProfiles', () => {

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

@ -45,6 +45,7 @@ import {
getAssetProfileIdentifier, getAssetProfileIdentifier,
getSum, getSum,
isAccountExcluded, isAccountExcluded,
isCashPosition,
isDraftActivity, isDraftActivity,
parseDate, parseDate,
resetHours, resetHours,
@ -1682,6 +1683,14 @@ export class PortfolioService {
for (const position of holdings) { for (const position of holdings) {
const value = position.valueInBaseCurrency; const value = position.valueInBaseCurrency;
if (
isCashPosition(position.assetProfile) ||
position.assetProfile.assetSubClass === AssetSubClass.CRYPTOCURRENCY
) {
// Cash and cryptocurrencies do not belong to any specific market
continue;
}
if (position.assetProfile.countries.length > 0) { if (position.assetProfile.countries.length > 0) {
markets.developedMarkets.valueInBaseCurrency += markets.developedMarkets.valueInBaseCurrency +=
position.markets.developedMarkets * value; position.markets.developedMarkets * value;
@ -1714,14 +1723,28 @@ export class PortfolioService {
}) })
).toNumber(); ).toNumber();
markets.developedMarkets.valueInPercentage = // The total can be zero when the portfolio only holds cash and / or
markets.developedMarkets.valueInBaseCurrency / marketsTotalInBaseCurrency; // cryptocurrencies, which are not assigned to any market
markets.emergingMarkets.valueInPercentage = const toPercentage = (valueInBaseCurrency: number, total: number) => {
markets.emergingMarkets.valueInBaseCurrency / marketsTotalInBaseCurrency; return total ? valueInBaseCurrency / total : 0;
markets.otherMarkets.valueInPercentage = };
markets.otherMarkets.valueInBaseCurrency / marketsTotalInBaseCurrency;
markets[UNKNOWN_KEY].valueInPercentage = markets.developedMarkets.valueInPercentage = toPercentage(
markets[UNKNOWN_KEY].valueInBaseCurrency / marketsTotalInBaseCurrency; markets.developedMarkets.valueInBaseCurrency,
marketsTotalInBaseCurrency
);
markets.emergingMarkets.valueInPercentage = toPercentage(
markets.emergingMarkets.valueInBaseCurrency,
marketsTotalInBaseCurrency
);
markets.otherMarkets.valueInPercentage = toPercentage(
markets.otherMarkets.valueInBaseCurrency,
marketsTotalInBaseCurrency
);
markets[UNKNOWN_KEY].valueInPercentage = toPercentage(
markets[UNKNOWN_KEY].valueInBaseCurrency,
marketsTotalInBaseCurrency
);
const marketsAdvancedTotal = const marketsAdvancedTotal =
marketsAdvanced.asiaPacific.valueInBaseCurrency + marketsAdvanced.asiaPacific.valueInBaseCurrency +
@ -1732,21 +1755,34 @@ export class PortfolioService {
marketsAdvanced.otherMarkets.valueInBaseCurrency + marketsAdvanced.otherMarkets.valueInBaseCurrency +
marketsAdvanced[UNKNOWN_KEY].valueInBaseCurrency; marketsAdvanced[UNKNOWN_KEY].valueInBaseCurrency;
marketsAdvanced.asiaPacific.valueInPercentage = marketsAdvanced.asiaPacific.valueInPercentage = toPercentage(
marketsAdvanced.asiaPacific.valueInBaseCurrency / marketsAdvancedTotal; marketsAdvanced.asiaPacific.valueInBaseCurrency,
marketsAdvanced.emergingMarkets.valueInPercentage = marketsAdvancedTotal
marketsAdvanced.emergingMarkets.valueInBaseCurrency / );
marketsAdvancedTotal; marketsAdvanced.emergingMarkets.valueInPercentage = toPercentage(
marketsAdvanced.europe.valueInPercentage = marketsAdvanced.emergingMarkets.valueInBaseCurrency,
marketsAdvanced.europe.valueInBaseCurrency / marketsAdvancedTotal; marketsAdvancedTotal
marketsAdvanced.japan.valueInPercentage = );
marketsAdvanced.japan.valueInBaseCurrency / marketsAdvancedTotal; marketsAdvanced.europe.valueInPercentage = toPercentage(
marketsAdvanced.northAmerica.valueInPercentage = marketsAdvanced.europe.valueInBaseCurrency,
marketsAdvanced.northAmerica.valueInBaseCurrency / marketsAdvancedTotal; marketsAdvancedTotal
marketsAdvanced.otherMarkets.valueInPercentage = );
marketsAdvanced.otherMarkets.valueInBaseCurrency / marketsAdvancedTotal; marketsAdvanced.japan.valueInPercentage = toPercentage(
marketsAdvanced[UNKNOWN_KEY].valueInPercentage = marketsAdvanced.japan.valueInBaseCurrency,
marketsAdvanced[UNKNOWN_KEY].valueInBaseCurrency / marketsAdvancedTotal; marketsAdvancedTotal
);
marketsAdvanced.northAmerica.valueInPercentage = toPercentage(
marketsAdvanced.northAmerica.valueInBaseCurrency,
marketsAdvancedTotal
);
marketsAdvanced.otherMarkets.valueInPercentage = toPercentage(
marketsAdvanced.otherMarkets.valueInBaseCurrency,
marketsAdvancedTotal
);
marketsAdvanced[UNKNOWN_KEY].valueInPercentage = toPercentage(
marketsAdvanced[UNKNOWN_KEY].valueInBaseCurrency,
marketsAdvancedTotal
);
return { markets, marketsAdvanced }; return { markets, marketsAdvanced };
} }

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

@ -388,7 +388,7 @@ export class GfAllocationsPageComponent implements OnInit {
[name: string]: { name: string; value: number }; [name: string]: { name: string; value: number };
} = {}; } = {};
let totalValueExcludingCashPositions = 0; let totalValueExcludingCashAndCryptoPositions = 0;
let totalValueInFunds = 0; let totalValueInFunds = 0;
for (const position of this.portfolioDetails.holdings) { for (const position of this.portfolioDetails.holdings) {
@ -415,11 +415,18 @@ export class GfAllocationsPageComponent implements OnInit {
: (position.valueInBaseCurrency ?? 0) : (position.valueInBaseCurrency ?? 0)
}; };
if (!isCashPosition(position.assetProfile)) { const isExcludedFromGeographicAnalysis =
isCashPosition(position.assetProfile) ||
position.assetProfile.assetSubClass === AssetSubClass.CRYPTOCURRENCY;
if (!isExcludedFromGeographicAnalysis) {
// Prepare analysis data by continents, countries, holdings and sectors // Prepare analysis data by continents, countries, holdings and sectors
// except for cash // except for cash and cryptocurrencies, which cannot be assigned to a
// specific continent
totalValueExcludingCashPositions += this.extractValue(position); totalValueExcludingCashAndCryptoPositions += this.extractValue(
position
);
if (position.assetProfile.countries.length > 0) { if (position.assetProfile.countries.length > 0) {
for (const country of position.assetProfile.countries) { for (const country of position.assetProfile.countries) {
@ -541,11 +548,11 @@ export class GfAllocationsPageComponent implements OnInit {
if (this.showValuesInPercentage()) { if (this.showValuesInPercentage()) {
// The values are percentages of the whole portfolio, but the analysis // The values are percentages of the whole portfolio, but the analysis
// data does not contain the cash positions // data does not contain the cash and cryptocurrency positions
for (const values of [this.continents, this.countries, this.sectors]) { for (const values of [this.continents, this.countries, this.sectors]) {
convertValuesToPercentagesOfTotal({ convertValuesToPercentagesOfTotal({
values, values,
total: totalValueExcludingCashPositions total: totalValueExcludingCashAndCryptoPositions
}); });
} }
} }

Loading…
Cancel
Save