From 4063c62a173ecf498e8ca7c4bf7cb9a16dabd628 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 12 Jul 2024 21:02:12 +0200 Subject: [PATCH 1/9] Feature/setup treemap chart for holdings (#3560) * Setup treemap chart * Update changelog --- CHANGELOG.md | 6 + .../home-holdings/home-holdings.component.ts | 31 +++- .../home-holdings/home-holdings.html | 78 +++++--- .../home-holdings/home-holdings.module.ts | 7 + .../src/lib/types/holding-view-mode.type.ts | 1 + libs/common/src/lib/types/index.ts | 2 + .../portfolio-proportion-chart.component.ts | 2 +- libs/ui/src/lib/treemap-chart/index.ts | 1 + .../treemap-chart.component.html | 13 ++ .../treemap-chart.component.scss | 4 + .../treemap-chart/treemap-chart.component.ts | 168 ++++++++++++++++++ package.json | 2 + yarn.lock | 10 ++ 13 files changed, 297 insertions(+), 28 deletions(-) create mode 100644 libs/common/src/lib/types/holding-view-mode.type.ts create mode 100644 libs/ui/src/lib/treemap-chart/index.ts create mode 100644 libs/ui/src/lib/treemap-chart/treemap-chart.component.html create mode 100644 libs/ui/src/lib/treemap-chart/treemap-chart.component.scss create mode 100644 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index ee0ad2fca..989c82104 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added + +- Added a chart to the holdings tab of the home page (experimental) + ## 2.94.0 - 2024-07-09 ### Changed diff --git a/apps/client/src/app/components/home-holdings/home-holdings.component.ts b/apps/client/src/app/components/home-holdings/home-holdings.component.ts index 5141bf9fa..86fd0ce0c 100644 --- a/apps/client/src/app/components/home-holdings/home-holdings.component.ts +++ b/apps/client/src/app/components/home-holdings/home-holdings.component.ts @@ -1,11 +1,21 @@ import { DataService } from '@ghostfolio/client/services/data.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; -import { PortfolioPosition, User } from '@ghostfolio/common/interfaces'; +import { + PortfolioPosition, + UniqueAsset, + User +} from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; -import { HoldingType, ToggleOption } from '@ghostfolio/common/types'; +import { + HoldingType, + HoldingViewMode, + ToggleOption +} from '@ghostfolio/common/types'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { FormControl } from '@angular/forms'; +import { Router } from '@angular/router'; import { DeviceDetectorService } from 'ngx-device-detector'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -26,6 +36,7 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { { label: $localize`Closed`, value: 'CLOSED' } ]; public user: User; + public viewModeFormControl = new FormControl('TABLE'); private unsubscribeSubject = new Subject(); @@ -34,6 +45,7 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { private dataService: DataService, private deviceService: DeviceDetectorService, private impersonationStorageService: ImpersonationStorageService, + private router: Router, private userService: UserService ) {} @@ -76,6 +88,13 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { public onChangeHoldingType(aHoldingType: HoldingType) { this.holdingType = aHoldingType; + if (this.holdingType === 'ACTIVE') { + this.viewModeFormControl.enable(); + } else if (this.holdingType === 'CLOSED') { + this.viewModeFormControl.disable(); + this.viewModeFormControl.setValue('TABLE'); + } + this.holdings = undefined; this.fetchHoldings() @@ -87,6 +106,14 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { }); } + public onSymbolClicked({ dataSource, symbol }: UniqueAsset) { + if (dataSource && symbol) { + this.router.navigate([], { + queryParams: { dataSource, symbol, holdingDetailDialog: true } + }); + } + } + public ngOnDestroy() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); diff --git a/apps/client/src/app/components/home-holdings/home-holdings.html b/apps/client/src/app/components/home-holdings/home-holdings.html index a2bd43636..be053415f 100644 --- a/apps/client/src/app/components/home-holdings/home-holdings.html +++ b/apps/client/src/app/components/home-holdings/home-holdings.html @@ -6,32 +6,60 @@
-
- -
- - @if (hasPermissionToCreateOrder && holdings?.length > 0) { -
- Manage Activities +
+ @if (user?.settings?.isExperimentalFeatures) { +
+
+ + + + + + + + +
+
+ } +
+
+
+ @if (viewModeFormControl.value === 'CHART') { + + } @else if (viewModeFormControl.value === 'TABLE') { + + @if (hasPermissionToCreateOrder && holdings?.length > 0) { + + } }
diff --git a/apps/client/src/app/components/home-holdings/home-holdings.module.ts b/apps/client/src/app/components/home-holdings/home-holdings.module.ts index f10adeab2..df951c1a8 100644 --- a/apps/client/src/app/components/home-holdings/home-holdings.module.ts +++ b/apps/client/src/app/components/home-holdings/home-holdings.module.ts @@ -1,9 +1,12 @@ import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module'; import { GfHoldingsTableComponent } from '@ghostfolio/ui/holdings-table'; +import { GfTreemapChartComponent } from '@ghostfolio/ui/treemap-chart'; import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; +import { MatButtonToggleModule } from '@angular/material/button-toggle'; import { RouterModule } from '@angular/router'; import { HomeHoldingsComponent } from './home-holdings.component'; @@ -12,9 +15,13 @@ import { HomeHoldingsComponent } from './home-holdings.component'; declarations: [HomeHoldingsComponent], imports: [ CommonModule, + FormsModule, GfHoldingsTableComponent, GfToggleModule, + GfTreemapChartComponent, MatButtonModule, + MatButtonToggleModule, + ReactiveFormsModule, RouterModule ], schemas: [CUSTOM_ELEMENTS_SCHEMA] diff --git a/libs/common/src/lib/types/holding-view-mode.type.ts b/libs/common/src/lib/types/holding-view-mode.type.ts new file mode 100644 index 000000000..50a4e2b29 --- /dev/null +++ b/libs/common/src/lib/types/holding-view-mode.type.ts @@ -0,0 +1 @@ +export type HoldingViewMode = 'CHART' | 'TABLE'; diff --git a/libs/common/src/lib/types/index.ts b/libs/common/src/lib/types/index.ts index fc4ddc4bf..65fdfe5f0 100644 --- a/libs/common/src/lib/types/index.ts +++ b/libs/common/src/lib/types/index.ts @@ -8,6 +8,7 @@ import type { DateRange } from './date-range.type'; import type { Granularity } from './granularity.type'; import type { GroupBy } from './group-by.type'; import type { HoldingType } from './holding-type.type'; +import type { HoldingViewMode } from './holding-view-mode.type'; import type { MarketAdvanced } from './market-advanced.type'; import type { MarketDataPreset } from './market-data-preset.type'; import type { MarketState } from './market-state.type'; @@ -30,6 +31,7 @@ export type { Granularity, GroupBy, HoldingType, + HoldingViewMode, Market, MarketAdvanced, MarketDataPreset, diff --git a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts index aa0a6cacd..c60ed3443 100644 --- a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts +++ b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts @@ -354,7 +354,7 @@ export class GfPortfolioProportionChartComponent * Color palette, inspired by https://yeun.github.io/open-color */ private getColorPalette() { - // + // TODO: Reuse require('open-color') return [ '#329af0', // blue 5 '#20c997', // teal 5 diff --git a/libs/ui/src/lib/treemap-chart/index.ts b/libs/ui/src/lib/treemap-chart/index.ts new file mode 100644 index 000000000..62f54ac11 --- /dev/null +++ b/libs/ui/src/lib/treemap-chart/index.ts @@ -0,0 +1 @@ +export * from './treemap-chart.component'; diff --git a/libs/ui/src/lib/treemap-chart/treemap-chart.component.html b/libs/ui/src/lib/treemap-chart/treemap-chart.component.html new file mode 100644 index 000000000..c7de5ef4d --- /dev/null +++ b/libs/ui/src/lib/treemap-chart/treemap-chart.component.html @@ -0,0 +1,13 @@ +@if (isLoading) { + +} + diff --git a/libs/ui/src/lib/treemap-chart/treemap-chart.component.scss b/libs/ui/src/lib/treemap-chart/treemap-chart.component.scss new file mode 100644 index 000000000..d041372c8 --- /dev/null +++ b/libs/ui/src/lib/treemap-chart/treemap-chart.component.scss @@ -0,0 +1,4 @@ +:host { + aspect-ratio: 16 / 9; + display: block; +} diff --git a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts new file mode 100644 index 000000000..557bdc2ae --- /dev/null +++ b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts @@ -0,0 +1,168 @@ +import { getLocale } from '@ghostfolio/common/helper'; +import { PortfolioPosition, UniqueAsset } from '@ghostfolio/common/interfaces'; + +import { CommonModule } from '@angular/common'; +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + ElementRef, + EventEmitter, + Input, + OnChanges, + OnDestroy, + Output, + ViewChild +} from '@angular/core'; +import { DataSource } from '@prisma/client'; +import { ChartConfiguration } from 'chart.js'; +import { LinearScale } from 'chart.js'; +import { Chart } from 'chart.js'; +import { TreemapController, TreemapElement } from 'chartjs-chart-treemap'; +import { orderBy } from 'lodash'; +import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; + +const { gray, green, red } = require('open-color'); + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [CommonModule, NgxSkeletonLoaderModule], + selector: 'gf-treemap-chart', + standalone: true, + styleUrls: ['./treemap-chart.component.scss'], + templateUrl: './treemap-chart.component.html' +}) +export class GfTreemapChartComponent + implements AfterViewInit, OnChanges, OnDestroy +{ + @Input() cursor: string; + @Input() holdings: PortfolioPosition[]; + + @Output() treemapChartClicked = new EventEmitter(); + + @ViewChild('chartCanvas') chartCanvas: ElementRef; + + public chart: Chart<'treemap'>; + public isLoading = true; + + public constructor() { + Chart.register(LinearScale, TreemapController, TreemapElement); + } + + public ngAfterViewInit() { + if (this.holdings) { + this.initialize(); + } + } + + public ngOnChanges() { + if (this.holdings) { + this.initialize(); + } + } + + public ngOnDestroy() { + this.chart?.destroy(); + } + + private initialize() { + this.isLoading = true; + + const data: ChartConfiguration['data'] = { + datasets: [ + { + backgroundColor(ctx) { + const netPerformancePercentWithCurrencyEffect = + ctx.raw._data.netPerformancePercentWithCurrencyEffect; + + if (netPerformancePercentWithCurrencyEffect > 0.03) { + return green[9]; + } else if (netPerformancePercentWithCurrencyEffect > 0.02) { + return green[7]; + } else if (netPerformancePercentWithCurrencyEffect > 0.01) { + return green[5]; + } else if (netPerformancePercentWithCurrencyEffect > 0) { + return green[3]; + } else if (netPerformancePercentWithCurrencyEffect === 0) { + return gray[3]; + } else if (netPerformancePercentWithCurrencyEffect > -0.01) { + return red[3]; + } else if (netPerformancePercentWithCurrencyEffect > -0.02) { + return red[5]; + } else if (netPerformancePercentWithCurrencyEffect > -0.03) { + return red[7]; + } else { + return red[9]; + } + }, + key: 'allocationInPercentage', + labels: { + align: 'left', + color: ['white'], + display: true, + font: [{ size: 14 }, { size: 11 }, { lineHeight: 2, size: 14 }], + formatter(ctx) { + const netPerformancePercentWithCurrencyEffect = + ctx.raw._data.netPerformancePercentWithCurrencyEffect; + + return [ + ctx.raw._data.name, + ctx.raw._data.symbol, + `${netPerformancePercentWithCurrencyEffect > 0 ? '+' : ''}${(ctx.raw._data.netPerformancePercentWithCurrencyEffect * 100).toFixed(2)}%` + ]; + }, + position: 'top' + }, + spacing: 1, + tree: this.holdings + } + ] + }; + + if (this.chartCanvas) { + if (this.chart) { + this.chart.data = data; + this.chart.update(); + } else { + this.chart = new Chart(this.chartCanvas.nativeElement, { + data, + options: { + animation: false, + onClick: (event, activeElements) => { + try { + const dataIndex = activeElements[0].index; + const datasetIndex = activeElements[0].datasetIndex; + + const dataset = orderBy( + event.chart.data.datasets[datasetIndex].tree, + ['allocationInPercentage'], + ['desc'] + ); + + const dataSource: DataSource = dataset[dataIndex].dataSource; + const symbol: string = dataset[dataIndex].symbol; + + this.treemapChartClicked.emit({ dataSource, symbol }); + } catch {} + }, + onHover: (event, chartElement) => { + if (this.cursor) { + event.native.target.style.cursor = chartElement[0] + ? this.cursor + : 'default'; + } + }, + plugins: { + tooltip: { + enabled: false + } + } + }, + type: 'treemap' + }); + } + } + + this.isLoading = false; + } +} diff --git a/package.json b/package.json index fca0a9a0f..49b43b303 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "cache-manager-redis-store": "2.0.0", "chart.js": "4.2.0", "chartjs-adapter-date-fns": "3.0.0", + "chartjs-chart-treemap": "2.3.1", "chartjs-plugin-annotation": "2.1.2", "chartjs-plugin-datalabels": "2.2.0", "cheerio": "1.0.0-rc.12", @@ -122,6 +123,7 @@ "ngx-markdown": "18.0.0", "ngx-skeleton-loader": "7.0.0", "ngx-stripe": "18.0.0", + "open-color": "1.9.1", "papaparse": "5.3.1", "passport": "0.7.0", "passport-google-oauth20": "2.0.0", diff --git a/yarn.lock b/yarn.lock index 8d6caaef2..e807877b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10071,6 +10071,11 @@ chartjs-adapter-date-fns@3.0.0: resolved "https://registry.yarnpkg.com/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-3.0.0.tgz#c25f63c7f317c1f96f9a7c44bd45eeedb8a478e5" integrity sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg== +chartjs-chart-treemap@2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/chartjs-chart-treemap/-/chartjs-chart-treemap-2.3.1.tgz#b0d27309ee373cb7706cabb262c48c53ffacf710" + integrity sha512-GW+iODLICIJhNZtHbTtaOjCwRIxmXcquXRKDFMsrkXyqyDeSN1aiVfzNNj6Xjy55soopqRA+YfHqjT2S2zF7lQ== + chartjs-plugin-annotation@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/chartjs-plugin-annotation/-/chartjs-plugin-annotation-2.1.2.tgz#8c307c931fda735a1acf1b606ad0e3fd7d96299b" @@ -16757,6 +16762,11 @@ onetime@^5.1.0, onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" +open-color@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/open-color/-/open-color-1.9.1.tgz#a6e6328f60eff7aa60e3e8fcfa50f53ff3eece35" + integrity sha512-vCseG/EQ6/RcvxhUcGJiHViOgrtz4x0XbZepXvKik66TMGkvbmjeJrKFyBEx6daG5rNyyd14zYXhz0hZVwQFOw== + open@8.4.2, open@^8.0.4, open@^8.0.9, open@^8.4.0: version "8.4.2" resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" From 96434c5a54631777b9af601f68d68b670db48454 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 12 Jul 2024 21:04:38 +0200 Subject: [PATCH 2/9] Release 2.95.0 (#3561) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 989c82104..df89cb128 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## 2.95.0 - 2024-07-12 ### Added diff --git a/package.json b/package.json index 49b43b303..8103533fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "2.94.0", + "version": "2.95.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio", From 9ecc3176a5615184c97e4b2d46861b8f52fa99d6 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 13 Jul 2024 10:45:10 +0200 Subject: [PATCH 3/9] Feature/improve treemap chart for holdings (#3563) * Various improvements * Introduce permission: accessHoldingsChart * Improve style of toggle * Add border radius * Update changelog --- CHANGELOG.md | 6 ++ apps/api/src/app/user/user.service.ts | 1 + .../home-holdings/home-holdings.component.ts | 56 ++++++++++--------- .../home-holdings/home-holdings.html | 4 +- .../home-holdings/home-holdings.scss | 6 ++ libs/common/src/lib/permissions.ts | 9 ++- .../treemap-chart/treemap-chart.component.ts | 2 +- 7 files changed, 55 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df89cb128..9557701c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Changed + +- Improved the chart of the holdings tab on the home page (experimental) + ## 2.95.0 - 2024-07-12 ### Added diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index ce0ec5524..02a65b6a0 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -237,6 +237,7 @@ export class UserService { currentPermissions = without( currentPermissions, + permissions.accessHoldingsChart, permissions.createAccess ); diff --git a/apps/client/src/app/components/home-holdings/home-holdings.component.ts b/apps/client/src/app/components/home-holdings/home-holdings.component.ts index 86fd0ce0c..3b99adb06 100644 --- a/apps/client/src/app/components/home-holdings/home-holdings.component.ts +++ b/apps/client/src/app/components/home-holdings/home-holdings.component.ts @@ -28,6 +28,7 @@ import { takeUntil } from 'rxjs/operators'; export class HomeHoldingsComponent implements OnDestroy, OnInit { public deviceType: string; public hasImpersonationId: boolean; + public hasPermissionToAccessHoldingsChart: boolean; public hasPermissionToCreateOrder: boolean; public holdings: PortfolioPosition[]; public holdingType: HoldingType = 'ACTIVE'; @@ -65,20 +66,17 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { if (state?.user) { this.user = state.user; + this.hasPermissionToAccessHoldingsChart = hasPermission( + this.user.permissions, + permissions.accessHoldingsChart + ); + this.hasPermissionToCreateOrder = hasPermission( this.user.permissions, permissions.createOrder ); - this.holdings = undefined; - - this.fetchHoldings() - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(({ holdings }) => { - this.holdings = holdings; - - this.changeDetectorRef.markForCheck(); - }); + this.initialize(); this.changeDetectorRef.markForCheck(); } @@ -88,22 +86,7 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { public onChangeHoldingType(aHoldingType: HoldingType) { this.holdingType = aHoldingType; - if (this.holdingType === 'ACTIVE') { - this.viewModeFormControl.enable(); - } else if (this.holdingType === 'CLOSED') { - this.viewModeFormControl.disable(); - this.viewModeFormControl.setValue('TABLE'); - } - - this.holdings = undefined; - - this.fetchHoldings() - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(({ holdings }) => { - this.holdings = holdings; - - this.changeDetectorRef.markForCheck(); - }); + this.initialize(); } public onSymbolClicked({ dataSource, symbol }: UniqueAsset) { @@ -131,4 +114,27 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { range: this.user?.settings?.dateRange }); } + + private initialize() { + this.viewModeFormControl.disable(); + + if ( + this.hasPermissionToAccessHoldingsChart && + this.holdingType === 'ACTIVE' + ) { + this.viewModeFormControl.enable(); + } else if (this.holdingType === 'CLOSED') { + this.viewModeFormControl.setValue('TABLE'); + } + + this.holdings = undefined; + + this.fetchHoldings() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(({ holdings }) => { + this.holdings = holdings; + + this.changeDetectorRef.markForCheck(); + }); + } } diff --git a/apps/client/src/app/components/home-holdings/home-holdings.html b/apps/client/src/app/components/home-holdings/home-holdings.html index be053415f..a2ea30a69 100644 --- a/apps/client/src/app/components/home-holdings/home-holdings.html +++ b/apps/client/src/app/components/home-holdings/home-holdings.html @@ -14,10 +14,10 @@ [formControl]="viewModeFormControl" [hideSingleSelectionIndicator]="true" > - + - + diff --git a/apps/client/src/app/components/home-holdings/home-holdings.scss b/apps/client/src/app/components/home-holdings/home-holdings.scss index 5d4e87f30..d6d129a39 100644 --- a/apps/client/src/app/components/home-holdings/home-holdings.scss +++ b/apps/client/src/app/components/home-holdings/home-holdings.scss @@ -1,3 +1,9 @@ :host { display: block; + + .mat-button-toggle-group { + .mat-button-toggle-appearance-standard { + --mat-standard-button-toggle-height: 1.5rem; + } + } } diff --git a/libs/common/src/lib/permissions.ts b/libs/common/src/lib/permissions.ts index 304c8ba24..1a235f1a9 100644 --- a/libs/common/src/lib/permissions.ts +++ b/libs/common/src/lib/permissions.ts @@ -5,6 +5,7 @@ import { Role } from '@prisma/client'; export const permissions = { accessAdminControl: 'accessAdminControl', accessAssistant: 'accessAssistant', + accessHoldingsChart: 'accessHoldingsChart', createAccess: 'createAccess', createAccount: 'createAccount', createAccountBalance: 'createAccountBalance', @@ -47,6 +48,7 @@ export function getPermissions(aRole: Role): string[] { return [ permissions.accessAdminControl, permissions.accessAssistant, + permissions.accessHoldingsChart, permissions.createAccess, permissions.createAccount, permissions.createAccountBalance, @@ -72,11 +74,16 @@ export function getPermissions(aRole: Role): string[] { ]; case 'DEMO': - return [permissions.accessAssistant, permissions.createUserAccount]; + return [ + permissions.accessAssistant, + permissions.accessHoldingsChart, + permissions.createUserAccount + ]; case 'USER': return [ permissions.accessAssistant, + permissions.accessHoldingsChart, permissions.createAccess, permissions.createAccount, permissions.createAccountBalance, diff --git a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts index 557bdc2ae..9ee6a7aeb 100644 --- a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts +++ b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts @@ -1,4 +1,3 @@ -import { getLocale } from '@ghostfolio/common/helper'; import { PortfolioPosition, UniqueAsset } from '@ghostfolio/common/interfaces'; import { CommonModule } from '@angular/common'; @@ -95,6 +94,7 @@ export class GfTreemapChartComponent return red[9]; } }, + borderRadius: 4, key: 'allocationInPercentage', labels: { align: 'left', From 6eb9d9d973066b901df8303d1896bde21eaf2390 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 13 Jul 2024 11:40:29 +0200 Subject: [PATCH 4/9] Feature/extend personal finance tools 20240713 (#3565) --- libs/common/src/lib/personal-finance-tools.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/libs/common/src/lib/personal-finance-tools.ts b/libs/common/src/lib/personal-finance-tools.ts index 8a20ca15c..af7879581 100644 --- a/libs/common/src/lib/personal-finance-tools.ts +++ b/libs/common/src/lib/personal-finance-tools.ts @@ -351,6 +351,18 @@ export const personalFinanceTools: Product[] = [ origin: `Italy`, slogan: 'Your Personal Finance Hub' }, + { + founded: 2008, + hasFreePlan: true, + hasSelfHostingAbility: false, + key: 'pocketsmith', + languages: ['English'], + name: 'PocketSmith', + origin: `New Zealand`, + pricingPerYear: '$120', + region: `Global`, + slogan: 'Know where your money is going' + }, { hasFreePlan: false, hasSelfHostingAbility: false, From 0640b24290b10c06d80bd53ccd811de495779332 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 13 Jul 2024 11:40:45 +0200 Subject: [PATCH 5/9] Feature/improve site.webmanifest (#3564) * Separate icon purposes * Update changelog --- CHANGELOG.md | 1 + apps/client/src/assets/site.webmanifest | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9557701c7..d3694f580 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Improved the chart of the holdings tab on the home page (experimental) +- Separated the icon purposes in the `site.webmanifest` ## 2.95.0 - 2024-07-12 diff --git a/apps/client/src/assets/site.webmanifest b/apps/client/src/assets/site.webmanifest index 3dc452fe9..8f1eceefb 100644 --- a/apps/client/src/assets/site.webmanifest +++ b/apps/client/src/assets/site.webmanifest @@ -7,10 +7,16 @@ { "sizes": "192x192", "src": "/assets/android-chrome-192x192.png", - "type": "image/png", - "purpose": "any maskable" + "type": "image/png" + }, + { + "purpose": "any", + "sizes": "512x512", + "src": "/assets/android-chrome-512x512.png", + "type": "image/png" }, { + "purpose": "maskable", "sizes": "512x512", "src": "/assets/android-chrome-512x512.png", "type": "image/png" @@ -21,5 +27,5 @@ "short_name": "Ghostfolio", "start_url": "/en/", "theme_color": "#FFFFFF", - "url": "https://www.ghostfol.io" + "url": "https://ghostfol.io" } From 9d6214e93ac40cd537f16ebdba01efc1a4d7abd9 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 13 Jul 2024 18:24:03 +0200 Subject: [PATCH 6/9] Bugfix/fix fees calculation in portfolio summary (#3567) * Fix fees calculation * Update changelog --- CHANGELOG.md | 4 ++++ .../app/portfolio/calculator/portfolio-calculator.ts | 11 +++++++++-- ...ulator-baln-buy-and-sell-in-two-activities.spec.ts | 1 + .../portfolio-calculator-baln-buy-and-sell.spec.ts | 1 + .../twr/portfolio-calculator-baln-buy.spec.ts | 1 + ...o-calculator-btcusd-buy-and-sell-partially.spec.ts | 1 + .../calculator/twr/portfolio-calculator-fee.spec.ts | 1 + .../twr/portfolio-calculator-googl-buy.spec.ts | 3 ++- .../calculator/twr/portfolio-calculator-item.spec.ts | 1 + ...lio-calculator-novn-buy-and-sell-partially.spec.ts | 1 + .../portfolio-calculator-novn-buy-and-sell.spec.ts | 1 + .../portfolio/calculator/twr/portfolio-calculator.ts | 4 ++-- libs/common/src/lib/models/timeline-position.ts | 4 ++++ 13 files changed, 29 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3694f580..1f7dfd026 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improved the chart of the holdings tab on the home page (experimental) - Separated the icon purposes in the `site.webmanifest` +### Fixed + +- Fixed an issue in the portfolio summary with the currency conversion of fees + ## 2.95.0 - 2024-07-12 ### Added diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index e021eb2d4..ec56a247c 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -300,6 +300,12 @@ export abstract class PortfolioCalculator { const errors: ResponseError['errors'] = []; for (const item of lastTransactionPoint.items) { + const feeInBaseCurrency = item.fee.mul( + exchangeRatesByCurrency[`${item.currency}${this.currency}`]?.[ + lastTransactionPoint.date + ] + ); + const marketPriceInBaseCurrency = ( marketSymbolMap[endDateString]?.[item.symbol] ?? item.averagePrice ).mul( @@ -340,10 +346,11 @@ export abstract class PortfolioCalculator { hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors; positions.push({ - dividend: totalDividend, - dividendInBaseCurrency: totalDividendInBaseCurrency, + feeInBaseCurrency, timeWeightedInvestment, timeWeightedInvestmentWithCurrencyEffect, + dividend: totalDividend, + dividendInBaseCurrency: totalDividendInBaseCurrency, averagePrice: item.averagePrice, currency: item.currency, dataSource: item.dataSource, diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts index 340f16b87..3eb166d2e 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts @@ -168,6 +168,7 @@ describe('PortfolioCalculator', () => { dividend: new Big('0'), dividendInBaseCurrency: new Big('0'), fee: new Big('3.2'), + feeInBaseCurrency: new Big('3.2'), firstBuyDate: '2021-11-22', grossPerformance: new Big('-12.6'), grossPerformancePercentage: new Big('-0.04408677396780965649'), diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts index 53ebdf19f..a3adde84b 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts @@ -153,6 +153,7 @@ describe('PortfolioCalculator', () => { dividend: new Big('0'), dividendInBaseCurrency: new Big('0'), fee: new Big('3.2'), + feeInBaseCurrency: new Big('3.2'), firstBuyDate: '2021-11-22', grossPerformance: new Big('-12.6'), grossPerformancePercentage: new Big('-0.0440867739678096571'), diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts index bab265887..6221e6240 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts @@ -138,6 +138,7 @@ describe('PortfolioCalculator', () => { dividend: new Big('0'), dividendInBaseCurrency: new Big('0'), fee: new Big('1.55'), + feeInBaseCurrency: new Big('1.55'), firstBuyDate: '2021-11-30', grossPerformance: new Big('24.6'), grossPerformancePercentage: new Big('0.09004392386530014641'), diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts index eba5d4674..85d39220a 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts @@ -166,6 +166,7 @@ describe('PortfolioCalculator', () => { dividend: new Big('0'), dividendInBaseCurrency: new Big('0'), fee: new Big('0'), + feeInBaseCurrency: new Big('0'), firstBuyDate: '2015-01-01', grossPerformance: new Big('27172.74'), grossPerformancePercentage: new Big('42.41978276196153750666'), diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-fee.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-fee.spec.ts index 88d7adb71..7e9bbc26d 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-fee.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-fee.spec.ts @@ -123,6 +123,7 @@ describe('PortfolioCalculator', () => { dividend: new Big('0'), dividendInBaseCurrency: new Big('0'), fee: new Big('49'), + feeInBaseCurrency: new Big('49'), firstBuyDate: '2021-09-01', grossPerformance: null, grossPerformancePercentage: null, diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts index 690f1eb51..31182c8cb 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts @@ -151,6 +151,7 @@ describe('PortfolioCalculator', () => { dividend: new Big('0'), dividendInBaseCurrency: new Big('0'), fee: new Big('1'), + feeInBaseCurrency: new Big('0.9238'), firstBuyDate: '2023-01-03', grossPerformance: new Big('27.33'), grossPerformancePercentage: new Big('0.3066651705565529623'), @@ -177,7 +178,7 @@ describe('PortfolioCalculator', () => { valueInBaseCurrency: new Big('103.10483') } ], - totalFeesWithCurrencyEffect: new Big('1'), + totalFeesWithCurrencyEffect: new Big('0.9238'), totalInterestWithCurrencyEffect: new Big('0'), totalInvestment: new Big('89.12'), totalInvestmentWithCurrencyEffect: new Big('82.329056'), diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-item.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-item.spec.ts index 422d119b2..985dc5feb 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-item.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-item.spec.ts @@ -123,6 +123,7 @@ describe('PortfolioCalculator', () => { dividend: new Big('0'), dividendInBaseCurrency: new Big('0'), fee: new Big('0'), + feeInBaseCurrency: new Big('0'), firstBuyDate: '2022-01-01', grossPerformance: null, grossPerformancePercentage: null, diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts index f65d2ba61..72cb16a5f 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts @@ -153,6 +153,7 @@ describe('PortfolioCalculator', () => { dividend: new Big('0'), dividendInBaseCurrency: new Big('0'), fee: new Big('4.25'), + feeInBaseCurrency: new Big('4.25'), firstBuyDate: '2022-03-07', grossPerformance: new Big('21.93'), grossPerformancePercentage: new Big('0.15113417083448194384'), diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts index 902f710ee..59cc13f0e 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts @@ -183,6 +183,7 @@ describe('PortfolioCalculator', () => { dividend: new Big('0'), dividendInBaseCurrency: new Big('0'), fee: new Big('0'), + feeInBaseCurrency: new Big('0'), firstBuyDate: '2022-03-07', grossPerformance: new Big('19.86'), grossPerformancePercentage: new Big('0.13100263852242744063'), diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts index f8b62a940..e9f1f3fda 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts @@ -34,9 +34,9 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { let totalTimeWeightedInvestmentWithCurrencyEffect = new Big(0); for (const currentPosition of positions) { - if (currentPosition.fee) { + if (currentPosition.feeInBaseCurrency) { totalFeesWithCurrencyEffect = totalFeesWithCurrencyEffect.plus( - currentPosition.fee + currentPosition.feeInBaseCurrency ); } diff --git a/libs/common/src/lib/models/timeline-position.ts b/libs/common/src/lib/models/timeline-position.ts index 412449590..545891464 100644 --- a/libs/common/src/lib/models/timeline-position.ts +++ b/libs/common/src/lib/models/timeline-position.ts @@ -24,6 +24,10 @@ export class TimelinePosition { @Type(() => Big) fee: Big; + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + feeInBaseCurrency: Big; + firstBuyDate: string; @Transform(transformToBig, { toClassOnly: true }) From 89be438e669bc725670283fdc4085c9f35c4f78e Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 13 Jul 2024 19:02:47 +0200 Subject: [PATCH 7/9] Bugfix/remove show condition of experimental features setting (#3568) * Remove show condition of experimental feature setting * Update changelog --- CHANGELOG.md | 1 + .../user-account-settings.html | 32 +++++++++---------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f7dfd026..d4650e927 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed an issue in the portfolio summary with the currency conversion of fees +- Removed the show condition of the experimental features setting in the user settings ## 2.95.0 - 2024-07-12 diff --git a/apps/client/src/app/components/user-account-settings/user-account-settings.html b/apps/client/src/app/components/user-account-settings/user-account-settings.html index c369f81bd..66eb37f61 100644 --- a/apps/client/src/app/components/user-account-settings/user-account-settings.html +++ b/apps/client/src/app/components/user-account-settings/user-account-settings.html @@ -196,25 +196,23 @@ />
- @if (hasPermissionToUpdateUserSettings) { -
-
-
Experimental Features
-
- Sneak peek at upcoming functionality -
-
-
- +
+
+
Experimental Features
+
+ Sneak peek at upcoming functionality
- } +
+ +
+
Ghostfolio User ID From 6c1317f978170fadb747080b6c332c0c6c1826ce Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 13 Jul 2024 20:11:40 +0200 Subject: [PATCH 8/9] Bugfix/fix search for holding in assistant (#3569) * Fix search for holding * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/account/account.service.ts | 4 +-- apps/api/src/app/order/order.service.ts | 32 +++++++++++++++++++-- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4650e927..193fe9d12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed an issue in the portfolio summary with the currency conversion of fees +- Fixed an issue in the the search for a holding - Removed the show condition of the experimental features setting in the user settings ## 2.95.0 - 2024-07-12 diff --git a/apps/api/src/app/account/account.service.ts b/apps/api/src/app/account/account.service.ts index 1564fa5b3..37876dde0 100644 --- a/apps/api/src/app/account/account.service.ts +++ b/apps/api/src/app/account/account.service.ts @@ -174,8 +174,8 @@ export class AccountService { ACCOUNT: filtersByAccount, ASSET_CLASS: filtersByAssetClass, TAG: filtersByTag - } = groupBy(filters, (filter) => { - return filter.type; + } = groupBy(filters, ({ type }) => { + return type; }); if (filtersByAccount?.length > 0) { diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index 0255a5011..b743eb2b7 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -312,10 +312,14 @@ export class OrderService { ACCOUNT: filtersByAccount, ASSET_CLASS: filtersByAssetClass, TAG: filtersByTag - } = groupBy(filters, (filter) => { - return filter.type; + } = groupBy(filters, ({ type }) => { + return type; }); + const searchQuery = filters?.find(({ type }) => { + return type === 'SEARCH_QUERY'; + })?.id; + if (filtersByAccount?.length > 0) { where.accountId = { in: filtersByAccount.map(({ id }) => { @@ -357,6 +361,30 @@ export class OrderService { }; } + if (searchQuery) { + const searchQueryWhereInput: Prisma.SymbolProfileWhereInput[] = [ + { id: { mode: 'insensitive', startsWith: searchQuery } }, + { isin: { mode: 'insensitive', startsWith: searchQuery } }, + { name: { mode: 'insensitive', startsWith: searchQuery } }, + { symbol: { mode: 'insensitive', startsWith: searchQuery } } + ]; + + if (where.SymbolProfile) { + where.SymbolProfile = { + AND: [ + where.SymbolProfile, + { + OR: searchQueryWhereInput + } + ] + }; + } else { + where.SymbolProfile = { + OR: searchQueryWhereInput + }; + } + } + if (filtersByTag?.length > 0) { where.tags = { some: { From c9fc3e402db38e2784e001e3449ec9d08da92472 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 13 Jul 2024 20:13:53 +0200 Subject: [PATCH 9/9] Release 2.96.0 (#3570) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 193fe9d12..ba8c5f80d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## 2.96.0 - 2024-07-13 ### Changed diff --git a/package.json b/package.json index 8103533fc..9acc17f2e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "2.95.0", + "version": "2.96.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio",