|
|
@ -2,7 +2,10 @@ import { GfBenchmarkComparatorComponent } from '@ghostfolio/client/components/be |
|
|
import { GfInvestmentChartComponent } from '@ghostfolio/client/components/investment-chart/investment-chart.component'; |
|
|
import { GfInvestmentChartComponent } from '@ghostfolio/client/components/investment-chart/investment-chart.component'; |
|
|
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; |
|
|
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; |
|
|
import { UserService } from '@ghostfolio/client/services/user/user.service'; |
|
|
import { UserService } from '@ghostfolio/client/services/user/user.service'; |
|
|
import { NUMERICAL_PRECISION_THRESHOLD_6_FIGURES } from '@ghostfolio/common/config'; |
|
|
import { |
|
|
|
|
|
DEFAULT_DATE_RANGE, |
|
|
|
|
|
NUMERICAL_PRECISION_THRESHOLD_6_FIGURES |
|
|
|
|
|
} from '@ghostfolio/common/config'; |
|
|
import { |
|
|
import { |
|
|
HistoricalDataItem, |
|
|
HistoricalDataItem, |
|
|
InvestmentItem, |
|
|
InvestmentItem, |
|
|
@ -24,9 +27,12 @@ import { Clipboard } from '@angular/cdk/clipboard'; |
|
|
import { |
|
|
import { |
|
|
ChangeDetectorRef, |
|
|
ChangeDetectorRef, |
|
|
Component, |
|
|
Component, |
|
|
|
|
|
computed, |
|
|
DestroyRef, |
|
|
DestroyRef, |
|
|
|
|
|
inject, |
|
|
OnInit, |
|
|
OnInit, |
|
|
ViewChild |
|
|
signal, |
|
|
|
|
|
viewChild |
|
|
} from '@angular/core'; |
|
|
} from '@angular/core'; |
|
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
|
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
|
|
import { MatButtonModule } from '@angular/material/button'; |
|
|
import { MatButtonModule } from '@angular/material/button'; |
|
|
@ -64,53 +70,57 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; |
|
|
templateUrl: './analysis-page.html' |
|
|
templateUrl: './analysis-page.html' |
|
|
}) |
|
|
}) |
|
|
export class GfAnalysisPageComponent implements OnInit { |
|
|
export class GfAnalysisPageComponent implements OnInit { |
|
|
@ViewChild(MatMenuTrigger) actionsMenuButton!: MatMenuTrigger; |
|
|
protected benchmark?: Partial<SymbolProfile>; |
|
|
|
|
|
protected benchmarkDataItems: HistoricalDataItem[] = []; |
|
|
public benchmark: Partial<SymbolProfile>; |
|
|
protected readonly benchmarks: Partial<SymbolProfile>[]; |
|
|
public benchmarkDataItems: HistoricalDataItem[] = []; |
|
|
protected bottom3: PortfolioPosition[]; |
|
|
public benchmarks: Partial<SymbolProfile>[]; |
|
|
protected dividendsByGroup: InvestmentItem[]; |
|
|
public bottom3: PortfolioPosition[]; |
|
|
protected readonly dividendTimelineDataLabel = $localize`Dividend`; |
|
|
public deviceType: string; |
|
|
protected hasImpersonationId: boolean; |
|
|
public dividendsByGroup: InvestmentItem[]; |
|
|
protected hasPermissionToReadAiPrompt: boolean; |
|
|
public dividendTimelineDataLabel = $localize`Dividend`; |
|
|
protected investments: InvestmentItem[]; |
|
|
public firstOrderDate: Date; |
|
|
protected readonly investmentTimelineDataLabel = $localize`Investment`; |
|
|
public hasImpersonationId: boolean; |
|
|
protected investmentsByGroup: InvestmentItem[]; |
|
|
public hasPermissionToReadAiPrompt: boolean; |
|
|
protected isLoadingAnalysisPrompt: boolean; |
|
|
public investments: InvestmentItem[]; |
|
|
protected isLoadingBenchmarkComparator: boolean; |
|
|
public investmentTimelineDataLabel = $localize`Investment`; |
|
|
protected isLoadingDividendTimelineChart: boolean; |
|
|
public investmentsByGroup: InvestmentItem[]; |
|
|
protected isLoadingInvestmentChart: boolean; |
|
|
public isLoadingAnalysisPrompt: boolean; |
|
|
protected isLoadingInvestmentTimelineChart: boolean; |
|
|
public isLoadingBenchmarkComparator: boolean; |
|
|
protected isLoadingPortfolioPrompt: boolean; |
|
|
public isLoadingDividendTimelineChart: boolean; |
|
|
protected readonly mode = signal<GroupBy>('month'); |
|
|
public isLoadingInvestmentChart: boolean; |
|
|
protected readonly modeOptions: ToggleOption[] = [ |
|
|
public isLoadingInvestmentTimelineChart: boolean; |
|
|
|
|
|
public isLoadingPortfolioPrompt: boolean; |
|
|
|
|
|
public mode: GroupBy = 'month'; |
|
|
|
|
|
public modeOptions: ToggleOption[] = [ |
|
|
|
|
|
{ label: $localize`Monthly`, value: 'month' }, |
|
|
{ label: $localize`Monthly`, value: 'month' }, |
|
|
{ label: $localize`Yearly`, value: 'year' } |
|
|
{ label: $localize`Yearly`, value: 'year' } |
|
|
]; |
|
|
]; |
|
|
public performance: PortfolioPerformance; |
|
|
protected performance: PortfolioPerformance; |
|
|
public performanceDataItems: HistoricalDataItem[]; |
|
|
protected performanceDataItems: HistoricalDataItem[]; |
|
|
public performanceDataItemsInPercentage: HistoricalDataItem[]; |
|
|
protected performanceDataItemsInPercentage: HistoricalDataItem[]; |
|
|
public portfolioEvolutionDataLabel = $localize`Investment`; |
|
|
protected readonly portfolioEvolutionDataLabel = $localize`Investment`; |
|
|
public precision = 2; |
|
|
protected precision = 2; |
|
|
public streaks: PortfolioInvestmentsResponse['streaks']; |
|
|
protected streaks: PortfolioInvestmentsResponse['streaks']; |
|
|
public top3: PortfolioPosition[]; |
|
|
protected top3: PortfolioPosition[]; |
|
|
public unitCurrentStreak: string; |
|
|
protected unitCurrentStreak: string; |
|
|
public unitLongestStreak: string; |
|
|
protected unitLongestStreak: string; |
|
|
public user: User; |
|
|
protected user: User; |
|
|
|
|
|
|
|
|
public constructor( |
|
|
private readonly actionsMenuButton = viewChild.required(MatMenuTrigger); |
|
|
private changeDetectorRef: ChangeDetectorRef, |
|
|
private readonly deviceType = computed( |
|
|
private clipboard: Clipboard, |
|
|
() => this.deviceDetectorService.deviceInfo().deviceType |
|
|
private dataService: DataService, |
|
|
); |
|
|
private destroyRef: DestroyRef, |
|
|
private firstOrderDate: Date; |
|
|
private deviceDetectorService: DeviceDetectorService, |
|
|
|
|
|
private impersonationStorageService: ImpersonationStorageService, |
|
|
private readonly changeDetectorRef = inject(ChangeDetectorRef); |
|
|
private snackBar: MatSnackBar, |
|
|
private readonly clipboard = inject(Clipboard); |
|
|
private userService: UserService |
|
|
private readonly dataService = inject(DataService); |
|
|
) { |
|
|
private readonly destroyRef = inject(DestroyRef); |
|
|
|
|
|
private readonly deviceDetectorService = inject(DeviceDetectorService); |
|
|
|
|
|
private readonly impersonationStorageService = inject( |
|
|
|
|
|
ImpersonationStorageService |
|
|
|
|
|
); |
|
|
|
|
|
private readonly snackBar = inject(MatSnackBar); |
|
|
|
|
|
private readonly userService = inject(UserService); |
|
|
|
|
|
|
|
|
|
|
|
public constructor() { |
|
|
const { benchmarks } = this.dataService.fetchInfo(); |
|
|
const { benchmarks } = this.dataService.fetchInfo(); |
|
|
this.benchmarks = benchmarks; |
|
|
this.benchmarks = benchmarks; |
|
|
|
|
|
|
|
|
@ -123,14 +133,16 @@ export class GfAnalysisPageComponent implements OnInit { |
|
|
? undefined |
|
|
? undefined |
|
|
: this.user?.settings?.savingsRate; |
|
|
: this.user?.settings?.savingsRate; |
|
|
|
|
|
|
|
|
return this.mode === 'year' |
|
|
if (savingsRatePerMonth === undefined) { |
|
|
|
|
|
return undefined; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return this.mode() === 'year' |
|
|
? savingsRatePerMonth * 12 |
|
|
? savingsRatePerMonth * 12 |
|
|
: savingsRatePerMonth; |
|
|
: savingsRatePerMonth; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
public ngOnInit() { |
|
|
public ngOnInit() { |
|
|
this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType; |
|
|
|
|
|
|
|
|
|
|
|
this.impersonationStorageService |
|
|
this.impersonationStorageService |
|
|
.onChangeHasImpersonation() |
|
|
.onChangeHasImpersonation() |
|
|
.pipe(takeUntilDestroyed(this.destroyRef)) |
|
|
.pipe(takeUntilDestroyed(this.destroyRef)) |
|
|
@ -158,7 +170,7 @@ export class GfAnalysisPageComponent implements OnInit { |
|
|
}); |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
public onChangeBenchmark(symbolProfileId: string) { |
|
|
protected onChangeBenchmark(symbolProfileId: string) { |
|
|
this.dataService |
|
|
this.dataService |
|
|
.putUserSetting({ benchmark: symbolProfileId }) |
|
|
.putUserSetting({ benchmark: symbolProfileId }) |
|
|
.pipe(takeUntilDestroyed(this.destroyRef)) |
|
|
.pipe(takeUntilDestroyed(this.destroyRef)) |
|
|
@ -174,12 +186,12 @@ export class GfAnalysisPageComponent implements OnInit { |
|
|
}); |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
public onChangeGroupBy(aMode: GroupBy) { |
|
|
protected onChangeGroupBy(aMode: GroupBy) { |
|
|
this.mode = aMode; |
|
|
this.mode.set(aMode); |
|
|
this.fetchDividendsAndInvestments(); |
|
|
this.fetchDividendsAndInvestments(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
public onCopyPromptToClipboard(mode: AiPromptMode) { |
|
|
protected onCopyPromptToClipboard(mode: AiPromptMode) { |
|
|
if (mode === 'analysis') { |
|
|
if (mode === 'analysis') { |
|
|
this.isLoadingAnalysisPrompt = true; |
|
|
this.isLoadingAnalysisPrompt = true; |
|
|
} else if (mode === 'portfolio') { |
|
|
} else if (mode === 'portfolio') { |
|
|
@ -210,7 +222,7 @@ export class GfAnalysisPageComponent implements OnInit { |
|
|
window.open('https://duck.ai', '_blank'); |
|
|
window.open('https://duck.ai', '_blank'); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
this.actionsMenuButton.closeMenu(); |
|
|
this.actionsMenuButton().closeMenu(); |
|
|
|
|
|
|
|
|
if (mode === 'analysis') { |
|
|
if (mode === 'analysis') { |
|
|
this.isLoadingAnalysisPrompt = false; |
|
|
this.isLoadingAnalysisPrompt = false; |
|
|
@ -227,8 +239,8 @@ export class GfAnalysisPageComponent implements OnInit { |
|
|
this.dataService |
|
|
this.dataService |
|
|
.fetchDividends({ |
|
|
.fetchDividends({ |
|
|
filters: this.userService.getFilters(), |
|
|
filters: this.userService.getFilters(), |
|
|
groupBy: this.mode, |
|
|
groupBy: this.mode(), |
|
|
range: this.user?.settings?.dateRange |
|
|
range: this.user?.settings?.dateRange ?? DEFAULT_DATE_RANGE |
|
|
}) |
|
|
}) |
|
|
.pipe(takeUntilDestroyed(this.destroyRef)) |
|
|
.pipe(takeUntilDestroyed(this.destroyRef)) |
|
|
.subscribe(({ dividends }) => { |
|
|
.subscribe(({ dividends }) => { |
|
|
@ -242,15 +254,15 @@ export class GfAnalysisPageComponent implements OnInit { |
|
|
this.dataService |
|
|
this.dataService |
|
|
.fetchInvestments({ |
|
|
.fetchInvestments({ |
|
|
filters: this.userService.getFilters(), |
|
|
filters: this.userService.getFilters(), |
|
|
groupBy: this.mode, |
|
|
groupBy: this.mode(), |
|
|
range: this.user?.settings?.dateRange |
|
|
range: this.user?.settings?.dateRange ?? DEFAULT_DATE_RANGE |
|
|
}) |
|
|
}) |
|
|
.pipe(takeUntilDestroyed(this.destroyRef)) |
|
|
.pipe(takeUntilDestroyed(this.destroyRef)) |
|
|
.subscribe(({ investments, streaks }) => { |
|
|
.subscribe(({ investments, streaks }) => { |
|
|
this.investmentsByGroup = investments; |
|
|
this.investmentsByGroup = investments; |
|
|
this.streaks = streaks; |
|
|
this.streaks = streaks; |
|
|
this.unitCurrentStreak = |
|
|
this.unitCurrentStreak = |
|
|
this.mode === 'year' |
|
|
this.mode() === 'year' |
|
|
? this.streaks?.currentStreak === 1 |
|
|
? this.streaks?.currentStreak === 1 |
|
|
? translate('YEAR') |
|
|
? translate('YEAR') |
|
|
: translate('YEARS') |
|
|
: translate('YEARS') |
|
|
@ -258,7 +270,7 @@ export class GfAnalysisPageComponent implements OnInit { |
|
|
? translate('MONTH') |
|
|
? translate('MONTH') |
|
|
: translate('MONTHS'); |
|
|
: translate('MONTHS'); |
|
|
this.unitLongestStreak = |
|
|
this.unitLongestStreak = |
|
|
this.mode === 'year' |
|
|
this.mode() === 'year' |
|
|
? this.streaks?.longestStreak === 1 |
|
|
? this.streaks?.longestStreak === 1 |
|
|
? translate('YEAR') |
|
|
? translate('YEAR') |
|
|
: translate('YEARS') |
|
|
: translate('YEARS') |
|
|
@ -278,7 +290,7 @@ export class GfAnalysisPageComponent implements OnInit { |
|
|
this.dataService |
|
|
this.dataService |
|
|
.fetchPortfolioPerformance({ |
|
|
.fetchPortfolioPerformance({ |
|
|
filters: this.userService.getFilters(), |
|
|
filters: this.userService.getFilters(), |
|
|
range: this.user?.settings?.dateRange |
|
|
range: this.user?.settings?.dateRange ?? DEFAULT_DATE_RANGE |
|
|
}) |
|
|
}) |
|
|
.pipe(takeUntilDestroyed(this.destroyRef)) |
|
|
.pipe(takeUntilDestroyed(this.destroyRef)) |
|
|
.subscribe(({ chart, firstOrderDate, performance }) => { |
|
|
.subscribe(({ chart, firstOrderDate, performance }) => { |
|
|
@ -298,13 +310,16 @@ export class GfAnalysisPageComponent implements OnInit { |
|
|
valueInPercentage, |
|
|
valueInPercentage, |
|
|
valueWithCurrencyEffect |
|
|
valueWithCurrencyEffect |
|
|
} |
|
|
} |
|
|
] of chart.entries()) { |
|
|
] of (chart ?? []).entries()) { |
|
|
|
|
|
// Ignore first item where value is 0
|
|
|
if (index > 0 || this.user?.settings?.dateRange === 'max') { |
|
|
if (index > 0 || this.user?.settings?.dateRange === 'max') { |
|
|
// Ignore first item where value is 0
|
|
|
if (totalInvestmentValueWithCurrencyEffect !== undefined) { |
|
|
this.investments.push({ |
|
|
this.investments.push({ |
|
|
date, |
|
|
date, |
|
|
investment: totalInvestmentValueWithCurrencyEffect |
|
|
investment: totalInvestmentValueWithCurrencyEffect |
|
|
}); |
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
this.performanceDataItems.push({ |
|
|
this.performanceDataItems.push({ |
|
|
date, |
|
|
date, |
|
|
value: isNumber(valueWithCurrencyEffect) |
|
|
value: isNumber(valueWithCurrencyEffect) |
|
|
@ -320,7 +335,7 @@ export class GfAnalysisPageComponent implements OnInit { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if ( |
|
|
if ( |
|
|
this.deviceType === 'mobile' && |
|
|
this.deviceType() === 'mobile' && |
|
|
this.performance.currentValueInBaseCurrency >= |
|
|
this.performance.currentValueInBaseCurrency >= |
|
|
NUMERICAL_PRECISION_THRESHOLD_6_FIGURES |
|
|
NUMERICAL_PRECISION_THRESHOLD_6_FIGURES |
|
|
) { |
|
|
) { |
|
|
@ -387,7 +402,7 @@ export class GfAnalysisPageComponent implements OnInit { |
|
|
dataSource, |
|
|
dataSource, |
|
|
symbol, |
|
|
symbol, |
|
|
filters: this.userService.getFilters(), |
|
|
filters: this.userService.getFilters(), |
|
|
range: this.user?.settings?.dateRange, |
|
|
range: this.user?.settings?.dateRange ?? DEFAULT_DATE_RANGE, |
|
|
startDate: this.firstOrderDate |
|
|
startDate: this.firstOrderDate |
|
|
}) |
|
|
}) |
|
|
.pipe(takeUntilDestroyed(this.destroyRef)) |
|
|
.pipe(takeUntilDestroyed(this.destroyRef)) |
|
|
|