Feature/allocations by etf holding (#3464)
* Setup allocations by ETF holding * Update changelog
This commit is contained in:
parent
3fb7e746df
commit
8a9ae9bb33
@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Introduced the allocations by ETF holding on the allocations page (experimental)
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Upgraded `prettier` from version `3.2.5` to `3.3.1`
|
- Upgraded `prettier` from version `3.2.5` to `3.3.1`
|
||||||
|
@ -335,6 +335,7 @@ export class AdminService {
|
|||||||
countries,
|
countries,
|
||||||
currency,
|
currency,
|
||||||
dataSource,
|
dataSource,
|
||||||
|
holdings,
|
||||||
name,
|
name,
|
||||||
scraperConfiguration,
|
scraperConfiguration,
|
||||||
sectors,
|
sectors,
|
||||||
@ -355,6 +356,7 @@ export class AdminService {
|
|||||||
countries,
|
countries,
|
||||||
currency,
|
currency,
|
||||||
dataSource,
|
dataSource,
|
||||||
|
holdings,
|
||||||
scraperConfiguration,
|
scraperConfiguration,
|
||||||
sectors,
|
sectors,
|
||||||
symbol,
|
symbol,
|
||||||
|
@ -13,10 +13,7 @@ import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/da
|
|||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
||||||
import {
|
import { DATA_GATHERING_QUEUE_PRIORITY_HIGH } from '@ghostfolio/common/config';
|
||||||
DATA_GATHERING_QUEUE_PRIORITY_HIGH,
|
|
||||||
DATA_GATHERING_QUEUE_PRIORITY_MEDIUM
|
|
||||||
} from '@ghostfolio/common/config';
|
|
||||||
import {
|
import {
|
||||||
DATE_FORMAT,
|
DATE_FORMAT,
|
||||||
getAssetProfileIdentifier,
|
getAssetProfileIdentifier,
|
||||||
@ -295,6 +292,7 @@ export class ImportService {
|
|||||||
figi,
|
figi,
|
||||||
figiComposite,
|
figiComposite,
|
||||||
figiShareClass,
|
figiShareClass,
|
||||||
|
holdings,
|
||||||
id,
|
id,
|
||||||
isin,
|
isin,
|
||||||
name,
|
name,
|
||||||
@ -367,6 +365,7 @@ export class ImportService {
|
|||||||
figi,
|
figi,
|
||||||
figiComposite,
|
figiComposite,
|
||||||
figiShareClass,
|
figiShareClass,
|
||||||
|
holdings,
|
||||||
id,
|
id,
|
||||||
isin,
|
isin,
|
||||||
name,
|
name,
|
||||||
@ -538,6 +537,7 @@ export class ImportService {
|
|||||||
assetSubClass: undefined,
|
assetSubClass: undefined,
|
||||||
countries: undefined,
|
countries: undefined,
|
||||||
createdAt: undefined,
|
createdAt: undefined,
|
||||||
|
holdings: undefined,
|
||||||
id: undefined,
|
id: undefined,
|
||||||
sectors: undefined,
|
sectors: undefined,
|
||||||
updatedAt: undefined
|
updatedAt: undefined
|
||||||
|
@ -20,6 +20,7 @@ export const symbolProfileDummyData = {
|
|||||||
assetSubClass: undefined,
|
assetSubClass: undefined,
|
||||||
countries: [],
|
countries: [],
|
||||||
createdAt: undefined,
|
createdAt: undefined,
|
||||||
|
holdings: [],
|
||||||
id: undefined,
|
id: undefined,
|
||||||
sectors: [],
|
sectors: [],
|
||||||
updatedAt: undefined
|
updatedAt: undefined
|
||||||
|
@ -499,6 +499,7 @@ export class PortfolioService {
|
|||||||
grossPerformancePercentageWithCurrencyEffect?.toNumber() ?? 0,
|
grossPerformancePercentageWithCurrencyEffect?.toNumber() ?? 0,
|
||||||
grossPerformanceWithCurrencyEffect:
|
grossPerformanceWithCurrencyEffect:
|
||||||
grossPerformanceWithCurrencyEffect?.toNumber() ?? 0,
|
grossPerformanceWithCurrencyEffect?.toNumber() ?? 0,
|
||||||
|
holdings: assetProfile.holdings,
|
||||||
investment: investment.toNumber(),
|
investment: investment.toNumber(),
|
||||||
marketState: dataProviderResponse?.marketState ?? 'delayed',
|
marketState: dataProviderResponse?.marketState ?? 'delayed',
|
||||||
name: assetProfile.name,
|
name: assetProfile.name,
|
||||||
@ -1465,6 +1466,7 @@ export class PortfolioService {
|
|||||||
grossPerformancePercent: 0,
|
grossPerformancePercent: 0,
|
||||||
grossPerformancePercentWithCurrencyEffect: 0,
|
grossPerformancePercentWithCurrencyEffect: 0,
|
||||||
grossPerformanceWithCurrencyEffect: 0,
|
grossPerformanceWithCurrencyEffect: 0,
|
||||||
|
holdings: [],
|
||||||
investment: balance,
|
investment: balance,
|
||||||
marketPrice: 0,
|
marketPrice: 0,
|
||||||
marketState: 'open',
|
marketState: 'open',
|
||||||
|
@ -181,6 +181,7 @@ export class DataGatheringService {
|
|||||||
figi,
|
figi,
|
||||||
figiComposite,
|
figiComposite,
|
||||||
figiShareClass,
|
figiShareClass,
|
||||||
|
holdings,
|
||||||
isin,
|
isin,
|
||||||
name,
|
name,
|
||||||
sectors,
|
sectors,
|
||||||
@ -198,6 +199,7 @@ export class DataGatheringService {
|
|||||||
figi,
|
figi,
|
||||||
figiComposite,
|
figiComposite,
|
||||||
figiShareClass,
|
figiShareClass,
|
||||||
|
holdings,
|
||||||
isin,
|
isin,
|
||||||
name,
|
name,
|
||||||
sectors,
|
sectors,
|
||||||
@ -212,6 +214,7 @@ export class DataGatheringService {
|
|||||||
figi,
|
figi,
|
||||||
figiComposite,
|
figiComposite,
|
||||||
figiShareClass,
|
figiShareClass,
|
||||||
|
holdings,
|
||||||
isin,
|
isin,
|
||||||
name,
|
name,
|
||||||
sectors,
|
sectors,
|
||||||
|
@ -36,6 +36,7 @@ export class DataEnhancerService {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
(assetProfile.countries as unknown as Prisma.JsonArray)?.length > 0 &&
|
(assetProfile.countries as unknown as Prisma.JsonArray)?.length > 0 &&
|
||||||
|
(assetProfile.holdings as unknown as Prisma.JsonArray)?.length > 0 &&
|
||||||
(assetProfile.sectors as unknown as Prisma.JsonArray)?.length > 0
|
(assetProfile.sectors as unknown as Prisma.JsonArray)?.length > 0
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
|
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
|
||||||
|
import { Holding } from '@ghostfolio/common/interfaces';
|
||||||
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
||||||
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
||||||
|
|
||||||
@ -155,11 +156,26 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!response.holdings ||
|
||||||
|
(response.holdings as unknown as Holding[]).length === 0
|
||||||
|
) {
|
||||||
|
response.holdings = [];
|
||||||
|
|
||||||
|
for (const { label, weight } of holdings?.topHoldings ?? []) {
|
||||||
|
response.holdings.push({
|
||||||
|
weight,
|
||||||
|
name: label
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!response.sectors ||
|
!response.sectors ||
|
||||||
(response.sectors as unknown as Sector[]).length === 0
|
(response.sectors as unknown as Sector[]).length === 0
|
||||||
) {
|
) {
|
||||||
response.sectors = [];
|
response.sectors = [];
|
||||||
|
|
||||||
for (const [name, value] of Object.entries<any>(
|
for (const [name, value] of Object.entries<any>(
|
||||||
holdings?.sectors ?? {}
|
holdings?.sectors ?? {}
|
||||||
)) {
|
)) {
|
||||||
|
@ -2,6 +2,7 @@ import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
|||||||
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||||
import {
|
import {
|
||||||
EnhancedSymbolProfile,
|
EnhancedSymbolProfile,
|
||||||
|
Holding,
|
||||||
ScraperConfiguration,
|
ScraperConfiguration,
|
||||||
UniqueAsset
|
UniqueAsset
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
@ -97,6 +98,7 @@ export class SymbolProfileService {
|
|||||||
countries,
|
countries,
|
||||||
currency,
|
currency,
|
||||||
dataSource,
|
dataSource,
|
||||||
|
holdings,
|
||||||
name,
|
name,
|
||||||
scraperConfiguration,
|
scraperConfiguration,
|
||||||
sectors,
|
sectors,
|
||||||
@ -112,6 +114,7 @@ export class SymbolProfileService {
|
|||||||
comment,
|
comment,
|
||||||
countries,
|
countries,
|
||||||
currency,
|
currency,
|
||||||
|
holdings,
|
||||||
name,
|
name,
|
||||||
scraperConfiguration,
|
scraperConfiguration,
|
||||||
sectors,
|
sectors,
|
||||||
@ -140,6 +143,7 @@ export class SymbolProfileService {
|
|||||||
symbolProfile?.countries as unknown as Prisma.JsonArray
|
symbolProfile?.countries as unknown as Prisma.JsonArray
|
||||||
),
|
),
|
||||||
dateOfFirstActivity: <Date>undefined,
|
dateOfFirstActivity: <Date>undefined,
|
||||||
|
holdings: this.getHoldings(symbolProfile),
|
||||||
scraperConfiguration: this.getScraperConfiguration(symbolProfile),
|
scraperConfiguration: this.getScraperConfiguration(symbolProfile),
|
||||||
sectors: this.getSectors(symbolProfile),
|
sectors: this.getSectors(symbolProfile),
|
||||||
symbolMapping: this.getSymbolMapping(symbolProfile)
|
symbolMapping: this.getSymbolMapping(symbolProfile)
|
||||||
@ -167,6 +171,14 @@ export class SymbolProfileService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(item.SymbolProfileOverrides.holdings as unknown as Holding[])
|
||||||
|
?.length > 0
|
||||||
|
) {
|
||||||
|
item.holdings = item.SymbolProfileOverrides
|
||||||
|
.holdings as unknown as Holding[];
|
||||||
|
}
|
||||||
|
|
||||||
item.name = item.SymbolProfileOverrides?.name ?? item.name;
|
item.name = item.SymbolProfileOverrides?.name ?? item.name;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -203,6 +215,19 @@ export class SymbolProfileService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getHoldings(symbolProfile: SymbolProfile): Holding[] {
|
||||||
|
return ((symbolProfile?.holdings as Prisma.JsonArray) ?? []).map(
|
||||||
|
(holding) => {
|
||||||
|
const { name, weight } = holding as Prisma.JsonObject;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: (name as string) ?? UNKNOWN_KEY,
|
||||||
|
valueInBaseCurrency: weight as number
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private getScraperConfiguration(
|
private getScraperConfiguration(
|
||||||
symbolProfile: SymbolProfile
|
symbolProfile: SymbolProfile
|
||||||
): ScraperConfiguration {
|
): ScraperConfiguration {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
.mat-mdc-table {
|
.gf-table {
|
||||||
th {
|
th {
|
||||||
::ng-deep {
|
::ng-deep {
|
||||||
.mat-sort-header-container {
|
.mat-sort-header-container {
|
||||||
|
@ -6,6 +6,7 @@ import { UserService } from '@ghostfolio/client/services/user/user.service';
|
|||||||
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||||
import { prettifySymbol } from '@ghostfolio/common/helper';
|
import { prettifySymbol } from '@ghostfolio/common/helper';
|
||||||
import {
|
import {
|
||||||
|
Holding,
|
||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
PortfolioPosition,
|
PortfolioPosition,
|
||||||
UniqueAsset,
|
UniqueAsset,
|
||||||
@ -84,6 +85,11 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
value: number;
|
value: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
public topHoldings: Holding[] = [];
|
||||||
|
public topHoldingsMap: {
|
||||||
|
[name: string]: { name: string; value: number };
|
||||||
|
};
|
||||||
|
public totalValueInEtf = 0;
|
||||||
public UNKNOWN_KEY = UNKNOWN_KEY;
|
public UNKNOWN_KEY = UNKNOWN_KEY;
|
||||||
public user: User;
|
public user: User;
|
||||||
public worldMapChartFormat: string;
|
public worldMapChartFormat: string;
|
||||||
@ -288,6 +294,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
value: 0
|
value: 0
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
this.topHoldingsMap = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
private initializeAllocationsData() {
|
private initializeAllocationsData() {
|
||||||
@ -337,7 +344,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (position.assetClass !== AssetClass.LIQUIDITY) {
|
if (position.assetClass !== AssetClass.LIQUIDITY) {
|
||||||
// Prepare analysis data by continents, countries and sectors except for liquidity
|
// Prepare analysis data by continents, countries, holdings and sectors except for liquidity
|
||||||
|
|
||||||
if (position.countries.length > 0) {
|
if (position.countries.length > 0) {
|
||||||
this.markets.developedMarkets.value +=
|
this.markets.developedMarkets.value +=
|
||||||
@ -445,6 +452,29 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
: this.portfolioDetails.holdings[symbol].valueInPercentage;
|
: this.portfolioDetails.holdings[symbol].valueInPercentage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (position.holdings.length > 0) {
|
||||||
|
for (const holding of position.holdings) {
|
||||||
|
const { name, valueInBaseCurrency } = holding;
|
||||||
|
|
||||||
|
if (this.topHoldingsMap[name]?.value) {
|
||||||
|
this.topHoldingsMap[name].value +=
|
||||||
|
valueInBaseCurrency *
|
||||||
|
(isNumber(position.valueInBaseCurrency)
|
||||||
|
? position.valueInBaseCurrency
|
||||||
|
: position.valueInPercentage);
|
||||||
|
} else {
|
||||||
|
this.topHoldingsMap[name] = {
|
||||||
|
name,
|
||||||
|
value:
|
||||||
|
valueInBaseCurrency *
|
||||||
|
(isNumber(position.valueInBaseCurrency)
|
||||||
|
? this.portfolioDetails.holdings[symbol].valueInBaseCurrency
|
||||||
|
: this.portfolioDetails.holdings[symbol].valueInPercentage)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (position.sectors.length > 0) {
|
if (position.sectors.length > 0) {
|
||||||
for (const sector of position.sectors) {
|
for (const sector of position.sectors) {
|
||||||
const { name, weight } = sector;
|
const { name, weight } = sector;
|
||||||
@ -475,6 +505,14 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.positions[symbol].assetSubClass === 'ETF' &&
|
||||||
|
!this.hasImpersonationId &&
|
||||||
|
!this.user.settings.isRestrictedView
|
||||||
|
) {
|
||||||
|
this.totalValueInEtf += this.positions[symbol].value;
|
||||||
|
}
|
||||||
|
|
||||||
this.symbols[prettifySymbol(symbol)] = {
|
this.symbols[prettifySymbol(symbol)] = {
|
||||||
dataSource: position.dataSource,
|
dataSource: position.dataSource,
|
||||||
name: position.name,
|
name: position.name,
|
||||||
@ -518,6 +556,21 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
this.markets.otherMarkets.value / marketsTotal;
|
this.markets.otherMarkets.value / marketsTotal;
|
||||||
this.markets[UNKNOWN_KEY].value =
|
this.markets[UNKNOWN_KEY].value =
|
||||||
this.markets[UNKNOWN_KEY].value / marketsTotal;
|
this.markets[UNKNOWN_KEY].value / marketsTotal;
|
||||||
|
|
||||||
|
if (!this.hasImpersonationId && !this.user.settings.isRestrictedView) {
|
||||||
|
this.topHoldings = Object.values(this.topHoldingsMap)
|
||||||
|
.map(({ name, value }) => {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
allocationInPercentage:
|
||||||
|
this.totalValueInEtf > 0 ? value / this.totalValueInEtf : 0,
|
||||||
|
valueInBaseCurrency: value
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
return b.valueInBaseCurrency - a.valueInBaseCurrency;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private openAccountDetailDialog(aAccountId: string) {
|
private openAccountDetailDialog(aAccountId: string) {
|
||||||
|
@ -330,5 +330,33 @@
|
|||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
|
@if (topHoldings?.length > 0 && user?.settings?.isExperimentalFeatures) {
|
||||||
|
<div class="col-md-12">
|
||||||
|
<mat-card appearance="outlined" class="mb-3">
|
||||||
|
<mat-card-header class="overflow-hidden w-100">
|
||||||
|
<mat-card-title class="align-items-center d-flex text-truncate"
|
||||||
|
><span i18n>By ETF Holding</span>
|
||||||
|
<gf-premium-indicator
|
||||||
|
*ngIf="user?.subscription?.type === 'Basic'"
|
||||||
|
class="ml-1"
|
||||||
|
/>
|
||||||
|
</mat-card-title>
|
||||||
|
<mat-card-subtitle>
|
||||||
|
<ng-container i18n
|
||||||
|
>Approximation based on the Top 15 holdings per
|
||||||
|
ETF</ng-container
|
||||||
|
>
|
||||||
|
</mat-card-subtitle>
|
||||||
|
</mat-card-header>
|
||||||
|
<mat-card-content>
|
||||||
|
<gf-top-holdings
|
||||||
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
|
[locale]="user?.settings?.locale"
|
||||||
|
[topHoldings]="topHoldings"
|
||||||
|
/>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module';
|
import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module';
|
||||||
import { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart';
|
import { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart';
|
||||||
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
|
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
|
||||||
|
import { GfTopHoldingsComponent } from '@ghostfolio/ui/top-holdings';
|
||||||
import { GfValueComponent } from '@ghostfolio/ui/value';
|
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
@ -19,8 +20,9 @@ import { AllocationsPageComponent } from './allocations-page.component';
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
GfPortfolioProportionChartComponent,
|
GfPortfolioProportionChartComponent,
|
||||||
GfPremiumIndicatorComponent,
|
GfPremiumIndicatorComponent,
|
||||||
GfWorldMapChartModule,
|
GfTopHoldingsComponent,
|
||||||
GfValueComponent,
|
GfValueComponent,
|
||||||
|
GfWorldMapChartModule,
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
MatDialogModule,
|
MatDialogModule,
|
||||||
MatProgressBarModule
|
MatProgressBarModule
|
||||||
|
@ -2,6 +2,7 @@ import { AssetClass, AssetSubClass, DataSource } from '@prisma/client';
|
|||||||
|
|
||||||
import { Country } from './country.interface';
|
import { Country } from './country.interface';
|
||||||
import { DataProviderInfo } from './data-provider-info.interface';
|
import { DataProviderInfo } from './data-provider-info.interface';
|
||||||
|
import { Holding } from './holding.interface';
|
||||||
import { ScraperConfiguration } from './scraper-configuration.interface';
|
import { ScraperConfiguration } from './scraper-configuration.interface';
|
||||||
import { Sector } from './sector.interface';
|
import { Sector } from './sector.interface';
|
||||||
|
|
||||||
@ -16,10 +17,11 @@ export interface EnhancedSymbolProfile {
|
|||||||
dataProviderInfo?: DataProviderInfo;
|
dataProviderInfo?: DataProviderInfo;
|
||||||
dataSource: DataSource;
|
dataSource: DataSource;
|
||||||
dateOfFirstActivity?: Date;
|
dateOfFirstActivity?: Date;
|
||||||
id: string;
|
|
||||||
figi?: string;
|
figi?: string;
|
||||||
figiComposite?: string;
|
figiComposite?: string;
|
||||||
figiShareClass?: string;
|
figiShareClass?: string;
|
||||||
|
holdings: Holding[];
|
||||||
|
id: string;
|
||||||
isin?: string;
|
isin?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
scraperConfiguration?: ScraperConfiguration;
|
scraperConfiguration?: ScraperConfiguration;
|
||||||
|
5
libs/common/src/lib/interfaces/holding.interface.ts
Normal file
5
libs/common/src/lib/interfaces/holding.interface.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export interface Holding {
|
||||||
|
allocationInPercentage?: number;
|
||||||
|
name: string;
|
||||||
|
valueInBaseCurrency: number;
|
||||||
|
}
|
@ -17,6 +17,7 @@ import type { Export } from './export.interface';
|
|||||||
import type { FilterGroup } from './filter-group.interface';
|
import type { FilterGroup } from './filter-group.interface';
|
||||||
import type { Filter } from './filter.interface';
|
import type { Filter } from './filter.interface';
|
||||||
import type { HistoricalDataItem } from './historical-data-item.interface';
|
import type { HistoricalDataItem } from './historical-data-item.interface';
|
||||||
|
import type { Holding } from './holding.interface';
|
||||||
import type { InfoItem } from './info-item.interface';
|
import type { InfoItem } from './info-item.interface';
|
||||||
import type { InvestmentItem } from './investment-item.interface';
|
import type { InvestmentItem } from './investment-item.interface';
|
||||||
import type { LineChartItem } from './line-chart-item.interface';
|
import type { LineChartItem } from './line-chart-item.interface';
|
||||||
@ -71,6 +72,7 @@ export {
|
|||||||
Filter,
|
Filter,
|
||||||
FilterGroup,
|
FilterGroup,
|
||||||
HistoricalDataItem,
|
HistoricalDataItem,
|
||||||
|
Holding,
|
||||||
ImportResponse,
|
ImportResponse,
|
||||||
InfoItem,
|
InfoItem,
|
||||||
InvestmentItem,
|
InvestmentItem,
|
||||||
|
@ -2,6 +2,7 @@ import { AssetClass, AssetSubClass, DataSource, Tag } from '@prisma/client';
|
|||||||
|
|
||||||
import { Market, MarketAdvanced, MarketState } from '../types';
|
import { Market, MarketAdvanced, MarketState } from '../types';
|
||||||
import { Country } from './country.interface';
|
import { Country } from './country.interface';
|
||||||
|
import { Holding } from './holding.interface';
|
||||||
import { Sector } from './sector.interface';
|
import { Sector } from './sector.interface';
|
||||||
|
|
||||||
export interface PortfolioPosition {
|
export interface PortfolioPosition {
|
||||||
@ -20,6 +21,7 @@ export interface PortfolioPosition {
|
|||||||
grossPerformancePercent: number;
|
grossPerformancePercent: number;
|
||||||
grossPerformancePercentWithCurrencyEffect: number;
|
grossPerformancePercentWithCurrencyEffect: number;
|
||||||
grossPerformanceWithCurrencyEffect: number;
|
grossPerformanceWithCurrencyEffect: number;
|
||||||
|
holdings: Holding[];
|
||||||
investment: number;
|
investment: number;
|
||||||
marketChange?: number;
|
marketChange?: number;
|
||||||
marketChangePercent?: number;
|
marketChangePercent?: number;
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
.holdings {
|
.holdings {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
|
||||||
.mat-mdc-table {
|
.gf-table {
|
||||||
th {
|
th {
|
||||||
::ng-deep {
|
::ng-deep {
|
||||||
.mat-sort-header-container {
|
.mat-sort-header-container {
|
||||||
|
1
libs/ui/src/lib/top-holdings/index.ts
Normal file
1
libs/ui/src/lib/top-holdings/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './top-holdings.component';
|
61
libs/ui/src/lib/top-holdings/top-holdings.component.html
Normal file
61
libs/ui/src/lib/top-holdings/top-holdings.component.html
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<table
|
||||||
|
class="gf-table w-100"
|
||||||
|
mat-table
|
||||||
|
matSort
|
||||||
|
matSortActive="allocationInPercentage"
|
||||||
|
matSortDirection="desc"
|
||||||
|
[dataSource]="dataSource"
|
||||||
|
>
|
||||||
|
<ng-container matColumnDef="name">
|
||||||
|
<th *matHeaderCellDef class="px-2" mat-header-cell mat-sort-header>
|
||||||
|
<ng-container i18n>Name</ng-container>
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="px-2" mat-cell>
|
||||||
|
{{ element?.name }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="valueInBaseCurrency">
|
||||||
|
<th
|
||||||
|
*matHeaderCellDef
|
||||||
|
class="justify-content-end px-2"
|
||||||
|
mat-header-cell
|
||||||
|
mat-sort-header
|
||||||
|
>
|
||||||
|
<ng-container i18n>Value</ng-container>
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="px-2" mat-cell>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<gf-value
|
||||||
|
[isCurrency]="true"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="element?.valueInBaseCurrency"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="allocationInPercentage">
|
||||||
|
<th
|
||||||
|
*matHeaderCellDef
|
||||||
|
class="justify-content-end px-2"
|
||||||
|
mat-header-cell
|
||||||
|
mat-sort-header
|
||||||
|
>
|
||||||
|
<span class="d-none d-sm-block" i18n>Allocation</span>
|
||||||
|
<span class="d-block d-sm-none" title="Allocation">%</span>
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="px-2" mat-cell>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<gf-value
|
||||||
|
[isPercent]="true"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="element?.allocationInPercentage"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
||||||
|
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
|
||||||
|
</table>
|
13
libs/ui/src/lib/top-holdings/top-holdings.component.scss
Normal file
13
libs/ui/src/lib/top-holdings/top-holdings.component.scss
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
.gf-table {
|
||||||
|
th {
|
||||||
|
::ng-deep {
|
||||||
|
.mat-sort-header-container {
|
||||||
|
justify-content: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
63
libs/ui/src/lib/top-holdings/top-holdings.component.ts
Normal file
63
libs/ui/src/lib/top-holdings/top-holdings.component.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { getLocale } from '@ghostfolio/common/helper';
|
||||||
|
import { Holding } from '@ghostfolio/common/interfaces';
|
||||||
|
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CUSTOM_ELEMENTS_SCHEMA,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
Input,
|
||||||
|
OnChanges,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
ViewChild
|
||||||
|
} from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatSort, MatSortModule } from '@angular/material/sort';
|
||||||
|
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
|
||||||
|
import { get } from 'lodash';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
imports: [GfValueComponent, MatButtonModule, MatSortModule, MatTableModule],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
|
selector: 'gf-top-holdings',
|
||||||
|
standalone: true,
|
||||||
|
styleUrls: ['./top-holdings.component.scss'],
|
||||||
|
templateUrl: './top-holdings.component.html'
|
||||||
|
})
|
||||||
|
export class GfTopHoldingsComponent implements OnChanges, OnDestroy, OnInit {
|
||||||
|
@Input() baseCurrency: string;
|
||||||
|
@Input() locale = getLocale();
|
||||||
|
@Input() topHoldings: Holding[];
|
||||||
|
|
||||||
|
@ViewChild(MatSort) sort: MatSort;
|
||||||
|
|
||||||
|
public dataSource: MatTableDataSource<Holding> = new MatTableDataSource();
|
||||||
|
public displayedColumns: string[] = [
|
||||||
|
'name',
|
||||||
|
'valueInBaseCurrency',
|
||||||
|
'allocationInPercentage'
|
||||||
|
];
|
||||||
|
|
||||||
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
|
public constructor() {}
|
||||||
|
|
||||||
|
public ngOnInit() {}
|
||||||
|
|
||||||
|
public ngOnChanges() {
|
||||||
|
if (this.topHoldings) {
|
||||||
|
this.dataSource = new MatTableDataSource(this.topHoldings);
|
||||||
|
|
||||||
|
this.dataSource.sort = this.sort;
|
||||||
|
this.dataSource.sortingDataAccessor = get;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "SymbolProfile" ADD COLUMN "holdings" JSONB DEFAULT '[]';
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "SymbolProfileOverrides" ADD COLUMN "holdings" JSONB DEFAULT '[]';
|
@ -164,6 +164,7 @@ model SymbolProfile {
|
|||||||
figi String?
|
figi String?
|
||||||
figiComposite String?
|
figiComposite String?
|
||||||
figiShareClass String?
|
figiShareClass String?
|
||||||
|
holdings Json? @default("[]")
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
isin String?
|
isin String?
|
||||||
name String?
|
name String?
|
||||||
@ -189,6 +190,7 @@ model SymbolProfileOverrides {
|
|||||||
assetClass AssetClass?
|
assetClass AssetClass?
|
||||||
assetSubClass AssetSubClass?
|
assetSubClass AssetSubClass?
|
||||||
countries Json? @default("[]")
|
countries Json? @default("[]")
|
||||||
|
holdings Json? @default("[]")
|
||||||
name String?
|
name String?
|
||||||
sectors Json? @default("[]")
|
sectors Json? @default("[]")
|
||||||
url String?
|
url String?
|
||||||
|
Loading…
x
Reference in New Issue
Block a user