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.
247 lines
7.7 KiB
247 lines
7.7 KiB
import { FamilyOfficeDataService } from '@ghostfolio/client/services/family-office-data.service';
|
|
import type { IPartnershipPerformance } from '@ghostfolio/common/interfaces';
|
|
import { GfBenchmarkComparisonChartComponent } from '@ghostfolio/ui/benchmark-comparison-chart';
|
|
import { GfPerformanceMetricsComponent } from '@ghostfolio/ui/performance-metrics';
|
|
|
|
import { CommonModule } from '@angular/common';
|
|
import {
|
|
ChangeDetectionStrategy,
|
|
ChangeDetectorRef,
|
|
Component,
|
|
DestroyRef,
|
|
OnInit
|
|
} from '@angular/core';
|
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
import { FormsModule } from '@angular/forms';
|
|
import { MatButtonModule } from '@angular/material/button';
|
|
import { MatCardModule } from '@angular/material/card';
|
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
import { MatIconModule } from '@angular/material/icon';
|
|
import { MatSelectModule } from '@angular/material/select';
|
|
import { MatTableModule } from '@angular/material/table';
|
|
import { ActivatedRoute, RouterModule } from '@angular/router';
|
|
|
|
@Component({
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
imports: [
|
|
CommonModule,
|
|
FormsModule,
|
|
GfBenchmarkComparisonChartComponent,
|
|
GfPerformanceMetricsComponent,
|
|
MatButtonModule,
|
|
MatCardModule,
|
|
MatFormFieldModule,
|
|
MatIconModule,
|
|
MatSelectModule,
|
|
MatTableModule,
|
|
RouterModule
|
|
],
|
|
selector: 'gf-partnership-performance-page',
|
|
standalone: true,
|
|
styles: [
|
|
`
|
|
:host {
|
|
display: block;
|
|
padding: 1rem;
|
|
}
|
|
|
|
.page-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.controls {
|
|
display: flex;
|
|
gap: 1rem;
|
|
margin-bottom: 1.5rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.returns-table {
|
|
margin-top: 1.5rem;
|
|
}
|
|
|
|
.back-link {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
text-decoration: none;
|
|
color: rgba(var(--dark-primary-text), 0.6);
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.loading {
|
|
text-align: center;
|
|
padding: 3rem;
|
|
color: rgba(var(--dark-primary-text), 0.5);
|
|
}
|
|
`
|
|
],
|
|
template: `
|
|
<a class="back-link" [routerLink]="['/partnerships', partnershipId]">
|
|
<mat-icon>arrow_back</mat-icon>
|
|
Back to Partnership
|
|
</a>
|
|
|
|
<div class="page-header">
|
|
<h1>{{ performance?.partnershipName }} — Performance</h1>
|
|
</div>
|
|
|
|
<div class="controls">
|
|
<mat-form-field>
|
|
<mat-label>Periodicity</mat-label>
|
|
<mat-select
|
|
[(value)]="periodicity"
|
|
(selectionChange)="fetchPerformance()"
|
|
>
|
|
<mat-option value="MONTHLY">Monthly</mat-option>
|
|
<mat-option value="QUARTERLY">Quarterly</mat-option>
|
|
<mat-option value="YEARLY">Yearly</mat-option>
|
|
</mat-select>
|
|
</mat-form-field>
|
|
<mat-form-field>
|
|
<mat-label>Benchmarks</mat-label>
|
|
<mat-select
|
|
multiple
|
|
[(value)]="selectedBenchmarks"
|
|
(selectionChange)="fetchPerformance()"
|
|
>
|
|
<mat-option value="SP500">S&P 500</mat-option>
|
|
<mat-option value="US_AGG_BOND">US Agg Bond</mat-option>
|
|
<mat-option value="REAL_ESTATE">Real Estate</mat-option>
|
|
<mat-option value="CPI_PROXY">CPI (TIPS)</mat-option>
|
|
</mat-select>
|
|
</mat-form-field>
|
|
</div>
|
|
|
|
@if (performance) {
|
|
<gf-performance-metrics
|
|
[dpi]="performance.metrics.dpi"
|
|
[irr]="performance.metrics.irr"
|
|
[periodReturns]="periodReturns"
|
|
[rvpi]="performance.metrics.rvpi"
|
|
[tvpi]="performance.metrics.tvpi"
|
|
/>
|
|
|
|
@if (performance.periods && performance.periods.length > 0) {
|
|
<div class="returns-table">
|
|
<h3>Period Returns Detail</h3>
|
|
<table mat-table [dataSource]="performance.periods">
|
|
<ng-container matColumnDef="periodStart">
|
|
<th *matHeaderCellDef mat-header-cell>Period Start</th>
|
|
<td *matCellDef="let p" mat-cell>
|
|
{{ p.periodStart | date: 'mediumDate' }}
|
|
</td>
|
|
</ng-container>
|
|
<ng-container matColumnDef="periodEnd">
|
|
<th *matHeaderCellDef mat-header-cell>Period End</th>
|
|
<td *matCellDef="let p" mat-cell>
|
|
{{ p.periodEnd | date: 'mediumDate' }}
|
|
</td>
|
|
</ng-container>
|
|
<ng-container matColumnDef="startNav">
|
|
<th *matHeaderCellDef mat-header-cell>Start NAV</th>
|
|
<td *matCellDef="let p" mat-cell>
|
|
{{ p.startNav | currency: 'USD' : 'symbol' : '1.0-0' }}
|
|
</td>
|
|
</ng-container>
|
|
<ng-container matColumnDef="endNav">
|
|
<th *matHeaderCellDef mat-header-cell>End NAV</th>
|
|
<td *matCellDef="let p" mat-cell>
|
|
{{ p.endNav | currency: 'USD' : 'symbol' : '1.0-0' }}
|
|
</td>
|
|
</ng-container>
|
|
<ng-container matColumnDef="returnPercent">
|
|
<th *matHeaderCellDef mat-header-cell>Return</th>
|
|
<td
|
|
*matCellDef="let p"
|
|
mat-cell
|
|
[style.color]="p.returnPercent >= 0 ? '#2e7d32' : '#c62828'"
|
|
>
|
|
{{ p.returnPercent | percent: '1.2-2' }}
|
|
</td>
|
|
</ng-container>
|
|
<tr *matHeaderRowDef="returnsColumns" mat-header-row></tr>
|
|
<tr *matRowDef="let row; columns: returnsColumns" mat-row></tr>
|
|
</table>
|
|
</div>
|
|
}
|
|
|
|
@if (
|
|
performance.benchmarkComparisons &&
|
|
performance.benchmarkComparisons.length > 0
|
|
) {
|
|
<div style="margin-top: 1.5rem;">
|
|
<h3>Benchmark Comparison</h3>
|
|
<gf-benchmark-comparison-chart
|
|
[benchmarks]="performance.benchmarkComparisons"
|
|
[partnershipName]="performance.partnershipName"
|
|
[partnershipReturn]="overallReturn"
|
|
/>
|
|
</div>
|
|
}
|
|
} @else {
|
|
<div class="loading">Loading performance data...</div>
|
|
}
|
|
`
|
|
})
|
|
export class PartnershipPerformancePageComponent implements OnInit {
|
|
public overallReturn: number = 0;
|
|
public partnershipId: string = '';
|
|
public performance: IPartnershipPerformance | null = null;
|
|
public periodicity: string = 'QUARTERLY';
|
|
public periodReturns: {
|
|
endDate: string;
|
|
return: number;
|
|
startDate: string;
|
|
}[] = [];
|
|
public returnsColumns = [
|
|
'periodStart',
|
|
'periodEnd',
|
|
'startNav',
|
|
'endNav',
|
|
'returnPercent'
|
|
];
|
|
public selectedBenchmarks: string[] = [];
|
|
|
|
public constructor(
|
|
private readonly activatedRoute: ActivatedRoute,
|
|
private readonly changeDetectorRef: ChangeDetectorRef,
|
|
private readonly destroyRef: DestroyRef,
|
|
private readonly familyOfficeDataService: FamilyOfficeDataService
|
|
) {}
|
|
|
|
public ngOnInit(): void {
|
|
this.partnershipId = this.activatedRoute.snapshot.paramMap.get('id') || '';
|
|
this.fetchPerformance();
|
|
}
|
|
|
|
public fetchPerformance(): void {
|
|
const params: any = {
|
|
periodicity: this.periodicity
|
|
};
|
|
|
|
if (this.selectedBenchmarks.length > 0) {
|
|
params.benchmarks = this.selectedBenchmarks.join(',');
|
|
}
|
|
|
|
this.familyOfficeDataService
|
|
.fetchPartnershipPerformance(this.partnershipId, params)
|
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
.subscribe((perf) => {
|
|
this.performance = perf;
|
|
this.periodReturns = (perf.periods || []).map((p: any) => ({
|
|
endDate: p.periodEnd,
|
|
return: p.returnPercent,
|
|
startDate: p.periodStart
|
|
}));
|
|
this.overallReturn = this.periodReturns.reduce(
|
|
(acc, p) => (1 + acc) * (1 + p.return) - 1,
|
|
0
|
|
);
|
|
this.changeDetectorRef.markForCheck();
|
|
});
|
|
}
|
|
}
|
|
|