Browse Source

Use asset profile data

pull/6499/head
Thomas Kaul 3 weeks ago
parent
commit
4cf74b5e60
  1. 85
      apps/api/src/app/endpoints/ai/ai.service.ts
  2. 12
      apps/api/src/app/endpoints/public/public.controller.ts
  3. 37
      apps/api/src/app/portfolio/portfolio.controller.ts
  4. 35
      apps/api/src/app/portfolio/portfolio.service.ts
  5. 10
      apps/api/src/models/rule.ts
  6. 3
      apps/api/src/models/rules/asset-class-cluster-risk/equity.ts
  7. 3
      apps/api/src/models/rules/asset-class-cluster-risk/fixed-income.ts
  8. 2
      apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts
  9. 2
      apps/api/src/models/rules/currency-cluster-risk/current-investment.ts
  10. 41
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
  11. 5
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
  12. 12
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html
  13. 65
      apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
  14. 16
      apps/client/src/app/pages/portfolio/analysis/analysis-page.html
  15. 26
      apps/client/src/app/pages/public/public-page.component.ts
  16. 14
      libs/common/src/lib/helper.ts
  17. 45
      libs/common/src/lib/interfaces/portfolio-position.interface.ts
  18. 22
      libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts
  19. 42
      libs/ui/src/lib/assistant/assistant.component.ts
  20. 18
      libs/ui/src/lib/holdings-table/holdings-table.component.html
  21. 4
      libs/ui/src/lib/holdings-table/holdings-table.component.ts
  22. 12
      libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.html
  23. 5
      libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.ts
  24. 2
      libs/ui/src/lib/top-holdings/top-holdings.component.html
  25. 4
      libs/ui/src/lib/top-holdings/top-holdings.component.ts
  26. 14
      libs/ui/src/lib/treemap-chart/treemap-chart.component.ts

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

