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
|
||||
|
||||
### Added
|
||||
|
||||
- Introduced the allocations by ETF holding on the allocations page (experimental)
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgraded `prettier` from version `3.2.5` to `3.3.1`
|
||||
|
@ -335,6 +335,7 @@ export class AdminService {
|
||||
countries,
|
||||
currency,
|
||||
dataSource,
|
||||
holdings,
|
||||
name,
|
||||
scraperConfiguration,
|
||||
sectors,
|
||||
@ -355,6 +356,7 @@ export class AdminService {
|
||||
countries,
|
||||
currency,
|
||||
dataSource,
|
||||
holdings,
|
||||
scraperConfiguration,
|
||||
sectors,
|
||||
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 { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
||||
import {
|
||||
DATA_GATHERING_QUEUE_PRIORITY_HIGH,
|
||||
DATA_GATHERING_QUEUE_PRIORITY_MEDIUM
|
||||
} from '@ghostfolio/common/config';
|
||||
import { DATA_GATHERING_QUEUE_PRIORITY_HIGH } from '@ghostfolio/common/config';
|
||||
import {
|
||||
DATE_FORMAT,
|
||||
getAssetProfileIdentifier,
|
||||
@ -295,6 +292,7 @@ export class ImportService {
|
||||
figi,
|
||||
figiComposite,
|
||||
figiShareClass,
|
||||
holdings,
|
||||
id,
|
||||
isin,
|
||||
name,
|
||||
@ -367,6 +365,7 @@ export class ImportService {
|
||||
figi,
|
||||
figiComposite,
|
||||
figiShareClass,
|
||||
holdings,
|
||||
id,
|
||||
isin,
|
||||
name,
|
||||
@ -538,6 +537,7 @@ export class ImportService {
|
||||
assetSubClass: undefined,
|
||||
countries: undefined,
|
||||
createdAt: undefined,
|
||||
holdings: undefined,
|
||||
id: undefined,
|
||||
sectors: undefined,
|
||||
updatedAt: undefined
|
||||
|
@ -20,6 +20,7 @@ export const symbolProfileDummyData = {
|
||||
assetSubClass: undefined,
|
||||
countries: [],
|
||||
createdAt: undefined,
|
||||
holdings: [],
|
||||
id: undefined,
|
||||
sectors: [],
|
||||
updatedAt: undefined
|
||||
|
@ -499,6 +499,7 @@ export class PortfolioService {
|
||||
grossPerformancePercentageWithCurrencyEffect?.toNumber() ?? 0,
|
||||
grossPerformanceWithCurrencyEffect:
|
||||
grossPerformanceWithCurrencyEffect?.toNumber() ?? 0,
|
||||
holdings: assetProfile.holdings,
|
||||
investment: investment.toNumber(),
|
||||
marketState: dataProviderResponse?.marketState ?? 'delayed',
|
||||
name: assetProfile.name,
|
||||
@ -1465,6 +1466,7 @@ export class PortfolioService {
|
||||
grossPerformancePercent: 0,
|
||||
grossPerformancePercentWithCurrencyEffect: 0,
|
||||
grossPerformanceWithCurrencyEffect: 0,
|
||||
holdings: [],
|
||||
investment: balance,
|
||||
marketPrice: 0,
|
||||
marketState: 'open',
|
||||
|
@ -181,6 +181,7 @@ export class DataGatheringService {
|
||||
figi,
|
||||
figiComposite,
|
||||
figiShareClass,
|
||||
holdings,
|
||||
isin,
|
||||
name,
|
||||
sectors,
|
||||
@ -198,6 +199,7 @@ export class DataGatheringService {
|
||||
figi,
|
||||
figiComposite,
|
||||
figiShareClass,
|
||||
holdings,
|
||||
isin,
|
||||
name,
|
||||
sectors,
|
||||
@ -212,6 +214,7 @@ export class DataGatheringService {
|
||||
figi,
|
||||
figiComposite,
|
||||
figiShareClass,
|
||||
holdings,
|
||||
isin,
|
||||
name,
|
||||
sectors,
|
||||
|
@ -36,6 +36,7 @@ export class DataEnhancerService {
|
||||
|
||||
if (
|
||||
(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
|
||||
) {
|
||||
return true;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||
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 { 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 (
|
||||
!response.sectors ||
|
||||
(response.sectors as unknown as Sector[]).length === 0
|
||||
) {
|
||||
response.sectors = [];
|
||||
|
||||
for (const [name, value] of Object.entries<any>(
|
||||
holdings?.sectors ?? {}
|
||||
)) {
|
||||
|
@ -2,6 +2,7 @@ import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||
import {
|
||||
EnhancedSymbolProfile,
|
||||
Holding,
|
||||
ScraperConfiguration,
|
||||
UniqueAsset
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
@ -97,6 +98,7 @@ export class SymbolProfileService {
|
||||
countries,
|
||||
currency,
|
||||
dataSource,
|
||||
holdings,
|
||||
name,
|
||||
scraperConfiguration,
|
||||
sectors,
|
||||
@ -112,6 +114,7 @@ export class SymbolProfileService {
|
||||
comment,
|
||||
countries,
|
||||
currency,
|
||||
holdings,
|
||||
name,
|
||||
scraperConfiguration,
|
||||
sectors,
|
||||
@ -140,6 +143,7 @@ export class SymbolProfileService {
|
||||
symbolProfile?.countries as unknown as Prisma.JsonArray
|
||||
),
|
||||
dateOfFirstActivity: <Date>undefined,
|
||||
holdings: this.getHoldings(symbolProfile),
|
||||
scraperConfiguration: this.getScraperConfiguration(symbolProfile),
|
||||
sectors: this.getSectors(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;
|
||||
|
||||
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(
|
||||
symbolProfile: SymbolProfile
|
||||
): ScraperConfiguration {
|
||||
|
@ -1,7 +1,7 @@
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
.mat-mdc-table {
|
||||
.gf-table {
|
||||
th {
|
||||
::ng-deep {
|
||||
.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 { prettifySymbol } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
Holding,
|
||||
PortfolioDetails,
|
||||
PortfolioPosition,
|
||||
UniqueAsset,
|
||||
@ -84,6 +85,11 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
value: number;
|
||||
};
|
||||
};
|
||||
public topHoldings: Holding[] = [];
|
||||
public topHoldingsMap: {
|
||||
[name: string]: { name: string; value: number };
|
||||
};
|
||||
public totalValueInEtf = 0;
|
||||
public UNKNOWN_KEY = UNKNOWN_KEY;
|
||||
public user: User;
|
||||
public worldMapChartFormat: string;
|
||||
@ -288,6 +294,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
value: 0
|
||||
}
|
||||
};
|
||||
this.topHoldingsMap = {};
|
||||
}
|
||||
|
||||
private initializeAllocationsData() {
|
||||
@ -337,7 +344,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
};
|
||||
|
||||
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) {
|
||||
this.markets.developedMarkets.value +=
|
||||
@ -445,6 +452,29 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
: 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) {
|
||||
for (const sector of position.sectors) {
|
||||
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)] = {
|
||||
dataSource: position.dataSource,
|
||||
name: position.name,
|
||||
@ -518,6 +556,21 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
this.markets.otherMarkets.value / marketsTotal;
|
||||
this.markets[UNKNOWN_KEY].value =
|
||||
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) {
|
||||
|
@ -330,5 +330,33 @@
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</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>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module';
|
||||
import { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart';
|
||||
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
|
||||
import { GfTopHoldingsComponent } from '@ghostfolio/ui/top-holdings';
|
||||
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
@ -19,8 +20,9 @@ import { AllocationsPageComponent } from './allocations-page.component';
|
||||
CommonModule,
|
||||
GfPortfolioProportionChartComponent,
|
||||
GfPremiumIndicatorComponent,
|
||||
GfWorldMapChartModule,
|
||||
GfTopHoldingsComponent,
|
||||
GfValueComponent,
|
||||
GfWorldMapChartModule,
|
||||
MatCardModule,
|
||||
MatDialogModule,
|
||||
MatProgressBarModule
|
||||
|
@ -2,6 +2,7 @@ import { AssetClass, AssetSubClass, DataSource } from '@prisma/client';
|
||||
|
||||
import { Country } from './country.interface';
|
||||
import { DataProviderInfo } from './data-provider-info.interface';
|
||||
import { Holding } from './holding.interface';
|
||||
import { ScraperConfiguration } from './scraper-configuration.interface';
|
||||
import { Sector } from './sector.interface';
|
||||
|
||||
@ -16,10 +17,11 @@ export interface EnhancedSymbolProfile {
|
||||
dataProviderInfo?: DataProviderInfo;
|
||||
dataSource: DataSource;
|
||||
dateOfFirstActivity?: Date;
|
||||
id: string;
|
||||
figi?: string;
|
||||
figiComposite?: string;
|
||||
figiShareClass?: string;
|
||||
holdings: Holding[];
|
||||
id: string;
|
||||
isin?: string;
|
||||
name?: string;
|
||||
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 { Filter } from './filter.interface';
|
||||
import type { HistoricalDataItem } from './historical-data-item.interface';
|
||||
import type { Holding } from './holding.interface';
|
||||
import type { InfoItem } from './info-item.interface';
|
||||
import type { InvestmentItem } from './investment-item.interface';
|
||||
import type { LineChartItem } from './line-chart-item.interface';
|
||||
@ -71,6 +72,7 @@ export {
|
||||
Filter,
|
||||
FilterGroup,
|
||||
HistoricalDataItem,
|
||||
Holding,
|
||||
ImportResponse,
|
||||
InfoItem,
|
||||
InvestmentItem,
|
||||
|
@ -2,6 +2,7 @@ import { AssetClass, AssetSubClass, DataSource, Tag } from '@prisma/client';
|
||||
|
||||
import { Market, MarketAdvanced, MarketState } from '../types';
|
||||
import { Country } from './country.interface';
|
||||
import { Holding } from './holding.interface';
|
||||
import { Sector } from './sector.interface';
|
||||
|
||||
export interface PortfolioPosition {
|
||||
@ -20,6 +21,7 @@ export interface PortfolioPosition {
|
||||
grossPerformancePercent: number;
|
||||
grossPerformancePercentWithCurrencyEffect: number;
|
||||
grossPerformanceWithCurrencyEffect: number;
|
||||
holdings: Holding[];
|
||||
investment: number;
|
||||
marketChange?: number;
|
||||
marketChangePercent?: number;
|
||||
|
@ -4,7 +4,7 @@
|
||||
.holdings {
|
||||
overflow-x: auto;
|
||||
|
||||
.mat-mdc-table {
|
||||
.gf-table {
|
||||
th {
|
||||
::ng-deep {
|
||||
.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?
|
||||
figiComposite String?
|
||||
figiShareClass String?
|
||||
holdings Json? @default("[]")
|
||||
id String @id @default(uuid())
|
||||
isin String?
|
||||
name String?
|
||||
@ -189,6 +190,7 @@ model SymbolProfileOverrides {
|
||||
assetClass AssetClass?
|
||||
assetSubClass AssetSubClass?
|
||||
countries Json? @default("[]")
|
||||
holdings Json? @default("[]")
|
||||
name String?
|
||||
sectors Json? @default("[]")
|
||||
url String?
|
||||
|
Loading…
x
Reference in New Issue
Block a user