Merge branch 'main' of github.com:ghostfolio/ghostfolio
All checks were successful
Docker image CD / build_and_push (push) Successful in 29m55s

This commit is contained in:
sudacode 2025-02-18 13:05:33 -08:00
commit 2413244bf1
16 changed files with 444 additions and 40 deletions

View File

@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Changed
- Reloaded the available tags after creating a custom tag in the holding detail dialog (experimental)
- Migrated the `@ghostfolio/client` components to control flow
- Migrated the `@ghostfolio/ui` components to control flow
### Fixed
- Added missing assets in _Storybook_ setup
@ -15,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Extended the tooltip in the chart of the holdings tab on the home page by the allocation, change and performance
- Added a new static portfolio analysis rule: _Regional Market Cluster Risk_ (Asia-Pacific Markets)
- Added a new static portfolio analysis rule: _Regional Market Cluster Risk_ (Japan)
- Added support to create custom tags in the holding detail dialog (experimental)

View File

@ -3,7 +3,6 @@ import { DataService } from '@ghostfolio/client/services/data.service';
import { PROPERTY_API_KEY_GHOSTFOLIO } from '@ghostfolio/common/config';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { CommonModule } from '@angular/common';
import { Component, Inject } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import {
@ -18,7 +17,6 @@ import { GhostfolioPremiumApiDialogParams } from './interfaces/interfaces';
@Component({
imports: [
CommonModule,
GfDialogFooterModule,
GfDialogHeaderModule,
GfPremiumIndicatorComponent,

View File

@ -1,4 +1,3 @@
import { CommonModule } from '@angular/common';
import {
CUSTOM_ELEMENTS_SCHEMA,
ChangeDetectionStrategy,
@ -10,7 +9,6 @@ import { DataSource } from '@prisma/client';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-asset-profile-icon',
styleUrls: ['./asset-profile-icon.component.scss'],

View File

@ -175,6 +175,9 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
]
});
}),
switchMap(() => {
return this.userService.get(true);
}),
takeUntil(this.unsubscribeSubject)
)
.subscribe();

View File

