Browse Source

Merge branch 'main' into bugfix/creation-of-custom-asset-profiles-with-ghostfolio-prefix

pull/8070/head
Thomas Kaul 6 days ago
committed by GitHub
parent
commit
78eb60b509
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 21
      CHANGELOG.md
  2. 13
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  3. 44
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.spec.ts
  4. 26
      apps/api/src/app/user/user.controller.ts
  5. 14
      apps/api/src/app/user/user.service.ts
  6. 9
      apps/api/src/assets/cryptocurrencies/cryptocurrencies.json
  7. 1
      apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts
  8. 236
      apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html
  9. 20
      apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
  10. 14
      apps/client/src/app/components/user-account-settings/user-account-settings.html
  11. 59
      apps/client/src/app/pages/faq/overview/faq-overview-page.html
  12. 48
      apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
  13. 16
      apps/client/src/app/pages/portfolio/analysis/analysis-page.html
  14. 8
      apps/client/src/app/pages/portfolio/analysis/analysis-page.scss
  15. 12
      apps/client/src/app/pages/resources/personal-finance-tools/interfaces/interfaces.ts
  16. 39
      apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
  17. 32
      apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
  18. 106
      apps/client/src/locales/messages.es.xlf
  19. 2
      libs/common/src/lib/config.ts
  20. 5
      libs/common/src/lib/dtos/delete-own-user.dto.ts
  21. 90
      libs/common/src/lib/personal-finance-tools.ts
  22. 27
      libs/ui/src/lib/account-selector/account-selector.component.stories.ts
  23. 10
      libs/ui/src/lib/accounts-table/accounts-table.component.stories.ts
  24. 10
      libs/ui/src/lib/activities-table/activities-table.component.stories.ts
  25. 5
      libs/ui/src/lib/entity-logo/entity-logo-image-source.service.ts
  26. 46
      libs/ui/src/lib/mocks/entity-logo-image-source.service.mock.ts
  27. 4
      package-lock.json
  28. 2
      package.json
  29. 2
      prisma/migrations/20261005010000_removed_index_for_data_source_from_market_data/migration.sql
  30. 1
      prisma/schema.prisma

21
CHANGELOG.md

@ -11,6 +11,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed an issue where a user could create a custom asset profile with a symbol of the prefix `GF_` - Fixed an issue where a user could create a custom asset profile with a symbol of the prefix `GF_`
## 3.82.0 - 2026-10-09
### Added
- Extended the content of the _General_ section by information about the import of activities from _Interactive Brokers_ on the Frequently Asked Questions (FAQ) page
### Changed
- Extended the user account deletion flow in the user settings of the user account page to users without a _Security Token_ within 14 days after the registration
- Improved the _Storybook_ stories of the account selector, accounts table, activities table and holdings table components
- Removed an unnecessary index from the market data database table
- Refreshed the cryptocurrencies list
- Improved the language localization for Spanish (`es`)
### Fixed
- Fixed the missing list numbers in the top and bottom holdings on the analysis page in Safari
- Fixed the missing first data point in the portfolio evolution chart on the analysis page
- Fixed the end date of calendar year date ranges in the portfolio performance calculation for instances in time zones with a negative UTC offset
- Fixed the missing mapping for Korea in the country weightings of the _Financial Modeling Prep_ service
## 3.81.0 - 2026-10-07 ## 3.81.0 - 2026-10-07
### Changed ### Changed

13
apps/api/src/app/portfolio/calculator/portfolio-calculator.ts

@ -1407,7 +1407,7 @@ export abstract class PortfolioCalculator {
return this.snapshot.totalLiabilitiesWithCurrencyEffect; return this.snapshot.totalLiabilitiesWithCurrencyEffect;
} }
public async getPerformance({ end, start }) { public async getPerformance({ end, start }: { end: Date; start: Date }) {
await this.snapshotPromise; await this.snapshotPromise;
const { historicalData } = this.snapshot; const { historicalData } = this.snapshot;
@ -1418,14 +1418,13 @@ export abstract class PortfolioCalculator {
let netPerformanceAtStartDate: number; let netPerformanceAtStartDate: number;
let netPerformanceWithCurrencyEffectAtStartDate: number; let netPerformanceWithCurrencyEffectAtStartDate: number;
const startDate = isEqual(start, endOfDay(start)) const endDate = this.toChartDate(end);
? resetHours(start) const startDate = this.toChartDate(start);
: start;
for (const historicalDataItem of historicalData) { for (const historicalDataItem of historicalData) {
const date = resetHours(parseDate(historicalDataItem.date)); const date = resetHours(parseDate(historicalDataItem.date));
if (!isBefore(date, startDate) && !isAfter(date, end)) { if (!isBefore(date, startDate) && !isAfter(date, endDate)) {
// Take the values at the start date from the first day of the date // Take the values at the start date from the first day of the date
// range // range
if (historicalDataItemsOfDateRange.length === 0) { if (historicalDataItemsOfDateRange.length === 0) {
@ -1915,4 +1914,8 @@ export abstract class PortfolioCalculator {
await this.initialize(attempt + 1); await this.initialize(attempt + 1);
} }
} }
private toChartDate(aDate: Date) {
return isEqual(aDate, endOfDay(aDate)) ? resetHours(aDate) : aDate;
}
} }

44
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.spec.ts

@ -1,3 +1,7 @@
/**
* @jest-environment <rootDir>/jest-environment-tz.js
* @jest-environment-options {"timeZone": "America/New_York"}
*/
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
import { userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
@ -9,6 +13,7 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/con
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper';
import { HistoricalDataItem } from '@ghostfolio/common/interfaces'; import { HistoricalDataItem } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
@ -183,4 +188,43 @@ describe('PortfolioCalculator', () => {
).toBeCloseTo(0.23, 10); ).toBeCloseTo(0.23, 10);
}); });
}); });
describe('get performance', () => {
it('with a calendar year date range', async () => {
const snapshot = await portfolioCalculator.getSnapshot();
snapshot.historicalData = [
getHistoricalDataItem({
date: '2016-12-31',
investment: 100,
netPerformance: 0
}),
getHistoricalDataItem({
date: '2017-12-31',
investment: 100,
netPerformance: 10
}),
getHistoricalDataItem({
date: '2018-01-01',
investment: 100,
netPerformance: 20
})
];
const { endDate, startDate } = getIntervalFromDateRange({
dateRange: '2017'
});
const { chart } = await portfolioCalculator.getPerformance({
end: endDate,
start: startDate
});
expect(
chart.map(({ date }) => {
return date;
})
).toEqual(['2016-12-31', '2017-12-31']);
});
});
}); });

26
apps/api/src/app/user/user.controller.ts