@ -89,53 +89,44 @@ export class AiService {
.sort((a, b) => {
return b.allocationInPercentage - a.allocationInPercentage;
})
.map(
({
allocationInPercentage,
assetClass,
assetSubClass,
currency,
name: label,
symbol
}) => {
return AiService.HOLDINGS_TABLE_COLUMN_DEFINITIONS.reduce(
(row, { key, name }) => {
switch (key) {
case 'ALLOCATION_PERCENTAGE':
row[name] = `${(allocationInPercentage * 100).toFixed(3)}%`;
break;
case 'ASSET_CLASS':
row[name] = assetClass ?? '';
break;
case 'ASSET_SUB_CLASS':
row[name] = assetSubClass ?? '';
break;
case 'CURRENCY':
row[name] = currency;
break;
case 'NAME':
row[name] = label;
break;
case 'SYMBOL':
row[name] = symbol;
break;
default:
row[name] = '';
break;
}
return row;
},
{} as Record<string, string>
);
}
);
.map(({ allocationInPercentage, assetProfile }) => {
return AiService.HOLDINGS_TABLE_COLUMN_DEFINITIONS.reduce(
(row, { key, name }) => {
switch (key) {
case 'ALLOCATION_PERCENTAGE':
row[name] = `${(allocationInPercentage * 100).toFixed(3)}%`;
break;
case 'ASSET_CLASS':
row[name] = assetProfile?.assetClass ?? '';
break;
case 'ASSET_SUB_CLASS':
row[name] = assetProfile?.assetSubClass ?? '';
break;
case 'CURRENCY':
row[name] = assetProfile?.currency ?? '';
break;
case 'NAME':
row[name] = assetProfile?.name ?? '';
break;
case 'SYMBOL':
row[name] = assetProfile?.symbol ?? '';
break;
default:
row[name] = '';
break;
}
return row;
},
{} as Record<string, string>
);
});
// Dynamic import to load ESM module from CommonJS context
// eslint-disable-next-line @typescript-eslint/no-implied-eval

12
apps/api/src/app/endpoints/public/public.controller.ts

@ -150,11 +150,11 @@ export class PublicController {
};
const totalValue = getSum(
Object.values(holdings).map(({ currency, marketPrice, quantity }) => {
Object.values(holdings).map(({ assetProfile, marketPrice, quantity }) => {
return new Big(
this.exchangeRateDataService.toCurrency(
quantity * marketPrice,
currency,
assetProfile.currency,
this.request.user?.settings?.settings.baseCurrency ??
DEFAULT_CURRENCY
)
@ -166,19 +166,11 @@ export class PublicController {
publicPortfolioResponse.holdings[symbol] = {
allocationInPercentage:
portfolioPosition.valueInBaseCurrency / totalValue,
assetClass: hasDetails ? portfolioPosition.assetClass : undefined,
assetProfile: hasDetails ? portfolioPosition.assetProfile : undefined,
countries: hasDetails ? portfolioPosition.countries : [],
currency: hasDetails ? portfolioPosition.currency : undefined,
dataSource: portfolioPosition.dataSource,
dateOfFirstActivity: portfolioPosition.dateOfFirstActivity,
markets: hasDetails ? portfolioPosition.markets : undefined,
name: portfolioPosition.name,
netPerformancePercentWithCurrencyEffect:
portfolioPosition.netPerformancePercentWithCurrencyEffect,
sectors: hasDetails ? portfolioPosition.sectors : [],
symbol: portfolioPosition.symbol,
url: portfolioPosition.url,
valueInPercentage: portfolioPosition.valueInBaseCurrency / totalValue
};
}

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

@ -141,10 +141,10 @@ export class PortfolioController {
.reduce((a, b) => a + b, 0);
const totalValue = Object.values(holdings)
.filter(({ assetClass, assetSubClass }) => {
.filter(({ assetProfile }) => {
return (
assetClass !== AssetClass.LIQUIDITY &&
assetSubClass !== AssetSubClass.CASH
assetProfile.assetClass !== AssetClass.LIQUIDITY &&
assetProfile.assetSubClass !== AssetSubClass.CASH
);
})
.map(({ valueInBaseCurrency }) => {
@ -212,24 +212,29 @@ export class PortfolioController {
}
for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
const assetProfile = portfolioPosition.assetProfile;
holdings[symbol] = {
...portfolioPosition,
assetClass:
hasDetails || portfolioPosition.assetClass === AssetClass.LIQUIDITY
? portfolioPosition.assetClass
: undefined,
assetSubClass:
hasDetails || portfolioPosition.assetSubClass === AssetSubClass.CASH
? portfolioPosition.assetSubClass
: undefined,
countries: hasDetails ? portfolioPosition.countries : [],
currency: hasDetails ? portfolioPosition.currency : undefined,
holdings: hasDetails ? portfolioPosition.holdings : [],
assetProfile: {
...assetProfile,
assetClass:
hasDetails || assetProfile.assetClass === AssetClass.LIQUIDITY
? assetProfile.assetClass
: undefined,
assetSubClass:
hasDetails || assetProfile.assetSubClass === AssetSubClass.CASH
? assetProfile.assetSubClass
: undefined,
countries: hasDetails ? assetProfile.countries : [],
currency: hasDetails ? assetProfile.currency : undefined,
holdings: hasDetails ? assetProfile.holdings : [],
sectors: hasDetails ? assetProfile.sectors : []
},
markets: hasDetails ? portfolioPosition.markets : undefined,
marketsAdvanced: hasDetails
? portfolioPosition.marketsAdvanced
: undefined,
sectors: hasDetails ? portfolioPosition.sectors : []
: undefined
};
}

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

@ -613,17 +613,15 @@ export class PortfolioService {
holdings[symbol] = {
activitiesCount,
currency,
markets,
marketsAdvanced,
marketPrice,
symbol,
tags,
allocationInPercentage: filteredValueInBaseCurrency.eq(0)
? 0
: valueInBaseCurrency.div(filteredValueInBaseCurrency).toNumber(),
assetClass: assetProfile.assetClass,
assetProfile: {
currency,
assetClass: assetProfile.assetClass,
assetSubClass: assetProfile.assetSubClass,
countries: assetProfile.countries,
@ -645,9 +643,6 @@ export class PortfolioService {
symbol: assetProfile.symbol,
url: assetProfile.url
},
assetSubClass: assetProfile.assetSubClass,
countries: assetProfile.countries,
dataSource: assetProfile.dataSource,
dateOfFirstActivity: parseDate(dateOfFirstActivity),
dividend: dividend?.toNumber() ?? 0,
grossPerformance: grossPerformance?.toNumber() ?? 0,
@ -656,19 +651,7 @@ export class PortfolioService {
grossPerformancePercentageWithCurrencyEffect?.toNumber() ?? 0,
grossPerformanceWithCurrencyEffect:
grossPerformanceWithCurrencyEffect?.toNumber() ?? 0,
holdings: assetProfile.holdings.map(
({ allocationInPercentage, name }) => {
return {
allocationInPercentage,
name,
valueInBaseCurrency: valueInBaseCurrency
.mul(allocationInPercentage)
.toNumber()
};
}
),
investment: investment.toNumber(),
name: assetProfile.name,
netPerformance: netPerformance?.toNumber() ?? 0,
netPerformancePercent: netPerformancePercentage?.toNumber() ?? 0,
netPerformancePercentWithCurrencyEffect:
@ -678,8 +661,6 @@ export class PortfolioService {
netPerformanceWithCurrencyEffect:
netPerformanceWithCurrencyEffectMap?.[dateRange]?.toNumber() ?? 0,
quantity: quantity.toNumber(),
sectors: assetProfile.sectors,
url: assetProfile.url,
valueInBaseCurrency: valueInBaseCurrency.toNumber()
};
}
@ -1447,8 +1428,8 @@ export class PortfolioService {
for (const [, position] of Object.entries(holdings)) {
const value = position.valueInBaseCurrency;
if (position.assetClass !== AssetClass.LIQUIDITY) {
if (position.countries.length > 0) {
if (position.assetProfile.assetClass !== AssetClass.LIQUIDITY) {
if (position.assetProfile.countries.length > 0) {
markets.developedMarkets.valueInBaseCurrency +=
position.markets.developedMarkets * value;
markets.emergingMarkets.valueInBaseCurrency +=
@ -1694,15 +1675,13 @@ export class PortfolioService {
currency: string;
}): PortfolioPosition {
return {
currency,
activitiesCount: 0,
allocationInPercentage: 0,
assetClass: AssetClass.LIQUIDITY,
assetSubClass: AssetSubClass.CASH,
assetProfile: {
currency,
assetClass: AssetClass.LIQUIDITY,
assetSubClass: AssetSubClass.CASH,
currency,
countries: [],
dataSource: undefined,
holdings: [],
@ -1710,25 +1689,19 @@ export class PortfolioService {
sectors: [],
symbol: currency
},
countries: [],
dataSource: undefined,
dateOfFirstActivity: undefined,
dividend: 0,
grossPerformance: 0,
grossPerformancePercent: 0,
grossPerformancePercentWithCurrencyEffect: 0,
grossPerformanceWithCurrencyEffect: 0,
holdings: [],
investment: balance,
marketPrice: 0,
name: currency,
netPerformance: 0,
netPerformancePercent: 0,
netPerformancePercentWithCurrencyEffect: 0,
netPerformanceWithCurrencyEffect: 0,
quantity: 0,
sectors: [],
symbol: currency,
tags: [],
valueInBaseCurrency: balance
};

10
apps/api/src/models/rule.ts

@ -1,6 +1,5 @@
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
import { groupBy } from '@ghostfolio/common/helper';
import {
PortfolioPosition,
PortfolioReportRule,
@ -9,6 +8,7 @@ import {
} from '@ghostfolio/common/interfaces';
import { Big } from 'big.js';
import { groupBy } from 'lodash';
import { EvaluationResult } from './interfaces/evaluation-result.interface';
import { RuleInterface } from './interfaces/rule.interface';
@ -41,10 +41,12 @@ export abstract class Rule<T extends RuleSettings> implements RuleInterface<T> {
public groupCurrentHoldingsByAttribute(
holdings: PortfolioPosition[],
attribute: keyof PortfolioPosition,
attribute:
| keyof PortfolioPosition
| `assetProfile.${Extract<keyof PortfolioPosition['assetProfile'], string>}`,
baseCurrency: string
) {
return Array.from(groupBy(attribute, holdings).entries()).map(
return Object.entries(groupBy(holdings, attribute)).map(
([attributeValue, objs]) => ({
groupKey: attributeValue,
investment: objs.reduce(
@ -59,7 +61,7 @@ export abstract class Rule<T extends RuleSettings> implements RuleInterface<T> {
new Big(currentValue.quantity)
.mul(currentValue.marketPrice ?? 0)
.toNumber(),
currentValue.currency,
currentValue.assetProfile.currency,
baseCurrency
),
0

3
apps/api/src/models/rules/asset-class-cluster-risk/equity.ts

@ -27,9 +27,10 @@ export class AssetClassClusterRiskEquity extends Rule<Settings> {
public evaluate(ruleSettings: Settings) {
const holdingsGroupedByAssetClass = this.groupCurrentHoldingsByAttribute(
this.holdings,
'assetClass',
'assetProfile.assetClass',
ruleSettings.baseCurrency
);
let totalValue = 0;
const equityValueInBaseCurrency =

3
apps/api/src/models/rules/asset-class-cluster-risk/fixed-income.ts

@ -27,9 +27,10 @@ export class AssetClassClusterRiskFixedIncome extends Rule<Settings> {
public evaluate(ruleSettings: Settings) {
const holdingsGroupedByAssetClass = this.groupCurrentHoldingsByAttribute(
this.holdings,
'assetClass',
'assetProfile.assetClass',
ruleSettings.baseCurrency
);
let totalValue = 0;
const fixedIncomeValueInBaseCurrency =

2
apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts

@ -27,7 +27,7 @@ export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule<Setti
public evaluate(ruleSettings: Settings) {
const holdingsGroupedByCurrency = this.groupCurrentHoldingsByAttribute(
this.holdings,
'currency',
'assetProfile.currency',
ruleSettings.baseCurrency
);

2
apps/api/src/models/rules/currency-cluster-risk/current-investment.ts

@ -27,7 +27,7 @@ export class CurrencyClusterRiskCurrentInvestment extends Rule<Settings> {
public evaluate(ruleSettings: Settings) {
const holdingsGroupedByCurrency = this.groupCurrentHoldingsByAttribute(
this.holdings,
'currency',
'assetProfile.currency',
ruleSettings.baseCurrency
);

41
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts

@ -136,34 +136,25 @@ export class GfCreateOrUpdateActivityDialogComponent implements OnDestroy {
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ holdings }) => {
this.defaultLookupItems = holdings
.filter(({ assetSubClass }) => {
return !['CASH'].includes(assetSubClass);
.filter(({ assetProfile }) => {
return !['CASH'].includes(assetProfile.assetSubClass);
})
.sort((a, b) => {
return a.name?.localeCompare(b.name);
return a.assetProfile.name?.localeCompare(b.assetProfile.name);
})
.map(
({
assetClass,
assetSubClass,
currency,
dataSource,
name,
symbol
}) => {
return {
assetClass,
assetSubClass,
currency,
dataSource,
name,
symbol,
dataProviderInfo: {
isPremium: false
}
};
}
);
.map(({ assetProfile }) => {
return {
assetClass: assetProfile.assetClass,
assetSubClass: assetProfile.assetSubClass,
currency: assetProfile.currency,
dataProviderInfo: {
isPremium: false
},
dataSource: assetProfile.dataSource,
name: assetProfile.name,
symbol: assetProfile.symbol
};
});
this.changeDetectorRef.markForCheck();
});

5
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts

@ -154,9 +154,10 @@ export class GfImportActivitiesDialogComponent implements OnDestroy {
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ holdings }) => {
this.holdings = sortBy(holdings, ({ name }) => {
return name.toLowerCase();
this.holdings = sortBy(holdings, ({ assetProfile }) => {
return assetProfile.name.toLowerCase();
});
this.assetProfileForm.get('assetProfileIdentifier').enable();
this.isLoading = false;

12
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html

@ -38,18 +38,18 @@
<mat-option
class="line-height-1"
[value]="{
dataSource: holding.dataSource,
name: holding.name,
symbol: holding.symbol
dataSource: holding.assetProfile.dataSource,
name: holding.assetProfile.name,
symbol: holding.assetProfile.symbol
}"
>
<span
><b>{{ holding.name }}</b></span
><b>{{ holding.assetProfile.name }}</b></span
>
<br />
<small class="text-muted"
>{{ holding.symbol | gfSymbol }} ·
{{ holding.currency }}</small
>{{ holding.assetProfile.symbol | gfSymbol }} ·
{{ holding.assetProfile.currency }}</small
>
</mat-option>
}

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

@ -74,16 +74,10 @@ export class GfAllocationsPageComponent implements OnInit {
public deviceType: string;
public hasImpersonationId: boolean;
public holdings: {
[symbol: string]: Pick<
PortfolioPosition,
| 'assetClass'
| 'assetClassLabel'
| 'assetSubClass'
| 'assetSubClassLabel'
| 'currency'
| 'exchange'
| 'name'
> & { etfProvider: string; value: number };
[symbol: string]: Pick<PortfolioPosition, 'assetProfile' | 'exchange'> & {
etfProvider: string;
value: number;
};
};
public isLoading = false;
public markets: {
@ -209,7 +203,7 @@ export class GfAllocationsPageComponent implements OnInit {
assetSubClass,
name
}: {
assetSubClass: PortfolioPosition['assetSubClass'];
assetSubClass: PortfolioPosition['assetProfile']['assetSubClass'];
name: string;
}) {
if (assetSubClass === 'ETF') {
@ -326,6 +320,7 @@ export class GfAllocationsPageComponent implements OnInit {
for (const [symbol, position] of Object.entries(
this.portfolioDetails.holdings
)) {
const { assetProfile } = position;
let value = 0;
if (this.hasImpersonationId) {
@ -336,24 +331,28 @@ export class GfAllocationsPageComponent implements OnInit {
this.holdings[symbol] = {
value,
assetClass: position.assetClass || (UNKNOWN_KEY as AssetClass),
assetClassLabel: position.assetClassLabel || UNKNOWN_KEY,
assetSubClass: position.assetSubClass || (UNKNOWN_KEY as AssetSubClass),
assetSubClassLabel: position.assetSubClassLabel || UNKNOWN_KEY,
currency: position.currency,
assetProfile: {
...assetProfile,
assetClass: assetProfile.assetClass || (UNKNOWN_KEY as AssetClass),
assetClassLabel: assetProfile.assetClassLabel || UNKNOWN_KEY,
assetSubClass:
assetProfile.assetSubClass || (UNKNOWN_KEY as AssetSubClass),
assetSubClassLabel: assetProfile.assetSubClassLabel || UNKNOWN_KEY,
currency: assetProfile.currency,
name: assetProfile.name
},
etfProvider: this.extractEtfProvider({
assetSubClass: position.assetSubClass,
name: position.name
assetSubClass: assetProfile.assetSubClass,
name: assetProfile.name
}),
exchange: position.exchange,
name: position.name
exchange: position.exchange
};
if (position.assetClass !== AssetClass.LIQUIDITY) {
if (assetProfile.assetClass !== AssetClass.LIQUIDITY) {
// Prepare analysis data by continents, countries, holdings and sectors except for liquidity
if (position.countries.length > 0) {
for (const country of position.countries) {
if (assetProfile.countries.length > 0) {
for (const country of assetProfile.countries) {
const { code, continent, name, weight } = country;
if (this.continents[continent]?.value) {
@ -404,8 +403,8 @@ export class GfAllocationsPageComponent implements OnInit {
: this.portfolioDetails.holdings[symbol].valueInPercentage;
}
if (position.holdings.length > 0) {
for (const holding of position.holdings) {
if (assetProfile.holdings.length > 0) {
for (const holding of assetProfile.holdings) {
const { allocationInPercentage, name, valueInBaseCurrency } =
holding;
@ -426,8 +425,8 @@ export class GfAllocationsPageComponent implements OnInit {
}
}
if (position.sectors.length > 0) {
for (const sector of position.sectors) {
if (assetProfile.sectors.length > 0) {
for (const sector of assetProfile.sectors) {
const { name, weight } = sector;
if (this.sectors[name]?.value) {
@ -456,13 +455,13 @@ export class GfAllocationsPageComponent implements OnInit {
}
}
if (this.holdings[symbol].assetSubClass === 'ETF') {
if (this.holdings[symbol].assetProfile.assetSubClass === 'ETF') {
this.totalValueInEtf += this.holdings[symbol].value;
}
this.symbols[prettifySymbol(symbol)] = {
dataSource: position.dataSource,
name: position.name,
dataSource: assetProfile.dataSource,
name: assetProfile.name,
symbol: prettifySymbol(symbol),
value: isNumber(position.valueInBaseCurrency)
? position.valueInBaseCurrency
@ -515,8 +514,8 @@ export class GfAllocationsPageComponent implements OnInit {
this.totalValueInEtf > 0 ? value / this.totalValueInEtf : 0,
parents: Object.entries(this.portfolioDetails.holdings)
.map(([symbol, holding]) => {
if (holding.holdings.length > 0) {
const currentParentHolding = holding.holdings.find(
if (holding.assetProfile.holdings.length > 0) {
const currentParentHolding = holding.assetProfile.holdings.find(
(parentHolding) => {
return parentHolding.name === name;
}
@ -526,7 +525,7 @@ export class GfAllocationsPageComponent implements OnInit {
? {
allocationInPercentage:
currentParentHolding.valueInBaseCurrency / value,
name: holding.name,
name: holding.assetProfile.name,
position: holding,
symbol: prettifySymbol(symbol),
valueInBaseCurrency:

16
apps/client/src/app/pages/portfolio/analysis/analysis-page.html

@ -312,13 +312,15 @@
<a
class="d-flex"
[queryParams]="{
dataSource: holding.dataSource,
dataSource: holding.assetProfile.dataSource,
holdingDetailDialog: true,
symbol: holding.symbol
symbol: holding.assetProfile.symbol
}"
[routerLink]="[]"
>
<div class="flex-grow-1 mr-2">{{ holding.name }}</div>
<div class="flex-grow-1 mr-2">
{{ holding.assetProfile.name }}
</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
@ -361,13 +363,15 @@
<a
class="d-flex"
[queryParams]="{
dataSource: holding.dataSource,
dataSource: holding.assetProfile.dataSource,
holdingDetailDialog: true,
symbol: holding.symbol
symbol: holding.assetProfile.symbol
}"
[routerLink]="[]"
>
<div class="flex-grow-1 mr-2">{{ holding.name }}</div>
<div class="flex-grow-1 mr-2">
{{ holding.assetProfile.name }}
</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"

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

@ -69,7 +69,11 @@ export class GfPublicPageComponent implements OnInit {
};
public pageSize = Number.MAX_SAFE_INTEGER;
public positions: {
[symbol: string]: Pick<PortfolioPosition, 'currency' | 'name'> & {
[symbol: string]: {
assetProfile: Pick<
PortfolioPosition['assetProfile'],
'currency' | 'name'
>;
value: number;
};
};
@ -166,19 +170,23 @@ export class GfPublicPageComponent implements OnInit {
for (const [symbol, position] of Object.entries(
this.publicPortfolioDetails.holdings
)) {
const { assetProfile } = position;
this.holdings.push(position);
this.positions[symbol] = {
currency: position.currency,
name: position.name,
assetProfile: {
currency: assetProfile.currency,
name: assetProfile.name
},
value: position.allocationInPercentage
};
if (position.assetClass !== AssetClass.LIQUIDITY) {
if (assetProfile.assetClass !== AssetClass.LIQUIDITY) {
// Prepare analysis data by continents, countries, holdings and sectors except for liquidity
if (position.countries.length > 0) {
for (const country of position.countries) {
if (assetProfile.countries.length > 0) {
for (const country of assetProfile.countries) {
const { code, continent, name, weight } = country;
if (this.continents[continent]?.value) {
@ -215,8 +223,8 @@ export class GfPublicPageComponent implements OnInit {
this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency;
}
if (position.sectors.length > 0) {
for (const sector of position.sectors) {
if (assetProfile.sectors.length > 0) {
for (const sector of assetProfile.sectors) {
const { name, weight } = sector;
if (this.sectors[name]?.value) {
@ -238,7 +246,7 @@ export class GfPublicPageComponent implements OnInit {
}
this.symbols[prettifySymbol(symbol)] = {
name: position.name,
name: assetProfile.name,
symbol: prettifySymbol(symbol),
value: isNumber(position.valueInBaseCurrency)
? position.valueInBaseCurrency

14
libs/common/src/lib/helper.ts

@ -342,20 +342,6 @@ export function getYesterday() {
return subDays(new Date(Date.UTC(year, month, day)), 1);
}
export function groupBy<T, K extends keyof T>(
key: K,
arr: T[]
): Map<T[K], T[]> {
const map = new Map<T[K], T[]>();
arr.forEach((t) => {
if (!map.has(t[key])) {
map.set(t[key], []);
}
map.get(t[key])!.push(t);
});
return map;
}
export function interpolate(template: string, context: any) {
return template?.replace(/[$]{([^}]+)}/g, (_, objectPath) => {
const properties = objectPath.split('.');

45
libs/common/src/lib/interfaces/portfolio-position.interface.ts

@ -1,22 +1,13 @@
import { Market, MarketAdvanced } from '@ghostfolio/common/types';
import { AssetClass, AssetSubClass, DataSource, Tag } from '@prisma/client';
import { Tag } from '@prisma/client';
import { Country } from './country.interface';
import { EnhancedSymbolProfile } from './enhanced-symbol-profile.interface';
import { Holding } from './holding.interface';
import { Sector } from './sector.interface';
export interface PortfolioPosition {
activitiesCount: number;
allocationInPercentage: number;
/** @deprecated */
assetClass?: AssetClass;
/** @deprecated */
assetClassLabel?: string;
assetProfile: Pick<
EnhancedSymbolProfile,
| 'assetClass'
@ -34,21 +25,6 @@ export interface PortfolioPosition {
assetSubClassLabel?: string;
};
/** @deprecated */
assetSubClass?: AssetSubClass;
/** @deprecated */
assetSubClassLabel?: string;
/** @deprecated */
countries: Country[];
/** @deprecated */
currency: string;
/** @deprecated */
dataSource: DataSource;
dateOfFirstActivity: Date;
dividend: number;
exchange?: string;
@ -56,38 +32,19 @@ export interface PortfolioPosition {
grossPerformancePercent: number;
grossPerformancePercentWithCurrencyEffect: number;
grossPerformanceWithCurrencyEffect: number;
/** @deprecated */
holdings: Holding[];
investment: number;
marketChange?: number;
marketChangePercent?: number;
marketPrice: number;
markets?: { [key in Market]: number };
marketsAdvanced?: { [key in MarketAdvanced]: number };
/** @deprecated */
name: string;
netPerformance: number;
netPerformancePercent: number;
netPerformancePercentWithCurrencyEffect: number;
netPerformanceWithCurrencyEffect: number;
quantity: number;
/** @deprecated */
sectors: Sector[];
/** @deprecated */
symbol: string;
tags?: Tag[];
type?: string;
/** @deprecated */
url?: string;
valueInBaseCurrency?: number;
valueInPercentage?: number;
}

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

@ -14,32 +14,10 @@ export interface PublicPortfolioResponse extends PublicPortfolioResponseV1 {
[symbol: string]: Pick<
PortfolioPosition,
| 'allocationInPercentage'
/** @deprecated */
| 'assetClass'
| 'assetProfile'
/** @deprecated */
| 'countries'
| 'currency'
/** @deprecated */
| 'dataSource'
| 'dateOfFirstActivity'
| 'markets'
/** @deprecated */
| 'name'
| 'netPerformancePercentWithCurrencyEffect'
/** @deprecated */
| 'sectors'
/** @deprecated */
| 'symbol'
/** @deprecated */
| 'url'
| 'valueInBaseCurrency'
| 'valueInPercentage'
>;

42
libs/ui/src/lib/assistant/assistant.component.ts

@ -480,11 +480,14 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ holdings }) => {
this.holdings = holdings
.filter(({ assetSubClass }) => {
return assetSubClass && !['CASH'].includes(assetSubClass);
.filter(({ assetProfile }) => {
return (
assetProfile.assetSubClass &&
!['CASH'].includes(assetProfile.assetSubClass)
);
})
.sort((a, b) => {
return a.name?.localeCompare(b.name);
return a.assetProfile.name?.localeCompare(b.assetProfile.name);
});
this.setPortfolioFilterFormValues();
@ -506,11 +509,11 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
type: 'ASSET_CLASS'
},
{
id: filterValue?.holding?.dataSource ?? '',
id: filterValue?.holding?.assetProfile?.dataSource ?? '',
type: 'DATA_SOURCE'
},
{
id: filterValue?.holding?.symbol ?? '',
id: filterValue?.holding?.assetProfile?.symbol ?? '',
type: 'SYMBOL'
},
{
@ -697,18 +700,16 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
return EMPTY;
}),
map(({ holdings }) => {
return holdings.map(
({ assetSubClass, currency, dataSource, name, symbol }) => {
return {
currency,
dataSource,
name,
symbol,
assetSubClassString: translate(assetSubClass ?? ''),
mode: SearchMode.HOLDING as const
};
}
);
return holdings.map(({ assetProfile }) => {
return {
assetSubClassString: translate(assetProfile.assetSubClass ?? ''),
currency: assetProfile.currency,
dataSource: assetProfile.dataSource,
mode: SearchMode.HOLDING as const,
name: assetProfile.name,
symbol: assetProfile.symbol
};
});
}),
takeUntil(this.unsubscribeSubject)
);
@ -752,12 +753,13 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
'filters.dataSource'
] as DataSource;
const symbol = this.user?.settings?.['filters.symbol'];
const selectedHolding = this.holdings.find((holding) => {
const selectedHolding = this.holdings.find(({ assetProfile }) => {
return (
!!(dataSource && symbol) &&
getAssetProfileIdentifier({
dataSource: holding.dataSource,
symbol: holding.symbol
dataSource: assetProfile.dataSource,
symbol: assetProfile.symbol
}) === getAssetProfileIdentifier({ dataSource, symbol })
);
});

18
libs/ui/src/lib/holdings-table/holdings-table.component.html

@ -11,9 +11,9 @@
<th *matHeaderCellDef class="px-1" mat-header-cell></th>
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
<gf-entity-logo
[dataSource]="element.dataSource"
[symbol]="element.symbol"
[tooltip]="element.name"
[dataSource]="element.assetProfile.dataSource"
[symbol]="element.assetProfile.symbol"
[tooltip]="element.assetProfile.name"
/>
</td>
</ng-container>
@ -24,13 +24,13 @@
</th>
<td *matCellDef="let element" class="line-height-1 px-1" mat-cell>
<div class="text-truncate">
{{ element.name }}
@if (element.name === element.symbol) {
<span>({{ element.assetSubClassLabel }})</span>
{{ element.assetProfile.name }}
@if (element.assetProfile.name === element.assetProfile.symbol) {
<span>({{ element.assetProfile.assetSubClassLabel }})</span>
}
</div>
<div>
<small class="text-muted">{{ element.symbol }}</small>
<small class="text-muted">{{ element.assetProfile.symbol }}</small>
</div>
</td>
</ng-container>
@ -185,8 +185,8 @@
(click)="
canShowDetails(row) &&
onOpenHoldingDialog({
dataSource: row.dataSource,
symbol: row.symbol
dataSource: row.assetProfile.dataSource,
symbol: row.assetProfile.symbol
})
"
></tr>

4
libs/ui/src/lib/holdings-table/holdings-table.component.ts

@ -103,10 +103,10 @@ export class GfHoldingsTableComponent {
});
}
protected canShowDetails(holding: PortfolioPosition): boolean {
protected canShowDetails({ assetProfile }: PortfolioPosition): boolean {
return (
this.hasPermissionToOpenDetails() &&
!this.ignoreAssetSubClasses.includes(holding.assetSubClass)
!this.ignoreAssetSubClasses.includes(assetProfile.assetSubClass)
);
}

12
libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.html

@ -29,18 +29,22 @@
[compareWith]="holdingComparisonFunction"
>
<mat-select-trigger>{{
filterForm.get('holding')?.value?.name
filterForm.get('holding')?.value?.assetProfile?.name
}}</mat-select-trigger>
<mat-option [value]="null" />
@for (holding of holdings(); track holding.name) {
@for (
holding of holdings();
track getAssetProfileIdentifier(holding.assetProfile)
) {
<mat-option [value]="holding">
<div class="line-height-1 text-truncate">
<span
><b>{{ holding.name }}</b></span
><b>{{ holding.assetProfile.name }}</b></span
>
<br />
<small class="text-muted"
>{{ holding.symbol | gfSymbol }} · {{ holding.currency }}</small
>{{ holding.assetProfile.symbol | gfSymbol }} ·
{{ holding.currency }}</small
>
</div>
</mat-option>

5
libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.ts

@ -63,6 +63,8 @@ export class GfPortfolioFilterFormComponent
public readonly holdings = input<PortfolioPosition[]>([]);
public readonly tags = input<Filter[]>([]);
public getAssetProfileIdentifier = getAssetProfileIdentifier;
public filterForm: FormGroup<{
account: FormControl<string | null>;
assetClass: FormControl<string | null>;
@ -109,7 +111,8 @@ export class GfPortfolioFilterFormComponent
}
return (
getAssetProfileIdentifier(option) === getAssetProfileIdentifier(value)
getAssetProfileIdentifier(option.assetProfile) ===
getAssetProfileIdentifier(value.assetProfile)
);
}

2
libs/ui/src/lib/top-holdings/top-holdings.component.html

@ -121,7 +121,7 @@
*matRowDef="let row; columns: displayedColumns"
mat-row
[ngClass]="{ 'cursor-pointer': row.position }"
(click)="onClickHolding(row.position)"
(click)="onClickHolding(row.position.assetProfile)"
></tr>
<tr
*matFooterRowDef="displayedColumns"

4
libs/ui/src/lib/top-holdings/top-holdings.component.ts

@ -89,8 +89,8 @@ export class GfTopHoldingsComponent implements OnChanges, OnDestroy {
}
}
public onClickHolding(assetProfileIdentifier: AssetProfileIdentifier) {
this.holdingClicked.emit(assetProfileIdentifier);
public onClickHolding({ dataSource, symbol }: AssetProfileIdentifier) {
this.holdingClicked.emit({ dataSource, symbol });
}
public onShowAllHoldings() {

14
libs/ui/src/lib/treemap-chart/treemap-chart.component.ts

@ -278,8 +278,8 @@ export class GfTreemapChartComponent
);
}
const name = raw._data.name;
const symbol = raw._data.symbol;
const name = raw._data.assetProfile.name;
const symbol = raw._data.assetProfile.symbol;
return [
isUUID(symbol) ? (name ?? symbol) : symbol,
@ -320,8 +320,10 @@ export class GfTreemapChartComponent
['desc']
) as PortfolioPosition[];
const dataSource: DataSource = dataset[dataIndex].dataSource;
const symbol: string = dataset[dataIndex].symbol;
const dataSource: DataSource =
dataset[dataIndex].assetProfile.dataSource;
const symbol: string = dataset[dataIndex].assetProfile.symbol;
this.treemapChartClicked.emit({ dataSource, symbol });
} catch {}
@ -355,10 +357,10 @@ export class GfTreemapChartComponent
callbacks: {
label: ({ raw }: GfTreemapTooltipItem) => {
const allocationInPercentage = `${(raw._data.allocationInPercentage * 100).toFixed(2)}%`;
const name = raw._data.name;
const name = raw._data.assetProfile.name;
const sign =
raw._data.netPerformancePercentWithCurrencyEffect > 0 ? '+' : '';
const symbol = raw._data.symbol;
const symbol = raw._data.assetProfile.symbol;
const netPerformanceInPercentageWithSign = `${sign}${(raw._data.netPerformancePercentWithCurrencyEffect * 100).toFixed(2)}%`;

Loading…
Cancel
Save