Compare commits

..

14 Commits

Author SHA1 Message Date
bb9415cc15 Release 1.272.0 (#2005) 2023-05-26 20:43:09 +02:00
b3baeb8a5d Feature/support case insensitive names in portfolio proportion chart (#2004)
* Sum up case insensitive names

* Update changelog
2023-05-26 20:41:33 +02:00
1f393e78f6 Feature/improve error handling in delete user endpoint (#2000)
* Improve error handling

* Update changelog
2023-05-25 17:27:33 +02:00
215f5eafa6 Feature/set asset profile as benchmark (#2002)
* Set asset profile as benchmark

* Update changelog

Co-authored-by: Arghya Ghosh <arghyag5@gmail.com>
2023-05-24 21:22:32 +02:00
1916e5343d Feature/decrease table density (#2001)
* Decrease table density

* Update changelog
2023-05-24 19:34:12 +02:00
fa9863fc54 Feature/upgrade ionicons to version 7.1.0 (#1997)
* Upgrade ionicons to version 7.1.0

* Update changelog
2023-05-23 14:45:21 +02:00
7bf48ef351 Improve style on about page (#1998)
* Center changelog button

* Update changelog
2023-05-22 20:20:16 +02:00
faef3606fd Feature/improve breadcrumb navigation style in blog posts for mobile (#1996)
* Improve style for mobile

* Update changelog
2023-05-21 07:50:45 +02:00
d0ccd4d238 Release 1.271.0 (#1995) 2023-05-20 18:13:41 +02:00
51e3650790 Feature/add blog post unlock your financial potential (#1994)
* Add blog post: Unlock your Financial Potential with Ghostfolio

* Update changelog
2023-05-20 18:12:12 +02:00
db29e2b666 Feature/extend financial modeling prep service (#1989)
* Add getHistorical() and search() logic

* Update changelog
2023-05-20 11:10:07 +02:00
655a68a847 Feature/change uptime to last 90 days (#1993)
* Change uptime to last 90 days

* Update changelog
2023-05-20 11:07:53 +02:00
86296b3591 Feature/improve local number formatting in value component (#1992)
* Improve local number formatting

* Update changelog
2023-05-20 10:53:04 +02:00
73c127f10c Bugfix/fix vertical alignment in gf toggle (#1991)
* Fix alignment

* Update changelog
2023-05-20 10:10:53 +02:00
51 changed files with 803 additions and 133 deletions

View File

@ -5,6 +5,37 @@ 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).
## 1.272.0 - 2023-05-26
### Added
- Added support to set an asset profile as a benchmark
### Changed
- Decreased the density of the `@angular/material` tables
- Improved the portfolio proportion chart component by supporting case insensitive names
- Improved the breadcrumb navigation style in the blog post pages for mobile
- Improved the error handling in the delete user endpoint
- Improved the style of the _Changelog & License_ button on the about page
- Upgraded `ionicons` from version `6.1.2` to `7.1.0`
## 1.271.0 - 2023-05-20
### Added
- Added the historical data and search functionality for the `FINANCIAL_MODELING_PREP` data source type
- Added a blog post: _Unlock your Financial Potential with Ghostfolio_
### Changed
- Improved the local number formatting in the value component
- Changed the uptime to the last 90 days on the _Open Startup_ (`/open`) page
### Fixed
- Fixed the vertical alignment in the toggle component
## 1.270.1 - 2023-05-19
### Added
@ -247,7 +278,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Changed the slide toggles to checkboxes on the account page
- Changed the slide toggles to checkboxes in the admin control panel
- Decreased the density of the theme
- Increased the density of the theme
- Migrated the style of various components to `@angular/material` `15` (mdc)
- Upgraded `@angular/cdk` and `@angular/material` from version `15.2.5` to `15.2.6`
- Upgraded `bull` from version `4.10.2` to `4.10.4`

View File

@ -2,23 +2,35 @@ import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interce
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
import {
BenchmarkMarketDataDetails,
BenchmarkResponse
BenchmarkResponse,
UniqueAsset
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
Body,
Controller,
Get,
HttpException,
Inject,
Param,
Post,
UseGuards,
UseInterceptors
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { DataSource } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { BenchmarkService } from './benchmark.service';
@Controller('benchmark')
export class BenchmarkController {
public constructor(private readonly benchmarkService: BenchmarkService) {}
public constructor(
private readonly benchmarkService: BenchmarkService,
@Inject(REQUEST) private readonly request: RequestWithUser
) {}
@Get()
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@ -45,4 +57,41 @@ export class BenchmarkController {
symbol
});
}
@Post()
@UseGuards(AuthGuard('jwt'))
public async addBenchmark(@Body() { dataSource, symbol }: UniqueAsset) {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
try {
const benchmark = await this.benchmarkService.addBenchmark({
dataSource,
symbol
});
if (!benchmark) {
throw new HttpException(
getReasonPhrase(StatusCodes.NOT_FOUND),
StatusCodes.NOT_FOUND
);
}
return benchmark;
} catch {
throw new HttpException(
getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR),
StatusCodes.INTERNAL_SERVER_ERROR
);
}
}
}

View File

@ -3,6 +3,7 @@ import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
import { Module } from '@nestjs/common';
@ -17,6 +18,7 @@ import { BenchmarkService } from './benchmark.service';
ConfigurationModule,
DataProviderModule,
MarketDataModule,
PrismaModule,
PropertyModule,
RedisCacheModule,
SymbolModule,

View File

@ -4,7 +4,15 @@ describe('BenchmarkService', () => {
let benchmarkService: BenchmarkService;
beforeAll(async () => {
benchmarkService = new BenchmarkService(null, null, null, null, null, null);
benchmarkService = new BenchmarkService(
null,
null,
null,
null,
null,
null,
null
);
});
it('calculateChangeInPercentage', async () => {

View File

@ -2,6 +2,7 @@ import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.s
import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import {
@ -11,6 +12,7 @@ import {
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import {
BenchmarkMarketDataDetails,
BenchmarkProperty,
BenchmarkResponse,
UniqueAsset
} from '@ghostfolio/common/interfaces';
@ -18,6 +20,7 @@ import { Injectable } from '@nestjs/common';
import { SymbolProfile } from '@prisma/client';
import Big from 'big.js';
import { format } from 'date-fns';
import { uniqBy } from 'lodash';
import ms from 'ms';
@Injectable()
@ -27,6 +30,7 @@ export class BenchmarkService {
public constructor(
private readonly dataProviderService: DataProviderService,
private readonly marketDataService: MarketDataService,
private readonly prismaService: PrismaService,
private readonly propertyService: PropertyService,
private readonly redisCacheService: RedisCacheService,
private readonly symbolProfileService: SymbolProfileService,
@ -116,9 +120,9 @@ export class BenchmarkService {
public async getBenchmarkAssetProfiles(): Promise<Partial<SymbolProfile>[]> {
const symbolProfileIds: string[] = (
((await this.propertyService.getByKey(PROPERTY_BENCHMARKS)) as {
symbolProfileId: string;
}[]) ?? []
((await this.propertyService.getByKey(
PROPERTY_BENCHMARKS
)) as BenchmarkProperty[]) ?? []
).map(({ symbolProfileId }) => {
return symbolProfileId;
});
@ -204,6 +208,43 @@ export class BenchmarkService {
return response;
}
public async addBenchmark({
dataSource,
symbol
}: UniqueAsset): Promise<Partial<SymbolProfile>> {
const assetProfile = await this.prismaService.symbolProfile.findFirst({
where: {
dataSource,
symbol
}
});
if (!assetProfile) {
return;
}
let benchmarks =
((await this.propertyService.getByKey(
PROPERTY_BENCHMARKS
)) as BenchmarkProperty[]) ?? [];
benchmarks.push({ symbolProfileId: assetProfile.id });
benchmarks = uniqBy(benchmarks, 'symbolProfileId');
await this.propertyService.put({
key: PROPERTY_BENCHMARKS,
value: JSON.stringify(benchmarks)
});
return {
dataSource,
symbol,
id: assetProfile.id,
name: assetProfile.name
};
}
private getMarketCondition(aPerformanceInPercent: number) {
return aPerformanceInPercent <= -0.2 ? 'BEAR_MARKET' : 'NEUTRAL_MARKET';
}

View File

@ -94,6 +94,13 @@ export class FrontendMiddleware implements NestMiddleware {
) {
featureGraphicPath = 'assets/images/blog/1000-stars-on-github.jpg';
title = `Ghostfolio reaches 1000 Stars on GitHub - ${title}`;
} else if (
request.path.startsWith(
'/en/blog/2023/05/unlock-your-financial-potential-with-ghostfolio'
)
) {
featureGraphicPath = 'assets/images/blog/20230520.jpg';
title = `Unlock your Financial Potential with Ghostfolio - ${title}`;
}
if (

View File

@ -17,19 +17,22 @@ import {
ghostfolioFearAndGreedIndexDataSource
} from '@ghostfolio/common/config';
import {
DATE_FORMAT,
encodeDataSource,
extractNumberFromString
} from '@ghostfolio/common/helper';
import { InfoItem } from '@ghostfolio/common/interfaces';
import { Statistics } from '@ghostfolio/common/interfaces/statistics.interface';
import { Subscription } from '@ghostfolio/common/interfaces/subscription.interface';
import {
InfoItem,
Statistics,
Subscription
} from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions';
import { SubscriptionOffer } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bent from 'bent';
import * as cheerio from 'cheerio';
import { subDays } from 'date-fns';
import { format, subDays } from 'date-fns';
@Injectable()
export class InfoService {
@ -344,7 +347,10 @@ export class InfoService {
)) as string;
const get = bent(
`https://betteruptime.com/api/v2/monitors/${monitorId}/sla`,
`https://betteruptime.com/api/v2/monitors/${monitorId}/sla?from=${format(
subDays(new Date(), 90),
DATE_FORMAT
)}&to${format(new Date(), DATE_FORMAT)}`,
'GET',
'json',
200,

View File

@ -4,7 +4,7 @@ import {
DEFAULT_LANGUAGE_CODE,
PROPERTY_STRIPE_CONFIG
} from '@ghostfolio/common/config';
import { Subscription as SubscriptionInterface } from '@ghostfolio/common/interfaces/subscription.interface';
import { Subscription as SubscriptionInterface } from '@ghostfolio/common/interfaces';
import { UserWithSettings } from '@ghostfolio/common/types';
import { SubscriptionType } from '@ghostfolio/common/types/subscription-type.type';
import { Injectable, Logger } from '@nestjs/common';

View File

@ -304,21 +304,29 @@ export class UserService {
}
public async deleteUser(where: Prisma.UserWhereUniqueInput): Promise<User> {
await this.prismaService.access.deleteMany({
where: { OR: [{ granteeUserId: where.id }, { userId: where.id }] }
});
try {
await this.prismaService.access.deleteMany({
where: { OR: [{ granteeUserId: where.id }, { userId: where.id }] }
});
} catch {}
await this.prismaService.account.deleteMany({
where: { userId: where.id }
});
try {
await this.prismaService.account.deleteMany({
where: { userId: where.id }
});
} catch {}
await this.prismaService.analytics.delete({
where: { userId: where.id }
});
try {
await this.prismaService.analytics.delete({
where: { userId: where.id }
});
} catch {}
await this.prismaService.order.deleteMany({
where: { userId: where.id }
});
try {
await this.prismaService.order.deleteMany({
where: { userId: where.id }
});
} catch {}
try {
await this.prismaService.settings.delete({

View File

@ -5,11 +5,13 @@ import {
IDataProviderHistoricalResponse,
IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces';
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
import { Granularity } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client';
import bent from 'bent';
import { format, isAfter, isBefore, isSameDay } from 'date-fns';
@Injectable()
export class FinancialModelingPrepService implements DataProviderInterface {
@ -61,9 +63,42 @@ export class FinancialModelingPrepService implements DataProviderInterface {
): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
}> {
return {
[aSymbol]: {}
};
try {
const get = bent(
`${this.URL}/historical-price-full/${aSymbol}?apikey=${this.apiKey}`,
'GET',
'json',
200
);
const { historical } = await get();
const result: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
} = {
[aSymbol]: {}
};
for (const { close, date } of historical) {
if (
(isSameDay(parseDate(date), from) ||
isAfter(parseDate(date), from)) &&
isBefore(parseDate(date), to)
) {
result[aSymbol][date] = {
marketPrice: close
};
}
}
return result;
} catch (error) {
throw new Error(
`Could not get historical market data for ${aSymbol} (${this.getName()}) from ${format(
from,
DATE_FORMAT
)} to ${format(to, DATE_FORMAT)}: [${error.name}] ${error.message}`
);
}
}
public getName(): DataSource {
@ -109,7 +144,32 @@ export class FinancialModelingPrepService implements DataProviderInterface {
}
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
return { items: [] };
let items: LookupItem[] = [];
try {
const get = bent(
`${this.URL}/search?query=${aQuery}&apikey=${this.apiKey}`,
'GET',
'json',
200
);
const result = await get();
items = result.map(({ currency, name, symbol }) => {
return {
// TODO: Add assetClass
// TODO: Add assetSubClass
currency,
name,
symbol,
dataSource: this.getName()
};
});
} catch (error) {
Logger.error(error, 'FinancialModelingPrepService');
}
return { items };
}
private getDataProviderInfo(): DataProviderInfo {

View File

@ -138,8 +138,7 @@ export class YahooFinanceService implements DataProviderInterface {
marketPrice: this.getConvertedValue({
symbol: aSymbol,
value: historicalItem.close
}),
performance: historicalItem.open - historicalItem.close
})
};
}

View File

@ -23,7 +23,6 @@ export interface IOrder {
export interface IDataProviderHistoricalResponse {
marketPrice: number;
performance?: number;
}
export interface IDataProviderResponse {

View File

@ -137,6 +137,13 @@ const routes: Routes = [
'./pages/blog/2023/03/1000-stars-on-github/1000-stars-on-github-page.module'
).then((m) => m.ThousandStarsOnGitHubPageModule)
},
{
path: 'blog/2023/05/unlock-your-financial-potential-with-ghostfolio',
loadChildren: () =>
import(
'./pages/blog/2023/05/unlock-your-financial-potential-with-ghostfolio/unlock-your-financial-potential-with-ghostfolio-page.module'
).then((m) => m.UnlockYourFinancialPotentialWithGhostfolioPageModule)
},
{
path: 'demo',
loadChildren: () =>

View File

@ -143,18 +143,6 @@
<ion-icon name="ellipsis-vertical"></ion-icon>
</button>
<mat-menu #assetProfileActionsMenu="matMenu" xPosition="before">
<button
mat-menu-item
(click)="onGatherSymbol({dataSource: element.dataSource, symbol: element.symbol})"
>
<ng-container i18n>Gather Historical Data</ng-container>
</button>
<button
mat-menu-item
(click)="onGatherProfileDataBySymbol({dataSource: element.dataSource, symbol: element.symbol})"
>
<ng-container i18n>Gather Profile Data</ng-container>
</button>
<button
mat-menu-item
[disabled]="element.activitiesCount !== 0"

View File

@ -10,13 +10,13 @@ import { FormBuilder } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto';
import { AdminService } from '@ghostfolio/client/services/admin.service';
import { DataService } from '@ghostfolio/client/services/data.service';
import {
AdminMarketDataDetails,
EnhancedSymbolProfile,
UniqueAsset
} from '@ghostfolio/common/interfaces';
import { translate } from '@ghostfolio/ui/i18n';
import { MarketData } from '@prisma/client';
import { MarketData, SymbolProfile } from '@prisma/client';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@ -37,9 +37,11 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
symbolMapping: ''
});
public assetSubClass: string;
public benchmarks: Partial<SymbolProfile>[];
public countries: {
[code: string]: { name: string; value: number };
};
public isBenchmark = false;
public marketDataDetails: MarketData[] = [];
public sectors: {
[name: string]: { name: string; value: number };
@ -51,11 +53,14 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef,
@Inject(MAT_DIALOG_DATA) public data: AssetProfileDialogParams,
private dataService: DataService,
public dialogRef: MatDialogRef<AssetProfileDialog>,
private formBuilder: FormBuilder
) {}
public ngOnInit(): void {
this.benchmarks = this.dataService.fetchInfo().benchmarks;
this.initialize();
}
@ -72,6 +77,9 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
this.assetClass = translate(this.assetProfile?.assetClass);
this.assetSubClass = translate(this.assetProfile?.assetSubClass);
this.countries = {};
this.isBenchmark = this.benchmarks.some(({ id }) => {
return id === this.assetProfile.id;
});
this.marketDataDetails = marketData;
this.sectors = {};
@ -128,6 +136,17 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
}
}
public onSetBenchmark({ dataSource, symbol }: UniqueAsset) {
this.dataService
.postBenchmark({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
setTimeout(() => {
window.location.reload();
}, 300);
});
}
public onSubmit() {
let symbolMapping = {};

View File

@ -37,6 +37,13 @@
>
<ng-container i18n>Gather Profile Data</ng-container>
</button>
<button
mat-menu-item
[disabled]="isBenchmark"
(click)="onSetBenchmark({dataSource: data.dataSource, symbol: data.symbol})"
>
<ng-container i18n>Set as Benchmark</ng-container>
</button>
</mat-menu>
</div>

View File

@ -1,10 +1,11 @@
<mat-radio-group
class="text-nowrap"
class="d-block text-nowrap"
[formControl]="option"
(change)="onValueChange()"
>
<mat-radio-button
*ngFor="let option of options"
class="d-inline-flex"
[disabled]="isLoading"
[ngClass]="{ 'cursor-pointer': !isLoading }"
[value]="option.value"

View File

@ -3,8 +3,7 @@ import { environment } from '@ghostfolio/client/../environments/environment';
import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
import { User } from '@ghostfolio/common/interfaces';
import { Statistics } from '@ghostfolio/common/interfaces/statistics.interface';
import { Statistics, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

View File

@ -6,12 +6,26 @@
<p>
Ghostfolio is a lightweight wealth management application for
individuals to keep track of stocks, ETFs or cryptocurrencies and make
solid, data-driven investment decisions. We share aggregated
<a href="https://ghostfol.io/{{ defaultLanguageCode }}/open"
solid, data-driven investment decisions. The source code is fully
available as
<a
href="https://github.com/ghostfolio/ghostfolio"
title="Find Ghostfolio on GitHub"
>open source software</a
>
(OSS) under the
<a
href="https://www.gnu.org/licenses/agpl-3.0.html"
title="GNU Affero General Public License"
>AGPL-3.0 license</a
>
and we share aggregated
<a
href="https://ghostfol.io/{{ defaultLanguageCode }}/open"
title="Open Startup"
>key metrics</a
>
of our platforms performance and the source code is fully available
as open source software (OSS). The project has been initiated by
of the platforms performance. The project has been initiated by
<a href="https://dotsilver.ch" title="Website of Thomas Kaul"
>Thomas Kaul</a
>
@ -130,6 +144,7 @@
<gf-value
size="large"
subLabel="(Last 24 hours)"
[locale]="user?.settings?.locale"
[value]="statistics?.activeUsers1d ?? '-'"
>Active Users</gf-value
>
@ -138,6 +153,7 @@
<gf-value
size="large"
subLabel="(Last 30 days)"
[locale]="user?.settings?.locale"
[value]="statistics?.newUsers30d ?? '-'"
>New Users</gf-value
>
@ -146,6 +162,7 @@
<gf-value
size="large"
subLabel="(Last 30 days)"
[locale]="user?.settings?.locale"
[value]="statistics?.activeUsers30d ?? '-'"
>Active Users</gf-value
>
@ -154,6 +171,7 @@
<a class="d-block" href="https://ghostfolio.slack.com">
<gf-value
size="large"
[locale]="user?.settings?.locale"
[value]="statistics?.slackCommunityUsers ?? '-'"
>Users in Slack community</gf-value
>
@ -166,6 +184,7 @@
>
<gf-value
size="large"
[locale]="user?.settings?.locale"
[value]="statistics?.gitHubContributors ?? '-'"
>Contributors on GitHub</gf-value
>
@ -178,6 +197,7 @@
>
<gf-value
size="large"
[locale]="user?.settings?.locale"
[value]="statistics?.gitHubStargazers ?? '-'"
>Stars on GitHub</gf-value
>
@ -201,7 +221,7 @@
</div>
<div
class="col-md-3 col-xs-12 my-2"
[ngClass]="{ 'offset-md-4': !hasPermissionForBlog }"
[ngClass]="{ 'mx-auto': !hasPermissionForBlog }"
>
<a
class="py-4 w-100"

View File

@ -202,7 +202,10 @@
<li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a>
</li>
<li aria-current="page" class="breadcrumb-item active">
<li
aria-current="page"
class="active breadcrumb-item text-truncate"
>
Hallo Ghostfolio
</li>
</ol>

View File

@ -182,7 +182,10 @@
<li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a>
</li>
<li aria-current="page" class="breadcrumb-item active">
<li
aria-current="page"
class="active breadcrumb-item text-truncate"
>
Hello Ghostfolio
</li>
</ol>

View File

@ -179,7 +179,10 @@
<li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a>
</li>
<li aria-current="page" class="breadcrumb-item active">
<li
aria-current="page"
class="active breadcrumb-item text-truncate"
>
Ghostfolio: First months in Open Source
</li>
</ol>

View File

@ -182,7 +182,10 @@
<li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a>
</li>
<li aria-current="page" class="breadcrumb-item active">
<li
aria-current="page"
class="active breadcrumb-item text-truncate"
>
Ghostfolio meets Internet Identity
</li>
</ol>

View File

@ -208,7 +208,10 @@
<li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a>
</li>
<li aria-current="page" class="breadcrumb-item active">
<li
aria-current="page"
class="active breadcrumb-item text-truncate"
>
How do I get my finances in order?
</li>
</ol>

View File

@ -191,7 +191,10 @@
<li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a>
</li>
<li aria-current="page" class="breadcrumb-item active">
<li
aria-current="page"
class="active breadcrumb-item text-truncate"
>
500 Stars on GitHub
</li>
</ol>

View File

@ -177,7 +177,10 @@
<li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a>
</li>
<li aria-current="page" class="breadcrumb-item active">
<li
aria-current="page"
class="active breadcrumb-item text-truncate"
>
Hacktoberfest 2022
</li>
</ol>

View File

@ -137,7 +137,10 @@
<li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a>
</li>
<li aria-current="page" class="breadcrumb-item active">
<li
aria-current="page"
class="active breadcrumb-item text-truncate"
>
Black Friday 2022
</li>
</ol>

View File

@ -167,7 +167,10 @@
<li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a>
</li>
<li aria-current="page" class="breadcrumb-item active">
<li
aria-current="page"
class="active breadcrumb-item text-truncate"
>
The importance of tracking your personal finances
</li>
</ol>

View File

@ -177,7 +177,10 @@
<li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a>
</li>
<li aria-current="page" class="breadcrumb-item active">
<li
aria-current="page"
class="active breadcrumb-item text-truncate"
>
Ghostfolio auf Sackgeld.com vorgestellt
</li>
</ol>

View File

@ -199,7 +199,10 @@
<li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a>
</li>
<li aria-current="page" class="breadcrumb-item active">
<li
aria-current="page"
class="active breadcrumb-item text-truncate"
>
Ghostfolio meets Umbrel
</li>
</ol>

View File

@ -244,7 +244,10 @@
<li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a>
</li>
<li aria-current="page" class="breadcrumb-item active">
<li
aria-current="page"
class="active breadcrumb-item text-truncate"
>
Ghostfolio reaches 1000 Stars on GitHub
</li>
</ol>

View File

@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { UnlockYourFinancialPotentialWithGhostfolioPageComponent } from './unlock-your-financial-potential-with-ghostfolio-page.component';
const routes: Routes = [
{
canActivate: [AuthGuard],
component: UnlockYourFinancialPotentialWithGhostfolioPageComponent,
path: '',
title: 'Unlock your Financial Potential with Ghostfolio'
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class UnlockYourFinancialPotentialWithGhostfolioRoutingModule {}

View File

@ -0,0 +1,9 @@
import { Component } from '@angular/core';
@Component({
host: { class: 'page' },
selector: 'gf-unlock-your-financial-potential-with-ghostfolio-page',
styleUrls: ['./unlock-your-financial-potential-with-ghostfolio-page.scss'],
templateUrl: './unlock-your-financial-potential-with-ghostfolio-page.html'
})
export class UnlockYourFinancialPotentialWithGhostfolioPageComponent {}

View File

@ -0,0 +1,244 @@
<div class="blog container">
<div class="row">
<div class="col-md-8 offset-md-2">
<article>
<div class="mb-4 text-center">
<h1 class="mb-1">Unlock your Financial Potential with Ghostfolio</h1>
<div class="mb-3 text-muted"><small>2023-05-20</small></div>
<img
alt="Unlock your financial potential with Ghostfolio Teaser"
class="border rounded w-100"
src="../assets/images/blog/20230520.jpg"
title="Unlock your financial potential with Ghostfolio"
/>
</div>
<section class="mb-4">
<p>
Managing personal finances effectively is crucial for those striving
for a secure future and financial independence. In todays digital
age, having a reliable wealth management software can greatly
simplify the process. Ghostfolio is a powerful
<a
href="https://github.com/ghostfolio/ghostfolio"
title="Find Ghostfolio on GitHub"
>open source solution</a
>
for individuals trading stocks, ETFs, or cryptocurrencies on
multiple platforms. This article explores the key reasons why
Ghostfolio is the ideal choice for those embracing diversification,
pursuing a buy & hold strategy, and seeking portfolio insights while
valuing privacy.
</p>
</section>
<section class="mb-4">
<h2 class="h4">Effortless Management for Multi-Platform Investors</h2>
<p>
Ghostfolio offers a holistic solution to efficiently monitor and
manage investment portfolios across multiple platforms. By
consolidating data from various accounts, Ghostfolio eliminates the
need to switch between platforms, saving users valuable time and
effort.
</p>
</section>
<section class="mb-4">
<h2 class="h4">Empowering Buy & Hold Strategies</h2>
<p>
For those committed to a
<a [routerLink]="['/resources']">buy & hold strategy</a>, Ghostfolio
provides an intuitive interface to monitor long-term investments.
Users can track performance over time, gaining insights into
portfolio growth and stability. With strong visualizations and
reporting <a [routerLink]="['/features']">features</a>, Ghostfolio
equips users to make well-informed decisions aligned with their
long-term investment goals.
</p>
</section>
<section class="mb-4">
<h2 class="h4">Deep Portfolio Insights</h2>
<p>
Understanding portfolio composition is vital for making informed
financial decisions. Ghostfolio provides comprehensive insights into
asset allocation, sector exposure, geographical diversification, and
individual asset performance. These detailed analytics empower users
to assess portfolio strengths and weaknesses, making necessary
adjustments to optimize their allocation.
</p>
</section>
<section class="mb-4">
<h2 class="h4">Privacy and Data Ownership</h2>
<p>
In the age of growing data security concerns, Ghostfolio sets itself
apart by giving the highest priority to privacy and data ownership.
As an open-source software, Ghostfolio ensures that users retain
complete control over their financial data. By eliminating the need
to trust third-party platforms with sensitive information,
Ghostfolio offers peace of mind to those who value privacy and data
security.
</p>
</section>
<section class="mb-4">
<h2 class="h4">Streamlined Minimalism for Financial Efficiency</h2>
<p>
Ghostfolio embraces a lightweight approach to personal finance
management, focusing on essential features without overwhelming
users. Its streamlined user interface and clean design provide a
seamless and clutter-free experience. This minimalist approach
enhances user satisfaction and boosts efficiency by eliminating
distractions and simplifying the financial management process.
</p>
</section>
<section class="mb-4">
<h2 class="h4">Driving Financial Independence (FIRE)</h2>
<p>
Achieving
<a [routerLink]="['/resources']">financial independence</a>
including early retirement (FIRE) requires careful planning,
monitoring, and forecasting. Ghostfolios robust features equip
individuals with tools to analyze, optimize and simulate investment
strategies. By providing insights, performance tracking, and
portfolio analysis, Ghostfolio serves as a valuable companion in the
pursuit of financial freedom.
</p>
</section>
<section class="mb-4">
<h2 class="h4">Farewell to Spreadsheet Hassles</h2>
<p>
While spreadsheets have traditionally been used to manage personal
finances, they can be time-consuming and prone to errors. Ghostfolio
offers a user-friendly alternative by automating data aggregation,
analysis, and reporting. Users can bid farewell to manual data entry
and complex formulas, relying instead on Ghostfolios user-friendly
and intuitive interface to efficiently manage their finances.
</p>
</section>
<section class="mb-4">
<h2 class="h4">Your Path to Financial Success with Ghostfolio</h2>
<p>
Ghostfolio, the open-source personal finance software, provides a
wide range of benefits for individuals involved in trading stocks,
ETFs, or cryptocurrencies. Whether you are pursuing a buy & hold
strategy, seeking valuable portfolio insights, or diversifying
financial resources while prioritizing privacy and data ownership,
Ghostfolio proves to be an invaluable tool on your journey towards
unlocking your financial potential. Say goodbye to spreadsheets and
embrace the power of Ghostfolio for simplified, secure, and
successful financial management.
</p>
</section>
<section class="mb-4 py-3">
<h2 class="h4 mb-0 text-center">
Would you like to <strong>unlock</strong> your
<strong>financial potential</strong>?
</h2>
<p class="lead mb-2 text-center">
Ghostfolio empowers you to manage your personal finances
effectively.
</p>
<div class="text-center">
<a color="primary" href="https://ghostfol.io" mat-flat-button>
Get Started
</a>
</div>
</section>
<section class="mb-4">
<ul class="list-inline">
<li class="list-inline-item">
<span class="badge badge-light">App</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Analysis</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Assets</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Budgeting</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Buy & Hold</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Cryptocurrencies</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Diversification</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">ETFs</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Finance</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Fintech</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">FIRE</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Ghostfolio</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Investment</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Management</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Minimalism</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Monitoring</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Open Source</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Personal Finance</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Planning</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Portfolio Tracker</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Privacy</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Retirement</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Software</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Spreadsheet</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Stock</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Strategy</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Wealth</span>
</li>
</ul>
</section>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a i18n [routerLink]="['/blog']">Blog</a>
</li>
<li
aria-current="page"
class="active breadcrumb-item text-truncate"
>
Unlock your Financial Potential with Ghostfolio
</li>
</ol>
</nav>
</article>
</div>
</div>
</div>

View File

@ -0,0 +1,19 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
import { UnlockYourFinancialPotentialWithGhostfolioRoutingModule } from './unlock-your-financial-potential-with-ghostfolio-page-routing.module';
import { UnlockYourFinancialPotentialWithGhostfolioPageComponent } from './unlock-your-financial-potential-with-ghostfolio-page.component';
@NgModule({
declarations: [UnlockYourFinancialPotentialWithGhostfolioPageComponent],
imports: [
CommonModule,
MatButtonModule,
RouterModule,
UnlockYourFinancialPotentialWithGhostfolioRoutingModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class UnlockYourFinancialPotentialWithGhostfolioPageModule {}

View File

@ -2,6 +2,32 @@
<div class="mb-5 row">
<div class="col">
<h3 class="d-none d-sm-block mb-3 text-center" i18n>Blog</h3>
<mat-card appearance="outlined" class="mb-3">
<mat-card-content>
<div class="container p-0">
<div class="flex-nowrap no-gutters row">
<a
class="d-flex overflow-hidden w-100"
href="../en/blog/2023/05/unlock-your-financial-potential-with-ghostfolio"
>
<div class="flex-grow-1 overflow-hidden">
<div class="h6 m-0 text-truncate">
Unlock your Financial Potential with Ghostfolio
</div>
<div class="d-flex text-muted">2023-05-20</div>
</div>
<div class="align-items-center d-flex">
<ion-icon
class="chevron text-muted"
name="chevron-forward-outline"
size="small"
></ion-icon>
</div>
</a>
</div>
</div>
</mat-card-content>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-content>
<div class="container p-0">

View File

@ -1,6 +1,6 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { DataService } from '@ghostfolio/client/services/data.service';
import { Statistics } from '@ghostfolio/common/interfaces/statistics.interface';
import { Statistics } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { format } from 'date-fns';
import { DeviceDetectorService } from 'ngx-device-detector';

View File

@ -1,7 +1,8 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { DataService } from '@ghostfolio/client/services/data.service';
import { Statistics } from '@ghostfolio/common/interfaces/statistics.interface';
import { Subject } from 'rxjs';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { Statistics, User } from '@ghostfolio/common/interfaces';
import { Subject, takeUntil } from 'rxjs';
@Component({
host: { class: 'page' },
@ -11,16 +12,31 @@ import { Subject } from 'rxjs';
})
export class OpenPageComponent implements OnDestroy, OnInit {
public statistics: Statistics;
public user: User;
private unsubscribeSubject = new Subject<void>();
public constructor(private dataService: DataService) {
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private userService: UserService
) {
const { statistics } = this.dataService.fetchInfo();
this.statistics = statistics;
}
public ngOnInit() {}
public ngOnInit() {
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
if (state?.user) {
this.user = state.user;
this.changeDetectorRef.markForCheck();
}
});
}
public ngOnDestroy() {
this.unsubscribeSubject.next();

View File

@ -4,15 +4,21 @@
<h3 class="d-none d-sm-block mb-3 text-center">Open Startup</h3>
<div class="intro-container">
<p>
At Ghostfolio, transparency is at the core of our values. We openly
share aggregated key metrics of our platforms performance and publish
At Ghostfolio, transparency is at the core of our values. We publish
the source code as
<a
href="https://github.com/ghostfolio/ghostfolio"
title="Contributors to Ghostfolio"
title="Find Ghostfolio on GitHub"
>open source software</a
>
(OSS).
(OSS) under the
<a
href="https://www.gnu.org/licenses/agpl-3.0.html"
title="GNU Affero General Public License"
>AGPL-3.0 license</a
>
and we openly share aggregated key metrics of the platforms
performance.
</p>
</div>
</div>
@ -27,6 +33,7 @@
<gf-value
size="large"
subLabel="(Last 24 hours)"
[locale]="user?.settings?.locale"
[value]="statistics?.activeUsers1d ?? '-'"
>Active Users</gf-value
>
@ -35,6 +42,7 @@
<gf-value
size="large"
subLabel="(Last 30 days)"
[locale]="user?.settings?.locale"
[value]="statistics?.newUsers30d ?? '-'"
>New Users</gf-value
>
@ -43,6 +51,7 @@
<gf-value
size="large"
subLabel="(Last 30 days)"
[locale]="user?.settings?.locale"
[value]="statistics?.activeUsers30d ?? '-'"
>Active Users</gf-value
>
@ -51,6 +60,7 @@
<a class="d-block" href="https://ghostfolio.slack.com">
<gf-value
size="large"
[locale]="user?.settings?.locale"
[value]="statistics?.slackCommunityUsers ?? '-'"
>Users in Slack community</gf-value
>
@ -63,6 +73,7 @@
>
<gf-value
size="large"
[locale]="user?.settings?.locale"
[value]="statistics?.gitHubContributors ?? '-'"
>Contributors on GitHub</gf-value
>
@ -75,6 +86,7 @@
>
<gf-value
size="large"
[locale]="user?.settings?.locale"
[value]="statistics?.gitHubStargazers ?? '-'"
>Stars on GitHub</gf-value
>
@ -87,6 +99,7 @@
>
<gf-value
size="large"
[locale]="user?.settings?.locale"
[value]="statistics?.dockerHubPulls ?? '-'"
>Pulls on Docker Hub</gf-value
>
@ -96,7 +109,9 @@
<a class="d-block" href="https://status.ghostfol.io">
<gf-value
size="large"
subLabel="(Last 90 days)"
[isPercent]="true"
[locale]="user?.settings?.locale"
[precision]="2"
[value]="statistics?.uptime ?? '-'"
>Uptime</gf-value

View File

@ -405,6 +405,10 @@ export class DataService {
return this.http.post<OrderModel>(`/api/v1/account`, aAccount);
}
public postBenchmark(benchmark: UniqueAsset) {
return this.http.post(`/api/v1/benchmark`, benchmark);
}
public postOrder(aOrder: CreateOrderDto) {
return this.http.post<OrderModel>(`/api/v1/order`, aOrder);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

View File

@ -6,106 +6,110 @@
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
<url>
<loc>https://ghostfol.io</loc>
<lastmod>2023-05-15T00:00:00+00:00</lastmod>
<lastmod>2023-05-20T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/de/blog</loc>
<lastmod>2023-05-15T00:00:00+00:00</lastmod>
<lastmod>2023-05-20T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/de/blog/2021/07/hallo-ghostfolio</loc>
<lastmod>2023-05-15T00:00:00+00:00</lastmod>
<lastmod>2023-05-20T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/de/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt</loc>
<lastmod>2023-05-15T00:00:00+00:00</lastmod>
<lastmod>2023-05-20T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/de/pricing</loc>
<lastmod>2023-05-15T00:00:00+00:00</lastmod>
<lastmod>2023-05-20T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/about</loc>
<lastmod>2023-05-15T00:00:00+00:00</lastmod>
<lastmod>2023-05-20T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/about/changelog</loc>
<lastmod>2023-05-15T00:00:00+00:00</lastmod>
<lastmod>2023-05-20T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/blog</loc>
<lastmod>2023-05-15T00:00:00+00:00</lastmod>
<lastmod>2023-05-20T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/blog/2021/07/hello-ghostfolio</loc>
<lastmod>2023-05-15T00:00:00+00:00</lastmod>
<lastmod>2023-05-20T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/blog/2022/01/ghostfolio-first-months-in-open-source</loc>
<lastmod>2023-05-15T00:00:00+00:00</lastmod>
<lastmod>2023-05-20T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/blog/2022/07/ghostfolio-meets-internet-identity</loc>
<lastmod>2023-05-15T00:00:00+00:00</lastmod>
<lastmod>2023-05-20T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/blog/2022/07/how-do-i-get-my-finances-in-order</loc>
<lastmod>2023-05-15T00:00:00+00:00</lastmod>
<lastmod>2023-05-20T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/blog/2022/08/500-stars-on-github</loc>
<lastmod>2023-05-15T00:00:00+00:00</lastmod>
<lastmod>2023-05-20T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/blog/2022/10/hacktoberfest-2022</loc>
<lastmod>2023-05-15T00:00:00+00:00</lastmod>
<lastmod>2023-05-20T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/blog/2022/11/black-friday-2022</loc>
<lastmod>2023-05-15T00:00:00+00:00</lastmod>
<lastmod>2023-05-20T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/blog/2022/12/the-importance-of-tracking-your-personal-finances</loc>
<lastmod>2023-05-15T00:00:00+00:00</lastmod>
<lastmod>2023-05-20T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/blog/2023/02/ghostfolio-meets-umbrel</loc>
<lastmod>2023-05-15T00:00:00+00:00</lastmod>
<lastmod>2023-05-20T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/blog/2023/03/ghostfolio-reaches-1000-stars-on-github</loc>
<lastmod>2023-05-15T00:00:00+00:00</lastmod>
<lastmod>2023-05-20T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/blog/2023/05/unlock-your-financial-potential-with-ghostfolio</loc>
<lastmod>2023-05-20T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/demo</loc>
<lastmod>2023-05-15T00:00:00+00:00</lastmod>
<lastmod>2023-05-20T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/faq</loc>
<lastmod>2023-05-15T00:00:00+00:00</lastmod>
<lastmod>2023-05-20T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/features</loc>
<lastmod>2023-05-15T00:00:00+00:00</lastmod>
<lastmod>2023-05-20T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/markets</loc>
<lastmod>2023-05-15T00:00:00+00:00</lastmod>
<lastmod>2023-05-20T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/open</loc>
<lastmod>2023-05-15T00:00:00+00:00</lastmod>
<lastmod>2023-05-20T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/pricing</loc>
<lastmod>2023-05-15T00:00:00+00:00</lastmod>
<lastmod>2023-05-20T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/register</loc>
<lastmod>2023-05-15T00:00:00+00:00</lastmod>
<lastmod>2023-05-20T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/resources</loc>
<lastmod>2023-05-15T00:00:00+00:00</lastmod>
<lastmod>2023-05-20T00:00:00+00:00</lastmod>
</url>
</urlset>

View File

@ -327,10 +327,13 @@ ngx-skeleton-loader {
.breadcrumb {
background-color: unset;
flex-wrap: nowrap;
padding: unset;
}
.breadcrumb-item {
flex-wrap: nowrap;
&.active {
color: unset;
}

View File

@ -86,6 +86,7 @@ $gf-theme-default: mat.define-light-theme(
);
@include mat.all-component-themes($gf-theme-default);
@include mat.button-density(0);
@include mat.table-density(-1);
// Create dark theme
$gf-theme-dark: mat.define-dark-theme(
@ -101,6 +102,7 @@ $gf-theme-dark: mat.define-dark-theme(
.is-dark-theme {
@include mat.all-component-colors($gf-theme-dark);
@include mat.button-density(0);
@include mat.table-density(-1);
}
:root {

View File

@ -0,0 +1,3 @@
export interface BenchmarkProperty {
symbolProfileId: string;
}

View File

@ -8,6 +8,7 @@ import {
AdminMarketDataItem
} from './admin-market-data.interface';
import { BenchmarkMarketDataDetails } from './benchmark-market-data-details.interface';
import { BenchmarkProperty } from './benchmark-property.interface';
import { Benchmark } from './benchmark.interface';
import { Coupon } from './coupon.interface';
import { DataProviderInfo } from './data-provider-info.interface';
@ -37,6 +38,8 @@ import { ImportResponse } from './responses/import-response.interface';
import { OAuthResponse } from './responses/oauth-response.interface';
import { PortfolioPerformanceResponse } from './responses/portfolio-performance-response.interface';
import { ScraperConfiguration } from './scraper-configuration.interface';
import { Statistics } from './statistics.interface';
import { Subscription } from './subscription.interface';
import { TimelinePosition } from './timeline-position.interface';
import { UniqueAsset } from './unique-asset.interface';
import { UserSettings } from './user-settings.interface';
@ -52,6 +55,7 @@ export {
AdminMarketDataItem,
Benchmark,
BenchmarkMarketDataDetails,
BenchmarkProperty,
BenchmarkResponse,
Coupon,
DataProviderInfo,
@ -80,6 +84,8 @@ export {
Position,
ResponseError,
ScraperConfiguration,
Statistics,
Subscription,
TimelinePosition,
UniqueAsset,
User,

View File

@ -100,38 +100,42 @@ export class PortfolioProportionChartComponent
};
Object.keys(this.positions).forEach((symbol) => {
if (this.positions[symbol][this.keys[0]]) {
if (chartData[this.positions[symbol][this.keys[0]]]) {
chartData[this.positions[symbol][this.keys[0]]].value = chartData[
this.positions[symbol][this.keys[0]]
].value.plus(this.positions[symbol].value);
if (this.positions[symbol][this.keys[0]].toUpperCase()) {
if (chartData[this.positions[symbol][this.keys[0]].toUpperCase()]) {
chartData[this.positions[symbol][this.keys[0]].toUpperCase()].value =
chartData[
this.positions[symbol][this.keys[0]].toUpperCase()
].value.plus(this.positions[symbol].value);
if (
chartData[this.positions[symbol][this.keys[0]]].subCategory[
this.positions[symbol][this.keys[1]]
]
chartData[this.positions[symbol][this.keys[0]].toUpperCase()]
.subCategory[this.positions[symbol][this.keys[1]]]
) {
chartData[this.positions[symbol][this.keys[0]]].subCategory[
this.positions[symbol][this.keys[1]]
].value = chartData[
this.positions[symbol][this.keys[0]]
].subCategory[this.positions[symbol][this.keys[1]]].value.plus(
this.positions[symbol].value
);
chartData[
this.positions[symbol][this.keys[0]].toUpperCase()
].subCategory[this.positions[symbol][this.keys[1]]].value =
chartData[
this.positions[symbol][this.keys[0]].toUpperCase()
].subCategory[this.positions[symbol][this.keys[1]]].value.plus(
this.positions[symbol].value
);
} else {
chartData[this.positions[symbol][this.keys[0]]].subCategory[
this.positions[symbol][this.keys[1]] ?? UNKNOWN_KEY
] = { value: new Big(this.positions[symbol].value) };
chartData[
this.positions[symbol][this.keys[0]].toUpperCase()
].subCategory[this.positions[symbol][this.keys[1]] ?? UNKNOWN_KEY] =
{ value: new Big(this.positions[symbol].value) };
}
} else {
chartData[this.positions[symbol][this.keys[0]]] = {
name: this.positions[symbol].name,
chartData[this.positions[symbol][this.keys[0]].toUpperCase()] = {
name: this.positions[symbol][this.keys[0]],
subCategory: {},
value: new Big(this.positions[symbol].value ?? 0)
};
if (this.positions[symbol][this.keys[1]]) {
chartData[this.positions[symbol][this.keys[0]]].subCategory = {
chartData[
this.positions[symbol][this.keys[0]].toUpperCase()
].subCategory = {
[this.positions[symbol][this.keys[1]]]: {
value: new Big(this.positions[symbol].value)
}
@ -232,8 +236,8 @@ export class PortfolioProportionChartComponent
}
];
let labels = chartDataSorted.map(([label]) => {
return label;
let labels = chartDataSorted.map(([symbol, { name }]) => {
return name;
});
if (this.keys[1]) {

View File

@ -21,7 +21,7 @@ export class ValueComponent implements OnChanges {
@Input() isCurrency = false;
@Input() isDate = false;
@Input() isPercent = false;
@Input() locale = getLocale();
@Input() locale: string | undefined;
@Input() position = '';
@Input() precision: number | undefined;
@Input() size: 'large' | 'medium' | 'small' = 'small';
@ -92,7 +92,7 @@ export class ValueComponent implements OnChanges {
});
} catch {}
} else {
this.formattedValue = this.value?.toString();
this.formattedValue = this.value?.toLocaleString(this.locale);
}
if (this.isAbsolute) {
@ -128,6 +128,11 @@ export class ValueComponent implements OnChanges {
this.formattedValue = '';
this.isNumber = false;
this.isString = false;
if (!this.locale) {
this.locale = getLocale();
}
this.useAbsoluteValue = false;
}
}

View File

@ -1,6 +1,6 @@
{
"name": "ghostfolio",
"version": "1.270.1",
"version": "1.272.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"scripts": {
@ -106,7 +106,7 @@
"envalid": "7.3.1",
"google-spreadsheet": "3.2.0",
"http-status-codes": "2.2.0",
"ionicons": "6.1.2",
"ionicons": "7.1.0",
"lodash": "4.17.21",
"marked": "4.2.12",
"ms": "3.0.0-canary.1",

View File

@ -11221,10 +11221,10 @@ invert-kv@^2.0.0:
resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02"
integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==
ionicons@6.1.2:
version "6.1.2"
resolved "https://registry.yarnpkg.com/ionicons/-/ionicons-6.1.2.tgz#805ed1ce272b653ac07a85f83514e5afa2c9677d"
integrity sha512-EL3jjlUzjPo8h2PfI+BUEjVMF9weSfLAFriNlk9pHFMTJq+7G12sAJBZ3AnRN8nTWA2pOS279PvFIWS3hbat+w==
ionicons@7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/ionicons/-/ionicons-7.1.0.tgz#25daa91345acedcb0f4fb7da670f5aff2e1f266a"
integrity sha512-iE4GuEdEHARJpp0sWL7WJZCzNCf5VxpNRhAjW0fLnZPnNL5qZOJUcfup2Z2Ty7Jk8Q5hacrHfGEB1lCwOdXqGg==
dependencies:
"@stencil/core" "^2.18.0"