mirror of https://github.com/ghostfolio/ghostfolio
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
244 lines
7.4 KiB
244 lines
7.4 KiB
import type { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
|
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
|
import { prettifySymbol } from '@ghostfolio/common/helper';
|
|
import {
|
|
PortfolioPosition,
|
|
PublicPortfolioResponse
|
|
} from '@ghostfolio/common/interfaces';
|
|
import { Market } from '@ghostfolio/common/types';
|
|
import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table/activities-table.component';
|
|
import { GfHoldingsTableComponent } from '@ghostfolio/ui/holdings-table/holdings-table.component';
|
|
import { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.component';
|
|
import { GfValueComponent } from '@ghostfolio/ui/value';
|
|
import { GfWorldMapChartComponent } from '@ghostfolio/ui/world-map-chart';
|
|
|
|
import { CommonModule } from '@angular/common';
|
|
import {
|
|
ChangeDetectorRef,
|
|
Component,
|
|
CUSTOM_ELEMENTS_SCHEMA,
|
|
OnInit
|
|
} from '@angular/core';
|
|
import { MatButtonModule } from '@angular/material/button';
|
|
import { MatCardModule } from '@angular/material/card';
|
|
import { MatTableDataSource } from '@angular/material/table';
|
|
import { ActivatedRoute, Router } from '@angular/router';
|
|
import { AssetClass } from '@prisma/client';
|
|
import { StatusCodes } from 'http-status-codes';
|
|
import { isNumber } from 'lodash';
|
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
|
import { EMPTY, Subject } from 'rxjs';
|
|
import { catchError, takeUntil } from 'rxjs/operators';
|
|
|
|
import { DataService } from '../../services/data.service';
|
|
|
|
@Component({
|
|
host: { class: 'page' },
|
|
imports: [
|
|
CommonModule,
|
|
GfActivitiesTableComponent,
|
|
GfHoldingsTableComponent,
|
|
GfPortfolioProportionChartComponent,
|
|
GfValueComponent,
|
|
GfWorldMapChartComponent,
|
|
MatButtonModule,
|
|
MatCardModule
|
|
],
|
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
selector: 'gf-public-page',
|
|
styleUrls: ['./public-page.scss'],
|
|
templateUrl: './public-page.html'
|
|
})
|
|
export class GfPublicPageComponent implements OnInit {
|
|
public continents: {
|
|
[code: string]: { name: string; value: number };
|
|
};
|
|
public countries: {
|
|
[code: string]: { name: string; value: number };
|
|
};
|
|
public defaultAlias = $localize`someone`;
|
|
public deviceType: string;
|
|
public holdings: PublicPortfolioResponse['holdings'][string][];
|
|
public latestActivitiesDataSource: MatTableDataSource<Activity>;
|
|
public markets: {
|
|
[key in Market]: { id: Market; valueInPercentage: number };
|
|
};
|
|
public maxSafeInteger = Number.MAX_SAFE_INTEGER;
|
|
public positions: {
|
|
[symbol: string]: Pick<PortfolioPosition, 'currency' | 'name'> & {
|
|
value: number;
|
|
};
|
|
};
|
|
public publicPortfolioDetails: PublicPortfolioResponse;
|
|
public sectors: {
|
|
[name: string]: { name: string; value: number };
|
|
};
|
|
public symbols: {
|
|
[name: string]: { name: string; symbol: string; value: number };
|
|
};
|
|
public UNKNOWN_KEY = UNKNOWN_KEY;
|
|
|
|
private accessId: string;
|
|
private unsubscribeSubject = new Subject<void>();
|
|
|
|
public constructor(
|
|
private activatedRoute: ActivatedRoute,
|
|
private changeDetectorRef: ChangeDetectorRef,
|
|
private dataService: DataService,
|
|
private deviceService: DeviceDetectorService,
|
|
private router: Router
|
|
) {
|
|
this.activatedRoute.params.subscribe((params) => {
|
|
this.accessId = params['id'];
|
|
});
|
|
}
|
|
|
|
public ngOnInit() {
|
|
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
|
|
|
this.dataService
|
|
.fetchPublicPortfolio(this.accessId)
|
|
.pipe(
|
|
takeUntil(this.unsubscribeSubject),
|
|
catchError((error) => {
|
|
if (error.status === StatusCodes.NOT_FOUND) {
|
|
console.error(error);
|
|
this.router.navigate(['/']);
|
|
}
|
|
|
|
return EMPTY;
|
|
})
|
|
)
|
|
.subscribe((portfolioPublicDetails) => {
|
|
this.publicPortfolioDetails = portfolioPublicDetails;
|
|
|
|
this.latestActivitiesDataSource = new MatTableDataSource(
|
|
(this.publicPortfolioDetails.latestActivities ||
|
|
[]) as unknown as Activity[]
|
|
);
|
|
|
|
this.initializeAnalysisData();
|
|
|
|
this.changeDetectorRef.markForCheck();
|
|
});
|
|
}
|
|
|
|
public initializeAnalysisData() {
|
|
this.continents = {
|
|
[UNKNOWN_KEY]: {
|
|
name: UNKNOWN_KEY,
|
|
value: 0
|
|
}
|
|
};
|
|
this.countries = {
|
|
[UNKNOWN_KEY]: {
|
|
name: UNKNOWN_KEY,
|
|
value: 0
|
|
}
|
|
};
|
|
this.holdings = [];
|
|
this.markets = this.publicPortfolioDetails.markets;
|
|
this.positions = {};
|
|
this.sectors = {
|
|
[UNKNOWN_KEY]: {
|
|
name: UNKNOWN_KEY,
|
|
value: 0
|
|
}
|
|
};
|
|
this.symbols = {
|
|
[UNKNOWN_KEY]: {
|
|
name: UNKNOWN_KEY,
|
|
symbol: UNKNOWN_KEY,
|
|
value: 0
|
|
}
|
|
};
|
|
|
|
for (const [symbol, position] of Object.entries(
|
|
this.publicPortfolioDetails.holdings
|
|
)) {
|
|
this.holdings.push(position);
|
|
|
|
this.positions[symbol] = {
|
|
currency: position.currency,
|
|
name: position.name,
|
|
value: position.allocationInPercentage
|
|
};
|
|
|
|
if (position.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) {
|
|
const { code, continent, name, weight } = country;
|
|
|
|
if (this.continents[continent]?.value) {
|
|
this.continents[continent].value +=
|
|
weight * position.valueInBaseCurrency;
|
|
} else {
|
|
this.continents[continent] = {
|
|
name: continent,
|
|
value:
|
|
weight *
|
|
this.publicPortfolioDetails.holdings[symbol]
|
|
.valueInBaseCurrency
|
|
};
|
|
}
|
|
|
|
if (this.countries[code]?.value) {
|
|
this.countries[code].value +=
|
|
weight * position.valueInBaseCurrency;
|
|
} else {
|
|
this.countries[code] = {
|
|
name,
|
|
value:
|
|
weight *
|
|
this.publicPortfolioDetails.holdings[symbol]
|
|
.valueInBaseCurrency
|
|
};
|
|
}
|
|
}
|
|
} else {
|
|
this.continents[UNKNOWN_KEY].value +=
|
|
this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency;
|
|
|
|
this.countries[UNKNOWN_KEY].value +=
|
|
this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency;
|
|
}
|
|
|
|
if (position.sectors.length > 0) {
|
|
for (const sector of position.sectors) {
|
|
const { name, weight } = sector;
|
|
|
|
if (this.sectors[name]?.value) {
|
|
this.sectors[name].value += weight * position.valueInBaseCurrency;
|
|
} else {
|
|
this.sectors[name] = {
|
|
name,
|
|
value:
|
|
weight *
|
|
this.publicPortfolioDetails.holdings[symbol]
|
|
.valueInBaseCurrency
|
|
};
|
|
}
|
|
}
|
|
} else {
|
|
this.sectors[UNKNOWN_KEY].value +=
|
|
this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency;
|
|
}
|
|
}
|
|
|
|
this.symbols[prettifySymbol(symbol)] = {
|
|
name: position.name,
|
|
symbol: prettifySymbol(symbol),
|
|
value: isNumber(position.valueInBaseCurrency)
|
|
? position.valueInBaseCurrency
|
|
: position.valueInPercentage
|
|
};
|
|
}
|
|
}
|
|
|
|
public ngOnDestroy() {
|
|
this.unsubscribeSubject.next();
|
|
this.unsubscribeSubject.complete();
|
|
}
|
|
}
|
|
|