@ -71,14 +71,21 @@ export class UserController {
@Delete() @Delete()
@HasPermission(permissions.deleteOwnUser) @HasPermission(permissions.deleteOwnUser)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard, ImpersonationGuard)
public async deleteOwnUser( public async deleteOwnUser(
@Body() data: DeleteOwnUserDto @Body() data: DeleteOwnUserDto,
@Impersonation() { isActive }: ImpersonationContext
): Promise<UserModel> { ): Promise<UserModel> {
const user = await this.validateAccessToken( const user = this.request.user;
data.accessToken,
this.request.user.id if (user.provider === 'ANONYMOUS') {
); await this.validateAccessToken(data.accessToken, user.id);
} else if (isActive) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.userService.deleteUser({ return this.userService.deleteUser({
id: user.id id: user.id
@ -260,6 +267,13 @@ export class UserController {
accessToken: string, accessToken: string,
userId: string userId: string
): Promise<UserModel> { ): Promise<UserModel> {
if (!accessToken) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const hashedAccessToken = this.userService.createAccessToken({ const hashedAccessToken = this.userService.createAccessToken({
password: accessToken, password: accessToken,
salt: this.configurationService.get('ACCESS_TOKEN_SALT') salt: this.configurationService.get('ACCESS_TOKEN_SALT')

14
apps/api/src/app/user/user.service.ts

@ -14,6 +14,7 @@ import {
DEFAULT_CURRENCY, DEFAULT_CURRENCY,
DEFAULT_DATE_RANGE, DEFAULT_DATE_RANGE,
DEFAULT_LOCALE, DEFAULT_LOCALE,
DELETE_OWN_USER_PERIOD,
PROPERTY_API_KEY_GHOSTFOLIO, PROPERTY_API_KEY_GHOSTFOLIO,
PROPERTY_IS_READ_ONLY_MODE, PROPERTY_IS_READ_ONLY_MODE,
PROPERTY_MAX_DAILY_REQUESTS, PROPERTY_MAX_DAILY_REQUESTS,
@ -45,7 +46,7 @@ import { Injectable, Logger } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
import { InjectThrottlerStorage, ThrottlerStorage } from '@nestjs/throttler'; import { InjectThrottlerStorage, ThrottlerStorage } from '@nestjs/throttler';
import { Prisma, Role, User } from '@prisma/client'; import { Prisma, Role, User } from '@prisma/client';
import { differenceInDays, subDays } from 'date-fns'; import { addMilliseconds, differenceInDays, isBefore, subDays } from 'date-fns';
import { isNil, without } from 'lodash-es'; import { isNil, without } from 'lodash-es';
import { createHmac } from 'node:crypto'; import { createHmac } from 'node:crypto';
@ -438,6 +439,17 @@ export class UserService {
currentPermissions.push(permissions.enableSubscriptionInterstitial); currentPermissions.push(permissions.enableSubscriptionInterstitial);
} }
if (
!hasRole(user, Role.DEMO) &&
user.provider !== 'ANONYMOUS' &&
isBefore(
new Date(),
addMilliseconds(user.createdAt, DELETE_OWN_USER_PERIOD)
)
) {
currentPermissions.push(permissions.deleteOwnUser);
}
currentPermissions = without( currentPermissions = without(
currentPermissions, currentPermissions,
permissions.accessHoldingsChart, permissions.accessHoldingsChart,

9
apps/api/src/assets/cryptocurrencies/cryptocurrencies.json

@ -3367,6 +3367,7 @@
"CARV": "CARV", "CARV": "CARV",
"CAS": "Cashaa", "CAS": "Cashaa",
"CASH": "Litecash", "CASH": "Litecash",
"CASHCAT": "Cash Cat",
"CASHCOIN": "CashCoin", "CASHCOIN": "CashCoin",
"CASHIO": "Cashio Dollar", "CASHIO": "Cashio Dollar",
"CASHLY": "Cashly", "CASHLY": "Cashly",
@ -10790,7 +10791,7 @@
"MARS": "Marscoin", "MARS": "Marscoin",
"MARS4": "MARS4", "MARS4": "MARS4",
"MARSC": "MarsCoin", "MARSC": "MarsCoin",
"MARSCOIN": "Marscoin", "MARSCOIN": "MarsCoin",
"MARSERC": "Mars", "MARSERC": "Mars",
"MARSH": "UnMarshal", "MARSH": "UnMarshal",
"MARSMI": "MarsMi", "MARSMI": "MarsMi",
@ -13919,6 +13920,7 @@
"PONKE": "Ponke", "PONKE": "Ponke",
"PONKEBNB": "Ponke BNB", "PONKEBNB": "Ponke BNB",
"PONKEI": "Chinese Ponkei the Original", "PONKEI": "Chinese Ponkei the Original",
"PONS": "Pons",
"PONTEM": "Pontem Liquidswap", "PONTEM": "Pontem Liquidswap",
"PONYO": "Ponyo Impact", "PONYO": "Ponyo Impact",
"PONZI": "PonziCoin", "PONZI": "PonziCoin",
@ -14660,6 +14662,7 @@
"RECORD": "Music Protocol", "RECORD": "Music Protocol",
"RECT": "ReflectionAI", "RECT": "ReflectionAI",
"RED": "RED", "RED": "RED",
"RED21707": "RedStone",
"REDC": "RedCab", "REDC": "RedCab",
"REDCO": "Redcoin", "REDCO": "Redcoin",
"REDDIT": "Reddit", "REDDIT": "Reddit",
@ -16488,7 +16491,7 @@
"SQUAWK": "Squawk", "SQUAWK": "Squawk",
"SQUEEZER": "Squeezer", "SQUEEZER": "Squeezer",
"SQUIBONK": "SQUIBONK", "SQUIBONK": "SQUIBONK",
"SQUID": "Squid Game", "SQUID": "SQUIDGames Token",
"SQUID2": "SquidDao", "SQUID2": "SquidDao",
"SQUIDGROW": "SquidGrow", "SQUIDGROW": "SquidGrow",
"SQUIDGROWV1": "SquidGrow v1", "SQUIDGROWV1": "SquidGrow v1",
@ -20250,7 +20253,7 @@
"ZPTC": "Zeptacoin", "ZPTC": "Zeptacoin",
"ZRC": "Zircuit", "ZRC": "Zircuit",
"ZRCOIN": "ZrCoin", "ZRCOIN": "ZrCoin",
"ZRO": "Carb0n.fi", "ZRO": "LayerZero",
"ZRO26997": "LayerZero", "ZRO26997": "LayerZero",
"ZRPY": "Zerpaay", "ZRPY": "Zerpaay",
"ZRS": "Zaros", "ZRS": "Zaros",

1
apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts

@ -58,6 +58,7 @@ export class FinancialModelingPrepService
'Congo (Dem. Rep. of the)': 'CD', 'Congo (Dem. Rep. of the)': 'CD',
'Congo (Rep. of)': 'CG', 'Congo (Rep. of)': 'CG',
'Czech Republic': 'CZ', 'Czech Republic': 'CZ',
Korea: 'KR',
'Korea (the Republic of)': 'KR', 'Korea (the Republic of)': 'KR',
Macau: 'MO', Macau: 'MO',
'Russian Federation': 'RU', 'Russian Federation': 'RU',

236
apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html

@ -6,123 +6,131 @@
(ngSubmit)="onSubmit()" (ngSubmit)="onSubmit()"
> >
<div class="py-3" mat-dialog-content> <div class="py-3" mat-dialog-content>
@if ( @if (data.rule.configuration?.threshold; as threshold) {
data.rule.configuration?.thresholdMin && @if (
data.rule.configuration?.thresholdMax data.rule.configuration.thresholdMin &&
) { data.rule.configuration.thresholdMax
<div class="w-100"> ) {
<h6 class="d-flex mb-0"> <div class="w-100">
<ng-container i18n>Threshold range</ng-container>: <h6 class="d-flex mb-0">
<gf-value <ng-container i18n>Threshold range</ng-container>:
class="ml-1" <gf-value
[isPercent]="data.rule.configuration.threshold.unit === '%'" class="ml-1"
[locale]="data.locale" [isPercent]="threshold.unit === '%'"
[precision]="2" [locale]="data.locale"
[value]="settingsForm.get('thresholdMin').value" [precision]="2"
/> [value]="settingsForm.get('thresholdMin')?.value"
<span class="mx-1">-</span> />
<gf-value <span class="mx-1">-</span>
[isPercent]="data.rule.configuration.threshold.unit === '%'" <gf-value
[locale]="data.locale" [isPercent]="threshold.unit === '%'"
[precision]="2" [locale]="data.locale"
[value]="settingsForm.get('thresholdMax').value" [precision]="2"
/> [value]="settingsForm.get('thresholdMax')?.value"
</h6> />
<div class="align-items-center d-flex w-100"> </h6>
<gf-value <div class="align-items-center d-flex w-100">
[isPercent]="data.rule.configuration.threshold.unit === '%'" <gf-value
[locale]="data.locale" [isPercent]="threshold.unit === '%'"
[precision]="2" [locale]="data.locale"
[value]="data.rule.configuration.threshold.min" [precision]="2"
/> [value]="threshold.min"
<mat-slider />
class="flex-grow-1" <mat-slider
[max]="data.rule.configuration.threshold.max" class="flex-grow-1"
[min]="data.rule.configuration.threshold.min" [max]="threshold.max"
[step]="data.rule.configuration.threshold.step" [min]="threshold.min"
> [step]="threshold.step"
<input formControlName="thresholdMin" matSliderStartThumb /> >
<input formControlName="thresholdMax" matSliderEndThumb /> <input formControlName="thresholdMin" matSliderStartThumb />
</mat-slider> <input formControlName="thresholdMax" matSliderEndThumb />
<gf-value </mat-slider>
[isPercent]="data.rule.configuration.threshold.unit === '%'" <gf-value
[locale]="data.locale" [isPercent]="threshold.unit === '%'"
[precision]="2" [locale]="data.locale"
[value]="data.rule.configuration.threshold.max" [precision]="2"
/> [value]="threshold.max"
/>
</div>
</div> </div>
</div> } @else {
} @else { <div
<div class="w-100" [class.d-none]="!data.rule.configuration.thresholdMin"> class="w-100"
<h6 class="d-flex mb-0"> [class.d-none]="!data.rule.configuration.thresholdMin"
<ng-container i18n>Threshold Min</ng-container>: >
<gf-value <h6 class="d-flex mb-0">
class="ml-1" <ng-container i18n>Threshold Min</ng-container>:
[isPercent]="data.rule.configuration.threshold.unit === '%'" <gf-value
[locale]="data.locale" class="ml-1"
[precision]="2" [isPercent]="threshold.unit === '%'"
[value]="settingsForm.get('thresholdMin').value" [locale]="data.locale"
/> [precision]="2"
</h6> [value]="settingsForm.get('thresholdMin')?.value"
<div class="align-items-center d-flex w-100"> />
<gf-value </h6>
[isPercent]="data.rule.configuration.threshold.unit === '%'" <div class="align-items-center d-flex w-100">
[locale]="data.locale" <gf-value
[precision]="2" [isPercent]="threshold.unit === '%'"
[value]="data.rule.configuration.threshold.min" [locale]="data.locale"
/> [precision]="2"
<mat-slider [value]="threshold.min"
class="flex-grow-1" />
name="thresholdMin" <mat-slider
[max]="data.rule.configuration.threshold.max" class="flex-grow-1"
[min]="data.rule.configuration.threshold.min" name="thresholdMin"
[step]="data.rule.configuration.threshold.step" [max]="threshold.max"
> [min]="threshold.min"
<input formControlName="thresholdMin" matSliderThumb /> [step]="threshold.step"
</mat-slider> >
<gf-value <input formControlName="thresholdMin" matSliderThumb />
[isPercent]="data.rule.configuration.threshold.unit === '%'" </mat-slider>
[locale]="data.locale" <gf-value
[precision]="2" [isPercent]="threshold.unit === '%'"
[value]="data.rule.configuration.threshold.max" [locale]="data.locale"
/> [precision]="2"
[value]="threshold.max"
/>
</div>
</div> </div>
</div> <div
<div class="w-100" [class.d-none]="!data.rule.configuration.thresholdMax"> class="w-100"
<h6 class="d-flex mb-0"> [class.d-none]="!data.rule.configuration.thresholdMax"
<ng-container i18n>Threshold Max</ng-container>: >
<gf-value <h6 class="d-flex mb-0">
class="ml-1" <ng-container i18n>Threshold Max</ng-container>:
[isPercent]="data.rule.configuration.threshold.unit === '%'" <gf-value
[locale]="data.locale" class="ml-1"
[precision]="2" [isPercent]="threshold.unit === '%'"
[value]="settingsForm.get('thresholdMax').value" [locale]="data.locale"
/> [precision]="2"
</h6> [value]="settingsForm.get('thresholdMax')?.value"
<div class="align-items-center d-flex w-100"> />
<gf-value </h6>
[isPercent]="data.rule.configuration.threshold.unit === '%'" <div class="align-items-center d-flex w-100">
[locale]="data.locale" <gf-value
[precision]="2" [isPercent]="threshold.unit === '%'"
[value]="data.rule.configuration.threshold.min" [locale]="data.locale"
/> [precision]="2"
<mat-slider [value]="threshold.min"
class="flex-grow-1" />
name="thresholdMax" <mat-slider
[max]="data.rule.configuration.threshold.max" class="flex-grow-1"
[min]="data.rule.configuration.threshold.min" name="thresholdMax"
[step]="data.rule.configuration.threshold.step" [max]="threshold.max"
> [min]="threshold.min"
<input formControlName="thresholdMax" matSliderThumb /> [step]="threshold.step"
</mat-slider> >
<gf-value <input formControlName="thresholdMax" matSliderThumb />
[isPercent]="data.rule.configuration.threshold.unit === '%'" </mat-slider>
[locale]="data.locale" <gf-value
[precision]="2" [isPercent]="threshold.unit === '%'"
[value]="data.rule.configuration.threshold.max" [locale]="data.locale"
/> [precision]="2"
[value]="threshold.max"
/>
</div>
</div> </div>
</div> }
} }
</div> </div>

20
apps/client/src/app/components/user-account-settings/user-account-settings.component.ts

@ -94,6 +94,7 @@ export class GfUserAccountSettingsComponent implements OnInit {
protected hasImpersonationId: boolean; protected hasImpersonationId: boolean;
protected hasPermissionToDeleteOwnUser: boolean; protected hasPermissionToDeleteOwnUser: boolean;
protected hasPermissionToRequestOwnUserDeletion: boolean; protected hasPermissionToRequestOwnUserDeletion: boolean;
protected hasPermissionToUpdateOwnAccessToken: boolean;
protected hasPermissionToUpdateViewMode: boolean; protected hasPermissionToUpdateViewMode: boolean;
protected hasPermissionToUpdateUserSettings: boolean; protected hasPermissionToUpdateUserSettings: boolean;
protected isAccessTokenHidden = true; protected isAccessTokenHidden = true;
@ -194,6 +195,11 @@ export class GfUserAccountSettingsComponent implements OnInit {
permissions.requestOwnUserDeletion permissions.requestOwnUserDeletion
); );
this.hasPermissionToUpdateOwnAccessToken = hasPermission(
this.user.permissions,
permissions.updateOwnAccessToken
);
this.hasPermissionToUpdateUserSettings = hasPermission( this.hasPermissionToUpdateUserSettings = hasPermission(
this.user.permissions, this.user.permissions,
permissions.updateUserSettings permissions.updateUserSettings
@ -271,13 +277,19 @@ export class GfUserAccountSettingsComponent implements OnInit {
this.notificationService.confirm({ this.notificationService.confirm({
confirmFn: () => { confirmFn: () => {
this.dataService this.dataService
.deleteOwnUser({ .deleteOwnUser(
accessToken: this.deleteOwnUserForm.controls.accessToken.value this.hasPermissionToUpdateOwnAccessToken
}) ? {
accessToken: this.deleteOwnUserForm.controls.accessToken.value
}
: {}
)
.pipe( .pipe(
catchError(() => { catchError(() => {
this.notificationService.alert({ this.notificationService.alert({
title: $localize`Oops! Incorrect Security Token.` title: this.hasPermissionToUpdateOwnAccessToken
? $localize`Oops! Incorrect Security Token.`
: $localize`Oops! Something went wrong.`
}); });
return EMPTY; return EMPTY;

14
apps/client/src/app/components/user-account-settings/user-account-settings.html

@ -318,7 +318,10 @@
<div class="d-flex py-1"> <div class="d-flex py-1">
<div class="pr-1 text-danger w-50" i18n>Danger Zone</div> <div class="pr-1 text-danger w-50" i18n>Danger Zone</div>
<div class="pl-1 w-50"> <div class="pl-1 w-50">
@if (hasPermissionToDeleteOwnUser) { @if (
hasPermissionToDeleteOwnUser &&
hasPermissionToUpdateOwnAccessToken
) {
<form <form
class="w-100" class="w-100"
[formGroup]="deleteOwnUserForm" [formGroup]="deleteOwnUserForm"
@ -363,6 +366,15 @@
Close Account Close Account
</button> </button>
</form> </form>
} @else if (hasPermissionToDeleteOwnUser && !hasImpersonationId) {
<button
color="warn"
i18n
mat-flat-button
(click)="onCloseAccount()"
>
Close Account
</button>
} @else if ( } @else if (
hasPermissionToRequestOwnUserDeletion && hasPermissionToRequestOwnUserDeletion &&
(user?.accounts?.length > 0 || user?.activitiesCount > 0) (user?.accounts?.length > 0 || user?.activitiesCount > 0)

59
apps/client/src/app/pages/faq/overview/faq-overview-page.html

@ -56,6 +56,65 @@
return).</mat-card-content return).</mat-card-content
> >
</mat-card> </mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title
>How can I import activities from
<i>Interactive Brokers</i> (IBKR)?</mat-card-title
>
</mat-card-header>
<mat-card-content>
<ol>
<li>Log in to your <i>Interactive Brokers</i> Client Portal</li>
<li>
Navigate to <strong>Performance &amp; Reports</strong> in the top
menu, then select <strong>Flex Queries</strong>
</li>
<li>
Click the <strong>+</strong> button next to Activity Flex Query
</li>
<li>Select <strong>Trades</strong> under Sections</li>
<li>Select <strong>Executions</strong> on the pop-up window</li>
<li>
Click <strong>SELECT ALL</strong> and then <strong>Save</strong>
</li>
<li>Enter a Query Name (e.g., "Ghostfolio Export")</li>
<li>
Under Delivery Configuration, set <strong>Format</strong> to
<strong>CSV</strong>,
<strong>Include header and trailer records</strong> to
<strong>No</strong>, <strong>Include column headers</strong> to
<strong>Yes</strong> and
<strong>Display single column header row</strong> to
<strong>Yes</strong>
</li>
<li>Click <strong>Create</strong></li>
<li>Run the Activity Flex Query</li>
<li>
Under Period, select <strong>Custom Date Range</strong> and choose
the dates you want to export. Run separate exports for longer
histories if necessary
</li>
<li>
Set <strong>Format</strong> to <strong>CSV</strong>, click
<strong>Run</strong> and download your trades
</li>
<li>
Import the CSV file in Ghostfolio: <strong>Portfolio</strong> →
<strong>Activities</strong> →
<strong>Import Activities...</strong>
</li>
</ol>
<p>
Please note: The export contains trades only. It does not contain
dividends, interest or taxes. Keep only rows with the asset class
<code>STK</code>. Remove all other rows before the import, for
example currency conversions (<code>CASH</code>, such as
<code>EUR.USD</code>). Symbols from exchanges outside the US might
need a manual change.
</p>
</mat-card-content>
</mat-card>
<mat-card appearance="outlined" class="mb-3"> <mat-card appearance="outlined" class="mb-3">
<mat-card-header> <mat-card-header>
<mat-card-title>Can I use Ghostfolio anonymously?</mat-card-title> <mat-card-title>Can I use Ghostfolio anonymously?</mat-card-title>

48
apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts

@ -329,36 +329,30 @@ export class GfAnalysisPageComponent implements OnInit {
this.performanceDataItems = []; this.performanceDataItems = [];
this.performanceDataItemsInPercentage = []; this.performanceDataItemsInPercentage = [];
for (const [ for (const {
index, date,
{ dividendInPercentageWithCurrencyEffect,
date, netPerformanceInPercentageWithCurrencyEffect,
dividendInPercentageWithCurrencyEffect, totalInvestmentValueWithCurrencyEffect,
netPerformanceInPercentageWithCurrencyEffect, valueInPercentage,
totalInvestmentValueWithCurrencyEffect, valueWithCurrencyEffect
valueInPercentage, } of chart ?? []) {
valueWithCurrencyEffect if (totalInvestmentValueWithCurrencyEffect !== undefined) {
this.investments.push({
date,
investment: totalInvestmentValueWithCurrencyEffect
});
} }
] of (chart ?? []).entries()) {
// Ignore first item where value is 0
if (index > 0 || this.user?.settings?.dateRange === 'max') {
if (totalInvestmentValueWithCurrencyEffect !== undefined) {
this.investments.push({
date,
investment: totalInvestmentValueWithCurrencyEffect
});
}
const value = isNumber(valueWithCurrencyEffect) const value = isNumber(valueWithCurrencyEffect)
? valueWithCurrencyEffect ? valueWithCurrencyEffect
: valueInPercentage; : valueInPercentage;
if (isNumber(value)) { if (isNumber(value)) {
this.performanceDataItems.push({ this.performanceDataItems.push({
date, date,
value value
}); });
}
} }
if (isNumber(netPerformanceInPercentageWithCurrencyEffect)) { if (isNumber(netPerformanceInPercentageWithCurrencyEffect)) {

16
apps/client/src/app/pages/portfolio/analysis/analysis-page.html

@ -330,7 +330,7 @@
> >
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<ol class="mb-0 ml-1 pl-3"> <ol class="holdings mb-0 ml-1 pl-3">
@for (holding of top3; track holding) { @for (holding of top3; track holding) {
<li class="py-1"> <li class="py-1">
<a <a
@ -342,8 +342,10 @@
}" }"
[routerLink]="[]" [routerLink]="[]"
> >
<div class="flex-grow-1 mr-2 text-truncate"> <div class="flex-grow-1 mr-2 name">
{{ holding.assetProfile.name }} <span class="align-top d-inline-block mw-100 text-truncate">
{{ holding.assetProfile.name }}
</span>
</div> </div>
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<gf-value <gf-value
@ -381,7 +383,7 @@
> >
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<ol class="mb-0 ml-1 pl-3"> <ol class="holdings mb-0 ml-1 pl-3">
@for (holding of bottom3; track holding) { @for (holding of bottom3; track holding) {
<li class="py-1"> <li class="py-1">
<a <a
@ -393,8 +395,10 @@
}" }"
[routerLink]="[]" [routerLink]="[]"
> >
<div class="flex-grow-1 mr-2 text-truncate"> <div class="flex-grow-1 mr-2 name">
{{ holding.assetProfile.name }} <span class="align-top d-inline-block mw-100 text-truncate">
{{ holding.assetProfile.name }}
</span>
</div> </div>
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<gf-value <gf-value

8
apps/client/src/app/pages/portfolio/analysis/analysis-page.scss

@ -4,4 +4,12 @@
.chart-container { .chart-container {
aspect-ratio: 16 / 9; aspect-ratio: 16 / 9;
} }
.holdings {
.name {
// Safari puts the list marker into this element, so the truncation is
// on the inner element (a hidden overflow here hides the marker)
min-width: 0;
}
}
} }

12
apps/client/src/app/pages/resources/personal-finance-tools/interfaces/interfaces.ts

@ -1,6 +1,16 @@
import type { Product } from '@ghostfolio/common/interfaces'; import type { Product } from '@ghostfolio/common/interfaces';
export type ResolvedProduct = Omit<Product, 'categories' | 'platforms'> & { export interface ResolvedRegion {
emojiFlag?: string;
name: string;
}
export type ResolvedProduct = Omit<
Product,
'categories' | 'origin' | 'platforms' | 'regions'
> & {
categories?: string[]; categories?: string[];
origin?: ResolvedRegion;
platforms?: string[]; platforms?: string[];
regions?: ResolvedRegion[];
}; };

39
apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts

@ -1,4 +1,4 @@
import { getCountryName } from '@ghostfolio/common/helper'; import { getCountryName, getEmojiFlag } from '@ghostfolio/common/helper';
import { Product } from '@ghostfolio/common/interfaces'; import { Product } from '@ghostfolio/common/interfaces';
import { personalFinanceTools } from '@ghostfolio/common/personal-finance-tools'; import { personalFinanceTools } from '@ghostfolio/common/personal-finance-tools';
import { publicRoutes } from '@ghostfolio/common/routes/routes'; import { publicRoutes } from '@ghostfolio/common/routes/routes';
@ -14,7 +14,7 @@ import {
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { ActivatedRoute, RouterModule } from '@angular/router'; import { ActivatedRoute, RouterModule } from '@angular/router';
import { ResolvedProduct } from './interfaces/interfaces'; import { ResolvedProduct, ResolvedRegion } from './interfaces/interfaces';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@ -58,9 +58,9 @@ export class GfProductPageComponent {
// 'Ukrainian (Українська)' // 'Ukrainian (Українська)'
], ],
name: 'Ghostfolio', name: 'Ghostfolio',
origin: getCountryName({ code: 'CH' }), origin: this.getResolvedCountry('CH'),
platforms: this.getSortedTranslations(['ANDROID', 'WEB']), platforms: this.getSortedTranslations(['ANDROID', 'WEB']),
regions: [$localize`Global`], regions: [{ name: $localize`Global` }],
slogan: 'Open Source Wealth Management', slogan: 'Open Source Wealth Management',
useAnonymously: true useAnonymously: true
})); }));
@ -75,20 +75,16 @@ export class GfProductPageComponent {
name: product?.name ?? '', name: product?.name ?? '',
...product, ...product,
categories: this.getSortedTranslations(product?.categories), categories: this.getSortedTranslations(product?.categories),
platforms: this.getSortedTranslations(product?.platforms) origin: product?.origin
}; ? this.getResolvedCountry(product.origin)
: undefined,
if (mappedProduct.origin) { platforms: this.getSortedTranslations(product?.platforms),
mappedProduct.origin = getCountryName({ code: mappedProduct.origin }); regions: product?.regions?.map((region) => {
}
if (mappedProduct.regions) {
mappedProduct.regions = mappedProduct.regions.map((region) => {
return region === 'Global' return region === 'Global'
? translate(region) ? { name: translate(region) }
: getCountryName({ code: region }); : this.getResolvedCountry(region);
}); })
} };
return mappedProduct; return mappedProduct;
}); });
@ -122,7 +118,7 @@ export class GfProductPageComponent {
...(categories ?? []), ...(categories ?? []),
...(platforms ?? []), ...(platforms ?? []),
name, name,
origin origin?.name
]; ];
} }
), ),
@ -175,6 +171,13 @@ export class GfProductPageComponent {
}); });
} }
private getResolvedCountry(code: string): ResolvedRegion {
return {
emojiFlag: getEmojiFlag(code),
name: getCountryName({ code })
};
}
private getSortedTranslations(values?: string[]) { private getSortedTranslations(values?: string[]) {
return values return values
?.map((value) => { ?.map((value) => {

32
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html

@ -100,27 +100,47 @@
</tr> </tr>
<tr class="mat-mdc-row"> <tr class="mat-mdc-row">
<td class="mat-mdc-cell px-3 py-2 text-right" i18n>Origin</td> <td class="mat-mdc-cell px-3 py-2 text-right" i18n>Origin</td>
<td class="mat-mdc-cell px-1 py-2">{{ product1().origin }}</td> <td class="mat-mdc-cell px-1 py-2">
<td class="mat-mdc-cell px-1 py-2">{{ product2().origin }}</td> @if (product1().origin; as origin) {
<span aria-hidden="true">{{ origin.emojiFlag }}</span
>&nbsp;{{ origin.name }}
}
</td>
<td class="mat-mdc-cell px-1 py-2">
@if (product2().origin; as origin) {
<span aria-hidden="true">{{ origin.emojiFlag }}</span
>&nbsp;{{ origin.name }}
}
</td>
</tr> </tr>
<tr class="mat-mdc-row"> <tr class="mat-mdc-row">
<td class="mat-mdc-cell px-3 py-2 text-right" i18n>Region</td> <td class="mat-mdc-cell px-3 py-2 text-right" i18n>Region</td>
<td class="mat-mdc-cell px-1 py-2"> <td class="mat-mdc-cell px-1 py-2">
@for ( @for (
region of product1().regions; region of product1().regions;
track region; track region.name;
let isLast = $last let isLast = $last
) { ) {
{{ region }}{{ isLast ? '' : ', ' }} @if (region.emojiFlag) {
<span aria-hidden="true">{{ region.emojiFlag }}</span
>&nbsp;{{ region.name }}{{ isLast ? '' : ', ' }}
} @else {
{{ region.name }}{{ isLast ? '' : ', ' }}
}
} }
</td> </td>
<td class="mat-mdc-cell px-1 py-2"> <td class="mat-mdc-cell px-1 py-2">
@for ( @for (
region of product2().regions; region of product2().regions;
track region; track region.name;
let isLast = $last let isLast = $last
) { ) {
{{ region }}{{ isLast ? '' : ', ' }} @if (region.emojiFlag) {
<span aria-hidden="true">{{ region.emojiFlag }}</span
>&nbsp;{{ region.name }}{{ isLast ? '' : ', ' }}
} @else {
{{ region.name }}{{ isLast ? '' : ', ' }}
}
} }
</td> </td>
</tr> </tr>

106
apps/client/src/locales/messages.es.xlf

@ -100,7 +100,7 @@
</trans-unit> </trans-unit>
<trans-unit id="8264632476919693099" datatype="html"> <trans-unit id="8264632476919693099" datatype="html">
<source>Any person who has the link can view the data.</source> <source>Any person who has the link can view the data.</source>
<target state="new">Any person who has the link can view the data.</target> <target state="translated">Cualquier persona que tenga el enlace puede ver los datos.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html</context>
<context context-type="linenumber">42</context> <context context-type="linenumber">42</context>
@ -388,7 +388,7 @@
</trans-unit> </trans-unit>
<trans-unit id="4472298507300372923" datatype="html"> <trans-unit id="4472298507300372923" datatype="html">
<source>View and manage</source> <source>View and manage</source>
<target state="new">View and manage</target> <target state="translated">Ver y administrar</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/access-level-icon/access-level-icon.component.html</context> <context context-type="sourcefile">libs/ui/src/lib/access-level-icon/access-level-icon.component.html</context>
<context context-type="linenumber">9</context> <context context-type="linenumber">9</context>
@ -648,7 +648,7 @@
</trans-unit> </trans-unit>
<trans-unit id="6249687295240367729" datatype="html"> <trans-unit id="6249687295240367729" datatype="html">
<source>Explore our guides to help you get started with investing and managing your finances.</source> <source>Explore our guides to help you get started with investing and managing your finances.</source>
<target state="new">Explore our guides to help you get started with investing and managing your finances.</target> <target state="translated">Explora nuestras guías para ayudarte a empezar a invertir y gestionar tus finanzas.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/resources/overview/resources-overview.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/resources/overview/resources-overview.component.ts</context>
<context context-type="linenumber">21</context> <context context-type="linenumber">21</context>
@ -892,7 +892,7 @@
</trans-unit> </trans-unit>
<trans-unit id="1022809987579838138" datatype="html"> <trans-unit id="1022809987579838138" datatype="html">
<source>Last Used</source> <source>Last Used</source>
<target state="new">Last Used</target> <target state="translated">Último uso</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.html</context> <context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.html</context>
<context context-type="linenumber">31</context> <context context-type="linenumber">31</context>
@ -900,7 +900,7 @@
</trans-unit> </trans-unit>
<trans-unit id="1029620928611278483" datatype="html"> <trans-unit id="1029620928611278483" datatype="html">
<source>An error occurred while merging this asset profile into <x id="PH" equiv-text="symbol"/> (<x id="PH_1" equiv-text="dataSource"/>).</source> <source>An error occurred while merging this asset profile into <x id="PH" equiv-text="symbol"/> (<x id="PH_1" equiv-text="dataSource"/>).</source>
<target state="new">An error occurred while merging this asset profile into <x id="PH" equiv-text="symbol"/> (<x id="PH_1" equiv-text="dataSource"/>).</target> <target state="translated">Se produjo un error al fusionar este perfil de activo en <x id="PH" equiv-text="symbol"/> (<x id="PH_1" equiv-text="dataSource"/>).</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts</context> <context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts</context>
<context context-type="linenumber">946</context> <context context-type="linenumber">946</context>
@ -944,7 +944,7 @@
</trans-unit> </trans-unit>
<trans-unit id="6623155069376115284" datatype="html"> <trans-unit id="6623155069376115284" datatype="html">
<source>Find quick answers to commonly asked questions about Ghostfolio in our Frequently Asked Questions (FAQ) section.</source> <source>Find quick answers to commonly asked questions about Ghostfolio in our Frequently Asked Questions (FAQ) section.</source>
<target state="new">Find quick answers to commonly asked questions about Ghostfolio in our Frequently Asked Questions (FAQ) section.</target> <target state="translated">Encuentra respuestas rápidas a las preguntas más frecuentes sobre Ghostfolio en nuestra sección de preguntas frecuentes (FAQ).</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/resources/overview/resources-overview.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/resources/overview/resources-overview.component.ts</context>
<context context-type="linenumber">16</context> <context context-type="linenumber">16</context>
@ -2188,7 +2188,7 @@
</trans-unit> </trans-unit>
<trans-unit id="4882027922359285902" datatype="html"> <trans-unit id="4882027922359285902" datatype="html">
<source>Restricted view and manage</source> <source>Restricted view and manage</source>
<target state="new">Restricted view and manage</target> <target state="translated">Vista restringida y administración</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/access-level-icon/access-level-icon.component.html</context> <context context-type="sourcefile">libs/ui/src/lib/access-level-icon/access-level-icon.component.html</context>
<context context-type="linenumber">5</context> <context context-type="linenumber">5</context>
@ -2744,7 +2744,7 @@
</trans-unit> </trans-unit>
<trans-unit id="5298486223740169185" datatype="html"> <trans-unit id="5298486223740169185" datatype="html">
<source>By <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="INTERPOLATION" equiv-text="{{ retirementDateLabel }}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/>, this is projected to increase to <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot; hasPermissionToShowValues ? withdrawalRatePerYearProjected?.toNumber() : null &quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isPercent]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [precision]=&quot;2&quot; [value]=&quot;user?.settings?.annualInterestRate / 100&quot; /&gt;"/> per year<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/> or <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE_1" ctype="x-gf_value_1" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot; hasPermissionToShowValues ? withdrawalRatePerMonthProjected?.toNumber() : null &quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isPercent]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [precision]=&quot;2&quot; [value]=&quot;user?.settings?.annualInterestRate / 100&quot; /&gt;"/> per month<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/>, assuming a <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE_2" ctype="x-gf_value_2" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isPercent]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [precision]=&quot;2&quot; [value]=&quot;user?.settings?.annualInterestRate / 100&quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isPercent]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [precision]=&quot;2&quot; [value]=&quot;user?.settings?.annualInterestRate / 100&quot; /&gt;"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/> annual interest rate.</source> <source>By <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="INTERPOLATION" equiv-text="{{ retirementDateLabel }}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/>, this is projected to increase to <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot; hasPermissionToShowValues ? withdrawalRatePerYearProjected?.toNumber() : null &quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isPercent]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [precision]=&quot;2&quot; [value]=&quot;user?.settings?.annualInterestRate / 100&quot; /&gt;"/> per year<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/> or <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE_1" ctype="x-gf_value_1" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot; hasPermissionToShowValues ? withdrawalRatePerMonthProjected?.toNumber() : null &quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isPercent]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [precision]=&quot;2&quot; [value]=&quot;user?.settings?.annualInterestRate / 100&quot; /&gt;"/> per month<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/>, assuming a <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE_2" ctype="x-gf_value_2" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isPercent]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [precision]=&quot;2&quot; [value]=&quot;user?.settings?.annualInterestRate / 100&quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isPercent]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [precision]=&quot;2&quot; [value]=&quot;user?.settings?.annualInterestRate / 100&quot; /&gt;"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/> annual interest rate.</source>
<target state="new">Para <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="INTERPOLATION" equiv-text="{{ retirementDateLabel }}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/>, se prevé que esto aumente a <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot;withdrawalRatePerYearProjected?.toNumber()&quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isPercent]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [precision]=&quot;2&quot; [value]=&quot;user?.settings?.annualInterestRate / 100&quot; /&gt;"/> por año<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/> o <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE_1" ctype="x-gf_value_1" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot;withdrawalRatePerMonthProjected?.toNumber()&quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isPercent]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [precision]=&quot;2&quot; [value]=&quot;user?.settings?.annualInterestRate / 100&quot; /&gt;"/> por mes<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/>, asumiendo una tasa de interés anual del <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE_2" ctype="x-gf_value_2" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isPercent]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [precision]=&quot;2&quot; [value]=&quot;user?.settings?.annualInterestRate / 100&quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isPercent]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [precision]=&quot;2&quot; [value]=&quot;user?.settings?.annualInterestRate / 100&quot; /&gt;"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/>.</target> <target state="translated">Para <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="INTERPOLATION" equiv-text="{{ retirementDateLabel }}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/>, se prevé que esto aumente a <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot;withdrawalRatePerYearProjected?.toNumber()&quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isPercent]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [precision]=&quot;2&quot; [value]=&quot;user?.settings?.annualInterestRate / 100&quot; /&gt;"/> por año<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/> o <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE_1" ctype="x-gf_value_1" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot;withdrawalRatePerMonthProjected?.toNumber()&quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isPercent]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [precision]=&quot;2&quot; [value]=&quot;user?.settings?.annualInterestRate / 100&quot; /&gt;"/> por mes<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/>, asumiendo una tasa de interés anual del <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE_2" ctype="x-gf_value_2" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isPercent]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [precision]=&quot;2&quot; [value]=&quot;user?.settings?.annualInterestRate / 100&quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isPercent]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [precision]=&quot;2&quot; [value]=&quot;user?.settings?.annualInterestRate / 100&quot; /&gt;"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/>.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">144</context> <context context-type="linenumber">144</context>
@ -3032,7 +3032,7 @@
</trans-unit> </trans-unit>
<trans-unit id="3885497195825665706" datatype="html"> <trans-unit id="3885497195825665706" datatype="html">
<source>Next</source> <source>Next</source>
<target state="new">Next</target> <target state="translated">Siguiente</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/resources/personal-finance-tools/product-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/resources/personal-finance-tools/product-page.html</context>
<context context-type="linenumber">406</context> <context context-type="linenumber">406</context>
@ -3160,7 +3160,7 @@
</trans-unit> </trans-unit>
<trans-unit id="1825717711340685842" datatype="html"> <trans-unit id="1825717711340685842" datatype="html">
<source>Export <x id="INTERPOLATION" equiv-text="{{ (totalItems ?? 0) &gt; 1 ? totalItems : &apos;&apos; }}"/> <x id="ICU" equiv-text="{totalItems, plural, =1 {Activity} other {Activities}}"/></source> <source>Export <x id="INTERPOLATION" equiv-text="{{ (totalItems ?? 0) &gt; 1 ? totalItems : &apos;&apos; }}"/> <x id="ICU" equiv-text="{totalItems, plural, =1 {Activity} other {Activities}}"/></source>
<target state="new">Exportar <x id="INTERPOLATION" equiv-text="{{ totalItems &gt; 1 ? totalItems : &apos;&apos; }}"/> <x id="ICU" equiv-text="{totalItems, plural, =1 {Actividad} other {Actividades}}"/></target> <target state="translated">Exportar <x id="INTERPOLATION" equiv-text="{{ totalItems &gt; 1 ? totalItems : &apos;&apos; }}"/> <x id="ICU" equiv-text="{totalItems, plural, =1 {Actividad} other {Actividades}}"/></target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.html</context> <context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.html</context>
<context context-type="linenumber">72</context> <context context-type="linenumber">72</context>
@ -3304,7 +3304,7 @@
</trans-unit> </trans-unit>
<trans-unit id="3358797625330883274" datatype="html"> <trans-unit id="3358797625330883274" datatype="html">
<source>Find quick answers to commonly asked questions about self-hosting Ghostfolio in our Frequently Asked Questions (FAQ) section.</source> <source>Find quick answers to commonly asked questions about self-hosting Ghostfolio in our Frequently Asked Questions (FAQ) section.</source>
<target state="new">Find quick answers to commonly asked questions about self-hosting Ghostfolio in our Frequently Asked Questions (FAQ) section.</target> <target state="translated">Encuentra respuestas rápidas a las preguntas más frecuentes sobre el autoalojamiento de Ghostfolio en nuestra sección de preguntas frecuentes (FAQ).</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/faq/self-hosting/self-hosting-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/faq/self-hosting/self-hosting-page.html</context>
<context context-type="linenumber">8</context> <context context-type="linenumber">8</context>
@ -3328,7 +3328,7 @@
</trans-unit> </trans-unit>
<trans-unit id="5552246967241337741" datatype="html"> <trans-unit id="5552246967241337741" datatype="html">
<source>If you retire today, you would be able to withdraw <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot; hasPermissionToShowValues ? withdrawalRatePerYear?.toNumber() : null &quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot; hasPermissionToShowValues ? fireWealth?.today.valueInBaseCurrency : null &quot; /&gt;"/> per year<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/> or <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE_1" ctype="x-gf_value_1" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot; hasPermissionToShowValues ? withdrawalRatePerMonth?.toNumber() : null &quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot; hasPermissionToShowValues ? fireWealth?.today.valueInBaseCurrency : null &quot; /&gt;"/> per month<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/>, based on your total assets of <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE_2" ctype="x-gf_value_2" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot; hasPermissionToShowValues ? fireWealth?.today.valueInBaseCurrency : null &quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot; hasPermissionToShowValues ? fireWealth?.today.valueInBaseCurrency : null &quot; /&gt;"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/> and a safe withdrawal rate (SWR) of</source> <source>If you retire today, you would be able to withdraw <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot; hasPermissionToShowValues ? withdrawalRatePerYear?.toNumber() : null &quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot; hasPermissionToShowValues ? fireWealth?.today.valueInBaseCurrency : null &quot; /&gt;"/> per year<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/> or <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE_1" ctype="x-gf_value_1" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot; hasPermissionToShowValues ? withdrawalRatePerMonth?.toNumber() : null &quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot; hasPermissionToShowValues ? fireWealth?.today.valueInBaseCurrency : null &quot; /&gt;"/> per month<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/>, based on your total assets of <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE_2" ctype="x-gf_value_2" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot; hasPermissionToShowValues ? fireWealth?.today.valueInBaseCurrency : null &quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot; hasPermissionToShowValues ? fireWealth?.today.valueInBaseCurrency : null &quot; /&gt;"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/> and a safe withdrawal rate (SWR) of</source>
<target state="new">Si te jubilas hoy, podrías retirar <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot;withdrawalRatePerYear?.toNumber()&quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot;fireWealth?.today.valueInBaseCurrency&quot; /&gt;"/> por año<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/> o <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE_1" ctype="x-gf_value_1" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot;withdrawalRatePerMonth?.toNumber()&quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot;fireWealth?.today.valueInBaseCurrency&quot; /&gt;"/> por mes<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/>, basado en tus activos totales de <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE_2" ctype="x-gf_value_2" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot;fireWealth?.today.valueInBaseCurrency&quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot;fireWealth?.today.valueInBaseCurrency&quot; /&gt;"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/> y una tasa de retiro segura (SWR) de</target> <target state="translated">Si te jubilas hoy, podrías retirar <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot;withdrawalRatePerYear?.toNumber()&quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot;fireWealth?.today.valueInBaseCurrency&quot; /&gt;"/> por año<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/> o <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE_1" ctype="x-gf_value_1" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot;withdrawalRatePerMonth?.toNumber()&quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot;fireWealth?.today.valueInBaseCurrency&quot; /&gt;"/> por mes<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/>, basado en tus activos totales de <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE_2" ctype="x-gf_value_2" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot;fireWealth?.today.valueInBaseCurrency&quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot;fireWealth?.today.valueInBaseCurrency&quot; /&gt;"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/> y una tasa de retiro segura (SWR) de</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">69</context> <context context-type="linenumber">69</context>
@ -3404,7 +3404,7 @@
</trans-unit> </trans-unit>
<trans-unit id="641154803270226443" datatype="html"> <trans-unit id="641154803270226443" datatype="html">
<source>Find quick answers to commonly asked questions about the fully managed Ghostfolio cloud offering in our Frequently Asked Questions (FAQ) section.</source> <source>Find quick answers to commonly asked questions about the fully managed Ghostfolio cloud offering in our Frequently Asked Questions (FAQ) section.</source>
<target state="new">Find quick answers to commonly asked questions about the fully managed Ghostfolio cloud offering in our Frequently Asked Questions (FAQ) section.</target> <target state="translated">Encuentra respuestas rápidas a las preguntas más frecuentes sobre la oferta de Ghostfolio en la nube totalmente gestionada en nuestra sección de preguntas frecuentes (FAQ).</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/faq/saas/saas-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/faq/saas/saas-page.html</context>
<context context-type="linenumber">8</context> <context context-type="linenumber">8</context>
@ -3484,7 +3484,7 @@
</trans-unit> </trans-unit>
<trans-unit id="7771702342975481710" datatype="html"> <trans-unit id="7771702342975481710" datatype="html">
<source>Add your first activity</source> <source>Add your first activity</source>
<target state="new">Add your first activity</target> <target state="translated">Añade tu primera operación</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/no-activities-info/no-activities-info.component.html</context> <context context-type="sourcefile">libs/ui/src/lib/no-activities-info/no-activities-info.component.html</context>
<context context-type="linenumber">12</context> <context context-type="linenumber">12</context>
@ -3584,7 +3584,7 @@
</trans-unit> </trans-unit>
<trans-unit id="472895619516205802" datatype="html"> <trans-unit id="472895619516205802" datatype="html">
<source>This action cannot be undone.</source> <source>This action cannot be undone.</source>
<target state="new">This action cannot be undone.</target> <target state="translated">Esta acción no se puede deshacer.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts</context> <context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts</context>
<context context-type="linenumber">965</context> <context context-type="linenumber">965</context>
@ -4108,7 +4108,7 @@
</trans-unit> </trans-unit>
<trans-unit id="5521091503920831865" datatype="html"> <trans-unit id="5521091503920831865" datatype="html">
<source>Add to Watchlist</source> <source>Add to Watchlist</source>
<target state="new">Add to Watchlist</target> <target state="translated">Añadir a la lista de seguimiento</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/common/src/lib/routes/routes.ts</context> <context context-type="sourcefile">libs/common/src/lib/routes/routes.ts</context>
<context context-type="linenumber">163</context> <context context-type="linenumber">163</context>
@ -4132,7 +4132,7 @@
</trans-unit> </trans-unit>
<trans-unit id="9099220454364126737" datatype="html"> <trans-unit id="9099220454364126737" datatype="html">
<source>This asset profile cannot be merged into <x id="PH" equiv-text="symbol"/> (<x id="PH_1" equiv-text="dataSource"/>).</source> <source>This asset profile cannot be merged into <x id="PH" equiv-text="symbol"/> (<x id="PH_1" equiv-text="dataSource"/>).</source>
<target state="new">This asset profile cannot be merged into <x id="PH" equiv-text="symbol"/> (<x id="PH_1" equiv-text="dataSource"/>).</target> <target state="translated">Este perfil de activo no se puede fusionar en <x id="PH" equiv-text="symbol"/> (<x id="PH_1" equiv-text="dataSource"/>).</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts</context> <context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts</context>
<context context-type="linenumber">924</context> <context context-type="linenumber">924</context>
@ -4184,7 +4184,7 @@
</trans-unit> </trans-unit>
<trans-unit id="6962264495000055621" datatype="html"> <trans-unit id="6962264495000055621" datatype="html">
<source>Do you really want to remove this received access?</source> <source>Do you really want to remove this received access?</source>
<target state="new">Do you really want to remove this received access?</target> <target state="translated">¿Seguro que quieres eliminar este acceso recibido?</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.ts</context> <context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.ts</context>
<context context-type="linenumber">152</context> <context context-type="linenumber">152</context>
@ -4216,7 +4216,7 @@
</trans-unit> </trans-unit>
<trans-unit id="7719274775614264692" datatype="html"> <trans-unit id="7719274775614264692" datatype="html">
<source>The data of this asset profile is moved to <x id="PH" equiv-text="symbol"/> (<x id="PH_1" equiv-text="dataSource"/>) and this asset profile is deleted.</source> <source>The data of this asset profile is moved to <x id="PH" equiv-text="symbol"/> (<x id="PH_1" equiv-text="dataSource"/>) and this asset profile is deleted.</source>
<target state="new">The data of this asset profile is moved to <x id="PH" equiv-text="symbol"/> (<x id="PH_1" equiv-text="dataSource"/>) and this asset profile is deleted.</target> <target state="translated">Los datos de este perfil de activo se trasladan a <x id="PH" equiv-text="symbol"/> (<x id="PH_1" equiv-text="dataSource"/>) y este perfil de activo se elimina.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts</context> <context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts</context>
<context context-type="linenumber">963</context> <context context-type="linenumber">963</context>
@ -4308,7 +4308,7 @@
</trans-unit> </trans-unit>
<trans-unit id="5176578390795646240" datatype="html"> <trans-unit id="5176578390795646240" datatype="html">
<source>thereof Dividend Performance</source> <source>thereof Dividend Performance</source>
<target state="new">thereof Dividend Performance</target> <target state="translated">del cual, rendimiento de dividendos</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context> <context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">178</context> <context context-type="linenumber">178</context>
@ -4696,7 +4696,7 @@
</trans-unit> </trans-unit>
<trans-unit id="5918319622596548216" datatype="html"> <trans-unit id="5918319622596548216" datatype="html">
<source>Financial Advisor</source> <source>Financial Advisor</source>
<target state="new">Financial Advisor</target> <target state="translated">Asesor financiero</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">110</context> <context context-type="linenumber">110</context>
@ -5384,7 +5384,7 @@
</trans-unit> </trans-unit>
<trans-unit id="6952711292901447467" datatype="html"> <trans-unit id="6952711292901447467" datatype="html">
<source>Learn key financial terms and concepts in our comprehensive glossary.</source> <source>Learn key financial terms and concepts in our comprehensive glossary.</source>
<target state="new">Learn key financial terms and concepts in our comprehensive glossary.</target> <target state="translated">Aprende términos y conceptos financieros clave en nuestro completo glosario.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/resources/overview/resources-overview.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/resources/overview/resources-overview.component.ts</context>
<context context-type="linenumber">31</context> <context context-type="linenumber">31</context>
@ -5829,7 +5829,7 @@
</trans-unit> </trans-unit>
<trans-unit id="5106905229162288681" datatype="html"> <trans-unit id="5106905229162288681" datatype="html">
<source>Expiration Date</source> <source>Expiration Date</source>
<target state="new">Expiration Date</target> <target state="translated">Fecha de caducidad</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html</context>
<context context-type="linenumber">54</context> <context context-type="linenumber">54</context>
@ -5926,7 +5926,7 @@
</trans-unit> </trans-unit>
<trans-unit id="4272904080612853299" datatype="html"> <trans-unit id="4272904080612853299" datatype="html">
<source>Do you really want to merge this asset profile into it?</source> <source>Do you really want to merge this asset profile into it?</source>
<target state="new">Do you really want to merge this asset profile into it?</target> <target state="translated">¿Seguro que quieres fusionar este perfil de activo en él?</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts</context> <context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts</context>
<context context-type="linenumber">969</context> <context context-type="linenumber">969</context>
@ -6194,7 +6194,7 @@
</trans-unit> </trans-unit>
<trans-unit id="assetClass.ALTERNATIVE_INVESTMENT" datatype="html"> <trans-unit id="assetClass.ALTERNATIVE_INVESTMENT" datatype="html">
<source>Alternative Investment</source> <source>Alternative Investment</source>
<target state="new">Alternative Investment</target> <target state="translated">Inversión alternativa</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">52</context> <context context-type="linenumber">52</context>
@ -6202,7 +6202,7 @@
</trans-unit> </trans-unit>
<trans-unit id="assetClass.COMMODITY" datatype="html"> <trans-unit id="assetClass.COMMODITY" datatype="html">
<source>Commodity</source> <source>Commodity</source>
<target state="new">Commodity</target> <target state="translated">Materia prima</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">53</context> <context context-type="linenumber">53</context>
@ -6210,7 +6210,7 @@
</trans-unit> </trans-unit>
<trans-unit id="assetClass.EQUITY" datatype="html"> <trans-unit id="assetClass.EQUITY" datatype="html">
<source>Equity</source> <source>Equity</source>
<target state="new">Equity</target> <target state="translated">Renta variable</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">54</context> <context context-type="linenumber">54</context>
@ -6218,7 +6218,7 @@
</trans-unit> </trans-unit>
<trans-unit id="assetClass.FIXED_INCOME" datatype="html"> <trans-unit id="assetClass.FIXED_INCOME" datatype="html">
<source>Fixed Income</source> <source>Fixed Income</source>
<target state="new">Fixed Income</target> <target state="translated">Renta fija</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">55</context> <context context-type="linenumber">55</context>
@ -6226,7 +6226,7 @@
</trans-unit> </trans-unit>
<trans-unit id="assetClass.LIQUIDITY" datatype="html"> <trans-unit id="assetClass.LIQUIDITY" datatype="html">
<source>Liquidity</source> <source>Liquidity</source>
<target state="new">Liquidity</target> <target state="translated">Liquidez</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">56</context> <context context-type="linenumber">56</context>
@ -6234,7 +6234,7 @@
</trans-unit> </trans-unit>
<trans-unit id="assetClass.REAL_ESTATE" datatype="html"> <trans-unit id="assetClass.REAL_ESTATE" datatype="html">
<source>Real Estate</source> <source>Real Estate</source>
<target state="new">Real Estate</target> <target state="translated">Inmobiliario</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">57</context> <context context-type="linenumber">57</context>
@ -6242,7 +6242,7 @@
</trans-unit> </trans-unit>
<trans-unit id="assetSubClass.BOND" datatype="html"> <trans-unit id="assetSubClass.BOND" datatype="html">
<source>Bond</source> <source>Bond</source>
<target state="new">Bond</target> <target state="translated">Bono</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">60</context> <context context-type="linenumber">60</context>
@ -6250,7 +6250,7 @@
</trans-unit> </trans-unit>
<trans-unit id="assetSubClass.CASH" datatype="html"> <trans-unit id="assetSubClass.CASH" datatype="html">
<source>Cash</source> <source>Cash</source>
<target state="new">Cash</target> <target state="translated">Efectivo</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">61</context> <context context-type="linenumber">61</context>
@ -6258,7 +6258,7 @@
</trans-unit> </trans-unit>
<trans-unit id="assetSubClass.COLLECTIBLE" datatype="html"> <trans-unit id="assetSubClass.COLLECTIBLE" datatype="html">
<source>Collectible</source> <source>Collectible</source>
<target state="new">Collectible</target> <target state="translated">Objeto de colección</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">62</context> <context context-type="linenumber">62</context>
@ -6266,7 +6266,7 @@
</trans-unit> </trans-unit>
<trans-unit id="assetSubClass.COMMODITY" datatype="html"> <trans-unit id="assetSubClass.COMMODITY" datatype="html">
<source>Commodity</source> <source>Commodity</source>
<target state="new">Commodity</target> <target state="translated">Materia prima</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">5</context> <context context-type="linenumber">5</context>
@ -6274,7 +6274,7 @@
</trans-unit> </trans-unit>
<trans-unit id="assetSubClass.CRYPTOCURRENCY" datatype="html"> <trans-unit id="assetSubClass.CRYPTOCURRENCY" datatype="html">
<source>Cryptocurrency</source> <source>Cryptocurrency</source>
<target state="new">Cryptocurrency</target> <target state="translated">Criptomoneda</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">63</context> <context context-type="linenumber">63</context>
@ -6282,7 +6282,7 @@
</trans-unit> </trans-unit>
<trans-unit id="assetSubClass.ETF" datatype="html"> <trans-unit id="assetSubClass.ETF" datatype="html">
<source>ETF</source> <source>ETF</source>
<target state="new">ETF</target> <target state="translated">ETF</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">64</context> <context context-type="linenumber">64</context>
@ -6290,7 +6290,7 @@
</trans-unit> </trans-unit>
<trans-unit id="assetSubClass.LOAN" datatype="html"> <trans-unit id="assetSubClass.LOAN" datatype="html">
<source>Loan</source> <source>Loan</source>
<target state="new">Loan</target> <target state="translated">Préstamo</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">65</context> <context context-type="linenumber">65</context>
@ -6298,7 +6298,7 @@
</trans-unit> </trans-unit>
<trans-unit id="assetSubClass.MUTUALFUND" datatype="html"> <trans-unit id="assetSubClass.MUTUALFUND" datatype="html">
<source>Mutual Fund</source> <source>Mutual Fund</source>
<target state="new">Mutual Fund</target> <target state="translated">Fondo de inversión</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">66</context> <context context-type="linenumber">66</context>
@ -6306,7 +6306,7 @@
</trans-unit> </trans-unit>
<trans-unit id="assetSubClass.PRECIOUS_METAL" datatype="html"> <trans-unit id="assetSubClass.PRECIOUS_METAL" datatype="html">
<source>Precious Metal</source> <source>Precious Metal</source>
<target state="new">Precious Metal</target> <target state="translated">Metal precioso</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">67</context> <context context-type="linenumber">67</context>
@ -6314,7 +6314,7 @@
</trans-unit> </trans-unit>
<trans-unit id="assetSubClass.PRIVATE_EQUITY" datatype="html"> <trans-unit id="assetSubClass.PRIVATE_EQUITY" datatype="html">
<source>Private Equity</source> <source>Private Equity</source>
<target state="new">Private Equity</target> <target state="translated">Capital privado</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">68</context> <context context-type="linenumber">68</context>
@ -6322,7 +6322,7 @@
</trans-unit> </trans-unit>
<trans-unit id="assetSubClass.STOCK" datatype="html"> <trans-unit id="assetSubClass.STOCK" datatype="html">
<source>Stock</source> <source>Stock</source>
<target state="new">Stock</target> <target state="translated">Acción</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">69</context> <context context-type="linenumber">69</context>
@ -6666,7 +6666,7 @@
</trans-unit> </trans-unit>
<trans-unit id="3995811497329884593" datatype="html"> <trans-unit id="3995811497329884593" datatype="html">
<source>Expires <x id="INTERPOLATION" equiv-text="{{ formatRelativeTime({ date: element.subscription.expiresAt, language: user?.settings?.language }) }}"/> (<x id="INTERPOLATION_1" equiv-text="{{ element.subscription.expiresAt | date: defaultDateFormat }}"/>)</source> <source>Expires <x id="INTERPOLATION" equiv-text="{{ formatRelativeTime({ date: element.subscription.expiresAt, language: user?.settings?.language }) }}"/> (<x id="INTERPOLATION_1" equiv-text="{{ element.subscription.expiresAt | date: defaultDateFormat }}"/>)</source>
<target state="new">Caduca <x id="INTERPOLATION" equiv-text="{{ formatDistanceToNow(element.subscription.expiresAt) }}"/> (<x id="INTERPOLATION_1" equiv-text="{{ element.subscription.expiresAt | date: defaultDateFormat }}"/>)</target> <target state="translated">Caduca <x id="INTERPOLATION" equiv-text="{{ formatDistanceToNow(element.subscription.expiresAt) }}"/> (<x id="INTERPOLATION_1" equiv-text="{{ element.subscription.expiresAt | date: defaultDateFormat }}"/>)</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-users/admin-users.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-users/admin-users.html</context>
<context context-type="linenumber">34</context> <context context-type="linenumber">34</context>
@ -6690,7 +6690,7 @@
</trans-unit> </trans-unit>
<trans-unit id="251113330629316982" datatype="html"> <trans-unit id="251113330629316982" datatype="html">
<source>Date must be in the future.</source> <source>Date must be in the future.</source>
<target state="new">Date must be in the future.</target> <target state="translated">La fecha debe estar en el futuro.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html</context>
<context context-type="linenumber">70</context> <context context-type="linenumber">70</context>
@ -7071,7 +7071,7 @@
</trans-unit> </trans-unit>
<trans-unit id="4452427314943113135" datatype="html"> <trans-unit id="4452427314943113135" datatype="html">
<source>Previous</source> <source>Previous</source>
<target state="new">Previous</target> <target state="translated">Anterior</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/resources/personal-finance-tools/product-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/resources/personal-finance-tools/product-page.html</context>
<context context-type="linenumber">396</context> <context context-type="linenumber">396</context>
@ -7243,7 +7243,7 @@
</trans-unit> </trans-unit>
<trans-unit id="2655368253225791055" datatype="html"> <trans-unit id="2655368253225791055" datatype="html">
<source>Find quick answers to commonly asked questions about Ghostfolio in our Frequently Asked Questions (FAQ) section. Discover what Ghostfolio is, explore its features, and learn about our privacy practices. Get all the information you need in one place.</source> <source>Find quick answers to commonly asked questions about Ghostfolio in our Frequently Asked Questions (FAQ) section. Discover what Ghostfolio is, explore its features, and learn about our privacy practices. Get all the information you need in one place.</source>
<target state="new">Find quick answers to commonly asked questions about Ghostfolio in our Frequently Asked Questions (FAQ) section. Discover what Ghostfolio is, explore its features, and learn about our privacy practices. Get all the information you need in one place.</target> <target state="translated">Encuentra respuestas rápidas a las preguntas más frecuentes sobre Ghostfolio en nuestra sección de preguntas frecuentes (FAQ). Descubre qué es Ghostfolio, explora sus funciones y conoce nuestras prácticas de privacidad. Obtén toda la información que necesitas en un solo lugar.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/faq/overview/faq-overview-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/faq/overview/faq-overview-page.html</context>
<context context-type="linenumber">8</context> <context context-type="linenumber">8</context>
@ -7395,7 +7395,7 @@
</trans-unit> </trans-unit>
<trans-unit id="7803225195017107428" datatype="html"> <trans-unit id="7803225195017107428" datatype="html">
<source>thereof Dividend</source> <source>thereof Dividend</source>
<target state="new">thereof Dividend</target> <target state="translated">de los cuales, dividendos</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context> <context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">102</context> <context context-type="linenumber">102</context>
@ -7937,7 +7937,7 @@
</trans-unit> </trans-unit>
<trans-unit id="9179002757707456211" datatype="html"> <trans-unit id="9179002757707456211" datatype="html">
<source>Access various market resources and tools to stay informed about financial markets.</source> <source>Access various market resources and tools to stay informed about financial markets.</source>
<target state="new">Access various market resources and tools to stay informed about financial markets.</target> <target state="translated">Accede a diversos recursos y herramientas del mercado para mantenerte informado sobre los mercados financieros.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/resources/overview/resources-overview.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/resources/overview/resources-overview.component.ts</context>
<context context-type="linenumber">26</context> <context context-type="linenumber">26</context>
@ -8217,7 +8217,7 @@
</trans-unit> </trans-unit>
<trans-unit id="1308755238280799396" datatype="html"> <trans-unit id="1308755238280799396" datatype="html">
<source>Oops! Could not remove the received access.</source> <source>Oops! Could not remove the received access.</source>
<target state="new">Oops! Could not remove the received access.</target> <target state="translated">¡Vaya! No se pudo eliminar el acceso recibido.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-access/user-account-access.component.ts</context> <context context-type="sourcefile">apps/client/src/app/components/user-account-access/user-account-access.component.ts</context>
<context context-type="linenumber">156</context> <context context-type="linenumber">156</context>
@ -8301,7 +8301,7 @@
</trans-unit> </trans-unit>
<trans-unit id="8816331549575943119" datatype="html"> <trans-unit id="8816331549575943119" datatype="html">
<source>Additional requests are granted while you are setting up your instance</source> <source>Additional requests are granted while you are setting up your instance</source>
<target state="new">Additional requests are granted while you are setting up your instance</target> <target state="translated">Se conceden solicitudes adicionales mientras configuras tu instancia</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-settings/admin-settings.component.ts</context> <context context-type="sourcefile">apps/client/src/app/components/admin-settings/admin-settings.component.ts</context>
<context context-type="linenumber">88</context> <context context-type="linenumber">88</context>
@ -8402,7 +8402,7 @@
</trans-unit> </trans-unit>
<trans-unit id="3606972039333274390" datatype="html"> <trans-unit id="3606972039333274390" datatype="html">
<source><x id="PH" equiv-text="symbol"/> (<x id="PH_1" equiv-text="dataSource"/>) is already in use.</source> <source><x id="PH" equiv-text="symbol"/> (<x id="PH_1" equiv-text="dataSource"/>) is already in use.</source>
<target state="new"><x id="PH" equiv-text="assetProfileIdentifier.symbol"/> (<x id="PH_1" equiv-text="assetProfileIdentifier.dataSource"/>) ya está en uso.</target> <target state="translated"><x id="PH" equiv-text="assetProfileIdentifier.symbol"/> (<x id="PH_1" equiv-text="assetProfileIdentifier.dataSource"/>) ya está en uso.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts</context> <context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts</context>
<context context-type="linenumber">967</context> <context context-type="linenumber">967</context>
@ -8833,7 +8833,7 @@
</trans-unit> </trans-unit>
<trans-unit id="7387635272539030076" datatype="html"> <trans-unit id="7387635272539030076" datatype="html">
<source>new</source> <source>new</source>
<target state="new">new</target> <target state="translated">nuevo</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/features/features-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/features/features-page.html</context>
<context context-type="linenumber">223</context> <context context-type="linenumber">223</context>
@ -9154,7 +9154,7 @@
</trans-unit> </trans-unit>
<trans-unit id="5814001514445505929" datatype="html"> <trans-unit id="5814001514445505929" datatype="html">
<source>Return on Average Investment (total return with dividends)</source> <source>Return on Average Investment (total return with dividends)</source>
<target state="new">Return on Average Investment (total return with dividends)</target> <target state="translated">Rentabilidad sobre la inversión media (rentabilidad total con dividendos)</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context> <context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">159</context> <context context-type="linenumber">159</context>
@ -9466,7 +9466,7 @@
</trans-unit> </trans-unit>
<trans-unit id="3394346142616268152" datatype="html"> <trans-unit id="3394346142616268152" datatype="html">
<source>Oops! Could not revoke the granted access.</source> <source>Oops! Could not revoke the granted access.</source>
<target state="new">Oops! Could not revoke the granted access.</target> <target state="translated">¡Vaya! No se pudo revocar el acceso concedido.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-access/user-account-access.component.ts</context> <context context-type="sourcefile">apps/client/src/app/components/user-account-access/user-account-access.component.ts</context>
<context context-type="linenumber">135</context> <context context-type="linenumber">135</context>

2
libs/common/src/lib/config.ts

@ -198,6 +198,8 @@ export const DEFAULT_REDACTED_PATHS = [
'valueInBaseCurrency' 'valueInBaseCurrency'
]; ];
export const DELETE_OWN_USER_PERIOD = ms('2 weeks');
// USX is handled separately // USX is handled separately
export const DERIVED_CURRENCIES = [ export const DERIVED_CURRENCIES = [
{ {

5
libs/common/src/lib/dtos/delete-own-user.dto.ts

@ -1,6 +1,7 @@
import { IsString } from 'class-validator'; import { IsOptional, IsString } from 'class-validator';
export class DeleteOwnUserDto { export class DeleteOwnUserDto {
@IsOptional()
@IsString() @IsString()
accessToken: string; accessToken?: string;
} }

90
libs/common/src/lib/personal-finance-tools.ts

@ -794,6 +794,28 @@ export const personalFinanceTools: Product[] = (
updatedAt: '2026-09-23', updatedAt: '2026-09-23',
url: 'https://www.comarch.com/finance/banking/comarch-wealth-management' url: 'https://www.comarch.com/finance/banking/comarch-wealth-management'
}, },
{
categories: [
'BUDGETING',
'CRYPTOCURRENCY',
'DIVIDEND_TRACKING',
'ETF_TRACKING',
'NET_WORTH_TRACKING',
'STOCK_TRACKING'
],
founded: 2019,
hasFreePlan: true,
key: 'compiledsanity-personal-finance',
languages: ['English'],
name: 'CompiledSanity Personal Finance',
note: 'License is a perpetual license',
origin: 'AU',
platforms: ['WEB'],
pricingPerYear: 'AUD 12',
slogan: 'Understand Your Money. Grow Your Wealth.',
updatedAt: '2026-10-08',
url: 'https://cspersonalfinance.io'
},
{ {
categories: ['WEALTH_MANAGEMENT'], categories: ['WEALTH_MANAGEMENT'],
founded: 2019, founded: 2019,
@ -1458,6 +1480,26 @@ export const personalFinanceTools: Product[] = (
updatedAt: '2026-07-05', updatedAt: '2026-07-05',
url: 'https://finateka.com' url: 'https://finateka.com'
}, },
{
categories: [
'BUDGETING',
'CRYPTOCURRENCY',
'DIVIDEND_TRACKING',
'NET_WORTH_TRACKING',
'STOCK_TRACKING'
],
hasFreePlan: true,
hasSelfHostingAbility: false,
key: 'finboard',
languages: ['Deutsch', 'English', 'Español'],
name: 'FinBoard',
origin: 'ES',
platforms: ['WEB'],
pricingPerYear: '€49.90',
slogan: 'Your Personal Finance Dashboard',
updatedAt: '2026-10-08',
url: 'https://finboard.me'
},
{ {
categories: ['NET_WORTH_TRACKING', 'STOCK_TRACKING'], categories: ['NET_WORTH_TRACKING', 'STOCK_TRACKING'],
founded: 2022, founded: 2022,
@ -2576,6 +2618,33 @@ export const personalFinanceTools: Product[] = (
updatedAt: '2026-09-02', updatedAt: '2026-09-02',
url: 'https://neontra.com' url: 'https://neontra.com'
}, },
{
categories: [
'BUDGETING',
'CRYPTOCURRENCY',
'ETF_TRACKING',
'NET_WORTH_TRACKING',
'STOCK_TRACKING'
],
hasFreePlan: true,
hasSelfHostingAbility: false,
key: 'neto',
languages: [
'Deutsch',
'English',
'Español',
'Français',
'Italiano',
'Português'
],
name: 'Neto',
origin: 'ES',
platforms: ['ANDROID', 'IOS'],
pricingPerYear: '€49.99',
slogan: 'Tus gastos, tus ahorros y tu patrimonio en una sola app',
updatedAt: '2026-10-08',
url: 'https://netoapp.es'
},
{ {
categories: ['INVESTMENT_RESEARCH', 'STOCK_TRACKING'], categories: ['INVESTMENT_RESEARCH', 'STOCK_TRACKING'],
hasFreePlan: true, hasFreePlan: true,
@ -2672,6 +2741,27 @@ export const personalFinanceTools: Product[] = (
updatedAt: '2026-09-23', updatedAt: '2026-09-23',
url: 'https://orion.com/advisor-tech/risk-intelligence' url: 'https://orion.com/advisor-tech/risk-intelligence'
}, },
{
categories: [
'CRYPTOCURRENCY',
'ETF_TRACKING',
'FAMILY_OFFICE',
'NET_WORTH_TRACKING',
'STOCK_TRACKING',
'WEALTH_MANAGEMENT'
],
hasFreePlan: false,
hasSelfHostingAbility: false,
key: 'ormax',
languages: ['English'],
name: 'Ormax',
origin: 'CY',
platforms: ['WEB'],
pricingPerYear: '$240',
slogan: 'AI Wealth Management',
updatedAt: '2026-10-08',
url: 'https://ormax.ai'
},
{ {
categories: [ categories: [
'CRYPTOCURRENCY', 'CRYPTOCURRENCY',

27
libs/ui/src/lib/account-selector/account-selector.component.stories.ts

@ -24,7 +24,7 @@ const ACCOUNTS: AccountWithPlatform[] = [
platform: { platform: {
id: '9da3a8a7-4795-43e3-a6db-ccb914189737', id: '9da3a8a7-4795-43e3-a6db-ccb914189737',
name: 'Coinbase', name: 'Coinbase',
url: 'https://coinbase.com' url: 'https://www.coinbase.com'
}, },
platformId: '9da3a8a7-4795-43e3-a6db-ccb914189737', platformId: '9da3a8a7-4795-43e3-a6db-ccb914189737',
updatedAt: new Date('2024-01-01'), updatedAt: new Date('2024-01-01'),
@ -33,13 +33,28 @@ const ACCOUNTS: AccountWithPlatform[] = [
{ {
comment: null, comment: null,
createdAt: new Date('2024-01-01'), createdAt: new Date('2024-01-01'),
currency: 'CHF', currency: 'USD',
id: 'd191b2d5-9d5a-4c5f-9d55-6f0e5f6a7b8c',
name: 'Private Banking Account',
platform: {
id: '43e8fcd1-5b79-4100-b678-d2229bd1660d',
name: 'J.P. Morgan',
url: 'https://www.jpmorgan.com'
},
platformId: '43e8fcd1-5b79-4100-b678-d2229bd1660d',
updatedAt: new Date('2024-01-01'),
userId: '081aa387-487d-4438-83a4-3060eb2a016e'
},
{
comment: null,
createdAt: new Date('2024-01-01'),
currency: 'USD',
id: '9da3a8a7-4795-43e3-a6db-ccb914189737', id: '9da3a8a7-4795-43e3-a6db-ccb914189737',
name: 'Ghostfolio Account', name: 'Trading Account',
platform: { platform: {
id: 'f3e9d0e5-1e0f-4a3c-8b12-1f2a3b4c5d6e', id: 'f3e9d0e5-1e0f-4a3c-8b12-1f2a3b4c5d6e',
name: 'Ghostfolio', name: 'Interactive Brokers',
url: 'https://ghostfol.io' url: 'https://interactivebrokers.com'
}, },
platformId: 'f3e9d0e5-1e0f-4a3c-8b12-1f2a3b4c5d6e', platformId: 'f3e9d0e5-1e0f-4a3c-8b12-1f2a3b4c5d6e',
updatedAt: new Date('2024-01-01'), updatedAt: new Date('2024-01-01'),
@ -49,7 +64,7 @@ const ACCOUNTS: AccountWithPlatform[] = [
comment: null, comment: null,
createdAt: new Date('2024-01-01'), createdAt: new Date('2024-01-01'),
currency: 'EUR', currency: 'EUR',
id: 'd191b2d5-9d5a-4c5f-9d55-6f0e5f6a7b8c', id: '267ec888-cb57-45d0-8085-938017417a2e',
name: 'Savings Account', name: 'Savings Account',
platformId: null, platformId: null,
updatedAt: new Date('2024-01-01'), updatedAt: new Date('2024-01-01'),

10
libs/ui/src/lib/accounts-table/accounts-table.component.stories.ts

@ -12,6 +12,8 @@ import type { Meta, StoryObj } from '@storybook/angular';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { GfEntityLogoComponent } from '../entity-logo'; import { GfEntityLogoComponent } from '../entity-logo';
import { EntityLogoImageSourceService } from '../entity-logo/entity-logo-image-source.service';
import { EntityLogoImageSourceServiceMock } from '../mocks/entity-logo-image-source.service.mock';
import { NotificationService } from '../notifications'; import { NotificationService } from '../notifications';
import { GfValueComponent } from '../value'; import { GfValueComponent } from '../value';
import { GfAccountsTableComponent } from './accounts-table.component'; import { GfAccountsTableComponent } from './accounts-table.component';
@ -108,7 +110,13 @@ export default {
NgxSkeletonLoaderModule, NgxSkeletonLoaderModule,
RouterModule.forChild([]) RouterModule.forChild([])
], ],
providers: [NotificationService] providers: [
NotificationService,
{
provide: EntityLogoImageSourceService,
useValue: new EntityLogoImageSourceServiceMock()
}
]
}) })
] ]
} as Meta<GfAccountsTableComponent>; } as Meta<GfAccountsTableComponent>;

10
libs/ui/src/lib/activities-table/activities-table.component.stories.ts

@ -16,6 +16,8 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { GfActivityTypeComponent } from '../activity-type/activity-type.component'; import { GfActivityTypeComponent } from '../activity-type/activity-type.component';
import { GfEntityLogoComponent } from '../entity-logo'; import { GfEntityLogoComponent } from '../entity-logo';
import { EntityLogoImageSourceService } from '../entity-logo/entity-logo-image-source.service';
import { EntityLogoImageSourceServiceMock } from '../mocks/entity-logo-image-source.service.mock';
import { GfNoActivitiesInfoComponent } from '../no-activities-info/no-activities-info.component'; import { GfNoActivitiesInfoComponent } from '../no-activities-info/no-activities-info.component';
import { NotificationService } from '../notifications'; import { NotificationService } from '../notifications';
import { GfValueComponent } from '../value'; import { GfValueComponent } from '../value';
@ -371,7 +373,13 @@ export default {
NgxSkeletonLoaderModule, NgxSkeletonLoaderModule,
RouterModule.forChild([]) RouterModule.forChild([])
], ],
providers: [NotificationService] providers: [
NotificationService,
{
provide: EntityLogoImageSourceService,
useValue: new EntityLogoImageSourceServiceMock()
}
]
}) })
] ]
} as Meta<GfActivitiesTableComponent>; } as Meta<GfActivitiesTableComponent>;

5
libs/ui/src/lib/entity-logo/entity-logo-image-source.service.ts

@ -2,8 +2,9 @@ import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import { Service } from '@angular/core'; import { Service } from '@angular/core';
// Must stay auto-provided: several table stories render gf-entity-logo // Must stay auto-provided: the currency selector and portfolio filter form
// without providing an override and resolve this from the root injector // stories render gf-entity-logo without providing an override and resolve
// this from the root injector
@Service() @Service()
export class EntityLogoImageSourceService { export class EntityLogoImageSourceService {
public getLogoUrlByAssetProfileIdentifier({ public getLogoUrlByAssetProfileIdentifier({

46
libs/ui/src/lib/mocks/entity-logo-image-source.service.mock.ts

@ -1,3 +1,4 @@
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import { DataSource } from '@ghostfolio/prisma/enums'; import { DataSource } from '@ghostfolio/prisma/enums';
@ -9,18 +10,47 @@ export class EntityLogoImageSourceServiceMock {
dataSource, dataSource,
symbol symbol
}: AssetProfileIdentifier) { }: AssetProfileIdentifier) {
if (dataSource === DataSource.YAHOO && symbol === 'AAPL') { switch (getAssetProfileIdentifier({ dataSource, symbol })) {
return 'data:image/png;base64,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'; case getAssetProfileIdentifier({
dataSource: DataSource.YAHOO,
symbol: 'AAPL'
}):
return 'data:image/png;base64,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';
case getAssetProfileIdentifier({
dataSource: DataSource.YAHOO,
symbol: 'BNDW'
}):
case getAssetProfileIdentifier({
dataSource: DataSource.YAHOO,
symbol: 'VT'
}):
case getAssetProfileIdentifier({
dataSource: DataSource.YAHOO,
symbol: 'VTI'
}):
return 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBwgHBgkIBwgKCgkLDRYPDQwMDRsUFRAWIB0iIiAdHx8kKDQsJCYxJx8fLT0tMTU3Ojo6Iys/RD84QzQ5OjcBCgoKDQwNGg8PGjclHyU3Nzc3Nzc3Nzc3Nzc3Nzc3Njc3Nzc3Nzc4NzU3Nzc3MDU1NTc1NTE3Ny0rMC0tLTctLf/AABEIACAAIAMBEQACEQEDEQH/xAAZAAACAwEAAAAAAAAAAAAAAAAEBwMFBgD/xAArEAACAQQAAggHAAAAAAAAAAABAgMABAURElEGBxMhQWFxgSIxMoKRkqH/xAAaAQACAwEBAAAAAAAAAAAAAAABAwIEBgAF/8QAKBEAAgICAAQEBwAAAAAAAAAAAQIAEQMEBTFBwRIhQnEGEyOhsdHh/9oADAMBAAIRAxEAPwB1XdzHaW8lxOWEcalmKoWIA8gCTQupJELsFXmZT4jpjgMxerZY7ICa4YEqnZOuwBs97KBURkVjQMt5+G7WunjyLQ9x2MNzWax+Dt1uMpM0MLNwhxE7jfnwg6oswXnE6+rl2W8OIWfcD8wmwvbfI2cV5ZuXgmXijYqV2Oej30QbFiLy4mxOUfyIhFGQiAyCN0N6wC0YKxW1yJEA8Ym79fqSKon6bzd4iN/Qo8yPuP7Nz1qXDZOXCdH7NwWvpxIxHgv0qfT4mP207MbpR1nicFQYRl2X9Ir9xg2tvHa20NvAvDFCgRF5KBoD8U8Cp4LuXYs3MyajIxWddOFZ47TNQxk9mOwnIHyG9qT7kj3FVthes03w9sgFsBPPzHeBdVqXOd6SLkb08aYqzWCI8joqv846GG2az0juMlNbW+UnrN9z2jgq1MnOrp0imijnieKaNZI3GmR1BDDkRXQqxU2poiB4rDY7ELMuMtI7ZZn43Cb0TrXt6DuoKoXlHZ9nLno5WupY0Yif/9k=';
case getAssetProfileIdentifier({
dataSource: DataSource.YAHOO,
symbol: 'IBIT'
}):
return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAbklEQVRYhe3UsQ2AMAwEwA8iksdhgzASozASbMA4llKEhtbFS0Ru/lsr9ilRXMYYYFLNLgAtKN/dfWf6LdT0CRFAgML+gr+TfgMre6CanQC2oPx092Mq4BseLSI66U8ggAACCCCAAAIIIIAA6YAXmvMQrFvWq40AAAAASUVORK5CYII=';
default:
return UNAVAILABLE_LOGO_URL;
} }
return UNAVAILABLE_LOGO_URL;
} }
public getLogoUrlByUrl(url: string) { public getLogoUrlByUrl(url: string) {
if (url === 'https://ghostfol.io') { switch (url) {
return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAP1BMVEU2z8v////x/Pspzcn0/Px73drR8/KC3tz6/v6e5eOl5+WM4d7n+PhI08/i9/eu6edV1dJk2NVx2teV4+G87evvttLSAAABDElEQVRYhe2V3Y6EIAyFrQXlTwHd93/WpeBsdiKbtF7tJJwbCKFfDlKO0zQ0NPQJwqrn1ZO2zlk9PWPgusClRcsJmHb4pWUTItDDu4zMBJ5w0yog4HGvB0gCgOoBZrYFdL16AMsmmD5AcQ3ofj3AwbOAX38BIhMw02ZjtQ+tLnht66mCAGA2eka1G3eabUZwD/PLbeuHenKMBODVV0Dru/TTQLgKAc1BvQ/9yAEkOnlon66oehEBIHp7dbSyPoIc0NEADMDnAa4wAvVK+CADRGx/VpNSi+iF3jMTUH4rJQ0aISPmVk+JoJiZeJw1QnaqL2OmWKSFkxnrZWsbXK4TzO5tmS+8TYaGhv6xvgEEfAgHGc7HRgAAAABJRU5ErkJggg=='; case 'https://ghostfol.io':
return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAP1BMVEU2z8v////x/Pspzcn0/Px73drR8/KC3tz6/v6e5eOl5+WM4d7n+PhI08/i9/eu6edV1dJk2NVx2teV4+G87evvttLSAAABDElEQVRYhe2V3Y6EIAyFrQXlTwHd93/WpeBsdiKbtF7tJJwbCKFfDlKO0zQ0NPQJwqrn1ZO2zlk9PWPgusClRcsJmHb4pWUTItDDu4zMBJ5w0yog4HGvB0gCgOoBZrYFdL16AMsmmD5AcQ3ofj3AwbOAX38BIhMw02ZjtQ+tLnht66mCAGA2eka1G3eabUZwD/PLbeuHenKMBODVV0Dru/TTQLgKAc1BvQ/9yAEkOnlon66oehEBIHp7dbSyPoIc0NEADMDnAa4wAvVK+CADRGx/VpNSi+iF3jMTUH4rJQ0aISPmVk+JoJiZeJw1QnaqL2OmWKSFkxnrZWsbXK4TzO5tmS+8TYaGhv6xvgEEfAgHGc7HRgAAAABJRU5ErkJggg==';
case 'https://interactivebrokers.com':
return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAkFBMVEXZICceHh4dHR0fHx8gICAUFBQTExMZGRkhISEaGhoVFRUYGBgSEhIXFxcWFhYcHBwbGxsiIiIRERG5HyTiICiyHyPEHyWtHiO/HyUvGxzOICYAFBMVGRkKFhYAGRgQFRVdGRtxHR+JHiGDHiI/HB2UHyJYGhxmGBolGxtIGxw2GxulHyN4HSCcHSIPHBxRHB764qjxAAADtUlEQVRYhYWXbZeiOBCFa5zpYW1X6W0gvBhAFBQV8f//u00RSFIh0HX6S3vOfby3kpQJ/Kfrn7H+DPUb6xfWBsvH+sLaivKwdqL28LNeA+Z6E+DQuwxsJ4DUrwOIvk2JgVGvAUsGNiNgU7UuAwrwU4DiXPUu/QSg+nmAosp8p34ErOs3fn+HSzFfAVEHAvizEKC/Z2HhNHCQgB8C8DoIm8IZgAIWAvDHE6DmzgASsL4CPG0gfPoLBhCwrv/aXDKAE1/QG4CFBvKb0McPvnUGQMCqAVwAAOgsA3tl4BN+aECP+uDOFwJ8KoCp12eA+0zo4Sz11MCBANxbiLcx6pNrv2iAAmb6BvXwWlqBTwVwbiG/xQUEyKp+McAImOmlgWrQQ9RyMgWJ3gTY+uvQQMi6YrYDNOADFhtQJ4MeWDozcNAGbIAy4PuR1MO5mAXYaz0CnPo+DqU+ww54Cx34QIA7wHnUh6+5ASOABVAGeAdTPfhaAAFwjfF+XEBRcWFvIf39BoAG6E9s0oe4i1cCEIDSvwMVIBAtdAwBrZcAEqCvY6UPK74e4OMv2Ab88QQNldRogPO8KHLuCvB3ANAVnBYQDVzE93Pe3qtLd033pUOPAKL3U60H9u493p6iECvrHuUsAAJ+LwOalvPdWf0b1+VMPwH0EDAA2a33Cr2lxGRBD8YKDAD7EBuAOOV8mxkAuJW23gTInwENyLre629GInEw0pIGGAHm74gGBCn3itg0AOxeWgY0YLxJGICm8LyCEQC8c8vAv0ACmAD24AJAWgDhADD1CCB3MQ14CQNeERFAhhE+XAB1F9QA0ULPO3ZmE+H5KC0DAvBrAfB8596Ot8RBZ3+/AFiXUQVIotc73+2KS0gNOAHGZVYBoih43fMdNw5n8J7rNWC8DE+ARACCV53vedqxUHwWQlNTvQZsHIAIAUHTlnu+T0/N81XVh0lvGlCA6TY+ApgEJOdWzJEyP4oqP+cdlABymx8BgQQE7CImkTUFiV4A6HNCAlg8AhJWcXuMEsA3WO8ZCQgUIGG3fM2AAZDPiQHAIgMQ3I4regkw3kMDIIkNAItOR3sIqAASYL6HEJBFBMDi69GtNwFfBoDFFJDFde5s4LcEkAedAKABCsBD4GzAAPBtABogAEEQw9DVAAXQDzoBCOaADJqydAUQAOtFytPs6QBkcN7PG0gA2wkw7MJRnQxiORfPucvAACAPyu3pKusk63YTf1in0qVHwBcB7PJ54WE85q4ACrBym7anKNV//w9w4apH/GbnrAAAAABJRU5ErkJggg==';
case 'https://www.coinbase.com':
return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAclBMVEVHcEz////////////////////////9/v////////////////////////////////////////////////8AUv8AUP8ATP/4+/8ARv9sjv8laP/F1f8KWv+rwf9Mgf/u9f/R3v/h6f80c/91nv+8zf+Pq/9HDuvmAAAAE3RSTlMAxpwd5o4v/gZyD0axz8tb2NVC3+ryxwAAA01JREFUWIWlV2uXgiAQ1cxIa2tLQFTEZ///L64C0qBonvZ+6RybGWCedzzPiXMcXKKrf0RJgo63Q3QJ4rNb0ongEv4kI1Bi8BM+g53qp+iauID86LRHPfx1qks8wviT/v24rj7iGG0f71t3TjUQ/Oivv+McvY9Pm6bsRMsx5rnoyqZJwSVWAnIOzSlV/2oJo5SSEZQyLF59Zf4OnRbOh0mgKjM86GIAQhnPSmPi4LDw1i8Et7UnGzwrpjssLRj9tHOqSxMEZ+nKHcz7G85W1CVYXmvBmR8ifbUipxvqA2he6KBaCXFS8UP9J/3hHXkvLaAjzAedPx/P13dQ0v7iAXW+9Xxwh0bJ3yf9WD0g3XO+vANXsTCP0BHI2PIwicVn1ulIaA8+lAPwTHLIPpIPwGyeGIQrN/yegAeqbCZFqSiLekBRCjx7HBXVO5SBCkHJ7eRnoq7QVFuFsNOL8FL+cx273BNJmYy6JEwBzuzTTF3hYpK4twRIWyQzFFaMiU6GIaFj2X/TFwwB4fVcf34EfclQ/sReoGpIwBewcqk/eAmeQYXKpsC7KAMw2lS49FHagkMIbrQTooV1ghcOUKhhPrBefos82UhQR+EFKreBSoxtUoN1MshX7yZ9CF3ASuQ2gPoM4CWlfE8WUmrFaOUFYFC8h8XRk78pCBHJHTFcBfLkT4q/NZD838C/n6CcCHJkNQ1GOQj55aj66b4wpo4w3jzJRr5PpIMrlemKE3pYTbrgov3FlCS5s5gCeZOm/bqcY1UML2hg6roWCquhMNNQppaG7Za2cEPfWk2Rm5amnfChqc6btm6q6Dm29atTgoni3dbrlbbuB2CwiPnwwO/BQmf/kQwMFu+kqKlrtOHt0fY4WcO1+3a4Tvwk5XvHe6sLyRDnuzLY7CMIpt4BS/qG4iBAcQDJ+khyJpKVWCTrC5qHbN5viGadbxFNwrgmWHOiCahuhh2BU+qUd2tU1yLb2RrZFqZIXXTdg3Sfszndp/gD3YcLRzIsHGLI4GnfGHK6BQvH4v3GwmLlyceVp9Urz9SrV1ceb9fSlWwsXSOiT2vffVN9QBw+1tV/w127a+S7R9N11+o7Injq5Rv4ww8ve5fvEXL9P9zU+u9fN9b/P6gEACs+iQ7UAAAAAElFTkSuQmCC';
case 'https://www.jpmorgan.com':
return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAY1BMVEUkJSb///8hIiMvMDHd3d0AAACoqaknKCmzs7SioqIODxGlpaYZGxxub2+XmJiNjY0TFRY4ODk/QEDn5+fu7u7V1tb29vbBwcGCg4NTU1TP0NB4eXpdXV5kZGQDBwlqamtGR0icW6P1AAACGElEQVRYhe2Ua5qrIAyGDYpYGhFBxVqr7n+VJ/HWqT3zjAvw+yOE8BJDSBRdunTp0qX/SKx6j5bZNv/2PQCcK0nOxWoeOVYZC+QVNvzwF/Hs+Ml0iW8AGp88+9oDQOV91UBiHlh2vAJOvPcn7Hmzn38gpQG4SYkoLTm85BD3GUAyopT0BaM3V3zRNBt0dNQN4K63E3KMBNoCIMRCZRRCNWx+MjBAfafxCOCwGvAvVPfgV8u8DN1ZQCRrgNaq1NCh9RqzNlVxGqDJVjAgr6AaZ5MQjWlPA4awRmAplDCbbNvE5yNYc5AaSXuSfs5LF+xpwPBcbiE1WtFFGLJh75+nAKO2Wj4bqHpkgNUpQDeJSN+TCc8ATG5SD00a06kMoMujdKCY6sKKM4CQpmkxqoELmAERZvw72FImTgFGqbXFpf4XwEgh9CoEJU4BtrrbAcJR/aVT1dLwALBSir8BEacObp5e8QFgW6jnt00AdvwV8KiZQEccAUuVipiyNFfrNyAL/BQt3WTFbeETIBQBckrSlICfl5Es0Oq9gSB2gWf48hAkO6Rc1/sJXO9ZJF0GzZP3lz2/d99O2xkP01SG0dgBlVI0tXRAYx57CI42UAODkCtatiYkrC57LC21zOokqQvCYR40zzter7O9TQrlXmPel2putNQx41nlFsEyXbIh9vlqWQgCSVuf/mjpb8My/OkQXbp06dKlD/0Do0chlGcV4zUAAAAASUVORK5CYII=';
default:
return UNAVAILABLE_LOGO_URL;
} }
return UNAVAILABLE_LOGO_URL;
} }
} }

4
package-lock.json

@ -1,12 +1,12 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "3.81.0", "version": "3.82.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ghostfolio", "name": "ghostfolio",
"version": "3.81.0", "version": "3.82.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {

2
package.json

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "3.81.0", "version": "3.82.0",
"homepage": "https://ghostfol.io", "homepage": "https://ghostfol.io",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio", "repository": "https://github.com/ghostfolio/ghostfolio",

2
prisma/migrations/20261005010000_removed_index_for_data_source_from_market_data/migration.sql

@ -0,0 +1,2 @@
-- DropIndex
DROP INDEX "MarketData_dataSource_idx";

1
prisma/schema.prisma

@ -174,7 +174,6 @@ model MarketData {
symbol String symbol String
@@unique([dataSource, symbol, date]) @@unique([dataSource, symbol, date])
@@index([dataSource])
@@index([date]) @@index([date])
@@index([state]) @@index([state])
@@index([symbol]) @@index([symbol])

Loading…
Cancel
Save