@ -1,6 +1,5 @@
import { XRayRulesSettings } from '@ghostfolio/common/interfaces';
import { CommonModule } from '@angular/common';
import { Component, Inject } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
@ -14,13 +13,7 @@ import { MatSliderModule } from '@angular/material/slider';
import { IRuleSettingsDialogParams } from './interfaces/interfaces';
@Component({
imports: [
CommonModule,
FormsModule,
MatButtonModule,
MatDialogModule,
MatSliderModule
],
imports: [FormsModule, MatButtonModule, MatDialogModule, MatSliderModule],
selector: 'gf-rule-settings-dialog',
styleUrls: ['./rule-settings-dialog.scss'],
templateUrl: './rule-settings-dialog.html'

View File

@ -1,4 +1,3 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
@ -6,7 +5,7 @@ import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { IAlertDialogParams } from './interfaces/interfaces';
@Component({
imports: [CommonModule, MatButtonModule, MatDialogModule],
imports: [MatButtonModule, MatDialogModule],
selector: 'gf-alert-dialog',
styleUrls: ['./alert-dialog.scss'],
templateUrl: './alert-dialog.html'

View File

@ -1,4 +1,3 @@
import { CommonModule } from '@angular/common';
import { Component, HostListener } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
@ -7,7 +6,7 @@ import { ConfirmationDialogType } from './confirmation-dialog.type';
import { IConfirmDialogParams } from './interfaces/interfaces';
@Component({
imports: [CommonModule, MatButtonModule, MatDialogModule],
imports: [MatButtonModule, MatDialogModule],
selector: 'gf-confirmation-dialog',
styleUrls: ['./confirmation-dialog.scss'],
templateUrl: './confirmation-dialog.html'

View File

@ -1,4 +1,3 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
@ -8,7 +7,6 @@ import { MatInputModule } from '@angular/material/input';
@Component({
imports: [
CommonModule,
FormsModule,
MatButtonModule,
MatDialogModule,

View File

@ -3,14 +3,13 @@ import { Product } from '@ghostfolio/common/interfaces';
import { personalFinanceTools } from '@ghostfolio/common/personal-finance-tools';
import { translate } from '@ghostfolio/ui/i18n';
import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { ActivatedRoute, RouterModule } from '@angular/router';
@Component({
host: { class: 'page' },
imports: [CommonModule, MatButtonModule, RouterModule],
imports: [MatButtonModule, RouterModule],
selector: 'gf-product-page',
styleUrls: ['./product-page.scss'],
templateUrl: './product-page.html'

View File

@ -8,7 +8,6 @@ import {
} from '@ghostfolio/common/interfaces';
import { GfLineChartComponent } from '@ghostfolio/ui/line-chart';
import { CommonModule } from '@angular/common';
import {
CUSTOM_ELEMENTS_SCHEMA,
ChangeDetectionStrategy,
@ -33,7 +32,6 @@ import { BenchmarkDetailDialogParams } from './interfaces/interfaces';
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'd-flex flex-column h-100' },
imports: [
CommonModule,
GfDialogFooterModule,
GfDialogHeaderModule,
GfLineChartComponent,

View File

@ -1,9 +1,16 @@
<small class="text-muted">
<ng-container i18n>Market data provided by</ng-container>&nbsp;<ng-container
*ngFor="let dataProviderInfo of dataProviderInfos; let last = last"
><a target="_blank" [href]="dataProviderInfo.url">{{
<ng-container i18n>Market data provided by</ng-container>&nbsp;
@for (
dataProviderInfo of dataProviderInfos;
track dataProviderInfo;
let last = $last
) {
<a target="_blank" [href]="dataProviderInfo.url">{{
dataProviderInfo.name
}}</a
><ng-container *ngIf="!last">, </ng-container></ng-container
>.
}}</a>
@if (!last) {
,&nbsp;
}
}
.
</small>

View File

@ -1,6 +1,5 @@
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
import { CommonModule } from '@angular/common';
import {
CUSTOM_ELEMENTS_SCHEMA,
ChangeDetectionStrategy,
@ -10,7 +9,6 @@ import {
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-data-provider-credits',
styleUrls: ['./data-provider-credits.component.scss'],

View File

@ -1,7 +1,6 @@
import { AdminService } from '@ghostfolio/client/services/admin.service';
import { DataService } from '@ghostfolio/client/services/data.service';
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
@ -29,7 +28,6 @@ import { HistoricalMarketDataEditorDialogParams } from './interfaces/interfaces'
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'h-100' },
imports: [
CommonModule,
FormsModule,
MatButtonModule,
MatDatepickerModule,

View File

@ -0,0 +1,392 @@
import { CommonModule } from '@angular/common';
import '@angular/localize/init';
import { moduleMetadata } from '@storybook/angular';
import type { Meta, StoryObj } from '@storybook/angular';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { GfTreemapChartComponent } from './treemap-chart.component';
export default {
title: 'Treemap Chart',
component: GfTreemapChartComponent,
decorators: [
moduleMetadata({
imports: [CommonModule, NgxSkeletonLoaderModule]
})
],
argTypes: {
colorScheme: {
control: {
type: 'select'
},
options: ['DARK', 'LIGHT']
},
cursor: {
control: {
type: 'select'
},
options: ['', 'pointer']
}
}
} as Meta<GfTreemapChartComponent>;
type Story = StoryObj<GfTreemapChartComponent>;
export const Default: Story = {
args: {
baseCurrency: 'USD',
colorScheme: 'LIGHT',
cursor: undefined,
dateRange: 'mtd',
holdings: [
{
allocationInPercentage: 0.042990776363386086,
assetClass: 'EQUITY',
assetSubClass: 'STOCK',
countries: [],
currency: 'USD',
dataSource: 'YAHOO',
dateOfFirstActivity: new Date('2021-12-01T00:00:00.000Z'),
dividend: 0,
grossPerformance: 3856,
grossPerformancePercent: 0.46047289228564603,
grossPerformancePercentWithCurrencyEffect: 0.46047289228564603,
grossPerformanceWithCurrencyEffect: 3856,
holdings: [],
investment: 8374,
marketPrice: 244.6,
name: 'Apple Inc',
netPerformance: 3855,
netPerformancePercent: 0.460353475041796,
netPerformancePercentWithCurrencyEffect: 0.036440677966101696,
netPerformanceWithCurrencyEffect: 430,
quantity: 50,
sectors: [],
symbol: 'AAPL',
tags: [],
transactionCount: 1,
url: 'https://www.apple.com',
valueInBaseCurrency: 12230
},
{
allocationInPercentage: 0.02377401948293552,
assetClass: 'EQUITY',
assetSubClass: 'STOCK',
countries: [],
currency: 'EUR',
dataSource: 'YAHOO',
dateOfFirstActivity: new Date('2021-04-23T00:00:00.000Z'),
dividend: 192,
grossPerformance: 2226.700251889169,
grossPerformancePercent: 0.49083842309827874,
grossPerformancePercentWithCurrencyEffect: 0.29306136948826367,
grossPerformanceWithCurrencyEffect: 1532.8272791336772,
holdings: [],
investment: 4536.523929471033,
marketPrice: 322.2,
name: 'Allianz SE',
netPerformance: 2222.2921914357685,
netPerformancePercent: 0.48986674069961134,
netPerformancePercentWithCurrencyEffect: 0.034489367670592026,
netPerformanceWithCurrencyEffect: 225.48257403052068,
quantity: 20,
sectors: [],
symbol: 'ALV.DE',
tags: [],
transactionCount: 2,
url: 'https://www.allianz.com',
valueInBaseCurrency: 6763.224181360202
},
{
allocationInPercentage: 0.08038536990007467,
assetClass: 'EQUITY',
assetSubClass: 'STOCK',
countries: [],
currency: 'USD',
dataSource: 'YAHOO',
dateOfFirstActivity: new Date('2018-10-01T00:00:00.000Z'),
dividend: 0,
grossPerformance: 12758.05,
grossPerformancePercent: 1.2619300787837724,
grossPerformancePercentWithCurrencyEffect: 1.2619300787837724,
grossPerformanceWithCurrencyEffect: 12758.05,
holdings: [],
investment: 10109.95,
marketPrice: 228.68,
name: 'Amazon.com, Inc.',
netPerformance: 12677.26,
netPerformancePercent: 1.253938941339967,
netPerformancePercentWithCurrencyEffect: -0.037866008722316276,
netPerformanceWithCurrencyEffect: -899.99926757812,
quantity: 100,
sectors: [],
symbol: 'AMZN',
tags: [],
transactionCount: 1,
url: 'https://www.aboutamazon.com',
valueInBaseCurrency: 22868
},
{
allocationInPercentage: 0.19216416482928922,
assetClass: 'LIQUIDITY',
assetSubClass: 'CRYPTOCURRENCY',
countries: [],
currency: 'USD',
dataSource: 'COINGECKO',
dateOfFirstActivity: new Date('2017-08-16T00:00:00.000Z'),
dividend: 0,
grossPerformance: 52666.7898248,
grossPerformancePercent: 26.333394912400003,
grossPerformancePercentWithCurrencyEffect: 26.333394912400003,
grossPerformanceWithCurrencyEffect: 52666.7898248,
holdings: [],
investment: 1999.9999999999998,
marketPrice: 97364,
name: 'Bitcoin',
netPerformance: 52636.8898248,
netPerformancePercent: 26.3184449124,
netPerformancePercentWithCurrencyEffect: -0.04760906442310894,
netPerformanceWithCurrencyEffect: -2732.737808972287,
quantity: 0.5614682,
sectors: [],
symbol: 'bitcoin',
tags: [],
transactionCount: 1,
url: null,
valueInBaseCurrency: 54666.7898248
},
{
allocationInPercentage: 0.007378652850073097,
assetClass: 'FIXED_INCOME',
assetSubClass: 'BOND',
countries: [],
currency: 'EUR',
dataSource: 'MANUAL',
dateOfFirstActivity: new Date('2021-02-01T00:00:00.000Z'),
dividend: 11.45,
grossPerformance: 0,
grossPerformancePercent: 0,
grossPerformancePercentWithCurrencyEffect: -0.1247202380342517,
grossPerformanceWithCurrencyEffect: -258.2576430160448,
holdings: [],
investment: 2099.0764063811926,
marketPrice: 1,
name: 'Bondora Go & Grow',
netPerformance: 0,
netPerformancePercent: 0,
netPerformancePercentWithCurrencyEffect: 0.009445843828715519,
netPerformanceWithCurrencyEffect: 19.6420125363184,
quantity: 2000,
sectors: [],
symbol: 'BONDORA_GO_AND_GROW',
tags: [],
transactionCount: 5,
url: null,
valueInBaseCurrency: 2099.0764063811926
},
{
allocationInPercentage: 0.07787531695543741,
assetClass: 'EQUITY',
assetSubClass: 'ETF',
countries: [],
currency: 'CHF',
dataSource: 'MANUAL',
dateOfFirstActivity: new Date('2021-04-01T00:00:00.000Z'),
dividend: 0,
grossPerformance: 4550.843985045582,
grossPerformancePercent: 0.3631417324494093,
grossPerformancePercentWithCurrencyEffect: 0.42037247857285137,
grossPerformanceWithCurrencyEffect: 5107.057936556927,
holdings: [],
investment: 17603.097090932337,
marketPrice: 188.22,
name: 'frankly Extreme 95 Index',
netPerformance: 4550.843985045582,
netPerformancePercent: 0.3631417324494093,
netPerformancePercentWithCurrencyEffect: 0.026190604904358043,
netPerformanceWithCurrencyEffect: 565.4165171873152,
quantity: 105.87328656807,
sectors: [],
symbol: 'FRANKLY95P',
tags: [],
transactionCount: 6,
url: 'https://www.frankly.ch',
valueInBaseCurrency: 22153.941075977917
},
{
allocationInPercentage: 0.04307127421937313,
assetClass: 'EQUITY',
assetSubClass: 'STOCK',
countries: [],
currency: 'USD',
dataSource: 'YAHOO',
dateOfFirstActivity: new Date('2023-01-03T00:00:00.000Z'),
dividend: 0,
grossPerformance: 5065.5,
grossPerformancePercent: 0.7047750229568411,
grossPerformancePercentWithCurrencyEffect: 0.7047750229568411,
grossPerformanceWithCurrencyEffect: 5065.5,
holdings: [],
investment: 7187.4,
marketPrice: 408.43,
name: 'Microsoft Corporation',
netPerformance: 5065.5,
netPerformancePercent: 0.7047750229568411,
netPerformancePercentWithCurrencyEffect: -0.015973588391056275,
netPerformanceWithCurrencyEffect: -198.899926757814,
quantity: 30,
sectors: [],
symbol: 'MSFT',
tags: [],
transactionCount: 1,
url: 'https://www.microsoft.com',
valueInBaseCurrency: 12252.9
},
{
allocationInPercentage: 0.18762679306394897,
assetClass: 'EQUITY',
assetSubClass: 'STOCK',
countries: [],
currency: 'USD',
dataSource: 'YAHOO',
dateOfFirstActivity: new Date('2017-01-03T00:00:00.000Z'),
dividend: 0,
grossPerformance: 51227.500000005,
grossPerformancePercent: 23.843379101756675,
grossPerformancePercentWithCurrencyEffect: 23.843379101756675,
grossPerformanceWithCurrencyEffect: 51227.500000005,
holdings: [],
investment: 2148.499999995,
marketPrice: 355.84,
name: 'Tesla, Inc.',
netPerformance: 51197.500000005,
netPerformancePercent: 23.829415871596066,
netPerformancePercentWithCurrencyEffect: -0.12051410125545206,
netPerformanceWithCurrencyEffect: -7314.00091552734,
quantity: 150,
sectors: [],
symbol: 'TSLA',
tags: [],
transactionCount: 1,
url: 'https://www.tesla.com',
valueInBaseCurrency: 53376
},
{
allocationInPercentage: 0.053051250766657634,
assetClass: 'EQUITY',
assetSubClass: 'ETF',
countries: [],
currency: 'USD',
dataSource: 'YAHOO',
dateOfFirstActivity: new Date('2019-03-01T00:00:00.000Z'),
dividend: 0,
grossPerformance: 6845.8,
grossPerformancePercent: 1.0164758094605268,
grossPerformancePercentWithCurrencyEffect: 1.0164758094605268,
grossPerformanceWithCurrencyEffect: 6845.8,
holdings: [],
investment: 8246.2,
marketPrice: 301.84,
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
netPerformance: 6746.3,
netPerformancePercent: 1.0017018833976383,
netPerformancePercentWithCurrencyEffect: 0.01085061564051406,
netPerformanceWithCurrencyEffect: 161.99969482422,
quantity: 50,
sectors: [],
symbol: 'VTI',
tags: [],
transactionCount: 5,
url: 'https://www.vanguard.com',
valueInBaseCurrency: 15092
},
{
allocationInPercentage: 0.0836576192450555,
assetClass: 'EQUITY',
assetSubClass: 'ETF',
countries: [],
currency: 'CHF',
dataSource: 'YAHOO',
dateOfFirstActivity: new Date('2018-03-01T00:00:00.000Z'),
dividend: 0,
grossPerformance: 6462.42356864925,
grossPerformancePercent: 0.5463044783973836,
grossPerformancePercentWithCurrencyEffect: 0.6282343505275325,
grossPerformanceWithCurrencyEffect: 7121.935580698947,
holdings: [],
investment: 17336.464702612564,
marketPrice: 129.74,
name: 'Vanguard FTSE All-World UCITS ETF',
netPerformance: 6373.040578098944,
netPerformancePercent: 0.5387484388540966,
netPerformancePercentWithCurrencyEffect: 0.008409682389650015,
netPerformanceWithCurrencyEffect: 198.47200506226807,
quantity: 165,
sectors: [],
symbol: 'VWRL.SW',
tags: [],
transactionCount: 5,
url: 'https://www.vanguard.com',
valueInBaseCurrency: 23798.888271261814
},
{
allocationInPercentage: 0.03265192235898284,
assetClass: 'EQUITY',
assetSubClass: 'ETF',
countries: [],
currency: 'EUR',
dataSource: 'YAHOO',
dateOfFirstActivity: new Date('2021-08-19T00:00:00.000Z'),
dividend: 0,
grossPerformance: 3112.7991183879094,
grossPerformancePercent: 0.5040147846036197,
grossPerformancePercentWithCurrencyEffect: 0.3516875105542396,
grossPerformanceWithCurrencyEffect: 2416.799201046856,
holdings: [],
investment: 6176.007556675063,
marketPrice: 118.005,
name: 'Xtrackers MSCI World UCITS ETF 1C',
netPerformance: 3081.4179261125105,
netPerformancePercent: 0.4989336392216841,
netPerformancePercentWithCurrencyEffect: 0.006460676966633529,
netPerformanceWithCurrencyEffect: 59.626750161726044,
quantity: 75,
sectors: [],
symbol: 'XDWD.DE',
tags: [],
transactionCount: 1,
url: null,
valueInBaseCurrency: 9288.806675062973
},
{
allocationInPercentage: 0.17537283996478595,
assetClass: 'LIQUIDITY',
assetSubClass: 'CASH',
countries: [],
currency: 'USD',
dataSource: 'MANUAL',
dateOfFirstActivity: new Date('2021-04-01T00:00:00.000Z'),
dividend: 0,
grossPerformance: 0,
grossPerformancePercent: 0,
grossPerformancePercentWithCurrencyEffect: 0,
grossPerformanceWithCurrencyEffect: 0,
holdings: [],
investment: 49890,
marketPrice: 0,
name: 'USD',
netPerformance: 0,
netPerformancePercent: 0,
netPerformancePercentWithCurrencyEffect: 0,
netPerformanceWithCurrencyEffect: 0,
quantity: 0,
sectors: [],
symbol: 'USD',
tags: [],
transactionCount: 0,
valueInBaseCurrency: 49890
}
],
locale: 'en-US'
}
};

View File

@ -342,24 +342,42 @@ export class GfTreemapChartComponent
}),
callbacks: {
label: (context) => {
const allocationInPercentage = `${((context.raw._data.allocationInPercentage as number) * 100).toFixed(2)}%`;
const name = context.raw._data.name;
const sign =
context.raw._data.netPerformancePercentWithCurrencyEffect > 0
? '+'
: '';
const symbol = context.raw._data.symbol;
const netPerformanceInPercentageWithSign = `${sign}${(context.raw._data.netPerformancePercentWithCurrencyEffect * 100).toFixed(2)}%`;
if (context.raw._data.valueInBaseCurrency !== null) {
const value = context.raw._data.valueInBaseCurrency as number;
return [
`${name ?? symbol}`,
`${name ?? symbol} (${allocationInPercentage})`,
`${value.toLocaleString(this.locale, {
maximumFractionDigits: 2,
minimumFractionDigits: 2
})} ${this.baseCurrency}`
})} ${this.baseCurrency}`,
'',
$localize`Change` + ' (' + $localize`Performance` + ')',
`${sign}${context.raw._data.netPerformanceWithCurrencyEffect.toLocaleString(
this.locale,
{
maximumFractionDigits: 2,
minimumFractionDigits: 2
}
)} ${this.baseCurrency} (${netPerformanceInPercentageWithSign})`
];
} else {
const percentage =
(context.raw._data.allocationInPercentage as number) * 100;
return [`${name ?? symbol}`, `${percentage.toFixed(2)}%`];
return [
`${name ?? symbol} (${allocationInPercentage})`,
'',
$localize`Performance`,
netPerformanceInPercentageWithSign
];
}
},
title: () => {

View File

@ -1,6 +1,5 @@
import { DateRange, MarketState } from '@ghostfolio/common/types';
import { CommonModule } from '@angular/common';
import {
CUSTOM_ELEMENTS_SCHEMA,
ChangeDetectionStrategy,
@ -11,7 +10,7 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, NgxSkeletonLoaderModule],
imports: [NgxSkeletonLoaderModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-trend-indicator',
styleUrls: ['./trend-indicator.component.scss'],