From 4ab3f813848c7696ee6d15383c4ed54fc7789484 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 3 Mar 2024 20:04:49 +0100 Subject: [PATCH 001/120] Extract getFactor() (#3089) * Extract getFactor() * Refactoring --- apps/api/src/app/order/order.service.ts | 4 +- .../interfaces/portfolio-order.interface.ts | 4 +- .../src/app/portfolio/portfolio-calculator.ts | 45 +++++-------------- .../src/app/portfolio/portfolio.service.ts | 3 +- apps/api/src/helper/portfolio.helper.ts | 21 +++++++++ apps/api/src/models/order.ts | 4 +- .../api/src/services/interfaces/interfaces.ts | 4 +- .../app/services/import-activities.service.ts | 18 ++++---- 8 files changed, 52 insertions(+), 51 deletions(-) create mode 100644 apps/api/src/helper/portfolio.helper.ts diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index 79738c27e..d200ee024 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -19,7 +19,7 @@ import { Order, Prisma, Tag, - Type as TypeOfOrder + Type as ActivityType } from '@prisma/client'; import Big from 'big.js'; import { endOfToday, isAfter } from 'date-fns'; @@ -229,7 +229,7 @@ export class OrderService { sortColumn?: string; sortDirection?: Prisma.SortOrder; take?: number; - types?: TypeOfOrder[]; + types?: ActivityType[]; userCurrency: string; userId: string; withExcludedAccounts?: boolean; diff --git a/apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts b/apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts index cc3a97752..161fbbecb 100644 --- a/apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts @@ -1,4 +1,4 @@ -import { DataSource, Tag, Type as TypeOfOrder } from '@prisma/client'; +import { DataSource, Tag, Type as ActivityType } from '@prisma/client'; import Big from 'big.js'; export interface PortfolioOrder { @@ -10,6 +10,6 @@ export interface PortfolioOrder { quantity: Big; symbol: string; tags?: Tag[]; - type: TypeOfOrder; + type: ActivityType; unitPrice: Big; } diff --git a/apps/api/src/app/portfolio/portfolio-calculator.ts b/apps/api/src/app/portfolio/portfolio-calculator.ts index 9b76aa735..27a5a278b 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.ts @@ -1,3 +1,4 @@ +import { getFactor } from '@ghostfolio/api/helper/portfolio.helper'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; @@ -12,7 +13,6 @@ import { import { GroupBy } from '@ghostfolio/common/types'; import { Logger } from '@nestjs/common'; -import { Type as TypeOfOrder } from '@prisma/client'; import Big from 'big.js'; import { addDays, @@ -76,7 +76,7 @@ export class PortfolioCalculator { let currentTransactionPointItem: TransactionPointSymbol; const oldAccumulatedSymbol = symbols[order.symbol]; - const factor = this.getFactor(order.type); + const factor = getFactor(order.type); const unitPrice = new Big(order.unitPrice); if (oldAccumulatedSymbol) { const newQuantity = order.quantity @@ -820,25 +820,6 @@ export class PortfolioCalculator { }; } - private getFactor(type: TypeOfOrder) { - let factor: number; - - switch (type) { - case 'BUY': - case 'ITEM': - factor = 1; - break; - case 'SELL': - factor = -1; - break; - default: - factor = 0; - break; - } - - return factor; - } - private getSymbolMetrics({ end, exchangeRates, @@ -989,7 +970,7 @@ export class PortfolioCalculator { itemType: 'start', name: '', quantity: new Big(0), - type: TypeOfOrder.BUY, + type: 'BUY', unitPrice: unitPriceAtStartDate }); @@ -1003,7 +984,7 @@ export class PortfolioCalculator { itemType: 'end', name: '', quantity: new Big(0), - type: TypeOfOrder.BUY, + type: 'BUY', unitPrice: unitPriceAtEndDate }); @@ -1030,7 +1011,7 @@ export class PortfolioCalculator { feeInBaseCurrency: new Big(0), name: '', quantity: new Big(0), - type: TypeOfOrder.BUY, + type: 'BUY', unitPrice: marketSymbolMap[format(day, DATE_FORMAT)]?.[symbol] ?? lastUnitPrice @@ -1131,24 +1112,24 @@ export class PortfolioCalculator { order.type === 'BUY' ? order.quantity .mul(order.unitPriceInBaseCurrency) - .mul(this.getFactor(order.type)) + .mul(getFactor(order.type)) : totalUnits.gt(0) ? totalInvestment .div(totalUnits) .mul(order.quantity) - .mul(this.getFactor(order.type)) + .mul(getFactor(order.type)) : new Big(0); const transactionInvestmentWithCurrencyEffect = order.type === 'BUY' ? order.quantity .mul(order.unitPriceInBaseCurrencyWithCurrencyEffect) - .mul(this.getFactor(order.type)) + .mul(getFactor(order.type)) : totalUnits.gt(0) ? totalInvestmentWithCurrencyEffect .div(totalUnits) .mul(order.quantity) - .mul(this.getFactor(order.type)) + .mul(getFactor(order.type)) : new Big(0); if (PortfolioCalculator.ENABLE_LOGGING) { @@ -1203,9 +1184,7 @@ export class PortfolioCalculator { order.feeInBaseCurrencyWithCurrencyEffect ?? 0 ); - totalUnits = totalUnits.plus( - order.quantity.mul(this.getFactor(order.type)) - ); + totalUnits = totalUnits.plus(order.quantity.mul(getFactor(order.type))); const valueOfInvestment = totalUnits.mul(order.unitPriceInBaseCurrency); @@ -1214,14 +1193,14 @@ export class PortfolioCalculator { ); const grossPerformanceFromSell = - order.type === TypeOfOrder.SELL + order.type === 'SELL' ? order.unitPriceInBaseCurrency .minus(lastAveragePrice) .mul(order.quantity) : new Big(0); const grossPerformanceFromSellWithCurrencyEffect = - order.type === TypeOfOrder.SELL + order.type === 'SELL' ? order.unitPriceInBaseCurrencyWithCurrencyEffect .minus(lastAveragePriceWithCurrencyEffect) .mul(order.quantity) diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index f268349c3..d07389049 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -7,6 +7,7 @@ import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.s import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface'; import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface'; import { UserService } from '@ghostfolio/api/app/user/user.service'; +import { getFactor } from '@ghostfolio/api/helper/portfolio.helper'; import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment'; import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account'; import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/base-currency-current-investment'; @@ -2131,7 +2132,7 @@ export class PortfolioService { ?.marketPriceInBaseCurrency ?? 0; if (['LIABILITY', 'SELL'].includes(type)) { - currentValueOfSymbolInBaseCurrency *= -1; + currentValueOfSymbolInBaseCurrency *= getFactor(type); } if (accounts[Account?.id || UNKNOWN_KEY]?.valueInBaseCurrency) { diff --git a/apps/api/src/helper/portfolio.helper.ts b/apps/api/src/helper/portfolio.helper.ts new file mode 100644 index 000000000..01b532cbf --- /dev/null +++ b/apps/api/src/helper/portfolio.helper.ts @@ -0,0 +1,21 @@ +import { Type as ActivityType } from '@prisma/client'; + +export function getFactor(activityType: ActivityType) { + let factor: number; + + switch (activityType) { + case 'BUY': + case 'ITEM': + factor = 1; + break; + case 'LIABILITY': + case 'SELL': + factor = -1; + break; + default: + factor = 0; + break; + } + + return factor; +} diff --git a/apps/api/src/models/order.ts b/apps/api/src/models/order.ts index 214f7e56a..6e6762101 100644 --- a/apps/api/src/models/order.ts +++ b/apps/api/src/models/order.ts @@ -1,6 +1,6 @@ import { IOrder } from '@ghostfolio/api/services/interfaces/interfaces'; -import { Account, SymbolProfile, Type as TypeOfOrder } from '@prisma/client'; +import { Account, SymbolProfile, Type as ActivityType } from '@prisma/client'; import { v4 as uuidv4 } from 'uuid'; export class Order { @@ -14,7 +14,7 @@ export class Order { private symbol: string; private symbolProfile: SymbolProfile; private total: number; - private type: TypeOfOrder; + private type: ActivityType; private unitPrice: number; public constructor(data: IOrder) { diff --git a/apps/api/src/services/interfaces/interfaces.ts b/apps/api/src/services/interfaces/interfaces.ts index bfd29d991..b945d0945 100644 --- a/apps/api/src/services/interfaces/interfaces.ts +++ b/apps/api/src/services/interfaces/interfaces.ts @@ -5,7 +5,7 @@ import { Account, DataSource, SymbolProfile, - Type as TypeOfOrder + Type as ActivityType } from '@prisma/client'; export interface IOrder { @@ -18,7 +18,7 @@ export interface IOrder { quantity: number; symbol: string; symbolProfile: SymbolProfile; - type: TypeOfOrder; + type: ActivityType; unitPrice: number; } diff --git a/apps/client/src/app/services/import-activities.service.ts b/apps/client/src/app/services/import-activities.service.ts index 5375c32aa..c1b2209b3 100644 --- a/apps/client/src/app/services/import-activities.service.ts +++ b/apps/client/src/app/services/import-activities.service.ts @@ -5,7 +5,7 @@ import { parseDate as parseDateHelper } from '@ghostfolio/common/helper'; import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { Account, DataSource, Type } from '@prisma/client'; +import { Account, DataSource, Type as ActivityType } from '@prisma/client'; import { isFinite } from 'lodash'; import { parse as csvToJson } from 'papaparse'; import { EMPTY } from 'rxjs'; @@ -328,26 +328,26 @@ export class ImportActivitiesService { content: any[]; index: number; item: any; - }) { + }): ActivityType { item = this.lowercaseKeys(item); for (const key of ImportActivitiesService.TYPE_KEYS) { if (item[key]) { switch (item[key].toLowerCase()) { case 'buy': - return Type.BUY; + return 'BUY'; case 'dividend': - return Type.DIVIDEND; + return 'DIVIDEND'; case 'fee': - return Type.FEE; + return 'FEE'; case 'interest': - return Type.INTEREST; + return 'INTEREST'; case 'item': - return Type.ITEM; + return 'ITEM'; case 'liability': - return Type.LIABILITY; + return 'LIABILITY'; case 'sell': - return Type.SELL; + return 'SELL'; default: break; } From c37ad9bad4af3200c3cb589afd428bfdb7983a35 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 4 Mar 2024 20:15:41 +0100 Subject: [PATCH 002/120] Bugfix/fix activities import (#3095) * Fix query parameter handling of booleans * Update changelog --- CHANGELOG.md | 4 ++++ apps/api/src/app/import/import.controller.ts | 4 +++- apps/api/src/app/portfolio/portfolio.controller.ts | 7 +++++-- apps/api/src/app/symbol/symbol.controller.ts | 4 +++- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51d86efa1..dbe4e13d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Optimized the calculation of the portfolio summary +### Fixed + +- Fixed the the activities import (query parameter handling) + ## 2.60.0 - 2024-03-02 ### Added diff --git a/apps/api/src/app/import/import.controller.ts b/apps/api/src/app/import/import.controller.ts index b7aff8634..29a06fc9f 100644 --- a/apps/api/src/app/import/import.controller.ts +++ b/apps/api/src/app/import/import.controller.ts @@ -43,8 +43,10 @@ export class ImportController { @UseInterceptors(TransformDataSourceInResponseInterceptor) public async import( @Body() importData: ImportDataDto, - @Query('dryRun') isDryRun = false + @Query('dryRun') isDryRunParam = 'false' ): Promise { + const isDryRun = isDryRunParam === 'true'; + if ( !hasPermission(this.request.user.permissions, permissions.createAccount) ) { diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 8f4acc060..e51321561 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -342,9 +342,12 @@ export class PortfolioController { @Query('assetClasses') filterByAssetClasses?: string, @Query('range') dateRange: DateRange = 'max', @Query('tags') filterByTags?: string, - @Query('withExcludedAccounts') withExcludedAccounts = false, - @Query('withItems') withItems = false + @Query('withExcludedAccounts') withExcludedAccountsParam = 'false', + @Query('withItems') withItemsParam = 'false' ): Promise { + const withExcludedAccounts = withExcludedAccountsParam === 'true'; + const withItems = withItemsParam === 'true'; + const hasReadRestrictedAccessPermission = this.userService.hasReadRestrictedAccessPermission({ impersonationId, diff --git a/apps/api/src/app/symbol/symbol.controller.ts b/apps/api/src/app/symbol/symbol.controller.ts index e41267b79..17e0056d6 100644 --- a/apps/api/src/app/symbol/symbol.controller.ts +++ b/apps/api/src/app/symbol/symbol.controller.ts @@ -39,9 +39,11 @@ export class SymbolController { @UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseInterceptors(TransformDataSourceInResponseInterceptor) public async lookupSymbol( - @Query('includeIndices') includeIndices = false, + @Query('includeIndices') includeIndicesParam = 'false', @Query('query') query = '' ): Promise<{ items: LookupItem[] }> { + const includeIndices = includeIndicesParam === 'true'; + try { return this.symbolService.lookup({ includeIndices, From 144d83195454030cc7368c64ffa61d2e49493726 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 4 Mar 2024 20:17:05 +0100 Subject: [PATCH 003/120] Release 2.61.0 (#3097) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbe4e13d9..e19467e59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## 2.61.0 - 2024-03-04 ### Changed diff --git a/package.json b/package.json index a23717491..cc83d588e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "2.60.0", + "version": "2.61.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio", From f1dc075c360c8aabebb271ad9be0d20e6199ae4a Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 5 Mar 2024 10:16:59 +0100 Subject: [PATCH 004/120] Update translations (#3093) --- apps/client/src/locales/messages.de.xlf | 64 ++++++++++++------------- apps/client/src/locales/messages.es.xlf | 64 ++++++++++++------------- apps/client/src/locales/messages.fr.xlf | 64 ++++++++++++------------- apps/client/src/locales/messages.it.xlf | 64 ++++++++++++------------- apps/client/src/locales/messages.nl.xlf | 64 ++++++++++++------------- apps/client/src/locales/messages.pl.xlf | 64 ++++++++++++------------- apps/client/src/locales/messages.pt.xlf | 64 ++++++++++++------------- apps/client/src/locales/messages.tr.xlf | 64 ++++++++++++------------- apps/client/src/locales/messages.xlf | 64 ++++++++++++------------- 9 files changed, 288 insertions(+), 288 deletions(-) diff --git a/apps/client/src/locales/messages.de.xlf b/apps/client/src/locales/messages.de.xlf index ff863284d..18f341aac 100644 --- a/apps/client/src/locales/messages.de.xlf +++ b/apps/client/src/locales/messages.de.xlf @@ -196,10 +196,6 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html 201 - - apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 262 - apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html 263 @@ -212,6 +208,10 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html 265 + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 266 + libs/ui/src/lib/account-balances/account-balances.component.html 20 @@ -494,7 +494,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 411 + 412 apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html @@ -534,7 +534,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 418 + 419 @@ -1468,7 +1468,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 260 + 261 @@ -1528,7 +1528,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 366 + 367 libs/ui/src/lib/assistant/assistant.html @@ -2300,7 +2300,7 @@ Zeitstrahl der Investitionen apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 294 + 298 @@ -2316,7 +2316,7 @@ Verlierer apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 214 + 216 @@ -2460,7 +2460,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 266 + 267 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2472,11 +2472,11 @@ Gebühr apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 285 + 286 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 313 + 314 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2496,7 +2496,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 322 + 323 @@ -2520,7 +2520,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 337 + 338 @@ -2884,7 +2884,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 353 + 354 @@ -3020,7 +3020,7 @@ libs/ui/src/lib/i18n.ts - 65 + 69 @@ -3032,7 +3032,7 @@ libs/ui/src/lib/i18n.ts - 66 + 70 @@ -3156,7 +3156,7 @@ Portfolio Wertentwicklung apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 264 + 268 @@ -3332,7 +3332,7 @@ Nordamerika libs/ui/src/lib/i18n.ts - 58 + 62 @@ -3340,7 +3340,7 @@ Afrika libs/ui/src/lib/i18n.ts - 55 + 59 @@ -3348,7 +3348,7 @@ Asien libs/ui/src/lib/i18n.ts - 56 + 60 @@ -3356,7 +3356,7 @@ Europa libs/ui/src/lib/i18n.ts - 57 + 61 @@ -3364,7 +3364,7 @@ Ozeanien libs/ui/src/lib/i18n.ts - 59 + 63 @@ -3372,7 +3372,7 @@ Südamerika libs/ui/src/lib/i18n.ts - 60 + 64 @@ -3464,7 +3464,7 @@ Zeitstrahl der Dividenden apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 352 + 356 @@ -4008,7 +4008,7 @@ Ups! Der historische Wechselkurs konnte nicht abgerufen werden vom apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 303 + 304 @@ -4320,7 +4320,7 @@ Aktueller Streak apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 315 + 319 @@ -4328,7 +4328,7 @@ Längster Streak apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 324 + 328 @@ -13740,7 +13740,7 @@ Extreme Angst libs/ui/src/lib/i18n.ts - 63 + 67 @@ -13748,7 +13748,7 @@ Extreme Gier libs/ui/src/lib/i18n.ts - 64 + 68 @@ -13756,7 +13756,7 @@ Neutral libs/ui/src/lib/i18n.ts - 67 + 71 diff --git a/apps/client/src/locales/messages.es.xlf b/apps/client/src/locales/messages.es.xlf index 6b281bd92..0ad1ae585 100644 --- a/apps/client/src/locales/messages.es.xlf +++ b/apps/client/src/locales/messages.es.xlf @@ -197,10 +197,6 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html 201 - - apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 262 - apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html 263 @@ -213,6 +209,10 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html 265 + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 266 + libs/ui/src/lib/account-balances/account-balances.component.html 20 @@ -495,7 +495,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 411 + 412 apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html @@ -535,7 +535,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 418 + 419 @@ -1466,7 +1466,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 260 + 261 @@ -1526,7 +1526,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 366 + 367 libs/ui/src/lib/assistant/assistant.html @@ -2298,7 +2298,7 @@ Cronología de la inversión apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 294 + 298 @@ -2314,7 +2314,7 @@ Lo peor apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 214 + 216 @@ -2458,7 +2458,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 266 + 267 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2470,11 +2470,11 @@ Comisión apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 285 + 286 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 313 + 314 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2494,7 +2494,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 322 + 323 @@ -2518,7 +2518,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 337 + 338 @@ -2870,7 +2870,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 353 + 354 @@ -3018,7 +3018,7 @@ libs/ui/src/lib/i18n.ts - 65 + 69 @@ -3030,7 +3030,7 @@ libs/ui/src/lib/i18n.ts - 66 + 70 @@ -3154,7 +3154,7 @@ Evolución cartera apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 264 + 268 @@ -3330,7 +3330,7 @@ América del Norte libs/ui/src/lib/i18n.ts - 58 + 62 @@ -3338,7 +3338,7 @@ África libs/ui/src/lib/i18n.ts - 55 + 59 @@ -3346,7 +3346,7 @@ Asia libs/ui/src/lib/i18n.ts - 56 + 60 @@ -3354,7 +3354,7 @@ Europa libs/ui/src/lib/i18n.ts - 57 + 61 @@ -3362,7 +3362,7 @@ Oceanía libs/ui/src/lib/i18n.ts - 59 + 63 @@ -3370,7 +3370,7 @@ América del Sur libs/ui/src/lib/i18n.ts - 60 + 64 @@ -3474,7 +3474,7 @@ Dividend Timeline apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 352 + 356 @@ -4006,7 +4006,7 @@ Oops! Could not get the historical exchange rate from apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 303 + 304 @@ -4318,7 +4318,7 @@ Current Streak apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 315 + 319 @@ -4326,7 +4326,7 @@ Longest Streak apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 324 + 328 @@ -13738,7 +13738,7 @@ Extreme Fear libs/ui/src/lib/i18n.ts - 63 + 67 @@ -13746,7 +13746,7 @@ Extreme Greed libs/ui/src/lib/i18n.ts - 64 + 68 @@ -13754,7 +13754,7 @@ Neutral libs/ui/src/lib/i18n.ts - 67 + 71 diff --git a/apps/client/src/locales/messages.fr.xlf b/apps/client/src/locales/messages.fr.xlf index bc24987e3..21a67fe36 100644 --- a/apps/client/src/locales/messages.fr.xlf +++ b/apps/client/src/locales/messages.fr.xlf @@ -252,10 +252,6 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html 201 - - apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 262 - apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html 263 @@ -268,6 +264,10 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html 265 + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 266 + libs/ui/src/lib/account-balances/account-balances.component.html 20 @@ -550,7 +550,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 411 + 412 apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html @@ -590,7 +590,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 418 + 419 @@ -622,7 +622,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 337 + 338 @@ -646,7 +646,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 353 + 354 @@ -826,7 +826,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 322 + 323 @@ -922,7 +922,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 366 + 367 libs/ui/src/lib/assistant/assistant.html @@ -1534,7 +1534,7 @@ libs/ui/src/lib/i18n.ts - 65 + 69 @@ -1546,7 +1546,7 @@ libs/ui/src/lib/i18n.ts - 66 + 70 @@ -1837,7 +1837,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 260 + 261 @@ -2665,7 +2665,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 266 + 267 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2677,11 +2677,11 @@ Frais apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 285 + 286 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 313 + 314 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2941,7 +2941,7 @@ Bas apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 214 + 216 @@ -2949,7 +2949,7 @@ Évolution du Portefeuille apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 264 + 268 @@ -2957,7 +2957,7 @@ Historique des Investissements apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 294 + 298 @@ -2965,7 +2965,7 @@ Historique des Dividendes apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 352 + 356 @@ -3489,7 +3489,7 @@ Afrique libs/ui/src/lib/i18n.ts - 55 + 59 @@ -3497,7 +3497,7 @@ Asie libs/ui/src/lib/i18n.ts - 56 + 60 @@ -3505,7 +3505,7 @@ Europe libs/ui/src/lib/i18n.ts - 57 + 61 @@ -3513,7 +3513,7 @@ Amérique du Nord libs/ui/src/lib/i18n.ts - 58 + 62 @@ -3521,7 +3521,7 @@ Océanie libs/ui/src/lib/i18n.ts - 59 + 63 @@ -3529,7 +3529,7 @@ Amérique du Sud libs/ui/src/lib/i18n.ts - 60 + 64 @@ -4005,7 +4005,7 @@ Oups ! Nous n'avons pas pu obtenir le taux de change historique à partir de apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 303 + 304 @@ -4317,7 +4317,7 @@ Série en cours apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 315 + 319 @@ -4325,7 +4325,7 @@ Série la plus longue apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 324 + 328 @@ -13737,7 +13737,7 @@ Extreme Fear libs/ui/src/lib/i18n.ts - 63 + 67 @@ -13745,7 +13745,7 @@ Extreme Greed libs/ui/src/lib/i18n.ts - 64 + 68 @@ -13753,7 +13753,7 @@ Neutral libs/ui/src/lib/i18n.ts - 67 + 71 diff --git a/apps/client/src/locales/messages.it.xlf b/apps/client/src/locales/messages.it.xlf index ea76f547d..049c7964e 100644 --- a/apps/client/src/locales/messages.it.xlf +++ b/apps/client/src/locales/messages.it.xlf @@ -197,10 +197,6 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html 201 - - apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 262 - apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html 263 @@ -213,6 +209,10 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html 265 + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 266 + libs/ui/src/lib/account-balances/account-balances.component.html 20 @@ -495,7 +495,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 411 + 412 apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html @@ -535,7 +535,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 418 + 419 @@ -1466,7 +1466,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 260 + 261 @@ -1526,7 +1526,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 366 + 367 libs/ui/src/lib/assistant/assistant.html @@ -2298,7 +2298,7 @@ Cronologia degli investimenti apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 294 + 298 @@ -2314,7 +2314,7 @@ In basso apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 214 + 216 @@ -2458,7 +2458,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 266 + 267 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2470,11 +2470,11 @@ Commissione apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 285 + 286 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 313 + 314 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2494,7 +2494,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 322 + 323 @@ -2518,7 +2518,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 337 + 338 @@ -2870,7 +2870,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 353 + 354 @@ -3018,7 +3018,7 @@ libs/ui/src/lib/i18n.ts - 65 + 69 @@ -3030,7 +3030,7 @@ libs/ui/src/lib/i18n.ts - 66 + 70 @@ -3154,7 +3154,7 @@ Evoluzione del portafoglio apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 264 + 268 @@ -3330,7 +3330,7 @@ Nord America libs/ui/src/lib/i18n.ts - 58 + 62 @@ -3338,7 +3338,7 @@ Africa libs/ui/src/lib/i18n.ts - 55 + 59 @@ -3346,7 +3346,7 @@ Asia libs/ui/src/lib/i18n.ts - 56 + 60 @@ -3354,7 +3354,7 @@ Europa libs/ui/src/lib/i18n.ts - 57 + 61 @@ -3362,7 +3362,7 @@ Oceania libs/ui/src/lib/i18n.ts - 59 + 63 @@ -3370,7 +3370,7 @@ Sud America libs/ui/src/lib/i18n.ts - 60 + 64 @@ -3474,7 +3474,7 @@ Cronologia dei dividendi apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 352 + 356 @@ -4006,7 +4006,7 @@ Ops! Impossibile ottenere il tasso di cambio storico da apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 303 + 304 @@ -4318,7 +4318,7 @@ Serie attuale apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 315 + 319 @@ -4326,7 +4326,7 @@ Serie più lunga apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 324 + 328 @@ -13738,7 +13738,7 @@ Extreme Fear libs/ui/src/lib/i18n.ts - 63 + 67 @@ -13746,7 +13746,7 @@ Extreme Greed libs/ui/src/lib/i18n.ts - 64 + 68 @@ -13754,7 +13754,7 @@ Neutral libs/ui/src/lib/i18n.ts - 67 + 71 diff --git a/apps/client/src/locales/messages.nl.xlf b/apps/client/src/locales/messages.nl.xlf index f4db03880..c70186bef 100644 --- a/apps/client/src/locales/messages.nl.xlf +++ b/apps/client/src/locales/messages.nl.xlf @@ -196,10 +196,6 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html 201 - - apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 262 - apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html 263 @@ -212,6 +208,10 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html 265 + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 266 + libs/ui/src/lib/account-balances/account-balances.component.html 20 @@ -494,7 +494,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 411 + 412 apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html @@ -534,7 +534,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 418 + 419 @@ -1465,7 +1465,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 260 + 261 @@ -1525,7 +1525,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 366 + 367 libs/ui/src/lib/assistant/assistant.html @@ -2297,7 +2297,7 @@ Tijdlijn investeringen apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 294 + 298 @@ -2313,7 +2313,7 @@ Verliezers apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 214 + 216 @@ -2457,7 +2457,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 266 + 267 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2469,11 +2469,11 @@ Transactiekosten apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 285 + 286 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 313 + 314 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2493,7 +2493,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 322 + 323 @@ -2517,7 +2517,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 337 + 338 @@ -2869,7 +2869,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 353 + 354 @@ -3017,7 +3017,7 @@ libs/ui/src/lib/i18n.ts - 65 + 69 @@ -3029,7 +3029,7 @@ libs/ui/src/lib/i18n.ts - 66 + 70 @@ -3153,7 +3153,7 @@ Waardeontwikkeling van portefeuille apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 264 + 268 @@ -3329,7 +3329,7 @@ Noord-Amerika libs/ui/src/lib/i18n.ts - 58 + 62 @@ -3337,7 +3337,7 @@ Afrika libs/ui/src/lib/i18n.ts - 55 + 59 @@ -3345,7 +3345,7 @@ Azië libs/ui/src/lib/i18n.ts - 56 + 60 @@ -3353,7 +3353,7 @@ Europa libs/ui/src/lib/i18n.ts - 57 + 61 @@ -3361,7 +3361,7 @@ Oceanië libs/ui/src/lib/i18n.ts - 59 + 63 @@ -3369,7 +3369,7 @@ Zuid-Amerika libs/ui/src/lib/i18n.ts - 60 + 64 @@ -3473,7 +3473,7 @@ Tijdlijn dividend apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 352 + 356 @@ -4005,7 +4005,7 @@ Oeps! Kon de historische wisselkoers niet krijgen van apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 303 + 304 @@ -4317,7 +4317,7 @@ Huidige reeks apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 315 + 319 @@ -4325,7 +4325,7 @@ Langste reeks apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 324 + 328 @@ -13737,7 +13737,7 @@ Extreme Fear libs/ui/src/lib/i18n.ts - 63 + 67 @@ -13745,7 +13745,7 @@ Extreme Greed libs/ui/src/lib/i18n.ts - 64 + 68 @@ -13753,7 +13753,7 @@ Neutral libs/ui/src/lib/i18n.ts - 67 + 71 diff --git a/apps/client/src/locales/messages.pl.xlf b/apps/client/src/locales/messages.pl.xlf index a18db441a..e5e5d6b27 100644 --- a/apps/client/src/locales/messages.pl.xlf +++ b/apps/client/src/locales/messages.pl.xlf @@ -1760,10 +1760,6 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html 201 - - apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 262 - apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html 263 @@ -1776,6 +1772,10 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html 265 + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 266 + libs/ui/src/lib/account-balances/account-balances.component.html 20 @@ -2058,7 +2058,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 411 + 412 apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html @@ -2098,7 +2098,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 418 + 419 @@ -2162,7 +2162,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 337 + 338 @@ -2186,7 +2186,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 353 + 354 @@ -2414,7 +2414,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 322 + 323 @@ -2710,7 +2710,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 366 + 367 libs/ui/src/lib/assistant/assistant.html @@ -2998,7 +2998,7 @@ libs/ui/src/lib/i18n.ts - 65 + 69 @@ -3010,7 +3010,7 @@ libs/ui/src/lib/i18n.ts - 66 + 70 @@ -3440,7 +3440,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 260 + 261 @@ -5020,7 +5020,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 266 + 267 libs/ui/src/lib/activities-table/activities-table.component.html @@ -5040,11 +5040,11 @@ Fee apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 285 + 286 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 313 + 314 libs/ui/src/lib/activities-table/activities-table.component.html @@ -5056,7 +5056,7 @@ Oops! Could not get the historical exchange rate from apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 303 + 304 @@ -5412,7 +5412,7 @@ Bottom apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 214 + 216 @@ -5420,7 +5420,7 @@ Portfolio Evolution apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 264 + 268 @@ -5428,7 +5428,7 @@ Investment Timeline apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 294 + 298 @@ -5436,7 +5436,7 @@ Current Streak apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 315 + 319 @@ -5444,7 +5444,7 @@ Longest Streak apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 324 + 328 @@ -5452,7 +5452,7 @@ Dividend Timeline apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 352 + 356 @@ -13672,7 +13672,7 @@ Africa libs/ui/src/lib/i18n.ts - 55 + 59 @@ -13680,7 +13680,7 @@ Asia libs/ui/src/lib/i18n.ts - 56 + 60 @@ -13688,7 +13688,7 @@ Europe libs/ui/src/lib/i18n.ts - 57 + 61 @@ -13696,7 +13696,7 @@ North America libs/ui/src/lib/i18n.ts - 58 + 62 @@ -13704,7 +13704,7 @@ Oceania libs/ui/src/lib/i18n.ts - 59 + 63 @@ -13712,7 +13712,7 @@ South America libs/ui/src/lib/i18n.ts - 60 + 64 @@ -13720,7 +13720,7 @@ Extreme Fear libs/ui/src/lib/i18n.ts - 63 + 67 @@ -13728,7 +13728,7 @@ Extreme Greed libs/ui/src/lib/i18n.ts - 64 + 68 @@ -13736,7 +13736,7 @@ Neutral libs/ui/src/lib/i18n.ts - 67 + 71 diff --git a/apps/client/src/locales/messages.pt.xlf b/apps/client/src/locales/messages.pt.xlf index 52f19e666..2d3eeaa55 100644 --- a/apps/client/src/locales/messages.pt.xlf +++ b/apps/client/src/locales/messages.pt.xlf @@ -252,10 +252,6 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html 201 - - apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 262 - apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html 263 @@ -268,6 +264,10 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html 265 + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 266 + libs/ui/src/lib/account-balances/account-balances.component.html 20 @@ -550,7 +550,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 411 + 412 apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html @@ -590,7 +590,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 418 + 419 @@ -622,7 +622,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 337 + 338 @@ -646,7 +646,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 353 + 354 @@ -1402,7 +1402,7 @@ libs/ui/src/lib/i18n.ts - 65 + 69 @@ -1414,7 +1414,7 @@ libs/ui/src/lib/i18n.ts - 66 + 70 @@ -1713,7 +1713,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 260 + 261 @@ -1849,7 +1849,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 366 + 367 libs/ui/src/lib/assistant/assistant.html @@ -2573,7 +2573,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 266 + 267 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2585,11 +2585,11 @@ Comissão apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 285 + 286 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 313 + 314 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2609,7 +2609,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 322 + 323 @@ -2821,7 +2821,7 @@ Fundo apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 214 + 216 @@ -2829,7 +2829,7 @@ Evolução do Portefólio apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 264 + 268 @@ -2837,7 +2837,7 @@ Cronograma de Investimento apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 294 + 298 @@ -3341,7 +3341,7 @@ África libs/ui/src/lib/i18n.ts - 55 + 59 @@ -3349,7 +3349,7 @@ Ásia libs/ui/src/lib/i18n.ts - 56 + 60 @@ -3357,7 +3357,7 @@ Europa libs/ui/src/lib/i18n.ts - 57 + 61 @@ -3365,7 +3365,7 @@ América do Norte libs/ui/src/lib/i18n.ts - 58 + 62 @@ -3373,7 +3373,7 @@ Oceânia libs/ui/src/lib/i18n.ts - 59 + 63 @@ -3381,7 +3381,7 @@ América do Sul libs/ui/src/lib/i18n.ts - 60 + 64 @@ -3541,7 +3541,7 @@ Cronograma de Dividendos apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 352 + 356 @@ -4005,7 +4005,7 @@ Oops! Não foi possível obter a taxa de câmbio histórica de apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 303 + 304 @@ -4317,7 +4317,7 @@ Série Atual apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 315 + 319 @@ -4325,7 +4325,7 @@ Série mais Longa apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 324 + 328 @@ -13737,7 +13737,7 @@ Extreme Fear libs/ui/src/lib/i18n.ts - 63 + 67 @@ -13745,7 +13745,7 @@ Extreme Greed libs/ui/src/lib/i18n.ts - 64 + 68 @@ -13753,7 +13753,7 @@ Neutral libs/ui/src/lib/i18n.ts - 67 + 71 diff --git a/apps/client/src/locales/messages.tr.xlf b/apps/client/src/locales/messages.tr.xlf index 1c91aa8de..1e137c100 100644 --- a/apps/client/src/locales/messages.tr.xlf +++ b/apps/client/src/locales/messages.tr.xlf @@ -1724,10 +1724,6 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html 201 - - apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 262 - apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html 263 @@ -1740,6 +1736,10 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html 265 + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 266 + libs/ui/src/lib/account-balances/account-balances.component.html 20 @@ -2022,7 +2022,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 411 + 412 apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html @@ -2062,7 +2062,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 418 + 419 @@ -2118,7 +2118,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 337 + 338 @@ -2142,7 +2142,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 353 + 354 @@ -2338,7 +2338,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 322 + 323 @@ -2454,7 +2454,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 366 + 367 libs/ui/src/lib/assistant/assistant.html @@ -2850,7 +2850,7 @@ libs/ui/src/lib/i18n.ts - 65 + 69 @@ -2862,7 +2862,7 @@ libs/ui/src/lib/i18n.ts - 66 + 70 @@ -3265,7 +3265,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 260 + 261 @@ -4497,7 +4497,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 266 + 267 libs/ui/src/lib/activities-table/activities-table.component.html @@ -4509,7 +4509,7 @@ Hay Allah! Geçmiş döviz kuru alınamadı: apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 303 + 304 @@ -4517,11 +4517,11 @@ Komisyon apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 285 + 286 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 313 + 314 libs/ui/src/lib/activities-table/activities-table.component.html @@ -4889,7 +4889,7 @@ Alt apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 214 + 216 @@ -4897,7 +4897,7 @@ Portföyün Gelişimi apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 264 + 268 @@ -4905,7 +4905,7 @@ Yatırım Zaman Çizelgesi apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 294 + 298 @@ -4913,7 +4913,7 @@ Güncel Seri apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 315 + 319 @@ -4921,7 +4921,7 @@ En Uzun Seri apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 324 + 328 @@ -4929,7 +4929,7 @@ Temettü Zaman Çizelgesi apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 352 + 356 @@ -13077,7 +13077,7 @@ Africa libs/ui/src/lib/i18n.ts - 55 + 59 @@ -13085,7 +13085,7 @@ Asia libs/ui/src/lib/i18n.ts - 56 + 60 @@ -13093,7 +13093,7 @@ Europe libs/ui/src/lib/i18n.ts - 57 + 61 @@ -13101,7 +13101,7 @@ North America libs/ui/src/lib/i18n.ts - 58 + 62 @@ -13109,7 +13109,7 @@ Oceania libs/ui/src/lib/i18n.ts - 59 + 63 @@ -13117,7 +13117,7 @@ South America libs/ui/src/lib/i18n.ts - 60 + 64 @@ -13737,7 +13737,7 @@ Extreme Fear libs/ui/src/lib/i18n.ts - 63 + 67 @@ -13745,7 +13745,7 @@ Extreme Greed libs/ui/src/lib/i18n.ts - 64 + 68 @@ -13753,7 +13753,7 @@ Neutral libs/ui/src/lib/i18n.ts - 67 + 71 diff --git a/apps/client/src/locales/messages.xlf b/apps/client/src/locales/messages.xlf index d50ca4a31..871afeace 100644 --- a/apps/client/src/locales/messages.xlf +++ b/apps/client/src/locales/messages.xlf @@ -1729,10 +1729,6 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html 201 - - apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 262 - apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html 263 @@ -1745,6 +1741,10 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html 265 + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 266 + libs/ui/src/lib/account-balances/account-balances.component.html 20 @@ -2008,7 +2008,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 411 + 412 apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html @@ -2047,7 +2047,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 418 + 419 @@ -2105,7 +2105,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 337 + 338 @@ -2128,7 +2128,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 353 + 354 @@ -2336,7 +2336,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 322 + 323 @@ -2607,7 +2607,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 366 + 367 libs/ui/src/lib/assistant/assistant.html @@ -2865,7 +2865,7 @@ libs/ui/src/lib/i18n.ts - 65 + 69 @@ -2876,7 +2876,7 @@ libs/ui/src/lib/i18n.ts - 66 + 70 @@ -3259,7 +3259,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 260 + 261 @@ -4673,7 +4673,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 266 + 267 libs/ui/src/lib/activities-table/activities-table.component.html @@ -4691,11 +4691,11 @@ Fee apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 285 + 286 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 313 + 314 libs/ui/src/lib/activities-table/activities-table.component.html @@ -4706,7 +4706,7 @@ Oops! Could not get the historical exchange rate from apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 303 + 304 @@ -5022,42 +5022,42 @@ Bottom apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 214 + 216 Portfolio Evolution apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 264 + 268 Investment Timeline apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 294 + 298 Current Streak apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 315 + 319 Longest Streak apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 324 + 328 Dividend Timeline apps/client/src/app/pages/portfolio/analysis/analysis-page.html - 352 + 356 @@ -13946,63 +13946,63 @@ Africa libs/ui/src/lib/i18n.ts - 55 + 59 Asia libs/ui/src/lib/i18n.ts - 56 + 60 Europe libs/ui/src/lib/i18n.ts - 57 + 61 North America libs/ui/src/lib/i18n.ts - 58 + 62 Oceania libs/ui/src/lib/i18n.ts - 59 + 63 South America libs/ui/src/lib/i18n.ts - 60 + 64 Extreme Fear libs/ui/src/lib/i18n.ts - 63 + 67 Extreme Greed libs/ui/src/lib/i18n.ts - 64 + 68 Neutral libs/ui/src/lib/i18n.ts - 67 + 71 From f3a8822a7710f3e16616f7f224cf846e1ebf4056 Mon Sep 17 00:00:00 2001 From: Gerard Du Pre <37554513+GerardPolloRebozado@users.noreply.github.com> Date: Wed, 6 Mar 2024 11:11:10 +0100 Subject: [PATCH 005/120] Feature/remove-v-from-version-in-admin-endpoint (#3101) * Remove "v" from version in admin endpoint --- apps/api/src/environments/environment.prod.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/src/environments/environment.prod.ts b/apps/api/src/environments/environment.prod.ts index bc8aa65a4..81b324963 100644 --- a/apps/api/src/environments/environment.prod.ts +++ b/apps/api/src/environments/environment.prod.ts @@ -1,4 +1,4 @@ export const environment = { production: true, - version: `v${require('../../../../package.json').version}` + version: `${require('../../../../package.json').version}` }; From c54392b7bbc0a653b4fcba9d9c4c62103706011f Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 6 Mar 2024 22:06:27 +0100 Subject: [PATCH 006/120] Bugfix/fix exception in account value calculation (#3109) * Fix exception in value of account calculation caused by liabilities * Update changelog --- CHANGELOG.md | 6 + .../src/app/portfolio/portfolio.service.ts | 125 ++++++++++-------- 2 files changed, 77 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e19467e59..0dce1cf4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Fixed + +- Fixed an issue in the account value calculation caused by liabilities + ## 2.61.0 - 2024-03-04 ### Changed diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index d07389049..0d14e35df 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -439,15 +439,33 @@ export class PortfolioService { portfolioItemsNow[position.symbol] = position; } - for (const item of currentPositions.positions) { - if (item.quantity.lte(0)) { + for (const { + currency, + firstBuyDate, + grossPerformance, + grossPerformanceWithCurrencyEffect, + grossPerformancePercentage, + grossPerformancePercentageWithCurrencyEffect, + investment, + marketPrice, + marketPriceInBaseCurrency, + netPerformance, + netPerformancePercentage, + netPerformancePercentageWithCurrencyEffect, + netPerformanceWithCurrencyEffect, + quantity, + symbol, + tags, + transactionCount + } of currentPositions.positions) { + if (quantity.eq(0)) { // Ignore positions without any quantity continue; } - const value = item.quantity.mul(item.marketPriceInBaseCurrency ?? 0); - const symbolProfile = symbolProfileMap[item.symbol]; - const dataProviderResponse = dataProviderResponses[item.symbol]; + const value = quantity.mul(marketPriceInBaseCurrency ?? 0); + const symbolProfile = symbolProfileMap[symbol]; + const dataProviderResponse = dataProviderResponses[symbol]; const markets: PortfolioPosition['markets'] = { [UNKNOWN_KEY]: 0, @@ -519,40 +537,39 @@ export class PortfolioService { .toNumber(); } - holdings[item.symbol] = { + holdings[symbol] = { + currency, markets, marketsAdvanced, + marketPrice, + symbol, + tags, + transactionCount, allocationInPercentage: filteredValueInBaseCurrency.eq(0) ? 0 : value.div(filteredValueInBaseCurrency).toNumber(), assetClass: symbolProfile.assetClass, assetSubClass: symbolProfile.assetSubClass, countries: symbolProfile.countries, - currency: item.currency, dataSource: symbolProfile.dataSource, - dateOfFirstActivity: parseDate(item.firstBuyDate), - grossPerformance: item.grossPerformance?.toNumber() ?? 0, - grossPerformancePercent: - item.grossPerformancePercentage?.toNumber() ?? 0, + dateOfFirstActivity: parseDate(firstBuyDate), + grossPerformance: grossPerformance?.toNumber() ?? 0, + grossPerformancePercent: grossPerformancePercentage?.toNumber() ?? 0, grossPerformancePercentWithCurrencyEffect: - item.grossPerformancePercentageWithCurrencyEffect?.toNumber() ?? 0, + grossPerformancePercentageWithCurrencyEffect?.toNumber() ?? 0, grossPerformanceWithCurrencyEffect: - item.grossPerformanceWithCurrencyEffect?.toNumber() ?? 0, - investment: item.investment.toNumber(), - marketPrice: item.marketPrice, + grossPerformanceWithCurrencyEffect?.toNumber() ?? 0, + investment: investment.toNumber(), marketState: dataProviderResponse?.marketState ?? 'delayed', name: symbolProfile.name, - netPerformance: item.netPerformance?.toNumber() ?? 0, - netPerformancePercent: item.netPerformancePercentage?.toNumber() ?? 0, + netPerformance: netPerformance?.toNumber() ?? 0, + netPerformancePercent: netPerformancePercentage?.toNumber() ?? 0, netPerformancePercentWithCurrencyEffect: - item.netPerformancePercentageWithCurrencyEffect?.toNumber() ?? 0, + netPerformancePercentageWithCurrencyEffect?.toNumber() ?? 0, netPerformanceWithCurrencyEffect: - item.netPerformanceWithCurrencyEffect?.toNumber() ?? 0, - quantity: item.quantity.toNumber(), + netPerformanceWithCurrencyEffect?.toNumber() ?? 0, + quantity: quantity.toNumber(), sectors: symbolProfile.sectors, - symbol: item.symbol, - tags: item.tags, - transactionCount: item.transactionCount, url: symbolProfile.url, valueInBaseCurrency: value.toNumber() }; @@ -1770,24 +1787,33 @@ export class PortfolioService { activityType: 'INTEREST' }).toNumber(); - const items = Object.keys(holdings) - .filter((symbol) => { - return isUUID(symbol) && holdings[symbol].dataSource === 'MANUAL'; - }) - .map((symbol) => { - return holdings[symbol].valueInBaseCurrency; - }) - .reduce( - (previous, current) => new Big(previous).plus(current), - new Big(0) - ) - .toNumber(); - - const liabilities = this.getSumOfActivityType({ - activities, - userCurrency, - activityType: 'LIABILITY' - }).toNumber(); + const items = getSum( + Object.keys(holdings) + .filter((symbol) => { + return ( + isUUID(symbol) && + holdings[symbol].dataSource === 'MANUAL' && + holdings[symbol].valueInBaseCurrency > 0 + ); + }) + .map((symbol) => { + return new Big(holdings[symbol].valueInBaseCurrency).abs(); + }) + ).toNumber(); + + const liabilities = getSum( + Object.keys(holdings) + .filter((symbol) => { + return ( + isUUID(symbol) && + holdings[symbol].dataSource === 'MANUAL' && + holdings[symbol].valueInBaseCurrency < 0 + ); + }) + .map((symbol) => { + return new Big(holdings[symbol].valueInBaseCurrency).abs(); + }) + ).toNumber(); const totalBuy = this.getSumOfActivityType({ userCurrency, @@ -1941,7 +1967,7 @@ export class PortfolioService { private async getTransactionPoints({ filters, includeDrafts = false, - types = ['BUY', 'ITEM', 'SELL'], + types = ['BUY', 'ITEM', 'LIABILITY', 'SELL'], userId, withExcludedAccounts = false }: { @@ -2076,19 +2102,10 @@ export class PortfolioService { }); for (const account of currentAccounts) { - let ordersByAccount = orders.filter(({ accountId }) => { + const ordersByAccount = orders.filter(({ accountId }) => { return accountId === account.id; }); - const ordersOfTypeItemOrLiabilityByAccount = - ordersOfTypeItemOrLiability.filter(({ accountId }) => { - return accountId === account.id; - }); - - ordersByAccount = ordersByAccount.concat( - ordersOfTypeItemOrLiabilityByAccount - ); - accounts[account.id] = { balance: account.balance, currency: account.currency, @@ -2128,8 +2145,8 @@ export class PortfolioService { } of ordersByAccount) { let currentValueOfSymbolInBaseCurrency = quantity * - portfolioItemsNow[SymbolProfile.symbol] - ?.marketPriceInBaseCurrency ?? 0; + (portfolioItemsNow[SymbolProfile.symbol]?.marketPriceInBaseCurrency ?? + 0); if (['LIABILITY', 'SELL'].includes(type)) { currentValueOfSymbolInBaseCurrency *= getFactor(type); From c641c28b12d11ef0d989140e45e51373ddb24c5f Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 6 Mar 2024 22:08:00 +0100 Subject: [PATCH 007/120] Release 2.61.1 (#3110) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dce1cf4e..1dce287c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## 2.61.1 - 2024-03-06 ### Fixed diff --git a/package.json b/package.json index cc83d588e..8faa5ff8a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "2.61.0", + "version": "2.61.1", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio", From 77358eed65ffcfb75ee0d00bd96649fa86d792ef Mon Sep 17 00:00:00 2001 From: Gerard Du Pre <37554513+GerardPolloRebozado@users.noreply.github.com> Date: Thu, 7 Mar 2024 19:38:57 +0100 Subject: [PATCH 008/120] Feature/Include user role in admin endpoint (#3107) * Include user role in admin endpoint --- apps/api/src/app/admin/admin.service.ts | 4 +++- libs/common/src/lib/interfaces/admin-data.interface.ts | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index 320601667..f78d1b61d 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -440,13 +440,14 @@ export class AdminService { }, createdAt: true, id: true, + role: true, Subscription: true }, take: 30 }); return usersWithAnalytics.map( - ({ _count, Analytics, createdAt, id, Subscription }) => { + ({ _count, Analytics, createdAt, id, role, Subscription }) => { const daysSinceRegistration = differenceInDays(new Date(), createdAt) + 1; const engagement = Analytics @@ -466,6 +467,7 @@ export class AdminService { createdAt, engagement, id, + role, subscription, accountCount: _count.Account || 0, country: Analytics?.country, diff --git a/libs/common/src/lib/interfaces/admin-data.interface.ts b/libs/common/src/lib/interfaces/admin-data.interface.ts index 6d51d5d52..2c6a5c501 100644 --- a/libs/common/src/lib/interfaces/admin-data.interface.ts +++ b/libs/common/src/lib/interfaces/admin-data.interface.ts @@ -1,3 +1,5 @@ +import { Role } from '@prisma/client'; + import { UniqueAsset } from './unique-asset.interface'; export interface AdminData { @@ -16,6 +18,7 @@ export interface AdminData { engagement: number; id: string; lastActivity: Date; + role: Role; transactionCount: number; }[]; version: string; From 07661d9262a6a926859b2ca7b9e75656086df126 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 7 Mar 2024 20:07:50 +0100 Subject: [PATCH 009/120] Feature/integrate dividend into transaction point concept (#3092) * Integrate dividend into transaction point concept * Update changelog --- CHANGELOG.md | 6 + apps/api/jest.config.ts | 1 - apps/api/src/app/order/order.service.ts | 2 + .../portfolio/current-rate.service.mock.ts | 9 ++ .../transaction-point-symbol.interface.ts | 2 + ...folio-calculator-baln-buy-and-sell.spec.ts | 2 + .../portfolio-calculator-baln-buy.spec.ts | 2 + ...ator-btcusd-buy-and-sell-partially.spec.ts | 2 + .../portfolio-calculator-googl-buy.spec.ts | 2 + ...-calculator-msft-buy-with-dividend.spec.ts | 118 ++++++++++++++++++ ...ulator-novn-buy-and-sell-partially.spec.ts | 2 + ...folio-calculator-novn-buy-and-sell.spec.ts | 2 + .../src/app/portfolio/portfolio-calculator.ts | 114 ++++++++++------- .../src/app/portfolio/portfolio.controller.ts | 22 +++- .../src/app/portfolio/portfolio.service.ts | 67 ++++------ .../exchange-rate-data.service.mock.ts | 7 ++ .../portfolio-summary.component.html | 2 +- .../portfolio-position.interface.ts | 1 + .../interfaces/portfolio-summary.interface.ts | 2 +- .../interfaces/symbol-metrics.interface.ts | 2 + .../interfaces/timeline-position.interface.ts | 2 + 21 files changed, 275 insertions(+), 94 deletions(-) create mode 100644 apps/api/src/app/portfolio/portfolio-calculator-msft-buy-with-dividend.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dce287c1..5da3b143b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Changed + +- Integrated dividend into the transaction point concept in the portfolio service + ## 2.61.1 - 2024-03-06 ### Fixed diff --git a/apps/api/jest.config.ts b/apps/api/jest.config.ts index 8152c3f2a..b87f91a79 100644 --- a/apps/api/jest.config.ts +++ b/apps/api/jest.config.ts @@ -13,7 +13,6 @@ export default { }, moduleFileExtensions: ['ts', 'js', 'html'], coverageDirectory: '../../coverage/apps/api', - testTimeout: 10000, testEnvironment: 'node', preset: '../../jest.preset.js' }; diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index d200ee024..c97243929 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -340,11 +340,13 @@ export class OrderService { return { ...order, value, + // TODO: Use exchange rate of date feeInBaseCurrency: this.exchangeRateDataService.toCurrency( order.fee, order.SymbolProfile.currency, userCurrency ), + // TODO: Use exchange rate of date valueInBaseCurrency: this.exchangeRateDataService.toCurrency( value, order.SymbolProfile.currency, diff --git a/apps/api/src/app/portfolio/current-rate.service.mock.ts b/apps/api/src/app/portfolio/current-rate.service.mock.ts index 76e7aae09..8f8d06112 100644 --- a/apps/api/src/app/portfolio/current-rate.service.mock.ts +++ b/apps/api/src/app/portfolio/current-rate.service.mock.ts @@ -43,6 +43,15 @@ function mockGetValue(symbol: string, date: Date) { return { marketPrice: 0 }; + case 'MSFT': + if (isSameDay(parseDate('2021-09-16'), date)) { + return { marketPrice: 89.12 }; + } else if (isSameDay(parseDate('2023-07-10'), date)) { + return { marketPrice: 331.83 }; + } + + return { marketPrice: 0 }; + case 'NOVN.SW': if (isSameDay(parseDate('2022-04-11'), date)) { return { marketPrice: 87.8 }; diff --git a/apps/api/src/app/portfolio/interfaces/transaction-point-symbol.interface.ts b/apps/api/src/app/portfolio/interfaces/transaction-point-symbol.interface.ts index 5350adccc..fb2f0a389 100644 --- a/apps/api/src/app/portfolio/interfaces/transaction-point-symbol.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/transaction-point-symbol.interface.ts @@ -2,8 +2,10 @@ import { DataSource, Tag } from '@prisma/client'; import Big from 'big.js'; export interface TransactionPointSymbol { + averagePrice: Big; currency: string; dataSource: DataSource; + dividend: Big; fee: Big; firstBuyDate: string; investment: Big; diff --git a/apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell.spec.ts index d81393719..9831e5b00 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell.spec.ts @@ -107,6 +107,8 @@ describe('PortfolioCalculator', () => { averagePrice: new Big('0'), currency: 'CHF', dataSource: 'YAHOO', + dividend: new Big('0'), + dividendInBaseCurrency: new Big('0'), fee: new Big('3.2'), firstBuyDate: '2021-11-22', grossPerformance: new Big('-12.6'), diff --git a/apps/api/src/app/portfolio/portfolio-calculator-baln-buy.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-baln-buy.spec.ts index e77335ab8..bb5986059 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-baln-buy.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-baln-buy.spec.ts @@ -96,6 +96,8 @@ describe('PortfolioCalculator', () => { averagePrice: new Big('136.6'), currency: 'CHF', dataSource: 'YAHOO', + dividend: new Big('0'), + dividendInBaseCurrency: new Big('0'), fee: new Big('1.55'), firstBuyDate: '2021-11-30', grossPerformance: new Big('24.6'), diff --git a/apps/api/src/app/portfolio/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts index 8f5573928..6126311c3 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts @@ -120,6 +120,8 @@ describe('PortfolioCalculator', () => { averagePrice: new Big('320.43'), currency: 'USD', dataSource: 'YAHOO', + dividend: new Big('0'), + dividendInBaseCurrency: new Big('0'), fee: new Big('0'), firstBuyDate: '2015-01-01', grossPerformance: new Big('27172.74'), diff --git a/apps/api/src/app/portfolio/portfolio-calculator-googl-buy.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-googl-buy.spec.ts index 502248388..d29498292 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-googl-buy.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-googl-buy.spec.ts @@ -109,6 +109,8 @@ describe('PortfolioCalculator', () => { averagePrice: new Big('89.12'), currency: 'USD', dataSource: 'YAHOO', + dividend: new Big('0'), + dividendInBaseCurrency: new Big('0'), fee: new Big('1'), firstBuyDate: '2023-01-03', grossPerformance: new Big('27.33'), diff --git a/apps/api/src/app/portfolio/portfolio-calculator-msft-buy-with-dividend.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-msft-buy-with-dividend.spec.ts new file mode 100644 index 000000000..de63bced1 --- /dev/null +++ b/apps/api/src/app/portfolio/portfolio-calculator-msft-buy-with-dividend.spec.ts @@ -0,0 +1,118 @@ +import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; +import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock'; +import { parseDate } from '@ghostfolio/common/helper'; + +import Big from 'big.js'; + +import { CurrentRateServiceMock } from './current-rate.service.mock'; +import { PortfolioCalculator } from './portfolio-calculator'; + +jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + CurrentRateService: jest.fn().mockImplementation(() => { + return CurrentRateServiceMock; + }) + }; +}); + +jest.mock( + '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service', + () => { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + ExchangeRateDataService: jest.fn().mockImplementation(() => { + return ExchangeRateDataServiceMock; + }) + }; + } +); + +describe('PortfolioCalculator', () => { + let currentRateService: CurrentRateService; + let exchangeRateDataService: ExchangeRateDataService; + + beforeEach(() => { + currentRateService = new CurrentRateService(null, null, null, null); + + exchangeRateDataService = new ExchangeRateDataService( + null, + null, + null, + null + ); + }); + + describe('get current positions', () => { + it.only('with MSFT buy', async () => { + const portfolioCalculator = new PortfolioCalculator({ + currentRateService, + exchangeRateDataService, + currency: 'USD', + orders: [ + { + currency: 'USD', + date: '2021-09-16', + dataSource: 'YAHOO', + fee: new Big(19), + name: 'Microsoft Inc.', + quantity: new Big(1), + symbol: 'MSFT', + type: 'BUY', + unitPrice: new Big(298.58) + }, + { + currency: 'USD', + date: '2021-11-16', + dataSource: 'YAHOO', + fee: new Big(0), + name: 'Microsoft Inc.', + quantity: new Big(1), + symbol: 'MSFT', + type: 'DIVIDEND', + unitPrice: new Big(0.62) + } + ] + }); + + portfolioCalculator.computeTransactionPoints(); + + const spy = jest + .spyOn(Date, 'now') + .mockImplementation(() => parseDate('2023-07-10').getTime()); + + const currentPositions = await portfolioCalculator.getCurrentPositions( + parseDate('2023-07-10') + ); + + spy.mockRestore(); + + expect(currentPositions).toMatchObject({ + errors: [], + hasErrors: false, + positions: [ + { + averagePrice: new Big('298.58'), + currency: 'USD', + dataSource: 'YAHOO', + dividend: new Big('0.62'), + dividendInBaseCurrency: new Big('0.62'), + fee: new Big('19'), + firstBuyDate: '2021-09-16', + investment: new Big('298.58'), + investmentWithCurrencyEffect: new Big('298.58'), + marketPrice: 331.83, + marketPriceInBaseCurrency: 331.83, + quantity: new Big('1'), + symbol: 'MSFT', + tags: undefined, + transactionCount: 2 + } + ], + totalInvestment: new Big('298.58'), + totalInvestmentWithCurrencyEffect: new Big('298.58') + }); + }); + }); +}); diff --git a/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell-partially.spec.ts index c46fd54d2..afddc5423 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell-partially.spec.ts @@ -107,6 +107,8 @@ describe('PortfolioCalculator', () => { averagePrice: new Big('75.80'), currency: 'CHF', dataSource: 'YAHOO', + dividend: new Big('0'), + dividendInBaseCurrency: new Big('0'), fee: new Big('4.25'), firstBuyDate: '2022-03-07', grossPerformance: new Big('21.93'), diff --git a/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts index fa3ebac9b..4b7750a63 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts @@ -133,6 +133,8 @@ describe('PortfolioCalculator', () => { averagePrice: new Big('0'), currency: 'CHF', dataSource: 'YAHOO', + dividend: new Big('0'), + dividendInBaseCurrency: new Big('0'), fee: new Big('0'), firstBuyDate: '2022-03-07', grossPerformance: new Big('19.86'), diff --git a/apps/api/src/app/portfolio/portfolio-calculator.ts b/apps/api/src/app/portfolio/portfolio-calculator.ts index 27a5a278b..f0551f3b8 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.ts @@ -70,6 +70,7 @@ export class PortfolioCalculator { let lastDate: string = null; let lastTransactionPoint: TransactionPoint = null; + for (const order of this.orders) { const currentDate = order.date; @@ -77,33 +78,32 @@ export class PortfolioCalculator { const oldAccumulatedSymbol = symbols[order.symbol]; const factor = getFactor(order.type); - const unitPrice = new Big(order.unitPrice); + if (oldAccumulatedSymbol) { + let investment = oldAccumulatedSymbol.investment; + const newQuantity = order.quantity .mul(factor) .plus(oldAccumulatedSymbol.quantity); - let investment = new Big(0); - - if (newQuantity.gt(0)) { - if (order.type === 'BUY') { - investment = oldAccumulatedSymbol.investment.plus( - order.quantity.mul(unitPrice) - ); - } else if (order.type === 'SELL') { - const averagePrice = oldAccumulatedSymbol.investment.div( - oldAccumulatedSymbol.quantity - ); - investment = oldAccumulatedSymbol.investment.minus( - order.quantity.mul(averagePrice) - ); - } + if (order.type === 'BUY') { + investment = oldAccumulatedSymbol.investment.plus( + order.quantity.mul(order.unitPrice) + ); + } else if (order.type === 'SELL') { + investment = oldAccumulatedSymbol.investment.minus( + order.quantity.mul(oldAccumulatedSymbol.averagePrice) + ); } currentTransactionPointItem = { investment, + averagePrice: newQuantity.gt(0) + ? investment.div(newQuantity) + : new Big(0), currency: order.currency, dataSource: order.dataSource, + dividend: new Big(0), fee: order.fee.plus(oldAccumulatedSymbol.fee), firstBuyDate: oldAccumulatedSymbol.firstBuyDate, quantity: newQuantity, @@ -113,11 +113,13 @@ export class PortfolioCalculator { }; } else { currentTransactionPointItem = { + averagePrice: order.unitPrice, currency: order.currency, dataSource: order.dataSource, + dividend: new Big(0), fee: order.fee, firstBuyDate: order.date, - investment: unitPrice.mul(order.quantity).mul(factor), + investment: order.unitPrice.mul(order.quantity).mul(factor), quantity: order.quantity.mul(factor), symbol: order.symbol, tags: order.tags, @@ -128,22 +130,28 @@ export class PortfolioCalculator { symbols[order.symbol] = currentTransactionPointItem; const items = lastTransactionPoint?.items ?? []; + const newItems = items.filter( (transactionPointItem) => transactionPointItem.symbol !== order.symbol ); + newItems.push(currentTransactionPointItem); + newItems.sort((a, b) => { return a.symbol?.localeCompare(b.symbol); }); + if (lastDate !== currentDate || lastTransactionPoint === null) { lastTransactionPoint = { date: currentDate, items: newItems }; + this.transactionPoints.push(lastTransactionPoint); } else { lastTransactionPoint.items = newItems; } + lastDate = currentDate; } } @@ -583,6 +591,8 @@ export class PortfolioCalculator { ); const { + dividend, + dividendInBaseCurrency, grossPerformance, grossPerformancePercentage, grossPerformancePercentageWithCurrencyEffect, @@ -608,11 +618,11 @@ export class PortfolioCalculator { hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors; positions.push({ + dividend, + dividendInBaseCurrency, timeWeightedInvestment, timeWeightedInvestmentWithCurrencyEffect, - averagePrice: item.quantity.eq(0) - ? new Big(0) - : item.investment.div(item.quantity), + averagePrice: item.averagePrice, currency: item.currency, dataSource: item.dataSource, fee: item.fee, @@ -842,6 +852,8 @@ export class PortfolioCalculator { const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)]; const currentValues: { [date: string]: Big } = {}; const currentValuesWithCurrencyEffect: { [date: string]: Big } = {}; + let dividend = new Big(0); + let dividendInBaseCurrency = new Big(0); let fees = new Big(0); let feesAtStartDate = new Big(0); let feesAtStartDateWithCurrencyEffect = new Big(0); @@ -894,6 +906,8 @@ export class PortfolioCalculator { return { currentValues: {}, currentValuesWithCurrencyEffect: {}, + dividend: new Big(0), + dividendInBaseCurrency: new Big(0), grossPerformance: new Big(0), grossPerformancePercentage: new Big(0), grossPerformancePercentageWithCurrencyEffect: new Big(0), @@ -934,6 +948,8 @@ export class PortfolioCalculator { return { currentValues: {}, currentValuesWithCurrencyEffect: {}, + dividend: new Big(0), + dividendInBaseCurrency: new Big(0), grossPerformance: new Big(0), grossPerformancePercentage: new Big(0), grossPerformancePercentageWithCurrencyEffect: new Big(0), @@ -1108,29 +1124,29 @@ export class PortfolioCalculator { valueOfInvestmentBeforeTransactionWithCurrencyEffect; } - const transactionInvestment = - order.type === 'BUY' - ? order.quantity - .mul(order.unitPriceInBaseCurrency) - .mul(getFactor(order.type)) - : totalUnits.gt(0) - ? totalInvestment - .div(totalUnits) - .mul(order.quantity) - .mul(getFactor(order.type)) - : new Big(0); - - const transactionInvestmentWithCurrencyEffect = - order.type === 'BUY' - ? order.quantity - .mul(order.unitPriceInBaseCurrencyWithCurrencyEffect) - .mul(getFactor(order.type)) - : totalUnits.gt(0) - ? totalInvestmentWithCurrencyEffect - .div(totalUnits) - .mul(order.quantity) - .mul(getFactor(order.type)) - : new Big(0); + let transactionInvestment = new Big(0); + let transactionInvestmentWithCurrencyEffect = new Big(0); + + if (order.type === 'BUY') { + transactionInvestment = order.quantity + .mul(order.unitPriceInBaseCurrency) + .mul(getFactor(order.type)); + transactionInvestmentWithCurrencyEffect = order.quantity + .mul(order.unitPriceInBaseCurrencyWithCurrencyEffect) + .mul(getFactor(order.type)); + } else if (order.type === 'SELL') { + if (totalUnits.gt(0)) { + transactionInvestment = totalInvestment + .div(totalUnits) + .mul(order.quantity) + .mul(getFactor(order.type)); + transactionInvestmentWithCurrencyEffect = + totalInvestmentWithCurrencyEffect + .div(totalUnits) + .mul(order.quantity) + .mul(getFactor(order.type)); + } + } if (PortfolioCalculator.ENABLE_LOGGING) { console.log('totalInvestment', totalInvestment.toNumber()); @@ -1186,6 +1202,13 @@ export class PortfolioCalculator { totalUnits = totalUnits.plus(order.quantity.mul(getFactor(order.type))); + if (order.type === 'DIVIDEND') { + dividend = dividend.plus(order.quantity.mul(order.unitPrice)); + dividendInBaseCurrency = dividendInBaseCurrency.plus( + dividend.mul(exchangeRateAtOrderDate ?? 1) + ); + } + const valueOfInvestment = totalUnits.mul(order.unitPriceInBaseCurrency); const valueOfInvestmentWithCurrencyEffect = totalUnits.mul( @@ -1277,7 +1300,7 @@ export class PortfolioCalculator { grossPerformanceWithCurrencyEffect; } - if (i > indexOfStartOrder) { + if (i > indexOfStartOrder && ['BUY', 'SELL'].includes(order.type)) { // Only consider periods with an investment for the calculation of // the time weighted investment if (valueOfInvestmentBeforeTransaction.gt(0)) { @@ -1471,6 +1494,7 @@ export class PortfolioCalculator { Time weighted investment with currency effect: ${timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect.toFixed( 2 )} + Total dividend: ${dividend.toFixed(2)} Gross performance: ${totalGrossPerformance.toFixed( 2 )} / ${grossPerformancePercentage.mul(100).toFixed(2)}% @@ -1495,6 +1519,8 @@ export class PortfolioCalculator { return { currentValues, currentValuesWithCurrencyEffect, + dividend, + dividendInBaseCurrency, grossPerformancePercentage, grossPerformancePercentageWithCurrencyEffect, initialValue, diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index e51321561..63e26e512 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -1,4 +1,5 @@ import { AccessService } from '@ghostfolio/api/app/access/access.service'; +import { OrderService } from '@ghostfolio/api/app/order/order.service'; import { UserService } from '@ghostfolio/api/app/user/user.service'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { @@ -11,6 +12,7 @@ import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interc import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; +import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service'; import { DEFAULT_CURRENCY, HEADER_KEY_IMPERSONATION @@ -57,6 +59,8 @@ export class PortfolioController { private readonly apiService: ApiService, private readonly configurationService: ConfigurationService, private readonly exchangeRateDataService: ExchangeRateDataService, + private readonly impersonationService: ImpersonationService, + private readonly orderService: OrderService, private readonly portfolioService: PortfolioService, @Inject(REQUEST) private readonly request: RequestWithUser, private readonly userService: UserService @@ -161,7 +165,7 @@ export class PortfolioController { 'currentNetPerformance', 'currentNetPerformanceWithCurrencyEffect', 'currentValue', - 'dividend', + 'dividendInBaseCurrency', 'emergencyFund', 'excludedAccountsAndActivities', 'fees', @@ -231,11 +235,21 @@ export class PortfolioController { filterByTags }); + const impersonationUserId = + await this.impersonationService.validateImpersonationId(impersonationId); + const userCurrency = this.request.user.Settings.settings.baseCurrency; + + const { activities } = await this.orderService.getOrders({ + filters, + userCurrency, + userId: impersonationUserId || this.request.user.id, + types: ['DIVIDEND'] + }); + let dividends = await this.portfolioService.getDividends({ + activities, dateRange, - filters, - groupBy, - impersonationId + groupBy }); if ( diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 0d14e35df..d55b3d647 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -218,27 +218,14 @@ export class PortfolioService { } public async getDividends({ - dateRange, - filters, - groupBy, - impersonationId + activities, + dateRange = 'max', + groupBy }: { - dateRange: DateRange; - filters?: Filter[]; + activities: Activity[]; + dateRange?: DateRange; groupBy?: GroupBy; - impersonationId: string; }): Promise { - const userId = await this.getUserId(impersonationId, this.request.user.id); - const user = await this.userService.user({ id: userId }); - const userCurrency = this.getUserCurrency(user); - - const { activities } = await this.orderService.getOrders({ - filters, - userCurrency, - userId, - types: ['DIVIDEND'] - }); - let dividends = activities.map(({ date, valueInBaseCurrency }) => { return { date: format(date, DATE_FORMAT), @@ -441,6 +428,7 @@ export class PortfolioService { for (const { currency, + dividend, firstBuyDate, grossPerformance, grossPerformanceWithCurrencyEffect, @@ -553,6 +541,7 @@ export class PortfolioService { countries: symbolProfile.countries, dataSource: symbolProfile.dataSource, dateOfFirstActivity: parseDate(firstBuyDate), + dividend: dividend?.toNumber() ?? 0, grossPerformance: grossPerformance?.toNumber() ?? 0, grossPerformancePercent: grossPerformancePercentage?.toNumber() ?? 0, grossPerformancePercentWithCurrencyEffect: @@ -723,7 +712,7 @@ export class PortfolioService { .filter((order) => { tags = tags.concat(order.tags); - return ['BUY', 'ITEM', 'SELL'].includes(order.type); + return ['BUY', 'DIVIDEND', 'ITEM', 'SELL'].includes(order.type); }) .map((order) => ({ currency: order.SymbolProfile.currency, @@ -764,6 +753,7 @@ export class PortfolioService { averagePrice, currency, dataSource, + dividendInBaseCurrency, fee, firstBuyDate, marketPrice, @@ -780,16 +770,6 @@ export class PortfolioService { return Account; }); - const dividendInBaseCurrency = getSum( - orders - .filter(({ type }) => { - return type === 'DIVIDEND'; - }) - .map(({ valueInBaseCurrency }) => { - return new Big(valueInBaseCurrency); - }) - ); - const historicalData = await this.dataProviderService.getHistorical( [{ dataSource, symbol: aSymbol }], 'day', @@ -823,9 +803,7 @@ export class PortfolioService { ); if (currentSymbol) { - currentAveragePrice = currentSymbol.quantity.eq(0) - ? 0 - : currentSymbol.investment.div(currentSymbol.quantity).toNumber(); + currentAveragePrice = currentSymbol.averagePrice.toNumber(); currentQuantity = currentSymbol.quantity.toNumber(); } @@ -1636,6 +1614,7 @@ export class PortfolioService { countries: [], dataSource: undefined, dateOfFirstActivity: undefined, + dividend: 0, grossPerformance: 0, grossPerformancePercent: 0, grossPerformancePercentWithCurrencyEffect: 0, @@ -1765,11 +1744,16 @@ export class PortfolioService { } } - const dividend = this.getSumOfActivityType({ - activities, - userCurrency, - activityType: 'DIVIDEND' - }).toNumber(); + const dividendInBaseCurrency = ( + await this.getDividends({ + activities: activities.filter(({ type }) => { + return type === 'DIVIDEND'; + }) + }) + ).reduce( + (previous, current) => new Big(previous).plus(current.investment), + new Big(0) + ); const emergencyFund = new Big( Math.max( @@ -1904,7 +1888,6 @@ export class PortfolioService { annualizedPerformancePercent, annualizedPerformancePercentWithCurrencyEffect, cash, - dividend, excludedAccountsAndActivities, fees, firstOrderDate, @@ -1915,6 +1898,7 @@ export class PortfolioService { totalBuy, totalSell, committedFunds: committedFunds.toNumber(), + dividendInBaseCurrency: dividendInBaseCurrency.toNumber(), emergencyFund: { assets: emergencyFundPositionsValueInBaseCurrency, cash: emergencyFund @@ -1967,7 +1951,7 @@ export class PortfolioService { private async getTransactionPoints({ filters, includeDrafts = false, - types = ['BUY', 'ITEM', 'LIABILITY', 'SELL'], + types = ['BUY', 'DIVIDEND', 'ITEM', 'LIABILITY', 'SELL'], userId, withExcludedAccounts = false }: { @@ -2144,14 +2128,11 @@ export class PortfolioService { type } of ordersByAccount) { let currentValueOfSymbolInBaseCurrency = + getFactor(type) * quantity * (portfolioItemsNow[SymbolProfile.symbol]?.marketPriceInBaseCurrency ?? 0); - if (['LIABILITY', 'SELL'].includes(type)) { - currentValueOfSymbolInBaseCurrency *= getFactor(type); - } - if (accounts[Account?.id || UNKNOWN_KEY]?.valueInBaseCurrency) { accounts[Account?.id || UNKNOWN_KEY].valueInBaseCurrency += currentValueOfSymbolInBaseCurrency; diff --git a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts index e32af51d3..a25f3a356 100644 --- a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts +++ b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts @@ -22,6 +22,13 @@ export const ExchangeRateDataServiceMock = { '2023-07-10': 0.8854 } }); + } else if (targetCurrency === 'USD') { + return Promise.resolve({ + USDUSD: { + '2018-01-01': 1, + '2023-07-10': 1 + } + }); } return Promise.resolve({}); diff --git a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html index c4aea8891..096271926 100644 --- a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html +++ b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -328,7 +328,7 @@ [isCurrency]="true" [locale]="locale" [unit]="baseCurrency" - [value]="isLoading ? undefined : summary?.dividend" + [value]="isLoading ? undefined : summary?.dividendInBaseCurrency" /> diff --git a/libs/common/src/lib/interfaces/portfolio-position.interface.ts b/libs/common/src/lib/interfaces/portfolio-position.interface.ts index 87db0117d..2e5772ef7 100644 --- a/libs/common/src/lib/interfaces/portfolio-position.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-position.interface.ts @@ -14,6 +14,7 @@ export interface PortfolioPosition { currency: string; dataSource: DataSource; dateOfFirstActivity: Date; + dividend: number; exchange?: string; grossPerformance: number; grossPerformancePercent: number; diff --git a/libs/common/src/lib/interfaces/portfolio-summary.interface.ts b/libs/common/src/lib/interfaces/portfolio-summary.interface.ts index f0bb4c3b1..aaf80c0cd 100644 --- a/libs/common/src/lib/interfaces/portfolio-summary.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-summary.interface.ts @@ -5,7 +5,7 @@ export interface PortfolioSummary extends PortfolioPerformance { annualizedPerformancePercentWithCurrencyEffect: number; cash: number; committedFunds: number; - dividend: number; + dividendInBaseCurrency: number; emergencyFund: { assets: number; cash: number; diff --git a/libs/common/src/lib/interfaces/symbol-metrics.interface.ts b/libs/common/src/lib/interfaces/symbol-metrics.interface.ts index e7cbf7460..c269810d8 100644 --- a/libs/common/src/lib/interfaces/symbol-metrics.interface.ts +++ b/libs/common/src/lib/interfaces/symbol-metrics.interface.ts @@ -7,6 +7,8 @@ export interface SymbolMetrics { currentValuesWithCurrencyEffect: { [date: string]: Big; }; + dividend: Big; + dividendInBaseCurrency: Big; grossPerformance: Big; grossPerformancePercentage: Big; grossPerformancePercentageWithCurrencyEffect: Big; diff --git a/libs/common/src/lib/interfaces/timeline-position.interface.ts b/libs/common/src/lib/interfaces/timeline-position.interface.ts index 220a0aa8f..831c29b31 100644 --- a/libs/common/src/lib/interfaces/timeline-position.interface.ts +++ b/libs/common/src/lib/interfaces/timeline-position.interface.ts @@ -5,6 +5,8 @@ export interface TimelinePosition { averagePrice: Big; currency: string; dataSource: DataSource; + dividend: Big; + dividendInBaseCurrency: Big; fee: Big; firstBuyDate: string; grossPerformance: Big; From 7a3237f1ff2fa20887c351921a7eae3f27131bb9 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 8 Mar 2024 18:59:23 +0100 Subject: [PATCH 010/120] Adapt style of inactive users (#3114) --- .../components/admin-users/admin-users.html | 20 +++++++++++++------ apps/client/src/styles.scss | 4 ++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/apps/client/src/app/components/admin-users/admin-users.html b/apps/client/src/app/components/admin-users/admin-users.html index 6b83a6ab0..b8eaa28ab 100644 --- a/apps/client/src/app/components/admin-users/admin-users.html +++ b/apps/client/src/app/components/admin-users/admin-users.html @@ -35,12 +35,20 @@ mat-cell >
- {{ - element.id - }} - {{ - (element.id | slice: 0 : 5) + '...' - }} + {{ element.id }} + {{ (element.id | slice: 0 : 5) + '...' }} Date: Fri, 8 Mar 2024 19:00:21 +0100 Subject: [PATCH 011/120] Feature/remove environment variable web auth rp (#3115) * Remove environment variable WEB_AUTH_RP_ID * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/auth/web-auth.service.ts | 2 +- .../src/services/configuration/configuration.service.ts | 7 +++---- apps/api/src/services/interfaces/environment.interface.ts | 1 - 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5da3b143b..54764beb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Integrated dividend into the transaction point concept in the portfolio service +- Removed the environment variable `WEB_AUTH_RP_ID` ## 2.61.1 - 2024-03-06 diff --git a/apps/api/src/app/auth/web-auth.service.ts b/apps/api/src/app/auth/web-auth.service.ts index a6e76ffbb..961bbe9a7 100644 --- a/apps/api/src/app/auth/web-auth.service.ts +++ b/apps/api/src/app/auth/web-auth.service.ts @@ -41,7 +41,7 @@ export class WebAuthService { ) {} get rpID() { - return this.configurationService.get('WEB_AUTH_RP_ID'); + return new URL(this.configurationService.get('ROOT_URL')).hostname; } get expectedOrigin() { diff --git a/apps/api/src/services/configuration/configuration.service.ts b/apps/api/src/services/configuration/configuration.service.ts index eb82be418..507e4a375 100644 --- a/apps/api/src/services/configuration/configuration.service.ts +++ b/apps/api/src/services/configuration/configuration.service.ts @@ -3,7 +3,7 @@ import { DEFAULT_ROOT_URL } from '@ghostfolio/common/config'; import { Injectable } from '@nestjs/common'; import { DataSource } from '@prisma/client'; -import { bool, cleanEnv, host, json, num, port, str } from 'envalid'; +import { bool, cleanEnv, host, json, num, port, str, url } from 'envalid'; @Injectable() export class ConfigurationService { @@ -48,14 +48,13 @@ export class ConfigurationService { REDIS_PASSWORD: str({ default: '' }), REDIS_PORT: port({ default: 6379 }), REQUEST_TIMEOUT: num({ default: 2000 }), - ROOT_URL: str({ default: DEFAULT_ROOT_URL }), + ROOT_URL: url({ default: DEFAULT_ROOT_URL }), STRIPE_PUBLIC_KEY: str({ default: '' }), STRIPE_SECRET_KEY: str({ default: '' }), TWITTER_ACCESS_TOKEN: str({ default: 'dummyAccessToken' }), TWITTER_ACCESS_TOKEN_SECRET: str({ default: 'dummyAccessTokenSecret' }), TWITTER_API_KEY: str({ default: 'dummyApiKey' }), - TWITTER_API_SECRET: str({ default: 'dummyApiSecret' }), - WEB_AUTH_RP_ID: host({ default: 'localhost' }) + TWITTER_API_SECRET: str({ default: 'dummyApiSecret' }) }); } diff --git a/apps/api/src/services/interfaces/environment.interface.ts b/apps/api/src/services/interfaces/environment.interface.ts index 09d0b0e5d..5d3145a28 100644 --- a/apps/api/src/services/interfaces/environment.interface.ts +++ b/apps/api/src/services/interfaces/environment.interface.ts @@ -42,5 +42,4 @@ export interface Environment extends CleanedEnvAccessors { TWITTER_ACCESS_TOKEN_SECRET: string; TWITTER_API_KEY: string; TWITTER_API_SECRET: string; - WEB_AUTH_RP_ID: string; } From bc8d8309d49060ca09a61d1657553f5d172d446c Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 9 Mar 2024 11:07:01 +0100 Subject: [PATCH 012/120] Improve handling of future liabilities (#3118) * Improve handling of future liabilities * Refactor currentValue to currentValueInBaseCurrency * Update changelog --- CHANGELOG.md | 4 + .../portfolio/current-rate.service.mock.ts | 2 + .../interfaces/current-positions.interface.ts | 4 +- ...folio-calculator-baln-buy-and-sell.spec.ts | 5 +- .../portfolio-calculator-baln-buy.spec.ts | 5 +- ...ator-btcusd-buy-and-sell-partially.spec.ts | 5 +- .../portfolio-calculator-googl-buy.spec.ts | 5 +- .../portfolio-calculator-no-orders.spec.ts | 2 +- ...ulator-novn-buy-and-sell-partially.spec.ts | 5 +- ...folio-calculator-novn-buy-and-sell.spec.ts | 5 +- .../src/app/portfolio/portfolio-calculator.ts | 87 ++++++++++--------- .../src/app/portfolio/portfolio.service.ts | 25 +++--- .../exchange-rate-data.service.mock.ts | 1 + .../exchange-rate-data.service.ts | 22 ++++- .../interfaces/timeline-position.interface.ts | 1 + 15 files changed, 108 insertions(+), 70 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54764beb0..531e0de91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Integrated dividend into the transaction point concept in the portfolio service - Removed the environment variable `WEB_AUTH_RP_ID` +### Fixed + +- Fixed an issue in the calculation of the portfolio summary caused by future liabilities + ## 2.61.1 - 2024-03-06 ### Fixed diff --git a/apps/api/src/app/portfolio/current-rate.service.mock.ts b/apps/api/src/app/portfolio/current-rate.service.mock.ts index 8f8d06112..ed9229691 100644 --- a/apps/api/src/app/portfolio/current-rate.service.mock.ts +++ b/apps/api/src/app/portfolio/current-rate.service.mock.ts @@ -46,6 +46,8 @@ function mockGetValue(symbol: string, date: Date) { case 'MSFT': if (isSameDay(parseDate('2021-09-16'), date)) { return { marketPrice: 89.12 }; + } else if (isSameDay(parseDate('2021-11-16'), date)) { + return { marketPrice: 339.51 }; } else if (isSameDay(parseDate('2023-07-10'), date)) { return { marketPrice: 331.83 }; } diff --git a/apps/api/src/app/portfolio/interfaces/current-positions.interface.ts b/apps/api/src/app/portfolio/interfaces/current-positions.interface.ts index 5807d6b5e..9ad9ee822 100644 --- a/apps/api/src/app/portfolio/interfaces/current-positions.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/current-positions.interface.ts @@ -3,7 +3,7 @@ import { ResponseError, TimelinePosition } from '@ghostfolio/common/interfaces'; import Big from 'big.js'; export interface CurrentPositions extends ResponseError { - positions: TimelinePosition[]; + currentValueInBaseCurrency: Big; grossPerformance: Big; grossPerformanceWithCurrencyEffect: Big; grossPerformancePercentage: Big; @@ -14,6 +14,6 @@ export interface CurrentPositions extends ResponseError { netPerformanceWithCurrencyEffect: Big; netPerformancePercentage: Big; netPerformancePercentageWithCurrencyEffect: Big; - currentValue: Big; + positions: TimelinePosition[]; totalInvestment: Big; } diff --git a/apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell.spec.ts index 9831e5b00..32320dce6 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell.spec.ts @@ -87,7 +87,7 @@ describe('PortfolioCalculator', () => { spy.mockRestore(); expect(currentPositions).toEqual({ - currentValue: new Big('0'), + currentValueInBaseCurrency: new Big('0'), errors: [], grossPerformance: new Big('-12.6'), grossPerformancePercentage: new Big('-0.0440867739678096571'), @@ -131,7 +131,8 @@ describe('PortfolioCalculator', () => { symbol: 'BALN.SW', timeWeightedInvestment: new Big('285.8'), timeWeightedInvestmentWithCurrencyEffect: new Big('285.8'), - transactionCount: 2 + transactionCount: 2, + valueInBaseCurrency: new Big('0') } ], totalInvestment: new Big('0'), diff --git a/apps/api/src/app/portfolio/portfolio-calculator-baln-buy.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-baln-buy.spec.ts index bb5986059..f0aae5536 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-baln-buy.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-baln-buy.spec.ts @@ -76,7 +76,7 @@ describe('PortfolioCalculator', () => { spy.mockRestore(); expect(currentPositions).toEqual({ - currentValue: new Big('297.8'), + currentValueInBaseCurrency: new Big('297.8'), errors: [], grossPerformance: new Big('24.6'), grossPerformancePercentage: new Big('0.09004392386530014641'), @@ -120,7 +120,8 @@ describe('PortfolioCalculator', () => { symbol: 'BALN.SW', timeWeightedInvestment: new Big('273.2'), timeWeightedInvestmentWithCurrencyEffect: new Big('273.2'), - transactionCount: 1 + transactionCount: 1, + valueInBaseCurrency: new Big('297.8') } ], totalInvestment: new Big('273.2'), diff --git a/apps/api/src/app/portfolio/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts index 6126311c3..d0a8aafc0 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts @@ -100,7 +100,7 @@ describe('PortfolioCalculator', () => { spy.mockRestore(); expect(currentPositions).toEqual({ - currentValue: new Big('13298.425356'), + currentValueInBaseCurrency: new Big('13298.425356'), errors: [], grossPerformance: new Big('27172.74'), grossPerformancePercentage: new Big('42.41978276196153750666'), @@ -151,7 +151,8 @@ describe('PortfolioCalculator', () => { timeWeightedInvestmentWithCurrencyEffect: new Big( '636.79469348020066587024' ), - transactionCount: 2 + transactionCount: 2, + valueInBaseCurrency: new Big('13298.425356') } ], totalInvestment: new Big('320.43'), diff --git a/apps/api/src/app/portfolio/portfolio-calculator-googl-buy.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-googl-buy.spec.ts index d29498292..c3d2eabff 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-googl-buy.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-googl-buy.spec.ts @@ -89,7 +89,7 @@ describe('PortfolioCalculator', () => { spy.mockRestore(); expect(currentPositions).toEqual({ - currentValue: new Big('103.10483'), + currentValueInBaseCurrency: new Big('103.10483'), errors: [], grossPerformance: new Big('27.33'), grossPerformancePercentage: new Big('0.3066651705565529623'), @@ -134,7 +134,8 @@ describe('PortfolioCalculator', () => { tags: undefined, timeWeightedInvestment: new Big('89.12'), timeWeightedInvestmentWithCurrencyEffect: new Big('82.329056'), - transactionCount: 1 + transactionCount: 1, + valueInBaseCurrency: new Big('103.10483') } ], totalInvestment: new Big('89.12'), diff --git a/apps/api/src/app/portfolio/portfolio-calculator-no-orders.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-no-orders.spec.ts index ab7234822..f87075c1f 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-no-orders.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-no-orders.spec.ts @@ -64,7 +64,7 @@ describe('PortfolioCalculator', () => { spy.mockRestore(); expect(currentPositions).toEqual({ - currentValue: new Big(0), + currentValueInBaseCurrency: new Big(0), grossPerformance: new Big(0), grossPerformancePercentage: new Big(0), grossPerformancePercentageWithCurrencyEffect: new Big(0), diff --git a/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell-partially.spec.ts index afddc5423..8aa5cf0cb 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell-partially.spec.ts @@ -87,7 +87,7 @@ describe('PortfolioCalculator', () => { spy.mockRestore(); expect(currentPositions).toEqual({ - currentValue: new Big('87.8'), + currentValueInBaseCurrency: new Big('87.8'), errors: [], grossPerformance: new Big('21.93'), grossPerformancePercentage: new Big('0.15113417083448194384'), @@ -133,7 +133,8 @@ describe('PortfolioCalculator', () => { timeWeightedInvestmentWithCurrencyEffect: new Big( '145.10285714285714285714' ), - transactionCount: 2 + transactionCount: 2, + valueInBaseCurrency: new Big('87.8') } ], totalInvestment: new Big('75.80'), diff --git a/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts index 4b7750a63..669013858 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts @@ -113,7 +113,7 @@ describe('PortfolioCalculator', () => { }); expect(currentPositions).toEqual({ - currentValue: new Big('0'), + currentValueInBaseCurrency: new Big('0'), errors: [], grossPerformance: new Big('19.86'), grossPerformancePercentage: new Big('0.13100263852242744063'), @@ -157,7 +157,8 @@ describe('PortfolioCalculator', () => { symbol: 'NOVN.SW', timeWeightedInvestment: new Big('151.6'), timeWeightedInvestmentWithCurrencyEffect: new Big('151.6'), - transactionCount: 2 + transactionCount: 2, + valueInBaseCurrency: new Big('0') } ], totalInvestment: new Big('0'), diff --git a/apps/api/src/app/portfolio/portfolio-calculator.ts b/apps/api/src/app/portfolio/portfolio-calculator.ts index f0551f3b8..07016efcf 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.ts @@ -22,6 +22,7 @@ import { format, isBefore, isSameDay, + max, subDays } from 'date-fns'; import { cloneDeep, first, isNumber, last, sortBy, uniq } from 'lodash'; @@ -449,16 +450,27 @@ export class PortfolioCalculator { public async getCurrentPositions( start: Date, - end = new Date(Date.now()) + end?: Date ): Promise { - const transactionPointsBeforeEndDate = - this.transactionPoints?.filter((transactionPoint) => { - return isBefore(parseDate(transactionPoint.date), end); - }) ?? []; + const lastTransactionPoint = last(this.transactionPoints); + + let endDate = end; + + if (!endDate) { + endDate = new Date(Date.now()); - if (!transactionPointsBeforeEndDate.length) { + if (lastTransactionPoint) { + endDate = max([endDate, parseDate(lastTransactionPoint.date)]); + } + } + + const transactionPoints = this.transactionPoints?.filter(({ date }) => { + return isBefore(parseDate(date), endDate); + }); + + if (!transactionPoints.length) { return { - currentValue: new Big(0), + currentValueInBaseCurrency: new Big(0), grossPerformance: new Big(0), grossPerformancePercentage: new Big(0), grossPerformancePercentageWithCurrencyEffect: new Big(0), @@ -473,41 +485,40 @@ export class PortfolioCalculator { }; } - const lastTransactionPoint = - transactionPointsBeforeEndDate[transactionPointsBeforeEndDate.length - 1]; - const currencies: { [symbol: string]: string } = {}; const dataGatheringItems: IDataGatheringItem[] = []; let dates: Date[] = []; - let firstIndex = transactionPointsBeforeEndDate.length; + let firstIndex = transactionPoints.length; let firstTransactionPoint: TransactionPoint = null; dates.push(resetHours(start)); - for (const item of transactionPointsBeforeEndDate[firstIndex - 1].items) { + + for (const { currency, dataSource, symbol } of transactionPoints[ + firstIndex - 1 + ].items) { dataGatheringItems.push({ - dataSource: item.dataSource, - symbol: item.symbol + dataSource, + symbol }); - currencies[item.symbol] = item.currency; + currencies[symbol] = currency; } - for (let i = 0; i < transactionPointsBeforeEndDate.length; i++) { + for (let i = 0; i < transactionPoints.length; i++) { if ( - !isBefore(parseDate(transactionPointsBeforeEndDate[i].date), start) && + !isBefore(parseDate(transactionPoints[i].date), start) && firstTransactionPoint === null ) { - firstTransactionPoint = transactionPointsBeforeEndDate[i]; + firstTransactionPoint = transactionPoints[i]; firstIndex = i; } + if (firstTransactionPoint !== null) { - dates.push( - resetHours(parseDate(transactionPointsBeforeEndDate[i].date)) - ); + dates.push(resetHours(parseDate(transactionPoints[i].date))); } } - dates.push(resetHours(end)); + dates.push(resetHours(endDate)); // Add dates of last week for fallback dates.push(subDays(resetHours(new Date()), 7)); @@ -534,7 +545,7 @@ export class PortfolioCalculator { let exchangeRatesByCurrency = await this.exchangeRateDataService.getExchangeRatesByCurrency({ currencies: uniq(Object.values(currencies)), - endDate: endOfDay(end), + endDate: endOfDay(endDate), startDate: parseDate(this.transactionPoints?.[0]?.date), targetCurrency: this.currency }); @@ -570,7 +581,7 @@ export class PortfolioCalculator { } } - const endDateString = format(end, DATE_FORMAT); + const endDateString = format(endDate, DATE_FORMAT); if (firstIndex > 0) { firstIndex--; @@ -582,9 +593,9 @@ export class PortfolioCalculator { const errors: ResponseError['errors'] = []; for (const item of lastTransactionPoint.items) { - const marketPriceInBaseCurrency = marketSymbolMap[endDateString]?.[ - item.symbol - ]?.mul( + const marketPriceInBaseCurrency = ( + marketSymbolMap[endDateString]?.[item.symbol] ?? item.averagePrice + ).mul( exchangeRatesByCurrency[`${item.currency}${this.currency}`]?.[ endDateString ] @@ -607,9 +618,9 @@ export class PortfolioCalculator { totalInvestment, totalInvestmentWithCurrencyEffect } = this.getSymbolMetrics({ - end, marketSymbolMap, start, + end: endDate, exchangeRates: exchangeRatesByCurrency[`${item.currency}${this.currency}`], symbol: item.symbol @@ -656,7 +667,10 @@ export class PortfolioCalculator { quantity: item.quantity, symbol: item.symbol, tags: item.tags, - transactionCount: item.transactionCount + transactionCount: item.transactionCount, + valueInBaseCurrency: new Big(marketPriceInBaseCurrency).mul( + item.quantity + ) }); if ( @@ -725,7 +739,7 @@ export class PortfolioCalculator { } private calculateOverallPerformance(positions: TimelinePosition[]) { - let currentValue = new Big(0); + let currentValueInBaseCurrency = new Big(0); let grossPerformance = new Big(0); let grossPerformanceWithCurrencyEffect = new Big(0); let hasErrors = false; @@ -737,14 +751,9 @@ export class PortfolioCalculator { let totalTimeWeightedInvestmentWithCurrencyEffect = new Big(0); for (const currentPosition of positions) { - if ( - currentPosition.investment && - currentPosition.marketPriceInBaseCurrency - ) { - currentValue = currentValue.plus( - new Big(currentPosition.marketPriceInBaseCurrency).mul( - currentPosition.quantity - ) + if (currentPosition.valueInBaseCurrency) { + currentValueInBaseCurrency = currentValueInBaseCurrency.plus( + currentPosition.valueInBaseCurrency ); } else { hasErrors = true; @@ -801,7 +810,7 @@ export class PortfolioCalculator { } return { - currentValue, + currentValueInBaseCurrency, grossPerformance, grossPerformanceWithCurrencyEffect, hasErrors, diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index d55b3d647..78a803e9e 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -378,9 +378,10 @@ export class PortfolioService { }); const holdings: PortfolioDetails['holdings'] = {}; - const totalValueInBaseCurrency = currentPositions.currentValue.plus( - cashDetails.balanceInBaseCurrency - ); + const totalValueInBaseCurrency = + currentPositions.currentValueInBaseCurrency.plus( + cashDetails.balanceInBaseCurrency + ); const isFilteredByAccount = filters?.some((filter) => { @@ -389,7 +390,7 @@ export class PortfolioService { let filteredValueInBaseCurrency = isFilteredByAccount ? totalValueInBaseCurrency - : currentPositions.currentValue; + : currentPositions.currentValueInBaseCurrency; if ( filters?.length === 0 || @@ -444,14 +445,14 @@ export class PortfolioService { quantity, symbol, tags, - transactionCount + transactionCount, + valueInBaseCurrency } of currentPositions.positions) { if (quantity.eq(0)) { // Ignore positions without any quantity continue; } - const value = quantity.mul(marketPriceInBaseCurrency ?? 0); const symbolProfile = symbolProfileMap[symbol]; const dataProviderResponse = dataProviderResponses[symbol]; @@ -517,11 +518,11 @@ export class PortfolioService { } } else { markets[UNKNOWN_KEY] = new Big(markets[UNKNOWN_KEY]) - .plus(value) + .plus(valueInBaseCurrency) .toNumber(); marketsAdvanced[UNKNOWN_KEY] = new Big(marketsAdvanced[UNKNOWN_KEY]) - .plus(value) + .plus(valueInBaseCurrency) .toNumber(); } @@ -535,7 +536,7 @@ export class PortfolioService { transactionCount, allocationInPercentage: filteredValueInBaseCurrency.eq(0) ? 0 - : value.div(filteredValueInBaseCurrency).toNumber(), + : valueInBaseCurrency.div(filteredValueInBaseCurrency).toNumber(), assetClass: symbolProfile.assetClass, assetSubClass: symbolProfile.assetSubClass, countries: symbolProfile.countries, @@ -560,7 +561,7 @@ export class PortfolioService { quantity: quantity.toNumber(), sectors: symbolProfile.sectors, url: symbolProfile.url, - valueInBaseCurrency: value.toNumber() + valueInBaseCurrency: valueInBaseCurrency.toNumber() }; } @@ -1175,7 +1176,7 @@ export class PortfolioService { const startDate = this.getStartDate(dateRange, portfolioStart); const { - currentValue, + currentValueInBaseCurrency, errors, grossPerformance, grossPerformancePercentage, @@ -1270,7 +1271,7 @@ export class PortfolioService { currentNetPerformancePercentWithCurrencyEffect.toNumber(), currentNetPerformanceWithCurrencyEffect: currentNetPerformanceWithCurrencyEffect.toNumber(), - currentValue: currentValue.toNumber(), + currentValue: currentValueInBaseCurrency.toNumber(), totalInvestment: totalInvestment.toNumber() } }; diff --git a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts index a25f3a356..59f5144d8 100644 --- a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts +++ b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts @@ -26,6 +26,7 @@ export const ExchangeRateDataServiceMock = { return Promise.resolve({ USDUSD: { '2018-01-01': 1, + '2021-11-16': 1, '2023-07-10': 1 } }); diff --git a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts index 148fac560..a02ddb597 100644 --- a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts +++ b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts @@ -73,7 +73,17 @@ export class ExchangeRateDataService { currencyTo: targetCurrency }); - let previousExchangeRate = 1; + const dateStrings = Object.keys( + exchangeRatesByCurrency[`${currency}${targetCurrency}`] + ); + const lastDateString = dateStrings.reduce((a, b) => { + return a > b ? a : b; + }); + + let previousExchangeRate = + exchangeRatesByCurrency[`${currency}${targetCurrency}`]?.[ + lastDateString + ] ?? 1; // Start from the most recent date and fill in missing exchange rates // using the latest available rate @@ -94,7 +104,7 @@ export class ExchangeRateDataService { exchangeRatesByCurrency[`${currency}${targetCurrency}`][dateString] = previousExchangeRate; - if (currency === DEFAULT_CURRENCY) { + if (currency === DEFAULT_CURRENCY && isBefore(date, new Date())) { Logger.error( `No exchange rate has been found for ${currency}${targetCurrency} at ${dateString}`, 'ExchangeRateDataService' @@ -433,13 +443,17 @@ export class ExchangeRateDataService { ]) * marketPriceBaseCurrencyToCurrency[format(date, DATE_FORMAT)]; - factors[format(date, DATE_FORMAT)] = factor; + if (isNaN(factor)) { + throw new Error('Exchange rate is not a number'); + } else { + factors[format(date, DATE_FORMAT)] = factor; + } } catch { Logger.error( `No exchange rate has been found for ${currencyFrom}${currencyTo} at ${format( date, DATE_FORMAT - )}`, + )}. Please complement market data for ${DEFAULT_CURRENCY}${currencyFrom} and ${DEFAULT_CURRENCY}${currencyTo}.`, 'ExchangeRateDataService' ); } diff --git a/libs/common/src/lib/interfaces/timeline-position.interface.ts b/libs/common/src/lib/interfaces/timeline-position.interface.ts index 831c29b31..8e5dbb8e8 100644 --- a/libs/common/src/lib/interfaces/timeline-position.interface.ts +++ b/libs/common/src/lib/interfaces/timeline-position.interface.ts @@ -27,4 +27,5 @@ export interface TimelinePosition { timeWeightedInvestment: Big; timeWeightedInvestmentWithCurrencyEffect: Big; transactionCount: number; + valueInBaseCurrency: Big; } From b642ce08e573e3c11724ef39a255ecc4d496d32d Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 9 Mar 2024 12:32:56 +0100 Subject: [PATCH 013/120] Refactor item type (#3119) --- .../portfolio-calculator.interface.ts | 2 +- .../src/app/portfolio/portfolio-calculator.ts | 22 +++++++++---------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/apps/api/src/app/portfolio/interfaces/portfolio-calculator.interface.ts b/apps/api/src/app/portfolio/interfaces/portfolio-calculator.interface.ts index 357b454fd..c5266fb33 100644 --- a/apps/api/src/app/portfolio/interfaces/portfolio-calculator.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/portfolio-calculator.interface.ts @@ -5,7 +5,7 @@ import { PortfolioOrder } from './portfolio-order.interface'; export interface PortfolioOrderItem extends PortfolioOrder { feeInBaseCurrency?: Big; feeInBaseCurrencyWithCurrencyEffect?: Big; - itemType?: '' | 'start' | 'end'; + itemType?: 'end' | 'start'; unitPriceInBaseCurrency?: Big; unitPriceInBaseCurrencyWithCurrencyEffect?: Big; } diff --git a/apps/api/src/app/portfolio/portfolio-calculator.ts b/apps/api/src/app/portfolio/portfolio-calculator.ts index 07016efcf..1521787e7 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.ts @@ -1049,28 +1049,26 @@ export class PortfolioCalculator { } } - // Sort orders so that the start and end placeholder order are at the right + // Sort orders so that the start and end placeholder order are at the correct // position - orders = sortBy(orders, (order) => { - let sortIndex = new Date(order.date); + orders = sortBy(orders, ({ date, itemType }) => { + let sortIndex = new Date(date); - if (order.itemType === 'start') { - sortIndex = addMilliseconds(sortIndex, -1); - } - - if (order.itemType === 'end') { + if (itemType === 'end') { sortIndex = addMilliseconds(sortIndex, 1); + } else if (itemType === 'start') { + sortIndex = addMilliseconds(sortIndex, -1); } return sortIndex.getTime(); }); - const indexOfStartOrder = orders.findIndex((order) => { - return order.itemType === 'start'; + const indexOfStartOrder = orders.findIndex(({ itemType }) => { + return itemType === 'start'; }); - const indexOfEndOrder = orders.findIndex((order) => { - return order.itemType === 'end'; + const indexOfEndOrder = orders.findIndex(({ itemType }) => { + return itemType === 'end'; }); let totalInvestmentDays = 0; From d9d71e78271a1baf8c39d12f6dfab8118794abf5 Mon Sep 17 00:00:00 2001 From: Gerard Du Pre <37554513+GerardPolloRebozado@users.noreply.github.com> Date: Sat, 9 Mar 2024 15:52:05 +0100 Subject: [PATCH 014/120] Fix issue with removing account from activity (#3112) * Fix issue with removing account from activity * Update changelog --------- Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> --- CHANGELOG.md | 3 +- apps/api/src/app/order/order.service.ts | 40 +++++++------------------ 2 files changed, 13 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 531e0de91..7c1e7a219 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed an issue in the calculation of the portfolio summary caused by future liabilities +- Fixed an issue with removing a linked account from a (wealth) item activity ## 2.61.1 - 2024-03-06 @@ -30,7 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Fixed the the activities import (query parameter handling) +- Fixed the activities import (query parameter handling) ## 2.60.0 - 2024-03-02 diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index c97243929..1603ed530 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -70,12 +70,7 @@ export class OrderService { const updateAccountBalance = data.updateAccountBalance ?? false; const userId = data.userId; - if ( - data.type === 'FEE' || - data.type === 'INTEREST' || - data.type === 'ITEM' || - data.type === 'LIABILITY' - ) { + if (['FEE', 'INTEREST', 'ITEM', 'LIABILITY'].includes(data.type)) { const assetClass = data.assetClass; const assetSubClass = data.assetSubClass; currency = data.SymbolProfile.connectOrCreate.create.currency; @@ -130,13 +125,9 @@ export class OrderService { const orderData: Prisma.OrderCreateInput = data; - const isDraft = - data.type === 'FEE' || - data.type === 'INTEREST' || - data.type === 'ITEM' || - data.type === 'LIABILITY' - ? false - : isAfter(data.date as Date, endOfToday()); + const isDraft = ['FEE', 'INTEREST', 'ITEM', 'LIABILITY'].includes(data.type) + ? false + : isAfter(data.date as Date, endOfToday()); const order = await this.prismaService.order.create({ data: { @@ -180,12 +171,7 @@ export class OrderService { where }); - if ( - order.type === 'FEE' || - order.type === 'INTEREST' || - order.type === 'ITEM' || - order.type === 'LIABILITY' - ) { + if (['FEE', 'INTEREST', 'ITEM', 'LIABILITY'].includes(order.type)) { await this.symbolProfileService.deleteById(order.symbolProfileId); } @@ -377,13 +363,10 @@ export class OrderService { dataSource?: DataSource; symbol?: string; tags?: Tag[]; + type?: ActivityType; }; where: Prisma.OrderWhereUniqueInput; }): Promise { - if (data.Account.connect.id_userId.id === null) { - delete data.Account; - } - if (!data.comment) { data.comment = null; } @@ -392,13 +375,12 @@ export class OrderService { let isDraft = false; - if ( - data.type === 'FEE' || - data.type === 'INTEREST' || - data.type === 'ITEM' || - data.type === 'LIABILITY' - ) { + if (['FEE', 'INTEREST', 'ITEM', 'LIABILITY'].includes(data.type)) { delete data.SymbolProfile.connect; + + if (data.Account?.connect?.id_userId?.id === null) { + data.Account = { disconnect: true }; + } } else { delete data.SymbolProfile.update; From d8bfb23f208867a0d5483f97046e761465c36c4a Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 9 Mar 2024 16:53:59 +0100 Subject: [PATCH 015/120] Refactor reduce() with getSum() (#3121) --- .../src/app/portfolio/portfolio.service.ts | 85 +++++++++---------- 1 file changed, 38 insertions(+), 47 deletions(-) diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 78a803e9e..5848a78c5 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -1575,29 +1575,26 @@ export class PortfolioService { private getFees({ activities, - date = new Date(0), userCurrency }: { activities: OrderWithAccount[]; - date?: Date; userCurrency: string; }) { - return activities - .filter((activity) => { - // Filter out all activities before given date (drafts) - return isBefore(date, new Date(activity.date)); - }) - .map(({ fee, SymbolProfile }) => { - return this.exchangeRateDataService.toCurrency( - fee, - SymbolProfile.currency, - userCurrency - ); - }) - .reduce( - (previous, current) => new Big(previous).plus(current), - new Big(0) - ); + return getSum( + activities + .filter(({ isDraft }) => { + return isDraft === false; + }) + .map(({ fee, SymbolProfile }) => { + return new Big( + this.exchangeRateDataService.toCurrency( + fee, + SymbolProfile.currency, + userCurrency + ) + ); + }) + ); } private getInitialCashPosition({ @@ -1745,15 +1742,16 @@ export class PortfolioService { } } - const dividendInBaseCurrency = ( - await this.getDividends({ - activities: activities.filter(({ type }) => { - return type === 'DIVIDEND'; + const dividendInBaseCurrency = getSum( + ( + await this.getDividends({ + activities: activities.filter(({ type }) => { + return type === 'DIVIDEND'; + }) }) + ).map(({ investment }) => { + return new Big(investment); }) - ).reduce( - (previous, current) => new Big(previous).plus(current.investment), - new Big(0) ); const emergencyFund = new Big( @@ -1919,34 +1917,27 @@ export class PortfolioService { private getSumOfActivityType({ activities, activityType, - date = new Date(0), userCurrency }: { activities: OrderWithAccount[]; activityType: ActivityType; - date?: Date; userCurrency: string; }) { - return activities - .filter((activity) => { - // Filter out all activities before given date (drafts) and - // activity type - return ( - isBefore(date, new Date(activity.date)) && - activity.type === activityType - ); - }) - .map(({ quantity, SymbolProfile, unitPrice }) => { - return this.exchangeRateDataService.toCurrency( - new Big(quantity).mul(unitPrice).toNumber(), - SymbolProfile.currency, - userCurrency - ); - }) - .reduce( - (previous, current) => new Big(previous).plus(current), - new Big(0) - ); + return getSum( + activities + .filter(({ isDraft, type }) => { + return isDraft === false && type === activityType; + }) + .map(({ quantity, SymbolProfile, unitPrice }) => { + return new Big( + this.exchangeRateDataService.toCurrency( + new Big(quantity).mul(unitPrice).toNumber(), + SymbolProfile.currency, + userCurrency + ) + ); + }) + ); } private async getTransactionPoints({ From 6d2a89736655804be70a22230018abdfc8d8d01e Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 9 Mar 2024 17:17:52 +0100 Subject: [PATCH 016/120] Refactor orders with activities (#3122) --- .../src/app/portfolio/portfolio.service.ts | 37 +++++++------------ 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 5848a78c5..f03e6ec84 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -348,7 +348,7 @@ export class PortfolioService { (user.Settings?.settings as UserSettings)?.emergencyFund ?? 0 ); - const { orders, portfolioOrders, transactionPoints } = + const { activities, portfolioOrders, transactionPoints } = await this.getTransactionPoints({ filters, userId, @@ -582,8 +582,8 @@ export class PortfolioService { } const { accounts, platforms } = await this.getValueOfAccountsAndPlatforms({ + activities, filters, - orders, portfolioItemsNow, userCurrency, userId, @@ -1282,7 +1282,7 @@ export class PortfolioService { const user = await this.userService.user({ id: userId }); const userCurrency = this.getUserCurrency(user); - const { orders, portfolioOrders, transactionPoints } = + const { activities, portfolioOrders, transactionPoints } = await this.getTransactionPoints({ userId, types: ['BUY', 'SELL'] @@ -1314,7 +1314,7 @@ export class PortfolioService { } const { accounts } = await this.getValueOfAccountsAndPlatforms({ - orders, + activities, portfolioItemsNow, userCurrency, userId @@ -1324,7 +1324,7 @@ export class PortfolioService { return { rules: { - accountClusterRisk: isEmpty(orders) + accountClusterRisk: isEmpty(activities) ? undefined : await this.rulesService.evaluate( [ @@ -1339,7 +1339,7 @@ export class PortfolioService { ], userSettings ), - currencyClusterRisk: isEmpty(orders) + currencyClusterRisk: isEmpty(activities) ? undefined : await this.rulesService.evaluate( [ @@ -1368,7 +1368,7 @@ export class PortfolioService { new FeeRatioInitialInvestment( this.exchangeRateDataService, currentPositions.totalInvestment.toNumber(), - this.getFees({ userCurrency, activities: orders }).toNumber() + this.getFees({ activities, userCurrency }).toNumber() ) ], userSettings @@ -1953,8 +1953,8 @@ export class PortfolioService { userId: string; withExcludedAccounts?: boolean; }): Promise<{ + activities: Activity[]; transactionPoints: TransactionPoint[]; - orders: Activity[]; portfolioOrders: PortfolioOrder[]; }> { const userCurrency = @@ -1970,7 +1970,7 @@ export class PortfolioService { }); if (count <= 0) { - return { transactionPoints: [], orders: [], portfolioOrders: [] }; + return { activities: [], transactionPoints: [], portfolioOrders: [] }; } const portfolioOrders: PortfolioOrder[] = activities.map((order) => ({ @@ -1996,8 +1996,8 @@ export class PortfolioService { portfolioCalculator.computeTransactionPoints(); return { + activities, portfolioOrders, - orders: activities, transactionPoints: portfolioCalculator.getTransactionPoints() }; } @@ -2018,29 +2018,20 @@ export class PortfolioService { } private async getValueOfAccountsAndPlatforms({ + activities, filters = [], - orders, portfolioItemsNow, userCurrency, userId, withExcludedAccounts = false }: { + activities: Activity[]; filters?: Filter[]; - orders: Activity[]; portfolioItemsNow: { [p: string]: TimelinePosition }; userCurrency: string; userId: string; withExcludedAccounts?: boolean; }) { - const { activities: ordersOfTypeItemOrLiability } = - await this.orderService.getOrders({ - filters, - userCurrency, - userId, - withExcludedAccounts, - types: ['LIABILITY'] - }); - const accounts: PortfolioDetails['accounts'] = {}; const platforms: PortfolioDetails['platforms'] = {}; @@ -2058,7 +2049,7 @@ export class PortfolioService { }); } else { const accountIds = uniq( - orders + activities .filter(({ accountId }) => { return accountId; }) @@ -2078,7 +2069,7 @@ export class PortfolioService { }); for (const account of currentAccounts) { - const ordersByAccount = orders.filter(({ accountId }) => { + const ordersByAccount = activities.filter(({ accountId }) => { return accountId === account.id; }); From eb75be8535f2b40e6f2a2ab57031840f866bce41 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 9 Mar 2024 19:56:26 +0100 Subject: [PATCH 017/120] Optimize details endpoint (#3123) * Make summary optional * Introduce dedicated holdings endpoint * Update changelog --- CHANGELOG.md | 2 + .../src/app/portfolio/portfolio.controller.ts | 62 +++++++++++------ .../src/app/portfolio/portfolio.service.ts | 69 ++++++++++++------- .../redact-values-in-response.interceptor.ts | 2 - .../account-detail-dialog.component.ts | 8 +-- .../portfolio-summary.component.html | 2 +- .../allocations/allocations-page.component.ts | 1 - .../allocations/allocations-page.html | 9 +-- .../holdings/holdings-page.component.ts | 30 ++------ apps/client/src/app/services/data.service.ts | 41 +++++++++++ libs/common/src/lib/helper.ts | 6 +- libs/common/src/lib/interfaces/index.ts | 2 + .../interfaces/portfolio-details.interface.ts | 5 +- .../interfaces/portfolio-summary.interface.ts | 4 +- .../portfolio-holdings-response.interface.ts | 5 ++ 15 files changed, 159 insertions(+), 89 deletions(-) create mode 100644 libs/common/src/lib/interfaces/responses/portfolio-holdings-response.interface.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c1e7a219..9918a4420 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Optimized the calculation of the accounts table +- Optimized the calculation of the portfolio holdings - Integrated dividend into the transaction point concept in the portfolio service - Removed the environment variable `WEB_AUTH_RP_ID` diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 63e26e512..748850204 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -20,6 +20,7 @@ import { import { PortfolioDetails, PortfolioDividends, + PortfolioHoldingsResponse, PortfolioInvestments, PortfolioPerformanceResponse, PortfolioPublicDetails, @@ -95,21 +96,15 @@ export class PortfolioController { filterByTags }); - const { - accounts, - filteredValueInBaseCurrency, - filteredValueInPercentage, - hasErrors, - holdings, - platforms, - summary, - totalValueInBaseCurrency - } = await this.portfolioService.getDetails({ - dateRange, - filters, - impersonationId, - userId: this.request.user.id - }); + const { accounts, hasErrors, holdings, platforms, summary } = + await this.portfolioService.getDetails({ + dateRange, + filters, + impersonationId, + userId: this.request.user.id, + withLiabilities: true, + withSummary: true + }); if (hasErrors || hasNotDefinedValuesInObject(holdings)) { hasError = true; @@ -164,19 +159,21 @@ export class PortfolioController { 'currentGrossPerformanceWithCurrencyEffect', 'currentNetPerformance', 'currentNetPerformanceWithCurrencyEffect', + 'currentNetWorth', 'currentValue', 'dividendInBaseCurrency', 'emergencyFund', 'excludedAccountsAndActivities', 'fees', + 'filteredValueInBaseCurrency', 'fireWealth', 'interest', 'items', 'liabilities', - 'netWorth', 'totalBuy', 'totalInvestment', - 'totalSell' + 'totalSell', + 'totalValueInBaseCurrency' ]); } @@ -203,12 +200,9 @@ export class PortfolioController { return { accounts, - filteredValueInBaseCurrency, - filteredValueInPercentage, hasError, holdings, platforms, - totalValueInBaseCurrency, summary: portfolioSummary }; } @@ -279,6 +273,33 @@ export class PortfolioController { return { dividends }; } + @Get('holdings') + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) + @UseInterceptors(RedactValuesInResponseInterceptor) + @UseInterceptors(TransformDataSourceInResponseInterceptor) + public async getHoldings( + @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string, + @Query('accounts') filterByAccounts?: string, + @Query('assetClasses') filterByAssetClasses?: string, + @Query('query') filterBySearchQuery?: string, + @Query('tags') filterByTags?: string + ): Promise { + const filters = this.apiService.buildFiltersFromQueryParams({ + filterByAccounts, + filterByAssetClasses, + filterBySearchQuery, + filterByTags + }); + + const { holdings } = await this.portfolioService.getDetails({ + filters, + impersonationId, + userId: this.request.user.id + }); + + return { holdings: Object.values(holdings) }; + } + @Get('investments') @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async getInvestments( @@ -502,7 +523,6 @@ export class PortfolioController { } const { holdings } = await this.portfolioService.getDetails({ - dateRange: 'max', filters: [{ id: 'EQUITY', type: 'ASSET_CLASS' }], impersonationId: access.userId, userId: user.id diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index f03e6ec84..9880abcc4 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -24,7 +24,12 @@ import { MAX_CHART_ITEMS, UNKNOWN_KEY } from '@ghostfolio/common/config'; -import { DATE_FORMAT, getSum, parseDate } from '@ghostfolio/common/helper'; +import { + DATE_FORMAT, + getAllActivityTypes, + getSum, + parseDate +} from '@ghostfolio/common/helper'; import { Accounts, EnhancedSymbolProfile, @@ -141,7 +146,8 @@ export class PortfolioService { filters, withExcludedAccounts, impersonationId: userId, - userId: this.request.user.id + userId: this.request.user.id, + withLiabilities: true }) ]); @@ -332,13 +338,17 @@ export class PortfolioService { filters, impersonationId, userId, - withExcludedAccounts = false + withExcludedAccounts = false, + withLiabilities = false, + withSummary = false }: { dateRange?: DateRange; filters?: Filter[]; impersonationId: string; userId: string; withExcludedAccounts?: boolean; + withLiabilities?: boolean; + withSummary?: boolean; }): Promise { userId = await this.getUserId(impersonationId, userId); const user = await this.userService.user({ id: userId }); @@ -352,7 +362,12 @@ export class PortfolioService { await this.getTransactionPoints({ filters, userId, - withExcludedAccounts + withExcludedAccounts, + types: withLiabilities + ? undefined + : getAllActivityTypes().filter((activityType) => { + return activityType !== 'LIABILITY'; + }) }); const portfolioCalculator = new PortfolioCalculator({ @@ -625,29 +640,29 @@ export class PortfolioService { }; } - const summary = await this.getSummary({ - holdings, - impersonationId, - userCurrency, - userId, - balanceInBaseCurrency: cashDetails.balanceInBaseCurrency, - emergencyFundPositionsValueInBaseCurrency: - this.getEmergencyFundPositionsValueInBaseCurrency({ - holdings - }) - }); + let summary: PortfolioSummary; + + if (withSummary) { + summary = await this.getSummary({ + filteredValueInBaseCurrency, + holdings, + impersonationId, + userCurrency, + userId, + balanceInBaseCurrency: cashDetails.balanceInBaseCurrency, + emergencyFundPositionsValueInBaseCurrency: + this.getEmergencyFundPositionsValueInBaseCurrency({ + holdings + }) + }); + } return { accounts, holdings, platforms, summary, - filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(), - filteredValueInPercentage: summary.netWorth - ? filteredValueInBaseCurrency.div(summary.netWorth).toNumber() - : 0, - hasErrors: currentPositions.hasErrors, - totalValueInBaseCurrency: summary.netWorth + hasErrors: currentPositions.hasErrors }; } @@ -1705,6 +1720,7 @@ export class PortfolioService { private async getSummary({ balanceInBaseCurrency, emergencyFundPositionsValueInBaseCurrency, + filteredValueInBaseCurrency, holdings, impersonationId, userCurrency, @@ -1712,6 +1728,7 @@ export class PortfolioService { }: { balanceInBaseCurrency: number; emergencyFundPositionsValueInBaseCurrency: number; + filteredValueInBaseCurrency: Big; holdings: PortfolioDetails['holdings']; impersonationId: string; userCurrency: string; @@ -1893,7 +1910,6 @@ export class PortfolioService { interest, items, liabilities, - netWorth, totalBuy, totalSell, committedFunds: committedFunds.toNumber(), @@ -1905,12 +1921,17 @@ export class PortfolioService { .toNumber(), total: emergencyFund.toNumber() }, + filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(), + filteredValueInPercentage: netWorth + ? filteredValueInBaseCurrency.div(netWorth).toNumber() + : undefined, fireWealth: new Big(performanceInformation.performance.currentValue) .minus(emergencyFundPositionsValueInBaseCurrency) .toNumber(), ordersCount: activities.filter(({ type }) => { return type === 'BUY' || type === 'SELL'; - }).length + }).length, + totalValueInBaseCurrency: netWorth }; } @@ -1943,7 +1964,7 @@ export class PortfolioService { private async getTransactionPoints({ filters, includeDrafts = false, - types = ['BUY', 'DIVIDEND', 'ITEM', 'LIABILITY', 'SELL'], + types = getAllActivityTypes(), userId, withExcludedAccounts = false }: { diff --git a/apps/api/src/interceptors/redact-values-in-response.interceptor.ts b/apps/api/src/interceptors/redact-values-in-response.interceptor.ts index b1889cf9d..78ae918d2 100644 --- a/apps/api/src/interceptors/redact-values-in-response.interceptor.ts +++ b/apps/api/src/interceptors/redact-values-in-response.interceptor.ts @@ -49,7 +49,6 @@ export class RedactValuesInResponseInterceptor 'dividendInBaseCurrency', 'fee', 'feeInBaseCurrency', - 'filteredValueInBaseCurrency', 'grossPerformance', 'grossPerformanceWithCurrencyEffect', 'investment', @@ -58,7 +57,6 @@ export class RedactValuesInResponseInterceptor 'quantity', 'symbolMapping', 'totalBalanceInBaseCurrency', - 'totalValueInBaseCurrency', 'unitPrice', 'value', 'valueInBaseCurrency' diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts index 2cd48a561..760f8081b 100644 --- a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts +++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts @@ -115,7 +115,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit { ); this.dataService - .fetchPortfolioDetails({ + .fetchPortfolioHoldings({ filters: [ { type: 'ACCOUNT', @@ -125,11 +125,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit { }) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(({ holdings }) => { - this.holdings = []; - - for (const [symbol, holding] of Object.entries(holdings)) { - this.holdings.push(holding); - } + this.holdings = holdings; this.changeDetectorRef.markForCheck(); }); diff --git a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html index 096271926..347767011 100644 --- a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html +++ b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -282,7 +282,7 @@ [isCurrency]="true" [locale]="locale" [unit]="baseCurrency" - [value]="isLoading ? undefined : summary?.netWorth" + [value]="isLoading ? undefined : summary?.totalValueInBaseCurrency" />
diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts index 330ae4227..67ad82316 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts @@ -281,7 +281,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { this.platforms = {}; this.portfolioDetails = { accounts: {}, - filteredValueInPercentage: 0, holdings: {}, platforms: {}, summary: undefined diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.html b/apps/client/src/app/pages/portfolio/allocations/allocations-page.html index 312061775..04bf96f39 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.html +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.html @@ -18,7 +18,7 @@ [value]=" isLoading ? undefined - : portfolioDetails?.filteredValueInPercentage + : portfolioDetails?.summary?.filteredValueInPercentage " /> @@ -26,10 +26,11 @@ diff --git a/apps/client/src/app/pages/portfolio/holdings/holdings-page.component.ts b/apps/client/src/app/pages/portfolio/holdings/holdings-page.component.ts index 6c4a058b7..6635393f5 100644 --- a/apps/client/src/app/pages/portfolio/holdings/holdings-page.component.ts +++ b/apps/client/src/app/pages/portfolio/holdings/holdings-page.component.ts @@ -3,11 +3,7 @@ import { PositionDetailDialog } from '@ghostfolio/client/components/position/pos import { DataService } from '@ghostfolio/client/services/data.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; -import { - PortfolioDetails, - PortfolioPosition, - User -} from '@ghostfolio/common/interfaces'; +import { PortfolioPosition, User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; @@ -28,8 +24,6 @@ export class HoldingsPageComponent implements OnDestroy, OnInit { public hasImpersonationId: boolean; public hasPermissionToCreateOrder: boolean; public holdings: PortfolioPosition[]; - public isLoading = false; - public portfolioDetails: PortfolioDetails; public user: User; private unsubscribeSubject = new Subject(); @@ -83,12 +77,10 @@ export class HoldingsPageComponent implements OnDestroy, OnInit { this.holdings = undefined; - this.fetchPortfolioDetails() + this.fetchHoldings() .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((portfolioDetails) => { - this.portfolioDetails = portfolioDetails; - - this.initialize(); + .subscribe(({ holdings }) => { + this.holdings = holdings; this.changeDetectorRef.markForCheck(); }); @@ -103,22 +95,12 @@ export class HoldingsPageComponent implements OnDestroy, OnInit { this.unsubscribeSubject.complete(); } - private fetchPortfolioDetails() { - return this.dataService.fetchPortfolioDetails({ + private fetchHoldings() { + return this.dataService.fetchPortfolioHoldings({ filters: this.userService.getFilters() }); } - private initialize() { - this.holdings = []; - - for (const [symbol, holding] of Object.entries( - this.portfolioDetails.holdings - )) { - this.holdings.push(holding); - } - } - private openPositionDialog({ dataSource, symbol diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 6555a964d..7741fd601 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -27,6 +27,7 @@ import { OAuthResponse, PortfolioDetails, PortfolioDividends, + PortfolioHoldingsResponse, PortfolioInvestments, PortfolioPerformanceResponse, PortfolioPublicDetails, @@ -434,6 +435,46 @@ export class DataService { ); } + public fetchPortfolioHoldings({ + filters + }: { + filters?: Filter[]; + } = {}) { + return this.http + .get('/api/v1/portfolio/holdings', { + params: this.buildFiltersAsQueryParams({ filters }) + }) + .pipe( + map((response) => { + if (response.holdings) { + for (const symbol of Object.keys(response.holdings)) { + response.holdings[symbol].assetClassLabel = translate( + response.holdings[symbol].assetClass + ); + + response.holdings[symbol].assetSubClassLabel = translate( + response.holdings[symbol].assetSubClass + ); + + response.holdings[symbol].dateOfFirstActivity = response.holdings[ + symbol + ].dateOfFirstActivity + ? parseISO(response.holdings[symbol].dateOfFirstActivity) + : undefined; + + response.holdings[symbol].value = isNumber( + response.holdings[symbol].value + ) + ? response.holdings[symbol].value + : response.holdings[symbol].valueInPercentage; + } + } + + return response; + }) + ); + } + public fetchPortfolioPerformance({ filters, range, diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index e55800628..c52ec9ca5 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -1,6 +1,6 @@ import * as currencies from '@dinero.js/currencies'; import { NumberParser } from '@internationalized/number'; -import { DataSource, MarketData } from '@prisma/client'; +import { DataSource, MarketData, Type as ActivityType } from '@prisma/client'; import Big from 'big.js'; import { getDate, @@ -138,6 +138,10 @@ export function extractNumberFromString({ } } +export function getAllActivityTypes(): ActivityType[] { + return ['BUY', 'DIVIDEND', 'FEE', 'ITEM', 'LIABILITY', 'SELL']; +} + export function getAssetProfileIdentifier({ dataSource, symbol }: UniqueAsset) { return `${dataSource}-${symbol}`; } diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts index 7d77826d0..dba1ac79a 100644 --- a/libs/common/src/lib/interfaces/index.ts +++ b/libs/common/src/lib/interfaces/index.ts @@ -40,6 +40,7 @@ import type { BenchmarkResponse } from './responses/benchmark-response.interface import type { ResponseError } from './responses/errors.interface'; import type { ImportResponse } from './responses/import-response.interface'; import type { OAuthResponse } from './responses/oauth-response.interface'; +import type { PortfolioHoldingsResponse } from './responses/portfolio-holdings-response.interface'; import type { PortfolioPerformanceResponse } from './responses/portfolio-performance-response.interface'; import type { ScraperConfiguration } from './scraper-configuration.interface'; import type { Statistics } from './statistics.interface'; @@ -81,6 +82,7 @@ export { PortfolioChart, PortfolioDetails, PortfolioDividends, + PortfolioHoldingsResponse, PortfolioInvestments, PortfolioItem, PortfolioOverview, diff --git a/libs/common/src/lib/interfaces/portfolio-details.interface.ts b/libs/common/src/lib/interfaces/portfolio-details.interface.ts index 565c17c7d..611ed8056 100644 --- a/libs/common/src/lib/interfaces/portfolio-details.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-details.interface.ts @@ -13,8 +13,6 @@ export interface PortfolioDetails { valueInPercentage?: number; }; }; - filteredValueInBaseCurrency?: number; - filteredValueInPercentage: number; holdings: { [symbol: string]: PortfolioPosition }; platforms: { [id: string]: { @@ -25,6 +23,5 @@ export interface PortfolioDetails { valueInPercentage?: number; }; }; - summary: PortfolioSummary; - totalValueInBaseCurrency?: number; + summary?: PortfolioSummary; } diff --git a/libs/common/src/lib/interfaces/portfolio-summary.interface.ts b/libs/common/src/lib/interfaces/portfolio-summary.interface.ts index aaf80c0cd..de04dc24c 100644 --- a/libs/common/src/lib/interfaces/portfolio-summary.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-summary.interface.ts @@ -13,13 +13,15 @@ export interface PortfolioSummary extends PortfolioPerformance { }; excludedAccountsAndActivities: number; fees: number; + filteredValueInBaseCurrency?: number; + filteredValueInPercentage?: number; fireWealth: number; firstOrderDate: Date; interest: number; items: number; liabilities: number; - netWorth: number; ordersCount: number; totalBuy: number; totalSell: number; + totalValueInBaseCurrency?: number; } diff --git a/libs/common/src/lib/interfaces/responses/portfolio-holdings-response.interface.ts b/libs/common/src/lib/interfaces/responses/portfolio-holdings-response.interface.ts new file mode 100644 index 000000000..d2cf38f55 --- /dev/null +++ b/libs/common/src/lib/interfaces/responses/portfolio-holdings-response.interface.ts @@ -0,0 +1,5 @@ +import { PortfolioPosition } from '@ghostfolio/common/interfaces'; + +export interface PortfolioHoldingsResponse { + holdings: PortfolioPosition[]; +} From ba73f6de2e3019c1803d9e5fa45fca66d80bb618 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 9 Mar 2024 19:57:56 +0100 Subject: [PATCH 018/120] Release 2.62.0 (#3124) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9918a4420..e4f689445 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## 2.62.0 - 2024-03-09 ### Changed diff --git a/package.json b/package.json index 8faa5ff8a..ddd5de1cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "2.61.1", + "version": "2.62.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio", From 0bca8897d69e8b88966195955f08a00d4e6c8c32 Mon Sep 17 00:00:00 2001 From: gizmodus Date: Sun, 10 Mar 2024 09:35:47 +0100 Subject: [PATCH 019/120] Fix average price calculation by only considering BUY transactions (#3125) * Fix average price calculation by only considering buy transactions * Update changelog --- CHANGELOG.md | 6 + ...aln-buy-and-sell-in-two-activities.spec.ts | 166 ++++++++++++++++++ .../src/app/portfolio/portfolio-calculator.ts | 60 +++---- 3 files changed, 199 insertions(+), 33 deletions(-) create mode 100644 apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e4f689445..33baa68c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Fixed + +- Fixed an issue in the performance calculation caused by multiple `SELL` activities on the same day + ## 2.62.0 - 2024-03-09 ### Changed diff --git a/apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts new file mode 100644 index 000000000..703931846 --- /dev/null +++ b/apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts @@ -0,0 +1,166 @@ +import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; +import { parseDate } from '@ghostfolio/common/helper'; + +import Big from 'big.js'; + +import { CurrentRateServiceMock } from './current-rate.service.mock'; +import { PortfolioCalculator } from './portfolio-calculator'; + +jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + CurrentRateService: jest.fn().mockImplementation(() => { + return CurrentRateServiceMock; + }) + }; +}); + +describe('PortfolioCalculator', () => { + let currentRateService: CurrentRateService; + let exchangeRateDataService: ExchangeRateDataService; + + beforeEach(() => { + currentRateService = new CurrentRateService(null, null, null, null); + + exchangeRateDataService = new ExchangeRateDataService( + null, + null, + null, + null + ); + }); + + describe('get current positions', () => { + it.only('with BALN.SW buy and sell in two activities', async () => { + const portfolioCalculator = new PortfolioCalculator({ + currentRateService, + exchangeRateDataService, + currency: 'CHF', + orders: [ + { + currency: 'CHF', + date: '2021-11-22', + dataSource: 'YAHOO', + fee: new Big(1.55), + name: 'Bâloise Holding AG', + quantity: new Big(2), + symbol: 'BALN.SW', + type: 'BUY', + unitPrice: new Big(142.9) + }, + { + currency: 'CHF', + date: '2021-11-30', + dataSource: 'YAHOO', + fee: new Big(1.65), + name: 'Bâloise Holding AG', + quantity: new Big(1), + symbol: 'BALN.SW', + type: 'SELL', + unitPrice: new Big(136.6) + }, + { + currency: 'CHF', + date: '2021-11-30', + dataSource: 'YAHOO', + fee: new Big(0), + name: 'Bâloise Holding AG', + quantity: new Big(1), + symbol: 'BALN.SW', + type: 'SELL', + unitPrice: new Big(136.6) + } + ] + }); + + portfolioCalculator.computeTransactionPoints(); + + const spy = jest + .spyOn(Date, 'now') + .mockImplementation(() => parseDate('2021-12-18').getTime()); + + const chartData = await portfolioCalculator.getChartData({ + start: parseDate('2021-11-22') + }); + + const currentPositions = await portfolioCalculator.getCurrentPositions( + parseDate('2021-11-22') + ); + + const investments = portfolioCalculator.getInvestments(); + + const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ + data: chartData, + groupBy: 'month' + }); + + spy.mockRestore(); + + expect(currentPositions).toEqual({ + currentValueInBaseCurrency: new Big('0'), + errors: [], + grossPerformance: new Big('-12.6'), + grossPerformancePercentage: new Big('-0.04408677396780965649'), + grossPerformancePercentageWithCurrencyEffect: new Big( + '-0.04408677396780965649' + ), + grossPerformanceWithCurrencyEffect: new Big('-12.6'), + hasErrors: false, + netPerformance: new Big('-15.8'), + netPerformancePercentage: new Big('-0.05528341497550734703'), + netPerformancePercentageWithCurrencyEffect: new Big( + '-0.05528341497550734703' + ), + netPerformanceWithCurrencyEffect: new Big('-15.8'), + positions: [ + { + averagePrice: new Big('0'), + currency: 'CHF', + dataSource: 'YAHOO', + dividend: new Big('0'), + dividendInBaseCurrency: new Big('0'), + fee: new Big('3.2'), + firstBuyDate: '2021-11-22', + grossPerformance: new Big('-12.6'), + grossPerformancePercentage: new Big('-0.04408677396780965649'), + grossPerformancePercentageWithCurrencyEffect: new Big( + '-0.04408677396780965649' + ), + grossPerformanceWithCurrencyEffect: new Big('-12.6'), + investment: new Big('0'), + investmentWithCurrencyEffect: new Big('0'), + netPerformance: new Big('-15.8'), + netPerformancePercentage: new Big('-0.05528341497550734703'), + netPerformancePercentageWithCurrencyEffect: new Big( + '-0.05528341497550734703' + ), + netPerformanceWithCurrencyEffect: new Big('-15.8'), + marketPrice: 148.9, + marketPriceInBaseCurrency: 148.9, + quantity: new Big('0'), + symbol: 'BALN.SW', + timeWeightedInvestment: new Big('285.80000000000000396627'), + timeWeightedInvestmentWithCurrencyEffect: new Big( + '285.80000000000000396627' + ), + transactionCount: 3, + valueInBaseCurrency: new Big('0') + } + ], + totalInvestment: new Big('0'), + totalInvestmentWithCurrencyEffect: new Big('0') + }); + + expect(investments).toEqual([ + { date: '2021-11-22', investment: new Big('285.8') }, + { date: '2021-11-30', investment: new Big('0') } + ]); + + expect(investmentsByMonth).toEqual([ + { date: '2021-11-01', investment: 0 }, + { date: '2021-12-01', investment: 0 } + ]); + }); + }); +}); diff --git a/apps/api/src/app/portfolio/portfolio-calculator.ts b/apps/api/src/app/portfolio/portfolio-calculator.ts index 1521787e7..faf954bbd 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.ts @@ -893,13 +893,10 @@ export class PortfolioCalculator { } = {}; let totalInvestment = new Big(0); + let totalInvestmentFromBuyTransactions = new Big(0); + let totalInvestmentFromBuyTransactionsWithCurrencyEffect = new Big(0); let totalInvestmentWithCurrencyEffect = new Big(0); - let totalInvestmentWithGrossPerformanceFromSell = new Big(0); - - let totalInvestmentWithGrossPerformanceFromSellWithCurrencyEffect = new Big( - 0 - ); - + let totalQuantityFromBuyTransactions = new Big(0); let totalUnits = new Big(0); let valueAtStartDate: Big; let valueAtStartDateWithCurrencyEffect: Big; @@ -1138,9 +1135,21 @@ export class PortfolioCalculator { transactionInvestment = order.quantity .mul(order.unitPriceInBaseCurrency) .mul(getFactor(order.type)); + transactionInvestmentWithCurrencyEffect = order.quantity .mul(order.unitPriceInBaseCurrencyWithCurrencyEffect) .mul(getFactor(order.type)); + + totalQuantityFromBuyTransactions = + totalQuantityFromBuyTransactions.plus(order.quantity); + + totalInvestmentFromBuyTransactions = + totalInvestmentFromBuyTransactions.plus(transactionInvestment); + + totalInvestmentFromBuyTransactionsWithCurrencyEffect = + totalInvestmentFromBuyTransactionsWithCurrencyEffect.plus( + transactionInvestmentWithCurrencyEffect + ); } else if (order.type === 'SELL') { if (totalUnits.gt(0)) { transactionInvestment = totalInvestment @@ -1245,35 +1254,21 @@ export class PortfolioCalculator { grossPerformanceFromSellWithCurrencyEffect ); - totalInvestmentWithGrossPerformanceFromSell = - totalInvestmentWithGrossPerformanceFromSell - .plus(transactionInvestment) - .plus(grossPerformanceFromSell); - - totalInvestmentWithGrossPerformanceFromSellWithCurrencyEffect = - totalInvestmentWithGrossPerformanceFromSellWithCurrencyEffect - .plus(transactionInvestmentWithCurrencyEffect) - .plus(grossPerformanceFromSellWithCurrencyEffect); - - lastAveragePrice = totalUnits.eq(0) + lastAveragePrice = totalQuantityFromBuyTransactions.eq(0) ? new Big(0) - : totalInvestmentWithGrossPerformanceFromSell.div(totalUnits); + : totalInvestmentFromBuyTransactions.div( + totalQuantityFromBuyTransactions + ); - lastAveragePriceWithCurrencyEffect = totalUnits.eq(0) + lastAveragePriceWithCurrencyEffect = totalQuantityFromBuyTransactions.eq( + 0 + ) ? new Big(0) - : totalInvestmentWithGrossPerformanceFromSellWithCurrencyEffect.div( - totalUnits + : totalInvestmentFromBuyTransactionsWithCurrencyEffect.div( + totalQuantityFromBuyTransactions ); if (PortfolioCalculator.ENABLE_LOGGING) { - console.log( - 'totalInvestmentWithGrossPerformanceFromSell', - totalInvestmentWithGrossPerformanceFromSell.toNumber() - ); - console.log( - 'totalInvestmentWithGrossPerformanceFromSellWithCurrencyEffect', - totalInvestmentWithGrossPerformanceFromSellWithCurrencyEffect.toNumber() - ); console.log( 'grossPerformanceFromSells', grossPerformanceFromSells.toNumber() @@ -1319,11 +1314,10 @@ export class PortfolioCalculator { orderDate, previousOrderDate ); - - // Set to at least 1 day, otherwise the transactions on the same day - // would not be considered in the time weighted calculation if (daysSinceLastOrder <= 0) { - daysSinceLastOrder = 1; + // The time between two activities on the same day is unknown + // -> Set it to the smallest floating point number greater than 0 + daysSinceLastOrder = Number.EPSILON; } // Sum up the total investment days since the start date to calculate From bb86f852033ff6b8f85ab8af48706c48d4279176 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 10 Mar 2024 09:50:43 +0100 Subject: [PATCH 020/120] Feature/add available home server systems to faq (#3126) * Add available home server systems * Update changelog * Add CasaOS to README.md --- CHANGELOG.md | 4 ++++ README.md | 2 +- .../faq/self-hosting/self-hosting-page.html | 19 +++++++++++++++++++ .../resources/resources-page.component.ts | 1 + .../app/pages/resources/resources-page.html | 17 +++++++++++++++++ 5 files changed, 42 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33baa68c4..44b641f7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Extended the content of the _Self-Hosting_ section by available home server systems on the Frequently Asked Questions (FAQ) page + ### Fixed - Fixed an issue in the performance calculation caused by multiple `SELL` activities on the same day diff --git a/README.md b/README.md index c82ad50c3..eb302936e 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,7 @@ docker compose --env-file ./.env -f docker/docker-compose.build.yml up -d ### Home Server Systems (Community) -Ghostfolio is available for various home server systems, including [Runtipi](https://www.runtipi.io/docs/apps-available), [TrueCharts](https://truecharts.org/charts/stable/ghostfolio), [Umbrel](https://apps.umbrel.com/app/ghostfolio), and [Unraid](https://unraid.net/community/apps?q=ghostfolio). +Ghostfolio is available for various home server systems, including [CasaOS](https://github.com/bigbeartechworld/big-bear-casaos), [Runtipi](https://www.runtipi.io/docs/apps-available), [TrueCharts](https://truecharts.org/charts/stable/ghostfolio), [Umbrel](https://apps.umbrel.com/app/ghostfolio), and [Unraid](https://unraid.net/community/apps?q=ghostfolio). ## Development diff --git a/apps/client/src/app/pages/faq/self-hosting/self-hosting-page.html b/apps/client/src/app/pages/faq/self-hosting/self-hosting-page.html index 5dcaa0cf7..1f95abef3 100644 --- a/apps/client/src/app/pages/faq/self-hosting/self-hosting-page.html +++ b/apps/client/src/app/pages/faq/self-hosting/self-hosting-page.html @@ -18,6 +18,25 @@ GitHub. + + + Which home server systems is Ghostfolio available + on? + + + The community has made Ghostfolio available on various home server + systems, including + CasaOS, Runtipi, + TrueCharts, Umbrel, and + Unraid. + + How do I add a new currency? diff --git a/apps/client/src/app/pages/resources/resources-page.component.ts b/apps/client/src/app/pages/resources/resources-page.component.ts index 5c9e690b9..23c5bf5eb 100644 --- a/apps/client/src/app/pages/resources/resources-page.component.ts +++ b/apps/client/src/app/pages/resources/resources-page.component.ts @@ -14,6 +14,7 @@ import { Subject } from 'rxjs'; export class ResourcesPageComponent implements OnInit { public hasPermissionForSubscription: boolean; public info: InfoItem; + public routerLinkFaq = ['/' + $localize`faq`]; public routerLinkResourcesPersonalFinanceTools = [ '/' + $localize`resources`, 'personal-finance-tools' diff --git a/apps/client/src/app/pages/resources/resources-page.html b/apps/client/src/app/pages/resources/resources-page.html index da57cc908..800c674cf 100644 --- a/apps/client/src/app/pages/resources/resources-page.html +++ b/apps/client/src/app/pages/resources/resources-page.html @@ -2,6 +2,23 @@

Resources

+

Ghostfolio

+
+
+
+

Frequently Asked Questions (FAQ)

+
+ Find quick answers to commonly asked questions about Ghostfolio in + our Frequently Asked Questions (FAQ) section. +
+ +
+
+

Guides

From d32dd5e8605ad4a8bdb1833d9348a7b464460921 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 11 Mar 2024 19:16:20 +0100 Subject: [PATCH 021/120] Feature/upgrade countries list to version 3.1.0 (#3131) * Upgrade countries-list to version 3.1.0 * Update changelog --- CHANGELOG.md | 4 ++++ .../data-enhancer/trackinsight/trackinsight.service.ts | 9 ++++----- .../services/symbol-profile/symbol-profile.service.ts | 5 ++--- package.json | 2 +- yarn.lock | 8 ++++---- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44b641f7f..f7ca1dc34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Extended the content of the _Self-Hosting_ section by available home server systems on the Frequently Asked Questions (FAQ) page +### Changed + +- Upgraded `countries-list` from version `2.6.1` to `3.1.0` + ### Fixed - Fixed an issue in the performance calculation caused by multiple `SELL` activities on the same day diff --git a/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts b/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts index 63d785253..ddc3d79f8 100644 --- a/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts +++ b/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts @@ -5,12 +5,12 @@ import { Sector } from '@ghostfolio/common/interfaces/sector.interface'; import { Injectable } from '@nestjs/common'; import { SymbolProfile } from '@prisma/client'; +import { countries } from 'countries-list'; import got from 'got'; @Injectable() export class TrackinsightDataEnhancerService implements DataEnhancerInterface { private static baseUrl = 'https://www.trackinsight.com/data-api'; - private static countries = require('countries-list/dist/countries.json'); private static countriesMapping = { 'Russian Federation': 'Russia' }; @@ -131,20 +131,19 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface { (response.countries as unknown as Country[]).length === 0 ) { response.countries = []; + for (const [name, value] of Object.entries( holdings?.countries ?? {} )) { let countryCode: string; - for (const [key, country] of Object.entries( - TrackinsightDataEnhancerService.countries - )) { + for (const [code, country] of Object.entries(countries)) { if ( country.name === name || country.name === TrackinsightDataEnhancerService.countriesMapping[name] ) { - countryCode = key; + countryCode = code; break; } } diff --git a/apps/api/src/services/symbol-profile/symbol-profile.service.ts b/apps/api/src/services/symbol-profile/symbol-profile.service.ts index a87b00d95..751feda22 100644 --- a/apps/api/src/services/symbol-profile/symbol-profile.service.ts +++ b/apps/api/src/services/symbol-profile/symbol-profile.service.ts @@ -189,9 +189,8 @@ export class SymbolProfileService { return { code, weight, - continent: - continents[countries[code as string]?.continent] ?? UNKNOWN_KEY, - name: countries[code as string]?.name ?? UNKNOWN_KEY + continent: continents[countries[code]?.continent] ?? UNKNOWN_KEY, + name: countries[code]?.name ?? UNKNOWN_KEY }; }); } diff --git a/package.json b/package.json index ddd5de1cf..5b92172e4 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "class-validator": "0.14.0", "color": "4.2.3", "countries-and-timezones": "3.4.1", - "countries-list": "2.6.1", + "countries-list": "3.1.0", "countup.js": "2.3.2", "date-fns": "2.29.3", "envalid": "7.3.1", diff --git a/yarn.lock b/yarn.lock index f71e7ad80..ba7e811ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9739,10 +9739,10 @@ countries-and-timezones@3.4.1: resolved "https://registry.yarnpkg.com/countries-and-timezones/-/countries-and-timezones-3.4.1.tgz#0ec2540f57e42f0f740eb2acaede786043347fe1" integrity sha512-INeHGCony4XUUR8iGL/lmt9s1Oi+n+gFHeJAMfbV5hJfYeDOB8JG1oxz5xFQu5oBZoRCJe/87k1Vzue9DoIauA== -countries-list@2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/countries-list/-/countries-list-2.6.1.tgz#d479757ac873b1e596ccea0a925962d20396c0cb" - integrity sha512-jXM1Nv3U56dPQ1DsUSsEaGmLHburo4fnB7m+1yhWDUVvx5gXCd1ok/y3gXCjXzhqyawG+igcPYcAl4qjkvopaQ== +countries-list@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/countries-list/-/countries-list-3.1.0.tgz#1cbe32f58659c7d6a1e744917689f24c84333ea8" + integrity sha512-HpTBLZba1VPTZSjUnUwR7SykxV7Z/7/+ZM5x5wi5tO99Qvom6bE2SC+AQ18016ujg3jSlYBbMITrHNnPAHSM9Q== countup.js@2.3.2: version "2.3.2" From e792924606dd98849008855c9320ff3f7eacf35d Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 11 Mar 2024 20:15:32 +0100 Subject: [PATCH 022/120] Update OSS friends (#3132) --- apps/client/src/assets/oss-friends.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/client/src/assets/oss-friends.json b/apps/client/src/assets/oss-friends.json index 73674e8dd..cbbbd5987 100644 --- a/apps/client/src/assets/oss-friends.json +++ b/apps/client/src/assets/oss-friends.json @@ -1,5 +1,5 @@ { - "createdAt": "2024-02-29T00:00:00.000Z", + "createdAt": "2024-03-11T00:00:00.000Z", "data": [ { "name": "Aptabase", @@ -8,7 +8,7 @@ }, { "name": "Argos", - "description": "Argos provides the developer tools to debug tests and detect visual regressions..", + "description": "Argos provides the developer tools to debug tests and detect visual regressions.", "href": "https://argos-ci.com" }, { From 59c064e3c8ccfcbe0b6f2b9cbbcd28586f40c904 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 11 Mar 2024 20:15:55 +0100 Subject: [PATCH 023/120] Feature/upgrade yahoo finance2 to version 2.10.0 (#3127) * Upgrade yahoo-finance2 to version 2.10.0 * Update changelog --- CHANGELOG.md | 1 + package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7ca1dc34..242dd9dfe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Upgraded `countries-list` from version `2.6.1` to `3.1.0` +- Upgraded `yahoo-finance2` from version `2.9.1` to `2.10.0` ### Fixed diff --git a/package.json b/package.json index 5b92172e4..bdac1e9b1 100644 --- a/package.json +++ b/package.json @@ -131,7 +131,7 @@ "svgmap": "2.6.0", "twitter-api-v2": "1.14.2", "uuid": "9.0.1", - "yahoo-finance2": "2.9.1", + "yahoo-finance2": "2.10.0", "zone.js": "0.14.3" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index ba7e811ef..8cf31c64b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19242,10 +19242,10 @@ y18n@^5.0.5: resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== -yahoo-finance2@2.9.1: - version "2.9.1" - resolved "https://registry.yarnpkg.com/yahoo-finance2/-/yahoo-finance2-2.9.1.tgz#43e22465403f48c688ff8e762f3894aac8014d70" - integrity sha512-s+i5arE6+zUwHRJnze4EsU5aCTmsMFKFeBc9sMzSceDOjH+BSeEZG9twMYtWlSCjKbWLCmUEUCxtH1fvcq+f6Q== +yahoo-finance2@2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/yahoo-finance2/-/yahoo-finance2-2.10.0.tgz#90a9d7984e3b35a11fff9850c55d1cd322ddbee3" + integrity sha512-yKHjMEnFVkgIvgyxjsNAMLf4xGCKM+HzThorCNuCoE71yoie75lR+WTd0HshdCnb8tpsCclFaP045I+p6Mu6aA== dependencies: "@types/tough-cookie" "^4.0.2" ajv "8.10.0" From 7a364472c83662f770f9c319e1040590146dea2f Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 11 Mar 2024 20:16:56 +0100 Subject: [PATCH 024/120] Bugfix/fix liability issue in allocations (#3133) * Remove liabilities from allocations calculation * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/portfolio/portfolio.controller.ts | 7 +++++-- apps/api/src/app/portfolio/portfolio.service.ts | 4 ++-- .../home-summary/home-summary.component.ts | 2 +- apps/client/src/app/services/data.service.ts | 12 ++++++++++-- 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 242dd9dfe..2fddca32c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed an issue in the performance calculation caused by multiple `SELL` activities on the same day +- Fixed an issue in the calculation on the allocations page caused by liabilities ## 2.62.0 - 2024-03-09 diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 748850204..557e8a5f5 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -76,8 +76,11 @@ export class PortfolioController { @Query('accounts') filterByAccounts?: string, @Query('assetClasses') filterByAssetClasses?: string, @Query('range') dateRange: DateRange = 'max', - @Query('tags') filterByTags?: string + @Query('tags') filterByTags?: string, + @Query('withLiabilities') withLiabilitiesParam = 'false' ): Promise { + const withLiabilities = withLiabilitiesParam === 'true'; + let hasDetails = true; let hasError = false; const hasReadRestrictedAccessPermission = @@ -101,8 +104,8 @@ export class PortfolioController { dateRange, filters, impersonationId, + withLiabilities, userId: this.request.user.id, - withLiabilities: true, withSummary: true }); diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 9880abcc4..74d7b382a 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -146,8 +146,7 @@ export class PortfolioService { filters, withExcludedAccounts, impersonationId: userId, - userId: this.request.user.id, - withLiabilities: true + userId: this.request.user.id }) ]); @@ -393,6 +392,7 @@ export class PortfolioService { }); const holdings: PortfolioDetails['holdings'] = {}; + const totalValueInBaseCurrency = currentPositions.currentValueInBaseCurrency.plus( cashDetails.balanceInBaseCurrency diff --git a/apps/client/src/app/components/home-summary/home-summary.component.ts b/apps/client/src/app/components/home-summary/home-summary.component.ts index b67b67ce5..bb68a4627 100644 --- a/apps/client/src/app/components/home-summary/home-summary.component.ts +++ b/apps/client/src/app/components/home-summary/home-summary.component.ts @@ -102,7 +102,7 @@ export class HomeSummaryComponent implements OnDestroy, OnInit { this.isLoading = true; this.dataService - .fetchPortfolioDetails() + .fetchPortfolioDetails({ withLiabilities: true }) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(({ summary }) => { this.summary = summary; diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 7741fd601..0c8781eb1 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -390,13 +390,21 @@ export class DataService { } public fetchPortfolioDetails({ - filters + filters, + withLiabilities = false }: { filters?: Filter[]; + withLiabilities?: boolean; } = {}): Observable { + let params = this.buildFiltersAsQueryParams({ filters }); + + if (withLiabilities) { + params = params.append('withLiabilities', withLiabilities); + } + return this.http .get('/api/v1/portfolio/details', { - params: this.buildFiltersAsQueryParams({ filters }) + params }) .pipe( map((response) => { From d23cb5f1902f3b5aca249e5b08e671253d117f38 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 11 Mar 2024 20:17:47 +0100 Subject: [PATCH 025/120] Feature/upgrade simplewebauthn dependencies to version 9.0 (#3130) * Upgrade @simplewebauthn/browser and @simplewebauthn/server to version 9.0 * Update changelog --- CHANGELOG.md | 1 + package.json | 6 +-- yarn.lock | 116 +++++++++++++-------------------------------------- 3 files changed, 34 insertions(+), 89 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fddca32c..18c2f4fcf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Upgraded `@simplewebauthn/browser` and `@simplewebauthn/server` from version `8.3` to `9.0` - Upgraded `countries-list` from version `2.6.1` to `3.1.0` - Upgraded `yahoo-finance2` from version `2.9.1` to `2.10.0` diff --git a/package.json b/package.json index bdac1e9b1..c8d2cbdba 100644 --- a/package.json +++ b/package.json @@ -83,8 +83,8 @@ "@nestjs/schedule": "3.0.2", "@nestjs/serve-static": "4.0.0", "@prisma/client": "5.10.2", - "@simplewebauthn/browser": "8.3.1", - "@simplewebauthn/server": "8.3.2", + "@simplewebauthn/browser": "9.0.1", + "@simplewebauthn/server": "9.0.3", "@stripe/stripe-js": "1.47.0", "alphavantage": "2.2.0", "big.js": "6.2.1", @@ -159,7 +159,7 @@ "@nx/web": "18.0.4", "@nx/workspace": "18.0.4", "@schematics/angular": "17.1.3", - "@simplewebauthn/typescript-types": "8.0.0", + "@simplewebauthn/types": "9.0.1", "@storybook/addon-essentials": "7.6.5", "@storybook/angular": "7.6.5", "@storybook/core-server": "7.6.5", diff --git a/yarn.lock b/yarn.lock index 8cf31c64b..7092b4b93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2744,36 +2744,6 @@ resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz#923ca57e173c6b232bbbb07347b1be982f03e783" integrity sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A== -"@cbor-extract/cbor-extract-darwin-arm64@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.1.1.tgz#5721f6dd3feae0b96d23122853ce977e0671b7a6" - integrity sha512-blVBy5MXz6m36Vx0DfLd7PChOQKEs8lK2bD1WJn/vVgG4FXZiZmZb2GECHFvVPA5T7OnODd9xZiL3nMCv6QUhA== - -"@cbor-extract/cbor-extract-darwin-x64@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.1.1.tgz#c25e7d0133950d87d101d7b3afafea8d50d83f5f" - integrity sha512-h6KFOzqk8jXTvkOftyRIWGrd7sKQzQv2jVdTL9nKSf3D2drCvQB/LHUxAOpPXo3pv2clDtKs3xnHalpEh3rDsw== - -"@cbor-extract/cbor-extract-linux-arm64@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.1.1.tgz#48f78e7d8f0fcc84ed074b6bfa6d15dd83187c63" - integrity sha512-SxAaRcYf8S0QHaMc7gvRSiTSr7nUYMqbUdErBEu+HYA4Q6UNydx1VwFE68hGcp1qvxcy9yT5U7gA+a5XikfwSQ== - -"@cbor-extract/cbor-extract-linux-arm@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.1.1.tgz#7507d346389cb682e44fab8fae9534edd52e2e41" - integrity sha512-ds0uikdcIGUjPyraV4oJqyVE5gl/qYBpa/Wnh6l6xLE2lj/hwnjT2XcZCChdXwW/YFZ1LUHs6waoYN8PmK0nKQ== - -"@cbor-extract/cbor-extract-linux-x64@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.1.1.tgz#b7c1d2be61c58ec18d58afbad52411ded63cd4cd" - integrity sha512-GVK+8fNIE9lJQHAlhOROYiI0Yd4bAZ4u++C2ZjlkS3YmO6hi+FUxe6Dqm+OKWTcMpL/l71N6CQAmaRcb4zyJuA== - -"@cbor-extract/cbor-extract-win32-x64@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.1.1.tgz#21b11a1a3f18c3e7d62fd5f87438b7ed2c64c1f7" - integrity sha512-2Niq1C41dCRIDeD8LddiH+mxGlO7HJ612Ll3D/E73ZWBmycued+8ghTr/Ho3CMOWPUEr08XtyBMVXAjqF+TcKw== - "@codewithdan/observable-store@2.2.15": version "2.2.15" resolved "https://registry.yarnpkg.com/@codewithdan/observable-store/-/observable-store-2.2.15.tgz#6d27e0988e182853def59a714b712f4389e558d2" @@ -3839,6 +3809,11 @@ resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== +"@levischuck/tiny-cbor@^0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@levischuck/tiny-cbor/-/tiny-cbor-0.2.2.tgz#84239ce80e1107b810f1fe9f66546d4f79f31aea" + integrity sha512-f5CnPw997Y2GQ8FAvtuVVC19FX8mwNNC+1XJcIi16n/LTJifKO6QBgGLgN3YEmqtGMk17SKSuoWES3imJVxAVw== + "@ljharb/through@^2.3.11": version "2.3.12" resolved "https://registry.yarnpkg.com/@ljharb/through/-/through-2.3.12.tgz#c418c43060eee193adce48b15c2206096a28e9ea" @@ -5214,7 +5189,7 @@ tslib "^2.3.0" yargs-parser "21.1.1" -"@peculiar/asn1-android@^2.3.6": +"@peculiar/asn1-android@^2.3.10": version "2.3.10" resolved "https://registry.yarnpkg.com/@peculiar/asn1-android/-/asn1-android-2.3.10.tgz#a2dde6227fa1ddea33d8ae7835768674e7a0baa6" integrity sha512-z9Rx9cFJv7UUablZISe7uksNbFJCq13hO0yEAOoIpAymALTLlvUOSLnGiQS7okPaM5dP42oTLhezH6XDXRXjGw== @@ -5223,7 +5198,7 @@ asn1js "^3.0.5" tslib "^2.6.2" -"@peculiar/asn1-ecc@^2.3.6": +"@peculiar/asn1-ecc@^2.3.8": version "2.3.8" resolved "https://registry.yarnpkg.com/@peculiar/asn1-ecc/-/asn1-ecc-2.3.8.tgz#6b1a18f64f221ae862c1038bb125fbf4342918a0" integrity sha512-Ah/Q15y3A/CtxbPibiLM/LKcMbnLTdUdLHUgdpB5f60sSvGkXzxJCu5ezGTFHogZXWNX3KSmYqilCrfdmBc6pQ== @@ -5233,7 +5208,7 @@ asn1js "^3.0.5" tslib "^2.6.2" -"@peculiar/asn1-rsa@^2.3.6": +"@peculiar/asn1-rsa@^2.3.8": version "2.3.8" resolved "https://registry.yarnpkg.com/@peculiar/asn1-rsa/-/asn1-rsa-2.3.8.tgz#6a6a0eaafc0aded9a44b679b522cc2417b09a3ba" integrity sha512-ES/RVEHu8VMYXgrg3gjb1m/XG0KJWnV4qyZZ7mAg7rrF3VTmRbLxO8mk+uy0Hme7geSMebp+Wvi2U6RLLEs12Q== @@ -5243,7 +5218,7 @@ asn1js "^3.0.5" tslib "^2.6.2" -"@peculiar/asn1-schema@^2.3.6", "@peculiar/asn1-schema@^2.3.8": +"@peculiar/asn1-schema@^2.3.8": version "2.3.8" resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.3.8.tgz#04b38832a814e25731232dd5be883460a156da3b" integrity sha512-ULB1XqHKx1WBU/tTFIA+uARuRoBVZ4pNdOA878RDrRbBfBGcSzi5HBkdScC6ZbHn8z7L8gmKCgPC1LHRrP46tA== @@ -5252,7 +5227,7 @@ pvtsutils "^1.3.5" tslib "^2.6.2" -"@peculiar/asn1-x509@^2.3.6", "@peculiar/asn1-x509@^2.3.8": +"@peculiar/asn1-x509@^2.3.8": version "2.3.8" resolved "https://registry.yarnpkg.com/@peculiar/asn1-x509/-/asn1-x509-2.3.8.tgz#865896e2b849cc3c55497ca685040ef889d357a3" integrity sha512-voKxGfDU1c6r9mKiN5ZUsZWh3Dy1BABvTM3cimf0tztNwyMJPhiXY94eRTgsMQe6ViLfT6EoXxkWVzcm3mFAFw== @@ -5725,37 +5700,32 @@ "@sigstore/protobuf-specs" "^0.2.1" tuf-js "^2.1.0" -"@simplewebauthn/browser@8.3.1": - version "8.3.1" - resolved "https://registry.yarnpkg.com/@simplewebauthn/browser/-/browser-8.3.1.tgz#f5c1aed6313d61944a9e13f16ae4495750bddf93" - integrity sha512-bMW7oOkxX4ydRAkkPtJ1do2k9yOoIGc/hZYebcuEOVdJoC6wwVpu97mYY7Mz8B9hLlcaR5WFgBsLl5tSJVzm8A== +"@simplewebauthn/browser@9.0.1": + version "9.0.1" + resolved "https://registry.yarnpkg.com/@simplewebauthn/browser/-/browser-9.0.1.tgz#46a12c2bcefcb199f7fcb6a7e883531cd6efde17" + integrity sha512-wD2WpbkaEP4170s13/HUxPcAV5y4ZXaKo1TfNklS5zDefPinIgXOpgz1kpEvobAsaLPa2KeH7AKKX/od1mrBJw== dependencies: - "@simplewebauthn/typescript-types" "^8.0.0" + "@simplewebauthn/types" "^9.0.1" -"@simplewebauthn/server@8.3.2": - version "8.3.2" - resolved "https://registry.yarnpkg.com/@simplewebauthn/server/-/server-8.3.2.tgz#dfdbe7af4c1258e786c4a0b1c83c54743ba7568c" - integrity sha512-ceo8t5gdO5W/JOePQWPDH+rAd8tO6QNalLU56rc9ItdzaTjk+qcYwQg/BKXDDg6117P3HKrRBkZwBrMJl4dOdA== +"@simplewebauthn/server@9.0.3": + version "9.0.3" + resolved "https://registry.yarnpkg.com/@simplewebauthn/server/-/server-9.0.3.tgz#5f73c19ff2420be94cc71a49085879c111d7872d" + integrity sha512-FMZieoBosrVLFxCnxPFD9Enhd1U7D8nidVDT4MsHc6l4fdVcjoeHjDueeXCloO1k5O/fZg1fsSXXPKbY2XTzDA== dependencies: "@hexagon/base64" "^1.1.27" - "@peculiar/asn1-android" "^2.3.6" - "@peculiar/asn1-ecc" "^2.3.6" - "@peculiar/asn1-rsa" "^2.3.6" - "@peculiar/asn1-schema" "^2.3.6" - "@peculiar/asn1-x509" "^2.3.6" - "@simplewebauthn/typescript-types" "^8.0.0" - cbor-x "^1.5.2" + "@levischuck/tiny-cbor" "^0.2.2" + "@peculiar/asn1-android" "^2.3.10" + "@peculiar/asn1-ecc" "^2.3.8" + "@peculiar/asn1-rsa" "^2.3.8" + "@peculiar/asn1-schema" "^2.3.8" + "@peculiar/asn1-x509" "^2.3.8" + "@simplewebauthn/types" "^9.0.1" cross-fetch "^4.0.0" -"@simplewebauthn/typescript-types@8.0.0": - version "8.0.0" - resolved "https://registry.yarnpkg.com/@simplewebauthn/typescript-types/-/typescript-types-8.0.0.tgz#1698a7228aba880c5c1deba1f13a4f9fd8851cb3" - integrity sha512-d7Izb2H+LZJteXMkS8DmpAarD6mZdpIOu/av/yH4/u/3Pd6DKFLyBM3j8BMmUvUqpzvJvHARNrRfQYto58mtTQ== - -"@simplewebauthn/typescript-types@^8.0.0": - version "8.3.3" - resolved "https://registry.yarnpkg.com/@simplewebauthn/typescript-types/-/typescript-types-8.3.3.tgz#4292656f4fae6c9e9c25e5b94a60fa038a7d11cc" - integrity sha512-YLfmT+HzzUuRtBPp93XgKzQPrFJ1F6f1vl7ltfmm6R9d2SZfr8E15B5CC7hkCwSTioJDCaEw4p3NZt3+nubaxA== +"@simplewebauthn/types@9.0.1", "@simplewebauthn/types@^9.0.1": + version "9.0.1" + resolved "https://registry.yarnpkg.com/@simplewebauthn/types/-/types-9.0.1.tgz#3a68d50e63d8821cf2067de3324c68d5e8120d0c" + integrity sha512-tGSRP1QvsAvsJmnOlRQyw/mvK9gnPtjEc5fg2+m8n+QUa+D7rvrKkOYyfpy42GTs90X3RDOnqJgfHt+qO67/+w== "@sinclair/typebox@^0.27.8": version "0.27.8" @@ -9078,27 +9048,6 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== -cbor-extract@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/cbor-extract/-/cbor-extract-2.1.1.tgz#f154b31529fdb6b7c70fb3ca448f44eda96a1b42" - integrity sha512-1UX977+L+zOJHsp0mWFG13GLwO6ucKgSmSW6JTl8B9GUvACvHeIVpFqhU92299Z6PfD09aTXDell5p+lp1rUFA== - dependencies: - node-gyp-build-optional-packages "5.0.3" - optionalDependencies: - "@cbor-extract/cbor-extract-darwin-arm64" "2.1.1" - "@cbor-extract/cbor-extract-darwin-x64" "2.1.1" - "@cbor-extract/cbor-extract-linux-arm" "2.1.1" - "@cbor-extract/cbor-extract-linux-arm64" "2.1.1" - "@cbor-extract/cbor-extract-linux-x64" "2.1.1" - "@cbor-extract/cbor-extract-win32-x64" "2.1.1" - -cbor-x@^1.5.2: - version "1.5.4" - resolved "https://registry.yarnpkg.com/cbor-x/-/cbor-x-1.5.4.tgz#8f0754fa8589cbd7339b613b2b5717d133508e98" - integrity sha512-PVKILDn+Rf6MRhhcyzGXi5eizn1i0i3F8Fe6UMMxXBnWkalq9+C5+VTmlIjAYM4iF2IYF2N+zToqAfYOp+3rfw== - optionalDependencies: - cbor-extract "^2.1.1" - chalk@^1.0.0, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -15213,11 +15162,6 @@ node-forge@^1, node-forge@^1.3.1: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== -node-gyp-build-optional-packages@5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.3.tgz#92a89d400352c44ad3975010368072b41ad66c17" - integrity sha512-k75jcVzk5wnnc/FMxsf4udAoTEUv2jY3ycfdSd3yWu6Cnd1oee6/CfZJApyscA4FJOmdoixWwiwOyf16RzD5JA== - node-gyp-build-optional-packages@5.0.7: version "5.0.7" resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.7.tgz#5d2632bbde0ab2f6e22f1bbac2199b07244ae0b3" From 9a3db919827cd2d1a0e05810ec4d33c2a4c49067 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 11 Mar 2024 20:19:23 +0100 Subject: [PATCH 026/120] Release 2.63.0 (#3134) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18c2f4fcf..425aa20cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## 2.63.0 - 2024-03-11 ### Added diff --git a/package.json b/package.json index c8d2cbdba..b5f7d9c3d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "2.62.0", + "version": "2.63.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio", From 473136e9aa4c9144cafacc56d6a7cd0f0b939cf9 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 11 Mar 2024 21:35:43 +0100 Subject: [PATCH 027/120] Release 2.63.1 (#3135) * Release 2.63.1 --- CHANGELOG.md | 4 ++- .../src/assets/cryptocurrencies/custom.json | 1 + .../eod-historical-data.service.ts | 31 +++++++------------ package.json | 2 +- 4 files changed, 17 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 425aa20cb..0877bcf74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## 2.63.0 - 2024-03-11 +## 2.63.1 - 2024-03-11 ### Added - Extended the content of the _Self-Hosting_ section by available home server systems on the Frequently Asked Questions (FAQ) page +- Added support for the cryptocurrency _Real Smurf Cat_ (`SMURFCAT-USD`) ### Changed @@ -21,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed an issue in the performance calculation caused by multiple `SELL` activities on the same day - Fixed an issue in the calculation on the allocations page caused by liabilities +- Fixed an issue with the currency in the request to get quotes from _EOD Historical Data_ ## 2.62.0 - 2024-03-09 diff --git a/apps/api/src/assets/cryptocurrencies/custom.json b/apps/api/src/assets/cryptocurrencies/custom.json index ffda9c526..ef08ca449 100644 --- a/apps/api/src/assets/cryptocurrencies/custom.json +++ b/apps/api/src/assets/cryptocurrencies/custom.json @@ -4,6 +4,7 @@ "LUNA1": "Terra", "LUNA2": "Terra", "SGB1": "Songbird", + "SMURFCAT": "Real Smurf Cat", "UNI1": "Uniswap", "UNI7083": "Uniswap", "UST": "TerraUSD" diff --git a/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts b/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts index af67d62e9..d8e7a2e0b 100644 --- a/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts +++ b/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts @@ -11,7 +11,6 @@ import { IDataProviderHistoricalResponse, IDataProviderResponse } from '@ghostfolio/api/services/interfaces/interfaces'; -import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { DEFAULT_CURRENCY, REPLACE_NAME_PARTS @@ -36,8 +35,7 @@ export class EodHistoricalDataService implements DataProviderInterface { private readonly URL = 'https://eodhistoricaldata.com/api'; public constructor( - private readonly configurationService: ConfigurationService, - private readonly symbolProfileService: SymbolProfileService + private readonly configurationService: ConfigurationService ) { this.apiKey = this.configurationService.get('API_KEY_EOD_HISTORICAL_DATA'); } @@ -232,29 +230,24 @@ export class EodHistoricalDataService implements DataProviderInterface { ? [realTimeResponse] : realTimeResponse; - const symbolProfiles = await this.symbolProfileService.getSymbolProfiles( - symbols.map((symbol) => { - return { - symbol, - dataSource: this.getName() - }; - }) - ); - response = quotes.reduce( - ( + async ( result: { [symbol: string]: IDataProviderResponse }, { close, code, timestamp } ) => { - const currency = symbolProfiles.find(({ symbol }) => { - return symbol === code; - })?.currency; + let currency: string; + + if (symbols.length === 1) { + const { items } = await this.search({ query: symbols[0] }); + + if (items.length === 1) { + currency = items[0].currency; + } + } if (isNumber(close)) { result[this.convertFromEodSymbol(code)] = { - currency: - currency ?? - this.convertFromEodSymbol(code)?.replace(DEFAULT_CURRENCY, ''), + currency, dataSource: this.getName(), marketPrice: close, marketState: isToday(new Date(timestamp * 1000)) diff --git a/package.json b/package.json index b5f7d9c3d..579c4151e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "2.63.0", + "version": "2.63.1", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio", From 671e4e316b3db329b2ba53ecd2d25acf35acdcd7 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 12 Mar 2024 20:50:32 +0100 Subject: [PATCH 028/120] Release 2.63.2 (#3138) --- CHANGELOG.md | 2 +- .../eod-historical-data.service.ts | 76 +++++++++++-------- package.json | 2 +- 3 files changed, 47 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0877bcf74..6f1c07500 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## 2.63.1 - 2024-03-11 +## 2.63.2 - 2024-03-12 ### Added diff --git a/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts b/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts index d8e7a2e0b..99104a78d 100644 --- a/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts +++ b/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts @@ -11,6 +11,7 @@ import { IDataProviderHistoricalResponse, IDataProviderResponse } from '@ghostfolio/api/services/interfaces/interfaces'; +import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { DEFAULT_CURRENCY, REPLACE_NAME_PARTS @@ -35,7 +36,8 @@ export class EodHistoricalDataService implements DataProviderInterface { private readonly URL = 'https://eodhistoricaldata.com/api'; public constructor( - private readonly configurationService: ConfigurationService + private readonly configurationService: ConfigurationService, + private readonly symbolProfileService: SymbolProfileService ) { this.apiKey = this.configurationService.get('API_KEY_EOD_HISTORICAL_DATA'); } @@ -230,41 +232,53 @@ export class EodHistoricalDataService implements DataProviderInterface { ? [realTimeResponse] : realTimeResponse; - response = quotes.reduce( - async ( - result: { [symbol: string]: IDataProviderResponse }, - { close, code, timestamp } - ) => { - let currency: string; + const symbolProfiles = await this.symbolProfileService.getSymbolProfiles( + symbols.map((symbol) => { + return { + symbol, + dataSource: this.getName() + }; + }) + ); - if (symbols.length === 1) { - const { items } = await this.search({ query: symbols[0] }); + for (const { close, code, timestamp } of quotes) { + let currency: string; - if (items.length === 1) { - currency = items[0].currency; - } - } + if (code.endsWith('.FOREX')) { + currency = this.convertFromEodSymbol(code)?.replace( + DEFAULT_CURRENCY, + '' + ); + } - if (isNumber(close)) { - result[this.convertFromEodSymbol(code)] = { - currency, - dataSource: this.getName(), - marketPrice: close, - marketState: isToday(new Date(timestamp * 1000)) - ? 'open' - : 'closed' - }; - } else { - Logger.error( - `Could not get quote for ${this.convertFromEodSymbol(code)} (${this.getName()})`, - 'EodHistoricalDataService' - ); + if (!currency) { + currency = symbolProfiles.find(({ symbol }) => { + return symbol === code; + })?.currency; + } + + if (!currency) { + const { items } = await this.search({ query: code }); + + if (items.length === 1) { + currency = items[0].currency; } + } - return result; - }, - {} - ); + if (isNumber(close)) { + response[this.convertFromEodSymbol(code)] = { + currency, + dataSource: this.getName(), + marketPrice: close, + marketState: isToday(new Date(timestamp * 1000)) ? 'open' : 'closed' + }; + } else { + Logger.error( + `Could not get quote for ${this.convertFromEodSymbol(code)} (${this.getName()})`, + 'EodHistoricalDataService' + ); + } + } return response; } catch (error) { diff --git a/package.json b/package.json index 579c4151e..c32114cf4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "2.63.1", + "version": "2.63.2", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio", From 40d93066ff3f3f9c883670b60bd98408f59d2de9 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 13 Mar 2024 20:21:54 +0100 Subject: [PATCH 029/120] Introduce .env.dev (#3120) --- .env.dev | 25 +++++++++++++++++++++++++ .env.example | 3 ++- README.md | 2 +- 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 .env.dev diff --git a/.env.dev b/.env.dev new file mode 100644 index 000000000..c4c8a0d35 --- /dev/null +++ b/.env.dev @@ -0,0 +1,25 @@ +COMPOSE_PROJECT_NAME=ghostfolio-development + +# CACHE +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD= + +# POSTGRES +POSTGRES_DB=ghostfolio-db +POSTGRES_USER=user +POSTGRES_PASSWORD= + +# VARIOUS +ACCESS_TOKEN_SALT= +DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?connect_timeout=300&sslmode=prefer +JWT_SECRET_KEY= + +# DEVELOPMENT + +# Nx 18 enables using plugins to infer targets by default +# This is disabled for existing workspaces to maintain compatibility +# For more info, see: https://nx.dev/concepts/inferred-tasks +NX_ADD_PLUGINS=false + +NX_NATIVE_COMMAND_RUNNER=false diff --git a/.env.example b/.env.example index 8df547e37..766894992 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -COMPOSE_PROJECT_NAME=ghostfolio-development +COMPOSE_PROJECT_NAME=ghostfolio # CACHE REDIS_HOST=localhost @@ -10,6 +10,7 @@ POSTGRES_DB=ghostfolio-db POSTGRES_USER=user POSTGRES_PASSWORD= +# VARIOUS ACCESS_TOKEN_SALT= DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?connect_timeout=300&sslmode=prefer JWT_SECRET_KEY= diff --git a/README.md b/README.md index eb302936e..9602d88c9 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,7 @@ Ghostfolio is available for various home server systems, including [CasaOS](http - [Node.js](https://nodejs.org/en/download) (version 18+) - [Yarn](https://yarnpkg.com/en/docs/install) - Create a local copy of this Git repository (clone) -- Copy the file `.env.example` to `.env` and populate it with your data (`cp .env.example .env`) +- Copy the file `.env.dev` to `.env` and populate it with your data (`cp .env.dev .env`) ### Setup From a0ddd1f9b99187acde7302a334cc1c22234d8921 Mon Sep 17 00:00:00 2001 From: helgehatt Date: Wed, 13 Mar 2024 20:44:33 +0100 Subject: [PATCH 030/120] Fix date conversion in import of historical market data (#3117) * Fix date conversion in import of historical market data * Update changelog --- CHANGELOG.md | 6 ++++++ .../admin-market-data-detail.component.ts | 3 +-- .../market-data-detail-dialog/interfaces/interfaces.ts | 2 +- .../market-data-detail-dialog.component.ts | 4 ++-- .../market-data-detail-dialog.html | 2 +- .../asset-profile-dialog.component.ts | 7 +++---- apps/client/src/app/services/admin.service.ts | 9 +++------ 7 files changed, 17 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f1c07500..f60fcdf26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Fixed + +- Fixed the date conversion of the import of historical market data in the admin control panel + ## 2.63.2 - 2024-03-12 ### Added diff --git a/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.ts b/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.ts index 5a2ec5265..26da886e7 100644 --- a/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.ts +++ b/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.ts @@ -155,15 +155,14 @@ export class AdminMarketDataDetailComponent implements OnChanges, OnInit { day: string; yearMonth: string; }) { - const date = parseISO(`${yearMonth}-${day}`); const marketPrice = this.marketDataByMonth[yearMonth]?.[day]?.marketPrice; const dialogRef = this.dialog.open(MarketDataDetailDialog, { data: { - date, marketPrice, currency: this.currency, dataSource: this.dataSource, + dateString: `${yearMonth}-${day}`, symbol: this.symbol, user: this.user }, diff --git a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/interfaces/interfaces.ts index 8f5447f9c..81188cd1f 100644 --- a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/interfaces/interfaces.ts +++ b/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/interfaces/interfaces.ts @@ -5,7 +5,7 @@ import { DataSource } from '@prisma/client'; export interface MarketDataDetailDialogParams { currency: string; dataSource: DataSource; - date: Date; + dateString: string; marketPrice: number; symbol: string; user: User; diff --git a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.component.ts b/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.component.ts index df8ac6067..6a44d0dfb 100644 --- a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.component.ts +++ b/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.component.ts @@ -45,7 +45,7 @@ export class MarketDataDetailDialog implements OnDestroy { this.adminService .fetchSymbolForDate({ dataSource: this.data.dataSource, - date: this.data.date, + dateString: this.data.dateString, symbol: this.data.symbol }) .pipe(takeUntil(this.unsubscribeSubject)) @@ -63,7 +63,7 @@ export class MarketDataDetailDialog implements OnDestroy { marketData: { marketData: [ { - date: this.data.date.toISOString(), + date: this.data.dateString, marketPrice: this.data.marketPrice } ] diff --git a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html b/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html index 5e16fc702..8e7e30649 100644 --- a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html +++ b/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html @@ -9,7 +9,7 @@ matInput name="date" [matDatepicker]="date" - [(ngModel)]="data.date" + [(ngModel)]="data.dateString" /> { - return { marketPrice, date: parseDate(date).toISOString() }; - }) + marketData }, symbol: this.data.symbol }) diff --git a/apps/client/src/app/services/admin.service.ts b/apps/client/src/app/services/admin.service.ts index 7d204c607..c850527a8 100644 --- a/apps/client/src/app/services/admin.service.ts +++ b/apps/client/src/app/services/admin.service.ts @@ -188,17 +188,14 @@ export class AdminService { public fetchSymbolForDate({ dataSource, - date, + dateString, symbol }: { dataSource: DataSource; - date: Date; + dateString: string; symbol: string; }) { - const url = `/api/v1/symbol/${dataSource}/${symbol}/${format( - date, - DATE_FORMAT - )}`; + const url = `/api/v1/symbol/${dataSource}/${symbol}/${dateString}`; return this.http.get(url); } From 8420cb830c678734e1096192cd806d203c0e0a28 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 14 Mar 2024 14:09:20 +0100 Subject: [PATCH 031/120] Feature/add product roadmap to faq (#3143) * Add product roadmap * Update changelog --- CHANGELOG.md | 4 ++ .../pages/faq/overview/faq-overview-page.html | 37 ++++++++++--------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f60fcdf26..77c556152 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Extended the content of the _General_ section by the product roadmap on the Frequently Asked Questions (FAQ) page + ### Fixed - Fixed the date conversion of the import of historical market data in the admin control panel diff --git a/apps/client/src/app/pages/faq/overview/faq-overview-page.html b/apps/client/src/app/pages/faq/overview/faq-overview-page.html index 97c2067d0..925871a60 100644 --- a/apps/client/src/app/pages/faq/overview/faq-overview-page.html +++ b/apps/client/src/app/pages/faq/overview/faq-overview-page.html @@ -33,10 +33,8 @@ - What else is included in Ghostfolio? + What else is included in Ghostfolio? + Please find a feature overview to manage your wealth here. @@ -44,10 +42,8 @@ - Can I use Ghostfolio anonymously? + Can I use Ghostfolio anonymously? + Yes, the authentication system via security token enables you to sign in securely and anonymously to Ghostfolio. There is no need for an @@ -56,10 +52,8 @@ - How can Ghostfolio be free? + How can Ghostfolio be free? + This project is driven by the efforts of contributors from around the world. The @@ -75,8 +69,8 @@ Do you monetize or sell my financial data? + > + No, we value your privacy. We do not sell or share your financial data with any third parties. - What is your business model? + What is your business model? + By offering Ghostfolio Premium, a @@ -96,6 +88,15 @@ users. + + + What is your product roadmap? + + At this time, we do not have a public roadmap + available. + Date: Fri, 15 Mar 2024 08:37:41 +0100 Subject: [PATCH 032/120] Feature/improve usability of platform and tag management (#3144) * Improve usability * Update changelog --- CHANGELOG.md | 5 +++++ .../components/admin-platform/admin-platform.component.html | 6 +++++- .../src/app/components/admin-tag/admin-tag.component.html | 6 +++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77c556152..bcf2ad4f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Extended the content of the _General_ section by the product roadmap on the Frequently Asked Questions (FAQ) page +### Changed + +- Improved the usability of the platform management in the admin control panel +- Improved the usability of the tag management in the admin control panel + ### Fixed - Fixed the date conversion of the import of historical market data in the admin control panel diff --git a/apps/client/src/app/components/admin-platform/admin-platform.component.html b/apps/client/src/app/components/admin-platform/admin-platform.component.html index bd7e82560..ecf3304cf 100644 --- a/apps/client/src/app/components/admin-platform/admin-platform.component.html +++ b/apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -91,7 +91,11 @@ Edit - - +
diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.module.ts b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.module.ts index 372608279..846b5e599 100644 --- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.module.ts +++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.module.ts @@ -1,4 +1,5 @@ import { GfAdminMarketDataDetailModule } from '@ghostfolio/client/components/admin-market-data-detail/admin-market-data-detail.module'; +import { AdminMarketDataService } from '@ghostfolio/client/components/admin-market-data/admin-market-data.service'; import { GfCurrencySelectorModule } from '@ghostfolio/ui/currency-selector/currency-selector.module'; import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module'; import { GfValueModule } from '@ghostfolio/ui/value'; @@ -36,6 +37,7 @@ import { AssetProfileDialog } from './asset-profile-dialog.component'; ReactiveFormsModule, TextFieldModule ], + providers: [AdminMarketDataService], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) export class GfAssetProfileDialogModule {} From 61ecd15f1d5812a3a929780ca62e95cab66bcf7d Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 19 Mar 2024 19:55:08 +0100 Subject: [PATCH 047/120] Feature/change edit button to a in admin market data page (#3164) * Change edit button to a * Update changelog --- CHANGELOG.md | 1 + .../admin-market-data/admin-market-data.html | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 440784758..2dc8cc0ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Moved the support to grant private access with permissions from experimental to general availability +- Improved the usability to edit market data in the admin control panel ## 2.64.0 - 2024-03-16 diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.html b/apps/client/src/app/components/admin-market-data/admin-market-data.html index a91b05bcc..83e7f7533 100644 --- a/apps/client/src/app/components/admin-market-data/admin-market-data.html +++ b/apps/client/src/app/components/admin-market-data/admin-market-data.html @@ -161,20 +161,20 @@ - +
diff --git a/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts b/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts index f176c0847..c6d7f8e89 100644 --- a/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts +++ b/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts @@ -48,7 +48,8 @@ export class UserAccountSettingsComponent implements OnDestroy, OnInit { 'nl', 'pl', 'pt', - 'tr' + 'tr', + 'zh' ]; public user: User; diff --git a/apps/client/src/app/components/user-account-settings/user-account-settings.html b/apps/client/src/app/components/user-account-settings/user-account-settings.html index df423ba16..1ad24e22e 100644 --- a/apps/client/src/app/components/user-account-settings/user-account-settings.html +++ b/apps/client/src/app/components/user-account-settings/user-account-settings.html @@ -76,6 +76,12 @@ Deutsch English + @if (user?.settings?.isExperimentalFeatures) { + Chinese (Community) + } Español (Community)Nederlands (Community) - Polski (Community) + @if (user?.settings?.isExperimentalFeatures) { + Polski (Community) + } Português (Community)

Multi-Language

- Use Ghostfolio in multiple languages: English, Dutch, French, - German, Italian, + Use Ghostfolio in multiple languages: English, + Dutch, French, German, Italian, Portuguese, Spanish and Turkish are currently supported.

diff --git a/apps/client/src/locales/messages.zh.xlf b/apps/client/src/locales/messages.zh.xlf new file mode 100644 index 000000000..f1e528dd0 --- /dev/null +++ b/apps/client/src/locales/messages.zh.xlf @@ -0,0 +1,6 @@ + + + + + + diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index 3bbe0ff8c..293f77488 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -137,7 +137,8 @@ export const SUPPORTED_LANGUAGE_CODES = [ 'nl', 'pl', 'pt', - 'tr' + 'tr', + 'zh' ]; export const UNKNOWN_KEY = 'UNKNOWN'; diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index b49589aab..e03ea1a1f 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -11,7 +11,7 @@ import { parseISO, subDays } from 'date-fns'; -import { de, es, fr, it, nl, pl, pt, tr } from 'date-fns/locale'; +import { de, es, fr, it, nl, pl, pt, tr, zhCN } from 'date-fns/locale'; import { ghostfolioScraperApiSymbolPrefix, locale } from './config'; import { Benchmark, UniqueAsset } from './interfaces'; @@ -178,6 +178,8 @@ export function getDateFnsLocale(aLanguageCode: string) { return pt; } else if (aLanguageCode === 'tr') { return tr; + } else if (aLanguageCode === 'zh') { + return zhCN; } return undefined; From af47889d6544001864069f9faabd8ae962eb0d25 Mon Sep 17 00:00:00 2001 From: qiaoy Date: Sun, 31 Mar 2024 17:24:32 +0800 Subject: [PATCH 082/120] Add Chinese translations (#3215) * Add Chinese translations --- apps/client/src/locales/messages.zh.xlf | 14997 +++++++++++++++++++++- 1 file changed, 14996 insertions(+), 1 deletion(-) diff --git a/apps/client/src/locales/messages.zh.xlf b/apps/client/src/locales/messages.zh.xlf index f1e528dd0..2887d5724 100644 --- a/apps/client/src/locales/messages.zh.xlf +++ b/apps/client/src/locales/messages.zh.xlf @@ -1,6 +1,15001 @@ + - + + + about + 关于 + + apps/client/src/app/app-routing.module.ts + 9 + + + apps/client/src/app/app.component.ts + 47 + + + apps/client/src/app/app.component.ts + 48 + + + apps/client/src/app/app.component.ts + 49 + + + apps/client/src/app/app.component.ts + 51 + + + apps/client/src/app/components/header/header.component.ts + 76 + + + apps/client/src/app/components/header/header.component.ts + 81 + + + apps/client/src/app/pages/about/about-page.component.ts + 45 + + + apps/client/src/app/pages/about/about-page.component.ts + 50 + + + apps/client/src/app/pages/about/about-page.component.ts + 55 + + + apps/client/src/app/pages/about/about-page.component.ts + 63 + + + apps/client/src/app/pages/about/about-page.component.ts + 74 + + + apps/client/src/app/pages/blog/2023/08/ghostfolio-joins-oss-friends/ghostfolio-joins-oss-friends-page.component.ts + 13 + + + apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.component.ts + 13 + + + apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.component.ts + 14 + + + apps/client/src/app/pages/blog/2023/09/hacktoberfest-2023/hacktoberfest-2023-page.component.ts + 13 + + + apps/client/src/app/pages/blog/2023/11/hacktoberfest-2023-debriefing/hacktoberfest-2023-debriefing-page.component.ts + 13 + + + apps/client/src/app/pages/landing/landing-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/personal-finance-tools-page.component.ts + 22 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/allinvestview-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/allvue-systems-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/altoo-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/basil-finance-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/beanvest-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/capitally-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/capmon-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/compound-planning-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/copilot-money-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/de.fi-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/delta-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/divvydiary-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/eightfigures-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/empower-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/exirio-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/fina-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/finary-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/finwise-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/folishare-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/getquin-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/gospatz-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/intuit-mint-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/justetf-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/kubera-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/magnifi-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/markets.sh-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/maybe-finance-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/monarch-money-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/monse-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/parqet-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/plannix-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/portfolio-dividend-tracker-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/portseido-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/projectionlab-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/rocket-money-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/seeking-alpha-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/sharesight-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/simple-portfolio-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/snowball-analytics-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/stockle-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/stockmarketeye-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/sumio-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/tiller-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/utluna-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/vyzer-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/wealthfolio-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/wealthica-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/whal-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/yeekatee-page.component.ts + 26 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/ynab-page.component.ts + 26 + + + + faq + 常见问题 + + apps/client/src/app/app-routing.module.ts + 10 + + + apps/client/src/app/app.component.ts + 54 + + + apps/client/src/app/pages/about/overview/about-overview-page.component.ts + 19 + + + apps/client/src/app/pages/faq/faq-page.component.ts + 37 + + + apps/client/src/app/pages/faq/faq-page.component.ts + 42 + + + apps/client/src/app/pages/faq/faq-page.component.ts + 48 + + + apps/client/src/app/pages/resources/resources-page.component.ts + 17 + + + + features + 功能 + + apps/client/src/app/app-routing.module.ts + 11 + + + apps/client/src/app/app.component.ts + 55 + + + apps/client/src/app/components/header/header.component.ts + 77 + + + apps/client/src/app/components/header/header.component.ts + 82 + + + apps/client/src/app/pages/about/overview/about-overview-page.component.ts + 20 + + + apps/client/src/app/pages/blog/2022/11/black-friday-2022/black-friday-2022-page.component.ts + 15 + + + apps/client/src/app/pages/blog/2023/03/1000-stars-on-github/1000-stars-on-github-page.component.ts + 13 + + + apps/client/src/app/pages/blog/2023/05/unlock-your-financial-potential-with-ghostfolio/unlock-your-financial-potential-with-ghostfolio-page.component.ts + 13 + + + apps/client/src/app/pages/blog/2023/07/exploring-the-path-to-fire/exploring-the-path-to-fire-page.component.ts + 13 + + + apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.component.ts + 15 + + + apps/client/src/app/pages/blog/2023/11/black-week-2023/black-week-2023-page.component.ts + 15 + + + apps/client/src/app/pages/blog/2023/11/hacktoberfest-2023-debriefing/hacktoberfest-2023-debriefing-page.component.ts + 14 + + + apps/client/src/app/pages/faq/overview/faq-overview-page.component.ts + 14 + + + apps/client/src/app/pages/pricing/pricing-page.component.ts + 35 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/allinvestview-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/allvue-systems-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/altoo-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/basil-finance-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/beanvest-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/capitally-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/capmon-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/compound-planning-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/copilot-money-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/de.fi-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/delta-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/divvydiary-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/eightfigures-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/empower-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/exirio-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/fina-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/finary-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/finwise-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/folishare-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/getquin-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/gospatz-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/intuit-mint-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/justetf-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/kubera-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/magnifi-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/markets.sh-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/maybe-finance-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/monarch-money-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/monse-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/parqet-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/plannix-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/portfolio-dividend-tracker-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/portseido-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/projectionlab-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/rocket-money-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/seeking-alpha-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/sharesight-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/simple-portfolio-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/snowball-analytics-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/stockle-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/stockmarketeye-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/sumio-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/tiller-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/utluna-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/vyzer-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/wealthfolio-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/wealthica-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/whal-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/yeekatee-page.component.ts + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/ynab-page.component.ts + 27 + + + + license + 许可 + + apps/client/src/app/app-routing.module.ts + 12 + + + apps/client/src/app/app.component.ts + 49 + + + apps/client/src/app/pages/about/about-page.component.ts + 55 + + + + markets + 市场 + + apps/client/src/app/app-routing.module.ts + 13 + + + apps/client/src/app/app.component.ts + 56 + + + apps/client/src/app/components/header/header.component.ts + 78 + + + apps/client/src/app/components/header/header.component.ts + 83 + + + apps/client/src/app/pages/blog/2022/08/500-stars-on-github/500-stars-on-github-page.component.ts + 13 + + + apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.component.ts + 16 + + + apps/client/src/app/pages/faq/saas/saas-page.component.ts + 14 + + + + pricing + 价钱 + + apps/client/src/app/app-routing.module.ts + 14 + + + apps/client/src/app/app.component.ts + 57 + + + apps/client/src/app/components/header/header.component.ts + 79 + + + apps/client/src/app/components/header/header.component.ts + 84 + + + apps/client/src/app/components/home-summary/home-summary.component.ts + 125 + + + apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.component.ts + 14 + + + apps/client/src/app/components/user-account-membership/user-account-membership.component.ts + 38 + + + apps/client/src/app/core/http-response.interceptor.ts + 81 + + + apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.component.ts + 13 + + + apps/client/src/app/pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.component.ts + 13 + + + apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.component.ts + 13 + + + apps/client/src/app/pages/blog/2022/08/500-stars-on-github/500-stars-on-github-page.component.ts + 14 + + + apps/client/src/app/pages/blog/2022/11/black-friday-2022/black-friday-2022-page.component.ts + 16 + + + apps/client/src/app/pages/blog/2023/03/1000-stars-on-github/1000-stars-on-github-page.component.ts + 14 + + + apps/client/src/app/pages/blog/2023/11/black-week-2023/black-week-2023-page.component.ts + 16 + + + apps/client/src/app/pages/faq/saas/saas-page.component.ts + 15 + + + libs/ui/src/lib/membership-card/membership-card.component.ts + 13 + + + + privacy-policy + 隐私政策 + + apps/client/src/app/app-routing.module.ts + 15 + + + apps/client/src/app/app.component.ts + 52 + + + apps/client/src/app/pages/about/about-page.component.ts + 63 + + + + register + 注册 + + apps/client/src/app/app-routing.module.ts + 16 + + + apps/client/src/app/app.component.ts + 58 + + + apps/client/src/app/components/header/header.component.ts + 85 + + + apps/client/src/app/core/auth.guard.ts + 54 + + + apps/client/src/app/pages/faq/saas/saas-page.component.ts + 16 + + + apps/client/src/app/pages/features/features-page.component.ts + 18 + + + apps/client/src/app/pages/landing/landing-page.component.ts + 27 + + + apps/client/src/app/pages/pricing/pricing-page.component.ts + 36 + + + + resources + 资源 + + apps/client/src/app/app-routing.module.ts + 17 + + + apps/client/src/app/app.component.ts + 59 + + + apps/client/src/app/components/header/header.component.ts + 80 + + + apps/client/src/app/components/header/header.component.ts + 86 + + + apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.component.ts + 14 + + + apps/client/src/app/pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.component.ts + 14 + + + apps/client/src/app/pages/blog/2022/07/how-do-i-get-my-finances-in-order/how-do-i-get-my-finances-in-order-page.component.ts + 13 + + + apps/client/src/app/pages/blog/2023/05/unlock-your-financial-potential-with-ghostfolio/unlock-your-financial-potential-with-ghostfolio-page.component.ts + 14 + + + apps/client/src/app/pages/features/features-page.component.ts + 19 + + + apps/client/src/app/pages/resources/personal-finance-tools/personal-finance-tools-page.component.ts + 14 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/allinvestview-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/allvue-systems-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/altoo-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/basil-finance-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/beanvest-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/capitally-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/capmon-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/compound-planning-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/copilot-money-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/de.fi-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/delta-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/divvydiary-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/eightfigures-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/empower-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/exirio-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/fina-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/finary-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/finwise-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/folishare-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/getquin-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/gospatz-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/intuit-mint-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/justetf-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/kubera-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/magnifi-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/markets.sh-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/maybe-finance-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/monarch-money-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/monse-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/parqet-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/plannix-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/portfolio-dividend-tracker-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/portseido-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/projectionlab-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/rocket-money-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/seeking-alpha-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/sharesight-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/simple-portfolio-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/snowball-analytics-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/stockle-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/stockmarketeye-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/sumio-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/tiller-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/utluna-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/vyzer-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/wealthfolio-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/wealthica-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/whal-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/yeekatee-page.component.ts + 29 + + + apps/client/src/app/pages/resources/personal-finance-tools/products/ynab-page.component.ts + 29 + + + apps/client/src/app/pages/resources/resources-page.component.ts + 19 + + + + You are using the Live Demo. + 您正在使用现场演示。 + + apps/client/src/app/app.component.html + 17 + + + + Create Account + 创建账户 + + apps/client/src/app/app.component.html + 18 + + + apps/client/src/app/pages/register/register-page.html + 26 + + + apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html + 2 + + + + Personal Finance + 个人财务 + + apps/client/src/app/app.component.html + 55 + + + + Markets + 市场 + + apps/client/src/app/app.component.html + 58 + + + apps/client/src/app/components/header/header.component.html + 381 + + + apps/client/src/app/components/home-market/home-market.html + 2 + + + apps/client/src/app/pages/resources/resources-page.html + 56 + + + + Resources + 资源 + + apps/client/src/app/app.component.html + 60 + + + apps/client/src/app/components/header/header.component.html + 80 + + + apps/client/src/app/components/header/header.component.html + 284 + + + apps/client/src/app/pages/resources/resources-page.html + 4 + + + + About + 关于 + + apps/client/src/app/app.component.html + 66 + + + apps/client/src/app/components/header/header.component.html + 111 + + + apps/client/src/app/components/header/header.component.html + 352 + + + + Blog + 博客 + + apps/client/src/app/app.component.html + 68 + + + apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.html + 204 + + + apps/client/src/app/pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.html + 183 + + + apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.html + 183 + + + apps/client/src/app/pages/blog/2022/07/ghostfolio-meets-internet-identity/ghostfolio-meets-internet-identity-page.html + 183 + + + apps/client/src/app/pages/blog/2022/07/how-do-i-get-my-finances-in-order/how-do-i-get-my-finances-in-order-page.html + 209 + + + apps/client/src/app/pages/blog/2022/08/500-stars-on-github/500-stars-on-github-page.html + 195 + + + apps/client/src/app/pages/blog/2022/10/hacktoberfest-2022/hacktoberfest-2022-page.html + 181 + + + apps/client/src/app/pages/blog/2022/11/black-friday-2022/black-friday-2022-page.html + 141 + + + apps/client/src/app/pages/blog/2022/12/the-importance-of-tracking-your-personal-finances/the-importance-of-tracking-your-personal-finances-page.html + 168 + + + apps/client/src/app/pages/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt/ghostfolio-auf-sackgeld-vorgestellt-page.html + 178 + + + apps/client/src/app/pages/blog/2023/02/ghostfolio-meets-umbrel/ghostfolio-meets-umbrel-page.html + 202 + + + apps/client/src/app/pages/blog/2023/03/1000-stars-on-github/1000-stars-on-github-page.html + 252 + + + apps/client/src/app/pages/blog/2023/05/unlock-your-financial-potential-with-ghostfolio/unlock-your-financial-potential-with-ghostfolio-page.html + 233 + + + apps/client/src/app/pages/blog/2023/07/exploring-the-path-to-fire/exploring-the-path-to-fire-page.html + 243 + + + apps/client/src/app/pages/blog/2023/08/ghostfolio-joins-oss-friends/ghostfolio-joins-oss-friends-page.html + 154 + + + apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.html + 273 + + + apps/client/src/app/pages/blog/2023/09/hacktoberfest-2023/hacktoberfest-2023-page.html + 181 + + + apps/client/src/app/pages/blog/2023/11/black-week-2023/black-week-2023-page.html + 148 + + + apps/client/src/app/pages/blog/2023/11/hacktoberfest-2023-debriefing/hacktoberfest-2023-debriefing-page.html + 270 + + + apps/client/src/app/pages/blog/blog-page.html + 5 + + + + Changelog + 变更日志 + + apps/client/src/app/app.component.html + 71 + + + apps/client/src/app/pages/about/changelog/changelog-page.html + 4 + + + + Features + 功能 + + apps/client/src/app/app.component.html + 73 + + + apps/client/src/app/components/header/header.component.html + 339 + + + apps/client/src/app/pages/features/features-page.html + 5 + + + + Frequently Asked Questions (FAQ) + 常见问题 (FAQ) + + apps/client/src/app/app.component.html + 76 + + + apps/client/src/app/pages/about/overview/about-overview-page.html + 146 + + + + License + 许可 + + apps/client/src/app/app.component.html + 80 + + + apps/client/src/app/pages/about/license/license-page.html + 4 + + + + Pricing + 价钱 + + apps/client/src/app/app.component.html + 86 + + + apps/client/src/app/components/header/header.component.html + 98 + + + apps/client/src/app/components/header/header.component.html + 296 + + + apps/client/src/app/components/header/header.component.html + 365 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 188 + + + + Privacy Policy + 隐私政策 + + apps/client/src/app/app.component.html + 90 + + + apps/client/src/app/pages/about/privacy-policy/privacy-policy-page.html + 4 + + + + Community + 社区 + + apps/client/src/app/app.component.html + 105 + + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 80 + + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 84 + + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 88 + + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 92 + + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 96 + + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 100 + + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 104 + + + apps/client/src/app/pages/features/features-page.html + 256 + + + + The risk of loss in trading can be substantial. It is not advisable to invest money you may need in the short term. + 交易损失的风险可能很大。不建议将短期内可能需要的资金进行投资。 + + apps/client/src/app/app.component.html + 179 + + + + Alias + 别名 + + apps/client/src/app/components/access-table/access-table.component.html + 3 + + + apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html + 11 + + + + Grantee + 受赠者 + + apps/client/src/app/components/access-table/access-table.component.html + 10 + + + + Type + 类型 + + apps/client/src/app/components/admin-jobs/admin-jobs.html + 28 + + + apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html + 22 + + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 12 + + + libs/ui/src/lib/activities-table/activities-table.component.html + 159 + + + + Details + 细节 + + apps/client/src/app/components/access-table/access-table.component.html + 32 + + + + Revoke + 撤销 + + apps/client/src/app/components/access-table/access-table.component.html + 59 + + + + Do you really want to revoke this granted access? + 您真的要撤销此授予的访问权限吗? + + apps/client/src/app/components/access-table/access-table.component.ts + 50 + + + + Cash Balance + 现金余额 + + apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html + 45 + + + apps/client/src/app/components/accounts-table/accounts-table.component.html + 117 + + + apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html + 34 + + + + Equity + 公平 + + apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html + 56 + + + + Activities + 活动 + + apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html + 61 + + + apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html + 90 + + + apps/client/src/app/components/accounts-table/accounts-table.component.html + 100 + + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 122 + + + apps/client/src/app/components/admin-tag/admin-tag.component.html + 44 + + + apps/client/src/app/components/admin-users/admin-users.html + 134 + + + apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html + 178 + + + apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html + 287 + + + apps/client/src/app/pages/portfolio/activities/activities-page.html + 4 + + + + Platform + 平台 + + apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html + 65 + + + apps/client/src/app/components/accounts-table/accounts-table.component.html + 72 + + + apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html + 48 + + + + Cash Balances + 现金余额 + + apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html + 114 + + + + Transfer Cash Balance + 转移现金余额 + + apps/client/src/app/components/accounts-table/accounts-table.component.html + 9 + + + apps/client/src/app/pages/accounts/transfer-balance/transfer-balance-dialog.html + 7 + + + + Name + 名称 + + apps/client/src/app/components/accounts-table/accounts-table.component.html + 34 + + + apps/client/src/app/components/admin-market-data/admin-market-data.html + 38 + + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 197 + + + apps/client/src/app/components/admin-platform/admin-platform.component.html + 30 + + + apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html + 7 + + + apps/client/src/app/components/admin-tag/admin-tag.component.html + 30 + + + apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.html + 7 + + + apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html + 15 + + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 138 + + + libs/ui/src/lib/activities-table/activities-table.component.html + 136 + + + libs/ui/src/lib/holdings-table/holdings-table.component.html + 28 + + + + Total + 全部的 + + apps/client/src/app/components/accounts-table/accounts-table.component.html + 45 + + + + Currency + 货币 + + apps/client/src/app/components/accounts-table/accounts-table.component.html + 55 + + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 103 + + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 203 + + + apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html + 25 + + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 144 + + + libs/ui/src/lib/activities-table/activities-table.component.html + 274 + + + + Value + 价值 + + apps/client/src/app/components/accounts-table/accounts-table.component.html + 152 + + + apps/client/src/app/components/accounts-table/accounts-table.component.html + 187 + + + apps/client/src/app/pages/accounts/transfer-balance/transfer-balance-dialog.html + 45 + + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 198 + + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 199 + + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 201 + + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 263 + + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 264 + + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 265 + + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 266 + + + libs/ui/src/lib/account-balances/account-balances.component.html + 20 + + + libs/ui/src/lib/activities-table/activities-table.component.html + 255 + + + libs/ui/src/lib/activities-table/activities-table.component.html + 291 + + + libs/ui/src/lib/holdings-table/holdings-table.component.html + 74 + + + + Edit + 编辑 + + apps/client/src/app/components/accounts-table/accounts-table.component.html + 254 + + + apps/client/src/app/components/admin-market-data/admin-market-data.html + 175 + + + apps/client/src/app/components/admin-overview/admin-overview.html + 80 + + + apps/client/src/app/components/admin-platform/admin-platform.component.html + 91 + + + apps/client/src/app/components/admin-tag/admin-tag.component.html + 71 + + + libs/ui/src/lib/activities-table/activities-table.component.html + 428 + + + + Delete + 删除 + + apps/client/src/app/components/accounts-table/accounts-table.component.html + 264 + + + apps/client/src/app/components/admin-market-data/admin-market-data.html + 190 + + + apps/client/src/app/components/admin-overview/admin-overview.html + 90 + + + apps/client/src/app/components/admin-overview/admin-overview.html + 199 + + + apps/client/src/app/components/admin-platform/admin-platform.component.html + 101 + + + apps/client/src/app/components/admin-tag/admin-tag.component.html + 81 + + + libs/ui/src/lib/account-balances/account-balances.component.html + 51 + + + libs/ui/src/lib/activities-table/activities-table.component.html + 450 + + + + Do you really want to delete this account? + 您真的要删除该帐户吗? + + apps/client/src/app/components/accounts-table/accounts-table.component.ts + 101 + + + + Asset Profile + 资产概况 + + apps/client/src/app/components/admin-jobs/admin-jobs.html + 31 + + + + Historical Market Data + 历史市场数据 + + apps/client/src/app/components/admin-jobs/admin-jobs.html + 37 + + + + Symbol + 符号 + + apps/client/src/app/components/admin-jobs/admin-jobs.html + 45 + + + apps/client/src/app/components/admin-market-data/admin-market-data.html + 24 + + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 98 + + + apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html + 34 + + + apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html + 257 + + + + Data Source + 数据源 + + apps/client/src/app/components/admin-jobs/admin-jobs.html + 54 + + + apps/client/src/app/components/admin-market-data/admin-market-data.html + 51 + + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 154 + + + + Attempts + 尝试 + + apps/client/src/app/components/admin-jobs/admin-jobs.html + 63 + + + + Created + 创建 + + apps/client/src/app/components/admin-jobs/admin-jobs.html + 72 + + + + Finished + 完成的 + + apps/client/src/app/components/admin-jobs/admin-jobs.html + 81 + + + + Status + 状况 + + apps/client/src/app/components/admin-jobs/admin-jobs.html + 90 + + + + Delete Jobs + 删除作业 + + apps/client/src/app/components/admin-jobs/admin-jobs.html + 126 + + + + View Data + 查看数据 + + apps/client/src/app/components/admin-jobs/admin-jobs.html + 141 + + + + View Stacktrace + 查看堆栈跟踪 + + apps/client/src/app/components/admin-jobs/admin-jobs.html + 148 + + + + Delete Job + 删除作业 + + apps/client/src/app/components/admin-jobs/admin-jobs.html + 151 + + + + Details for + 详细信息 + + apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html + 2 + + + + Date + 日期 + + apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html + 6 + + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 160 + + + libs/ui/src/lib/account-balances/account-balances.component.html + 11 + + + libs/ui/src/lib/activities-table/activities-table.component.html + 168 + + + + Market Price + 市场价 + + apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html + 26 + + + apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html + 81 + + + + Cancel + 取消 + + apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html + 46 + + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 330 + + + apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html + 40 + + + apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html + 19 + + + apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.html + 13 + + + apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html + 60 + + + apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html + 103 + + + apps/client/src/app/pages/accounts/transfer-balance/transfer-balance-dialog.html + 57 + + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 412 + + + apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html + 38 + + + + Save + 保存 + + apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html + 48 + + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 337 + + + apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html + 47 + + + apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html + 26 + + + apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.html + 20 + + + apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html + 67 + + + apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html + 110 + + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 419 + + + + Currencies + 货币 + + apps/client/src/app/components/admin-market-data/admin-market-data.component.ts + 67 + + + + ETFs without Countries + 没有国家的 ETF + + apps/client/src/app/components/admin-market-data/admin-market-data.component.ts + 72 + + + + ETFs without Sectors + 无行业类别的 ETF + + apps/client/src/app/components/admin-market-data/admin-market-data.component.ts + 77 + + + + Do you really want to delete this asset profile? + 您确实要删除此资产配置文件吗? + + apps/client/src/app/components/admin-market-data/admin-market-data.component.ts + 185 + + + + Filter by... + 过滤... + + apps/client/src/app/components/admin-market-data/admin-market-data.component.ts + 282 + + + + Asset Class + 资产类别 + + apps/client/src/app/components/admin-market-data/admin-market-data.html + 60 + + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 131 + + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 212 + + + apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html + 184 + + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 338 + + + + Asset Sub Class + 资产子类别 + + apps/client/src/app/components/admin-market-data/admin-market-data.html + 69 + + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 140 + + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 225 + + + apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html + 193 + + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 354 + + + + First Activity + 第一个活动 + + apps/client/src/app/components/admin-market-data/admin-market-data.html + 78 + + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 113 + + + apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html + 166 + + + libs/ui/src/lib/holdings-table/holdings-table.component.html + 50 + + + + Activities Count + 活动计数 + + apps/client/src/app/components/admin-market-data/admin-market-data.html + 87 + + + + Historical Data + 历史数据 + + apps/client/src/app/components/admin-market-data/admin-market-data.html + 96 + + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 65 + + + + Sectors Count + 行业数 + + apps/client/src/app/components/admin-market-data/admin-market-data.html + 105 + + + + Countries Count + 国家数 + + apps/client/src/app/components/admin-market-data/admin-market-data.html + 114 + + + + Gather Recent Data + 收集最近的数据 + + apps/client/src/app/components/admin-market-data/admin-market-data.html + 144 + + + + Gather All Data + 收集所有数据 + + apps/client/src/app/components/admin-market-data/admin-market-data.html + 147 + + + + Gather Profile Data + 收集个人资料数据 + + apps/client/src/app/components/admin-market-data/admin-market-data.html + 150 + + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 45 + + + + Oops! Could not parse historical data. + 哎呀!无法解析历史数据。 + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts + 223 + + + + Refresh + 刷新 + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 22 + + + + Gather Historical Data + 收集历史数据 + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 32 + + + + Import + 导入 + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 91 + + + apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html + 154 + + + apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html + 189 + + + + Sector + 行业 + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 159 + + + apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html + 210 + + + + Country + 国家 + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 168 + + + apps/client/src/app/components/admin-users/admin-users.html + 77 + + + apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html + 220 + + + + Sectors + 行业 + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 173 + + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 295 + + + apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html + 226 + + + apps/client/src/app/pages/public/public-page.html + 45 + + + + Countries + 国家 + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 183 + + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 306 + + + apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html + 238 + + + + Benchmark + 基准 + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 253 + + + + Symbol Mapping + 符号映射 + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 259 + + + + Scraper Configuration + 刮削配置 + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 270 + + + + Note + 笔记 + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 317 + + + apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html + 78 + + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 323 + + + + Add Asset Profile + 添加资产概况 + + apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html + 7 + + + + Search + 搜索 + + apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html + 16 + + + + Add Manually + 手动添加 + + apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html + 19 + + + + Name, symbol or ISIN + 名称、符号或 ISIN + + apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html + 25 + + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 122 + + + + Please add a currency: + 请添加货币: + + apps/client/src/app/components/admin-overview/admin-overview.component.ts + 122 + + + + is an invalid currency! + 是无效的货币! + + apps/client/src/app/components/admin-overview/admin-overview.component.ts + 129 + + + + Do you really want to delete this coupon? + 您确实要删除此优惠券吗? + + apps/client/src/app/components/admin-overview/admin-overview.component.ts + 140 + + + + Do you really want to delete this currency? + 您真的要删除该货币吗? + + apps/client/src/app/components/admin-overview/admin-overview.component.ts + 153 + + + + Do you really want to delete this system message? + 您真的要删除这条系统消息吗? + + apps/client/src/app/components/admin-overview/admin-overview.component.ts + 166 + + + + Do you really want to flush the cache? + 您真的要刷新缓存吗? + + apps/client/src/app/components/admin-overview/admin-overview.component.ts + 183 + + + + Please set your system message: + 请设置您的系统消息: + + apps/client/src/app/components/admin-overview/admin-overview.component.ts + 214 + + + + Version + 版本 + + apps/client/src/app/components/admin-overview/admin-overview.html + 7 + + + + User Count + 用户数 + + apps/client/src/app/components/admin-overview/admin-overview.html + 13 + + + + Activity Count + 活动计数 + + apps/client/src/app/components/admin-overview/admin-overview.html + 23 + + + + per User + 每位用户 + + apps/client/src/app/components/admin-overview/admin-overview.html + 32 + + + + Exchange Rates + 汇率 + + apps/client/src/app/components/admin-overview/admin-overview.html + 37 + + + + Add Currency + 添加货币 + + apps/client/src/app/components/admin-overview/admin-overview.html + 104 + + + + User Signup + 用户注册 + + apps/client/src/app/components/admin-overview/admin-overview.html + 110 + + + + Read-only Mode + 只读模式 + + apps/client/src/app/components/admin-overview/admin-overview.html + 123 + + + + System Message + 系统信息 + + apps/client/src/app/components/admin-overview/admin-overview.html + 145 + + + + Set Message + 设置留言 + + apps/client/src/app/components/admin-overview/admin-overview.html + 165 + + + + Coupons + 优惠券 + + apps/client/src/app/components/admin-overview/admin-overview.html + 173 + + + + Add + 添加 + + apps/client/src/app/components/admin-overview/admin-overview.html + 231 + + + + Housekeeping + 家政 + + apps/client/src/app/components/admin-overview/admin-overview.html + 238 + + + + Flush Cache + 刷新缓存 + + apps/client/src/app/components/admin-overview/admin-overview.html + 242 + + + + Add Platform + 添加平台 + + apps/client/src/app/components/admin-platform/admin-platform.component.html + 11 + + + + Url + 网址 + + apps/client/src/app/components/admin-platform/admin-platform.component.html + 50 + + + apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html + 13 + + + + Accounts + 帐户 + + apps/client/src/app/components/admin-platform/admin-platform.component.html + 64 + + + apps/client/src/app/components/admin-users/admin-users.html + 113 + + + apps/client/src/app/components/header/header.component.html + 54 + + + apps/client/src/app/components/header/header.component.html + 257 + + + apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html + 312 + + + apps/client/src/app/pages/accounts/accounts-page.html + 4 + + + libs/ui/src/lib/assistant/assistant.html + 107 + + + + Do you really want to delete this platform? + 您真的要删除这个平台吗? + + apps/client/src/app/components/admin-platform/admin-platform.component.ts + 79 + + + + Update platform + 更新平台 + + apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html + 2 + + + + Add platform + 添加平台 + + apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html + 3 + + + + Platforms + 平台 + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 4 + + + + Tags + 标签 + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 10 + + + apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html + 332 + + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 367 + + + libs/ui/src/lib/assistant/assistant.html + 127 + + + + Add Tag + 添加标签 + + apps/client/src/app/components/admin-tag/admin-tag.component.html + 11 + + + + Do you really want to delete this tag? + 您真的要删除此标签吗? + + apps/client/src/app/components/admin-tag/admin-tag.component.ts + 79 + + + + Update tag + 更新标签 + + apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.html + 2 + + + + Add tag + 添加标签 + + apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.html + 3 + + + + Do you really want to delete this user? + 您真的要删除该用户吗? + + apps/client/src/app/components/admin-users/admin-users.component.ts + 113 + + + + User + 用户 + + apps/client/src/app/components/admin-users/admin-users.html + 29 + + + + Registration + 注册 + + apps/client/src/app/components/admin-users/admin-users.html + 96 + + + + Engagement per Day + 每天的参与度 + + apps/client/src/app/components/admin-users/admin-users.html + 158 + + + + Last Request + 最后请求 + + apps/client/src/app/components/admin-users/admin-users.html + 183 + + + + Impersonate User + 模拟用户 + + apps/client/src/app/components/admin-users/admin-users.html + 222 + + + + Delete User + 删除用户 + + apps/client/src/app/components/admin-users/admin-users.html + 232 + + + + Performance + 表现 + + apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html + 6 + + + apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html + 59 + + + libs/ui/src/lib/holdings-table/holdings-table.component.html + 119 + + + + Compare with... + 与之比较... + + apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html + 19 + + + + Manage Benchmarks + 管理基准 + + apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html + 38 + + + + Portfolio + 文件夹 + + apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts + 110 + + + apps/client/src/app/pages/portfolio/portfolio-page-routing.module.ts + 48 + + + + Benchmark + 基准 + + apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts + 119 + + + + Current Market Mood + 当前市场情绪 + + apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.html + 12 + + + + Overview + 概述 + + apps/client/src/app/components/header/header.component.html + 28 + + + apps/client/src/app/components/header/header.component.html + 239 + + + + Portfolio + 文件夹 + + apps/client/src/app/components/header/header.component.html + 41 + + + apps/client/src/app/components/header/header.component.html + 249 + + + + Admin Control + 管理控制 + + apps/client/src/app/components/header/header.component.html + 67 + + + apps/client/src/app/components/header/header.component.html + 273 + + + + Me + + + apps/client/src/app/components/header/header.component.html + 206 + + + + User + 用户 + + apps/client/src/app/components/header/header.component.html + 225 + + + + My Ghostfolio + 我的 Ghostfolio + + apps/client/src/app/components/header/header.component.html + 264 + + + + About Ghostfolio + 关于 Ghostfolio + + apps/client/src/app/components/header/header.component.html + 304 + + + apps/client/src/app/pages/about/overview/about-overview-page.html + 5 + + + + Sign in + 登入 + + apps/client/src/app/components/header/header.component.html + 394 + + + apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html + 71 + + + + Get started + 开始使用 + + apps/client/src/app/components/header/header.component.html + 406 + + + + Sign in + 登入 + + apps/client/src/app/components/header/header.component.ts + 226 + + + apps/client/src/app/pages/webauthn/webauthn-page-routing.module.ts + 7 + + + + Oops! Incorrect Security Token. + 哎呀!安全令牌不正确。 + + apps/client/src/app/components/header/header.component.ts + 240 + + + + Manage Activities + 管理活动 + + apps/client/src/app/components/home-holdings/home-holdings.html + 22 + + + apps/client/src/app/pages/portfolio/holdings/holdings-page.html + 32 + + + + Fear + 恐惧 + + apps/client/src/app/components/home-market/home-market.component.ts + 25 + + + libs/ui/src/lib/i18n.ts + 69 + + + + Greed + 贪婪 + + apps/client/src/app/components/home-market/home-market.component.ts + 26 + + + libs/ui/src/lib/i18n.ts + 70 + + + + Last Days + 最后的 + + apps/client/src/app/components/home-market/home-market.html + 6 + + + + Welcome to Ghostfolio + 欢迎来到Ghostfolio + + apps/client/src/app/components/home-overview/home-overview.html + 7 + + + + Ready to take control of your personal finances? + 准备好掌控您的个人财务了吗? + + apps/client/src/app/components/home-overview/home-overview.html + 8 + + + + Setup your accounts + 设置您的帐户 + + apps/client/src/app/components/home-overview/home-overview.html + 15 + + + + Get a comprehensive financial overview by adding your bank and brokerage accounts. + 通过添加您的银行和经纪账户来获取全面的财务概览。 + + apps/client/src/app/components/home-overview/home-overview.html + 17 + + + + Capture your activities + 记录你的活动 + + apps/client/src/app/components/home-overview/home-overview.html + 24 + + + + Record your investment activities to keep your portfolio up to date. + 记录您的投资活动以使您的投资组合保持最新状态。 + + apps/client/src/app/components/home-overview/home-overview.html + 26 + + + + Monitor and analyze your portfolio + 监控和分析您的投资组合 + + apps/client/src/app/components/home-overview/home-overview.html + 33 + + + + Track your progress in real-time with comprehensive analysis and insights. + 通过全面的分析和见解实时跟踪您的进度。 + + apps/client/src/app/components/home-overview/home-overview.html + 35 + + + + Setup accounts + 设置帐户 + + apps/client/src/app/components/home-overview/home-overview.html + 48 + + + + Add activity + 添加活动 + + apps/client/src/app/components/home-overview/home-overview.html + 56 + + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 8 + + + + This feature requires a subscription. + 此功能需要订阅。 + + apps/client/src/app/components/home-summary/home-summary.component.ts + 113 + + + apps/client/src/app/core/http-response.interceptor.ts + 68 + + + + Upgrade Plan + 升级计划 + + apps/client/src/app/components/home-summary/home-summary.component.ts + 115 + + + apps/client/src/app/core/http-response.interceptor.ts + 70 + + + + Summary + 概括 + + apps/client/src/app/components/home-summary/home-summary.html + 2 + + + + Total Amount + 总金额 + + apps/client/src/app/components/investment-chart/investment-chart.component.ts + 191 + + + + Savings Rate + 储蓄率 + + apps/client/src/app/components/investment-chart/investment-chart.component.ts + 263 + + + + Security Token + 安全令牌 + + apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html + 11 + + + apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html + 10 + + + + or + + + apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html + 31 + + + apps/client/src/app/pages/landing/landing-page.html + 429 + + + apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html + 99 + + + apps/client/src/app/pages/register/register-page.html + 29 + + + apps/client/src/app/pages/webauthn/webauthn-page.html + 29 + + + + Sign in with Internet Identity + 使用互联网身份登录 + + apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html + 42 + + + + Sign in with Google + 使用 Google 登录 + + apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html + 52 + + + + Stay signed in + 保持登录 + + apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html + 61 + + + + Time in Market + 上市时间 + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 3 + + + + + + + + + + + + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 12 + + + + {VAR_PLURAL, plural, =1 {transaction} other {transactions}} + {VAR_PLURAL,复数,=1 {交易} 其他{交易}} + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 14 + + + + Buy + + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 21 + + + + Sell + + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 33 + + + + Investment + 投资 + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 48 + + + apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html + 134 + + + + Absolute Gross Performance + 绝对总业绩 + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 60 + + + + Gross Performance + 总表现 + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 77 + + + + Fees + 费用 + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 100 + + + apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html + 156 + + + apps/client/src/app/pages/portfolio/fire/fire-page.html + 161 + + + + Absolute Net Performance + 绝对净绩效 + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 116 + + + + Net Performance + 净绩效 + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 133 + + + + Total Assets + 总资产 + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 159 + + + + Valuables + 贵重物品 + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 172 + + + + Emergency Fund + 应急基金 + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 184 + + + apps/client/src/app/pages/features/features-page.html + 89 + + + apps/client/src/app/pages/portfolio/fire/fire-page.html + 122 + + + + Cash + 现金 + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 205 + + + + Assets + 资产 + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 218 + + + + Buying Power + 购买力 + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 231 + + + + Excluded from Analysis + 从分析中排除 + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 243 + + + + Liabilities + 负债 + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 258 + + + apps/client/src/app/pages/features/features-page.html + 102 + + + + Net Worth + 净值 + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 278 + + + + Annualized Performance + 年化业绩 + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 290 + + + + Interest + 利息 + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 312 + + + + Dividend + 股息 + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 324 + + + apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html + 145 + + + apps/client/src/app/pages/features/features-page.html + 63 + + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 196 + + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 261 + + + + Please enter the amount of your emergency fund: + 请输入您的应急基金金额: + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts + 53 + + + + Change + 修改 + + apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html + 48 + + + + Average Unit Price + 平均单价 + + apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html + 70 + + + + Minimum Price + 最低价格 + + apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html + 97 + + + + Maximum Price + 最高价格 + + apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html + 113 + + + + Quantity + 数量 + + apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html + 123 + + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 183 + + + libs/ui/src/lib/activities-table/activities-table.component.html + 184 + + + + Report Data Glitch + 报告数据故障 + + apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html + 350 + + + + Are you an ambitious investor who needs the full picture? + 您是一位雄心勃勃、需要全面了解的投资者吗? + + apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html + 12 + + + + Upgrade to Ghostfolio Premium today and gain access to exclusive features to enhance your investment experience: + 立即升级至 Ghostfolio Premium 并获得独家功能,以增强您的投资体验: + + apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html + 15 + + + + Portfolio Summary + 投资组合摘要 + + apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html + 22 + + + apps/client/src/app/pages/pricing/pricing-page.html + 55 + + + apps/client/src/app/pages/pricing/pricing-page.html + 199 + + + + Portfolio Allocations + 投资组合分配 + + apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html + 26 + + + apps/client/src/app/pages/features/features-page.html + 160 + + + apps/client/src/app/pages/pricing/pricing-page.html + 59 + + + apps/client/src/app/pages/pricing/pricing-page.html + 203 + + + + Performance Benchmarks + 性能基准 + + apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html + 30 + + + apps/client/src/app/pages/pricing/pricing-page.html + 63 + + + apps/client/src/app/pages/pricing/pricing-page.html + 207 + + + + FIRE Calculator + 财务自由计算器 + + apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html + 34 + + + apps/client/src/app/pages/pricing/pricing-page.html + 67 + + + apps/client/src/app/pages/pricing/pricing-page.html + 211 + + + + Professional Data Provider + 专业数据提供商 + + apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html + 38 + + + apps/client/src/app/pages/pricing/pricing-page.html + 226 + + + + and more Features... + 以及更多功能... + + apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html + 42 + + + apps/client/src/app/pages/pricing/pricing-page.html + 83 + + + apps/client/src/app/pages/pricing/pricing-page.html + 231 + + + + Get the tools to effectively manage your finances and refine your personal investment strategy. + 获取有效管理财务和完善个人投资策略的工具。 + + apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html + 45 + + + + Skip + 跳过 + + apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html + 52 + + + + Upgrade Plan + 升级计划 + + apps/client/src/app/components/header/header.component.html + 177 + + + apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html + 59 + + + apps/client/src/app/components/user-account-membership/user-account-membership.html + 20 + + + apps/client/src/app/pages/pricing/pricing-page.html + 268 + + + + Today + 今天 + + apps/client/src/app/components/toggle/toggle.component.ts + 22 + + + libs/ui/src/lib/assistant/assistant.component.ts + 99 + + + + YTD + 年初至今 + + apps/client/src/app/components/toggle/toggle.component.ts + 23 + + + libs/ui/src/lib/assistant/assistant.component.ts + 109 + + + + 1Y + 1年 + + apps/client/src/app/components/toggle/toggle.component.ts + 24 + + + libs/ui/src/lib/assistant/assistant.component.ts + 112 + + + + 5Y + 5年 + + apps/client/src/app/components/toggle/toggle.component.ts + 25 + + + libs/ui/src/lib/assistant/assistant.component.ts + 114 + + + + Max + 最大限度 + + apps/client/src/app/components/toggle/toggle.component.ts + 26 + + + libs/ui/src/lib/assistant/assistant.component.ts + 117 + + + + Grant access + 授予访问权限 + + apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html + 7 + + + + Public + 公开 + + apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html + 25 + + + + Granted Access + 授予访问权限 + + apps/client/src/app/components/user-account-access/user-account-access.html + 5 + + + + Please enter your coupon code: + 请输入您的优惠券代码: + + apps/client/src/app/components/user-account-membership/user-account-membership.component.ts + 111 + + + + Could not redeem coupon code + 无法兑换优惠券代码 + + apps/client/src/app/components/user-account-membership/user-account-membership.component.ts + 121 + + + + Coupon code has been redeemed + 优惠券代码已兑换 + + apps/client/src/app/components/user-account-membership/user-account-membership.component.ts + 133 + + + + Reload + 重新加载 + + apps/client/src/app/components/user-account-membership/user-account-membership.component.ts + 134 + + + + per year + 每年 + + apps/client/src/app/components/user-account-membership/user-account-membership.html + 41 + + + apps/client/src/app/pages/pricing/pricing-page.html + 254 + + + + Try Premium + 尝试高级版 + + apps/client/src/app/components/user-account-membership/user-account-membership.html + 50 + + + + Redeem Coupon + 兑换优惠券 + + apps/client/src/app/components/user-account-membership/user-account-membership.html + 63 + + + + Auto + 自动 + + apps/client/src/app/components/user-account-settings/user-account-settings.component.ts + 33 + + + + Do you really want to remove this sign in method? + 您确实要删除此登录方法吗? + + apps/client/src/app/components/user-account-settings/user-account-settings.component.ts + 188 + + + + Settings + 设置 + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 2 + + + + Presenter View + 演示者视图 + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 7 + + + + Protection for sensitive information like absolute performances and quantity values + 保护绝对性能和数量值等敏感信息 + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 8 + + + + Base Currency + 基础货币 + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 27 + + + + Language + 语言 + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 50 + + + + Locale + 语言环境 + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 113 + + + + Date and number format + 日期和数字格式 + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 115 + + + + Appearance + 外貌 + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 138 + + + + Auto + 自动 + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 152 + + + + Light + 明亮 + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 153 + + + + Dark + 黑暗 + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 154 + + + + Zen Mode + 极简模式 + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 163 + + + apps/client/src/app/pages/features/features-page.html + 190 + + + + Distraction-free experience for turbulent times + 动荡时期的无干扰体验 + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 164 + + + + Biometric Authentication + 生物识别认证 + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 180 + + + + Sign in with fingerprint + 使用指纹登录 + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 181 + + + + Experimental Features + 实验性功能 + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 198 + + + + Sneak peek at upcoming functionality + 预览即将推出的功能 + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 199 + + + + User ID + 用户身份 + + apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html + 47 + + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 215 + + + + Export Data + 导出数据 + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 223 + + + + This feature is currently unavailable. + 此功能目前无法使用。 + + apps/client/src/app/core/http-response.interceptor.ts + 60 + + + + Please try again later. + 请稍后再试。 + + apps/client/src/app/core/http-response.interceptor.ts + 62 + + + apps/client/src/app/core/http-response.interceptor.ts + 89 + + + apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts + 138 + + + + Oops! Something went wrong. + 哎呀!出了些问题。 + + apps/client/src/app/core/http-response.interceptor.ts + 87 + + + apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts + 136 + + + + Okay + 好的 + + apps/client/src/app/core/http-response.interceptor.ts + 90 + + + apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts + 139 + + + + About + 关于 + + apps/client/src/app/pages/about/about-page-routing.module.ts + 51 + + + apps/client/src/app/pages/about/about-page.component.ts + 44 + + + apps/client/src/app/pages/about/overview/about-overview-page-routing.module.ts + 13 + + + + Changelog + 更新日志 + + apps/client/src/app/pages/about/about-page.component.ts + 49 + + + apps/client/src/app/pages/about/changelog/changelog-page-routing.module.ts + 13 + + + + License + 许可 + + apps/client/src/app/pages/about/about-page.component.ts + 54 + + + apps/client/src/app/pages/about/license/license-page-routing.module.ts + 13 + + + + Privacy Policy + 隐私政策 + + apps/client/src/app/pages/about/about-page.component.ts + 62 + + + apps/client/src/app/pages/about/privacy-policy/privacy-policy-page-routing.module.ts + 13 + + + + Our + 我们的 + + apps/client/src/app/pages/about/oss-friends/oss-friends-page.html + 6 + + + + Discover other exciting Open Source Software projects + 发现其他令人兴奋的开源软件项目 + + apps/client/src/app/pages/about/oss-friends/oss-friends-page.html + 9 + + + + Visit + 访问 + + apps/client/src/app/pages/about/oss-friends/oss-friends-page.html + 28 + + + + Accounts + 账户 + + apps/client/src/app/pages/accounts/accounts-page-routing.module.ts + 13 + + + + Oops, cash balance transfer has failed. + 糟糕,现金余额转账失败。 + + apps/client/src/app/pages/accounts/accounts-page.component.ts + 306 + + + + Update account + 更新账户 + + apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html + 8 + + + + Add account + 新增帐户 + + apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html + 10 + + + + Account ID + 帐户ID + + apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html + 96 + + + + From + + + apps/client/src/app/pages/accounts/transfer-balance/transfer-balance-dialog.html + 11 + + + + To + + + apps/client/src/app/pages/accounts/transfer-balance/transfer-balance-dialog.html + 28 + + + + Transfer + 转移 + + apps/client/src/app/pages/accounts/transfer-balance/transfer-balance-dialog.html + 64 + + + + Admin Control + 管理控制 + + apps/client/src/app/pages/admin/admin-page-routing.module.ts + 20 + + + + Market Data + 市场数据 + + apps/client/src/app/pages/admin/admin-page-routing.module.ts + 30 + + + apps/client/src/app/pages/admin/admin-page.component.ts + 37 + + + + Settings + 设置 + + apps/client/src/app/pages/admin/admin-page-routing.module.ts + 35 + + + apps/client/src/app/pages/admin/admin-page.component.ts + 32 + + + apps/client/src/app/pages/user-account/user-account-page-routing.module.ts + 18 + + + apps/client/src/app/pages/user-account/user-account-page.component.ts + 35 + + + + Users + 用户 + + apps/client/src/app/pages/admin/admin-page-routing.module.ts + 40 + + + apps/client/src/app/pages/admin/admin-page.component.ts + 47 + + + + Overview + 概述 + + apps/client/src/app/pages/admin/admin-page.component.ts + 27 + + + apps/client/src/app/pages/home/home-page.component.ts + 34 + + + apps/client/src/app/pages/zen/zen-page-routing.module.ts + 19 + + + apps/client/src/app/pages/zen/zen-page.component.ts + 34 + + + + Blog + 博客 + + apps/client/src/app/pages/blog/blog-page-routing.module.ts + 13 + + + + Discover the latest Ghostfolio updates and insights on personal finance + 了解最新的 Ghostfolio 更新和个人理财见解 + + apps/client/src/app/pages/blog/blog-page.html + 7 + + + + As you are already logged in, you cannot access the demo account. + 由于您已经登录,因此无法访问模拟帐户。 + + apps/client/src/app/pages/demo/demo-page.component.ts + 32 + + + + Frequently Asked Questions (FAQ) + 常见问题 (FAQ) + + apps/client/src/app/pages/faq/faq-page-routing.module.ts + 34 + + + apps/client/src/app/pages/faq/overview/faq-overview-page-routing.module.ts + 13 + + + + Frequently Asked Questions (FAQ) + 常见问题 (FAQ) + + apps/client/src/app/pages/faq/overview/faq-overview-page.html + 4 + + + apps/client/src/app/pages/faq/saas/saas-page.html + 4 + + + apps/client/src/app/pages/faq/self-hosting/self-hosting-page.html + 4 + + + + Features + 功能 + + apps/client/src/app/pages/features/features-page-routing.module.ts + 13 + + + + Check out the numerous features of Ghostfolio to manage your wealth + 查看 Ghostfolio 的众多功能来管理您的财富 + + apps/client/src/app/pages/features/features-page.html + 6 + + + + Stocks + 股票 + + apps/client/src/app/pages/features/features-page.html + 15 + + + + ETFs + ETF + + apps/client/src/app/pages/features/features-page.html + 25 + + + + Bonds + 债券 + + apps/client/src/app/pages/features/features-page.html + 38 + + + + Cryptocurrencies + 加密货币 + + apps/client/src/app/pages/features/features-page.html + 51 + + + + Wealth Items + 财富项目 + + apps/client/src/app/pages/features/features-page.html + 76 + + + + Import and Export + 导入和导出 + + apps/client/src/app/pages/features/features-page.html + 115 + + + + Multi-Accounts + 多账户 + + apps/client/src/app/pages/features/features-page.html + 127 + + + + Portfolio Calculations + 投资组合计算 + + apps/client/src/app/pages/features/features-page.html + 141 + + + + Dark Mode + 深色模式 + + apps/client/src/app/pages/features/features-page.html + 177 + + + + Market Mood + 市场情绪 + + apps/client/src/app/pages/features/features-page.html + 205 + + + + Static Analysis + 静态分析 + + apps/client/src/app/pages/features/features-page.html + 224 + + + + Multi-Language + 多语言 + + apps/client/src/app/pages/features/features-page.html + 241 + + + + Open Source Software + 开源软件 + + apps/client/src/app/pages/features/features-page.html + 275 + + + + Get Started + 立即开始 + + apps/client/src/app/pages/features/features-page.html + 300 + + + apps/client/src/app/pages/public/public-page.html + 153 + + + + Holdings + 控股 + + apps/client/src/app/pages/home/home-page-routing.module.ts + 23 + + + apps/client/src/app/pages/home/home-page.component.ts + 39 + + + apps/client/src/app/pages/portfolio/holdings/holdings-page-routing.module.ts + 13 + + + apps/client/src/app/pages/portfolio/portfolio-page.component.ts + 39 + + + apps/client/src/app/pages/zen/zen-page.component.ts + 39 + + + + Summary + 概括 + + apps/client/src/app/pages/home/home-page-routing.module.ts + 28 + + + apps/client/src/app/pages/home/home-page.component.ts + 44 + + + + Markets + 市场 + + apps/client/src/app/pages/home/home-page-routing.module.ts + 33 + + + apps/client/src/app/pages/home/home-page.component.ts + 49 + + + apps/client/src/app/pages/markets/markets-page-routing.module.ts + 13 + + + + Ghostfolio is a personal finance dashboard to keep track of your net worth including cash, stocks, ETFs and cryptocurrencies across multiple platforms. + Ghostfolio 是一个个人财务仪表板,用于跨多个平台跟踪您的净资产,包括现金、股票、ETF 和加密货币。 + + apps/client/src/app/pages/i18n/i18n-page.html + 4 + + + + app, asset, cryptocurrency, dashboard, etf, finance, management, performance, portfolio, software, stock, trading, wealth, web3 + 应用程序、资产、加密货币、仪表板、etf、财务、管理、绩效、投资组合、软件、股票、交易、财富、web3 + + apps/client/src/app/pages/i18n/i18n-page.html + 9 + + + + Open Source Wealth Management Software + 开源财富管理软件 + + apps/client/src/app/pages/i18n/i18n-page.html + 14 + + + + New + 新的 + + apps/client/src/app/pages/landing/landing-page.html + 7 + + + + Manage your wealth like a boss + 像老板一样管理您的财富 + + apps/client/src/app/pages/landing/landing-page.html + 11 + + + + Ghostfolio is a privacy-first, open source dashboard for your personal finances. Break down your asset allocation, know your net worth and make solid, data-driven investment decisions. + Ghostfolio 是一个隐私优先、开源的个人财务仪表板。分解您的资产配置,了解您的净资产并做出可靠的、数据驱动的投资决策。 + + apps/client/src/app/pages/landing/landing-page.html + 15 + + + + Get Started + 开始使用 + + apps/client/src/app/pages/landing/landing-page.html + 47 + + + apps/client/src/app/pages/landing/landing-page.html + 425 + + + + or + + + apps/client/src/app/pages/landing/landing-page.html + 52 + + + + Live Demo + 现场演示 + + apps/client/src/app/pages/landing/landing-page.html + 55 + + + apps/client/src/app/pages/landing/landing-page.html + 430 + + + + Monthly Active Users + 每月活跃用户数 + + apps/client/src/app/pages/landing/landing-page.html + 75 + + + + Stars on GitHub + GitHub 上的星星 + + apps/client/src/app/pages/landing/landing-page.html + 93 + + + apps/client/src/app/pages/open/open-page.html + 103 + + + + Pulls on Docker Hub + 拉动 Docker Hub + + apps/client/src/app/pages/landing/landing-page.html + 111 + + + apps/client/src/app/pages/open/open-page.html + 117 + + + + As seen in + 如图所示 + + apps/client/src/app/pages/landing/landing-page.html + 119 + + + + Protect your assets. Refine your personal investment strategy. + 保护你的资产。完善你的个人投资策略 + + apps/client/src/app/pages/landing/landing-page.html + 221 + + + + Ghostfolio empowers busy people to keep track of stocks, ETFs or cryptocurrencies without being tracked. + Ghostfolio 使忙碌的人们能够在不被追踪的情况下跟踪股票、ETF 或加密货币。 + + apps/client/src/app/pages/landing/landing-page.html + 225 + + + + 360° View + 360° 视角 + + apps/client/src/app/pages/landing/landing-page.html + 236 + + + + Get the full picture of your personal finances across multiple platforms. + 跨多个平台全面了解您的个人财务状况。 + + apps/client/src/app/pages/landing/landing-page.html + 238 + + + + Web3 Ready + Web3 就绪 + + apps/client/src/app/pages/landing/landing-page.html + 247 + + + + Use Ghostfolio anonymously and own your financial data. + 匿名使用 Ghostfolio 并拥有您的财务数据。 + + apps/client/src/app/pages/landing/landing-page.html + 249 + + + + Open Source + 开源 + + apps/client/src/app/pages/landing/landing-page.html + 257 + + + + Benefit from continuous improvements through a strong community. + 通过强大的社区不断改进,从中受益。 + + apps/client/src/app/pages/landing/landing-page.html + 259 + + + + Why Ghostfolio? + 为什么使用Ghostfolio + + apps/client/src/app/pages/landing/landing-page.html + 268 + + + + Ghostfolio is for you if you are... + 如果您符合以下条件,那么 Ghostfolio 适合您... + + apps/client/src/app/pages/landing/landing-page.html + 269 + + + + trading stocks, ETFs or cryptocurrencies on multiple platforms + 在多个平台上交易股票、ETF 或加密货币 + + apps/client/src/app/pages/landing/landing-page.html + 276 + + + + pursuing a buy & hold strategy + 采取买入并持有策略 + + apps/client/src/app/pages/landing/landing-page.html + 282 + + + + interested in getting insights of your portfolio composition + 有兴趣深入了解您的投资组合构成 + + apps/client/src/app/pages/landing/landing-page.html + 287 + + + + valuing privacy and data ownership + 重视隐私和数据所有权 + + apps/client/src/app/pages/landing/landing-page.html + 292 + + + + into minimalism + 进入极简主义 + + apps/client/src/app/pages/landing/landing-page.html + 295 + + + + caring about diversifying your financial resources + 关心您的财务资源多元化 + + apps/client/src/app/pages/landing/landing-page.html + 299 + + + + interested in financial independence + 对财务独立感兴趣 + + apps/client/src/app/pages/landing/landing-page.html + 303 + + + + saying no to spreadsheets in + 对电子表格说不 + + apps/client/src/app/pages/landing/landing-page.html + 307 + + + + still reading this list + 仍在阅读此列表 + + apps/client/src/app/pages/landing/landing-page.html + 310 + + + + Learn more about Ghostfolio + 了解有关 Ghostfolio 的更多信息 + + apps/client/src/app/pages/landing/landing-page.html + 315 + + + + What our users are saying + 我们的什么用户正在说 + + apps/client/src/app/pages/landing/landing-page.html + 323 + + + + Members from around the globe are using Ghostfolio Premium + 来自世界各地的会员正在使用Ghostfolio 高级版 + + apps/client/src/app/pages/landing/landing-page.html + 355 + + + + How does Ghostfolio work? + 如何幽灵作品集工作? + + apps/client/src/app/pages/landing/landing-page.html + 367 + + + + Get started in only 3 steps + 只需 3 步即可开始 + + apps/client/src/app/pages/landing/landing-page.html + 370 + + + + Sign up anonymously* + 匿名注册* + + apps/client/src/app/pages/landing/landing-page.html + 376 + + + + * no e-mail address nor credit card required + * 无需电子邮件地址或信用卡 + + apps/client/src/app/pages/landing/landing-page.html + 378 + + + + Add any of your historical transactions + 添加您的任何历史交易 + + apps/client/src/app/pages/landing/landing-page.html + 389 + + + + Get valuable insights of your portfolio composition + 获取有关您的投资组合构成的宝贵见解 + + apps/client/src/app/pages/landing/landing-page.html + 401 + + + + Are you ready? + 准备好? + + apps/client/src/app/pages/landing/landing-page.html + 413 + + + + Join now or check out the example account + 立即加入或查看示例帐户 + + apps/client/src/app/pages/landing/landing-page.html + 414 + + + + At Ghostfolio, transparency is at the core of our values. We publish the source code as open source software (OSS) under the AGPL-3.0 license and we openly share aggregated key metrics of the platform’s operational status. + 在 Ghostfolio,透明度是我们价值观的核心。我们将源代码发布为开源软件(OSS)下AGPL-3.0许可证我们公开分享平台运营状态的汇总关键指标。 + + apps/client/src/app/pages/open/open-page.html + 6 + + + + (Last 24 hours) + (最近 24 小时) + + apps/client/src/app/pages/open/open-page.html + 37 + + + + Active Users + 活跃用户 + + apps/client/src/app/pages/open/open-page.html + 40 + + + apps/client/src/app/pages/open/open-page.html + 62 + + + + (Last 30 days) + (最近 30 天) + + apps/client/src/app/pages/open/open-page.html + 48 + + + apps/client/src/app/pages/open/open-page.html + 59 + + + + New Users + 新用户 + + apps/client/src/app/pages/open/open-page.html + 51 + + + + Users in Slack community + Slack 社区的用户 + + apps/client/src/app/pages/open/open-page.html + 75 + + + + Contributors on GitHub + GitHub 上的贡献者 + + apps/client/src/app/pages/open/open-page.html + 89 + + + + (Last 90 days) + (过去 90 天) + + apps/client/src/app/pages/open/open-page.html + 127 + + + + Uptime + 正常运行时间 + + apps/client/src/app/pages/open/open-page.html + 132 + + + + Activities + 活动 + + apps/client/src/app/pages/portfolio/activities/activities-page-routing.module.ts + 13 + + + apps/client/src/app/pages/portfolio/portfolio-page.component.ts + 44 + + + + Do you really want to delete all your activities? + 您真的要删除所有活动吗? + + apps/client/src/app/pages/portfolio/activities/activities-page.component.ts + 168 + + + + Update activity + 更新活动 + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 7 + + + + Stocks, ETFs, bonds, cryptocurrencies, commodities + 股票、ETF、债券、加密货币、大宗商品 + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 22 + + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 62 + + + + One-time fee, annual account fees + 一次性费用、年度账户费用 + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 30 + + + + Distribution of corporate earnings + 企业盈利分配 + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 38 + + + + Revenue for lending out money + 放贷收入 + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 46 + + + + Mortgages, personal loans, credit cards + 抵押贷款、个人贷款、信用卡 + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 54 + + + + Luxury items, real estate, private companies + 奢侈品、房地产、私营公司 + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 70 + + + + Account + 帐户 + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 82 + + + libs/ui/src/lib/activities-table/activities-table.component.html + 306 + + + + Update Cash Balance + 更新现金余额 + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 110 + + + + Unit Price + 单价 + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 203 + + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 267 + + + libs/ui/src/lib/activities-table/activities-table.component.html + 208 + + + + Oops! Could not get the historical exchange rate from + 哎呀!无法从以下来源获取历史汇率 + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 232 + + + + Fee + 费用 + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 286 + + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 314 + + + libs/ui/src/lib/activities-table/activities-table.component.html + 232 + + + + Oops! Could not get the historical exchange rate from + 哎呀!无法获取历史汇率 + + apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html + 304 + + + + Import Activities + 导入活动 + + apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts + 44 + + + + Import Dividends + 导入股息 + + apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts + 86 + + + + Importing data... + 正在导入数据... + + apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts + 120 + + + + Import has been completed + 导入已完成 + + apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts + 128 + + + + Validating data... + 验证数据... + + apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts + 233 + + + + Select Holding + 选择控股 + + apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html + 20 + + + + Select File + 选择文件 + + apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html + 23 + + + + Holding + 保持 + + apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html + 33 + + + + Load Dividends + 加载股息 + + apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html + 70 + + + + Choose or drop a file here + 在此处选择或放置文件 + + apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html + 86 + + + + The following file formats are supported: + 支持以下文件格式: + + apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html + 92 + + + + Select Dividends + 选择股息 + + apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html + 115 + + + + Select Activities + 选择活动 + + apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html + 118 + + + + Back + 后退 + + apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html + 145 + + + apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html + 181 + + + + Allocations + 分配 + + apps/client/src/app/pages/portfolio/allocations/allocations-page-routing.module.ts + 13 + + + apps/client/src/app/pages/portfolio/portfolio-page.component.ts + 49 + + + + Allocations + 分配 + + apps/client/src/app/pages/portfolio/allocations/allocations-page.html + 4 + + + + Proportion of Net Worth + 占净资产的比例 + + apps/client/src/app/pages/portfolio/allocations/allocations-page.html + 12 + + + + By Platform + 按平台 + + apps/client/src/app/pages/portfolio/allocations/allocations-page.html + 44 + + + + By Currency + 按货币 + + apps/client/src/app/pages/portfolio/allocations/allocations-page.html + 63 + + + + By Asset Class + 按资产类别 + + apps/client/src/app/pages/portfolio/allocations/allocations-page.html + 86 + + + + By Holding + 通过持有 + + apps/client/src/app/pages/portfolio/allocations/allocations-page.html + 109 + + + + By Sector + 按部门 + + apps/client/src/app/pages/portfolio/allocations/allocations-page.html + 132 + + + + By Continent + 按大陆 + + apps/client/src/app/pages/portfolio/allocations/allocations-page.html + 156 + + + + By Market + 按市场 + + apps/client/src/app/pages/portfolio/allocations/allocations-page.html + 179 + + + + Regions + 区域 + + apps/client/src/app/pages/portfolio/allocations/allocations-page.html + 203 + + + apps/client/src/app/pages/public/public-page.html + 76 + + + + Developed Markets + 发达市场 + + apps/client/src/app/pages/portfolio/allocations/allocations-page.html + 228 + + + apps/client/src/app/pages/public/public-page.html + 93 + + + + Emerging Markets + 新兴市场 + + apps/client/src/app/pages/portfolio/allocations/allocations-page.html + 237 + + + apps/client/src/app/pages/public/public-page.html + 102 + + + + Other Markets + 其他市场 + + apps/client/src/app/pages/portfolio/allocations/allocations-page.html + 246 + + + apps/client/src/app/pages/public/public-page.html + 111 + + + + No data available + 无可用数据 + + apps/client/src/app/pages/portfolio/allocations/allocations-page.html + 258 + + + apps/client/src/app/pages/public/public-page.html + 123 + + + + By Account + 按帐户 + + apps/client/src/app/pages/portfolio/allocations/allocations-page.html + 270 + + + + By ETF Provider + 按 ETF 提供商 + + apps/client/src/app/pages/portfolio/allocations/allocations-page.html + 290 + + + + By Country + 按国家/地区 + + apps/client/src/app/pages/portfolio/allocations/allocations-page.html + 313 + + + + Analysis + 分析 + + apps/client/src/app/pages/portfolio/analysis/analysis-page-routing.module.ts + 13 + + + apps/client/src/app/pages/portfolio/portfolio-page.component.ts + 34 + + + + Dividend + 股息 + + apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts + 42 + + + libs/ui/src/lib/i18n.ts + 31 + + + + Deposit + 订金 + + libs/ui/src/lib/fire-calculator/fire-calculator.component.ts + 332 + + + + Monthly + 每月 + + apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts + 54 + + + + Yearly + 每年 + + apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts + 55 + + + + Analysis + 分析 + + apps/client/src/app/pages/portfolio/analysis/analysis-page.html + 2 + + + + Top + 顶部 + + apps/client/src/app/pages/portfolio/analysis/analysis-page.html + 168 + + + + Bottom + 底部 + + apps/client/src/app/pages/portfolio/analysis/analysis-page.html + 216 + + + + Portfolio Evolution + 投资组合演变 + + apps/client/src/app/pages/portfolio/analysis/analysis-page.html + 268 + + + + Investment Timeline + 投资时间表 + + apps/client/src/app/pages/portfolio/analysis/analysis-page.html + 298 + + + + Current Streak + 当前连胜 + + apps/client/src/app/pages/portfolio/analysis/analysis-page.html + 319 + + + + Longest Streak + 最长连续纪录 + + apps/client/src/app/pages/portfolio/analysis/analysis-page.html + 328 + + + + Dividend Timeline + 股息时间表 + + apps/client/src/app/pages/portfolio/analysis/analysis-page.html + 356 + + + + FIRE + 财务独立 + + apps/client/src/app/pages/portfolio/fire/fire-page-routing.module.ts + 13 + + + + FIRE + 财务独立 + + apps/client/src/app/pages/portfolio/fire/fire-page.html + 4 + + + + Calculator + 计算器 + + apps/client/src/app/pages/portfolio/fire/fire-page.html + 7 + + + + 4% Rule + 4%规则 + + apps/client/src/app/pages/portfolio/fire/fire-page.html + 41 + + + + Ghostfolio X-ray uses static analysis to identify potential issues and risks in your portfolio. + Ghostfolio X-ray 使用静态分析来识别您的投资组合中的潜在问题和风险。 + + apps/client/src/app/pages/portfolio/fire/fire-page.html + 111 + + + + Currency Cluster Risks + 货币集群风险 + + apps/client/src/app/pages/portfolio/fire/fire-page.html + 135 + + + + Account Cluster Risks + 账户集群风险 + + apps/client/src/app/pages/portfolio/fire/fire-page.html + 148 + + + + Holdings + 控股 + + apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html + 77 + + + apps/client/src/app/pages/portfolio/holdings/holdings-page.html + 4 + + + apps/client/src/app/pages/public/public-page.html + 14 + + + libs/ui/src/lib/assistant/assistant.html + 46 + + + + Pricing + 价钱 + + apps/client/src/app/pages/pricing/pricing-page-routing.module.ts + 13 + + + + Pricing Plans + 定价计划 + + apps/client/src/app/pages/pricing/pricing-page.html + 4 + + + + Our official Ghostfolio Premium cloud offering is the easiest way to get started. Due to the time it saves, this will be the best option for most people. Revenue is used to cover the costs of the hosting infrastructure and to fund ongoing development. + 我们的官方 Ghostfolio Premium 云产品是最简单的入门方法。由于它节省了时间,这对于大多数人来说将是最佳选择。收入用于支付托管基础设施的成本和资助持续开发。 + + apps/client/src/app/pages/pricing/pricing-page.html + 6 + + + + If you prefer to run Ghostfolio on your own infrastructure, please find the source code and further instructions on GitHub. + 如果你希望在自己的基础设施上运行 Ghostfolio,请查看源代码和进一步的说明GitHub + + apps/client/src/app/pages/pricing/pricing-page.html + 24 + + + + For tech-savvy investors who prefer to run Ghostfolio on their own infrastructure. + 适合喜欢在自己的基础设施上运行 Ghostfolio 的精通技术的投资者。 + + apps/client/src/app/pages/pricing/pricing-page.html + 36 + + + + Unlimited Transactions + 无限交易 + + apps/client/src/app/pages/pricing/pricing-page.html + 43 + + + apps/client/src/app/pages/pricing/pricing-page.html + 126 + + + apps/client/src/app/pages/pricing/pricing-page.html + 187 + + + + Unlimited Accounts + 无限账户 + + apps/client/src/app/pages/pricing/pricing-page.html + 47 + + + apps/client/src/app/pages/pricing/pricing-page.html + 130 + + + apps/client/src/app/pages/pricing/pricing-page.html + 191 + + + + Portfolio Performance + 投资组合表现 + + apps/client/src/app/pages/pricing/pricing-page.html + 51 + + + apps/client/src/app/pages/pricing/pricing-page.html + 134 + + + apps/client/src/app/pages/pricing/pricing-page.html + 195 + + + + Data Import and Export + 数据导入与导出 + + apps/client/src/app/pages/pricing/pricing-page.html + 71 + + + apps/client/src/app/pages/pricing/pricing-page.html + 138 + + + apps/client/src/app/pages/pricing/pricing-page.html + 215 + + + + Community Support + 社区支持 + + apps/client/src/app/pages/pricing/pricing-page.html + 88 + + + + Self-hosted, update manually. + 自托管,手动更新。 + + apps/client/src/app/pages/pricing/pricing-page.html + 92 + + + + Free + 自由的 + + apps/client/src/app/pages/pricing/pricing-page.html + 93 + + + apps/client/src/app/pages/pricing/pricing-page.html + 150 + + + + For new investors who are just getting started with trading. + 适合刚开始交易的新投资者。 + + apps/client/src/app/pages/pricing/pricing-page.html + 120 + + + + Fully managed Ghostfolio cloud offering. + 完全托管的 Ghostfolio 云产品。 + + apps/client/src/app/pages/pricing/pricing-page.html + 149 + + + apps/client/src/app/pages/pricing/pricing-page.html + 240 + + + + For ambitious investors who need the full picture of their financial assets. + 适合需要全面了解其金融资产的雄心勃勃的投资者。 + + apps/client/src/app/pages/pricing/pricing-page.html + 180 + + + + Email and Chat Support + 电子邮件和聊天支持 + + apps/client/src/app/pages/pricing/pricing-page.html + 236 + + + + Renew Plan + 更新计划 + + apps/client/src/app/components/header/header.component.html + 185 + + + apps/client/src/app/components/user-account-membership/user-account-membership.html + 28 + + + apps/client/src/app/pages/pricing/pricing-page.html + 276 + + + + One-time payment, no auto-renewal. + 一次性付款,无自动续订。 + + apps/client/src/app/pages/pricing/pricing-page.html + 280 + + + + Get Started + 开始使用 + + apps/client/src/app/pages/pricing/pricing-page.html + 291 + + + + It’s free. + 免费。 + + apps/client/src/app/pages/pricing/pricing-page.html + 294 + + + + Hello, has shared a Portfolio with you! + 你好,分享了一个文件夹与你! + + apps/client/src/app/pages/public/public-page.html + 4 + + + + Currencies + 货币 + + apps/client/src/app/pages/public/public-page.html + 30 + + + + Continents + 大陆 + + apps/client/src/app/pages/public/public-page.html + 60 + + + + Ghostfolio empowers you to keep track of your wealth. + Ghostfolio 使您能够跟踪您的财富。 + + apps/client/src/app/pages/public/public-page.html + 148 + + + + Registration + 注册 + + apps/client/src/app/pages/register/register-page-routing.module.ts + 13 + + + + Continue with Internet Identity + 继续互联网身份 + + apps/client/src/app/pages/register/register-page.html + 41 + + + + Continue with Google + 继续使用谷歌 + + apps/client/src/app/pages/register/register-page.html + 51 + + + + Copy to clipboard + 复制到剪贴板 + + apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html + 26 + + + + I agree to have stored my Security Token from above in a secure place. If I lose it, I cannot get my account back. + 我同意存储我的保安编码器从上面看,在一个安全的地方。如果我丢失了它,我将无法找回我的帐户。 + + apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html + 32 + + + + Agree and continue + 同意并继续 + + apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html + 45 + + + + Personal Finance Tools + 个人理财工具 + + apps/client/src/app/pages/resources/personal-finance-tools/personal-finance-tools-page-routing.module.ts + 14 + + + + open-source-alternative-to + 开源替代方案 + + apps/client/src/app/pages/resources/personal-finance-tools/personal-finance-tools-page-routing.module.ts + 23 + + + apps/client/src/app/pages/resources/personal-finance-tools/personal-finance-tools-page.component.ts + 13 + + + + Open Source Alternative to + 开源替代方案 + + apps/client/src/app/pages/resources/personal-finance-tools/personal-finance-tools-page-routing.module.ts + 26 + + + + Discover Open Source Alternatives for Personal Finance Tools + 发现个人理财工具的开源替代品 + + apps/client/src/app/pages/resources/personal-finance-tools/personal-finance-tools-page.html + 4 + + + + This overview page features a curated collection of personal finance tools compared to the open source alternative Ghostfolio. If you value transparency, data privacy, and community collaboration, Ghostfolio provides an excellent opportunity to take control of your financial management. + 此概述页面包含与开源替代方案相比的精选个人理财工具集合Ghostfolio。如果您重视透明度、数据隐私和社区协作,Ghostfolio 提供了一个绝佳的机会来控制您的财务管理。 + + apps/client/src/app/pages/resources/personal-finance-tools/personal-finance-tools-page.html + 8 + + + + Explore the links below to compare a variety of personal finance tools with Ghostfolio. + 浏览下面的链接,将各种个人理财工具与 Ghostfolio 进行比较。 + + apps/client/src/app/pages/resources/personal-finance-tools/personal-finance-tools-page.html + 16 + + + + Open Source Alternative to + 开源替代方案 + + apps/client/src/app/pages/resources/personal-finance-tools/personal-finance-tools-page.html + 38 + + + + The Open Source Alternative to + 开源替代方案 + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 8 + + + + Are you looking for an open source alternative to ? Ghostfolio is a powerful portfolio management tool that provides individuals with a comprehensive platform to track, analyze, and optimize their investments. Whether you are an experienced investor or just starting out, Ghostfolio offers an intuitive user interface and a wide range of functionalities to help you make informed decisions and take control of your financial future. + 您是否正在寻找开源替代方案幽灵作品集是一个强大的投资组合管理工具,为个人提供一个全面的平台来跟踪、分析和优化他们的投资。无论您是经验丰富的投资者还是刚刚起步的投资者,Ghostfolio 都提供直观的用户界面和广泛的功能帮助您做出明智的决定并掌控您的财务未来。 + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 13 + + + + Ghostfolio is an open source software (OSS), providing a cost-effective alternative to making it particularly suitable for individuals on a tight budget, such as those pursuing Financial Independence, Retire Early (FIRE). By leveraging the collective efforts of a community of developers and personal finance enthusiasts, Ghostfolio continuously enhances its capabilities, security, and user experience. + Ghostfolio 是一款开源软件 (OSS),提供了一种经济高效的替代方案使其特别适合预算紧张的个人,例如追求财务独立,提前退休(FIRE) 。通过利用开发者社区和个人理财爱好者的集体努力,Ghostfolio 不断增强其功能、安全性和用户体验。 + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 27 + + + + Let’s dive deeper into the detailed Ghostfolio vs comparison table below to gain a thorough understanding of how Ghostfolio positions itself relative to . We will explore various aspects such as features, data privacy, pricing, and more, allowing you to make a well-informed choice for your personal requirements. + 让我们更深入地了解 Ghostfolio 与下面的比较表可帮助您全面了解 Ghostfolio 相对于其他产品的定位。我们将探讨功能、数据隐私、定价等各个方面,让您根据个人需求做出明智的选择。 + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 38 + + + + Ghostfolio vs comparison table + Ghostfolio vs比较表 + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 49 + + + + Founded + 成立 + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 72 + + + + Origin + 来源 + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 77 + + + + Region + 地区 + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 82 + + + + Available in + 可用于 + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 87 + + + + ✅ Yes + ✅ 是的 + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 109 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 116 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 130 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 141 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 155 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 162 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 174 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 181 + + + + ❌ No + ❌ 没有 + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 111 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 134 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 145 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 157 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 164 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 176 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 183 + + + + ❌ No + ❌ 没有 + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 118 + + + + Self-Hosting + 自托管 + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 123 + + + + Use anonymously + 匿名使用 + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 150 + + + + Free Plan + 免费计划 + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 169 + + + + Starting from + 从...开始 + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 195 + + + + year + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 191 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 197 + + + + Notes + 笔记 + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 202 + + + + Please note that the information provided in the Ghostfolio vs comparison table is based on our independent research and analysis. This website is not affiliated with or any other product mentioned in the comparison. As the landscape of personal finance tools evolves, it is essential to verify any specific details or changes directly from the respective product page. Data needs a refresh? Help us maintain accurate data on GitHub. + 请注意,Ghostfolio 与 Ghostfolio 中提供的信息比较表基于我们的独立研究和分析。该网站不隶属于或比较中提到的任何其他产品。随着个人理财工具格局的不断发展,直接从相应的产品页面验证任何具体的细节或变化至关重要。数据需要刷新吗?帮助我们维护准确的数据GitHub + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 210 + + + + Ready to take your investments to the next level? + 准备好带走你的投资下一级 + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 223 + + + + Effortlessly track, analyze, and visualize your wealth with Ghostfolio. + 使用 Ghostfolio 轻松跟踪、分析和可视化您的财富。 + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 227 + + + + Get Started + 开始使用 + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 232 + + + + Personal Finance Tools + 个人理财工具 + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html + 308 + + + + Switzerland + 瑞士 + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 72 + + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 102 + + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 530 + + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 580 + + + + Global + 全球的 + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 73 + + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 341 + + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 462 + + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 581 + + + + United States + 美国 + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 93 + + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 149 + + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 159 + + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 201 + + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 210 + + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 220 + + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 232 + + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 242 + + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 294 + + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 316 + + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 327 + + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 352 + + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 354 + + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 364 + + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 429 + + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 439 + + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 449 + + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 518 + + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 541 + + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 569 + + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 591 + + + + France + 法国 + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 121 + + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 482 + + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 498 + + + + Poland + 波兰 + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 131 + + + + Germany + 德国 + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 140 + + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 190 + + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 274 + + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 284 + + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 305 + + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 339 + + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 385 + + + + Belgium + 比利时 + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 179 + + + + South Africa + 南非 + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 251 + + + + Austria + 奥地利 + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 262 + + + + Italy + 意大利 + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 396 + + + + Netherlands + 荷兰 + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 406 + + + + Thailand + 泰国 + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 418 + + + + New Zealand + 新西兰 + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 460 + + + + Czech Republic + 捷克共和国 + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 471 + + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 508 + + + + Finland + 芬兰 + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 490 + + + + Canada + 加拿大 + + apps/client/src/app/pages/resources/personal-finance-tools/products.ts + 561 + + + + Resources + 资源 + + apps/client/src/app/pages/resources/resources-page-routing.module.ts + 13 + + + + Guides + 指南 + + apps/client/src/app/pages/resources/resources-page.html + 22 + + + + Glossary + 词汇表 + + apps/client/src/app/pages/resources/resources-page.html + 92 + + + + Membership + 会员资格 + + apps/client/src/app/pages/user-account/user-account-page-routing.module.ts + 23 + + + apps/client/src/app/pages/user-account/user-account-page.component.ts + 40 + + + + Access + 使用权 + + apps/client/src/app/pages/user-account/user-account-page-routing.module.ts + 28 + + + apps/client/src/app/pages/user-account/user-account-page.component.ts + 46 + + + + My Ghostfolio + 我的 Ghostfolio + + apps/client/src/app/pages/user-account/user-account-page-routing.module.ts + 33 + + + + Oops, authentication has failed. + 糟糕,身份验证失败。 + + apps/client/src/app/pages/webauthn/webauthn-page.html + 19 + + + + Try again + 再试一次 + + apps/client/src/app/pages/webauthn/webauthn-page.html + 27 + + + + Go back to Home Page + 返回首页 + + apps/client/src/app/pages/webauthn/webauthn-page.html + 31 + + + + Do you really want to delete this account balance? + 您确实要删除该帐户余额吗? + + libs/ui/src/lib/account-balances/account-balances.component.ts + 58 + + + + Import Activities + 进口活动 + + libs/ui/src/lib/activities-table/activities-table.component.html + 9 + + + libs/ui/src/lib/activities-table/activities-table.component.html + 369 + + + + Import Dividends + 导入股息 + + libs/ui/src/lib/activities-table/activities-table.component.html + 29 + + + libs/ui/src/lib/activities-table/activities-table.component.html + 381 + + + + Export Activities + 出口活动 + + libs/ui/src/lib/activities-table/activities-table.component.html + 41 + + + libs/ui/src/lib/activities-table/activities-table.component.html + 394 + + + + Export Drafts as ICS + 将汇票导出为 ICS + + libs/ui/src/lib/activities-table/activities-table.component.html + 54 + + + libs/ui/src/lib/activities-table/activities-table.component.html + 407 + + + + Delete all Activities + 删除所有活动 + + libs/ui/src/lib/activities-table/activities-table.component.html + 65 + + + + Draft + 草稿 + + libs/ui/src/lib/activities-table/activities-table.component.html + 143 + + + + Clone + 克隆 + + libs/ui/src/lib/activities-table/activities-table.component.html + 434 + + + + Export Draft as ICS + 将汇票导出为 ICS + + libs/ui/src/lib/activities-table/activities-table.component.html + 444 + + + + Do you really want to delete this activity? + 您确实要删除此活动吗? + + libs/ui/src/lib/activities-table/activities-table.component.ts + 175 + + + + Find holding... + 查找持有... + + libs/ui/src/lib/assistant/assistant.component.ts + 126 + + + + No entries... + 没有条目... + + libs/ui/src/lib/assistant/assistant.html + 63 + + + libs/ui/src/lib/assistant/assistant.html + 84 + + + + Asset Profiles + 资产概况 + + libs/ui/src/lib/assistant/assistant.html + 67 + + + + Index + 指数 + + libs/ui/src/lib/benchmark/benchmark.component.html + 3 + + + + 50-Day Trend + 50 天趋势 + + libs/ui/src/lib/benchmark/benchmark.component.html + 15 + + + + 200-Day Trend + 200天趋势 + + libs/ui/src/lib/benchmark/benchmark.component.html + 40 + + + + Last All Time High + 上次历史最高纪录 + + libs/ui/src/lib/benchmark/benchmark.component.html + 65 + + + + Change from All Time High + 从历史最高点开始变化 + + libs/ui/src/lib/benchmark/benchmark.component.html + 81 + + + + from ATH + 来自 ATH + + libs/ui/src/lib/benchmark/benchmark.component.html + 83 + + + + Market data provided by + 市场数据提供者 + + libs/ui/src/lib/data-provider-credits/data-provider-credits.component.html + 2 + + + + Savings Rate per Month + 每月储蓄率 + + libs/ui/src/lib/fire-calculator/fire-calculator.component.html + 10 + + + + Annual Interest Rate + 年利率 + + libs/ui/src/lib/fire-calculator/fire-calculator.component.html + 21 + + + + Retirement Date + 退休日期 + + libs/ui/src/lib/fire-calculator/fire-calculator.component.html + 32 + + + + Projected Total Amount + 预计总额 + + libs/ui/src/lib/fire-calculator/fire-calculator.component.html + 60 + + + + Interest + 利息 + + libs/ui/src/lib/fire-calculator/fire-calculator.component.ts + 342 + + + libs/ui/src/lib/i18n.ts + 33 + + + + Savings + 储蓄 + + libs/ui/src/lib/fire-calculator/fire-calculator.component.ts + 352 + + + + Allocation + 分配 + + libs/ui/src/lib/holdings-table/holdings-table.component.html + 98 + + + + Show all + 显示所有 + + libs/ui/src/lib/holdings-table/holdings-table.component.html + 174 + + + + Account + 帐户 + + libs/ui/src/lib/i18n.ts + 4 + + + + Asia-Pacific + 亚太 + + libs/ui/src/lib/i18n.ts + 5 + + + + Asset Class + 资产类别 + + libs/ui/src/lib/i18n.ts + 6 + + + + Asset Sub Class + 资产子类别 + + libs/ui/src/lib/i18n.ts + 7 + + + + Core + 核心 + + libs/ui/src/lib/i18n.ts + 8 + + + + Switch to Ghostfolio Premium or Ghostfolio Open Source easily + 轻松切换到 Ghostfolio Premium 或 Ghostfolio Open Source + + libs/ui/src/lib/i18n.ts + 9 + + + + Switch to Ghostfolio Premium easily + 轻松切换到 Ghostfolio Premium + + libs/ui/src/lib/i18n.ts + 10 + + + + Switch to Ghostfolio Open Source or Ghostfolio Basic easily + 轻松切换到 Ghostfolio Open Source 或 Ghostfolio Basic + + libs/ui/src/lib/i18n.ts + 11 + + + + Emergency Fund + 应急基金 + + libs/ui/src/lib/i18n.ts + 12 + + + + Grant + 授予 + + libs/ui/src/lib/i18n.ts + 13 + + + + Higher Risk + 风险较高 + + libs/ui/src/lib/i18n.ts + 14 + + + + This activity already exists. + 这项活动已经存在。 + + libs/ui/src/lib/i18n.ts + 15 + + + + Japan + 日本 + + libs/ui/src/lib/i18n.ts + 16 + + + + Lower Risk + 降低风险 + + libs/ui/src/lib/i18n.ts + 17 + + + + Month + + + libs/ui/src/lib/i18n.ts + 18 + + + + Months + 几个月 + + libs/ui/src/lib/i18n.ts + 19 + + + + Other + 其他 + + libs/ui/src/lib/i18n.ts + 20 + + + libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts + 385 + + + + Preset + 预设 + + libs/ui/src/lib/i18n.ts + 21 + + + + Retirement Provision + 退休金 + + libs/ui/src/lib/i18n.ts + 22 + + + + Satellite + 卫星 + + libs/ui/src/lib/i18n.ts + 23 + + + + Symbol + 符号 + + libs/ui/src/lib/i18n.ts + 24 + + + + Tag + 标签 + + libs/ui/src/lib/i18n.ts + 25 + + + + Year + + + libs/ui/src/lib/i18n.ts + 26 + + + + Years + + + libs/ui/src/lib/i18n.ts + 27 + + + + Buy + + + libs/ui/src/lib/i18n.ts + 30 + + + + Fee + 费用 + + libs/ui/src/lib/i18n.ts + 32 + + + + Valuable + 有价值的 + + libs/ui/src/lib/i18n.ts + 34 + + + + Liability + 责任 + + libs/ui/src/lib/i18n.ts + 35 + + + + Sell + + + libs/ui/src/lib/i18n.ts + 36 + + + + Cash + 现金 + + libs/ui/src/lib/i18n.ts + 39 + + + + Commodity + 商品 + + libs/ui/src/lib/i18n.ts + 40 + + + + Equity + 公平 + + libs/ui/src/lib/i18n.ts + 41 + + + + Fixed Income + 固定收入 + + libs/ui/src/lib/i18n.ts + 42 + + + + Real Estate + 房地产 + + libs/ui/src/lib/i18n.ts + 43 + + + + Bond + 纽带 + + libs/ui/src/lib/i18n.ts + 46 + + + + Cryptocurrency + 加密货币 + + libs/ui/src/lib/i18n.ts + 47 + + + + ETF + 交易所交易基金 + + libs/ui/src/lib/i18n.ts + 48 + + + + Mutual Fund + 共同基金 + + libs/ui/src/lib/i18n.ts + 49 + + + + Precious Metal + 贵金属 + + libs/ui/src/lib/i18n.ts + 50 + + + + Private Equity + 私人产权 + + libs/ui/src/lib/i18n.ts + 51 + + + + Stock + 库存 + + libs/ui/src/lib/i18n.ts + 52 + + + + Africa + 非洲 + + libs/ui/src/lib/i18n.ts + 59 + + + + Asia + 亚洲 + + libs/ui/src/lib/i18n.ts + 60 + + + + Europe + 欧洲 + + libs/ui/src/lib/i18n.ts + 61 + + + + North America + 北美 + + libs/ui/src/lib/i18n.ts + 62 + + + + Oceania + 大洋洲 + + libs/ui/src/lib/i18n.ts + 63 + + + + South America + 南美洲 + + libs/ui/src/lib/i18n.ts + 64 + + + + Extreme Fear + 极度恐惧 + + libs/ui/src/lib/i18n.ts + 67 + + + + Extreme Greed + 极度贪婪 + + libs/ui/src/lib/i18n.ts + 68 + + + + Neutral + 中性的 + + libs/ui/src/lib/i18n.ts + 71 + + + + Membership + 会员资格 + + libs/ui/src/lib/membership-card/membership-card.component.html + 18 + + + + Valid until + 有效期至 + + libs/ui/src/lib/membership-card/membership-card.component.html + 23 + + + + Time to add your first activity. + 是时候添加您的第一个活动了。 + + libs/ui/src/lib/no-transactions-info/no-transactions-info.component.html + 12 + + + + No data available + 无可用数据 + + libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts + 387 + + + libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts + 400 + + + + If a translation is missing, kindly support us in extending it here. + 如果翻译缺失,请支持我们进行扩展这里 + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 55 + + + + Date Range + 日期范围 + + libs/ui/src/lib/assistant/assistant.html + 93 + + + + The current market price is + 当前市场价格为 + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts + 317 + + + + Test + 测试 + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 288 + + + + Oops! Could not grant access. + 哎呀!无法授予访问权限。 + + apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts + 80 + + + + Restricted view + 视野受限 + + apps/client/src/app/components/access-table/access-table.component.html + 25 + + + apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html + 36 + + + + Permission + 允许 + + apps/client/src/app/components/access-table/access-table.component.html + 17 + + + apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html + 33 + + + + Private + 私人的 + + apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html + 24 + + + + Job Queue + 作业队列 + + apps/client/src/app/pages/admin/admin-page-routing.module.ts + 25 + + + apps/client/src/app/pages/admin/admin-page.component.ts + 42 + + + + Market data is delayed for + 市场数据延迟 + + apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts + 82 + + + + Absolute Currency Performance + 绝对货币表现 + + apps/client/src/app/pages/portfolio/analysis/analysis-page.html + 73 + + + + Absolute Net Performance + 绝对净性能 + + apps/client/src/app/pages/portfolio/analysis/analysis-page.html + 121 + + + + Absolute Asset Performance + 绝对资产绩效 + + apps/client/src/app/pages/portfolio/analysis/analysis-page.html + 29 + + + + Investment + 投资 + + apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts + 46 + + + apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts + 60 + + + + Asset Performance + 资产绩效 + + apps/client/src/app/pages/portfolio/analysis/analysis-page.html + 51 + + + + Net Performance + 净绩效 + + apps/client/src/app/pages/portfolio/analysis/analysis-page.html + 140 + + + + Currency Performance + 货币表现 + + apps/client/src/app/pages/portfolio/analysis/analysis-page.html + 98 + + + + Year to date + 今年迄今为止 + + libs/ui/src/lib/assistant/assistant.component.ts + 109 + + + + Week to date + 本周至今 + + libs/ui/src/lib/assistant/assistant.component.ts + 101 + + + + Month to date + 本月至今 + + libs/ui/src/lib/assistant/assistant.component.ts + 105 + + + + MTD + 最大输运量 + + libs/ui/src/lib/assistant/assistant.component.ts + 105 + + + + WTD + 世界贸易组织 + + libs/ui/src/lib/assistant/assistant.component.ts + 101 + + + + Oops! A data provider is experiencing the hiccups. + 哎呀!数据提供商遇到了问题。 + + apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html + 8 + + + + View + 看法 + + apps/client/src/app/components/access-table/access-table.component.html + 22 + + + apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html + 39 + + + + Reset Filters + 重置过滤器 + + libs/ui/src/lib/assistant/assistant.html + 155 + + + + If you retire today, you would be able to withdraw per year or per month, based on your total assets of and a withdrawal rate of 4%. + 如果你今天退休,你可以领取每年或者每月,根据您的总资产提款率为4%。 + + apps/client/src/app/pages/portfolio/fire/fire-page.html + 68 + + + + year + + + libs/ui/src/lib/assistant/assistant.component.ts + 112 + + + + years + + + libs/ui/src/lib/assistant/assistant.component.ts + 114 + + + + Apply Filters + 应用过滤器 + + libs/ui/src/lib/assistant/assistant.html + 165 + + + + Asset Classes + 资产类别 + + libs/ui/src/lib/assistant/assistant.html + 138 + + + + self-hosting + 自托管 + + apps/client/src/app/pages/faq/faq-page.component.ts + 48 + + + + FAQ + 常见问题 + + apps/client/src/app/pages/faq/saas/saas-page-routing.module.ts + 13 + + + apps/client/src/app/pages/faq/self-hosting/self-hosting-page-routing.module.ts + 13 + + + + Self-Hosting + 自托管 + + apps/client/src/app/pages/faq/faq-page.component.ts + 47 + + + apps/client/src/app/pages/faq/self-hosting/self-hosting-page-routing.module.ts + 13 + + + + Data Gathering + 数据收集 + + apps/client/src/app/components/admin-overview/admin-overview.html + 134 + + + + General + 一般的 + + apps/client/src/app/pages/faq/faq-page.component.ts + 36 + + + + Cloud + + + apps/client/src/app/pages/faq/faq-page.component.ts + 41 + + + apps/client/src/app/pages/faq/saas/saas-page-routing.module.ts + 13 + + + + Oops! It looks like you’re making too many requests. Please slow down a bit. + 哎呀!看来您提出了太多要求。请慢一点。 + + apps/client/src/app/core/http-response.interceptor.ts + 105 + + + + My Account + 我的账户 + + apps/client/src/app/pages/i18n/i18n-page.html + 13 + + + + Closed + 关闭 + + apps/client/src/app/pages/portfolio/holdings/holdings-page.component.ts + 31 + + + + Active + 积极的 + + apps/client/src/app/pages/portfolio/holdings/holdings-page.component.ts + 30 + + + + Activity + 活动 + + apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html + 176 + + From 084467ee9adce35f2099d4e8ea1119e55ff40cb7 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 31 Mar 2024 11:24:50 +0200 Subject: [PATCH 083/120] Feature/reverse order of specific years in date range selector of assistant (#3221) * Reverse order * Update changelog --- CHANGELOG.md | 6 +++++- libs/ui/src/lib/assistant/assistant.component.ts | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39cefd95f..ce6ec2a7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,12 +11,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Set up the language localization for Chinese (`zh`) +### Changed + +- Improved the usability of the date range support by specific years (`2023`, `2022`, `2021`, etc.) in the assistant (experimental) + ## 2.69.0 - 2024-03-30 ### Added - Added the date range support in the activities table on the portfolio activities page (experimental) -- Extended the date range support by specific years (`2023`, `2022`, `2021`, etc.) in the assistant (experimental) +- Extended the date range support by specific years (`2021`, `2022`, `2023`, etc.) in the assistant (experimental) - Set up `Tini` to avoid zombie processes and perform signal forwarding in docker image ### Changed diff --git a/libs/ui/src/lib/assistant/assistant.component.ts b/libs/ui/src/lib/assistant/assistant.component.ts index bd8355125..310bede05 100644 --- a/libs/ui/src/lib/assistant/assistant.component.ts +++ b/libs/ui/src/lib/assistant/assistant.component.ts @@ -219,6 +219,7 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit { return { label: format(date, 'yyyy'), value: format(date, 'yyyy') }; }) .slice(0, -1) + .reverse() ); } From 34997f91db6faac15821d6176a799c65360638f9 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 31 Mar 2024 11:38:09 +0200 Subject: [PATCH 084/120] Feature/setup webpack bundle analyzer (#3222) * Set up Webpack Bundle Analyzer * Update changelog --- CHANGELOG.md | 1 + package.json | 4 ++- yarn.lock | 78 +++++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce6ec2a7d..1ce4ef296 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Set up the language localization for Chinese (`zh`) +- Set up _Webpack Bundle Analyzer_ ### Changed diff --git a/package.json b/package.json index aa9bf0fd0..05ffd3e49 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "affected:libs": "nx affected:libs", "affected:lint": "nx affected:lint", "affected:test": "nx affected:test", + "analyze:client": "nx run client:build:production --stats-json && webpack-bundle-analyzer -p 1234 dist/apps/client/en/stats.json", "angular": "node --max_old_space_size=32768 ./node_modules/@angular/cli/bin/ng", "build:production": "nx run api:copy-assets && nx run api:build:production && nx run client:copy-assets && nx run client:build:production && yarn replace-placeholders-in-build", "build:storybook": "nx run ui:build-storybook", @@ -197,7 +198,8 @@ "ts-jest": "29.1.0", "ts-node": "10.9.1", "tslib": "2.6.0", - "typescript": "5.3.3" + "typescript": "5.3.3", + "webpack-bundle-analyzer": "4.10.1" }, "engines": { "node": ">=18" diff --git a/yarn.lock b/yarn.lock index 03e4cca81..ee83c3606 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5352,6 +5352,11 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== +"@polka/url@^1.0.0-next.24": + version "1.0.0-next.25" + resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.25.tgz#f077fdc0b5d0078d30893396ff4827a13f99e817" + integrity sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ== + "@prisma/client@5.11.0": version "5.11.0" resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.11.0.tgz#d8e55fab85163415b2245fb408b9106f83c8106d" @@ -8039,11 +8044,21 @@ acorn-jsx@^5.3.2: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== +acorn-walk@^8.0.0: + version "8.3.2" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.2.tgz#7703af9415f1b6db9315d6895503862e231d34aa" + integrity sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A== + acorn-walk@^8.0.2, acorn-walk@^8.1.1: version "8.2.0" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== +acorn@^8.0.4: + version "8.11.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" + integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== + acorn@^8.1.0, acorn@^8.10.0, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8.1, acorn@^8.8.2, acorn@^8.9.0: version "8.10.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" @@ -10474,6 +10489,11 @@ dayjs@^1.11.7: resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0" integrity sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ== +debounce@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" + integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug== + debug@2.6.9, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -10849,7 +10869,7 @@ dotenv@^16.0.0, dotenv@~16.3.1: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== -duplexer@^0.1.1: +duplexer@^0.1.1, duplexer@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== @@ -12455,6 +12475,13 @@ gunzip-maybe@^1.4.2: pumpify "^1.3.3" through2 "^2.0.3" +gzip-size@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462" + integrity sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q== + dependencies: + duplexer "^0.1.2" + handle-thing@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" @@ -12596,7 +12623,7 @@ html-entities@^2.1.0, html-entities@^2.3.2: resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.4.0.tgz#edd0cee70402584c8c76cc2c0556db09d1f45061" integrity sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ== -html-escaper@^2.0.0: +html-escaper@^2.0.0, html-escaper@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== @@ -13207,6 +13234,11 @@ is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" +is-plain-object@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" + integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== + is-potential-custom-element-name@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" @@ -15122,7 +15154,7 @@ mri@^1.1.0, mri@^1.2.0: resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== -mrmime@2.0.0: +mrmime@2.0.0, mrmime@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-2.0.0.tgz#151082a6e06e59a9a39b46b3e14d5cfe92b3abb4" integrity sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw== @@ -15693,7 +15725,7 @@ open@^7.0.3: is-docker "^2.0.0" is-wsl "^2.1.1" -opener@^1.5.1: +opener@^1.5.1, opener@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== @@ -17649,6 +17681,15 @@ simple-update-notifier@^2.0.0: dependencies: semver "^7.5.3" +sirv@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/sirv/-/sirv-2.0.4.tgz#5dd9a725c578e34e449f332703eb2a74e46a29b0" + integrity sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ== + dependencies: + "@polka/url" "^1.0.0-next.24" + mrmime "^2.0.0" + totalist "^3.0.0" + sisteransi@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" @@ -18377,6 +18418,11 @@ toidentifier@1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== +totalist@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.1.tgz#ba3a3d600c915b1a97872348f79c127475f6acf8" + integrity sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ== + tough-cookie-file-store@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/tough-cookie-file-store/-/tough-cookie-file-store-2.0.3.tgz#788f7a6fe5cd8f61a1afb71b2f0b964ebf914b80" @@ -19080,6 +19126,25 @@ webidl-conversions@^7.0.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== +webpack-bundle-analyzer@4.10.1: + version "4.10.1" + resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.1.tgz#84b7473b630a7b8c21c741f81d8fe4593208b454" + integrity sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ== + dependencies: + "@discoveryjs/json-ext" "0.5.7" + acorn "^8.0.4" + acorn-walk "^8.0.0" + commander "^7.2.0" + debounce "^1.2.1" + escape-string-regexp "^4.0.0" + gzip-size "^6.0.0" + html-escaper "^2.0.2" + is-plain-object "^5.0.0" + opener "^1.5.2" + picocolors "^1.0.0" + sirv "^2.0.3" + ws "^7.3.1" + webpack-dev-middleware@6.1.1, webpack-dev-middleware@^6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-6.1.1.tgz#6bbc257ec83ae15522de7a62f995630efde7cc3d" @@ -19409,6 +19474,11 @@ ws@^6.1.0: dependencies: async-limiter "~1.0.0" +ws@^7.3.1: + version "7.5.9" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" + integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== + ws@^8.11.0, ws@^8.13.0, ws@^8.2.3: version "8.14.2" resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.2.tgz#6c249a806eb2db7a20d26d51e7709eab7b2e6c7f" From 0c684748025d26e0b22182ea71f98288d67ae668 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 31 Mar 2024 11:41:44 +0200 Subject: [PATCH 085/120] Extract locales (#3223) --- apps/client/src/locales/messages.de.xlf | 200 ++++++++------- apps/client/src/locales/messages.es.xlf | 200 ++++++++------- apps/client/src/locales/messages.fr.xlf | 200 ++++++++------- apps/client/src/locales/messages.it.xlf | 200 ++++++++------- apps/client/src/locales/messages.nl.xlf | 200 ++++++++------- apps/client/src/locales/messages.pl.xlf | 200 ++++++++------- apps/client/src/locales/messages.pt.xlf | 200 ++++++++------- apps/client/src/locales/messages.tr.xlf | 200 ++++++++------- apps/client/src/locales/messages.xlf | 200 ++++++++------- apps/client/src/locales/messages.zh.xlf | 318 ++++++++++++------------ 10 files changed, 1099 insertions(+), 1019 deletions(-) diff --git a/apps/client/src/locales/messages.de.xlf b/apps/client/src/locales/messages.de.xlf index bbe0d1edd..58b55bc01 100644 --- a/apps/client/src/locales/messages.de.xlf +++ b/apps/client/src/locales/messages.de.xlf @@ -22,7 +22,7 @@ Das Ausfallrisiko beim Börsenhandel kann erheblich sein. Es ist nicht ratsam, Geld zu investieren, welches du kurzfristig benötigst. apps/client/src/app/app.component.html - 179 + 184 @@ -94,7 +94,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 122 + 139 apps/client/src/app/components/admin-tag/admin-tag.component.html @@ -130,7 +130,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 197 + 214 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -270,7 +270,11 @@ apps/client/src/app/components/admin-market-data/admin-market-data.html - 190 + 194 + + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 62 apps/client/src/app/components/admin-overview/admin-overview.html @@ -326,7 +330,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 98 + 115 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -474,7 +478,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 330 + 347 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -490,7 +494,7 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 60 + 58 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -518,7 +522,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 337 + 354 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -534,7 +538,7 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 67 + 65 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -554,7 +558,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 113 + 130 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -582,7 +586,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 65 + 82 @@ -1492,11 +1496,11 @@ Sektoren apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 173 + 190 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 295 + 312 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -1512,11 +1516,11 @@ Länder apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 183 + 200 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 306 + 323 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -1592,7 +1596,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 99 + 188 @@ -1604,7 +1608,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 109 + 198 @@ -1616,7 +1620,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 112 + 202 @@ -1628,7 +1632,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 114 + 206 @@ -1640,7 +1644,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 117 + 209 @@ -1876,7 +1880,7 @@ Möchtest du diese Anmeldemethode wirklich löschen? apps/client/src/app/components/user-account-settings/user-account-settings.component.ts - 188 + 189 @@ -1948,7 +1952,7 @@ Lokalität apps/client/src/app/components/user-account-settings/user-account-settings.html - 113 + 121 @@ -1956,7 +1960,7 @@ Datums- und Zahlenformat apps/client/src/app/components/user-account-settings/user-account-settings.html - 115 + 123 @@ -1964,7 +1968,7 @@ Zen Modus apps/client/src/app/components/user-account-settings/user-account-settings.html - 163 + 171 apps/client/src/app/pages/features/features-page.html @@ -1976,7 +1980,7 @@ Einloggen mit Fingerabdruck apps/client/src/app/components/user-account-settings/user-account-settings.html - 181 + 189 @@ -1984,11 +1988,11 @@ Benutzer ID apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 47 + 45 apps/client/src/app/components/user-account-settings/user-account-settings.html - 215 + 223 @@ -2056,11 +2060,11 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 103 + 120 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 203 + 220 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -2496,7 +2500,7 @@ Kommentar apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 317 + 334 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -2516,11 +2520,11 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 131 + 148 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 212 + 229 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2572,7 +2576,7 @@ Portfolio apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts - 110 + 116 apps/client/src/app/pages/portfolio/portfolio-page-routing.module.ts @@ -2880,11 +2884,11 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 140 + 157 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 225 + 242 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2900,7 +2904,7 @@ Sektor apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 159 + 176 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2912,7 +2916,7 @@ Land apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 168 + 185 apps/client/src/app/components/admin-users/admin-users.html @@ -2972,7 +2976,7 @@ Monatlich apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 54 + 55 @@ -3048,7 +3052,7 @@ Filtern nach... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 282 + 281 @@ -3076,7 +3080,7 @@ Experimentelle Funktionen apps/client/src/app/components/user-account-settings/user-account-settings.html - 198 + 206 @@ -3092,7 +3096,7 @@ Benchmark apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts - 119 + 128 @@ -3124,7 +3128,7 @@ Aussehen apps/client/src/app/components/user-account-settings/user-account-settings.html - 138 + 146 @@ -3132,7 +3136,7 @@ Automatisch apps/client/src/app/components/user-account-settings/user-account-settings.html - 152 + 160 @@ -3140,7 +3144,7 @@ Hell apps/client/src/app/components/user-account-settings/user-account-settings.html - 153 + 161 @@ -3148,7 +3152,7 @@ Dunkel apps/client/src/app/components/user-account-settings/user-account-settings.html - 154 + 162 @@ -3156,7 +3160,7 @@ Gesamtbetrag apps/client/src/app/components/investment-chart/investment-chart.component.ts - 191 + 142 @@ -3172,7 +3176,7 @@ Sparrate apps/client/src/app/components/investment-chart/investment-chart.component.ts - 263 + 214 @@ -3412,31 +3416,35 @@ apps/client/src/app/components/user-account-settings/user-account-settings.html - 80 + 81 apps/client/src/app/components/user-account-settings/user-account-settings.html - 84 + 86 apps/client/src/app/components/user-account-settings/user-account-settings.html - 88 + 90 apps/client/src/app/components/user-account-settings/user-account-settings.html - 92 + 94 apps/client/src/app/components/user-account-settings/user-account-settings.html - 96 + 98 apps/client/src/app/components/user-account-settings/user-account-settings.html - 100 + 103 apps/client/src/app/components/user-account-settings/user-account-settings.html - 104 + 108 + + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 112 apps/client/src/app/pages/features/features-page.html @@ -3464,7 +3472,7 @@ Symbol Zuordnung apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 259 + 276 @@ -3480,7 +3488,7 @@ Dividenden apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 42 + 43 libs/ui/src/lib/i18n.ts @@ -3516,7 +3524,7 @@ Importieren apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 91 + 108 apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html @@ -3584,7 +3592,7 @@ Jährlich apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 55 + 56 @@ -3664,7 +3672,7 @@ Unbeschwertes Erlebnis für turbulente Zeiten apps/client/src/app/components/user-account-settings/user-account-settings.html - 164 + 172 @@ -3672,7 +3680,7 @@ Vorschau auf kommende Funktionalität apps/client/src/app/components/user-account-settings/user-account-settings.html - 199 + 207 @@ -4100,7 +4108,7 @@ Möchtest du wirklich alle Aktivitäten löschen? apps/client/src/app/pages/portfolio/activities/activities-page.component.ts - 168 + 171 @@ -4524,7 +4532,7 @@ Scraper Konfiguration apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 270 + 287 @@ -9868,7 +9876,7 @@ ETFs ohne Länder apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 72 + 77 @@ -9876,7 +9884,7 @@ ETFs ohne Sektoren apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 77 + 82 @@ -10008,7 +10016,7 @@ Biometrische Authentifizierung apps/client/src/app/components/user-account-settings/user-account-settings.html - 180 + 188 @@ -10092,7 +10100,7 @@ Daten exportieren apps/client/src/app/components/user-account-settings/user-account-settings.html - 223 + 231 @@ -10100,7 +10108,7 @@ Währungen apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 67 + 72 @@ -10480,7 +10488,7 @@ apps/client/src/app/app.component.ts - 54 + 55 apps/client/src/app/pages/about/overview/about-overview-page.component.ts @@ -10512,7 +10520,7 @@ apps/client/src/app/app.component.ts - 55 + 56 apps/client/src/app/components/header/header.component.ts @@ -10772,19 +10780,19 @@ apps/client/src/app/app.component.ts - 47 + 48 apps/client/src/app/app.component.ts - 48 + 49 apps/client/src/app/app.component.ts - 49 + 50 apps/client/src/app/app.component.ts - 51 + 52 apps/client/src/app/components/header/header.component.ts @@ -11052,7 +11060,7 @@ apps/client/src/app/app.component.ts - 52 + 53 apps/client/src/app/pages/about/about-page.component.ts @@ -11068,7 +11076,7 @@ apps/client/src/app/app.component.ts - 49 + 50 apps/client/src/app/pages/about/about-page.component.ts @@ -11084,7 +11092,7 @@ apps/client/src/app/app.component.ts - 56 + 57 apps/client/src/app/components/header/header.component.ts @@ -11116,7 +11124,7 @@ apps/client/src/app/app.component.ts - 57 + 58 apps/client/src/app/components/header/header.component.ts @@ -11188,7 +11196,7 @@ apps/client/src/app/app.component.ts - 58 + 59 apps/client/src/app/components/header/header.component.ts @@ -11224,7 +11232,7 @@ apps/client/src/app/app.component.ts - 59 + 60 apps/client/src/app/components/header/header.component.ts @@ -13344,7 +13352,7 @@ Benchmark apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 253 + 270 @@ -13424,7 +13432,7 @@ Finde Position... libs/ui/src/lib/assistant/assistant.component.ts - 126 + 111 @@ -13451,8 +13459,8 @@ Do you really want to delete this asset profile? Möchtest du dieses Anlageprofil wirklich löschen? - apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 185 + apps/client/src/app/components/admin-market-data/admin-market-data.service.ts + 13 @@ -13776,7 +13784,7 @@ Ups! Die historischen Daten konnten nicht geparsed werden. apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts - 223 + 233 @@ -14648,7 +14656,7 @@ Der aktuelle Marktpreis ist apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts - 317 + 327 @@ -14656,7 +14664,7 @@ Test apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 288 + 305 @@ -14732,11 +14740,11 @@ Einlage apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 46 + 47 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 60 + 61 @@ -14792,7 +14800,7 @@ Seit Wochenbeginn libs/ui/src/lib/assistant/assistant.component.ts - 101 + 190 @@ -14800,7 +14808,7 @@ WTD libs/ui/src/lib/assistant/assistant.component.ts - 101 + 190 @@ -14808,7 +14816,7 @@ Seit Monatsbeginn libs/ui/src/lib/assistant/assistant.component.ts - 105 + 194 @@ -14816,7 +14824,7 @@ MTD libs/ui/src/lib/assistant/assistant.component.ts - 105 + 194 @@ -14824,7 +14832,7 @@ Seit Jahresbeginn libs/ui/src/lib/assistant/assistant.component.ts - 109 + 198 @@ -14836,7 +14844,7 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 39 + 38 @@ -14868,7 +14876,7 @@ Jahr libs/ui/src/lib/assistant/assistant.component.ts - 112 + 202 @@ -14876,7 +14884,7 @@ Jahre libs/ui/src/lib/assistant/assistant.component.ts - 114 + 206 diff --git a/apps/client/src/locales/messages.es.xlf b/apps/client/src/locales/messages.es.xlf index 6535d4034..6f4bd17fc 100644 --- a/apps/client/src/locales/messages.es.xlf +++ b/apps/client/src/locales/messages.es.xlf @@ -23,7 +23,7 @@ El riesgo de pérdida en trading puede ser importante. No es aconsejable invertir dinero que puedas necesitar a corto plazo. apps/client/src/app/app.component.html - 179 + 184 @@ -95,7 +95,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 122 + 139 apps/client/src/app/components/admin-tag/admin-tag.component.html @@ -131,7 +131,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 197 + 214 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -271,7 +271,11 @@ apps/client/src/app/components/admin-market-data/admin-market-data.html - 190 + 194 + + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 62 apps/client/src/app/components/admin-overview/admin-overview.html @@ -327,7 +331,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 98 + 115 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -475,7 +479,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 330 + 347 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -491,7 +495,7 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 60 + 58 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -519,7 +523,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 337 + 354 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -535,7 +539,7 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 67 + 65 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -555,7 +559,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 113 + 130 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -583,7 +587,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 65 + 82 @@ -1490,11 +1494,11 @@ Sectores apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 173 + 190 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 295 + 312 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -1510,11 +1514,11 @@ Países apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 183 + 200 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 306 + 323 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -1590,7 +1594,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 99 + 188 @@ -1602,7 +1606,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 109 + 198 @@ -1614,7 +1618,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 112 + 202 @@ -1626,7 +1630,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 114 + 206 @@ -1638,7 +1642,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 117 + 209 @@ -1874,7 +1878,7 @@ ¿Estás seguro de eliminar este método de acceso? apps/client/src/app/components/user-account-settings/user-account-settings.component.ts - 188 + 189 @@ -1946,7 +1950,7 @@ Ubicación apps/client/src/app/components/user-account-settings/user-account-settings.html - 113 + 121 @@ -1954,7 +1958,7 @@ Formato de fecha y número apps/client/src/app/components/user-account-settings/user-account-settings.html - 115 + 123 @@ -1962,7 +1966,7 @@ Modo Zen apps/client/src/app/components/user-account-settings/user-account-settings.html - 163 + 171 apps/client/src/app/pages/features/features-page.html @@ -1974,7 +1978,7 @@ Accede con huella digital apps/client/src/app/components/user-account-settings/user-account-settings.html - 181 + 189 @@ -1982,11 +1986,11 @@ ID usuario apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 47 + 45 apps/client/src/app/components/user-account-settings/user-account-settings.html - 215 + 223 @@ -2054,11 +2058,11 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 103 + 120 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 203 + 220 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -2494,7 +2498,7 @@ Nota apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 317 + 334 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -2514,11 +2518,11 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 131 + 148 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 212 + 229 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2570,7 +2574,7 @@ Cartera apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts - 110 + 116 apps/client/src/app/pages/portfolio/portfolio-page-routing.module.ts @@ -2866,11 +2870,11 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 140 + 157 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 225 + 242 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2926,7 +2930,7 @@ Sector apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 159 + 176 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2938,7 +2942,7 @@ País apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 168 + 185 apps/client/src/app/components/admin-users/admin-users.html @@ -2998,7 +3002,7 @@ Mensual apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 54 + 55 @@ -3046,7 +3050,7 @@ Filtrar por... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 282 + 281 @@ -3074,7 +3078,7 @@ Funcionalidades experimentales apps/client/src/app/components/user-account-settings/user-account-settings.html - 198 + 206 @@ -3082,7 +3086,7 @@ Benchmark apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts - 119 + 128 @@ -3122,7 +3126,7 @@ Apariencia apps/client/src/app/components/user-account-settings/user-account-settings.html - 138 + 146 @@ -3130,7 +3134,7 @@ Automático apps/client/src/app/components/user-account-settings/user-account-settings.html - 152 + 160 @@ -3138,7 +3142,7 @@ Claro apps/client/src/app/components/user-account-settings/user-account-settings.html - 153 + 161 @@ -3146,7 +3150,7 @@ Oscuro apps/client/src/app/components/user-account-settings/user-account-settings.html - 154 + 162 @@ -3154,7 +3158,7 @@ Importe total apps/client/src/app/components/investment-chart/investment-chart.component.ts - 191 + 142 @@ -3170,7 +3174,7 @@ Tasa de ahorro apps/client/src/app/components/investment-chart/investment-chart.component.ts - 263 + 214 @@ -3410,31 +3414,35 @@ apps/client/src/app/components/user-account-settings/user-account-settings.html - 80 + 81 apps/client/src/app/components/user-account-settings/user-account-settings.html - 84 + 86 apps/client/src/app/components/user-account-settings/user-account-settings.html - 88 + 90 apps/client/src/app/components/user-account-settings/user-account-settings.html - 92 + 94 apps/client/src/app/components/user-account-settings/user-account-settings.html - 96 + 98 apps/client/src/app/components/user-account-settings/user-account-settings.html - 100 + 103 apps/client/src/app/components/user-account-settings/user-account-settings.html - 104 + 108 + + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 112 apps/client/src/app/pages/features/features-page.html @@ -3462,7 +3470,7 @@ Mapeo de símbolos apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 259 + 276 @@ -3470,7 +3478,7 @@ Dividend apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 42 + 43 libs/ui/src/lib/i18n.ts @@ -3514,7 +3522,7 @@ Import apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 91 + 108 apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html @@ -3582,7 +3590,7 @@ Yearly apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 55 + 56 @@ -3662,7 +3670,7 @@ Distraction-free experience for turbulent times apps/client/src/app/components/user-account-settings/user-account-settings.html - 164 + 172 @@ -3670,7 +3678,7 @@ Sneak peek at upcoming functionality apps/client/src/app/components/user-account-settings/user-account-settings.html - 199 + 207 @@ -4098,7 +4106,7 @@ Do you really want to delete all your activities? apps/client/src/app/pages/portfolio/activities/activities-page.component.ts - 168 + 171 @@ -4522,7 +4530,7 @@ Scraper Configuration apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 270 + 287 @@ -9866,7 +9874,7 @@ ETFs without Countries apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 72 + 77 @@ -9874,7 +9882,7 @@ ETFs without Sectors apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 77 + 82 @@ -10006,7 +10014,7 @@ Biometric Authentication apps/client/src/app/components/user-account-settings/user-account-settings.html - 180 + 188 @@ -10090,7 +10098,7 @@ Export Data apps/client/src/app/components/user-account-settings/user-account-settings.html - 223 + 231 @@ -10098,7 +10106,7 @@ Currencies apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 67 + 72 @@ -10478,7 +10486,7 @@ apps/client/src/app/app.component.ts - 54 + 55 apps/client/src/app/pages/about/overview/about-overview-page.component.ts @@ -10510,7 +10518,7 @@ apps/client/src/app/app.component.ts - 55 + 56 apps/client/src/app/components/header/header.component.ts @@ -10770,19 +10778,19 @@ apps/client/src/app/app.component.ts - 47 + 48 apps/client/src/app/app.component.ts - 48 + 49 apps/client/src/app/app.component.ts - 49 + 50 apps/client/src/app/app.component.ts - 51 + 52 apps/client/src/app/components/header/header.component.ts @@ -11050,7 +11058,7 @@ apps/client/src/app/app.component.ts - 52 + 53 apps/client/src/app/pages/about/about-page.component.ts @@ -11066,7 +11074,7 @@ apps/client/src/app/app.component.ts - 49 + 50 apps/client/src/app/pages/about/about-page.component.ts @@ -11082,7 +11090,7 @@ apps/client/src/app/app.component.ts - 56 + 57 apps/client/src/app/components/header/header.component.ts @@ -11114,7 +11122,7 @@ apps/client/src/app/app.component.ts - 57 + 58 apps/client/src/app/components/header/header.component.ts @@ -11186,7 +11194,7 @@ apps/client/src/app/app.component.ts - 58 + 59 apps/client/src/app/components/header/header.component.ts @@ -11222,7 +11230,7 @@ apps/client/src/app/app.component.ts - 59 + 60 apps/client/src/app/components/header/header.component.ts @@ -13342,7 +13350,7 @@ Benchmark apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 253 + 270 @@ -13422,7 +13430,7 @@ Find holding... libs/ui/src/lib/assistant/assistant.component.ts - 126 + 111 @@ -13449,8 +13457,8 @@ Do you really want to delete this asset profile? Do you really want to delete this asset profile? - apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 185 + apps/client/src/app/components/admin-market-data/admin-market-data.service.ts + 13 @@ -13774,7 +13782,7 @@ Oops! Could not parse historical data. apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts - 223 + 233 @@ -14646,7 +14654,7 @@ The current market price is apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts - 317 + 327 @@ -14654,7 +14662,7 @@ Test apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 288 + 305 @@ -14730,11 +14738,11 @@ Investment apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 46 + 47 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 60 + 61 @@ -14790,7 +14798,7 @@ Week to date libs/ui/src/lib/assistant/assistant.component.ts - 101 + 190 @@ -14798,7 +14806,7 @@ WTD libs/ui/src/lib/assistant/assistant.component.ts - 101 + 190 @@ -14806,7 +14814,7 @@ Month to date libs/ui/src/lib/assistant/assistant.component.ts - 105 + 194 @@ -14814,7 +14822,7 @@ MTD libs/ui/src/lib/assistant/assistant.component.ts - 105 + 194 @@ -14822,7 +14830,7 @@ Year to date libs/ui/src/lib/assistant/assistant.component.ts - 109 + 198 @@ -14834,7 +14842,7 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 39 + 38 @@ -14866,7 +14874,7 @@ year libs/ui/src/lib/assistant/assistant.component.ts - 112 + 202 @@ -14874,7 +14882,7 @@ years libs/ui/src/lib/assistant/assistant.component.ts - 114 + 206 diff --git a/apps/client/src/locales/messages.fr.xlf b/apps/client/src/locales/messages.fr.xlf index 9faea802b..fd2fdae57 100644 --- a/apps/client/src/locales/messages.fr.xlf +++ b/apps/client/src/locales/messages.fr.xlf @@ -6,7 +6,7 @@ Le risque de perte en investissant peut être important. Il est déconseillé d'investir de l'argent dont vous pourriez avoir besoin à court terme. apps/client/src/app/app.component.html - 179 + 184 @@ -106,7 +106,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 122 + 139 apps/client/src/app/components/admin-tag/admin-tag.component.html @@ -142,7 +142,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 197 + 214 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -194,11 +194,11 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 103 + 120 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 203 + 220 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -326,7 +326,11 @@ apps/client/src/app/components/admin-market-data/admin-market-data.html - 190 + 194 + + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 62 apps/client/src/app/components/admin-overview/admin-overview.html @@ -374,7 +378,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 98 + 115 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -530,7 +534,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 330 + 347 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -546,7 +550,7 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 60 + 58 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -574,7 +578,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 337 + 354 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -590,7 +594,7 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 67 + 65 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -606,7 +610,7 @@ Filtrer par... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 282 + 281 @@ -618,11 +622,11 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 131 + 148 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 212 + 229 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -642,11 +646,11 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 140 + 157 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 225 + 242 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -666,7 +670,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 113 + 130 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -694,7 +698,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 65 + 82 @@ -754,7 +758,7 @@ Secteur apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 159 + 176 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -766,7 +770,7 @@ Pays apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 168 + 185 apps/client/src/app/components/admin-users/admin-users.html @@ -782,11 +786,11 @@ Secteurs apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 173 + 190 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 295 + 312 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -802,11 +806,11 @@ Pays apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 183 + 200 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 306 + 323 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -818,7 +822,7 @@ Équivalence de Symboles apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 259 + 276 @@ -826,7 +830,7 @@ Note apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 317 + 334 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -1102,7 +1106,7 @@ Portefeuille apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts - 110 + 116 apps/client/src/app/pages/portfolio/portfolio-page-routing.module.ts @@ -1114,7 +1118,7 @@ Référence apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts - 119 + 128 @@ -1602,7 +1606,7 @@ Montant Total apps/client/src/app/components/investment-chart/investment-chart.component.ts - 191 + 142 @@ -1610,7 +1614,7 @@ Taux d'Épargne apps/client/src/app/components/investment-chart/investment-chart.component.ts - 263 + 214 @@ -1921,7 +1925,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 99 + 188 @@ -1933,7 +1937,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 109 + 198 @@ -1945,7 +1949,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 112 + 202 @@ -1957,7 +1961,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 114 + 206 @@ -1969,7 +1973,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 117 + 209 @@ -2137,7 +2141,7 @@ Voulez-vous vraiment supprimer cette méthode de connexion ? apps/client/src/app/components/user-account-settings/user-account-settings.component.ts - 188 + 189 @@ -2221,31 +2225,35 @@ apps/client/src/app/components/user-account-settings/user-account-settings.html - 80 + 81 apps/client/src/app/components/user-account-settings/user-account-settings.html - 84 + 86 apps/client/src/app/components/user-account-settings/user-account-settings.html - 88 + 90 apps/client/src/app/components/user-account-settings/user-account-settings.html - 92 + 94 apps/client/src/app/components/user-account-settings/user-account-settings.html - 96 + 98 apps/client/src/app/components/user-account-settings/user-account-settings.html - 100 + 103 apps/client/src/app/components/user-account-settings/user-account-settings.html - 104 + 108 + + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 112 apps/client/src/app/pages/features/features-page.html @@ -2257,7 +2265,7 @@ Paramètres régionaux apps/client/src/app/components/user-account-settings/user-account-settings.html - 113 + 121 @@ -2265,7 +2273,7 @@ Format de date et d'heure apps/client/src/app/components/user-account-settings/user-account-settings.html - 115 + 123 @@ -2273,7 +2281,7 @@ Apparence apps/client/src/app/components/user-account-settings/user-account-settings.html - 138 + 146 @@ -2281,7 +2289,7 @@ Auto apps/client/src/app/components/user-account-settings/user-account-settings.html - 152 + 160 @@ -2289,7 +2297,7 @@ Clair apps/client/src/app/components/user-account-settings/user-account-settings.html - 153 + 161 @@ -2297,7 +2305,7 @@ Sombre apps/client/src/app/components/user-account-settings/user-account-settings.html - 154 + 162 @@ -2305,7 +2313,7 @@ Mode Zen apps/client/src/app/components/user-account-settings/user-account-settings.html - 163 + 171 apps/client/src/app/pages/features/features-page.html @@ -2317,7 +2325,7 @@ Se connecter avec empreinte apps/client/src/app/components/user-account-settings/user-account-settings.html - 181 + 189 @@ -2325,7 +2333,7 @@ Fonctionnalités expérimentales apps/client/src/app/components/user-account-settings/user-account-settings.html - 198 + 206 @@ -2333,11 +2341,11 @@ ID d'utilisateur apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 47 + 45 apps/client/src/app/components/user-account-settings/user-account-settings.html - 215 + 223 @@ -2745,7 +2753,7 @@ Importer apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 91 + 108 apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html @@ -2905,7 +2913,7 @@ Dividende apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 42 + 43 libs/ui/src/lib/i18n.ts @@ -2925,7 +2933,7 @@ Mensuel apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 54 + 55 @@ -3581,7 +3589,7 @@ Annuel apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 55 + 56 @@ -3661,7 +3669,7 @@ Expérience sans distraction pour les périodes tumultueuses apps/client/src/app/components/user-account-settings/user-account-settings.html - 164 + 172 @@ -3669,7 +3677,7 @@ Avant-première de fonctionnalités futures apps/client/src/app/components/user-account-settings/user-account-settings.html - 199 + 207 @@ -4097,7 +4105,7 @@ Voulez-vous vraiment supprimer toutes vos activités ? apps/client/src/app/pages/portfolio/activities/activities-page.component.ts - 168 + 171 @@ -4521,7 +4529,7 @@ Scraper Configuration apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 270 + 287 @@ -9865,7 +9873,7 @@ ETFs without Countries apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 72 + 77 @@ -9873,7 +9881,7 @@ ETFs without Sectors apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 77 + 82 @@ -10005,7 +10013,7 @@ Biometric Authentication apps/client/src/app/components/user-account-settings/user-account-settings.html - 180 + 188 @@ -10089,7 +10097,7 @@ Export Data apps/client/src/app/components/user-account-settings/user-account-settings.html - 223 + 231 @@ -10097,7 +10105,7 @@ Currencies apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 67 + 72 @@ -10477,7 +10485,7 @@ apps/client/src/app/app.component.ts - 54 + 55 apps/client/src/app/pages/about/overview/about-overview-page.component.ts @@ -10509,7 +10517,7 @@ apps/client/src/app/app.component.ts - 55 + 56 apps/client/src/app/components/header/header.component.ts @@ -10769,19 +10777,19 @@ apps/client/src/app/app.component.ts - 47 + 48 apps/client/src/app/app.component.ts - 48 + 49 apps/client/src/app/app.component.ts - 49 + 50 apps/client/src/app/app.component.ts - 51 + 52 apps/client/src/app/components/header/header.component.ts @@ -11049,7 +11057,7 @@ apps/client/src/app/app.component.ts - 52 + 53 apps/client/src/app/pages/about/about-page.component.ts @@ -11065,7 +11073,7 @@ apps/client/src/app/app.component.ts - 49 + 50 apps/client/src/app/pages/about/about-page.component.ts @@ -11081,7 +11089,7 @@ apps/client/src/app/app.component.ts - 56 + 57 apps/client/src/app/components/header/header.component.ts @@ -11113,7 +11121,7 @@ apps/client/src/app/app.component.ts - 57 + 58 apps/client/src/app/components/header/header.component.ts @@ -11185,7 +11193,7 @@ apps/client/src/app/app.component.ts - 58 + 59 apps/client/src/app/components/header/header.component.ts @@ -11221,7 +11229,7 @@ apps/client/src/app/app.component.ts - 59 + 60 apps/client/src/app/components/header/header.component.ts @@ -13341,7 +13349,7 @@ Benchmark apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 253 + 270 @@ -13421,7 +13429,7 @@ Find holding... libs/ui/src/lib/assistant/assistant.component.ts - 126 + 111 @@ -13448,8 +13456,8 @@ Do you really want to delete this asset profile? Do you really want to delete this asset profile? - apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 185 + apps/client/src/app/components/admin-market-data/admin-market-data.service.ts + 13 @@ -13773,7 +13781,7 @@ Oops! Could not parse historical data. apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts - 223 + 233 @@ -14645,7 +14653,7 @@ The current market price is apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts - 317 + 327 @@ -14653,7 +14661,7 @@ Test apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 288 + 305 @@ -14729,11 +14737,11 @@ Investment apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 46 + 47 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 60 + 61 @@ -14789,7 +14797,7 @@ Week to date libs/ui/src/lib/assistant/assistant.component.ts - 101 + 190 @@ -14797,7 +14805,7 @@ WTD libs/ui/src/lib/assistant/assistant.component.ts - 101 + 190 @@ -14805,7 +14813,7 @@ Month to date libs/ui/src/lib/assistant/assistant.component.ts - 105 + 194 @@ -14813,7 +14821,7 @@ MTD libs/ui/src/lib/assistant/assistant.component.ts - 105 + 194 @@ -14821,7 +14829,7 @@ Year to date libs/ui/src/lib/assistant/assistant.component.ts - 109 + 198 @@ -14833,7 +14841,7 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 39 + 38 @@ -14865,7 +14873,7 @@ year libs/ui/src/lib/assistant/assistant.component.ts - 112 + 202 @@ -14873,7 +14881,7 @@ years libs/ui/src/lib/assistant/assistant.component.ts - 114 + 206 diff --git a/apps/client/src/locales/messages.it.xlf b/apps/client/src/locales/messages.it.xlf index 581bdc55c..283dcd868 100644 --- a/apps/client/src/locales/messages.it.xlf +++ b/apps/client/src/locales/messages.it.xlf @@ -23,7 +23,7 @@ Il rischio di perdita nel trading può essere notevole. Non è consigliabile investire denaro di cui potresti avere bisogno a breve termine. apps/client/src/app/app.component.html - 179 + 184 @@ -95,7 +95,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 122 + 139 apps/client/src/app/components/admin-tag/admin-tag.component.html @@ -131,7 +131,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 197 + 214 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -271,7 +271,11 @@ apps/client/src/app/components/admin-market-data/admin-market-data.html - 190 + 194 + + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 62 apps/client/src/app/components/admin-overview/admin-overview.html @@ -327,7 +331,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 98 + 115 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -475,7 +479,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 330 + 347 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -491,7 +495,7 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 60 + 58 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -519,7 +523,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 337 + 354 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -535,7 +539,7 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 67 + 65 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -555,7 +559,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 113 + 130 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -583,7 +587,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 65 + 82 @@ -1490,11 +1494,11 @@ Settori apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 173 + 190 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 295 + 312 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -1510,11 +1514,11 @@ Paesi apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 183 + 200 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 306 + 323 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -1590,7 +1594,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 99 + 188 @@ -1602,7 +1606,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 109 + 198 @@ -1614,7 +1618,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 112 + 202 @@ -1626,7 +1630,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 114 + 206 @@ -1638,7 +1642,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 117 + 209 @@ -1874,7 +1878,7 @@ Vuoi davvero rimuovere questo metodo di accesso? apps/client/src/app/components/user-account-settings/user-account-settings.component.ts - 188 + 189 @@ -1946,7 +1950,7 @@ Locale apps/client/src/app/components/user-account-settings/user-account-settings.html - 113 + 121 @@ -1954,7 +1958,7 @@ Formato data e numero apps/client/src/app/components/user-account-settings/user-account-settings.html - 115 + 123 @@ -1962,7 +1966,7 @@ Modalità Zen apps/client/src/app/components/user-account-settings/user-account-settings.html - 163 + 171 apps/client/src/app/pages/features/features-page.html @@ -1974,7 +1978,7 @@ Accesso con impronta digitale apps/client/src/app/components/user-account-settings/user-account-settings.html - 181 + 189 @@ -1982,11 +1986,11 @@ ID utente apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 47 + 45 apps/client/src/app/components/user-account-settings/user-account-settings.html - 215 + 223 @@ -2054,11 +2058,11 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 103 + 120 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 203 + 220 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -2494,7 +2498,7 @@ Nota apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 317 + 334 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -2514,11 +2518,11 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 131 + 148 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 212 + 229 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2570,7 +2574,7 @@ Portafoglio apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts - 110 + 116 apps/client/src/app/pages/portfolio/portfolio-page-routing.module.ts @@ -2866,11 +2870,11 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 140 + 157 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 225 + 242 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2926,7 +2930,7 @@ Settore apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 159 + 176 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2938,7 +2942,7 @@ Paese apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 168 + 185 apps/client/src/app/components/admin-users/admin-users.html @@ -2998,7 +3002,7 @@ Mensile apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 54 + 55 @@ -3046,7 +3050,7 @@ Filtra per... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 282 + 281 @@ -3074,7 +3078,7 @@ Funzionalità sperimentali apps/client/src/app/components/user-account-settings/user-account-settings.html - 198 + 206 @@ -3082,7 +3086,7 @@ Benchmark apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts - 119 + 128 @@ -3122,7 +3126,7 @@ Aspetto apps/client/src/app/components/user-account-settings/user-account-settings.html - 138 + 146 @@ -3130,7 +3134,7 @@ Auto apps/client/src/app/components/user-account-settings/user-account-settings.html - 152 + 160 @@ -3138,7 +3142,7 @@ Chiaro apps/client/src/app/components/user-account-settings/user-account-settings.html - 153 + 161 @@ -3146,7 +3150,7 @@ Scuro apps/client/src/app/components/user-account-settings/user-account-settings.html - 154 + 162 @@ -3154,7 +3158,7 @@ Importo totale apps/client/src/app/components/investment-chart/investment-chart.component.ts - 191 + 142 @@ -3170,7 +3174,7 @@ Tasso di risparmio apps/client/src/app/components/investment-chart/investment-chart.component.ts - 263 + 214 @@ -3410,31 +3414,35 @@ apps/client/src/app/components/user-account-settings/user-account-settings.html - 80 + 81 apps/client/src/app/components/user-account-settings/user-account-settings.html - 84 + 86 apps/client/src/app/components/user-account-settings/user-account-settings.html - 88 + 90 apps/client/src/app/components/user-account-settings/user-account-settings.html - 92 + 94 apps/client/src/app/components/user-account-settings/user-account-settings.html - 96 + 98 apps/client/src/app/components/user-account-settings/user-account-settings.html - 100 + 103 apps/client/src/app/components/user-account-settings/user-account-settings.html - 104 + 108 + + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 112 apps/client/src/app/pages/features/features-page.html @@ -3462,7 +3470,7 @@ Mappatura dei simboli apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 259 + 276 @@ -3470,7 +3478,7 @@ Dividendi apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 42 + 43 libs/ui/src/lib/i18n.ts @@ -3514,7 +3522,7 @@ Importa apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 91 + 108 apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html @@ -3582,7 +3590,7 @@ Annuale apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 55 + 56 @@ -3662,7 +3670,7 @@ Esperienza priva di distrazioni per i periodi più turbolenti apps/client/src/app/components/user-account-settings/user-account-settings.html - 164 + 172 @@ -3670,7 +3678,7 @@ Un'anteprima delle funzionalità in arrivo apps/client/src/app/components/user-account-settings/user-account-settings.html - 199 + 207 @@ -4098,7 +4106,7 @@ Vuoi davvero eliminare tutte le tue attività? apps/client/src/app/pages/portfolio/activities/activities-page.component.ts - 168 + 171 @@ -4522,7 +4530,7 @@ Configurazione dello scraper apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 270 + 287 @@ -9866,7 +9874,7 @@ ETF senza paesi apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 72 + 77 @@ -9874,7 +9882,7 @@ ETF senza settori apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 77 + 82 @@ -10006,7 +10014,7 @@ Autenticazione biometrica apps/client/src/app/components/user-account-settings/user-account-settings.html - 180 + 188 @@ -10090,7 +10098,7 @@ Esporta dati apps/client/src/app/components/user-account-settings/user-account-settings.html - 223 + 231 @@ -10098,7 +10106,7 @@ Valute apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 67 + 72 @@ -10478,7 +10486,7 @@ apps/client/src/app/app.component.ts - 54 + 55 apps/client/src/app/pages/about/overview/about-overview-page.component.ts @@ -10510,7 +10518,7 @@ apps/client/src/app/app.component.ts - 55 + 56 apps/client/src/app/components/header/header.component.ts @@ -10770,19 +10778,19 @@ apps/client/src/app/app.component.ts - 47 + 48 apps/client/src/app/app.component.ts - 48 + 49 apps/client/src/app/app.component.ts - 49 + 50 apps/client/src/app/app.component.ts - 51 + 52 apps/client/src/app/components/header/header.component.ts @@ -11050,7 +11058,7 @@ apps/client/src/app/app.component.ts - 52 + 53 apps/client/src/app/pages/about/about-page.component.ts @@ -11066,7 +11074,7 @@ apps/client/src/app/app.component.ts - 49 + 50 apps/client/src/app/pages/about/about-page.component.ts @@ -11082,7 +11090,7 @@ apps/client/src/app/app.component.ts - 56 + 57 apps/client/src/app/components/header/header.component.ts @@ -11114,7 +11122,7 @@ apps/client/src/app/app.component.ts - 57 + 58 apps/client/src/app/components/header/header.component.ts @@ -11186,7 +11194,7 @@ apps/client/src/app/app.component.ts - 58 + 59 apps/client/src/app/components/header/header.component.ts @@ -11222,7 +11230,7 @@ apps/client/src/app/app.component.ts - 59 + 60 apps/client/src/app/components/header/header.component.ts @@ -13342,7 +13350,7 @@ Benchmark apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 253 + 270 @@ -13422,7 +13430,7 @@ Find holding... libs/ui/src/lib/assistant/assistant.component.ts - 126 + 111 @@ -13449,8 +13457,8 @@ Do you really want to delete this asset profile? Do you really want to delete this asset profile? - apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 185 + apps/client/src/app/components/admin-market-data/admin-market-data.service.ts + 13 @@ -13774,7 +13782,7 @@ Oops! Could not parse historical data. apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts - 223 + 233 @@ -14646,7 +14654,7 @@ The current market price is apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts - 317 + 327 @@ -14654,7 +14662,7 @@ Test apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 288 + 305 @@ -14730,11 +14738,11 @@ Investment apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 46 + 47 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 60 + 61 @@ -14790,7 +14798,7 @@ Week to date libs/ui/src/lib/assistant/assistant.component.ts - 101 + 190 @@ -14798,7 +14806,7 @@ WTD libs/ui/src/lib/assistant/assistant.component.ts - 101 + 190 @@ -14806,7 +14814,7 @@ Month to date libs/ui/src/lib/assistant/assistant.component.ts - 105 + 194 @@ -14814,7 +14822,7 @@ MTD libs/ui/src/lib/assistant/assistant.component.ts - 105 + 194 @@ -14822,7 +14830,7 @@ Year to date libs/ui/src/lib/assistant/assistant.component.ts - 109 + 198 @@ -14834,7 +14842,7 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 39 + 38 @@ -14866,7 +14874,7 @@ year libs/ui/src/lib/assistant/assistant.component.ts - 112 + 202 @@ -14874,7 +14882,7 @@ years libs/ui/src/lib/assistant/assistant.component.ts - 114 + 206 diff --git a/apps/client/src/locales/messages.nl.xlf b/apps/client/src/locales/messages.nl.xlf index 39d0d9532..4a2874300 100644 --- a/apps/client/src/locales/messages.nl.xlf +++ b/apps/client/src/locales/messages.nl.xlf @@ -22,7 +22,7 @@ Het risico op verlies bij handelen kan aanzienlijk zijn. Het is niet aan te raden om geld te investeren dat je misschien op korte termijn nodig heeft. apps/client/src/app/app.component.html - 179 + 184 @@ -94,7 +94,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 122 + 139 apps/client/src/app/components/admin-tag/admin-tag.component.html @@ -130,7 +130,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 197 + 214 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -270,7 +270,11 @@ apps/client/src/app/components/admin-market-data/admin-market-data.html - 190 + 194 + + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 62 apps/client/src/app/components/admin-overview/admin-overview.html @@ -326,7 +330,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 98 + 115 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -474,7 +478,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 330 + 347 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -490,7 +494,7 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 60 + 58 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -518,7 +522,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 337 + 354 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -534,7 +538,7 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 67 + 65 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -554,7 +558,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 113 + 130 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -582,7 +586,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 65 + 82 @@ -1489,11 +1493,11 @@ Sectoren apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 173 + 190 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 295 + 312 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -1509,11 +1513,11 @@ Landen apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 183 + 200 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 306 + 323 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -1589,7 +1593,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 99 + 188 @@ -1601,7 +1605,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 109 + 198 @@ -1613,7 +1617,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 112 + 202 @@ -1625,7 +1629,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 114 + 206 @@ -1637,7 +1641,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 117 + 209 @@ -1873,7 +1877,7 @@ Wil je deze aanmeldingsmethode echt verwijderen? apps/client/src/app/components/user-account-settings/user-account-settings.component.ts - 188 + 189 @@ -1945,7 +1949,7 @@ Locatie apps/client/src/app/components/user-account-settings/user-account-settings.html - 113 + 121 @@ -1953,7 +1957,7 @@ Datum- en getalnotatie apps/client/src/app/components/user-account-settings/user-account-settings.html - 115 + 123 @@ -1961,7 +1965,7 @@ Zen-modus apps/client/src/app/components/user-account-settings/user-account-settings.html - 163 + 171 apps/client/src/app/pages/features/features-page.html @@ -1973,7 +1977,7 @@ Aanmelden met vingerafdruk apps/client/src/app/components/user-account-settings/user-account-settings.html - 181 + 189 @@ -1981,11 +1985,11 @@ Gebruikers-ID apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 47 + 45 apps/client/src/app/components/user-account-settings/user-account-settings.html - 215 + 223 @@ -2053,11 +2057,11 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 103 + 120 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 203 + 220 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -2493,7 +2497,7 @@ Opmerking apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 317 + 334 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -2513,11 +2517,11 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 131 + 148 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 212 + 229 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2569,7 +2573,7 @@ Portefeuille apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts - 110 + 116 apps/client/src/app/pages/portfolio/portfolio-page-routing.module.ts @@ -2865,11 +2869,11 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 140 + 157 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 225 + 242 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2925,7 +2929,7 @@ Sector apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 159 + 176 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2937,7 +2941,7 @@ Land apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 168 + 185 apps/client/src/app/components/admin-users/admin-users.html @@ -2997,7 +3001,7 @@ Maandelijks apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 54 + 55 @@ -3045,7 +3049,7 @@ Filter op... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 282 + 281 @@ -3073,7 +3077,7 @@ Experimentele functies apps/client/src/app/components/user-account-settings/user-account-settings.html - 198 + 206 @@ -3081,7 +3085,7 @@ Benchmark apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts - 119 + 128 @@ -3121,7 +3125,7 @@ Weergave apps/client/src/app/components/user-account-settings/user-account-settings.html - 138 + 146 @@ -3129,7 +3133,7 @@ Automatisch apps/client/src/app/components/user-account-settings/user-account-settings.html - 152 + 160 @@ -3137,7 +3141,7 @@ Licht apps/client/src/app/components/user-account-settings/user-account-settings.html - 153 + 161 @@ -3145,7 +3149,7 @@ Donker apps/client/src/app/components/user-account-settings/user-account-settings.html - 154 + 162 @@ -3153,7 +3157,7 @@ Totaalbedrag apps/client/src/app/components/investment-chart/investment-chart.component.ts - 191 + 142 @@ -3169,7 +3173,7 @@ Spaarrente apps/client/src/app/components/investment-chart/investment-chart.component.ts - 263 + 214 @@ -3409,31 +3413,35 @@ apps/client/src/app/components/user-account-settings/user-account-settings.html - 80 + 81 apps/client/src/app/components/user-account-settings/user-account-settings.html - 84 + 86 apps/client/src/app/components/user-account-settings/user-account-settings.html - 88 + 90 apps/client/src/app/components/user-account-settings/user-account-settings.html - 92 + 94 apps/client/src/app/components/user-account-settings/user-account-settings.html - 96 + 98 apps/client/src/app/components/user-account-settings/user-account-settings.html - 100 + 103 apps/client/src/app/components/user-account-settings/user-account-settings.html - 104 + 108 + + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 112 apps/client/src/app/pages/features/features-page.html @@ -3461,7 +3469,7 @@ Symbool toewijzen apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 259 + 276 @@ -3469,7 +3477,7 @@ Dividend apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 42 + 43 libs/ui/src/lib/i18n.ts @@ -3513,7 +3521,7 @@ Importeren apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 91 + 108 apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html @@ -3581,7 +3589,7 @@ Jaarlijks apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 55 + 56 @@ -3661,7 +3669,7 @@ Afleidingsvrije ervaring voor roerige tijden apps/client/src/app/components/user-account-settings/user-account-settings.html - 164 + 172 @@ -3669,7 +3677,7 @@ Voorproefje van nieuwe functionaliteit apps/client/src/app/components/user-account-settings/user-account-settings.html - 199 + 207 @@ -4097,7 +4105,7 @@ Wil je echt al je activiteiten verwijderen? apps/client/src/app/pages/portfolio/activities/activities-page.component.ts - 168 + 171 @@ -4521,7 +4529,7 @@ Scraper instellingen apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 270 + 287 @@ -9865,7 +9873,7 @@ ETF's zonder Landen apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 72 + 77 @@ -9873,7 +9881,7 @@ ETF's zonder Sectoren apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 77 + 82 @@ -10005,7 +10013,7 @@ Biometrische authenticatie apps/client/src/app/components/user-account-settings/user-account-settings.html - 180 + 188 @@ -10089,7 +10097,7 @@ Exporteer Data apps/client/src/app/components/user-account-settings/user-account-settings.html - 223 + 231 @@ -10097,7 +10105,7 @@ Valuta apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 67 + 72 @@ -10477,7 +10485,7 @@ apps/client/src/app/app.component.ts - 54 + 55 apps/client/src/app/pages/about/overview/about-overview-page.component.ts @@ -10509,7 +10517,7 @@ apps/client/src/app/app.component.ts - 55 + 56 apps/client/src/app/components/header/header.component.ts @@ -10769,19 +10777,19 @@ apps/client/src/app/app.component.ts - 47 + 48 apps/client/src/app/app.component.ts - 48 + 49 apps/client/src/app/app.component.ts - 49 + 50 apps/client/src/app/app.component.ts - 51 + 52 apps/client/src/app/components/header/header.component.ts @@ -11049,7 +11057,7 @@ apps/client/src/app/app.component.ts - 52 + 53 apps/client/src/app/pages/about/about-page.component.ts @@ -11065,7 +11073,7 @@ apps/client/src/app/app.component.ts - 49 + 50 apps/client/src/app/pages/about/about-page.component.ts @@ -11081,7 +11089,7 @@ apps/client/src/app/app.component.ts - 56 + 57 apps/client/src/app/components/header/header.component.ts @@ -11113,7 +11121,7 @@ apps/client/src/app/app.component.ts - 57 + 58 apps/client/src/app/components/header/header.component.ts @@ -11185,7 +11193,7 @@ apps/client/src/app/app.component.ts - 58 + 59 apps/client/src/app/components/header/header.component.ts @@ -11221,7 +11229,7 @@ apps/client/src/app/app.component.ts - 59 + 60 apps/client/src/app/components/header/header.component.ts @@ -13341,7 +13349,7 @@ Benchmark apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 253 + 270 @@ -13421,7 +13429,7 @@ Find holding... libs/ui/src/lib/assistant/assistant.component.ts - 126 + 111 @@ -13448,8 +13456,8 @@ Do you really want to delete this asset profile? Do you really want to delete this asset profile? - apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 185 + apps/client/src/app/components/admin-market-data/admin-market-data.service.ts + 13 @@ -13773,7 +13781,7 @@ Oops! Could not parse historical data. apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts - 223 + 233 @@ -14645,7 +14653,7 @@ The current market price is apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts - 317 + 327 @@ -14653,7 +14661,7 @@ Test apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 288 + 305 @@ -14729,11 +14737,11 @@ Investment apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 46 + 47 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 60 + 61 @@ -14789,7 +14797,7 @@ Week to date libs/ui/src/lib/assistant/assistant.component.ts - 101 + 190 @@ -14797,7 +14805,7 @@ WTD libs/ui/src/lib/assistant/assistant.component.ts - 101 + 190 @@ -14805,7 +14813,7 @@ Month to date libs/ui/src/lib/assistant/assistant.component.ts - 105 + 194 @@ -14813,7 +14821,7 @@ MTD libs/ui/src/lib/assistant/assistant.component.ts - 105 + 194 @@ -14821,7 +14829,7 @@ Year to date libs/ui/src/lib/assistant/assistant.component.ts - 109 + 198 @@ -14833,7 +14841,7 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 39 + 38 @@ -14865,7 +14873,7 @@ year libs/ui/src/lib/assistant/assistant.component.ts - 112 + 202 @@ -14873,7 +14881,7 @@ years libs/ui/src/lib/assistant/assistant.component.ts - 114 + 206 diff --git a/apps/client/src/locales/messages.pl.xlf b/apps/client/src/locales/messages.pl.xlf index f59f2652a..5a1611384 100644 --- a/apps/client/src/locales/messages.pl.xlf +++ b/apps/client/src/locales/messages.pl.xlf @@ -10,19 +10,19 @@ apps/client/src/app/app.component.ts - 47 + 48 apps/client/src/app/app.component.ts - 48 + 49 apps/client/src/app/app.component.ts - 49 + 50 apps/client/src/app/app.component.ts - 51 + 52 apps/client/src/app/components/header/header.component.ts @@ -290,7 +290,7 @@ apps/client/src/app/app.component.ts - 54 + 55 apps/client/src/app/pages/about/overview/about-overview-page.component.ts @@ -322,7 +322,7 @@ apps/client/src/app/app.component.ts - 55 + 56 apps/client/src/app/components/header/header.component.ts @@ -582,7 +582,7 @@ apps/client/src/app/app.component.ts - 49 + 50 apps/client/src/app/pages/about/about-page.component.ts @@ -598,7 +598,7 @@ apps/client/src/app/app.component.ts - 56 + 57 apps/client/src/app/components/header/header.component.ts @@ -630,7 +630,7 @@ apps/client/src/app/app.component.ts - 57 + 58 apps/client/src/app/components/header/header.component.ts @@ -702,7 +702,7 @@ apps/client/src/app/app.component.ts - 52 + 53 apps/client/src/app/pages/about/about-page.component.ts @@ -718,7 +718,7 @@ apps/client/src/app/app.component.ts - 58 + 59 apps/client/src/app/components/header/header.component.ts @@ -754,7 +754,7 @@ apps/client/src/app/app.component.ts - 59 + 60 apps/client/src/app/components/header/header.component.ts @@ -1462,31 +1462,35 @@ apps/client/src/app/components/user-account-settings/user-account-settings.html - 80 + 81 apps/client/src/app/components/user-account-settings/user-account-settings.html - 84 + 86 apps/client/src/app/components/user-account-settings/user-account-settings.html - 88 + 90 apps/client/src/app/components/user-account-settings/user-account-settings.html - 92 + 94 apps/client/src/app/components/user-account-settings/user-account-settings.html - 96 + 98 apps/client/src/app/components/user-account-settings/user-account-settings.html - 100 + 103 apps/client/src/app/components/user-account-settings/user-account-settings.html - 104 + 108 + + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 112 apps/client/src/app/pages/features/features-page.html @@ -1498,7 +1502,7 @@ The risk of loss in trading can be substantial. It is not advisable to invest money you may need in the short term. apps/client/src/app/app.component.html - 179 + 184 @@ -1606,7 +1610,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 122 + 139 apps/client/src/app/components/admin-tag/admin-tag.component.html @@ -1670,7 +1674,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 197 + 214 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -1722,11 +1726,11 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 103 + 120 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 203 + 220 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -1838,7 +1842,11 @@ apps/client/src/app/components/admin-market-data/admin-market-data.html - 190 + 194 + + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 62 apps/client/src/app/components/admin-overview/admin-overview.html @@ -1902,7 +1910,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 98 + 115 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -2042,7 +2050,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 330 + 347 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -2058,7 +2066,7 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 60 + 58 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -2086,7 +2094,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 337 + 354 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -2102,7 +2110,7 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 67 + 65 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -2118,7 +2126,7 @@ Currencies apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 67 + 72 @@ -2126,7 +2134,7 @@ ETFs without Countries apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 72 + 77 @@ -2134,15 +2142,15 @@ ETFs without Sectors apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 77 + 82 Do you really want to delete this asset profile? Do you really want to delete this asset profile? - apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 185 + apps/client/src/app/components/admin-market-data/admin-market-data.service.ts + 13 @@ -2150,7 +2158,7 @@ Filter by... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 282 + 281 @@ -2162,11 +2170,11 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 131 + 148 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 212 + 229 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2186,11 +2194,11 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 140 + 157 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 225 + 242 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2210,7 +2218,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 113 + 130 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2238,7 +2246,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 65 + 82 @@ -2290,7 +2298,7 @@ Oops! Could not parse historical data. apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts - 223 + 233 @@ -2314,7 +2322,7 @@ Import apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 91 + 108 apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html @@ -2330,7 +2338,7 @@ Sector apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 159 + 176 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2342,7 +2350,7 @@ Country apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 168 + 185 apps/client/src/app/components/admin-users/admin-users.html @@ -2358,11 +2366,11 @@ Sectors apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 173 + 190 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 295 + 312 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2378,11 +2386,11 @@ Countries apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 183 + 200 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 306 + 323 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2394,7 +2402,7 @@ Benchmark apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 253 + 270 @@ -2402,7 +2410,7 @@ Symbol Mapping apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 259 + 276 @@ -2410,7 +2418,7 @@ Scraper Configuration apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 270 + 287 @@ -2418,7 +2426,7 @@ Note apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 317 + 334 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -2854,7 +2862,7 @@ Portfolio apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts - 110 + 116 apps/client/src/app/pages/portfolio/portfolio-page-routing.module.ts @@ -2866,7 +2874,7 @@ Benchmark apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts - 119 + 128 @@ -3154,7 +3162,7 @@ Total Amount apps/client/src/app/components/investment-chart/investment-chart.component.ts - 191 + 142 @@ -3162,7 +3170,7 @@ Savings Rate apps/client/src/app/components/investment-chart/investment-chart.component.ts - 263 + 214 @@ -3676,7 +3684,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 99 + 188 @@ -3688,7 +3696,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 109 + 198 @@ -3700,7 +3708,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 112 + 202 @@ -3712,7 +3720,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 114 + 206 @@ -3724,7 +3732,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 117 + 209 @@ -3824,7 +3832,7 @@ Do you really want to remove this sign in method? apps/client/src/app/components/user-account-settings/user-account-settings.component.ts - 188 + 189 @@ -3872,7 +3880,7 @@ Locale apps/client/src/app/components/user-account-settings/user-account-settings.html - 113 + 121 @@ -3880,7 +3888,7 @@ Date and number format apps/client/src/app/components/user-account-settings/user-account-settings.html - 115 + 123 @@ -3888,7 +3896,7 @@ Appearance apps/client/src/app/components/user-account-settings/user-account-settings.html - 138 + 146 @@ -3896,7 +3904,7 @@ Auto apps/client/src/app/components/user-account-settings/user-account-settings.html - 152 + 160 @@ -3904,7 +3912,7 @@ Light apps/client/src/app/components/user-account-settings/user-account-settings.html - 153 + 161 @@ -3912,7 +3920,7 @@ Dark apps/client/src/app/components/user-account-settings/user-account-settings.html - 154 + 162 @@ -3920,7 +3928,7 @@ Zen Mode apps/client/src/app/components/user-account-settings/user-account-settings.html - 163 + 171 apps/client/src/app/pages/features/features-page.html @@ -3932,7 +3940,7 @@ Distraction-free experience for turbulent times apps/client/src/app/components/user-account-settings/user-account-settings.html - 164 + 172 @@ -3940,7 +3948,7 @@ Biometric Authentication apps/client/src/app/components/user-account-settings/user-account-settings.html - 180 + 188 @@ -3948,7 +3956,7 @@ Sign in with fingerprint apps/client/src/app/components/user-account-settings/user-account-settings.html - 181 + 189 @@ -3956,7 +3964,7 @@ Experimental Features apps/client/src/app/components/user-account-settings/user-account-settings.html - 198 + 206 @@ -3964,7 +3972,7 @@ Sneak peek at upcoming functionality apps/client/src/app/components/user-account-settings/user-account-settings.html - 199 + 207 @@ -3972,11 +3980,11 @@ User ID apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 47 + 45 apps/client/src/app/components/user-account-settings/user-account-settings.html - 215 + 223 @@ -3984,7 +3992,7 @@ Export Data apps/client/src/app/components/user-account-settings/user-account-settings.html - 223 + 231 @@ -4940,7 +4948,7 @@ Do you really want to delete all your activities? apps/client/src/app/pages/portfolio/activities/activities-page.component.ts - 168 + 171 @@ -5372,7 +5380,7 @@ Dividend apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 42 + 43 libs/ui/src/lib/i18n.ts @@ -5392,7 +5400,7 @@ Monthly apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 54 + 55 @@ -5400,7 +5408,7 @@ Yearly apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 55 + 56 @@ -13216,7 +13224,7 @@ Find holding... libs/ui/src/lib/assistant/assistant.component.ts - 126 + 111 @@ -14648,7 +14656,7 @@ The current market price is apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts - 317 + 327 @@ -14656,7 +14664,7 @@ Test apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 288 + 305 @@ -14732,11 +14740,11 @@ Investment apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 46 + 47 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 60 + 61 @@ -14792,7 +14800,7 @@ Week to date libs/ui/src/lib/assistant/assistant.component.ts - 101 + 190 @@ -14800,7 +14808,7 @@ WTD libs/ui/src/lib/assistant/assistant.component.ts - 101 + 190 @@ -14808,7 +14816,7 @@ Month to date libs/ui/src/lib/assistant/assistant.component.ts - 105 + 194 @@ -14816,7 +14824,7 @@ MTD libs/ui/src/lib/assistant/assistant.component.ts - 105 + 194 @@ -14824,7 +14832,7 @@ Year to date libs/ui/src/lib/assistant/assistant.component.ts - 109 + 198 @@ -14836,7 +14844,7 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 39 + 38 @@ -14868,7 +14876,7 @@ year libs/ui/src/lib/assistant/assistant.component.ts - 112 + 202 @@ -14876,7 +14884,7 @@ years libs/ui/src/lib/assistant/assistant.component.ts - 114 + 206 diff --git a/apps/client/src/locales/messages.pt.xlf b/apps/client/src/locales/messages.pt.xlf index 60d8a37cf..3970b11aa 100644 --- a/apps/client/src/locales/messages.pt.xlf +++ b/apps/client/src/locales/messages.pt.xlf @@ -6,7 +6,7 @@ O risco de perda em investimentos pode ser substancial. Não é aconselhável investir dinheiro que possa vir a precisar a curto prazo. apps/client/src/app/app.component.html - 179 + 184 @@ -106,7 +106,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 122 + 139 apps/client/src/app/components/admin-tag/admin-tag.component.html @@ -142,7 +142,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 197 + 214 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -194,11 +194,11 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 103 + 120 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 203 + 220 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -326,7 +326,11 @@ apps/client/src/app/components/admin-market-data/admin-market-data.html - 190 + 194 + + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 62 apps/client/src/app/components/admin-overview/admin-overview.html @@ -374,7 +378,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 98 + 115 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -530,7 +534,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 330 + 347 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -546,7 +550,7 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 60 + 58 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -574,7 +578,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 337 + 354 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -590,7 +594,7 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 67 + 65 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -606,7 +610,7 @@ Filtrar por... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 282 + 281 @@ -618,11 +622,11 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 131 + 148 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 212 + 229 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -642,11 +646,11 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 140 + 157 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 225 + 242 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -666,7 +670,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 113 + 130 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -694,7 +698,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 65 + 82 @@ -970,7 +974,7 @@ Portefólio apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts - 110 + 116 apps/client/src/app/pages/portfolio/portfolio-page-routing.module.ts @@ -982,7 +986,7 @@ Referência apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts - 119 + 128 @@ -1478,7 +1482,7 @@ Valor Total apps/client/src/app/components/investment-chart/investment-chart.component.ts - 191 + 142 @@ -1486,7 +1490,7 @@ Taxa de Poupança apps/client/src/app/components/investment-chart/investment-chart.component.ts - 263 + 214 @@ -1785,7 +1789,7 @@ Setor apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 159 + 176 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -1797,7 +1801,7 @@ País apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 168 + 185 apps/client/src/app/components/admin-users/admin-users.html @@ -1813,11 +1817,11 @@ Setores apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 173 + 190 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 295 + 312 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -1833,11 +1837,11 @@ Países apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 183 + 200 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 306 + 323 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -1897,7 +1901,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 99 + 188 @@ -1909,7 +1913,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 109 + 198 @@ -1921,7 +1925,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 112 + 202 @@ -1933,7 +1937,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 114 + 206 @@ -1945,7 +1949,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 117 + 209 @@ -2113,7 +2117,7 @@ Deseja realmente remover este método de início de sessão? apps/client/src/app/components/user-account-settings/user-account-settings.component.ts - 188 + 189 @@ -2205,7 +2209,7 @@ Localidade apps/client/src/app/components/user-account-settings/user-account-settings.html - 113 + 121 @@ -2213,7 +2217,7 @@ Formato de números e datas apps/client/src/app/components/user-account-settings/user-account-settings.html - 115 + 123 @@ -2221,7 +2225,7 @@ Modo Zen apps/client/src/app/components/user-account-settings/user-account-settings.html - 163 + 171 apps/client/src/app/pages/features/features-page.html @@ -2233,7 +2237,7 @@ Aparência apps/client/src/app/components/user-account-settings/user-account-settings.html - 138 + 146 @@ -2241,7 +2245,7 @@ Auto apps/client/src/app/components/user-account-settings/user-account-settings.html - 152 + 160 @@ -2249,7 +2253,7 @@ Claro apps/client/src/app/components/user-account-settings/user-account-settings.html - 153 + 161 @@ -2257,7 +2261,7 @@ Escuro apps/client/src/app/components/user-account-settings/user-account-settings.html - 154 + 162 @@ -2265,7 +2269,7 @@ Iniciar sessão com impressão digital apps/client/src/app/components/user-account-settings/user-account-settings.html - 181 + 189 @@ -2273,7 +2277,7 @@ Funcionalidades Experimentais apps/client/src/app/components/user-account-settings/user-account-settings.html - 198 + 206 @@ -2281,11 +2285,11 @@ ID do Utilizador apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 47 + 45 apps/client/src/app/components/user-account-settings/user-account-settings.html - 215 + 223 @@ -2609,7 +2613,7 @@ Nota apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 317 + 334 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -2805,7 +2809,7 @@ Mensalmente apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 54 + 55 @@ -3433,7 +3437,7 @@ Mapeamento de Símbolo apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 259 + 276 @@ -3453,31 +3457,35 @@ apps/client/src/app/components/user-account-settings/user-account-settings.html - 80 + 81 apps/client/src/app/components/user-account-settings/user-account-settings.html - 84 + 86 apps/client/src/app/components/user-account-settings/user-account-settings.html - 88 + 90 apps/client/src/app/components/user-account-settings/user-account-settings.html - 92 + 94 apps/client/src/app/components/user-account-settings/user-account-settings.html - 96 + 98 apps/client/src/app/components/user-account-settings/user-account-settings.html - 100 + 103 apps/client/src/app/components/user-account-settings/user-account-settings.html - 104 + 108 + + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 112 apps/client/src/app/pages/features/features-page.html @@ -3521,7 +3529,7 @@ Importar apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 91 + 108 apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html @@ -3537,7 +3545,7 @@ Dividendos apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 42 + 43 libs/ui/src/lib/i18n.ts @@ -3581,7 +3589,7 @@ Anualmente apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 55 + 56 @@ -3661,7 +3669,7 @@ Experiência sem distrações para tempos turbulentos apps/client/src/app/components/user-account-settings/user-account-settings.html - 164 + 172 @@ -3669,7 +3677,7 @@ Acesso antecipado a funcionalidades futuras apps/client/src/app/components/user-account-settings/user-account-settings.html - 199 + 207 @@ -4097,7 +4105,7 @@ Deseja mesmo eliminar todas as suas atividades? apps/client/src/app/pages/portfolio/activities/activities-page.component.ts - 168 + 171 @@ -4521,7 +4529,7 @@ Scraper Configuration apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 270 + 287 @@ -9865,7 +9873,7 @@ ETFs without Countries apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 72 + 77 @@ -9873,7 +9881,7 @@ ETFs without Sectors apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 77 + 82 @@ -10005,7 +10013,7 @@ Biometric Authentication apps/client/src/app/components/user-account-settings/user-account-settings.html - 180 + 188 @@ -10089,7 +10097,7 @@ Export Data apps/client/src/app/components/user-account-settings/user-account-settings.html - 223 + 231 @@ -10097,7 +10105,7 @@ Currencies apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 67 + 72 @@ -10477,7 +10485,7 @@ apps/client/src/app/app.component.ts - 54 + 55 apps/client/src/app/pages/about/overview/about-overview-page.component.ts @@ -10509,7 +10517,7 @@ apps/client/src/app/app.component.ts - 55 + 56 apps/client/src/app/components/header/header.component.ts @@ -10769,19 +10777,19 @@ apps/client/src/app/app.component.ts - 47 + 48 apps/client/src/app/app.component.ts - 48 + 49 apps/client/src/app/app.component.ts - 49 + 50 apps/client/src/app/app.component.ts - 51 + 52 apps/client/src/app/components/header/header.component.ts @@ -11049,7 +11057,7 @@ apps/client/src/app/app.component.ts - 52 + 53 apps/client/src/app/pages/about/about-page.component.ts @@ -11065,7 +11073,7 @@ apps/client/src/app/app.component.ts - 49 + 50 apps/client/src/app/pages/about/about-page.component.ts @@ -11081,7 +11089,7 @@ apps/client/src/app/app.component.ts - 56 + 57 apps/client/src/app/components/header/header.component.ts @@ -11113,7 +11121,7 @@ apps/client/src/app/app.component.ts - 57 + 58 apps/client/src/app/components/header/header.component.ts @@ -11185,7 +11193,7 @@ apps/client/src/app/app.component.ts - 58 + 59 apps/client/src/app/components/header/header.component.ts @@ -11221,7 +11229,7 @@ apps/client/src/app/app.component.ts - 59 + 60 apps/client/src/app/components/header/header.component.ts @@ -13341,7 +13349,7 @@ Benchmark apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 253 + 270 @@ -13421,7 +13429,7 @@ Find holding... libs/ui/src/lib/assistant/assistant.component.ts - 126 + 111 @@ -13448,8 +13456,8 @@ Do you really want to delete this asset profile? Do you really want to delete this asset profile? - apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 185 + apps/client/src/app/components/admin-market-data/admin-market-data.service.ts + 13 @@ -13773,7 +13781,7 @@ Oops! Could not parse historical data. apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts - 223 + 233 @@ -14645,7 +14653,7 @@ The current market price is apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts - 317 + 327 @@ -14653,7 +14661,7 @@ Test apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 288 + 305 @@ -14729,11 +14737,11 @@ Investment apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 46 + 47 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 60 + 61 @@ -14789,7 +14797,7 @@ Week to date libs/ui/src/lib/assistant/assistant.component.ts - 101 + 190 @@ -14797,7 +14805,7 @@ WTD libs/ui/src/lib/assistant/assistant.component.ts - 101 + 190 @@ -14805,7 +14813,7 @@ Month to date libs/ui/src/lib/assistant/assistant.component.ts - 105 + 194 @@ -14813,7 +14821,7 @@ MTD libs/ui/src/lib/assistant/assistant.component.ts - 105 + 194 @@ -14821,7 +14829,7 @@ Year to date libs/ui/src/lib/assistant/assistant.component.ts - 109 + 198 @@ -14833,7 +14841,7 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 39 + 38 @@ -14865,7 +14873,7 @@ year libs/ui/src/lib/assistant/assistant.component.ts - 112 + 202 @@ -14873,7 +14881,7 @@ years libs/ui/src/lib/assistant/assistant.component.ts - 114 + 206 diff --git a/apps/client/src/locales/messages.tr.xlf b/apps/client/src/locales/messages.tr.xlf index ae9ae985c..2052e6667 100644 --- a/apps/client/src/locales/messages.tr.xlf +++ b/apps/client/src/locales/messages.tr.xlf @@ -10,19 +10,19 @@ apps/client/src/app/app.component.ts - 47 + 48 apps/client/src/app/app.component.ts - 48 + 49 apps/client/src/app/app.component.ts - 49 + 50 apps/client/src/app/app.component.ts - 51 + 52 apps/client/src/app/components/header/header.component.ts @@ -290,7 +290,7 @@ apps/client/src/app/app.component.ts - 54 + 55 apps/client/src/app/pages/about/overview/about-overview-page.component.ts @@ -322,7 +322,7 @@ apps/client/src/app/app.component.ts - 55 + 56 apps/client/src/app/components/header/header.component.ts @@ -582,7 +582,7 @@ apps/client/src/app/app.component.ts - 49 + 50 apps/client/src/app/pages/about/about-page.component.ts @@ -598,7 +598,7 @@ apps/client/src/app/app.component.ts - 56 + 57 apps/client/src/app/components/header/header.component.ts @@ -630,7 +630,7 @@ apps/client/src/app/app.component.ts - 57 + 58 apps/client/src/app/components/header/header.component.ts @@ -702,7 +702,7 @@ apps/client/src/app/app.component.ts - 52 + 53 apps/client/src/app/pages/about/about-page.component.ts @@ -718,7 +718,7 @@ apps/client/src/app/app.component.ts - 58 + 59 apps/client/src/app/components/header/header.component.ts @@ -754,7 +754,7 @@ apps/client/src/app/app.component.ts - 59 + 60 apps/client/src/app/components/header/header.component.ts @@ -1438,31 +1438,35 @@ apps/client/src/app/components/user-account-settings/user-account-settings.html - 80 + 81 apps/client/src/app/components/user-account-settings/user-account-settings.html - 84 + 86 apps/client/src/app/components/user-account-settings/user-account-settings.html - 88 + 90 apps/client/src/app/components/user-account-settings/user-account-settings.html - 92 + 94 apps/client/src/app/components/user-account-settings/user-account-settings.html - 96 + 98 apps/client/src/app/components/user-account-settings/user-account-settings.html - 100 + 103 apps/client/src/app/components/user-account-settings/user-account-settings.html - 104 + 108 + + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 112 apps/client/src/app/pages/features/features-page.html @@ -1474,7 +1478,7 @@ Alım satımda kayıp riski büyük boyutta olabilir. Kısa vadede ihtiyaç duyabileceğiniz parayla yatırım yapmak tavsiye edilmez. apps/client/src/app/app.component.html - 179 + 184 @@ -1598,7 +1602,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 122 + 139 apps/client/src/app/components/admin-tag/admin-tag.component.html @@ -1634,7 +1638,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 197 + 214 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -1686,11 +1690,11 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 103 + 120 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 203 + 220 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -1802,7 +1806,11 @@ apps/client/src/app/components/admin-market-data/admin-market-data.html - 190 + 194 + + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 62 apps/client/src/app/components/admin-overview/admin-overview.html @@ -1850,7 +1858,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 98 + 115 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -2006,7 +2014,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 330 + 347 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -2022,7 +2030,7 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 60 + 58 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -2050,7 +2058,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 337 + 354 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -2066,7 +2074,7 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 67 + 65 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -2082,7 +2090,7 @@ Para Birimleri apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 67 + 72 @@ -2090,7 +2098,7 @@ Ülkesi Olmayan ETF'ler apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 72 + 77 @@ -2098,7 +2106,7 @@ Sektörü Olmayan ETF'ler apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 77 + 82 @@ -2106,7 +2114,7 @@ Filtrele... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 282 + 281 @@ -2118,11 +2126,11 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 131 + 148 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 212 + 229 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2142,11 +2150,11 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 140 + 157 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 225 + 242 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2166,7 +2174,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 113 + 130 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2194,7 +2202,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 65 + 82 @@ -2262,7 +2270,7 @@ Sektör apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 159 + 176 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2274,7 +2282,7 @@ Ülke apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 168 + 185 apps/client/src/app/components/admin-users/admin-users.html @@ -2290,11 +2298,11 @@ Sektörler apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 173 + 190 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 295 + 312 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2310,11 +2318,11 @@ Ülkeler apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 183 + 200 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 306 + 323 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2326,7 +2334,7 @@ Sembol Eşleştirme apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 259 + 276 @@ -2334,7 +2342,7 @@ Veri Toplayıcı Yapılandırması apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 270 + 287 @@ -2342,7 +2350,7 @@ Not apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 317 + 334 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -2714,7 +2722,7 @@ Portföy apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts - 110 + 116 apps/client/src/app/pages/portfolio/portfolio-page-routing.module.ts @@ -2726,7 +2734,7 @@ Karşılaştırma Ölçütü apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts - 119 + 128 @@ -3006,7 +3014,7 @@ Toplam Tutar apps/client/src/app/components/investment-chart/investment-chart.component.ts - 191 + 142 @@ -3014,7 +3022,7 @@ Tasarruf Oranı apps/client/src/app/components/investment-chart/investment-chart.component.ts - 263 + 214 @@ -3517,7 +3525,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 99 + 188 @@ -3529,7 +3537,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 109 + 198 @@ -3541,7 +3549,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 112 + 202 @@ -3553,7 +3561,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 114 + 206 @@ -3565,7 +3573,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 117 + 209 @@ -3941,7 +3949,7 @@ Zen Modu apps/client/src/app/components/user-account-settings/user-account-settings.html - 163 + 171 apps/client/src/app/pages/features/features-page.html @@ -4441,7 +4449,7 @@ Tüm işlemlerinizi silmeyi gerçekten istiyor musunuz? apps/client/src/app/pages/portfolio/activities/activities-page.component.ts - 168 + 171 @@ -4653,7 +4661,7 @@ İçe Aktar apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 91 + 108 apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html @@ -4849,7 +4857,7 @@ Temettü apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 42 + 43 libs/ui/src/lib/i18n.ts @@ -4869,7 +4877,7 @@ Aylık apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 54 + 55 @@ -4877,7 +4885,7 @@ Yıllık apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 55 + 56 @@ -12353,7 +12361,7 @@ Bu giriş yöntemini kaldırmayı gerçekten istiyor musunuz? apps/client/src/app/components/user-account-settings/user-account-settings.component.ts - 188 + 189 @@ -12437,7 +12445,7 @@ Yerel Ayarlar apps/client/src/app/components/user-account-settings/user-account-settings.html - 113 + 121 @@ -12445,7 +12453,7 @@ Tarih ve Sayı Formatları apps/client/src/app/components/user-account-settings/user-account-settings.html - 115 + 123 @@ -12453,7 +12461,7 @@ Görünüm apps/client/src/app/components/user-account-settings/user-account-settings.html - 138 + 146 @@ -12461,7 +12469,7 @@ Otomatik apps/client/src/app/components/user-account-settings/user-account-settings.html - 152 + 160 @@ -12469,7 +12477,7 @@ Açık apps/client/src/app/components/user-account-settings/user-account-settings.html - 153 + 161 @@ -12477,7 +12485,7 @@ Koyu apps/client/src/app/components/user-account-settings/user-account-settings.html - 154 + 162 @@ -12485,7 +12493,7 @@ Çalkantılı zamanlar için dikkat dağıtmayan bir deneyim apps/client/src/app/components/user-account-settings/user-account-settings.html - 164 + 172 @@ -12493,7 +12501,7 @@ Biyometrik Kimlik Doğrulama apps/client/src/app/components/user-account-settings/user-account-settings.html - 180 + 188 @@ -12501,7 +12509,7 @@ Parmak iziyle oturum aç apps/client/src/app/components/user-account-settings/user-account-settings.html - 181 + 189 @@ -12509,7 +12517,7 @@ Deneysel Özellikler apps/client/src/app/components/user-account-settings/user-account-settings.html - 198 + 206 @@ -12517,7 +12525,7 @@ Gelecek özelliklere göz atın apps/client/src/app/components/user-account-settings/user-account-settings.html - 199 + 207 @@ -12525,11 +12533,11 @@ Kullanıcı Kimliği apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 47 + 45 apps/client/src/app/components/user-account-settings/user-account-settings.html - 215 + 223 @@ -12537,7 +12545,7 @@ Verileri Dışa Aktar apps/client/src/app/components/user-account-settings/user-account-settings.html - 223 + 231 @@ -13341,7 +13349,7 @@ Benchmark apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 253 + 270 @@ -13421,7 +13429,7 @@ Find holding... libs/ui/src/lib/assistant/assistant.component.ts - 126 + 111 @@ -13448,8 +13456,8 @@ Do you really want to delete this asset profile? Do you really want to delete this asset profile? - apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 185 + apps/client/src/app/components/admin-market-data/admin-market-data.service.ts + 13 @@ -13773,7 +13781,7 @@ Oops! Could not parse historical data. apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts - 223 + 233 @@ -14645,7 +14653,7 @@ The current market price is apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts - 317 + 327 @@ -14653,7 +14661,7 @@ Test apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 288 + 305 @@ -14729,11 +14737,11 @@ Investment apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 46 + 47 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 60 + 61 @@ -14789,7 +14797,7 @@ Week to date libs/ui/src/lib/assistant/assistant.component.ts - 101 + 190 @@ -14797,7 +14805,7 @@ WTD libs/ui/src/lib/assistant/assistant.component.ts - 101 + 190 @@ -14805,7 +14813,7 @@ Month to date libs/ui/src/lib/assistant/assistant.component.ts - 105 + 194 @@ -14813,7 +14821,7 @@ MTD libs/ui/src/lib/assistant/assistant.component.ts - 105 + 194 @@ -14821,7 +14829,7 @@ Year to date libs/ui/src/lib/assistant/assistant.component.ts - 109 + 198 @@ -14833,7 +14841,7 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 39 + 38 @@ -14865,7 +14873,7 @@ year libs/ui/src/lib/assistant/assistant.component.ts - 112 + 202 @@ -14873,7 +14881,7 @@ years libs/ui/src/lib/assistant/assistant.component.ts - 114 + 206 diff --git a/apps/client/src/locales/messages.xlf b/apps/client/src/locales/messages.xlf index b5fca055a..1cb9af6dd 100644 --- a/apps/client/src/locales/messages.xlf +++ b/apps/client/src/locales/messages.xlf @@ -10,19 +10,19 @@ apps/client/src/app/app.component.ts - 47 + 48 apps/client/src/app/app.component.ts - 48 + 49 apps/client/src/app/app.component.ts - 49 + 50 apps/client/src/app/app.component.ts - 51 + 52 apps/client/src/app/components/header/header.component.ts @@ -289,7 +289,7 @@ apps/client/src/app/app.component.ts - 54 + 55 apps/client/src/app/pages/about/overview/about-overview-page.component.ts @@ -320,7 +320,7 @@ apps/client/src/app/app.component.ts - 55 + 56 apps/client/src/app/components/header/header.component.ts @@ -579,7 +579,7 @@ apps/client/src/app/app.component.ts - 49 + 50 apps/client/src/app/pages/about/about-page.component.ts @@ -594,7 +594,7 @@ apps/client/src/app/app.component.ts - 56 + 57 apps/client/src/app/components/header/header.component.ts @@ -625,7 +625,7 @@ apps/client/src/app/app.component.ts - 57 + 58 apps/client/src/app/components/header/header.component.ts @@ -696,7 +696,7 @@ apps/client/src/app/app.component.ts - 52 + 53 apps/client/src/app/pages/about/about-page.component.ts @@ -711,7 +711,7 @@ apps/client/src/app/app.component.ts - 58 + 59 apps/client/src/app/components/header/header.component.ts @@ -746,7 +746,7 @@ apps/client/src/app/app.component.ts - 59 + 60 apps/client/src/app/components/header/header.component.ts @@ -1440,31 +1440,35 @@ apps/client/src/app/components/user-account-settings/user-account-settings.html - 80 + 81 apps/client/src/app/components/user-account-settings/user-account-settings.html - 84 + 86 apps/client/src/app/components/user-account-settings/user-account-settings.html - 88 + 90 apps/client/src/app/components/user-account-settings/user-account-settings.html - 92 + 94 apps/client/src/app/components/user-account-settings/user-account-settings.html - 96 + 98 apps/client/src/app/components/user-account-settings/user-account-settings.html - 100 + 103 apps/client/src/app/components/user-account-settings/user-account-settings.html - 104 + 108 + + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 112 apps/client/src/app/pages/features/features-page.html @@ -1475,7 +1479,7 @@ The risk of loss in trading can be substantial. It is not advisable to invest money you may need in the short term. apps/client/src/app/app.component.html - 179 + 184 @@ -1574,7 +1578,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 122 + 139 apps/client/src/app/components/admin-tag/admin-tag.component.html @@ -1642,7 +1646,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 197 + 214 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -1692,11 +1696,11 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 103 + 120 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 203 + 220 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -1805,7 +1809,11 @@ apps/client/src/app/components/admin-market-data/admin-market-data.html - 190 + 194 + + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 62 apps/client/src/app/components/admin-overview/admin-overview.html @@ -1865,7 +1873,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 98 + 115 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -1992,7 +2000,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 330 + 347 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -2008,7 +2016,7 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 60 + 58 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -2035,7 +2043,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 337 + 354 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -2051,7 +2059,7 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 67 + 65 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -2066,35 +2074,35 @@ Currencies apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 67 + 72 ETFs without Countries apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 72 + 77 ETFs without Sectors apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 77 + 82 Do you really want to delete this asset profile? - apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 185 + apps/client/src/app/components/admin-market-data/admin-market-data.service.ts + 13 Filter by... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 282 + 281 @@ -2105,11 +2113,11 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 131 + 148 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 212 + 229 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2128,11 +2136,11 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 140 + 157 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 225 + 242 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2151,7 +2159,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 113 + 130 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2177,7 +2185,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 65 + 82 @@ -2223,7 +2231,7 @@ Oops! Could not parse historical data. apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts - 223 + 233 @@ -2244,7 +2252,7 @@ Import apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 91 + 108 apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html @@ -2259,7 +2267,7 @@ Sector apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 159 + 176 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2270,7 +2278,7 @@ Country apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 168 + 185 apps/client/src/app/components/admin-users/admin-users.html @@ -2285,11 +2293,11 @@ Sectors apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 173 + 190 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 295 + 312 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2304,11 +2312,11 @@ Countries apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 183 + 200 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 306 + 323 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2319,28 +2327,28 @@ Benchmark apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 253 + 270 Symbol Mapping apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 259 + 276 Scraper Configuration apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 270 + 287 Note apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 317 + 334 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -2736,7 +2744,7 @@ Portfolio apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts - 110 + 116 apps/client/src/app/pages/portfolio/portfolio-page-routing.module.ts @@ -2747,7 +2755,7 @@ Benchmark apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts - 119 + 128 @@ -3005,14 +3013,14 @@ Total Amount apps/client/src/app/components/investment-chart/investment-chart.component.ts - 191 + 142 Savings Rate apps/client/src/app/components/investment-chart/investment-chart.component.ts - 263 + 214 @@ -3476,7 +3484,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 99 + 188 @@ -3487,7 +3495,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 109 + 198 @@ -3498,7 +3506,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 112 + 202 @@ -3509,7 +3517,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 114 + 206 @@ -3520,7 +3528,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 117 + 209 @@ -3608,7 +3616,7 @@ Do you really want to remove this sign in method? apps/client/src/app/components/user-account-settings/user-account-settings.component.ts - 188 + 189 @@ -3650,49 +3658,49 @@ Locale apps/client/src/app/components/user-account-settings/user-account-settings.html - 113 + 121 Date and number format apps/client/src/app/components/user-account-settings/user-account-settings.html - 115 + 123 Appearance apps/client/src/app/components/user-account-settings/user-account-settings.html - 138 + 146 Auto apps/client/src/app/components/user-account-settings/user-account-settings.html - 152 + 160 Light apps/client/src/app/components/user-account-settings/user-account-settings.html - 153 + 161 Dark apps/client/src/app/components/user-account-settings/user-account-settings.html - 154 + 162 Zen Mode apps/client/src/app/components/user-account-settings/user-account-settings.html - 163 + 171 apps/client/src/app/pages/features/features-page.html @@ -3703,53 +3711,53 @@ Distraction-free experience for turbulent times apps/client/src/app/components/user-account-settings/user-account-settings.html - 164 + 172 Biometric Authentication apps/client/src/app/components/user-account-settings/user-account-settings.html - 180 + 188 Sign in with fingerprint apps/client/src/app/components/user-account-settings/user-account-settings.html - 181 + 189 Experimental Features apps/client/src/app/components/user-account-settings/user-account-settings.html - 198 + 206 Sneak peek at upcoming functionality apps/client/src/app/components/user-account-settings/user-account-settings.html - 199 + 207 User ID apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 47 + 45 apps/client/src/app/components/user-account-settings/user-account-settings.html - 215 + 223 Export Data apps/client/src/app/components/user-account-settings/user-account-settings.html - 223 + 231 @@ -4603,7 +4611,7 @@ Do you really want to delete all your activities? apps/client/src/app/pages/portfolio/activities/activities-page.component.ts - 168 + 171 @@ -4988,7 +4996,7 @@ Dividend apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 42 + 43 libs/ui/src/lib/i18n.ts @@ -5006,14 +5014,14 @@ Monthly apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 54 + 55 Yearly apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 55 + 56 @@ -13533,7 +13541,7 @@ Find holding... libs/ui/src/lib/assistant/assistant.component.ts - 126 + 111 @@ -14067,14 +14075,14 @@ The current market price is apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts - 317 + 327 Test apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 288 + 305 @@ -14156,11 +14164,11 @@ Investment apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 46 + 47 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 60 + 61 @@ -14188,35 +14196,35 @@ Year to date libs/ui/src/lib/assistant/assistant.component.ts - 109 + 198 Week to date libs/ui/src/lib/assistant/assistant.component.ts - 101 + 190 Month to date libs/ui/src/lib/assistant/assistant.component.ts - 105 + 194 MTD libs/ui/src/lib/assistant/assistant.component.ts - 105 + 194 WTD libs/ui/src/lib/assistant/assistant.component.ts - 101 + 190 @@ -14234,7 +14242,7 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 39 + 38 @@ -14255,14 +14263,14 @@ year libs/ui/src/lib/assistant/assistant.component.ts - 112 + 202 years libs/ui/src/lib/assistant/assistant.component.ts - 114 + 206 diff --git a/apps/client/src/locales/messages.zh.xlf b/apps/client/src/locales/messages.zh.xlf index 2887d5724..7ff1ce5bb 100644 --- a/apps/client/src/locales/messages.zh.xlf +++ b/apps/client/src/locales/messages.zh.xlf @@ -1,6 +1,6 @@ - + about @@ -11,19 +11,19 @@ apps/client/src/app/app.component.ts - 47 + 48 apps/client/src/app/app.component.ts - 48 + 49 apps/client/src/app/app.component.ts - 49 + 50 apps/client/src/app/app.component.ts - 51 + 52 apps/client/src/app/components/header/header.component.ts @@ -291,7 +291,7 @@ apps/client/src/app/app.component.ts - 54 + 55 apps/client/src/app/pages/about/overview/about-overview-page.component.ts @@ -323,7 +323,7 @@ apps/client/src/app/app.component.ts - 55 + 56 apps/client/src/app/components/header/header.component.ts @@ -583,7 +583,7 @@ apps/client/src/app/app.component.ts - 49 + 50 apps/client/src/app/pages/about/about-page.component.ts @@ -599,7 +599,7 @@ apps/client/src/app/app.component.ts - 56 + 57 apps/client/src/app/components/header/header.component.ts @@ -631,7 +631,7 @@ apps/client/src/app/app.component.ts - 57 + 58 apps/client/src/app/components/header/header.component.ts @@ -703,7 +703,7 @@ apps/client/src/app/app.component.ts - 52 + 53 apps/client/src/app/pages/about/about-page.component.ts @@ -719,7 +719,7 @@ apps/client/src/app/app.component.ts - 58 + 59 apps/client/src/app/components/header/header.component.ts @@ -755,7 +755,7 @@ apps/client/src/app/app.component.ts - 59 + 60 apps/client/src/app/components/header/header.component.ts @@ -1463,31 +1463,35 @@ apps/client/src/app/components/user-account-settings/user-account-settings.html - 80 + 81 apps/client/src/app/components/user-account-settings/user-account-settings.html - 84 + 86 apps/client/src/app/components/user-account-settings/user-account-settings.html - 88 + 90 apps/client/src/app/components/user-account-settings/user-account-settings.html - 92 + 94 apps/client/src/app/components/user-account-settings/user-account-settings.html - 96 + 98 apps/client/src/app/components/user-account-settings/user-account-settings.html - 100 + 103 apps/client/src/app/components/user-account-settings/user-account-settings.html - 104 + 108 + + + apps/client/src/app/components/user-account-settings/user-account-settings.html + 112 apps/client/src/app/pages/features/features-page.html @@ -1499,7 +1503,7 @@ 交易损失的风险可能很大。不建议将短期内可能需要的资金进行投资。 apps/client/src/app/app.component.html - 179 + 184 @@ -1607,7 +1611,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 122 + 139 apps/client/src/app/components/admin-tag/admin-tag.component.html @@ -1679,7 +1683,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 197 + 214 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -1731,11 +1735,11 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 103 + 120 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 203 + 220 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -1847,7 +1851,11 @@ apps/client/src/app/components/admin-market-data/admin-market-data.html - 190 + 194 + + + apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html + 62 apps/client/src/app/components/admin-overview/admin-overview.html @@ -1911,7 +1919,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 98 + 115 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -2003,8 +2011,8 @@ - Details for - 详细信息 + Details for + 详细信息 apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html 2 @@ -2051,7 +2059,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 330 + 347 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -2067,7 +2075,7 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 60 + 58 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -2095,7 +2103,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 337 + 354 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -2111,7 +2119,7 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 67 + 65 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -2127,7 +2135,7 @@ 货币 apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 67 + 72 @@ -2135,7 +2143,7 @@ 没有国家的 ETF apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 72 + 77 @@ -2143,15 +2151,15 @@ 无行业类别的 ETF apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 77 + 82 Do you really want to delete this asset profile? 您确实要删除此资产配置文件吗? - apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 185 + apps/client/src/app/components/admin-market-data/admin-market-data.service.ts + 13 @@ -2159,7 +2167,7 @@ 过滤... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 282 + 281 @@ -2171,11 +2179,11 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 131 + 148 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 212 + 229 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2195,11 +2203,11 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 140 + 157 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 225 + 242 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2219,7 +2227,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 113 + 130 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2247,7 +2255,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 65 + 82 @@ -2299,7 +2307,7 @@ 哎呀!无法解析历史数据。 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts - 223 + 233 @@ -2323,7 +2331,7 @@ 导入 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 91 + 108 apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html @@ -2339,7 +2347,7 @@ 行业 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 159 + 176 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2351,7 +2359,7 @@ 国家 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 168 + 185 apps/client/src/app/components/admin-users/admin-users.html @@ -2367,11 +2375,11 @@ 行业 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 173 + 190 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 295 + 312 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2387,11 +2395,11 @@ 国家 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 183 + 200 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 306 + 323 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -2403,7 +2411,7 @@ 基准 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 253 + 270 @@ -2411,7 +2419,7 @@ 符号映射 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 259 + 276 @@ -2419,7 +2427,7 @@ 刮削配置 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 270 + 287 @@ -2427,7 +2435,7 @@ 笔记 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 317 + 334 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -2483,8 +2491,8 @@ - is an invalid currency! - 是无效的货币! + is an invalid currency! + 是无效的货币! apps/client/src/app/components/admin-overview/admin-overview.component.ts 129 @@ -2871,7 +2879,7 @@ 文件夹 apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts - 110 + 116 apps/client/src/app/pages/portfolio/portfolio-page-routing.module.ts @@ -2883,7 +2891,7 @@ 基准 apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts - 119 + 128 @@ -3043,8 +3051,8 @@ - Last Days - 最后的 + Last Days + 最后的 apps/client/src/app/components/home-market/home-market.html 6 @@ -3171,7 +3179,7 @@ 总金额 apps/client/src/app/components/investment-chart/investment-chart.component.ts - 191 + 142 @@ -3179,7 +3187,7 @@ 储蓄率 apps/client/src/app/components/investment-chart/investment-chart.component.ts - 263 + 214 @@ -3252,12 +3260,12 @@ - - + + - - + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -3693,7 +3701,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 99 + 188 @@ -3705,7 +3713,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 109 + 198 @@ -3717,7 +3725,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 112 + 202 @@ -3729,7 +3737,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 114 + 206 @@ -3741,7 +3749,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 117 + 209 @@ -3841,7 +3849,7 @@ 您确实要删除此登录方法吗? apps/client/src/app/components/user-account-settings/user-account-settings.component.ts - 188 + 189 @@ -3889,7 +3897,7 @@ 语言环境 apps/client/src/app/components/user-account-settings/user-account-settings.html - 113 + 121 @@ -3897,7 +3905,7 @@ 日期和数字格式 apps/client/src/app/components/user-account-settings/user-account-settings.html - 115 + 123 @@ -3905,7 +3913,7 @@ 外貌 apps/client/src/app/components/user-account-settings/user-account-settings.html - 138 + 146 @@ -3913,7 +3921,7 @@ 自动 apps/client/src/app/components/user-account-settings/user-account-settings.html - 152 + 160 @@ -3921,7 +3929,7 @@ 明亮 apps/client/src/app/components/user-account-settings/user-account-settings.html - 153 + 161 @@ -3929,7 +3937,7 @@ 黑暗 apps/client/src/app/components/user-account-settings/user-account-settings.html - 154 + 162 @@ -3937,7 +3945,7 @@ 极简模式 apps/client/src/app/components/user-account-settings/user-account-settings.html - 163 + 171 apps/client/src/app/pages/features/features-page.html @@ -3949,7 +3957,7 @@ 动荡时期的无干扰体验 apps/client/src/app/components/user-account-settings/user-account-settings.html - 164 + 172 @@ -3957,7 +3965,7 @@ 生物识别认证 apps/client/src/app/components/user-account-settings/user-account-settings.html - 180 + 188 @@ -3965,7 +3973,7 @@ 使用指纹登录 apps/client/src/app/components/user-account-settings/user-account-settings.html - 181 + 189 @@ -3973,7 +3981,7 @@ 实验性功能 apps/client/src/app/components/user-account-settings/user-account-settings.html - 198 + 206 @@ -3981,7 +3989,7 @@ 预览即将推出的功能 apps/client/src/app/components/user-account-settings/user-account-settings.html - 199 + 207 @@ -3989,11 +3997,11 @@ 用户身份 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 47 + 45 apps/client/src/app/components/user-account-settings/user-account-settings.html - 215 + 223 @@ -4001,7 +4009,7 @@ 导出数据 apps/client/src/app/components/user-account-settings/user-account-settings.html - 223 + 231 @@ -4621,8 +4629,8 @@ - Protect your assets. Refine your personal investment strategy. - 保护你的资产。完善你的个人投资策略 + Protect your assets. Refine your personal investment strategy. + 保护你的资产。完善你的个人投资策略 apps/client/src/app/pages/landing/landing-page.html 221 @@ -4685,8 +4693,8 @@ - Why Ghostfolio? - 为什么使用Ghostfolio + Why Ghostfolio? + 为什么使用Ghostfolio apps/client/src/app/pages/landing/landing-page.html 268 @@ -4757,8 +4765,8 @@ - saying no to spreadsheets in - 对电子表格说不 + saying no to spreadsheets in + 对电子表格说不 apps/client/src/app/pages/landing/landing-page.html 307 @@ -4781,24 +4789,24 @@ - What our users are saying - 我们的什么用户正在说 + What our users are saying + 我们的什么用户正在说 apps/client/src/app/pages/landing/landing-page.html 323 - Members from around the globe are using Ghostfolio Premium - 来自世界各地的会员正在使用Ghostfolio 高级版 + Members from around the globe are using Ghostfolio Premium + 来自世界各地的会员正在使用Ghostfolio 高级版 apps/client/src/app/pages/landing/landing-page.html 355 - How does Ghostfolio work? - 如何幽灵作品集工作? + How does Ghostfolio work? + 如何幽灵作品集工作? apps/client/src/app/pages/landing/landing-page.html 367 @@ -4821,8 +4829,8 @@ - * no e-mail address nor credit card required - * 无需电子邮件地址或信用卡 + * no e-mail address nor credit card required + * 无需电子邮件地址或信用卡 apps/client/src/app/pages/landing/landing-page.html 378 @@ -4845,24 +4853,24 @@ - Are you ready? - 准备好? + Are you ready? + 准备好? apps/client/src/app/pages/landing/landing-page.html 413 - Join now or check out the example account - 立即加入或查看示例帐户 + Join now or check out the example account + 立即加入或查看示例帐户 apps/client/src/app/pages/landing/landing-page.html 414 - At Ghostfolio, transparency is at the core of our values. We publish the source code as open source software (OSS) under the AGPL-3.0 license and we openly share aggregated key metrics of the platform’s operational status. - 在 Ghostfolio,透明度是我们价值观的核心。我们将源代码发布为开源软件(OSS)下AGPL-3.0许可证我们公开分享平台运营状态的汇总关键指标。 + At Ghostfolio, transparency is at the core of our values. We publish the source code as open source software (OSS) under the AGPL-3.0 license and we openly share aggregated key metrics of the platform’s operational status. + 在 Ghostfolio,透明度是我们价值观的核心。我们将源代码发布为开源软件(OSS)下AGPL-3.0许可证我们公开分享平台运营状态的汇总关键指标。 apps/client/src/app/pages/open/open-page.html 6 @@ -4957,7 +4965,7 @@ 您真的要删除所有活动吗? apps/client/src/app/pages/portfolio/activities/activities-page.component.ts - 168 + 171 @@ -5389,7 +5397,7 @@ 股息 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 42 + 43 libs/ui/src/lib/i18n.ts @@ -5409,7 +5417,7 @@ 每月 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 54 + 55 @@ -5417,7 +5425,7 @@ 每年 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 55 + 56 @@ -5585,8 +5593,8 @@ - If you prefer to run Ghostfolio on your own infrastructure, please find the source code and further instructions on GitHub. - 如果你希望在自己的基础设施上运行 Ghostfolio,请查看源代码和进一步的说明GitHub + If you prefer to run Ghostfolio on your own infrastructure, please find the source code and further instructions on GitHub. + 如果你希望在自己的基础设施上运行 Ghostfolio,请查看源代码和进一步的说明GitHub apps/client/src/app/pages/pricing/pricing-page.html 24 @@ -5769,8 +5777,8 @@ - Hello, has shared a Portfolio with you! - 你好,分享了一个文件夹与你! + Hello, has shared a Portfolio with you! + 你好,分享了一个文件夹与你! apps/client/src/app/pages/public/public-page.html 4 @@ -5833,8 +5841,8 @@ - I agree to have stored my Security Token from above in a secure place. If I lose it, I cannot get my account back. - 我同意存储我的保安编码器从上面看,在一个安全的地方。如果我丢失了它,我将无法找回我的帐户。 + I agree to have stored my Security Token from above in a secure place. If I lose it, I cannot get my account back. + 我同意存储我的保安编码器从上面看,在一个安全的地方。如果我丢失了它,我将无法找回我的帐户。 apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html 32 @@ -5869,8 +5877,8 @@ - Open Source Alternative to - 开源替代方案 + Open Source Alternative to + 开源替代方案 apps/client/src/app/pages/resources/personal-finance-tools/personal-finance-tools-page-routing.module.ts 26 @@ -5885,8 +5893,8 @@ - This overview page features a curated collection of personal finance tools compared to the open source alternative Ghostfolio. If you value transparency, data privacy, and community collaboration, Ghostfolio provides an excellent opportunity to take control of your financial management. - 此概述页面包含与开源替代方案相比的精选个人理财工具集合Ghostfolio。如果您重视透明度、数据隐私和社区协作,Ghostfolio 提供了一个绝佳的机会来控制您的财务管理。 + This overview page features a curated collection of personal finance tools compared to the open source alternative Ghostfolio. If you value transparency, data privacy, and community collaboration, Ghostfolio provides an excellent opportunity to take control of your financial management. + 此概述页面包含与开源替代方案相比的精选个人理财工具集合Ghostfolio。如果您重视透明度、数据隐私和社区协作,Ghostfolio 提供了一个绝佳的机会来控制您的财务管理。 apps/client/src/app/pages/resources/personal-finance-tools/personal-finance-tools-page.html 8 @@ -5901,8 +5909,8 @@ - Open Source Alternative to - 开源替代方案 + Open Source Alternative to + 开源替代方案 apps/client/src/app/pages/resources/personal-finance-tools/personal-finance-tools-page.html 38 @@ -6113,8 +6121,8 @@ - Are you looking for an open source alternative to ? Ghostfolio is a powerful portfolio management tool that provides individuals with a comprehensive platform to track, analyze, and optimize their investments. Whether you are an experienced investor or just starting out, Ghostfolio offers an intuitive user interface and a wide range of functionalities to help you make informed decisions and take control of your financial future. - 您是否正在寻找开源替代方案幽灵作品集是一个强大的投资组合管理工具,为个人提供一个全面的平台来跟踪、分析和优化他们的投资。无论您是经验丰富的投资者还是刚刚起步的投资者,Ghostfolio 都提供直观的用户界面和广泛的功能帮助您做出明智的决定并掌控您的财务未来。 + Are you looking for an open source alternative to ? Ghostfolio is a powerful portfolio management tool that provides individuals with a comprehensive platform to track, analyze, and optimize their investments. Whether you are an experienced investor or just starting out, Ghostfolio offers an intuitive user interface and a wide range of functionalities to help you make informed decisions and take control of your financial future. + 您是否正在寻找开源替代方案幽灵作品集是一个强大的投资组合管理工具,为个人提供一个全面的平台来跟踪、分析和优化他们的投资。无论您是经验丰富的投资者还是刚刚起步的投资者,Ghostfolio 都提供直观的用户界面和广泛的功能帮助您做出明智的决定并掌控您的财务未来。 apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html 13 @@ -6317,8 +6325,8 @@ - Ghostfolio is an open source software (OSS), providing a cost-effective alternative to making it particularly suitable for individuals on a tight budget, such as those pursuing Financial Independence, Retire Early (FIRE). By leveraging the collective efforts of a community of developers and personal finance enthusiasts, Ghostfolio continuously enhances its capabilities, security, and user experience. - Ghostfolio 是一款开源软件 (OSS),提供了一种经济高效的替代方案使其特别适合预算紧张的个人,例如追求财务独立,提前退休(FIRE) 。通过利用开发者社区和个人理财爱好者的集体努力,Ghostfolio 不断增强其功能、安全性和用户体验。 + Ghostfolio is an open source software (OSS), providing a cost-effective alternative to making it particularly suitable for individuals on a tight budget, such as those pursuing Financial Independence, Retire Early (FIRE). By leveraging the collective efforts of a community of developers and personal finance enthusiasts, Ghostfolio continuously enhances its capabilities, security, and user experience. + Ghostfolio 是一款开源软件 (OSS),提供了一种经济高效的替代方案使其特别适合预算紧张的个人,例如追求财务独立,提前退休(FIRE) 。通过利用开发者社区和个人理财爱好者的集体努力,Ghostfolio 不断增强其功能、安全性和用户体验。 apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html 27 @@ -6521,8 +6529,8 @@ - Let’s dive deeper into the detailed Ghostfolio vs comparison table below to gain a thorough understanding of how Ghostfolio positions itself relative to . We will explore various aspects such as features, data privacy, pricing, and more, allowing you to make a well-informed choice for your personal requirements. - 让我们更深入地了解 Ghostfolio 与下面的比较表可帮助您全面了解 Ghostfolio 相对于其他产品的定位。我们将探讨功能、数据隐私、定价等各个方面,让您根据个人需求做出明智的选择。 + Let’s dive deeper into the detailed Ghostfolio vs comparison table below to gain a thorough understanding of how Ghostfolio positions itself relative to . We will explore various aspects such as features, data privacy, pricing, and more, allowing you to make a well-informed choice for your personal requirements. + 让我们更深入地了解 Ghostfolio 与下面的比较表可帮助您全面了解 Ghostfolio 相对于其他产品的定位。我们将探讨功能、数据隐私、定价等各个方面,让您根据个人需求做出明智的选择。 apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html 38 @@ -6725,8 +6733,8 @@ - Ghostfolio vs comparison table - Ghostfolio vs比较表 + Ghostfolio vs comparison table + Ghostfolio vs比较表 apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html 49 @@ -12581,8 +12589,8 @@ - Please note that the information provided in the Ghostfolio vs comparison table is based on our independent research and analysis. This website is not affiliated with or any other product mentioned in the comparison. As the landscape of personal finance tools evolves, it is essential to verify any specific details or changes directly from the respective product page. Data needs a refresh? Help us maintain accurate data on GitHub. - 请注意,Ghostfolio 与 Ghostfolio 中提供的信息比较表基于我们的独立研究和分析。该网站不隶属于或比较中提到的任何其他产品。随着个人理财工具格局的不断发展,直接从相应的产品页面验证任何具体的细节或变化至关重要。数据需要刷新吗?帮助我们维护准确的数据GitHub + Please note that the information provided in the Ghostfolio vs comparison table is based on our independent research and analysis. This website is not affiliated with or any other product mentioned in the comparison. As the landscape of personal finance tools evolves, it is essential to verify any specific details or changes directly from the respective product page. Data needs a refresh? Help us maintain accurate data on GitHub. + 请注意,Ghostfolio 与 Ghostfolio 中提供的信息比较表基于我们的独立研究和分析。该网站不隶属于或比较中提到的任何其他产品。随着个人理财工具格局的不断发展,直接从相应的产品页面验证任何具体的细节或变化至关重要。数据需要刷新吗?帮助我们维护准确的数据GitHub apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html 210 @@ -12785,8 +12793,8 @@ - Ready to take your investments to the next level? - 准备好带走你的投资下一级 + Ready to take your investments to the next level? + 准备好带走你的投资下一级 apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html 223 @@ -14049,7 +14057,7 @@ 查找持有... libs/ui/src/lib/assistant/assistant.component.ts - 126 + 111 @@ -14637,8 +14645,8 @@ - If a translation is missing, kindly support us in extending it here. - 如果翻译缺失,请支持我们进行扩展这里 + If a translation is missing, kindly support us in extending it here. + 如果翻译缺失,请支持我们进行扩展这里 apps/client/src/app/components/user-account-settings/user-account-settings.html 55 @@ -14657,7 +14665,7 @@ 当前市场价格为 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts - 317 + 327 @@ -14665,7 +14673,7 @@ 测试 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 288 + 305 @@ -14757,11 +14765,11 @@ 投资 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 46 + 47 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 60 + 61 @@ -14793,7 +14801,7 @@ 今年迄今为止 libs/ui/src/lib/assistant/assistant.component.ts - 109 + 198 @@ -14801,7 +14809,7 @@ 本周至今 libs/ui/src/lib/assistant/assistant.component.ts - 101 + 190 @@ -14809,7 +14817,7 @@ 本月至今 libs/ui/src/lib/assistant/assistant.component.ts - 105 + 194 @@ -14817,7 +14825,7 @@ 最大输运量 libs/ui/src/lib/assistant/assistant.component.ts - 105 + 194 @@ -14825,7 +14833,7 @@ 世界贸易组织 libs/ui/src/lib/assistant/assistant.component.ts - 101 + 190 @@ -14845,7 +14853,7 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html - 39 + 38 @@ -14857,8 +14865,8 @@ - If you retire today, you would be able to withdraw per year or per month, based on your total assets of and a withdrawal rate of 4%. - 如果你今天退休,你可以领取每年或者每月,根据您的总资产提款率为4%。 + If you retire today, you would be able to withdraw per year or per month, based on your total assets of and a withdrawal rate of 4%. + 如果你今天退休,你可以领取每年或者每月,根据您的总资产提款率为4%。 apps/client/src/app/pages/portfolio/fire/fire-page.html 68 @@ -14869,7 +14877,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 112 + 202 @@ -14877,7 +14885,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 114 + 206 From 8cd6c34ed83071cb93124a34b17cff1bc740294d Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Sun, 31 Mar 2024 20:07:58 +0200 Subject: [PATCH 086/120] Feature/introduce portfolio calculator factory (#3214) * Introduce portfolio calculator factory * Update changelog --- CHANGELOG.md | 1 + .../calculator/mwr/portfolio-calculator.ts | 37 + .../portfolio-calculator-test-utils.ts | 25 + .../portfolio-calculator.factory.ts | 51 ++ .../calculator/portfolio-calculator.ts | 788 ++++++++++++++++++ ...aln-buy-and-sell-in-two-activities.spec.ts | 107 ++- ...folio-calculator-baln-buy-and-sell.spec.ts | 81 +- .../twr/portfolio-calculator-baln-buy.spec.ts | 55 +- ...ator-btcusd-buy-and-sell-partially.spec.ts | 81 +- .../portfolio-calculator-googl-buy.spec.ts | 55 +- ...-calculator-msft-buy-with-dividend.spec.ts | 81 +- .../portfolio-calculator-no-orders.spec.ts | 20 +- ...ulator-novn-buy-and-sell-partially.spec.ts | 82 +- ...folio-calculator-novn-buy-and-sell.spec.ts | 81 +- .../twr/portfolio-calculator.spec.ts | 25 +- .../calculator/twr/portfolio-calculator.ts | 765 +---------------- .../interfaces/current-positions.interface.ts | 1 + ...e.ts => portfolio-order-item.interface.ts} | 0 .../api/src/app/portfolio/portfolio.module.ts | 2 + .../src/app/portfolio/portfolio.service.ts | 75 +- 20 files changed, 1361 insertions(+), 1052 deletions(-) create mode 100644 apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts create mode 100644 apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts create mode 100644 apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts create mode 100644 apps/api/src/app/portfolio/calculator/portfolio-calculator.ts rename apps/api/src/app/portfolio/interfaces/{portfolio-calculator.interface.ts => portfolio-order-item.interface.ts} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ce4ef296..7a0b55060 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Improved the usability of the date range support by specific years (`2023`, `2022`, `2021`, etc.) in the assistant (experimental) +- Introduced a factory for the portfolio calculations to support different algorithms in future ## 2.69.0 - 2024-03-30 diff --git a/apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts new file mode 100644 index 000000000..ec744f624 --- /dev/null +++ b/apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts @@ -0,0 +1,37 @@ +import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; +import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface'; +import { + SymbolMetrics, + TimelinePosition, + UniqueAsset +} from '@ghostfolio/common/interfaces'; + +export class MWRPortfolioCalculator extends PortfolioCalculator { + protected calculateOverallPerformance( + positions: TimelinePosition[] + ): CurrentPositions { + throw new Error('Method not implemented.'); + } + + protected getSymbolMetrics({ + dataSource, + end, + exchangeRates, + isChartMode = false, + marketSymbolMap, + start, + step = 1, + symbol + }: { + end: Date; + exchangeRates: { [dateString: string]: number }; + isChartMode?: boolean; + marketSymbolMap: { + [date: string]: { [symbol: string]: Big }; + }; + start: Date; + step?: number; + } & UniqueAsset): SymbolMetrics { + throw new Error('Method not implemented.'); + } +} diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts new file mode 100644 index 000000000..6d1939fcd --- /dev/null +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts @@ -0,0 +1,25 @@ +export const activityDummyData = { + accountId: undefined, + accountUserId: undefined, + comment: undefined, + createdAt: new Date(), + feeInBaseCurrency: undefined, + id: undefined, + isDraft: false, + symbolProfileId: undefined, + updatedAt: new Date(), + userId: undefined, + value: undefined, + valueInBaseCurrency: undefined +}; + +export const symbolProfileDummyData = { + activitiesCount: undefined, + assetClass: undefined, + assetSubClass: undefined, + countries: [], + createdAt: undefined, + id: undefined, + sectors: [], + updatedAt: undefined +}; diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts new file mode 100644 index 000000000..cf1fe9324 --- /dev/null +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts @@ -0,0 +1,51 @@ +import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; +import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; + +import { Injectable } from '@nestjs/common'; + +import { MWRPortfolioCalculator } from './mwr/portfolio-calculator'; +import { PortfolioCalculator } from './portfolio-calculator'; +import { TWRPortfolioCalculator } from './twr/portfolio-calculator'; + +export enum PerformanceCalculationType { + MWR = 'MWR', // Money-Weighted Rate of Return + TWR = 'TWR' // Time-Weighted Rate of Return +} + +@Injectable() +export class PortfolioCalculatorFactory { + public constructor( + private readonly currentRateService: CurrentRateService, + private readonly exchangeRateDataService: ExchangeRateDataService + ) {} + + public createCalculator({ + activities, + calculationType, + currency + }: { + activities: Activity[]; + calculationType: PerformanceCalculationType; + currency: string; + }): PortfolioCalculator { + switch (calculationType) { + case PerformanceCalculationType.MWR: + return new MWRPortfolioCalculator({ + activities, + currency, + currentRateService: this.currentRateService, + exchangeRateDataService: this.exchangeRateDataService + }); + case PerformanceCalculationType.TWR: + return new TWRPortfolioCalculator({ + activities, + currency, + currentRateService: this.currentRateService, + exchangeRateDataService: this.exchangeRateDataService + }); + default: + throw new Error('Invalid calculation type'); + } + } +} diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts new file mode 100644 index 000000000..c3edc86d9 --- /dev/null +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -0,0 +1,788 @@ +import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; +import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; +import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface'; +import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface'; +import { TransactionPointSymbol } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point-symbol.interface'; +import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface'; +import { getFactor } from '@ghostfolio/api/helper/portfolio.helper'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; +import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; +import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; +import { + DataProviderInfo, + HistoricalDataItem, + InvestmentItem, + ResponseError, + SymbolMetrics, + TimelinePosition, + UniqueAsset +} from '@ghostfolio/common/interfaces'; +import { GroupBy } from '@ghostfolio/common/types'; + +import { Big } from 'big.js'; +import { + eachDayOfInterval, + endOfDay, + format, + isBefore, + isSameDay, + max, + subDays +} from 'date-fns'; +import { isNumber, last, uniq } from 'lodash'; + +export abstract class PortfolioCalculator { + protected static readonly ENABLE_LOGGING = false; + + protected orders: PortfolioOrder[]; + + private currency: string; + private currentRateService: CurrentRateService; + private dataProviderInfos: DataProviderInfo[]; + private exchangeRateDataService: ExchangeRateDataService; + private transactionPoints: TransactionPoint[]; + + public constructor({ + activities, + currency, + currentRateService, + exchangeRateDataService + }: { + activities: Activity[]; + currency: string; + currentRateService: CurrentRateService; + exchangeRateDataService: ExchangeRateDataService; + }) { + this.currency = currency; + this.currentRateService = currentRateService; + this.exchangeRateDataService = exchangeRateDataService; + this.orders = activities.map( + ({ date, fee, quantity, SymbolProfile, type, unitPrice }) => { + return { + SymbolProfile, + type, + date: format(date, DATE_FORMAT), + fee: new Big(fee), + quantity: new Big(quantity), + unitPrice: new Big(unitPrice) + }; + } + ); + + this.orders.sort((a, b) => { + return a.date?.localeCompare(b.date); + }); + + this.computeTransactionPoints(); + } + + protected abstract calculateOverallPerformance( + positions: TimelinePosition[] + ): CurrentPositions; + + public getAnnualizedPerformancePercent({ + daysInMarket, + netPerformancePercent + }: { + daysInMarket: number; + netPerformancePercent: Big; + }): Big { + if (isNumber(daysInMarket) && daysInMarket > 0) { + const exponent = new Big(365).div(daysInMarket).toNumber(); + return new Big( + Math.pow(netPerformancePercent.plus(1).toNumber(), exponent) + ).minus(1); + } + + return new Big(0); + } + + public async getChartData({ + end = new Date(Date.now()), + start, + step = 1 + }: { + end?: Date; + start: Date; + step?: number; + }): Promise { + const symbols: { [symbol: string]: boolean } = {}; + + const transactionPointsBeforeEndDate = + this.transactionPoints?.filter((transactionPoint) => { + return isBefore(parseDate(transactionPoint.date), end); + }) ?? []; + + const currencies: { [symbol: string]: string } = {}; + const dataGatheringItems: IDataGatheringItem[] = []; + const firstIndex = transactionPointsBeforeEndDate.length; + + let dates = eachDayOfInterval({ start, end }, { step }).map((date) => { + return resetHours(date); + }); + + const includesEndDate = isSameDay(last(dates), end); + + if (!includesEndDate) { + dates.push(resetHours(end)); + } + + if (transactionPointsBeforeEndDate.length > 0) { + for (const { + currency, + dataSource, + symbol + } of transactionPointsBeforeEndDate[firstIndex - 1].items) { + dataGatheringItems.push({ + dataSource, + symbol + }); + currencies[symbol] = currency; + symbols[symbol] = true; + } + } + + const { dataProviderInfos, values: marketSymbols } = + await this.currentRateService.getValues({ + dataGatheringItems, + dateQuery: { + in: dates + } + }); + + this.dataProviderInfos = dataProviderInfos; + + const marketSymbolMap: { + [date: string]: { [symbol: string]: Big }; + } = {}; + + let exchangeRatesByCurrency = + await this.exchangeRateDataService.getExchangeRatesByCurrency({ + currencies: uniq(Object.values(currencies)), + endDate: endOfDay(end), + startDate: this.getStartDate(), + targetCurrency: this.currency + }); + + for (const marketSymbol of marketSymbols) { + const dateString = format(marketSymbol.date, DATE_FORMAT); + if (!marketSymbolMap[dateString]) { + marketSymbolMap[dateString] = {}; + } + if (marketSymbol.marketPrice) { + marketSymbolMap[dateString][marketSymbol.symbol] = new Big( + marketSymbol.marketPrice + ); + } + } + + const accumulatedValuesByDate: { + [date: string]: { + investmentValueWithCurrencyEffect: Big; + totalCurrentValue: Big; + totalCurrentValueWithCurrencyEffect: Big; + totalInvestmentValue: Big; + totalInvestmentValueWithCurrencyEffect: Big; + totalNetPerformanceValue: Big; + totalNetPerformanceValueWithCurrencyEffect: Big; + totalTimeWeightedInvestmentValue: Big; + totalTimeWeightedInvestmentValueWithCurrencyEffect: Big; + }; + } = {}; + + const valuesBySymbol: { + [symbol: string]: { + currentValues: { [date: string]: Big }; + currentValuesWithCurrencyEffect: { [date: string]: Big }; + investmentValuesAccumulated: { [date: string]: Big }; + investmentValuesAccumulatedWithCurrencyEffect: { [date: string]: Big }; + investmentValuesWithCurrencyEffect: { [date: string]: Big }; + netPerformanceValues: { [date: string]: Big }; + netPerformanceValuesWithCurrencyEffect: { [date: string]: Big }; + timeWeightedInvestmentValues: { [date: string]: Big }; + timeWeightedInvestmentValuesWithCurrencyEffect: { [date: string]: Big }; + }; + } = {}; + + for (const symbol of Object.keys(symbols)) { + const { + currentValues, + currentValuesWithCurrencyEffect, + investmentValuesAccumulated, + investmentValuesAccumulatedWithCurrencyEffect, + investmentValuesWithCurrencyEffect, + netPerformanceValues, + netPerformanceValuesWithCurrencyEffect, + timeWeightedInvestmentValues, + timeWeightedInvestmentValuesWithCurrencyEffect + } = this.getSymbolMetrics({ + end, + marketSymbolMap, + start, + step, + symbol, + dataSource: null, + exchangeRates: + exchangeRatesByCurrency[`${currencies[symbol]}${this.currency}`], + isChartMode: true + }); + + valuesBySymbol[symbol] = { + currentValues, + currentValuesWithCurrencyEffect, + investmentValuesAccumulated, + investmentValuesAccumulatedWithCurrencyEffect, + investmentValuesWithCurrencyEffect, + netPerformanceValues, + netPerformanceValuesWithCurrencyEffect, + timeWeightedInvestmentValues, + timeWeightedInvestmentValuesWithCurrencyEffect + }; + } + + for (const currentDate of dates) { + const dateString = format(currentDate, DATE_FORMAT); + + for (const symbol of Object.keys(valuesBySymbol)) { + const symbolValues = valuesBySymbol[symbol]; + + const currentValue = + symbolValues.currentValues?.[dateString] ?? new Big(0); + + const currentValueWithCurrencyEffect = + symbolValues.currentValuesWithCurrencyEffect?.[dateString] ?? + new Big(0); + + const investmentValueAccumulated = + symbolValues.investmentValuesAccumulated?.[dateString] ?? new Big(0); + + const investmentValueAccumulatedWithCurrencyEffect = + symbolValues.investmentValuesAccumulatedWithCurrencyEffect?.[ + dateString + ] ?? new Big(0); + + const investmentValueWithCurrencyEffect = + symbolValues.investmentValuesWithCurrencyEffect?.[dateString] ?? + new Big(0); + + const netPerformanceValue = + symbolValues.netPerformanceValues?.[dateString] ?? new Big(0); + + const netPerformanceValueWithCurrencyEffect = + symbolValues.netPerformanceValuesWithCurrencyEffect?.[dateString] ?? + new Big(0); + + const timeWeightedInvestmentValue = + symbolValues.timeWeightedInvestmentValues?.[dateString] ?? new Big(0); + + const timeWeightedInvestmentValueWithCurrencyEffect = + symbolValues.timeWeightedInvestmentValuesWithCurrencyEffect?.[ + dateString + ] ?? new Big(0); + + accumulatedValuesByDate[dateString] = { + investmentValueWithCurrencyEffect: ( + accumulatedValuesByDate[dateString] + ?.investmentValueWithCurrencyEffect ?? new Big(0) + ).add(investmentValueWithCurrencyEffect), + totalCurrentValue: ( + accumulatedValuesByDate[dateString]?.totalCurrentValue ?? new Big(0) + ).add(currentValue), + totalCurrentValueWithCurrencyEffect: ( + accumulatedValuesByDate[dateString] + ?.totalCurrentValueWithCurrencyEffect ?? new Big(0) + ).add(currentValueWithCurrencyEffect), + totalInvestmentValue: ( + accumulatedValuesByDate[dateString]?.totalInvestmentValue ?? + new Big(0) + ).add(investmentValueAccumulated), + totalInvestmentValueWithCurrencyEffect: ( + accumulatedValuesByDate[dateString] + ?.totalInvestmentValueWithCurrencyEffect ?? new Big(0) + ).add(investmentValueAccumulatedWithCurrencyEffect), + totalNetPerformanceValue: ( + accumulatedValuesByDate[dateString]?.totalNetPerformanceValue ?? + new Big(0) + ).add(netPerformanceValue), + totalNetPerformanceValueWithCurrencyEffect: ( + accumulatedValuesByDate[dateString] + ?.totalNetPerformanceValueWithCurrencyEffect ?? new Big(0) + ).add(netPerformanceValueWithCurrencyEffect), + totalTimeWeightedInvestmentValue: ( + accumulatedValuesByDate[dateString] + ?.totalTimeWeightedInvestmentValue ?? new Big(0) + ).add(timeWeightedInvestmentValue), + totalTimeWeightedInvestmentValueWithCurrencyEffect: ( + accumulatedValuesByDate[dateString] + ?.totalTimeWeightedInvestmentValueWithCurrencyEffect ?? new Big(0) + ).add(timeWeightedInvestmentValueWithCurrencyEffect) + }; + } + } + + return Object.entries(accumulatedValuesByDate).map(([date, values]) => { + const { + investmentValueWithCurrencyEffect, + totalCurrentValue, + totalCurrentValueWithCurrencyEffect, + totalInvestmentValue, + totalInvestmentValueWithCurrencyEffect, + totalNetPerformanceValue, + totalNetPerformanceValueWithCurrencyEffect, + totalTimeWeightedInvestmentValue, + totalTimeWeightedInvestmentValueWithCurrencyEffect + } = values; + + const netPerformanceInPercentage = totalTimeWeightedInvestmentValue.eq(0) + ? 0 + : totalNetPerformanceValue + .div(totalTimeWeightedInvestmentValue) + .mul(100) + .toNumber(); + + const netPerformanceInPercentageWithCurrencyEffect = + totalTimeWeightedInvestmentValueWithCurrencyEffect.eq(0) + ? 0 + : totalNetPerformanceValueWithCurrencyEffect + .div(totalTimeWeightedInvestmentValueWithCurrencyEffect) + .mul(100) + .toNumber(); + + return { + date, + netPerformanceInPercentage, + netPerformanceInPercentageWithCurrencyEffect, + investmentValueWithCurrencyEffect: + investmentValueWithCurrencyEffect.toNumber(), + netPerformance: totalNetPerformanceValue.toNumber(), + netPerformanceWithCurrencyEffect: + totalNetPerformanceValueWithCurrencyEffect.toNumber(), + totalInvestment: totalInvestmentValue.toNumber(), + totalInvestmentValueWithCurrencyEffect: + totalInvestmentValueWithCurrencyEffect.toNumber(), + value: totalCurrentValue.toNumber(), + valueWithCurrencyEffect: totalCurrentValueWithCurrencyEffect.toNumber() + }; + }); + } + + public async getCurrentPositions( + start: Date, + end?: Date + ): Promise { + const lastTransactionPoint = last(this.transactionPoints); + + let endDate = end; + + if (!endDate) { + endDate = new Date(Date.now()); + + if (lastTransactionPoint) { + endDate = max([endDate, parseDate(lastTransactionPoint.date)]); + } + } + + const transactionPoints = this.transactionPoints?.filter(({ date }) => { + return isBefore(parseDate(date), endDate); + }); + + if (!transactionPoints.length) { + return { + currentValueInBaseCurrency: new Big(0), + grossPerformance: new Big(0), + grossPerformancePercentage: new Big(0), + grossPerformancePercentageWithCurrencyEffect: new Big(0), + grossPerformanceWithCurrencyEffect: new Big(0), + hasErrors: false, + netPerformance: new Big(0), + netPerformancePercentage: new Big(0), + netPerformancePercentageWithCurrencyEffect: new Big(0), + netPerformanceWithCurrencyEffect: new Big(0), + positions: [], + totalInvestment: new Big(0), + totalInvestmentWithCurrencyEffect: new Big(0) + }; + } + + const currencies: { [symbol: string]: string } = {}; + const dataGatheringItems: IDataGatheringItem[] = []; + let dates: Date[] = []; + let firstIndex = transactionPoints.length; + let firstTransactionPoint: TransactionPoint = null; + + dates.push(resetHours(start)); + + for (const { currency, dataSource, symbol } of transactionPoints[ + firstIndex - 1 + ].items) { + dataGatheringItems.push({ + dataSource, + symbol + }); + + currencies[symbol] = currency; + } + + for (let i = 0; i < transactionPoints.length; i++) { + if ( + !isBefore(parseDate(transactionPoints[i].date), start) && + firstTransactionPoint === null + ) { + firstTransactionPoint = transactionPoints[i]; + firstIndex = i; + } + + if (firstTransactionPoint !== null) { + dates.push(resetHours(parseDate(transactionPoints[i].date))); + } + } + + dates.push(resetHours(endDate)); + + // Add dates of last week for fallback + dates.push(subDays(resetHours(new Date()), 7)); + dates.push(subDays(resetHours(new Date()), 6)); + dates.push(subDays(resetHours(new Date()), 5)); + dates.push(subDays(resetHours(new Date()), 4)); + dates.push(subDays(resetHours(new Date()), 3)); + dates.push(subDays(resetHours(new Date()), 2)); + dates.push(subDays(resetHours(new Date()), 1)); + dates.push(resetHours(new Date())); + + dates = uniq( + dates.map((date) => { + return date.getTime(); + }) + ) + .map((timestamp) => { + return new Date(timestamp); + }) + .sort((a, b) => { + return a.getTime() - b.getTime(); + }); + + let exchangeRatesByCurrency = + await this.exchangeRateDataService.getExchangeRatesByCurrency({ + currencies: uniq(Object.values(currencies)), + endDate: endOfDay(endDate), + startDate: this.getStartDate(), + targetCurrency: this.currency + }); + + const { + dataProviderInfos, + errors: currentRateErrors, + values: marketSymbols + } = await this.currentRateService.getValues({ + dataGatheringItems, + dateQuery: { + in: dates + } + }); + + this.dataProviderInfos = dataProviderInfos; + + const marketSymbolMap: { + [date: string]: { [symbol: string]: Big }; + } = {}; + + for (const marketSymbol of marketSymbols) { + const date = format(marketSymbol.date, DATE_FORMAT); + + if (!marketSymbolMap[date]) { + marketSymbolMap[date] = {}; + } + + if (marketSymbol.marketPrice) { + marketSymbolMap[date][marketSymbol.symbol] = new Big( + marketSymbol.marketPrice + ); + } + } + + const endDateString = format(endDate, DATE_FORMAT); + + if (firstIndex > 0) { + firstIndex--; + } + + const positions: TimelinePosition[] = []; + let hasAnySymbolMetricsErrors = false; + + const errors: ResponseError['errors'] = []; + + for (const item of lastTransactionPoint.items) { + const marketPriceInBaseCurrency = ( + marketSymbolMap[endDateString]?.[item.symbol] ?? item.averagePrice + ).mul( + exchangeRatesByCurrency[`${item.currency}${this.currency}`]?.[ + endDateString + ] + ); + + const { + grossPerformance, + grossPerformancePercentage, + grossPerformancePercentageWithCurrencyEffect, + grossPerformanceWithCurrencyEffect, + hasErrors, + netPerformance, + netPerformancePercentage, + netPerformancePercentageWithCurrencyEffect, + netPerformanceWithCurrencyEffect, + timeWeightedInvestment, + timeWeightedInvestmentWithCurrencyEffect, + totalDividend, + totalDividendInBaseCurrency, + totalInvestment, + totalInvestmentWithCurrencyEffect + } = this.getSymbolMetrics({ + marketSymbolMap, + start, + dataSource: item.dataSource, + end: endDate, + exchangeRates: + exchangeRatesByCurrency[`${item.currency}${this.currency}`], + symbol: item.symbol + }); + + hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors; + + positions.push({ + dividend: totalDividend, + dividendInBaseCurrency: totalDividendInBaseCurrency, + timeWeightedInvestment, + timeWeightedInvestmentWithCurrencyEffect, + averagePrice: item.averagePrice, + currency: item.currency, + dataSource: item.dataSource, + fee: item.fee, + firstBuyDate: item.firstBuyDate, + grossPerformance: !hasErrors ? grossPerformance ?? null : null, + grossPerformancePercentage: !hasErrors + ? grossPerformancePercentage ?? null + : null, + grossPerformancePercentageWithCurrencyEffect: !hasErrors + ? grossPerformancePercentageWithCurrencyEffect ?? null + : null, + grossPerformanceWithCurrencyEffect: !hasErrors + ? grossPerformanceWithCurrencyEffect ?? null + : null, + investment: totalInvestment, + investmentWithCurrencyEffect: totalInvestmentWithCurrencyEffect, + marketPrice: + marketSymbolMap[endDateString]?.[item.symbol]?.toNumber() ?? null, + marketPriceInBaseCurrency: + marketPriceInBaseCurrency?.toNumber() ?? null, + netPerformance: !hasErrors ? netPerformance ?? null : null, + netPerformancePercentage: !hasErrors + ? netPerformancePercentage ?? null + : null, + netPerformancePercentageWithCurrencyEffect: !hasErrors + ? netPerformancePercentageWithCurrencyEffect ?? null + : null, + netPerformanceWithCurrencyEffect: !hasErrors + ? netPerformanceWithCurrencyEffect ?? null + : null, + quantity: item.quantity, + symbol: item.symbol, + tags: item.tags, + transactionCount: item.transactionCount, + valueInBaseCurrency: new Big(marketPriceInBaseCurrency).mul( + item.quantity + ) + }); + + if ( + (hasErrors || + currentRateErrors.find(({ dataSource, symbol }) => { + return dataSource === item.dataSource && symbol === item.symbol; + })) && + item.investment.gt(0) + ) { + errors.push({ dataSource: item.dataSource, symbol: item.symbol }); + } + } + + const overall = this.calculateOverallPerformance(positions); + + return { + ...overall, + errors, + positions, + hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors + }; + } + + public getDataProviderInfos() { + return this.dataProviderInfos; + } + + public getInvestments(): { date: string; investment: Big }[] { + if (this.transactionPoints.length === 0) { + return []; + } + + return this.transactionPoints.map((transactionPoint) => { + return { + date: transactionPoint.date, + investment: transactionPoint.items.reduce( + (investment, transactionPointSymbol) => + investment.plus(transactionPointSymbol.investment), + new Big(0) + ) + }; + }); + } + + public getInvestmentsByGroup({ + data, + groupBy + }: { + data: HistoricalDataItem[]; + groupBy: GroupBy; + }): InvestmentItem[] { + const groupedData: { [dateGroup: string]: Big } = {}; + + for (const { date, investmentValueWithCurrencyEffect } of data) { + const dateGroup = + groupBy === 'month' ? date.substring(0, 7) : date.substring(0, 4); + groupedData[dateGroup] = (groupedData[dateGroup] ?? new Big(0)).plus( + investmentValueWithCurrencyEffect + ); + } + + return Object.keys(groupedData).map((dateGroup) => ({ + date: groupBy === 'month' ? `${dateGroup}-01` : `${dateGroup}-01-01`, + investment: groupedData[dateGroup].toNumber() + })); + } + + public getStartDate() { + return this.transactionPoints.length > 0 + ? parseDate(this.transactionPoints[0].date) + : new Date(); + } + + protected abstract getSymbolMetrics({ + dataSource, + end, + exchangeRates, + isChartMode, + marketSymbolMap, + start, + step, + symbol + }: { + end: Date; + exchangeRates: { [dateString: string]: number }; + isChartMode?: boolean; + marketSymbolMap: { + [date: string]: { [symbol: string]: Big }; + }; + start: Date; + step?: number; + } & UniqueAsset): SymbolMetrics; + + public getTransactionPoints() { + return this.transactionPoints; + } + + private computeTransactionPoints() { + this.transactionPoints = []; + const symbols: { [symbol: string]: TransactionPointSymbol } = {}; + + let lastDate: string = null; + let lastTransactionPoint: TransactionPoint = null; + + for (const { + fee, + date, + quantity, + SymbolProfile, + tags, + type, + unitPrice + } of this.orders) { + let currentTransactionPointItem: TransactionPointSymbol; + const oldAccumulatedSymbol = symbols[SymbolProfile.symbol]; + + const factor = getFactor(type); + + if (oldAccumulatedSymbol) { + let investment = oldAccumulatedSymbol.investment; + + const newQuantity = quantity + .mul(factor) + .plus(oldAccumulatedSymbol.quantity); + + if (type === 'BUY') { + investment = oldAccumulatedSymbol.investment.plus( + quantity.mul(unitPrice) + ); + } else if (type === 'SELL') { + investment = oldAccumulatedSymbol.investment.minus( + quantity.mul(oldAccumulatedSymbol.averagePrice) + ); + } + + currentTransactionPointItem = { + investment, + tags, + averagePrice: newQuantity.gt(0) + ? investment.div(newQuantity) + : new Big(0), + currency: SymbolProfile.currency, + dataSource: SymbolProfile.dataSource, + dividend: new Big(0), + fee: fee.plus(oldAccumulatedSymbol.fee), + firstBuyDate: oldAccumulatedSymbol.firstBuyDate, + quantity: newQuantity, + symbol: SymbolProfile.symbol, + transactionCount: oldAccumulatedSymbol.transactionCount + 1 + }; + } else { + currentTransactionPointItem = { + fee, + tags, + averagePrice: unitPrice, + currency: SymbolProfile.currency, + dataSource: SymbolProfile.dataSource, + dividend: new Big(0), + firstBuyDate: date, + investment: unitPrice.mul(quantity).mul(factor), + quantity: quantity.mul(factor), + symbol: SymbolProfile.symbol, + transactionCount: 1 + }; + } + + symbols[SymbolProfile.symbol] = currentTransactionPointItem; + + const items = lastTransactionPoint?.items ?? []; + + const newItems = items.filter(({ symbol }) => { + return symbol !== SymbolProfile.symbol; + }); + + newItems.push(currentTransactionPointItem); + + newItems.sort((a, b) => { + return a.symbol?.localeCompare(b.symbol); + }); + + if (lastDate !== date || lastTransactionPoint === null) { + lastTransactionPoint = { + date, + items: newItems + }; + + this.transactionPoints.push(lastTransactionPoint); + } else { + lastTransactionPoint.items = newItems; + } + + lastDate = date; + } + } +} diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts index e08de8e22..ee71a1fb3 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts @@ -1,4 +1,12 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; +import { + activityDummyData, + symbolProfileDummyData +} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; +import { + PortfolioCalculatorFactory, + PerformanceCalculationType +} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; @@ -6,8 +14,6 @@ import { parseDate } from '@ghostfolio/common/helper'; import { Big } from 'big.js'; -import { PortfolioCalculator } from './portfolio-calculator'; - jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { return { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -20,6 +26,7 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { describe('PortfolioCalculator', () => { let currentRateService: CurrentRateService; let exchangeRateDataService: ExchangeRateDataService; + let factory: PortfolioCalculatorFactory; beforeEach(() => { currentRateService = new CurrentRateService(null, null, null, null); @@ -30,54 +37,66 @@ describe('PortfolioCalculator', () => { null, null ); + + factory = new PortfolioCalculatorFactory( + currentRateService, + exchangeRateDataService + ); }); describe('get current positions', () => { it.only('with BALN.SW buy and sell in two activities', async () => { - const portfolioCalculator = new PortfolioCalculator({ - currentRateService, - exchangeRateDataService, - activities: [ - { - date: new Date('2021-11-22'), - fee: 1.55, - quantity: 2, - SymbolProfile: { - currency: 'CHF', - dataSource: 'YAHOO', - name: 'Bâloise Holding AG', - symbol: 'BALN.SW' - }, - type: 'BUY', - unitPrice: 142.9 + const activities: Activity[] = [ + { + ...activityDummyData, + date: new Date('2021-11-22'), + fee: 1.55, + quantity: 2, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'CHF', + dataSource: 'YAHOO', + name: 'Bâloise Holding AG', + symbol: 'BALN.SW' }, - { - date: new Date('2021-11-30'), - fee: 1.65, - quantity: 1, - SymbolProfile: { - currency: 'CHF', - dataSource: 'YAHOO', - name: 'Bâloise Holding AG', - symbol: 'BALN.SW' - }, - type: 'SELL', - unitPrice: 136.6 + type: 'BUY', + unitPrice: 142.9 + }, + { + ...activityDummyData, + date: new Date('2021-11-30'), + fee: 1.65, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'CHF', + dataSource: 'YAHOO', + name: 'Bâloise Holding AG', + symbol: 'BALN.SW' }, - { - date: new Date('2021-11-30'), - fee: 0, - quantity: 1, - SymbolProfile: { - currency: 'CHF', - dataSource: 'YAHOO', - name: 'Bâloise Holding AG', - symbol: 'BALN.SW' - }, - type: 'SELL', - unitPrice: 136.6 - } - ], + type: 'SELL', + unitPrice: 136.6 + }, + { + ...activityDummyData, + date: new Date('2021-11-30'), + fee: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'CHF', + dataSource: 'YAHOO', + name: 'Bâloise Holding AG', + symbol: 'BALN.SW' + }, + type: 'SELL', + unitPrice: 136.6 + } + ]; + + const portfolioCalculator = factory.createCalculator({ + activities, + calculationType: PerformanceCalculationType.TWR, currency: 'CHF' }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts index 411c5e4db..69078be7c 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts @@ -1,4 +1,12 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; +import { + activityDummyData, + symbolProfileDummyData +} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; +import { + PerformanceCalculationType, + PortfolioCalculatorFactory +} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; @@ -6,8 +14,6 @@ import { parseDate } from '@ghostfolio/common/helper'; import { Big } from 'big.js'; -import { PortfolioCalculator } from './portfolio-calculator'; - jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { return { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -20,6 +26,7 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { describe('PortfolioCalculator', () => { let currentRateService: CurrentRateService; let exchangeRateDataService: ExchangeRateDataService; + let factory: PortfolioCalculatorFactory; beforeEach(() => { currentRateService = new CurrentRateService(null, null, null, null); @@ -30,41 +37,51 @@ describe('PortfolioCalculator', () => { null, null ); + + factory = new PortfolioCalculatorFactory( + currentRateService, + exchangeRateDataService + ); }); describe('get current positions', () => { it.only('with BALN.SW buy and sell', async () => { - const portfolioCalculator = new PortfolioCalculator({ - currentRateService, - exchangeRateDataService, - activities: [ - { - date: new Date('2021-11-22'), - fee: 1.55, - quantity: 2, - SymbolProfile: { - currency: 'CHF', - dataSource: 'YAHOO', - name: 'Bâloise Holding AG', - symbol: 'BALN.SW' - }, - type: 'BUY', - unitPrice: 142.9 + const activities: Activity[] = [ + { + ...activityDummyData, + date: new Date('2021-11-22'), + fee: 1.55, + quantity: 2, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'CHF', + dataSource: 'YAHOO', + name: 'Bâloise Holding AG', + symbol: 'BALN.SW' }, - { - date: new Date('2021-11-30'), - fee: 1.65, - quantity: 2, - SymbolProfile: { - currency: 'CHF', - dataSource: 'YAHOO', - name: 'Bâloise Holding AG', - symbol: 'BALN.SW' - }, - type: 'SELL', - unitPrice: 136.6 - } - ], + type: 'BUY', + unitPrice: 142.9 + }, + { + ...activityDummyData, + date: new Date('2021-11-30'), + fee: 1.65, + quantity: 2, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'CHF', + dataSource: 'YAHOO', + name: 'Bâloise Holding AG', + symbol: 'BALN.SW' + }, + type: 'SELL', + unitPrice: 136.6 + } + ]; + + const portfolioCalculator = factory.createCalculator({ + activities, + calculationType: PerformanceCalculationType.TWR, currency: 'CHF' }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts index 32f582647..b52aac68d 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts @@ -1,4 +1,12 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; +import { + activityDummyData, + symbolProfileDummyData +} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; +import { + PortfolioCalculatorFactory, + PerformanceCalculationType +} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; @@ -6,8 +14,6 @@ import { parseDate } from '@ghostfolio/common/helper'; import { Big } from 'big.js'; -import { PortfolioCalculator } from './portfolio-calculator'; - jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { return { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -20,6 +26,7 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { describe('PortfolioCalculator', () => { let currentRateService: CurrentRateService; let exchangeRateDataService: ExchangeRateDataService; + let factory: PortfolioCalculatorFactory; beforeEach(() => { currentRateService = new CurrentRateService(null, null, null, null); @@ -30,28 +37,36 @@ describe('PortfolioCalculator', () => { null, null ); + + factory = new PortfolioCalculatorFactory( + currentRateService, + exchangeRateDataService + ); }); describe('get current positions', () => { it.only('with BALN.SW buy', async () => { - const portfolioCalculator = new PortfolioCalculator({ - currentRateService, - exchangeRateDataService, - activities: [ - { - date: new Date('2021-11-30'), - fee: 1.55, - quantity: 2, - SymbolProfile: { - currency: 'CHF', - dataSource: 'YAHOO', - name: 'Bâloise Holding AG', - symbol: 'BALN.SW' - }, - type: 'BUY', - unitPrice: 136.6 - } - ], + const activities: Activity[] = [ + { + ...activityDummyData, + date: new Date('2021-11-30'), + fee: 1.55, + quantity: 2, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'CHF', + dataSource: 'YAHOO', + name: 'Bâloise Holding AG', + symbol: 'BALN.SW' + }, + type: 'BUY', + unitPrice: 136.6 + } + ]; + + const portfolioCalculator = factory.createCalculator({ + activities, + calculationType: PerformanceCalculationType.TWR, currency: 'CHF' }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts index f5687f810..420ba48f1 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts @@ -1,4 +1,12 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; +import { + activityDummyData, + symbolProfileDummyData +} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; +import { + PortfolioCalculatorFactory, + PerformanceCalculationType +} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; @@ -7,8 +15,6 @@ import { parseDate } from '@ghostfolio/common/helper'; import { Big } from 'big.js'; -import { PortfolioCalculator } from './portfolio-calculator'; - jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { return { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -33,6 +39,7 @@ jest.mock( describe('PortfolioCalculator', () => { let currentRateService: CurrentRateService; let exchangeRateDataService: ExchangeRateDataService; + let factory: PortfolioCalculatorFactory; beforeEach(() => { currentRateService = new CurrentRateService(null, null, null, null); @@ -43,41 +50,51 @@ describe('PortfolioCalculator', () => { null, null ); + + factory = new PortfolioCalculatorFactory( + currentRateService, + exchangeRateDataService + ); }); describe('get current positions', () => { it.only('with BTCUSD buy and sell partially', async () => { - const portfolioCalculator = new PortfolioCalculator({ - currentRateService, - exchangeRateDataService, - activities: [ - { - date: new Date('2015-01-01'), - fee: 0, - quantity: 2, - SymbolProfile: { - currency: 'USD', - dataSource: 'YAHOO', - name: 'Bitcoin USD', - symbol: 'BTCUSD' - }, - type: 'BUY', - unitPrice: 320.43 + const activities: Activity[] = [ + { + ...activityDummyData, + date: new Date('2015-01-01'), + fee: 0, + quantity: 2, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'Bitcoin USD', + symbol: 'BTCUSD' }, - { - date: new Date('2017-12-31'), - fee: 0, - quantity: 1, - SymbolProfile: { - currency: 'USD', - dataSource: 'YAHOO', - name: 'Bitcoin USD', - symbol: 'BTCUSD' - }, - type: 'SELL', - unitPrice: 14156.4 - } - ], + type: 'BUY', + unitPrice: 320.43 + }, + { + ...activityDummyData, + date: new Date('2017-12-31'), + fee: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'Bitcoin USD', + symbol: 'BTCUSD' + }, + type: 'SELL', + unitPrice: 14156.4 + } + ]; + + const portfolioCalculator = factory.createCalculator({ + activities, + calculationType: PerformanceCalculationType.TWR, currency: 'CHF' }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts index 743733733..5f33d771b 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts @@ -1,4 +1,12 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; +import { + activityDummyData, + symbolProfileDummyData +} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; +import { + PortfolioCalculatorFactory, + PerformanceCalculationType +} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; @@ -7,8 +15,6 @@ import { parseDate } from '@ghostfolio/common/helper'; import { Big } from 'big.js'; -import { PortfolioCalculator } from './portfolio-calculator'; - jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { return { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -33,6 +39,7 @@ jest.mock( describe('PortfolioCalculator', () => { let currentRateService: CurrentRateService; let exchangeRateDataService: ExchangeRateDataService; + let factory: PortfolioCalculatorFactory; beforeEach(() => { currentRateService = new CurrentRateService(null, null, null, null); @@ -43,28 +50,36 @@ describe('PortfolioCalculator', () => { null, null ); + + factory = new PortfolioCalculatorFactory( + currentRateService, + exchangeRateDataService + ); }); describe('get current positions', () => { it.only('with GOOGL buy', async () => { - const portfolioCalculator = new PortfolioCalculator({ - currentRateService, - exchangeRateDataService, - activities: [ - { - date: new Date('2023-01-03'), - fee: 1, - quantity: 1, - SymbolProfile: { - currency: 'USD', - dataSource: 'YAHOO', - name: 'Alphabet Inc.', - symbol: 'GOOGL' - }, - type: 'BUY', - unitPrice: 89.12 - } - ], + const activities: Activity[] = [ + { + ...activityDummyData, + date: new Date('2023-01-03'), + fee: 1, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'Alphabet Inc.', + symbol: 'GOOGL' + }, + type: 'BUY', + unitPrice: 89.12 + } + ]; + + const portfolioCalculator = factory.createCalculator({ + activities, + calculationType: PerformanceCalculationType.TWR, currency: 'CHF' }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts index afd5e9ff2..a2c106784 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts @@ -1,4 +1,12 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; +import { + activityDummyData, + symbolProfileDummyData +} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; +import { + PerformanceCalculationType, + PortfolioCalculatorFactory +} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; @@ -7,8 +15,6 @@ import { parseDate } from '@ghostfolio/common/helper'; import { Big } from 'big.js'; -import { PortfolioCalculator } from './portfolio-calculator'; - jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { return { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -33,6 +39,7 @@ jest.mock( describe('PortfolioCalculator', () => { let currentRateService: CurrentRateService; let exchangeRateDataService: ExchangeRateDataService; + let factory: PortfolioCalculatorFactory; beforeEach(() => { currentRateService = new CurrentRateService(null, null, null, null); @@ -43,41 +50,51 @@ describe('PortfolioCalculator', () => { null, null ); + + factory = new PortfolioCalculatorFactory( + currentRateService, + exchangeRateDataService + ); }); describe('get current positions', () => { it.only('with MSFT buy', async () => { - const portfolioCalculator = new PortfolioCalculator({ - currentRateService, - exchangeRateDataService, - activities: [ - { - date: new Date('2021-09-16'), - fee: 19, - quantity: 1, - SymbolProfile: { - currency: 'USD', - dataSource: 'YAHOO', - name: 'Microsoft Inc.', - symbol: 'MSFT' - }, - type: 'BUY', - unitPrice: 298.58 + const activities: Activity[] = [ + { + ...activityDummyData, + date: new Date('2021-09-16'), + fee: 19, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'Microsoft Inc.', + symbol: 'MSFT' }, - { - date: new Date('2021-11-16'), - fee: 0, - quantity: 1, - SymbolProfile: { - currency: 'USD', - dataSource: 'YAHOO', - name: 'Microsoft Inc.', - symbol: 'MSFT' - }, - type: 'DIVIDEND', - unitPrice: 0.62 - } - ], + type: 'BUY', + unitPrice: 298.58 + }, + { + ...activityDummyData, + date: new Date('2021-11-16'), + fee: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'Microsoft Inc.', + symbol: 'MSFT' + }, + type: 'DIVIDEND', + unitPrice: 0.62 + } + ]; + + const portfolioCalculator = factory.createCalculator({ + activities, + calculationType: PerformanceCalculationType.TWR, currency: 'USD' }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-no-orders.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-no-orders.spec.ts index 39a563b85..905747519 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-no-orders.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-no-orders.spec.ts @@ -1,3 +1,7 @@ +import { + PerformanceCalculationType, + PortfolioCalculatorFactory +} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; @@ -6,8 +10,6 @@ import { parseDate } from '@ghostfolio/common/helper'; import { Big } from 'big.js'; import { subDays } from 'date-fns'; -import { PortfolioCalculator } from './portfolio-calculator'; - jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { return { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -20,6 +22,7 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { describe('PortfolioCalculator', () => { let currentRateService: CurrentRateService; let exchangeRateDataService: ExchangeRateDataService; + let factory: PortfolioCalculatorFactory; beforeEach(() => { currentRateService = new CurrentRateService(null, null, null, null); @@ -30,14 +33,18 @@ describe('PortfolioCalculator', () => { null, null ); + + factory = new PortfolioCalculatorFactory( + currentRateService, + exchangeRateDataService + ); }); describe('get current positions', () => { it('with no orders', async () => { - const portfolioCalculator = new PortfolioCalculator({ - currentRateService, - exchangeRateDataService, + const portfolioCalculator = factory.createCalculator({ activities: [], + calculationType: PerformanceCalculationType.TWR, currency: 'CHF' }); @@ -73,7 +80,8 @@ describe('PortfolioCalculator', () => { netPerformancePercentageWithCurrencyEffect: new Big(0), netPerformanceWithCurrencyEffect: new Big(0), positions: [], - totalInvestment: new Big(0) + totalInvestment: new Big(0), + totalInvestmentWithCurrencyEffect: new Big(0) }); expect(investments).toEqual([]); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts index d7e7c6eab..21e0bb499 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts @@ -1,4 +1,12 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; +import { + activityDummyData, + symbolProfileDummyData +} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; +import { + PerformanceCalculationType, + PortfolioCalculatorFactory +} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; @@ -6,8 +14,6 @@ import { parseDate } from '@ghostfolio/common/helper'; import { Big } from 'big.js'; -import { PortfolioCalculator } from './portfolio-calculator'; - jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { return { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -20,6 +26,7 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { describe('PortfolioCalculator', () => { let currentRateService: CurrentRateService; let exchangeRateDataService: ExchangeRateDataService; + let factory: PortfolioCalculatorFactory; beforeEach(() => { currentRateService = new CurrentRateService(null, null, null, null); @@ -30,44 +37,53 @@ describe('PortfolioCalculator', () => { null, null ); + + factory = new PortfolioCalculatorFactory( + currentRateService, + exchangeRateDataService + ); }); describe('get current positions', () => { it.only('with NOVN.SW buy and sell partially', async () => { - const portfolioCalculator = new PortfolioCalculator({ - currentRateService, - exchangeRateDataService, - activities: [ - { - date: new Date('2022-03-07'), - fee: 1.3, - quantity: 2, - SymbolProfile: { - currency: 'CHF', - dataSource: 'YAHOO', - name: 'Novartis AG', - symbol: 'NOVN.SW' - }, - type: 'BUY', - unitPrice: 75.8 + const activities: Activity[] = [ + { + ...activityDummyData, + date: new Date('2022-03-07'), + fee: 1.3, + quantity: 2, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'CHF', + dataSource: 'YAHOO', + name: 'Novartis AG', + symbol: 'NOVN.SW' }, - { - date: new Date('2022-04-08'), - fee: 2.95, - quantity: 1, - SymbolProfile: { - currency: 'CHF', - dataSource: 'YAHOO', - name: 'Novartis AG', - symbol: 'NOVN.SW' - }, - type: 'SELL', - unitPrice: 85.73 - } - ], + type: 'BUY', + unitPrice: 75.8 + }, + { + ...activityDummyData, + date: new Date('2022-04-08'), + fee: 2.95, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'CHF', + dataSource: 'YAHOO', + name: 'Novartis AG', + symbol: 'NOVN.SW' + }, + type: 'SELL', + unitPrice: 85.73 + } + ]; + + const portfolioCalculator = factory.createCalculator({ + activities, + calculationType: PerformanceCalculationType.TWR, currency: 'CHF' }); - const spy = jest .spyOn(Date, 'now') .mockImplementation(() => parseDate('2022-04-11').getTime()); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts index 68eecea22..28920ece7 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts @@ -1,4 +1,12 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; +import { + activityDummyData, + symbolProfileDummyData +} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; +import { + PerformanceCalculationType, + PortfolioCalculatorFactory +} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; @@ -6,8 +14,6 @@ import { parseDate } from '@ghostfolio/common/helper'; import { Big } from 'big.js'; -import { PortfolioCalculator } from './portfolio-calculator'; - jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { return { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -20,6 +26,7 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { describe('PortfolioCalculator', () => { let currentRateService: CurrentRateService; let exchangeRateDataService: ExchangeRateDataService; + let factory: PortfolioCalculatorFactory; beforeEach(() => { currentRateService = new CurrentRateService(null, null, null, null); @@ -30,41 +37,51 @@ describe('PortfolioCalculator', () => { null, null ); + + factory = new PortfolioCalculatorFactory( + currentRateService, + exchangeRateDataService + ); }); describe('get current positions', () => { it.only('with NOVN.SW buy and sell', async () => { - const portfolioCalculator = new PortfolioCalculator({ - currentRateService, - exchangeRateDataService, - activities: [ - { - date: new Date('2022-03-07'), - fee: 0, - quantity: 2, - SymbolProfile: { - currency: 'CHF', - dataSource: 'YAHOO', - name: 'Novartis AG', - symbol: 'NOVN.SW' - }, - type: 'BUY', - unitPrice: 75.8 + const activities: Activity[] = [ + { + ...activityDummyData, + date: new Date('2022-03-07'), + fee: 0, + quantity: 2, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'CHF', + dataSource: 'YAHOO', + name: 'Novartis AG', + symbol: 'NOVN.SW' }, - { - date: new Date('2022-04-08'), - fee: 0, - quantity: 2, - SymbolProfile: { - currency: 'CHF', - dataSource: 'YAHOO', - name: 'Novartis AG', - symbol: 'NOVN.SW' - }, - type: 'SELL', - unitPrice: 85.73 - } - ], + type: 'BUY', + unitPrice: 75.8 + }, + { + ...activityDummyData, + date: new Date('2022-04-08'), + fee: 0, + quantity: 2, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'CHF', + dataSource: 'YAHOO', + name: 'Novartis AG', + symbol: 'NOVN.SW' + }, + type: 'SELL', + unitPrice: 85.73 + } + ]; + + const portfolioCalculator = factory.createCalculator({ + activities, + calculationType: PerformanceCalculationType.TWR, currency: 'CHF' }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.spec.ts index de011d813..b68f4358d 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.spec.ts @@ -1,13 +1,16 @@ +import { + PerformanceCalculationType, + PortfolioCalculatorFactory +} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { Big } from 'big.js'; -import { PortfolioCalculator } from './portfolio-calculator'; - describe('PortfolioCalculator', () => { let currentRateService: CurrentRateService; let exchangeRateDataService: ExchangeRateDataService; + let factory: PortfolioCalculatorFactory; beforeEach(() => { currentRateService = new CurrentRateService(null, null, null, null); @@ -18,17 +21,21 @@ describe('PortfolioCalculator', () => { null, null ); - }); - describe('annualized performance percentage', () => { - const portfolioCalculator = new PortfolioCalculator({ - activities: [], + factory = new PortfolioCalculatorFactory( currentRateService, - exchangeRateDataService, - currency: 'USD' - }); + exchangeRateDataService + ); + }); + describe('annualized performance percentage', () => { it('Get annualized performance', async () => { + const portfolioCalculator = factory.createCalculator({ + activities: [], + calculationType: PerformanceCalculationType.TWR, + currency: 'CHF' + }); + expect( portfolioCalculator .getAnnualizedPerformancePercent({ diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts index 6ea93a670..0fee9c5c7 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts @@ -1,24 +1,13 @@ -import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; -import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; +import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface'; -import { PortfolioOrderItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator.interface'; -import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface'; -import { TransactionPointSymbol } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point-symbol.interface'; -import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface'; +import { PortfolioOrderItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order-item.interface'; import { getFactor } from '@ghostfolio/api/helper/portfolio.helper'; -import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; -import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; -import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; +import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { - DataProviderInfo, - HistoricalDataItem, - InvestmentItem, - ResponseError, SymbolMetrics, TimelinePosition, UniqueAsset } from '@ghostfolio/common/interfaces'; -import { GroupBy } from '@ghostfolio/common/types'; import { Logger } from '@nestjs/common'; import { Big } from 'big.js'; @@ -26,638 +15,15 @@ import { addDays, addMilliseconds, differenceInDays, - eachDayOfInterval, - endOfDay, format, - isBefore, - isSameDay, - max, - subDays + isBefore } from 'date-fns'; -import { cloneDeep, first, isNumber, last, sortBy, uniq } from 'lodash'; - -export class PortfolioCalculator { - private static readonly ENABLE_LOGGING = false; - - private currency: string; - private currentRateService: CurrentRateService; - private dataProviderInfos: DataProviderInfo[]; - private exchangeRateDataService: ExchangeRateDataService; - private orders: PortfolioOrder[]; - private transactionPoints: TransactionPoint[]; - - public constructor({ - activities, - currency, - currentRateService, - exchangeRateDataService - }: { - activities: Activity[]; - currency: string; - currentRateService: CurrentRateService; - exchangeRateDataService: ExchangeRateDataService; - }) { - this.currency = currency; - this.currentRateService = currentRateService; - this.exchangeRateDataService = exchangeRateDataService; - this.orders = activities.map( - ({ date, fee, quantity, SymbolProfile, type, unitPrice }) => { - return { - SymbolProfile, - type, - date: format(date, DATE_FORMAT), - fee: new Big(fee), - quantity: new Big(quantity), - unitPrice: new Big(unitPrice) - }; - } - ); - - this.orders.sort((a, b) => { - return a.date?.localeCompare(b.date); - }); - - this.computeTransactionPoints(); - } - - public getAnnualizedPerformancePercent({ - daysInMarket, - netPerformancePercent - }: { - daysInMarket: number; - netPerformancePercent: Big; - }): Big { - if (isNumber(daysInMarket) && daysInMarket > 0) { - const exponent = new Big(365).div(daysInMarket).toNumber(); - return new Big( - Math.pow(netPerformancePercent.plus(1).toNumber(), exponent) - ).minus(1); - } - - return new Big(0); - } - - public async getChartData({ - end = new Date(Date.now()), - start, - step = 1 - }: { - end?: Date; - start: Date; - step?: number; - }): Promise { - const symbols: { [symbol: string]: boolean } = {}; - - const transactionPointsBeforeEndDate = - this.transactionPoints?.filter((transactionPoint) => { - return isBefore(parseDate(transactionPoint.date), end); - }) ?? []; - - const currencies: { [symbol: string]: string } = {}; - const dataGatheringItems: IDataGatheringItem[] = []; - const firstIndex = transactionPointsBeforeEndDate.length; - - let dates = eachDayOfInterval({ start, end }, { step }).map((date) => { - return resetHours(date); - }); - - const includesEndDate = isSameDay(last(dates), end); - - if (!includesEndDate) { - dates.push(resetHours(end)); - } - - if (transactionPointsBeforeEndDate.length > 0) { - for (const { - currency, - dataSource, - symbol - } of transactionPointsBeforeEndDate[firstIndex - 1].items) { - dataGatheringItems.push({ - dataSource, - symbol - }); - currencies[symbol] = currency; - symbols[symbol] = true; - } - } - - const { dataProviderInfos, values: marketSymbols } = - await this.currentRateService.getValues({ - dataGatheringItems, - dateQuery: { - in: dates - } - }); - - this.dataProviderInfos = dataProviderInfos; - - const marketSymbolMap: { - [date: string]: { [symbol: string]: Big }; - } = {}; - - let exchangeRatesByCurrency = - await this.exchangeRateDataService.getExchangeRatesByCurrency({ - currencies: uniq(Object.values(currencies)), - endDate: endOfDay(end), - startDate: this.getStartDate(), - targetCurrency: this.currency - }); - - for (const marketSymbol of marketSymbols) { - const dateString = format(marketSymbol.date, DATE_FORMAT); - if (!marketSymbolMap[dateString]) { - marketSymbolMap[dateString] = {}; - } - if (marketSymbol.marketPrice) { - marketSymbolMap[dateString][marketSymbol.symbol] = new Big( - marketSymbol.marketPrice - ); - } - } - - const accumulatedValuesByDate: { - [date: string]: { - investmentValueWithCurrencyEffect: Big; - totalCurrentValue: Big; - totalCurrentValueWithCurrencyEffect: Big; - totalInvestmentValue: Big; - totalInvestmentValueWithCurrencyEffect: Big; - totalNetPerformanceValue: Big; - totalNetPerformanceValueWithCurrencyEffect: Big; - totalTimeWeightedInvestmentValue: Big; - totalTimeWeightedInvestmentValueWithCurrencyEffect: Big; - }; - } = {}; - - const valuesBySymbol: { - [symbol: string]: { - currentValues: { [date: string]: Big }; - currentValuesWithCurrencyEffect: { [date: string]: Big }; - investmentValuesAccumulated: { [date: string]: Big }; - investmentValuesAccumulatedWithCurrencyEffect: { [date: string]: Big }; - investmentValuesWithCurrencyEffect: { [date: string]: Big }; - netPerformanceValues: { [date: string]: Big }; - netPerformanceValuesWithCurrencyEffect: { [date: string]: Big }; - timeWeightedInvestmentValues: { [date: string]: Big }; - timeWeightedInvestmentValuesWithCurrencyEffect: { [date: string]: Big }; - }; - } = {}; - - for (const symbol of Object.keys(symbols)) { - const { - currentValues, - currentValuesWithCurrencyEffect, - investmentValuesAccumulated, - investmentValuesAccumulatedWithCurrencyEffect, - investmentValuesWithCurrencyEffect, - netPerformanceValues, - netPerformanceValuesWithCurrencyEffect, - timeWeightedInvestmentValues, - timeWeightedInvestmentValuesWithCurrencyEffect - } = this.getSymbolMetrics({ - end, - marketSymbolMap, - start, - step, - symbol, - dataSource: null, - exchangeRates: - exchangeRatesByCurrency[`${currencies[symbol]}${this.currency}`], - isChartMode: true - }); - - valuesBySymbol[symbol] = { - currentValues, - currentValuesWithCurrencyEffect, - investmentValuesAccumulated, - investmentValuesAccumulatedWithCurrencyEffect, - investmentValuesWithCurrencyEffect, - netPerformanceValues, - netPerformanceValuesWithCurrencyEffect, - timeWeightedInvestmentValues, - timeWeightedInvestmentValuesWithCurrencyEffect - }; - } - - for (const currentDate of dates) { - const dateString = format(currentDate, DATE_FORMAT); - - for (const symbol of Object.keys(valuesBySymbol)) { - const symbolValues = valuesBySymbol[symbol]; - - const currentValue = - symbolValues.currentValues?.[dateString] ?? new Big(0); - - const currentValueWithCurrencyEffect = - symbolValues.currentValuesWithCurrencyEffect?.[dateString] ?? - new Big(0); - - const investmentValueAccumulated = - symbolValues.investmentValuesAccumulated?.[dateString] ?? new Big(0); - - const investmentValueAccumulatedWithCurrencyEffect = - symbolValues.investmentValuesAccumulatedWithCurrencyEffect?.[ - dateString - ] ?? new Big(0); - - const investmentValueWithCurrencyEffect = - symbolValues.investmentValuesWithCurrencyEffect?.[dateString] ?? - new Big(0); - - const netPerformanceValue = - symbolValues.netPerformanceValues?.[dateString] ?? new Big(0); - - const netPerformanceValueWithCurrencyEffect = - symbolValues.netPerformanceValuesWithCurrencyEffect?.[dateString] ?? - new Big(0); - - const timeWeightedInvestmentValue = - symbolValues.timeWeightedInvestmentValues?.[dateString] ?? new Big(0); - - const timeWeightedInvestmentValueWithCurrencyEffect = - symbolValues.timeWeightedInvestmentValuesWithCurrencyEffect?.[ - dateString - ] ?? new Big(0); - - accumulatedValuesByDate[dateString] = { - investmentValueWithCurrencyEffect: ( - accumulatedValuesByDate[dateString] - ?.investmentValueWithCurrencyEffect ?? new Big(0) - ).add(investmentValueWithCurrencyEffect), - totalCurrentValue: ( - accumulatedValuesByDate[dateString]?.totalCurrentValue ?? new Big(0) - ).add(currentValue), - totalCurrentValueWithCurrencyEffect: ( - accumulatedValuesByDate[dateString] - ?.totalCurrentValueWithCurrencyEffect ?? new Big(0) - ).add(currentValueWithCurrencyEffect), - totalInvestmentValue: ( - accumulatedValuesByDate[dateString]?.totalInvestmentValue ?? - new Big(0) - ).add(investmentValueAccumulated), - totalInvestmentValueWithCurrencyEffect: ( - accumulatedValuesByDate[dateString] - ?.totalInvestmentValueWithCurrencyEffect ?? new Big(0) - ).add(investmentValueAccumulatedWithCurrencyEffect), - totalNetPerformanceValue: ( - accumulatedValuesByDate[dateString]?.totalNetPerformanceValue ?? - new Big(0) - ).add(netPerformanceValue), - totalNetPerformanceValueWithCurrencyEffect: ( - accumulatedValuesByDate[dateString] - ?.totalNetPerformanceValueWithCurrencyEffect ?? new Big(0) - ).add(netPerformanceValueWithCurrencyEffect), - totalTimeWeightedInvestmentValue: ( - accumulatedValuesByDate[dateString] - ?.totalTimeWeightedInvestmentValue ?? new Big(0) - ).add(timeWeightedInvestmentValue), - totalTimeWeightedInvestmentValueWithCurrencyEffect: ( - accumulatedValuesByDate[dateString] - ?.totalTimeWeightedInvestmentValueWithCurrencyEffect ?? new Big(0) - ).add(timeWeightedInvestmentValueWithCurrencyEffect) - }; - } - } - - return Object.entries(accumulatedValuesByDate).map(([date, values]) => { - const { - investmentValueWithCurrencyEffect, - totalCurrentValue, - totalCurrentValueWithCurrencyEffect, - totalInvestmentValue, - totalInvestmentValueWithCurrencyEffect, - totalNetPerformanceValue, - totalNetPerformanceValueWithCurrencyEffect, - totalTimeWeightedInvestmentValue, - totalTimeWeightedInvestmentValueWithCurrencyEffect - } = values; - - const netPerformanceInPercentage = totalTimeWeightedInvestmentValue.eq(0) - ? 0 - : totalNetPerformanceValue - .div(totalTimeWeightedInvestmentValue) - .mul(100) - .toNumber(); - - const netPerformanceInPercentageWithCurrencyEffect = - totalTimeWeightedInvestmentValueWithCurrencyEffect.eq(0) - ? 0 - : totalNetPerformanceValueWithCurrencyEffect - .div(totalTimeWeightedInvestmentValueWithCurrencyEffect) - .mul(100) - .toNumber(); - - return { - date, - netPerformanceInPercentage, - netPerformanceInPercentageWithCurrencyEffect, - investmentValueWithCurrencyEffect: - investmentValueWithCurrencyEffect.toNumber(), - netPerformance: totalNetPerformanceValue.toNumber(), - netPerformanceWithCurrencyEffect: - totalNetPerformanceValueWithCurrencyEffect.toNumber(), - totalInvestment: totalInvestmentValue.toNumber(), - totalInvestmentValueWithCurrencyEffect: - totalInvestmentValueWithCurrencyEffect.toNumber(), - value: totalCurrentValue.toNumber(), - valueWithCurrencyEffect: totalCurrentValueWithCurrencyEffect.toNumber() - }; - }); - } - - public async getCurrentPositions( - start: Date, - end?: Date - ): Promise { - const lastTransactionPoint = last(this.transactionPoints); - - let endDate = end; - - if (!endDate) { - endDate = new Date(Date.now()); - - if (lastTransactionPoint) { - endDate = max([endDate, parseDate(lastTransactionPoint.date)]); - } - } - - const transactionPoints = this.transactionPoints?.filter(({ date }) => { - return isBefore(parseDate(date), endDate); - }); - - if (!transactionPoints.length) { - return { - currentValueInBaseCurrency: new Big(0), - grossPerformance: new Big(0), - grossPerformancePercentage: new Big(0), - grossPerformancePercentageWithCurrencyEffect: new Big(0), - grossPerformanceWithCurrencyEffect: new Big(0), - hasErrors: false, - netPerformance: new Big(0), - netPerformancePercentage: new Big(0), - netPerformancePercentageWithCurrencyEffect: new Big(0), - netPerformanceWithCurrencyEffect: new Big(0), - positions: [], - totalInvestment: new Big(0) - }; - } - - const currencies: { [symbol: string]: string } = {}; - const dataGatheringItems: IDataGatheringItem[] = []; - let dates: Date[] = []; - let firstIndex = transactionPoints.length; - let firstTransactionPoint: TransactionPoint = null; +import { cloneDeep, first, last, sortBy } from 'lodash'; - dates.push(resetHours(start)); - - for (const { currency, dataSource, symbol } of transactionPoints[ - firstIndex - 1 - ].items) { - dataGatheringItems.push({ - dataSource, - symbol - }); - - currencies[symbol] = currency; - } - - for (let i = 0; i < transactionPoints.length; i++) { - if ( - !isBefore(parseDate(transactionPoints[i].date), start) && - firstTransactionPoint === null - ) { - firstTransactionPoint = transactionPoints[i]; - firstIndex = i; - } - - if (firstTransactionPoint !== null) { - dates.push(resetHours(parseDate(transactionPoints[i].date))); - } - } - - dates.push(resetHours(endDate)); - - // Add dates of last week for fallback - dates.push(subDays(resetHours(new Date()), 7)); - dates.push(subDays(resetHours(new Date()), 6)); - dates.push(subDays(resetHours(new Date()), 5)); - dates.push(subDays(resetHours(new Date()), 4)); - dates.push(subDays(resetHours(new Date()), 3)); - dates.push(subDays(resetHours(new Date()), 2)); - dates.push(subDays(resetHours(new Date()), 1)); - dates.push(resetHours(new Date())); - - dates = uniq( - dates.map((date) => { - return date.getTime(); - }) - ) - .map((timestamp) => { - return new Date(timestamp); - }) - .sort((a, b) => { - return a.getTime() - b.getTime(); - }); - - let exchangeRatesByCurrency = - await this.exchangeRateDataService.getExchangeRatesByCurrency({ - currencies: uniq(Object.values(currencies)), - endDate: endOfDay(endDate), - startDate: this.getStartDate(), - targetCurrency: this.currency - }); - - const { - dataProviderInfos, - errors: currentRateErrors, - values: marketSymbols - } = await this.currentRateService.getValues({ - dataGatheringItems, - dateQuery: { - in: dates - } - }); - - this.dataProviderInfos = dataProviderInfos; - - const marketSymbolMap: { - [date: string]: { [symbol: string]: Big }; - } = {}; - - for (const marketSymbol of marketSymbols) { - const date = format(marketSymbol.date, DATE_FORMAT); - - if (!marketSymbolMap[date]) { - marketSymbolMap[date] = {}; - } - - if (marketSymbol.marketPrice) { - marketSymbolMap[date][marketSymbol.symbol] = new Big( - marketSymbol.marketPrice - ); - } - } - - const endDateString = format(endDate, DATE_FORMAT); - - if (firstIndex > 0) { - firstIndex--; - } - - const positions: TimelinePosition[] = []; - let hasAnySymbolMetricsErrors = false; - - const errors: ResponseError['errors'] = []; - - for (const item of lastTransactionPoint.items) { - const marketPriceInBaseCurrency = ( - marketSymbolMap[endDateString]?.[item.symbol] ?? item.averagePrice - ).mul( - exchangeRatesByCurrency[`${item.currency}${this.currency}`]?.[ - endDateString - ] - ); - - const { - grossPerformance, - grossPerformancePercentage, - grossPerformancePercentageWithCurrencyEffect, - grossPerformanceWithCurrencyEffect, - hasErrors, - netPerformance, - netPerformancePercentage, - netPerformancePercentageWithCurrencyEffect, - netPerformanceWithCurrencyEffect, - timeWeightedInvestment, - timeWeightedInvestmentWithCurrencyEffect, - totalDividend, - totalDividendInBaseCurrency, - totalInvestment, - totalInvestmentWithCurrencyEffect - } = this.getSymbolMetrics({ - marketSymbolMap, - start, - dataSource: item.dataSource, - end: endDate, - exchangeRates: - exchangeRatesByCurrency[`${item.currency}${this.currency}`], - symbol: item.symbol - }); - - hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors; - - positions.push({ - dividend: totalDividend, - dividendInBaseCurrency: totalDividendInBaseCurrency, - timeWeightedInvestment, - timeWeightedInvestmentWithCurrencyEffect, - averagePrice: item.averagePrice, - currency: item.currency, - dataSource: item.dataSource, - fee: item.fee, - firstBuyDate: item.firstBuyDate, - grossPerformance: !hasErrors ? grossPerformance ?? null : null, - grossPerformancePercentage: !hasErrors - ? grossPerformancePercentage ?? null - : null, - grossPerformancePercentageWithCurrencyEffect: !hasErrors - ? grossPerformancePercentageWithCurrencyEffect ?? null - : null, - grossPerformanceWithCurrencyEffect: !hasErrors - ? grossPerformanceWithCurrencyEffect ?? null - : null, - investment: totalInvestment, - investmentWithCurrencyEffect: totalInvestmentWithCurrencyEffect, - marketPrice: - marketSymbolMap[endDateString]?.[item.symbol]?.toNumber() ?? null, - marketPriceInBaseCurrency: - marketPriceInBaseCurrency?.toNumber() ?? null, - netPerformance: !hasErrors ? netPerformance ?? null : null, - netPerformancePercentage: !hasErrors - ? netPerformancePercentage ?? null - : null, - netPerformancePercentageWithCurrencyEffect: !hasErrors - ? netPerformancePercentageWithCurrencyEffect ?? null - : null, - netPerformanceWithCurrencyEffect: !hasErrors - ? netPerformanceWithCurrencyEffect ?? null - : null, - quantity: item.quantity, - symbol: item.symbol, - tags: item.tags, - transactionCount: item.transactionCount, - valueInBaseCurrency: new Big(marketPriceInBaseCurrency).mul( - item.quantity - ) - }); - - if ( - (hasErrors || - currentRateErrors.find(({ dataSource, symbol }) => { - return dataSource === item.dataSource && symbol === item.symbol; - })) && - item.investment.gt(0) - ) { - errors.push({ dataSource: item.dataSource, symbol: item.symbol }); - } - } - - const overall = this.calculateOverallPerformance(positions); - - return { - ...overall, - errors, - positions, - hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors - }; - } - - public getDataProviderInfos() { - return this.dataProviderInfos; - } - - public getInvestments(): { date: string; investment: Big }[] { - if (this.transactionPoints.length === 0) { - return []; - } - - return this.transactionPoints.map((transactionPoint) => { - return { - date: transactionPoint.date, - investment: transactionPoint.items.reduce( - (investment, transactionPointSymbol) => - investment.plus(transactionPointSymbol.investment), - new Big(0) - ) - }; - }); - } - - public getInvestmentsByGroup({ - data, - groupBy - }: { - data: HistoricalDataItem[]; - groupBy: GroupBy; - }): InvestmentItem[] { - const groupedData: { [dateGroup: string]: Big } = {}; - - for (const { date, investmentValueWithCurrencyEffect } of data) { - const dateGroup = - groupBy === 'month' ? date.substring(0, 7) : date.substring(0, 4); - groupedData[dateGroup] = (groupedData[dateGroup] ?? new Big(0)).plus( - investmentValueWithCurrencyEffect - ); - } - - return Object.keys(groupedData).map((dateGroup) => ({ - date: groupBy === 'month' ? `${dateGroup}-01` : `${dateGroup}-01-01`, - investment: groupedData[dateGroup].toNumber() - })); - } - - private calculateOverallPerformance(positions: TimelinePosition[]) { +export class TWRPortfolioCalculator extends PortfolioCalculator { + protected calculateOverallPerformance( + positions: TimelinePosition[] + ): CurrentPositions { let currentValueInBaseCurrency = new Big(0); let grossPerformance = new Big(0); let grossPerformanceWithCurrencyEffect = new Big(0); @@ -754,119 +120,12 @@ export class PortfolioCalculator { ? new Big(0) : grossPerformanceWithCurrencyEffect.div( totalTimeWeightedInvestmentWithCurrencyEffect - ) + ), + positions }; } - public getStartDate() { - return this.transactionPoints.length > 0 - ? parseDate(this.transactionPoints[0].date) - : new Date(); - } - - public getTransactionPoints() { - return this.transactionPoints; - } - - private computeTransactionPoints() { - this.transactionPoints = []; - const symbols: { [symbol: string]: TransactionPointSymbol } = {}; - - let lastDate: string = null; - let lastTransactionPoint: TransactionPoint = null; - - for (const { - fee, - date, - quantity, - SymbolProfile, - tags, - type, - unitPrice - } of this.orders) { - let currentTransactionPointItem: TransactionPointSymbol; - const oldAccumulatedSymbol = symbols[SymbolProfile.symbol]; - - const factor = getFactor(type); - - if (oldAccumulatedSymbol) { - let investment = oldAccumulatedSymbol.investment; - - const newQuantity = quantity - .mul(factor) - .plus(oldAccumulatedSymbol.quantity); - - if (type === 'BUY') { - investment = oldAccumulatedSymbol.investment.plus( - quantity.mul(unitPrice) - ); - } else if (type === 'SELL') { - investment = oldAccumulatedSymbol.investment.minus( - quantity.mul(oldAccumulatedSymbol.averagePrice) - ); - } - - currentTransactionPointItem = { - investment, - tags, - averagePrice: newQuantity.gt(0) - ? investment.div(newQuantity) - : new Big(0), - currency: SymbolProfile.currency, - dataSource: SymbolProfile.dataSource, - dividend: new Big(0), - fee: fee.plus(oldAccumulatedSymbol.fee), - firstBuyDate: oldAccumulatedSymbol.firstBuyDate, - quantity: newQuantity, - symbol: SymbolProfile.symbol, - transactionCount: oldAccumulatedSymbol.transactionCount + 1 - }; - } else { - currentTransactionPointItem = { - fee, - tags, - averagePrice: unitPrice, - currency: SymbolProfile.currency, - dataSource: SymbolProfile.dataSource, - dividend: new Big(0), - firstBuyDate: date, - investment: unitPrice.mul(quantity).mul(factor), - quantity: quantity.mul(factor), - symbol: SymbolProfile.symbol, - transactionCount: 1 - }; - } - - symbols[SymbolProfile.symbol] = currentTransactionPointItem; - - const items = lastTransactionPoint?.items ?? []; - - const newItems = items.filter(({ symbol }) => { - return symbol !== SymbolProfile.symbol; - }); - - newItems.push(currentTransactionPointItem); - - newItems.sort((a, b) => { - return a.symbol?.localeCompare(b.symbol); - }); - - if (lastDate !== date || lastTransactionPoint === null) { - lastTransactionPoint = { - date, - items: newItems - }; - - this.transactionPoints.push(lastTransactionPoint); - } else { - lastTransactionPoint.items = newItems; - } - - lastDate = date; - } - } - - private getSymbolMetrics({ + protected getSymbolMetrics({ dataSource, end, exchangeRates, diff --git a/apps/api/src/app/portfolio/interfaces/current-positions.interface.ts b/apps/api/src/app/portfolio/interfaces/current-positions.interface.ts index cf759b7ac..308cc4037 100644 --- a/apps/api/src/app/portfolio/interfaces/current-positions.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/current-positions.interface.ts @@ -16,4 +16,5 @@ export interface CurrentPositions extends ResponseError { netPerformancePercentageWithCurrencyEffect: Big; positions: TimelinePosition[]; totalInvestment: Big; + totalInvestmentWithCurrencyEffect: Big; } diff --git a/apps/api/src/app/portfolio/interfaces/portfolio-calculator.interface.ts b/apps/api/src/app/portfolio/interfaces/portfolio-order-item.interface.ts similarity index 100% rename from apps/api/src/app/portfolio/interfaces/portfolio-calculator.interface.ts rename to apps/api/src/app/portfolio/interfaces/portfolio-order-item.interface.ts diff --git a/apps/api/src/app/portfolio/portfolio.module.ts b/apps/api/src/app/portfolio/portfolio.module.ts index 4b5034979..6b06bf02d 100644 --- a/apps/api/src/app/portfolio/portfolio.module.ts +++ b/apps/api/src/app/portfolio/portfolio.module.ts @@ -15,6 +15,7 @@ import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/sym import { Module } from '@nestjs/common'; +import { PortfolioCalculatorFactory } from './calculator/portfolio-calculator.factory'; import { CurrentRateService } from './current-rate.service'; import { PortfolioController } from './portfolio.controller'; import { PortfolioService } from './portfolio.service'; @@ -41,6 +42,7 @@ import { RulesService } from './rules.service'; AccountBalanceService, AccountService, CurrentRateService, + PortfolioCalculatorFactory, PortfolioService, RulesService ] diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 8384427c3..566ad4049 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -3,7 +3,6 @@ import { AccountService } from '@ghostfolio/api/app/account/account.service'; import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details.interface'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { OrderService } from '@ghostfolio/api/app/order/order.service'; -import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { UserService } from '@ghostfolio/api/app/user/user.service'; import { getFactor, @@ -81,7 +80,11 @@ import { } from 'date-fns'; import { isEmpty, last, uniq, uniqBy } from 'lodash'; -import { PortfolioCalculator } from './calculator/twr/portfolio-calculator'; +import { PortfolioCalculator } from './calculator/portfolio-calculator'; +import { + PerformanceCalculationType, + PortfolioCalculatorFactory +} from './calculator/portfolio-calculator.factory'; import { HistoricalDataContainer, PortfolioPositionDetail @@ -98,7 +101,7 @@ export class PortfolioService { public constructor( private readonly accountBalanceService: AccountBalanceService, private readonly accountService: AccountService, - private readonly currentRateService: CurrentRateService, + private readonly calculatorFactory: PortfolioCalculatorFactory, private readonly dataProviderService: DataProviderService, private readonly exchangeRateDataService: ExchangeRateDataService, private readonly impersonationService: ImpersonationService, @@ -265,11 +268,10 @@ export class PortfolioService { }; } - const portfolioCalculator = new PortfolioCalculator({ + const portfolioCalculator = this.calculatorFactory.createCalculator({ activities, - currency: this.request.user.Settings.settings.baseCurrency, - currentRateService: this.currentRateService, - exchangeRateDataService: this.exchangeRateDataService + calculationType: PerformanceCalculationType.TWR, + currency: this.request.user.Settings.settings.baseCurrency }); const { items } = await this.getChart({ @@ -354,11 +356,10 @@ export class PortfolioService { withExcludedAccounts }); - const portfolioCalculator = new PortfolioCalculator({ + const portfolioCalculator = this.calculatorFactory.createCalculator({ activities, - currency: userCurrency, - currentRateService: this.currentRateService, - exchangeRateDataService: this.exchangeRateDataService + calculationType: PerformanceCalculationType.TWR, + currency: userCurrency }); const { startDate } = getInterval( @@ -720,15 +721,14 @@ export class PortfolioService { tags = uniqBy(tags, 'id'); - const portfolioCalculator = new PortfolioCalculator({ + const portfolioCalculator = this.calculatorFactory.createCalculator({ activities: orders.filter((order) => { tags = tags.concat(order.tags); return ['BUY', 'DIVIDEND', 'ITEM', 'SELL'].includes(order.type); }), - currency: userCurrency, - currentRateService: this.currentRateService, - exchangeRateDataService: this.exchangeRateDataService + calculationType: PerformanceCalculationType.TWR, + currency: userCurrency }); const portfolioStart = portfolioCalculator.getStartDate(); @@ -963,11 +963,10 @@ export class PortfolioService { }; } - const portfolioCalculator = new PortfolioCalculator({ + const portfolioCalculator = this.calculatorFactory.createCalculator({ activities, - currency: this.request.user.Settings.settings.baseCurrency, - currentRateService: this.currentRateService, - exchangeRateDataService: this.exchangeRateDataService + calculationType: PerformanceCalculationType.TWR, + currency: this.request.user.Settings.settings.baseCurrency }); const currentPositions = await portfolioCalculator.getCurrentPositions( @@ -1152,11 +1151,10 @@ export class PortfolioService { }; } - const portfolioCalculator = new PortfolioCalculator({ + const portfolioCalculator = this.calculatorFactory.createCalculator({ activities, - currency: userCurrency, - currentRateService: this.currentRateService, - exchangeRateDataService: this.exchangeRateDataService + calculationType: PerformanceCalculationType.TWR, + currency: userCurrency }); const { @@ -1270,11 +1268,10 @@ export class PortfolioService { types: ['BUY', 'SELL'] }); - const portfolioCalculator = new PortfolioCalculator({ + const portfolioCalculator = this.calculatorFactory.createCalculator({ activities, - currency: userCurrency, - currentRateService: this.currentRateService, - exchangeRateDataService: this.exchangeRateDataService + calculationType: PerformanceCalculationType.TWR, + currency: this.request.user.Settings.settings.baseCurrency }); const currentPositions = await portfolioCalculator.getCurrentPositions( @@ -1772,12 +1769,12 @@ export class PortfolioService { const daysInMarket = differenceInDays(new Date(), firstOrderDate); - const annualizedPerformancePercent = new PortfolioCalculator({ - activities: [], - currency: userCurrency, - currentRateService: this.currentRateService, - exchangeRateDataService: this.exchangeRateDataService - }) + const annualizedPerformancePercent = this.calculatorFactory + .createCalculator({ + activities: [], + calculationType: PerformanceCalculationType.TWR, + currency: userCurrency + }) .getAnnualizedPerformancePercent({ daysInMarket, netPerformancePercent: new Big( @@ -1787,12 +1784,12 @@ export class PortfolioService { ?.toNumber(); const annualizedPerformancePercentWithCurrencyEffect = - new PortfolioCalculator({ - activities: [], - currency: userCurrency, - currentRateService: this.currentRateService, - exchangeRateDataService: this.exchangeRateDataService - }) + this.calculatorFactory + .createCalculator({ + activities: [], + calculationType: PerformanceCalculationType.TWR, + currency: userCurrency + }) .getAnnualizedPerformancePercent({ daysInMarket, netPerformancePercent: new Big( From 1b81409b35117321cceed0177b7f30786ff2b095 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 1 Apr 2024 09:02:10 +0200 Subject: [PATCH 087/120] Add OpenAlternative logo (#3225) --- .../src/app/pages/landing/landing-page.html | 6 +++--- .../src/app/pages/landing/landing-page.scss | 9 +++------ .../src/assets/images/logo-openalternative.svg | 11 +++++++++++ .../src/assets/images/logo-openstartup.png | Bin 57189 -> 0 bytes 4 files changed, 17 insertions(+), 9 deletions(-) create mode 100644 apps/client/src/assets/images/logo-openalternative.svg delete mode 100644 apps/client/src/assets/images/logo-openstartup.png diff --git a/apps/client/src/app/pages/landing/landing-page.html b/apps/client/src/app/pages/landing/landing-page.html index e48b5e6ed..579b35702 100644 --- a/apps/client/src/app/pages/landing/landing-page.html +++ b/apps/client/src/app/pages/landing/landing-page.html @@ -152,10 +152,10 @@
diff --git a/apps/client/src/app/pages/landing/landing-page.scss b/apps/client/src/app/pages/landing/landing-page.scss index 6d5578ffb..6a8dd8ec5 100644 --- a/apps/client/src/app/pages/landing/landing-page.scss +++ b/apps/client/src/app/pages/landing/landing-page.scss @@ -57,12 +57,8 @@ mask-image: url('/assets/images/logo-hacker-news.svg'); } - &.logo-openstartup { - background-image: url('/assets/images/logo-openstartup.png'); - background-position: center; - background-repeat: no-repeat; - background-size: contain; - filter: grayscale(1); + &.logo-openalternative { + mask-image: url('/assets/images/logo-openalternative.svg'); } &.logo-privacy-tools { @@ -133,6 +129,7 @@ &.logo-alternative-to, &.logo-dev-community, &.logo-hacker-news, + &.logo-openalternative, &.logo-privacy-tools, &.logo-reddit, &.logo-sackgeld, diff --git a/apps/client/src/assets/images/logo-openalternative.svg b/apps/client/src/assets/images/logo-openalternative.svg new file mode 100644 index 000000000..b8488e9ac --- /dev/null +++ b/apps/client/src/assets/images/logo-openalternative.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/apps/client/src/assets/images/logo-openstartup.png b/apps/client/src/assets/images/logo-openstartup.png deleted file mode 100644 index 1eaa2c43017fddd6cdfb32408bea9040870423c0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 57189 zcmc$_Ral(e(gjG6V8PwpEx5ZAT!Xv22X}XOcL)&N-QC@xad(I5oH^h3pNqL4Zu)_K zrFQMARkc=a!sKPe;b5>~KtMp?Bqc-?K|ny=KtR4=Kz#-NWrjcz9ryw6C?u&21^n@Z zG71F&Aq0^W5mf%AbEXaHiLyY@i0gj1=m7)?q|R zIO{LUW}Lj!FDj^PHivHi>Y57*YMs#b`Kax1OeT}=EID_xd8lNT;BooB`Pi}Ub(;}r z7)8{Ls0Z~O924~0mtYX#+<77asg3Yu|Ns8(``@oQ#BP>}{^vShB6%1jJ<@7a8){8t z;eW3BdlT?Ntp6V`l!x$hXRxvq<=C+&c6bcsNvT8%M3A#Sx=*7Hs1y93Rb62NO>Ga3 zbe1QUKMXo&g<@pb<*W*DN)=J?e&zC3cWpg+p2_SX{67CbH$l;ZMgkx(E)`id-@j-Q zkb`~_nP~?~pC9=VLx{vwJ$l+;-3+&=Y*Z(g57Z6(>s7*jpsyJE*(sF=BHV0-&W(Fz z_;*2)_J4nqE30xO^q`QdzPuJGtqK|$_-8ZxWZS8++0hK@5QpKn7qpg=A(%m2EJY?|MGX*1 zpMP14G4wz?yhL=h|Uk=tZ>)-8?7eWL^D7aOk$QAfR6XLz9XgG4jXT}0gc+{J4!9M=T&)H*x z@5#!h-5zDB6pK>Xzx%87Ow!IqOthdp?(Q@pd9V%xDlgv%I zpYFC+`V}qRgh++sTub2rzkJK}2P<*xr4d>_95e)RZT&CvzO)Hw z25=2>HokuoEF26NwBcaR39kM*FdHW=gylf@HsdDqbln{I3N9X4GAV&cJ?)Pe^t2fb@-fv+$h9$46NHbw1U z!M;uY!TPTQ373Iu!m#S^$YgN1t7^C+!66i{;fm*N83lje!k&3iS-g?G@5mf18M8!K zUBp7Q;6Aq+i7>?ytEcc-V&N(CF_BrK%^|~~My@vAK`diY*o+_x zA6+|4Fv0ZY0CQfBy`Q`n?kuZTgL1t_{`P)Rah}<9|;5lk8!vhYwIMa&$ z-`EHLyJh}hEYE(=|M+6*{%1|NTRSE75Hvd&R^ zH%)hfGSd&=blwfQ@5_SIe4O_~+T#RAlI00zHG#Y*ny|+y}3bNmE_3SLW)%}7J3OD`_T=*FX`Np*y zfNo}(TSjiaQ?Z>W49!!q)vD?Bb{Mp3wTuo#$pq}i98O?^dRbT6Q0*Y01;P=!LN^k( z&K6D%XAcBMG*WmeRm1xwM@3Ej`_x?l;8S-6HpVy$1}`H~r9IKveylQJ7|8*~3l$!2 z;7dAiJNtDv|7M*;4SD1zYvo#t$Tbcz4PL?Ia9 zwh38qhoh5Q%wJgf*~RHGN7(Hf_ePNSUW?aZGB(+Q&~;IyaCq9ySy8vN0uJCR{J z9naVx=Z4?>bJ=zcI>ah60Ec2{`AxSyT5mW**7oSSo!bA;|5YKx{J$xdkF!UqI)B9b zu3zGXB8X{PhwfUcCe*L0Jt4Fc4^WUdd^^2tb)Ax7|E*UrZRmzVmqe%Hn~< zLov_Oz(~}Wz;}lIZ^$Kz{KrLY=$tMHm6%9DJWd^T7%mdjL-~{#S zdwlZhPGfH)xHgG@dEeBI@qU+|wK`9OobuPW2e1-4b-?M%a{$IL|ue|}PT+DhzKV<=_zsI++5cCViuJJo|yEvikH z)M$Nb~zTsp#dt0UNdZ950z)&RrGPM%gzY*7&=Yiof%xIAEZ)gpy%^(boay+Q#E;^a~2^oRiWr{r9W0i_gVni32gB8jtWIVnq z?7~YJe&8k%H9IF1-jAA)%0Az$`W`p{_fnD>n6qWrPRYjAkZ0;n_}y%o8_N&RsDY<( z0YFQ3r{sMlaM#L|j}eNeXZRnS_shTm8lrW^&+<$Ans)QxiEX?-Ipu6*(?@@2|ANod z%w6St50%%O0k2L=ow0p)1)}m($>U8Pvsz;d&UiGrvKbSNMyGbv@yqHQ5mrSTK1`zL zy^e$%jQLMK@HHvnK${;8hh>kr()Gl)jFsG;jIkrxStc>51#|3=BdVJ4Z212T>-BbE z>KviyUK@?e?i5(OTN5Cq_vC&LSna?2Y^!(KHE^8=<|aYwS3s*)_gLZ$^6`&<@~ zAecQbn@^N!5j&m_w~ly-Fo_q>b8aXe&RvLjJ{zmIB;GV9I8XVT_O&B7v^7w8se?jw zzPuJ(20YPmXC-nd%3v-3{BPS}=Sb2mk)7}!!*o{NpV88FmAv@Sh{9ZXms)Hj$J7;P zJ*ZwYE393K&Z~dP_w+WP|D@)4(BPq6X)kPy`@W5b(&J0&!|MCgFnlc>S;6IlO8bo6 z{zU&qH{t{?9LZPLxrTgyfF>DmxH-}Di?Av=z>rGV^j$$(S+5tZ3uMGYgcoaB;vZY; z{$mRYqM^h>+63v`gI#j9z33gO5AU_v;_10YsdMfL;Wn`vBE!rzxB^NNFMo1;@U+!e zFoJhAFTfuZY(khum|vHoiO4^ubXJ}Vnf(fayOsntU*k4rrOn3b0UGr+pqDI1LR_Wtdo zfr&(6DliLxUe_fgj3=)~l#m_9*KudCG_hiDh*rMq_fm9*9RCQix6vB5_?0hqh7H3x zq`$F%NU7iC`F0Nxx@D<8)>b%bdlX`oLyXL{5q_#WoewwlbHO}9quGsFc7rct^cKmO zzDEl?+HBMP7(ZF;)+kzR%C-Z9PTP4!`ZV!urSIPOb1Td5>L>(-vtXcDv-uKuDp4Vz2ep~F=T^vwPufV^u{VPt2ZOoHuH*h3Fpe0 zZSOJ5lWFgBwr=RBkRqNp!^OYMB+$^j?a6yB1JH(er(Eoa8Zmn_pQH`yepgRk$37WV#E zm60LwXOc;-bPN)oLJ4N%nZ<);uC1SlFsa}FX)tr02)kL_M9&IrR=+5VA}ZkZ2x%RW zU~#Tn$$X75ljx8ZUaPafgH~Q%j!Ij-mB_cu#QUse*xTgiDzd_|Uhb%`4!-I#ojkQ0 zNU_hnFJp8-IO&2(4Ad|SPlDjaCl{{`=?XETzq|{!aLA<=_ZyqyZ(4NqS3VrD z*3}EZF{}>B^r~yKxLLT&n8k3i3HUuyx3ibK(sz1&oZ)_<1dG82|8E&Iy6BCi3_p@t z;YUj^Su)y)h?@I3tb;%QoxxZpW-S*zG@f6t>WKjqgOWR9H2YY)Tl7B6R=kD#-#okM7jhvw{_eiJi)p058?o+KIv2+GsvzAtRE&ruD^w2FZBM*IV7Tg zf*bjTOoQc;n4FXsw4{3(+i|45ixwDyXd?Zb%rZ9|xk*4#3~{>B4v9F2capszcaGOo zI%c$`&)egTEuppSn4fxqG3@h~pAGJiQDa<<_a-uUWT1CVdN)$wx?? z6W5O@e?0`(TVD9e!-a}K@~i_09nYgR){Uu_)4hpTIGyF$pQyiL6>m6@AT-i1T!%e5 z7`EEqPK}&zZ{0{R+LHep(o*c8<61ZduQ>M(C+dj4T+_Inshn*an%UtY0#O5mIt8?M zlRB3_V+$rsEM+G(rtu5=UR4eIv&%EZ-gFixIdOWDpfRdV4`@#@_uhXW`LWQo@}Fc6 z?BeIYC($H+3~saCe)saDz?-T(`&QRvS~=-!J%X8!A*TK^(OQKZ{XblS6o{NUQ0F@H zi2(>mb6XkVoWc$ZS{R?SFXqqJ6N+LtJ}#< z<|i%O0zYNFCs6)&+-P;RDbBNQ+!P%;>`p`I<75lvXdDrs|Y0`lTtYFidUk zMBG`+@Juqzfd#cGL(IheSAQ|ErNDR1kBc9%b^h@N3JGOQ&fyNyI8-Pt2tZpr7c-uZ z8eV!X7tq{3Xol|5e&(GgBn*onFj>DmMA1)qaOq|VUcMucHp5WZ+3R+#ICqu2FFXIs zi=jk;ohWRcc35`%B@V|ff8mUsGqXv**Lj#(T!P{nAM;B^57@yo^Dw+vamQ>f(G|Jvej<$IUj*7XtX4y7!<>(qBw>jN z)juZi1DV4BRhbJ}=J(J|m5aIOeSTyz0$w*6tlHUjYku1Qs$71E|8dU>?t4W@If?cv zgaT@<0zTcUQcb>&WEMnm5DU$;2^Cg~Gl;1uk=k8#@g$fGPE1Zptx4nkvYITr`4;F? zd=%aHtX2w-+AjR%#pq18^VTO&K|nLFB9l>=Jk@Yfe9y}BY1n%GSrJ~5gUKe_##I)- zfaaqwUYHYQ-a+X?-7#I*GKr({5~|$cZ&tfC8zBh~K|;Ez>eKiGbAA`R|1$&bKW2zK zRbP})@r+tF7}I1kvlMlC%$E`nNG!6r*JK?lH_mWH=Fgw`#a51GWAINGgyGWXce?qg zwI2#cK2my(sWvV41pe6CgKpyUPzd?x92b7|1 zy-jDxR~uVulCE1tBmMuz9ZNkR6ox? zj1k75O+4!-F)dZ7QT5|sEjJ9r2=w*Sfn6<_Te~ZQw#lJ?x_pQJtC$cV>ajBr2q?Av zm`{A?MuJ{NGU>%&E@`^wV9zEMZ$g!Ea(W(ns?4li6(PzOuXY~hi2u>N=AAlsVe7Mm z!s_{%Wsph*pICvE&)J)6^NIm^x;Luk!)<4Ap6lr|6oXsSMg>Y7&1rF-I4WiO;a>VD zF1e;$>4)thKJa4HP@hUk&ay zdMWi+VJ$h&CqY<1Smu)PxEVjVuG5fMCk?|u+}X&R>`hJ`*Tl|a&0x!QM7Y~Cz8nFo zCx+)fher8?33hRannv`32yI8nuN(?b?L>uJN-1?JiE}@05{RhSDE%(s{YY$l*b#bK z6)5@mkmb9gk_6-ndKFm12z{W)Y=ixmm@0;H}##M(ADbN2# z(QiCiKlOX9pcxU;jqk>Wv1sZ1PhVLqjDKU7K48rmKE9B^`+wH~j}y*julmgstym$h z3zo(c{FzSFoU1Rby0Pk_E_nR#JY)ytGXvgAasx0kgkKn(w2qEr)yx34o^DTCw=iF4QkJfEZj5(Rqd0(D4n8#^R=v zXd1f;RaBA8!^JA^NO5DVtb|eAD|@Vi#L(ri>n)2x{@pTic_s5u68k=CRDISw(E&ew zaNnVSqcS&Xox%=#Bgpt#{bLk~)kv*9nEA3Miex`hRpffqDCUD9)HHX-a{Ds#w~rYy z&>FJ~*o1Ikn{b8ehy3HlVv-xz8ogOZPaN(ViXM{q9uL12GK!&T_4xS)%(JMc`*ftB zOQ@52RvNj^@yA0av#jlDQ=QKp&8vPR8l>?=c!XVp6e2ISI*v&uo*HnqPb{8yrFg9R z3&jic4<-!9ln~k+jT*v=)By9oYLJCS&M_|BJ9A>ol60`9EyKll-J_h*a;(`cnA(y| zFa}#8yWk}i;kUTD(#}w)=u)S92Y<7J*yNBlFw<>DDlsv25icy z1WVbUq)se5;h;+lmSSsBAHy+#QIEWLcMeOF%&}ns?3IfBTQj3eQN)guk8zC9PFAr? zbV2#aLs}c%v4y9^`B2i!uM!_unMHW2Pb4AUN0Tzka8v|2#t|@3Ej(4swL>V1rx_|) zQ?JZT*j}fPUQHyEI0M^!5f?KTzu&_$gMfVde);#H7JDCNBw*uP+xQ|d?>3+kIpRxl zTmigRk{A(vUOfjtkpZj2N_;yl$yg{F9lDuoB{}w2EF$H)h;Q<#XH@J0f)A6u>@hDH z&+7s=W=WxzgK0G}Xsi-^O)0E6DJ#JT+2y1!9+c!bzrtzwYVWS(fO4 z^u;r^Sj~vx{QK(Cv0?B_tN|&&)K?_kA6v^~xr3QSUf;xSq+a3c?is2A5K3DDbac$nFN{?&`$5#)T zf=()DaGk=!O|cLP1PzR_Gp_ zcP^@H3^^kFhrWr>QH)jwY`0OgD{~dWzcM6|q}HTC^P*AMmp&A`>5$h-8$$u{MCa$3 zI|!r+Fe<54sQ_Ayi{-o zwJ{_<8rCjbA4j2ul@j(~aDdt=9n_Won|q$oa^wB5NQ~yMmvj6*PvMh;;{l1yiMl1L zqHlh);R0bqZc&S#ST6TBTU~o@?h$vn&idVszq-#yGnZ?X?5**~8!(8b0!E)JAt+F9 zxbK(7_D2sk(l}?DTFVx`4UGk6RO^P2o2J^EtV_u)5xkFf9t*g@oE*8Zb>b7;fg}&t zcwqRVMg_IkaKf$;Tq)b6T0ZU&vip)FbeeTG9PO;_iNtN064?iZWxut?e^L!+L-(I~ z+_jw0xJh;Qxn|FzmyQSr;Y2~gHC&YAEP+Zg{NuS~|8eWig0H$pel z?@a*B!}_Y0ppb}_HMHXR*9Yz!KtQvtSAvmH-ymmW1M)rYr-gpMp4hxrTWtDOuF z{Tfp~&#J)~{LWhrX3w?uf6fB%A<7ii6)n?@eA5J936N}XJ2;uFkCzb8Xz0WlO{CIo z@eEv178?f>oX^bfvtPH0-Xfi3NA;S6TET)lium zT2;n==HBC-p86;^zec~x!dYZqR;)7yd&z}@elk*+Sr|=Py=O7(`5w)9FVxn4%sH&J z_Az0msyfehYLZCzk|8Ep4?B$9%+|e2*&?X`iZ98Mw$4uGRIgDY-2lo`$qNb zZft>Ls=?%r$6}ddRCRV!SmFr2+Zqg}_&{_ni;K7RFd!a30kJ}|-U^&)fBhEGv!x}h8Uu5YZp#k z(&zJmUa%KrhR~=t=;mCKlagYW+(Pj_-H!9jo<}G-L|&3$nt;Gj6|gUQ4cBF^mSI;k z@H=bjx4W+|T;g}b+A-lg+dVkkpKDh8jFNnZ@FDj~t#m+V+M{NP3k9{w22L2dSzN^JDj3whg24&%RN?$(jl$KoDGwDowpcZn592II@vhy-FZ>!)3x?6VuxM0mPxUz3%SEZ7s=U4V;w(e-c_az6h$pwXTx^tZ!2L zX+9{gGi?{X)^&uJV-VQlL6_yuaLEM}HXL=GA#}(~F;qXi)PU9V9lZ{M30#U7lq?#c zu*Eayd@IZ2ra9b3!dZwxD)b>oXe58oj4qki=<$*k($zJq}@oVQujmZ3>q zGoz`BX_r)K8BxI=rt^z!23XXl$Wjm1ZhH0R$+aTk0ezGqFl(?~lPbfA>>P>YcO|mh zd0QdGUkSNtAU~=g@1-E7Nzyg;<>Kb%S)wgpOet**QTpu9ZFdev_ielOYxz4}s}@Uh zT=b~o0lXG0^;EU1K_!^SIzxuKPIH$D&}@unWDQwz?m(68{m>y3hcM2r?QT`=D8 zVjf~}+d@{=FaWe>mkK%pwg-iAO*wa%uXXgtNmuJ5<$Btx5*&-2gZMz!b6oi=QKx5X1xhL1Rh)P z#%gh$1ACxC3T)&RWprH|1HHjVA?eGH9>N%&roD$LcIV<%@kLMIY?y~Yz)%}VoBd*i zasdfEG?}fU=C8Rk!a|xh@{7&rmclUmgs>=v9Cn}w3j|DX);UU!7Jz9@=!6?Y-1LJq zW<%@f*SG|DvFrXf7Ad}dkk2(e`nM#gwgI|;Wi}m=w=jACJUXtB@vwrFexR@&TZCmIyUb^$wDu(#%iU>R--lW9m_ zp#ujrL;NC^qXF1120E+FD+B&lo2jU)t1L-OPCrudqYf4H^)QC4c;Q_s>DN(wKdQuB?5-<41fqmmjJI z42oJ=eMXvFBS09yKOIJaP6kmsOl#37IwFyLL%ARs{6%pf4s@9w8BTVvAmUhKmm*UD(GKg#S?H(vc%kk2#{2f9GTocNd zOJsX1tn!#eA! zTy(^w_I0BElBxSmMK|@zoo)D;S_Y{Bn{Osbuj*a|>(Nho@t-A7t zUVo!iUMN>Nu00)gRs)7de8O+bG}>HY0wdQM7Jk5v*rzkpy?IpsF^|oro#7U|9Kqq5 zRZOQ1EHhSj&MZ`oH%a;~@%^(eugoA}3vI^Hx_2WQ23vbUr^}{yGkQCDtoC3CvXCpJ z`eqR`RZ$%WN$mF9N#ee3Ry=R7mm#@vwupJ-Sm)%>OWEI{`HB9Bn89Dc0MbuU^d>*~ zJZ3tzxB81zHgkrhQS@V{jpg_oN!l#fN-IItEDATw;8q&3_D$t^{v{e0hp73z8RWog`-l?(GNIC3lSSoVAuqx$=t{;=LOTDr z`D2p>*ehqOO@EclG}L2Uh&^qb36-cS2>i!pa!YNU9INL$YLc_7KyG1dyEqEF+9+B2 zs>D}se$c6l9wzD?kr}`St`=#PP~Urn#M2gs!Z2S}<`eIl8Ia{}9|!{7%hPK8A}T(2 zHE$Jd)D0F{kkNHKp?Y#afaS}j$`X@77cJoaHXDvT`L(t)+y329rZllB-0gfD1geeH z&?4hz^Y=!A<-OA5+}p3PXpw=cYDjXe#7vxcUhW4txr4hq?2V(2M`(S(4`pt1XMLOX zrR)b-fU`oMs~GXFCbd#Y=`e8?ouHo*QDyduQCDwKiv z?|drnlkK6tSznGAk+*MNt~pY&>j(G{E)YN^RX!ex(wS7Q32+3m-W^L3gFAscx32m! zeojtQR$H~J#4jZ5T3w4?m9(dhsz5)C-^u$pmO5=wu*T_c=jT1xlNRgv^I)E>9yGLE zhW)-(;Q>N&P9EsZ0z4WakJH@r#!|b&Inv87(6qfRTHe%DJ%@U%wl&5}e@j=7VD5Yz4~*vgZb5y28az+`o-*~zvv@{W z`>**6zcb)WpMI}j*H-NyFu6Pd-8Nd_d};22DQ$pSV&)p2#du%f#%c5J5umYA50k62Nm3YTD(P1e|#51aSAif8du=*4)@!} zzW7VBbp5O!wO$Vl^Hb3k_sylkS_zZ9ivXF7AylSuw@EWd(V)(!goz-Ls!gcha%cnGH+IiRZ>V%&8iBu5K*@ zyh?)^`zk|TsG;UfpH?g~w>D|Eq*(#8G3%A=j=NZAf>>!ooL{dwEbdWFuC^}Q_L5&^ zG1JrMV}qi0a6t0}{>GTxz~O!avFa7cCbz$%?ASMxRF(sRzR^vMSg4Q7Q+=U8`KZuh zNGNgqplSycPpupWUi=YRxf8T=9E0B&{|!Oa_$B1R!|(W2K#;YOC3v&ZjN%lh4hf0- z(VTZPNgEkWuH`0>cBQ^mLiK~M2kz0g6iE(qsA1BMyCw!>KHd4sT%^@Tx2uSdKa`;~++=p{KxC7xsLm+WvhB>8&Iv^5x`WqeefS0B|$ z(z~PYy~xP#3!^zy(i76VNbQB_Bb8ls!@G+?36~|@+WOw9gkKE3NVak#_%63fdpFTB zueG|rW89%o*+=@`!0i5HE$$53f8rKeE4Vuyu-OK5V&6+eBe^jl89q9Pqwh2p+sYml z;m2E%0{1?*8S(GluLkcK6MvxPPUHh9&Ngoga-J7Ze9evM@uw$_8;~XKXETB2z}XM| z52#JMwm@*UAUo%gpM?P*DAIH6;S<7$^Tdeb*Uz!aV-tdt9@uqN@oU-x{;>+SXISaU z2t@l+g>nqDvuq)zqpG9T1iLqFCk_s?u7GRTZBvSm;G1bbHBKV;cwCqpV_vHgza3g^ z{fLRYI6tQZfnZ4(TjT^LlOZhLm5QU3M}ok{+J>lksXkP}2a#)Aoj(49<0sJ(c`v|4 z+tc?CXan?|!9qE(+d&vk??5dI`QCGFKl$FFm9(Nt82u7K9td(R8zzZOS~)wtC2)AZ zT~ar=SF6eqX#Y8CN)C#>ZT_a|tt9rNjv`(@W)_Z8hdhrkN_fG$%5tQukI}K0zuCV@ zB2O)eAM{0`QCy!IC=CvnMjC2(9znCTik3R{5Ow$_(NaN+>}2Fl1ipF=J|!9E@wgkZG?x;%KA^ZOq1OFmG?+3 za*_I<9OFE?Y;@EG!bO(rX?74d_1E`}1d>a{#Hu-;W*C6`f+XP+4aQbqL3l@=9E;4& zujHk@X)w+SO6-0EtB2WjFlG@t&eLb=2rp<+Vio-S6;a}ax?JKctP!e_$l!`7`8?39 z3}jBOyOnltscwRAU&uCRyWL$kG+S;Yi^3WfV-sQAc6R2KZ5Rt^(xo3V_w`Y202>ba z){nXAA9(wQK8{*7@$LP9x@E9*?17#2tD2Gocf}@X6^)KV#f!OgnO8gfQ*QWD#)NWR zt8>wbp8w3f$x!_if86A!;Gc&KR1Qs?F8q$0(0z3Bxr>ONt~DzSpynRbbZ?zLgPvAi z7{Ooawcb#8Uyq>2_<~lhqQ%%PFL|nsySBn&KGvI0Rwk zTP+EE_>J}z%r8s6qZ-MP2Z9~|$4N~+`^`%5i^46@hIci5ZS_I@?6u6Ez}&dYXE&s&_A(RHpc6#Q z7QkvyB}9^8{J>+0!DSQe=BKzm)hD(SXTIg*lzg|jt3#OT@wh6gVUhYx3&RDQXRnsk zH`?Z6pp~w(Z%%{49kY4bJHETN32t@-2e~EcLuDQ58JY-~Yn%kFZDMhBr;S3sD;>au zS4J7{C5+9+m=WxSU_M8MV1y|wiw*7r7VzQ~Fq;X7g9h$Rv+WdpHJf*c^dFsM<3kfQ zkkjNE;K5&SiEl#i2HwPPAU@_U(FeUQJS4V7_GpikH zy&s@ZF-H2@uZYkHl%xWtmOZ}DxWLZiFI=`1VOJw{(eQL$ag60*P|mM3fAi}mk+%TC zfo#(qC7Ii!SYXiB!+mT?2emU=-H{m$Vg#e!_j&P{Vxm|H)aF4f#O|%D!-T;aI_k0( z(YRX#l^K|HQ<0f(v*PL9T~pZm!PcP10mtewpSDUi=>@8C$#ZF15Wx1!ToAB@%u<|q zre^SYPe?K5I?BQ5i$TuC9e(WRL>{TblzfyY}Fogr`FL~avD&$B-PAuvK%I;ALbz)Sr_t?`z9jK~|Lu8X6{3^1J0)O!j3x6D>{C7 zazzZJlHTXH1pQ4hK#PWc;f{RLcb&87vJBxdbT~KaokN86?yh2_$wzx6DvKh}Ft>XF zlvK|c&3gAmQEsGhg#z=rZfcq4z;=Z02cmAH9D5hf?)==cUkU}Tedn>Sl$MeqB?PH| zUN^6J6cG>*V^wxs4bVF$bv6fz%z5l6BBFqY$94(b;7}UdqwB821FTNx5|HOB5%-L) z5guh))-r7!?$jno_GvcVEv$Jep)XWC+BKzC*8jXs0tLMwhJbn@NavnzSG6WM@0NAJ zY?Sye>67=GwFVSH#q=_TK)@$$u$>yw5%I;JvOu%NoYi&ajUlQm7t3Xuma5X7 zZA-eM#7?ctagmzM)RMQG7QOi(FspvNm(h%1XEw-Ex)tcI~yBI8LH zuov$9NO6%l@`CKGXxd0e%VqL0OAp%nWj2GqZF`h zvcLWs5W2|(*Alp0kgzt`v9R6A5WhWVZSasltePvEfNpE&M1Vty|4LjG`^D?xx>z^Q z68e#k8c9Yq;mmKnp{E1Kd$lkxrnWL>cn+Ssr4l&qs#gjt$rANBp9YJfR)z4A!y_&&EcmhDNA*?kl*uUWlWBT za5T2r4saUa$WJ>#aNfwTw?(v8r!lmGmFvuUX{bNq6yLpyvexYhZXx%$kZ#5-*?1nY zR(wY;#pF5#p4o=5%&5x5-s*)_PNO%9k5}L0ErL!t40?^K9#V4j7SIGN^XBI7j>5un ze}g>|p+?Yg+s}7XDNmBs*Oan@5YFV9?gfaZq`d^|GJ@gLApaeWP#Ep z7;#9Ta06N+1}zy7hoqO(XKJ-_T-YC%4G*^dusgRnId{t20$+Z0C}%5Mc&zhgL0FT( zD3JFJjPKivj8S&u2OD^R+g#I3cgKFSWy;Vlim0X1S`yElNhWcXcF?WCErpu7G@CTE z7-1=Jl95iLGU0E6JpU+hV}9*e#~Swrg!PxVZF1)MexKFQTl9{uv zQ9h>`k1c_ZFR)Inw%V4@;_*PPb(3P3)HaoH^wa0+OHToG>FYkNm95X>I~Gf~SKPzC z4RcAFzxfMT9n=b0H`n~9mUe$*9m#N+J9l5o~SYB0oguV<1MRZ+KU53anxa5z-3scF zKf!<-Wdt+Oie_o2mW#e2a$01Y@aNJqRq=J{A{p3tSU3{IC^4~0tZTuxhqT+Ox#!%fPt*@@S5|kx!CXKRJmjjF#}lii#>d z9mA$05XX=#qF7e)Br}1xrp?7af=?n%t{I>(WygW|)z zd*ANhta;Hpbhpv+@42+z4ExOi9Zpy-aMJT2O``U8UyI9ejT>FgkdAFzBSst4h<|nv+;w1JGR1hMP!ou?#bgzI`-xp^BvjQWF^AUpzLjk z5L*n1r;2*R07^1d2b!jadB{U}{=qzcJh>9OB35qSlWGNME|OFgE?_}<@gKiYu_nL& zS|`){K;Id8z-DLB<+3=yzFT)-7=oSJj_}mIb99+%hT4@JDHESiv<~FHv}2_Gxw{0# zBd9L3gSz>e4hS)B>us~ikx}U#PdB}$1&Je3y!jA{vf~vPSK>hq*H$h^f}&+q5Wu(} z42$WZ$&PK1bU{EGLdcdNH-a~3(uD)ukHtI=M(yB5-nZEQsuN$poU{M-p3m}=qge*4 zK$C|c^m?Dsa+31~oo|_KODHk#kBsSx1p9}3GpM#IwywEKzI2F`w6oH~+TksA_*u&@ zgvFuG!Ikq(cz}**%-Sp1V9c-vq8v?dVrVfz?H-(f*o1_l27a3ftJ_#QFURk$kMons z>g3R%XG)_#k~)2XfJYEdjG|^JzF$%@J}j>C@{ajWTWIBJwT`ye{S1Eudz01dV_fU| z-y&J>u3X@NCEPkRGi@YRcr?X)32F%){2;fjVfd2$q=r>-9wx{i*)Dl&3}wJimx97Q z(e2A&9?C^-BFBp5LufNdryWaP+uCCDBe%QY{4%6F5urZzyEa>GBo{WlP~Rr@`%F{8Y$w65Qv( zJZfyE<nAy8UXmHN#%jx8IlYbI#pSYPbpXDxi%%LygVgTAM46q znEdN{AzOATY&#f{gxYB_{t>t}9$mHSbojI+I@?ZsS8XqUb-t=oO`Y*6`I!g)F(%t? z=F*tfG?7^u#@mX&MsT9tErCWl_K_-AG?StxXH}MJOe#&b=E8e9|820tR&D~2zR3B7 zY+xv4ZdvrmCVc5?5L#fnqHF9sp6GZ$ZP1N!r;zF4-P|-yw$lc}=<>xF-`vr9`*#!> z^&-coW?inVM1cw@w|RkVx`miC?3I-4d790FFQ^zq12SHP&79}Uawx?ehY?X5$A=}u z2a$j;YwW(~h^}7R9b1%)&u;6W?C9>gs5m7Q6p9msmh6n6Jw$F5a_wJrL(7swLPW*4 zsr{-|a!=jbkF#L|G|ClMC7X6BZ8Qp_Debgt#aPymLf;qZ_}n3|^Ka;|_3&-6blga!W|(^_@yplt4{xyUp@An$SiOe)}1L z-<2X&%V6o}cCxHdl2)os7@55UprKM^Pc0zMI3?i{pp3(xMZ14OdTwq02nbb0R5`Gjw1=r2Eg# zw?GDJz+#(REHBG}J!al9y%{)W{DGm+*&}C|eZ_gUg>eQpBMv;QhQawPYgjfra~*r8 z-7}Jr$y&E@BQQ)FgT<@AC3e<*yRxOdG|Ob=m-%C&rXctN_uE8)wsKKq1xtCGlm-h(6TfG`^G?cC2@Xbg4Dy8Gh*%2;eWYU~>p?^L{G>=)i`@ z4`7Y-iO*kZw9ykA_8u&X3p)4!Nn3Ec4HnHwdKdKW71KrZ{SG$Tc;H;~O$C{c>a5u@eFIs__)h4?oR9 z$$Yz!NltzSBvG_qy#1Ks;sSZCS7}We%IP(_TLVlD>dS0wVtS^7XXRs*^vDI)PhxNI zY&V}A<`q_K$SmJLW-SZlqxoTp@L={hYEMP0Rfh*uW0oy2tj22OU<{Mf)>a!&X+Qb%!syt%-g}LY&IZR#+jq znA~0V;8{FwCXNi?a?LitTje^hR0mF5j6eIw6^*}HzQP=qcyG&n%OFZbg5to+i>R)- z5aBn=JPWTP>Cz&q6|YWuyN7!8I_M@}Scn9P(s7#)wJj(e79iI@xr89rf;oJWn}oA2 zFPBre+by_L_=PFLpf{7z`W)iZukpbG%mWtCM`FTLU(U;dCn^??E}_^BN|gE@j+%+X zJWr_iI69LPzeRt_>HdmReoHq2Y8H736k)aVydrpuELBI?(-uS) zyGQ3gdq~}49;7;Hat=$$<#y&7ic2WS*ltv9e5!2|Qv*S8sbVr}C&2XoEKIntiRVh- zXTh%WoQPZL25142d&iG!@m)G}Mi9(@O){#?6aDVH1oQ#paXX`ZphIwGz4#Y2>PVdd zVEYHvhwtreZr7pTJ;MqVco1Nt?O@EocvuCy+~!bCkfOv$ZZbswb_t~g@ViF_dBMN& zARNW(`-HTSQ>6xQT67nZqSx!8HVwdW_}b(mEp#$>_fj;(*uikp1w1UNmwbJ zD)MeQ46(Ehyxt79ztm{L|N3<}(adHVyYOwdZw9r4-r47aaru4mZn>aYYd5C{K6TZ2 z&3AFJ&0yy&9nD-By@^-2I~dMYe&OizO{Z^v;2pWwhOz@Iftb0caa2-Z442>@$R}>_M|)JjoUEFJaWmkPt77}U?8oB+C71$ zEudic*XWWFl;|Q;W-u>~FT&!19ndbENP2MLn3ZIeTjrd6UJFQTJ);+kwgymtV|U*7+(c(Vazm+SLrVH=B;spm4NZXyyNKl!Gso zP+GWXYUOqKV5B=Scp|=Y`Sp!WHenj9m3|~+>=o6DB2sQJ{Y3k?s6ednlJHLEocRd& zuF#>j2PG}l(%*?Dbtc^+xEGY7ii%2Skm&s%UVj=C??hWu!_wDtYS~|Oi=~_kqeeoH zHW7S9wFlJ8fi9KY@Kpa@W--t@;E;rR-zf(7N8!=>d@vBv`?Ck)HN4D(({Pysl}=9M zfkB0bHO)i9@)xW%Z(;HiV|!4@1(s8&N7{sOk6XH6fTbzIrHQM1(xw#ra=7iPt3N5~ zN0Lok{|cX7H`8<0J5KN<3jVw7l)wCCi7A#k+ z!B2N9Zh;ZF9i>R`uXGeyx?(@RCoZZ4pGuGYF>Jnh+w>lTQ@hXjGCr>AsMOKaj6d$S zkb%qs)<7&tmE>*b&^3_nP5aaKNm8}_BbNxYodw0^4`0!4V(9*6QRt=*vK<&0e8j{r zTq$SUK)umvR6;wf#|uRfm&Mn{st|wU}*?R_#gwY9H;ODysa$9hQ zo9imE3x-xW=dqi9l)Gh41f?*pS!AIcJ%e21!dcOLaX)DbNUf%wYdJ=C5%%VbKE!ui z&4t`3chiTPjMCLw)Vq;*IzpSCBJ>P~;dXy8=j@%BH&XbF>5OjaKYORDk55i~2Y3|d zqb0>neHRNI6(9`ur3yFRbu>8S!x;dp6&SQXP}e@wrNPF63_P$%7f7X#BcBFd2fIhZ z>ZhzJRu%Rea+gbu)1i*R8SDiEDRLXq z-$b`sNq|>%u0<&G2_M8uhuo{9>cloH)$kU1_j~Yi6@ppY<>;D4lS{GL+669jUL_Ei zLQ>2~nxdb@QL$L~kl29PtkL9CZ+E`ME0(B58f!W*=Hu)td=ftOXyO?tf14*P`cJz+ z4vaB_eGR&MZQ;XQabN8W_qm=swcAy`8i~3Q5Oi=2ULw@hl6Fu3!>*2h@gE@uQ!3pS*s}@*0x~PaB`*U48tRO z(>A9k_+>kU1M16}67PqecQ}a?laoP;FvM&0@F)_Q=@C2Fi+wf|j z5uL5S+k;#_RQ)T70GDEU^v|DryPr$vs!snLHI~0| zlZj0O=bsQ>KE1&#UayP8>pQ~0-cQN{5hF=dsa8=O(TrCDBgScoCgAf&qoav)?pUL= zizCWt8gq&GA2F7p3GM_xNMiZOm4#J*6q{$D+@neL0>XQOjF``_p~jS%_KNlUkU-X5 zXKCwu^^o(gz%2fH^T9R9B{9X-FS6bbdQXok7Sc&$;R1c>V!Q0&dh_9O(VF&Ri-L;O zz;g8@xp!3IW_b6yIDT_V-K==oCygri<^Sx`!|(=JnBhWo(u4)tmW1u2t|BB#-(Q&4 z0ZnRCv|?e)a-D5st8O`OwN%^@{`-!UTdLqr+)Z{utPyW;e;N*VWKXGt_j^bR4D@}M z^!pZaaNm~I-y5mM{}H!OzBHSZy1{LjBI zrR)_SP0;K}>fK&E#GR%Yp|;tEwzjzJLeBo%WTOLEG_!hSawFPFkj`@Es7%jz06QO4 zVdF=Cd(tP0+u61cQgo?=BhpvwA;{gn2I;QsC3v^PL_nJK1FMebuOHZcc2H*IwXJbI zz$5zqYylaC2$23Q7-kHOBcx%3HJjc(7Ak78eL|T448l;3Y2(fZ*}2yN@>j}kf+3t? zjs{u{w2{4v|75%A$VOF+k#f<4AI1MP(0#zIU(t(y*K_-Gl5X#8gU!AVIGcgOmY%fi zuS5EGAee4h1vs4bX_Zl`L|CXL@M~v_TBZHK+2=$>6V#F(?9`&AaHmw@3%ntUd&986 zWoATqGsiW$n2R2iJ*MY1-B0r?yXt50JlADs5_4pV4#Z<%G8sDI1qi(hl5` zo+g=nU3X0cO0j{<+Q{#RV^L~Lj)!QB9-BhO_&>`}=m!yn9+A$s6d}ZryYY^attZ_i35q z&wJ(RCwk6m30^Uc5Tx&v1%tyTNcg{xlqtB9rD|trPQ7bAhOOu-hSr*L=?TM3TL(Z( z;fF~B?mJ|6YfFF7waT!E4N==hMD06gwZt3Rw|#{@!du8&k5VcCdrT_vF@kiVwP%+Su@Do^~qSACQ|!|$q4ZTTkbKhOq-)Qwi^w# zVSYyw#HqK$Em$xe-ZBPQamv1Ut-vR$)FPhhG>K{DZ>CwE`P1E|$)ixV3$(Dk04H-= zVbT!v=K_pli*q{>b?%=zmFi2!DHX|8PE94+&M~U&mVZBHCj0R1D*dpf?D zFmlIfuUet&f%itat)3K#)kc+Zl@~X6wavfNWBeXtGrD^~Yry;V?3&`l4SdAVuJw7+MxSvW1Kwz>sB-e}{`z}N+oUhpx z{=h}_zn*>Z8D|5*u(g5RN4Aq*viGVl+g{BSbbki7Kb2ag&UUCXNVYU-367j zPeD1)n5KNK(Py6I<;7E;I)1zw;@!No57EaU%_SfssQ>_=D^z1h8DJQHd*aFSR@pcp zWC%v*dmd)H3P#zWkvtbFDS*#PDv3to?MixPC=uX!AO_ao`eoOG03H%OkGqbKrY7$D z0^NZbe%O!2C9ZV{((SiwPMn>0Ig=fxXC6Qm|PP)ob z+d5U4aaIM%Rd{pz;M%`4bP52V`(u}CI}Mk}?-=Q5hELC4Y89E)Sc`H?tDKl%GSxC! zNi}Rv;yKszHQ;eoU*8MVf?t<6@&TUONIJ&x(QvaVB_P#)(2Pi=$-R{~)b|&%{a-ow zUe6fIWi0Yvp*|&mCqbdq2qI!%>+F@(zs-Aq-)in;U+h%yyq;T{iJnxmu%gl+T4tGvJ$!kqsTHg#mS%+1lnie=b!?1s(&t#Cx;R4 z9ugC6k6TS2jk-7KD|U=eq?g_dcK%z}Fp8v`FV1yDo~_20$}u&GsFYJR4bHjGp!&^% zYxL`Nr&kp(nK9xK6boC3(!+I+=8#4cNy{W_*0l<7c6XRzRp`2fikUBdp>1>*Tc1TR z-U8vF?cnNa!R@^574M6ScpxQ7*5#ctpP)eX=kU1qYV!VEn^;Mgyw-7=9swd+>a1Xm z{T%3PQKMR}a?ZsOMls254zF3?ef0-3R*#4(0eaO!8z)0=s3sV8W3wiJVIMwwsaD*D z!a^cY8&)(WG@j4Z8kPgBIAmYEhF>_4q9`NNg2+nj53AMAniY&hE&mo+L>z|!v&%J8 zXEL%_aKyVK$=6y33~?(y%+ABO;%~I@I8wpPWM`_;oH?-*O&CHS>qHhwHpbiaik5mw zM!q@73~`wdit1GPJMqDwD938m>WrOgn;hNRT;MT%T?^Lvwd#xkNtp8uS4ky|kq@NUy9!fh z_R!0PK+>|h@mx+QF#ddy^m4%GkkruS>6-_6bPoV@3~__~6~8TG8@YMp7f4qlpm5gU z7*FjIo7+L-;+*Drq^E(-ZUAP*^LZ((=P8Fw1DQ}9bs~tVs*Zs3`pe&9{=g?CRU=6(%a8KL%ZHd3_~6nkI3?$uD~TYgRg6h zwfpP%iroAf#Z?bej5Ea&2w%NLVR3hLttIZ;dJA)-Wn0EE7Az-_nO$DmZioOxuIWm|xss?LmcS zTBG#a4*~zQCQ#uD7^^4%o@k2rX{{7W$rX1_i|wWJW{>q+%9`KEXf_{_x-|ZxpPmat zBDTfe$&e}s9Ie%$QhSooP41SCMt}G2`IY#Q@qG4irpp!nLg4@)(`Kvix;RZ=IdKQqrd6#3X5@|D>Vwh)j zUQ%n_@q?V0pSsI`JAEwhMr1WI59ga^Sq-o#USQ@zLPOSl869ksDp};Q|5k{ZfgF!0 z0x7|GQ?@L_cae%c^N|vso{KqNyVc(u>^S1z_?(LZW88^!`Yv2_EM5vAerm*;aiXIA z-cJ_th>VN4n|zTEm=+8*A->04pVp%k=n33et&;}doAN6#G%kdV+jgzflfylD z08RvZ^R-EI)IJb_^=#*x>q^;I^SwI`%qsKej#wzh?u}aHGw*GvD0;p|Qz_@2jK;Eq zdxPZMb837{F8;Xe-A$O_R%;Vr;TQ2qZO11j94AYk z*&Y0NCF$3R^4kiy!S3mNlHqD>4$ReI9mYdOS&Xf$W8ZCm&m(oiuu^rSYd(DkCbTi{ zuDw5qY5$^rzl>U?|Ab>gCq#6MN|i$V4fZTg^E&7t>Ym7OIu-^gXFEMnoj5}B6s3d!p!@Bw_8qkl3d zy5^GvWS0qF?K5kB!${PvfNpWF`FBRnC&$cyu#)s}m~r19c5G5LDPVSA)FqsE*m24qU4(p%v6J~JqmP+Ni5xLyvdV#4!J|ME(~Qw|4G<;$B6rPTV0 zx~R6xsAd}+(0KRq&K4NpNXd|mMhLeS7;WXMDI_ZjFd-jo|412v~gX3Sq?_@waQ;wOC49De3qQb>8ZZ^_n< z2(_<2dJ2u-P~W^NmgjVy#Zrr^dX?5(fmeqj&0tI!^@zq!J|_wX6`irq7M$8=qZzL? z;4rx}f{0as?EW}q_)3k=yw{dDc5s})TfS45e3Ui~_t5NyeCdvU|BX<)&TAv8-+-{B z2JPWS8%;x58Egz*7cEWPdyETY@>nA3tJ50YqDI~cP9LL3)OMYg$#k!oUeyn37jXn9 z+~?L!{Mj zY!QajBml306p$6z2v`5Kne2o?^lCYQMz|bbOs!c*R2eZ=dsN;-X#eku|JyRUZhzYc zm8pIehFrwP{p(~y^DLn1<$|t6cPT=QSXGJ;qyFbrHG(jgh4+EE>NZCb^gwD->U^Rn zP_1Y6CnAz`j^e+8p5HxXlk$8Gf3^h$5DYGY{4h;^`YG0qA^7{m>yrc8qb8B2f-;of!Ugw7bALcJe7+ zk%g^TVNAul=yf3ku#k3Ji8^r=@QH{G5owTQbi7|4*ex|@`==)T#>4GkBsK36-K=C1kHqf*7|jUbc&kTiq^m6H?0=ao6h_BY@`y3B*R30 zV}}^mc{Nj*u`nKvloQOfLKWiT5R5Q)8*yUw?3?V1gYG-ou?kFrGgZx z?6`woF$5xsMnfk99|3mJ9YP`+InoMrNeI1xpW=GoJ31({4ECM-o$C<#-sE{(2;Hp@ z(n#l(7nblXKqpbb*Cg9-OQiq00msCq5s--KBLAPBdyOIX2r(5!L@u|(kfR{uAXRa3 zMOWo3ElRxqeNN#C9K({DjBZjoZCiZ%o+Cdpl(!S(Y|7F-q2DEjMAWTN=T~4)f{vxf z>tD`hd*C3=t5^~3IcApso~D#Mh<&ob3RbG0=T^;5!oMUv^K`F$d|PG}2f+PFBs!rH6ITQ!r~l@TwMs& zy(sncR_~uuIh;L6L}hPvOSwu~Mf1TxM?Fc4kaI9KbUIb(oIO-a8uYBL_A=lvgyB28 z7yz-d5%BaXWE$3Mr&SOV#m9`#jhR&At~z5-fjW}axw`izHcR^S1+ps!m6n zT8eYXyaP_k0LSZCE)Huyu?I%^384xhHx4dYfUjxkiLsfG2t3f=KCRFi3~A^vZkKt( z=So7KnhG)($$^p?>+3UEFpf=sV(1NrDk>NtHI(m?J(4L_g#zkUAJF#c5 zd}}LH)JD17>G{Q&i)?I3V4W1Oj_y#~?*3(V(w1Gji-0k1S#p}Y5nKO7vMs_j`6vaP zL`kE?dT8ACyXiuEzs=aqH%b-hlXyJ-)xC3Q^D+4vk$k>eo5U9*2bHN01-7zO|Fj{) zu+6sq?7{ai`lC(V+Us=lMJ#W2tf@Mg_4RPV`pIk|?weFQ}zHygR&lP@LtWaQ6E#X(U< z#5wY%-pL)@?$=2=#Tq((Q})`SDuEj=Kv*vN8iaBk^;nX9JFU6bnTRE(SGH)i53@6py}$Gl=7}{^E`ja;dI7l8AcJIXQtxR*phIy0 zP0HfdC7D16?mC=9cUm|5evB`~SSCrI4<}w~3bD!2awIsz_wki(;S}d_?;rmJxIHD# z->i0vwzx=V<>lmu49_e{noMA6OrC97loh-G5rxO|T|MSC#_?ULPu7^h1aW|oNZyux z5fIkc`yKQ717cAG+|hi1yXl+x5XO_7G0Plxx*82qIY-<#41Q^dr0p|Yt)mL&PNy0~ zXMQeKy&iD9vb+?Y`}I_505e2;JrijLx3peWUU)HSt7lf~+HE`7U6qd6B5INyR$#_3ts)a!{E zzOJ7Z`H>yPs!E0f#anc8*KSXuRKpDC0J7w_F9?A$&*E~{kXQ*E8K+vbr$KX$*ASQF z@A?ih`||O-(<(>jdAAfyiHz$S_b~ zB;vQ1z_$-!CZNn9R5xy>&&AvMUEg;@&RwuXa225nJ`bubZ-G-QJhbSQW2rvG^d#+Y zrGGEI8e!^ezGtX7@YJMm)QH?2{o=JXnAP`byPuq zB~1_R#PE&haUO~p3Y6HBj*}TvI6J;0ZR6XSeM5$X>79jo!AMyt6GH{ovy>+{PtzB(^5HZyo28l_dk_5V0}Ww zCQ@5nc!HY2(8mVj1%FM6-E+vlen+BI^ToSazjWrdd*%pEI?mu)Zz~3EFWE35%cRAH zCCZHx9_zfOtkG`!_KHnNS&HNY4Ba|LX+W9W7+bsw1R^+D|7S*Z?Vv(ph?8r=6&Nm| zDiSa;yY$&hJ5=}>ER&F@4f2J6z8FeLqNqpG_pd-{6J9rovO018<5;}%`eNxKOiJC? zA^Or@II4=16fp4#%?fC4e(r*-6QAb0|B+SIA)FGDb_o0a#Xq%=wbBq99JbNI0&vZn zI@e=gNRHmIPrY@`y%Je=K4Gd!5zJaxGgP~$(N z<=YmT)+rT)j)(G`;)ksXRCC)u1PfozOKf}{0$MDCI~}o&Nnh{WdwXJO@CxgpIMnE~ z1kDy$c|M_J9j)uPt@kPHP_RBJvGNKlIl$3F22y| zzIR&!e1}JH#Yt7bM!gDpE`t(@&px_6G338#TTlymfHf-~sOgUsBlCoB`rXG6la^7@ zSgBS~VcLY<$K;VB3xQFmUay_O7sW$JfqIe&IkntQIj4{bHG9DA5LUYv!;D0TP(!?7F*m8Y&U)VT9BqDmS25g^U_P zVUL{RTf#KiCa=5S&wuyR9_18%%XiMmW$6WWbAr~xZcu{aF4o;*nXYpYMu9#Ep#R3K zdmI0k%%YvHNM@ZMhIAJ_%WgoRu}c z#Sg@1h<%sMwAtu`k7Y8w4Iv&_Y768~2?lk8ZqXmR0)2~1m{<$GlYGAG(}}3Tk%O5| zoAtHt3NP9DFV?l+mK2LKQ|L!c*W=MqM~+3~*aIynDJD!(^dSS?q_BZuh6+39%cl9g z(3zcl0cx0(PH1BZ!t8V@XVMDof)qkOG?+;8H4fgEHx&D6%e8`$WyC@M7#loL^>r?| zIBMiVq3NH&@DEO3@6ZXSo(LW3;tL5;&<4&g^)Q$h=T{cInZ|*?0KR9~-V5=ex>wQ1 zSdS{xXU%c<2I6z3ZjpG2&48ezRmnaOE-M(&)YLPs%i;k` z(a689AnvSrH8swIjwGNM3+r^wzBdIZ5z4CKf(cu>M?hS^PI$=GV>gzNPQ1?60hY#t zuLSpiD-8PX(SK?iG7(U>d^%F!3v`IFx_QWdXvgoAxb_HYkHRcx9~}KRMh)oM-wT*~ zv1@t!^c6ny&A} zaN!*6+!*!#qc38po#dB0)bafZtTyM;ypOm3b{)_Nn8(4#2hkKJb{;*qi+D(I#134t z*S}riK@QNu(~>XnP}`KDm~Ac)aZjv5gg5jfXear4lNj%-q0Cq`)rDSP_?F9rLBFqv zR;+z!fo7*AJ7DKGW|aBQK+}93Bf^(!mYgTakROz@k91o6xfE@J5ZYDN9(fjy5!Xh` zxc{eC;{SiG7#+N)zGhAij7~v>QN_&vp`ZW3s~o}`qRO^lTzObbOP3+R^ABVjS%t79 z=$nc%^cko%cTagi-ep?K%}Q7)i~sV5x^1>w6#rN#Mwe?TI?KO1Nbi1mLU>-osV&qK zOoKR43?Vy4J(B^_lxbE!dOSbS)jaV=9wMw<*dY|^Z}p(k=<$LufI@Mw*CngQn}o2k zYV;*86_d$aVX;W`k=rINavRhBJB(IsPc6Ej9;s{&=qwaS+UG9hz76*d)HG_xQ@%Q@ z3k~dZ+84q)FrOoyOnGoo{(P6TN34=l$zdnH-Q9HOCKidar(P-a9aviAX-tOc?^NsL zGHuQ{n^E<)B%WrSvi^SfST?aEvOz(sLIQ=)oFsN8wkf)*#LE#TS4C<;%TLjL&DEk~ zWmS1*^A=Z;K(@MU@H7xt8o;{HvK7wKRS&35JJUrMI+T`CA1HtU(VI9SL(L5yHK;HF zDvZ0Kurs~WVVLL;y;hr3j!8ImS>^r%wSKr;)AcUJnncz(Y5RR%H+dE4BlHK9CqIvOrBx( z8kt>{zGp3^FgpR28q~CY1o!t(NYYE7RIdXxEu(2pBqSBIfkXN!%0B<|{`K*@UCs;O zejg#E5C_qoC+rSdN1XQZf`ou7)FJDkKbD3_+&Te;j6?Z!JP=sM*<>XS0o)nf2pIM< zPdtj*DFZx`3bCTN8zL2-;^&<452`~A*s(P2tQrzg8A=Tfbl975-Tvt{*O*Q?jx#Ps z-iu+-=Ic+eZ->PC@6vN$j#+)i)`ls8GTMc~xmg@yKFN6~`W|xSISSDumciYEEggCJ zAzmSeJ2wuXF`A$p!=^$F>-$>S1jO~Ot)td`=RsellDM2oL`8jxjV9&Q{65~`a7-am z7oh(6QwrJsq$Gi_tb;W2NzjU&RTkIB^OCIfP4IA=+_R~|!?_vLT%QiIcQ1I&VkKi5WadgH_!_Bz&5{I4OvgjEVg?xfHgR z)5oavm{3Veqa=Oc9%O+s`jnDJf+WJTqKhF(6PZ64a^BD_-<@x;GZaj|wJt_Ra$(?h z8)I)Jtqn;3oWD+aPOs#DA=t*t7J3K+Z;Jxlnvhz4gg+W!#gSDsu98H9cTV^`mS~IHQDX#W?|nE zf=lnV(bK7VMJShNFk=aw28*4uMWsLAwEFILA!kq@-7o__dQrQ+ZQYSUQkwyayqaU-Vl)Tl6kXK+T>HcBMf#{GoR%J!15R zAUUeK$abN=ia(F4-Xf$uflo$dB2z2W5pO+!2rWjDCN2GU9f?P|SnZ@>kc@h59M5!^ z^3PJdA-J5sS9Hq!vM3;VXn6oH^Jg0(bplCj1|6$VfSoD4@5Ns$7uSS1Jv5dogIO4HH#g3c zbe12BAu$?9ptg{o3Pm3H!xBe=SLCI&_$8c$Jh&s)5|R1w1&%^{ej4~n82gY6Zx#IO ze_qiU0u=6*Q7QP8$MB0hDnV~4f#PjNKT}=OQL?l&_J4PJDD$&5%9K>0hJ&{vq{e5& zbkLgLSF6iJ0xkwp%`f_@aexz8#8(#+AFW@uyL3#;mXhWGI1)Kr8g!D~9#UE|TrTtA&hluBWb}UcF;NblArI9|=#@tH(RN|)4_3u2agTqC@g=Ryf+WlVqv8Z##Dt7w)clvXz zxgHBVAqptrO^CV(z}~K8V}u9eMj%(Eopn9RP#^(A3_wq6&dvsvA`ffE4Kwu^W@=&! zL0h@H$Y&6FlPvlmjKq)6eVH)x5?p#qN2?YbViKhfWMS?KKbzek1z;1c=ixx{EM|PH z6%iP*h~~+!@#5f*O9Ow1@Z&a!K^uz$xE)aQg*GbkWgKycq?~lp7Y`-;>uueh9fpy$ z#XxwEOHZt*c)QFP60g;F51Nr+!+EA@Ubn4wT`x}8kAFO;MQTOK)Ua=UAon4uHs&F} z#v3-b?`6Pfi6(ogcg+ZcTe5!l2P%2(BO z0HuoAf#hlU)$@LT&S+7KB^XHl1m`B4*rhc%1Oe>IU2~?L{$sh4qv3B4_~WXGCBVGU z?8B^h@fL?H^>|F!tRRuJvN}0QDlDjUea|E=91hCCXk|y5@R1A(z12(}>mSx~RyUHa@TR$9_#(DD#0P`*rf3XvOJL<@=*94TMwUrbmj84?}2QICb z%Za3iICCus>n(QV+kLlNF6w(1WJZDyKmQ1Sy-)Ke`#zzi+|iWF9L5g>LF@#{r>n+Y zFJYKlu~47e9nJ8WpgE3qL^8{lvX(Y6<&XwF7ogI-Sd2)gW@}~5?3OEa8<1*ayZq<1 z0B8WC#@aHh$c!IPNaN*%tvt8LIIDOu*;fiRn=WnzPF>#Bc3p1h24pw1E<7G~eGJN zGT=fp$RR%e*q2^iHMKSy=8(g423iT4NYycdo<)h~%M>v@UR*mz?5*mI4bvv(LDr1s zM6LVqy>@^a)K(LVo22hOL;C~UmKEirT+%6Jix?c#)ZwzEa{9NIm2ALH=Jx=AVOn)x zX>+1iJxl~{XGkGEdwrXpG&hz?CN~{q9o-@0coEycQsXuVk4Ca|8eQh7Xf-EU(V8EO zS*c=Ok^BrlYw`Vf5;tV=w1wY?A(0T#i*&70kB^?_pMv9I8avghyCJ2E!b5RWG#8G7 z&P}*B=ON*tInx{|PEYt7xcU_!K?>iLlTK%`%JAKl80o#%xP_8rJ@5BU5j-aL zE=KSBp`pq#>&cjG&EFI*-gobXY(KdM&yA5#jU@8Sf7$Jlfp^86V1JzFRBB2*k%MGV z3^OR|89ltF-VzZ!SC)2$Nw1@US?ZLT+O~nQWXIO#^Zg>rRU-#_Ma(aBPZDzYvAuP|HD7WrkZZeaCOReP3$|uG5 z{G!1vgW%gPdw${%KRcRj(IOz%idqkOb(;j`E1h#%9pw|VT3^OE-`k%YvS{g#W zv*RQ(0WYZ^=SQ}`b3)pr#p)M2!Y=PT9dX~~PWK7tOV|;0*I>Pfz2fD`C&e)MecK<{Z`rG(7kwK* zfkH0%Cv6Q590~&)7kc$3*8x05T`CD1+qOQII3E^jY>17xh+R*|CFV zv%Cgb-m@b9gdfOImYeBRv{zj&82(84jh-f1EX_7^#M7PP+vX%uFrSy3e?@d`YIcua z2Pbj90IuvHfXt6oq^VP9Yb~4?2h4N-3jwOLh9eyG~-a3!tcCUQ`x{`H7~#|aNT%CzbIuI9W~8c~!`aP#vjtP$rC zBYr_eqxxu;rH&K?8jK>QafbJeImz%{;%%;c3W7m$%y!l~Cv+TEH?Lba`<`^Gh~7t7 zb{3@kV21H%NG<;bE$K%?J0$cnjk38;d#5*KoD9S9yvb;5aAtOx+pu1$C(`9&+7a`odJg|cHO zUKn=Hb(ZnG2Pb*nA_N6~dbA)QOmMz4Ifo`)L)4axXqy!}VQaZs_IXt>i@ThKzWArH zIER?AiqG}}F_N;s?TS!K+(J>s2eG+uk=cZeGcw04Zp81bOPLIf6mogbg1+Aj08 z>LOdJp3>55rr#5aXH@oN)2Gy(f_%o=7_dKMZ7IQ`b100l<1UTER^!h*F!M2N>2Uf) zQ}D8?jj33n6|I>zb&Cw`%)>YmWUj!3d7+-D%y|EA!fRarum}3@8LNLQc%} zE0Uu$oUjv#Au!J<%P^eIjNOUC9kqq*EA22q{geV052pm1gZWNTA>yrdIpUn|oxt9{ zmU0#Bx82f;8D<^17OIFsszPK-7lk`3J&85=*Hf;AEP+lCSn>_q%Hb$n)VbY6Sqn=K z=%fQkod8))pPt>fHcJS}>&jtwUx|~+jLuhYd(Z>`ssP}wNc>MtBwTU7N)5P1s(~L%Odbo^e-9-4Cl8trc%dNCpv{#EJRQm}yF8 z#hPV-d~jOJIHV`4UL0nW0IDMydWb69B(Z z#}h3zxRNq#(XUaa8I90O2(KgLOTJtyvT^`+cRSQP#UM9+$H_gK?}*41^za7o#B0q> zpH(_oieYB}(qMxd83|7Ia%jBVkFjK);u+T^OD|RQp7y1&};nph|Yx& z_Fw$UPk-YyuS*eRmO;gAt*RVkP^<_M$`a2S^AAIEfG-`${hfdW`~WF=z%br3q4=X=-qv+SVG(>|m2yxnpDjKZ ziIEu4)h-A6_@RPP1340w+t8Du5Pe2^`rcItPlqDzt?S@cKw~J0%!H%Y zi5ZIHSx)#S>|&b>6yVqM1fYg{UVl3P??N_cm2-!o zfM^5vWH^5qlcPj+v)PSH%$cfWK6}z@?7(7nWP{pf-#M^cn%L00p1AHDD$1vbUQ{JU z!h(z*xBLUFrd{NlMq=!sW>{At6uIEVNBYtDql)-piJ82UYPG=v{99N_I%7GtCRT(> z%qWR4iW_E9TvBz8{8+zJzr#}2OR%Y19pas$u}MYI^zS0f&1EdFIY(Y1G2j+DB!Bld z^y3?g%Zt|f62^=s0HOZnFzchJxjCt3{Hg*g(?Bmw+ko<}71m%*i!Ky~L$qJv5Knn2 zQLKQyJI-S7k9$PMi!8k|3fX{qZZq3yY6cO&IG@+1NmQ1auJ6v;QtPhp-m>)wu*JGt z^H1K{BOha&^$3{H41~K>OG#ySy7@$z;U{5}`Q-@N{TDX|1i3Gg`c#h5%bm%k&Z3#( zu$*7?`n3)mqPY*(J5-fKI&_{7K5Ekja75Oh0k$t*bo6@{cnKJ$?jt#D|CjlX4-uc? z0^pVE$m2@;-7xF?c@%a+g!wk>GT5o<8DAYdgq zg=;feusduAo?Dm!XKd;pbg5Oh;H!xbMm9Ole~IwW`?D`@-%}QO$#*1wEyLD(l(sbI?1A_ppE5 z1JiF#6H7C0Dt?_a*=-Fd1?J^RzFd%7wDOmNq6j&Us~#)c3zwylMMM9bUKfRO@EL%b zlHfo>ma3ZyT4RB*sE-^QZQH_E{f(qDqdf=nFjcxgZ9}=mBbQJH z!BE4SDCLSMmSS!i?o$!24*piR@1D(xlz-GDVjO@IYXbK=9QUJgRdT@^C-xuxh`>0E zAtKh4e7i`rV}ah5Akqi?6kqfA9Q59XbOtS7UzA3-A^qyt^KqOP`>}Fm?AEjwF0A5M zbegfgjNsM}05Q>}cGIi<#RnVY$iS`w#kg`g6QKr2<8QHfD|)uLqrlDojvgkpY=-0j zzM#J^JDN_uvKEXGdWdJ48WGzb|Ar6aeKs}pUp^d9b<-DbD2WmTbq;B>z*Epuu*gRm zYDl6>p$}~eQki~W#yZ;GFO%73NCLGMR}rT?Q91*so!PWHdZ>a?t=kjMf3h=V#}mk)N7kwR;71le0w#IKr3|F%|lxx9hHU)FB%?9gC3#0Z8R&WbzF zO{-SBop1Y^PIUcDmc`WEc{C#fVj^z)x_?hwud1CraHsDRcX{uYNUyv9_V`_c%PRjv z40B!*%1K7!Uoz7(TsjdL%*GFgJ}8PYP8gRQ{+LP19N^&Cngd(LrWMw4!!@2nIo9e; z_C~pQ5Zm)XIq&OmcYjHOsJ-pv*%M!=KZgdn(ZhVi`~Y1UNb~KZb(gxS5|bt#D%mx; z$Abn0)?m0Ovz^%oaiUKVBvoRnG1s2=WSg(p#(adwFvhI%*jVZI{ZQ1F0`|QN^C$BR zm$BC#mp+vQ^wO)>wcp`yb2QP>-(+cj=u>^LeHWwx-%__4P8X&xddNBB@?<--rvGII zN_cqkw{FH`_;H(E*oRbcRDm?}2Tkf;D>vV6v7uU(8)koRe_WjL*XtkmR<{yRl#>$& z?T*&(Kh(VuU!qwU{SQ}f*%nvRb?xFV!Ge2$1b1yL5ZqlG4ekVK+zG)UxVyW%TX1)G zXx#nn>)yxnW&eV%)vH!j%{j(6t5t+#9sT1SRhX3l?;nf%XG$7M;cK&N?c?!I;Sb9( zk&weGW36-gbgVgqln{}en(-A>+?uf_Rx%&p2{dVJCjLZnN+m$jDwzKFkhr^e$2QWq zIF;#QP2yxvqjut7ZHxglO(DVQ6yC~euVQO|iOUVKq!WSe zr74G$6n$x$1+r2-xdPyMEb@CoqiiO;@K#0@iK2Q}7E2H6$>3LRG8Ir8OG;Zp7(GG6 zbAx+TD+Wz|)=gUZ9$B%n>gblQ+ zq2Z9utvZ*KZGSEJ@M?xY;1f4ZQH)EwbV|P4Wi-RXHw(1K=z< zy#~Fye@oeM74X$x88bbu zLwwOV@H^f=OYtIXKNW3+;Ld?mgoZde%D)tR&?^$Plo5CCD#2A9;)p0RP89m)*^v`^ zZ(=yS=V)OL-W|JBgpOD&I^gw21fMM+XyS>v5f5#XxN(jqBYH4mK%t;?BSuzPW%>Kl zQcJxlq8aGu+w z5~*x|-|1bWMm|vt-h@OGGn=o{)L<`eTmdJCV!gDNRzEKJ{MS zSU8`rU@c;eGi&nKf*TJ#z-*dKDZ?O$I4i!;k-Gg@gCsQX3IHNWPd0DP`8o1t0~)6s zSoR;friI5TwO)ZLhRG%gg#8<`U4xfS>TF1Db~v?*&ICoySy-5X^J!v8c}T@rkUwjB zvg}AQ#`GIAV;VyC?cW`rFQktsdz8aZ^1OMLe_?Z)JsP=U4vo1yD%D7eCiAJ%FN(2;sJw4N%}WXv5ZOcZpZs(wl*tzT$$ENz^FL zM3?wPov4_-S3c>i=RXLh?F7GDF7OMSn0qpj#%PYmE%eE*o>9n&^1lMJ%3AJx>^5=X zro9NnBv5qm#$w?{g#v`}_gP5VkFO6OBreSo0|Lpikum08Gh()u^N~7t^sO(NEJc81 zPCaoWDjPfCB@_WB%R>CDnhlg|ixZI+KEd@FD*Qc41%iXI(hS6hm$<5DgVFq&%AmVD zOG&%)>U)1&wCUG8Agq4kWfPff*ozhkg^l@!0-*iG6wxS!Z8+PHq+~+&u@bOUhJ!dl zB!GI{z-Yvei|aREz-t2T_1hY_nV9+eem4e8M;FUL_YwAH#x%!I_+6^C1j{vVg95&u zES)Z?5+N%71KmAR2bA`y-kL5#ncju<)tTCPy@zKd<_wXmXq4 zAg-4GP&Yx3E~^pE_l7IyrXq$L$qy|YAC!gkNB<~vXX1Uzq(gI)954;kC*f&0&U*{) zO8PO6#K3Cj7p$m($1DW5lovOFskM*}1W-%6=I)0aMR+irZvI|Q3gUIXWex6#L<(dw z353A16nGlh(nWKDf|p+wXyxk%a2yK-H|ocv=;!+$Hs@1Qg5ux<)zl008{7>-i8$Jb zMjb+jC851^HZaP8)4f%X8S8(%g$U|*zf)zgmizd;=fR>9?{wXJ_mNm&$26@XNi(+1 z#_~)3E$p$AbfoVMF|&mSaXP*fpwB1Vbm+w(A`jt$jm&TIa(uFT%_tq|e77I0tfoS6 zVQj0Pz9Hq6+Pal?0*b#`enruyaX8dGTdh5u;8Y{5*MQ+88v==dT*&IR{Jvn=zJ6d! zJ!E}TIJS>J56AZud;9l9Hum8xZ7D7-1GxWFyCWMzCspzGn%Uj?%TgxKA3%7zXd;H;ZQY_9 z)2QWZ!>xgFcA9UqclC$uD%|wfg>u3cL<2MfHX#tJ{pMvs1lIsO#h&JxC1j)LZ)|sI zd^|Yt28vKlbiXatt(5N%2ez+N(&KJ)}wcJE-s5jM1dn5`&uQMf#$hnoPZVxQ1#8dAw)pPKhfgfdW zTBTZn0pjfuk4Cj-sJQu_Fn3UCTSG23BBUl!o^k9}-TJh}i_Un*;ePW_fkK99zIVQ+ z)EnFA-jlac&nrS%MXSdXVp4aRG(G}zYq||cp6DLxjn&BX!00yFjk7zG={jH4bl`T- zbST+?cVLhyQs^uHi8{*m5;k%%I+JGqD`7|eh#ef??%U_@PwSX<XoElg^pfV)QW(!}7ytr>&ZTG=g;%Uu+;Ps05v5;n~zvHq={eW2kD zlvR;JI@vUf4iOpjz_IRh-fvNIIHEE%9-VL?bV1bK%!Tj8oMVMs=*uRCqYXefMyjbi zw_ML^^K)f3?wa;(EI=7n^`JWgvt8-;ynG}dyt^K291r6`%`J*NiMd2D`GmM>Vss?$^8x-inTVJ#r&Sox2@2R2RG6J-WFPbi zL1iQ}$n=D;pzKL^aRQ&LY?|dY`iI8lNTez&1K z{B^GzkrC=(t5Q?^xS7Pq?bY4~r$;mO&?ovOkJW7`!+36nWo$^FInzkH_S4~j_u*vq zXh%!Fm>ZGm)P{EZq;L#qpb$?_#)C36+FeZ@mN8Pk-QBM@OR0-(! z{SuX0Vywp~jlOT}IygF|4~pJ?Eu;I}qul|^BGxp#-Bkur(^Y#r1}(x_q5PFw)R&+d zdA=M6%PS`mS3hvtJY}k-joDc{ZNW^O_l%zD_117qe{_k8AwiKg%)l1!b(*-06<%M3 zOtw}1liNZ{LY~Yy-jBx)O7Do&>F&Dp10y(XW;`WDPc1;z= zWPX1FnhBCPpu3IvuHS30LOM>y7Q9(HF+yyhEylE^)^L|4U^$f~!=5vXFdT>V#uo^O zVI9VVxO062PQ31wS@#Z5uA%l2VTV`Km4oUcsxirBk3Zs?+JBU{gl7M%pes;6DbM!@ z*lXs;uE5*<>qIz#r<}~2OYwV`Cq-K;akyG8lFy?wp4Ep6rJ)GN>bpB?vfP_L z)y>?5Gb&MHS;)&$V+@gtsOsPzro|aDy+^Tc|pMr17Sh+a&IN!PdeaOCU|EfgG13U9~x@R zrBd>I&|Tq?D37app264|oKZ;3#S&FPUABd}N=NG?lYaIA3n@zn=jACO`l>KUb~AXP z@|!5xzROr>mVw4>p=+Lin9E4&mSFI@JZ#y5M%`+MvRkX}W+L2m`u@j&YCZNw0eVR8 zj7a87kzSaaZ81(vXx$mIdz5hAo{b}OOFmYpi+4Mh^^{0v9G+!RBj-M$-Jtx=83T;q z=zvy{z~7MZV%6<9L3NYe(6S$9TPq~bh3k1vUV4a%FW?4dI;f4b1gP>VO*!^r9Mxe} zs`NQbU%DHV#C^M|n%6GqB@d|ZpsmmlJqvU68&SMg^GmV3?4<4MZv*JJOml24PaHU- zY80Xg%((`_P$pJpT0LuDgcSuM+^OHFyeov;twu(lFws&kEIrNdEVEjjeTu4CR>L+` zv+2&YAg(I34>7m%Xgjs_as(zFawIukvso#Jk2`ywi1UrVuSlCe)1x>d2H(Im@ljbd z=GYks<(%{s95wv8mK?h{mrs)r=aZ(2(k`|c&Rj#@d^Uqc zq12O!V{qcfNFGazjE&>WKj*ND^(>1Q*9DH@<4BEbACSU@tN&#xr15VDw!N?F4kbrR zxv^(bYu6w^%7XVN`zVMbi%sqJ_aVKz)+&nM8v_#8O*Rjm&hYuzIms1{AE|Ler$M?g zgs&2P7tDy^nr_e?b%V}ln?7GtC=wpxx92wlW|D^-w`FRn-R}LV3%`i|ZPIhJSoDsM zrG=Fk#_$l6F%Cz@H}l4;{K*DMCMgK~2JQ@< zSZQwotTc_M#f`-~HEi%FR;$la_^YpF@|y48)nIw(V(9c|i!(w45$ zx|Q<=qtnir4@=s-XX8YG!xLg`B%`Y!a)D9I6Pr%R+1%HI6LP{RiV}dL7IMhm$Rh}| z%1j$CsggiMpB8Wu$fy$SxA|@XwSn|Hr6uF5K8=jV%4Fo{5>nucHp0RGWXu`(P68!(hwj&L;52GO za5pGwb#~tcQCTb1_ydmyUcM7%RKjIDx^K7V3(CBNgFu@uQhEyLVP}6Bs?Fg&UZ1$1 zNBulNKkD?vAJ7u9T-3tQm$qakvJvk3=dM-5Os1O z1shB51f)g9SLWcO6ZzUnS^n|w+;UeVBHmLT*&pF}F1wQO;ahw3G`^q=WwH#+zWZG= zsIdzY0rS8AP1*Ia@IBa}hICJxQoc#4+wOefidnoRWbyhLFS)q%1Fe%@syfh%QvCX8 z+j8gMKj`0xI7bbFJXpwP#h#vi<6NpUWc_>dM{o9(&1VRy6uOSkQ>#I%B;SDLl&a-| ztt#6))?)h>usa3c7&(Jvo2PBehSz3ycMLR_(x%#9R?^)snNbnfyc2NHf#Lr1*#$HY zSPi$`3Q`?$0*!pL+oy!z=B>uyX8@_4N5U}&pj5f_Ao4N0t_8u#+|q00pJE1zN!hh$>3EaQC;Z3T0mc$cFCJH`v% z66#3I&Y4Ceou#GT?T;FyxUmVwX5p}qQDr?U{EJQ08-L0P{#qV8inJSYGC9@0Z_Kfi z;ERe~_cSKe4zEs^MYDkv3}1=F&8$+HOsRpOm*POkW|hxaO0JalEa zykA^a0+q0UO@s_UWB^U%oN^+KkW&B8?YCSlYTP*DrDq0gJi>J2;8bMTp5G16NKFG>u&xm*7ZA0bHE z4FuyLV%$`-Fq+b2@6VUXzXaOV`+0>rMGnmI+7bR1?4M@tc)`pSFGOajpjL zh5!^crVHx#Qm-JcF33ci&-^BJOEy?{NztugPB%x^7yKyE(+8^_e=-PYi(aX3{!8&^ zvXUl_x31Q3I|e~arFe9I@T{UB006n1?A8K>vxvK>`Y=tOzZc5H1vIcofr*{Vw4_mT zT^y@EnyCGIVc`p?LRVjQk>KS`_H|aXRe-nmKK|fSVuQ1eHqAWq7dJ2D7bxi_ZPy}; z7g`UrKO&P0jS8ijN{Lnuq-X2lKaN^@+>xtHa74xIIZ&&IIOt+sPYvh4UbF-CV?Q+u z6=C0$RS*!<@vQkv)*<_ROcl^}9BaB?*Z|zl*UB5;ZS8XrmY_<9Cqx2z%=Z#a6>owh z-&M^#brUzC6*bju8UR%FvGflM>VE)}&UPNaV=1Pia! zlMZvA+FBj%{XQAQd`9wHu4#QEOol0kno>m24IuWXjFwU8-ZeoEwH+yW_tFa8m=*0=l2%Seyaq9wsaY*p5I)@S=pgWS zoq&M5vB*EpeOj&YZISD^5D92d&Z?6^uSlbou}U>ve^DQfMUl*axr6SxdYl#S_I`V6 zzV?;}wO(Zo?bet+y(HcH4g*!df~lSv?RytR!h9C*?MXyM!M5?%2-VtX&?~%87m)8; zPgB^qH7EiQANXyoxjDI6!g4F*d4!F6;>-}EMpT!TA#H;DN z#%gDygt#BQX?D2}8!Mpql;1-xCNliZ`lWa*IpjUKf7OOnL&i9@VjlJkcL#wJe%!>}T8%H(WEBs*D4LwL?L37sxm#4zYm0ew zT^Yp=;R_ z^e*0;!(7gMA=wI|-;IWgZtgEP+BRq8p{U=mq%e2+ZBTkh4$_Kt0Y_D!z}yz^}b}y~Vp`xtKJeJ~<|o zM@O1$lEd04c)ehgmxNrrHY1Wws8$JtULou)L&de1kFD46SVy>x-g8 z^?CO$9@`j$@;1}++Ptz5aG7X9oS>?${6khDI^c|f$a~%Xc9l@G2RtP-k%Vd)k6QiU z@-7(WI@n@0OLC)w(0fqr{Aw*v>YH2@mDK!qaD(ONlt^jF31$Dw7x-@wDiYoGgw%@pRM&|+T%hJ6#0Mtj_tAqqRa z;RF!wPq6`0gLHpEJ!boaHKXM9VR>&-eW!2$M+m0G}fKG^8fP!l(mZsc4hHP(g+CUsg;@=Rb;5y zd%l~9oq%@ei*lcCr&2|L#qnO1?hJY=?7bplk(^pyu-Dg#MqQM@o%Off5B}_=8J|Jv z1orc(HBX5GkC=Bepj^JUi|Ag7Se-^~=Cqwu5uRWEr(nQ)0-J2z4l%y3D;BsoEC3uC z`4$u!Hk5=XIx$x>+ysKiR!KSqB9aI+je^<<3x70PlEfe`1ldm>D-tj-PWOD@ON9f! z6;_o*GT-Lx2Bb3JHBTo8ybH0h9kzXNt~6n@=SvHOjcOpN=zdT&eyMFLu&IL~qr3O` zF`T;1tW}x{R&HQ%Pj%&Sj>tgi+CWHzzE%1u+ankz)SwvQb+=_pBHNh8_r9{jce2-z zz|LXjh>)MMN6+|33dv}x{7q5=H$BKAR_=q|t(7&cAJ|175^WV@g>SjOygH=vZ>h}7 z=Ng+=Anyei9b!-e;_cinIKbb(I3(Tm9^&rRj44R1+g&H_D^)4q`03J|m91(lx(dXl zK1Mfr&$u!X?rmeOqUwsLqW?SHkuwgN(15!9djC9>S|Mg&au#M1FB|v5L*O=76xW^s zvd&p2IP9*UPrmt5MYt?O#7#7u_xk6R)$J2nTh^dN=QQC`pYFO@=B%)a>#6TQ9XMa3 zb>jLFEm>QAdI{ps&v6Imr_Rq?OpEs+490{*;aX8Bi(^MrKB-F|$t=frKQ^~VYw5aA zJsPt4e?d7zqIfhB)d}mqv>mlGdx(K|WZnh93L>H9(>0}^g z+RY)R8VvzEDa-SdH!MM-CrZ~KKfEVnU1SLjCLTa2U+s*KtLa?_XE^X})!E&~#&sH| zN;?ildV`tI%Z+kGc8G@myMm{=ON<&>AyWWPxmM}hnA}4^n#sp~qvA>>O&}m93ptZM z?p^YnJtT#2;mV8UN0*Vzqs%JI-n5e1{r0#PcBHr3lG0`;3}~T4dXoBOsU|2zI}W={ z#^E{SU?IKBUJYVvqw;FI(m+cOh+HCeyVKTFGvMGq51i3`@t#gk*_YDYqTT(h7gUmv zH$QH(9f7#MKpbzR8~Ru8UIp-N2`59d7!d6ty`t9K3$i;_*K+8^C`&b6T8(p;w~X}S zai$cBr&z(a?Qe(k44OXqF zV!69xdJA1MDuN~&*K$4~V(#T9ewyF0ao z+ntx^RzEA`4S#(JbA!R$)<%lB&-;G8v+dZ3CR9te@{gVydDt2qfcBpkpRD^Q6!C-R zI^IKR)#03p3$R(*ocVKgYT$Z?TJcYo+cvsB! z9!r+u0Ss}rvM8B5>bH^TrD1#yfLmJJ=#pbfuN&djSatNWw?eXDjMEgIcN~K7Zomb787y5w`idLdSX%XTEeH6a{^OD~lCLtmXg0gz@=s-nj%qZz-5PO1HBQ3@pwU;sYXA%mJuFQt;7y2lSz{y*)HtrX7Gy4+J|_vUhGOjXU0YU%3*G@Z|b zl2v&uobA5#n(FDeB;_y@)abzS-_DxY+8J<+P7WADIDeFaX?~VRY<_;jjXT(2qaj`J zorOQ@&oVXO~1@WUL+q5jy(Ylb0K#p<2Wm;?cWa=gLOfp z*t-lW*>n@ypO@lUk-CH`568&KIz6)$H*dcK$J>{(c6|}^l3q>PR0732jGI| zE}yq2DIKfGrtnWsGJ_tibnc0YUpsvr8k^oM&eEN;uS9_x5`cn$6H-slSaQ`H52HV| z*(XS#*xwyKQ;s2zV74V6K{~fHCgiQVeO_VkgaE+b8!NT1Y$+a!mxFD#>2zA6nOH`( zeet9j3X}$YO~h$!p%xPw)`!DQPQkJgg-CB|>Cke{$Wif2ynG0HY$#B0uSHOC zWYIw_`1IR6#5=){g3m3)p=EUd@IW?)ik&bs$#)A=Kb~=q_w`+|{j^D~ms|ykiCO}0 zxK_J(SxxJzB7XoK!6Xq(jub)hK|+RMH9UfgDE+0oG3$ zih5~$D!6Ad>2cF%NfcZ`lw>HrQjboc`6)8I*9!|A*65+)M>Atw)JwDX^#B*fa2jLt zqZiv1o$@txHhw3EKF%OO^#Xi_~B$!P700+>hB#)K~hbX(E{*ig4>j0jf?gJ?kB{ zpwV->>Zjg-^V&nBdoTv$r-itmogxbR(*w?XvO*>s;$IVP##k(Ta<}~bJNBG4%D#od zMxtLsV zdBo0cu(R`@w&dk^5(EBT5f-Uvlzcwj>P3}PE-El{vY*Y6Gq5)`7Ef>JwmY+v#C!~? zeJMiBtMQ|S2x$gjx<(P7F*{;I8KgVk`D~E#pL0@a2i*Zb_{N6F!fUEVI305JFwX$j z*E2htn9%l)-=Gm2SN0F;t@Sp*sMlnT83*t+5P4`Pf@*GhpR4#6<(-QGmC#7b=ffz5 zZ)I3-T@^g>>k~F2MvF@Wa?Rq2=4M+qPUlFy>>miE23+Eyj~%EnbUqOG5kn9H`Qp*i z*mcg^NpLXAhUd{tqnWV?N}FT$u`B+rhW<>4ZB^CFJzgei1#fhQr|1ohX`LfB_ms>x z_gpkbW>V^bL*5%M;n81D7j<{l9~TE%_J&~?HN!~WF9kVr{Il(>T&1 z_II(vN%5&HGOn!Da3kVuZ?$}}3Op*ocjBxeVyvyk0XeLOw)eK~d1-$FwHy|Q9@Q#Z zNkjjIWYPA&ZQ0?#mH$f(@!5!n0hn_;+;Y%s=Y2qQ#LQDq<5bUig(^Jlu=JEZqRSXl z_VwvTgb%y!uG6S-sbMsOOhN3b+(xk)j5QsC=@=AzMBTfV#=Y8F>3a~X8VDl&qL)9~*2o{9VD3ZjW?U-?l%%I_1!&T`cxRiMCcqKY%Nq&g%l z=K?wr77yt`wt7^Kr}v3RGM;V%LMI zsVVoEFCl-Uj(peDJ7UJh8rKMgOVJ(8+cDd??l}OGKi!?mJ=24Ok&b_xHwXZ^1$J|C zSuC-b2cNLwNQ?XyYkW)s0Um}uPXT6!)emScLs}5;nQcZnzM0fE8Fcho$-bq-oAG56 z$)OYzR1vvFE(USZZYT;&`e)Wn6E1653Ppa51jD3lL?VtxqGvji4;xGddD8Xj1AJi7 zi`#PbXY7o;oKVSv3ohx*pxB(WNX;9Pn+$b9t2z^$O4HL0Fw(se&rSbVu@0V$2}lF~ zDPtRPcRMGf6qz@Mxu7FksFeawiP0x~vQwP>C9k1jEp~QyzeFKm!CV;1@V-{fm~KXP z<*4oe|SAuH!shOYH zv1W<%bEek!6i&E%@$Asg4HT@;kM)l#v1i*CdoC4s9GkGAe`=13P@nTM(+gs>Yy$N+ zH+EHbV^J46gF2G#_)hd#-s%-<4`IN|4tUoc6C@N2o7T58V5N}VJjd)i=;98n@>UX@ z^8m+K$DoSfe9R>Locphc{7?FSZ|=}Sn}+H+nNMn)ou)ohK*W0JJ)mQwS5~jwp|79MkJAB zImuDOd^33rysJa~lBcaq1}WpF4oXM6OZvC$kmEka##J+z4e`yG%J|!#JqUkE5k#{B!w)d}fi}%b>a2boU&3u% zd)}mx(VM2MhWABO{Q$9zQyGJ0lx4Xh(L$kT`!nF&4Qr&NS|z*YzafLUNnBt*O_L7< zZnY%JgW|Zq?ud$-hZU@ci)`lGda5~7_$QiMB?6oAoyF)zy7y~@q1;21$Y@vjH6b?J&`(bvuO$uzxA{%Vryzm;0 znM@x0Ruklx#>rq7)Q9~Fub=jane&eJ*%Z)L*f0U0$&mzr@ma|> zzjVF{*zNI~Jh!e5~{Obr;iGnhv-jjt240nJJp~ zu(hB=TIeKj!xD{?lLurS2r>^R@nc#nG~szDGk?zcwf>dBsxLDwoEqu?=g#KCB2|)z zgok*`sgrICn{DzLCPFT-$*Y#}leRsT4c22l=9vML%>CN8sMNXi(@SEQBc~)kUp2+z zq3|6{DD`NDeQmKb>Y3TqPfpV$B=Bl+r^)d|v~BsHh_WwuC)$TsAMJD6B6l6Qqk4pL zbJV~SwD`Bvfn+R|ra0~DM#9J5jtJL=xgtMtPXK!@dZdSQsyUZ#TNo~fx?-SI3 z53Je7^^-VJ@8BCnh6%oGe*lAlhwV5S)Zsj=3irHzTOx4^45BE%`5RUS`6Z#C*1^rY ztrG*0vK~o0<)H`1Bp@s>u{6j9;ul5pc=#UCtQMKL_^k)5h3U^R25{T{Mjd~mEKmE6oI=EE)a6}Te zKJcaSxKaE`jji}?nG!iA<+--b$2*7ApWFWw_Sh9FLTW{fD}dm+cNbQd>A)>rv}?OJ z6oklUdvN=NUJ{ZpHJ5BlJtK^IAm2cltVzrZ{a%isSa7R4X;z$B8JaZa0@c4xdH+{t zrg*jwIq_0h1oQ5$Mt28GOB)o6^SxlT)0=c<-u2IX4M2W*3CGQrE_w8Ft7mfwaSlMc z2_Eu24Wp}c`lsuPNi>Wjx`EizwB7BH3af^4pI#ykZIzfo^0OGlXw` z_3t&qX(0%+{KU zgvP~q2;zpbu$O?>zEf)s^3oHc5lv0jW3#P?xWsSi>w{Z*VD|G(gOXU1k# zvXDq-V8nc|TJ&{-4?LHr(N#M1J+#XD6n4X2cLgUIW$9~`1#l(-Wq2V~h8HsNZ3Qey zl%gT=HPz^(%v*@@ilK>W!PhmmBr1S`gCp(ZjqXPiLU*?prXir&jzd zW2fIg<4Gtk4?nCbm}U7ezZ?opyY(d9Al9hm;p+R8mnYZhu0>aYitEcXV$2JE*8y@epm(qmWZ|MeN z|LKpwD7_%CxHRQ(p_s)k!Dq04ik^dVn=c*_y2*WFSDbo{&*_hJs>v$$b zh`eJ??^`Z-Vu{%3=z;{AyC)T)a`G56rm3%LkR*qbEQ|VSec}8x+hj%A&4m&t2uUkH zyTHg(@SyEq*wte3I1w)pEwu8wBd+91Fm={Lyd!}SHBCHk*Gu1DFmbnvIhkw9b`ZH;O&AV$VG^J18D@dtB2>3ae`3YMa554l ziFz#(LY~Pg8?Vc}FrV!95d4isSc_v;CLdV#PBAXqaOsH zM(%VR;qm4dBa*rM)c*fg_L%6Qb$Z-@AgbYnl5a=-jk@WQ(7_1n2;~ZXp z!C8=q*0Do@9nE?Z#pq%EdMlYa>V)NKH^3lIny0NbChC0oHP;2c@scA4TxGwBya-5nn3|^{LaZ>-*)KGZK(Y zlj~bfW@M@gzh}kpav<9ZX%==?c70Xiijc$d#69VRTQ(^6?Un@DKlJ=6kw57`qzUrF zee@Qb^`HO0nWX{?lJ9fNiXy82Mu)Fb@%pa~FsXSORh`{1=%|EP`Q!T#onmAp4!Z`@ z$@a~G02Tu)21Oaq7wI$Y-gjj3nTyylkBbuI8YqLr8CzQgT7Wk8EhDi$TaSpdZmEVx z`S7#CQ1#GgVgF{iP7zaeUU31U8>|s#=!q9bt=XeE*9Oe;4&DeP9Q(&shdL5g0kYb) z7cLY0-yf45xt!I_9A44P5OtuaUI1q^90lt>8lt~0fVaD223q}AYxat1%TmSX_P(=e zj;6v?^Xu{tSSB_3b?}FBBfC^fc4@s-4F0p@d7V~)WCj-ILw^-0gxq&0v6*_@ zmd4X|v@?WeP0ExOij{^G>essjQ8kN}MpFl8!zRAf^s2Y&`7TsfiMksh?jail!_*Y- zOZ?6<<%(M}Q>#J`rp&_(NshYLW4-8hR5cTvTPSOI`8cd!$UD8|3X(h{()t}*bDah+XUlk5dR~j zIoU6a+D7iG;w{!T`&c%sBJ`~c5<|dJHErjxvJldf(rZkYiX<&SXn;u? zfHE(rT3PxSC5CQL0HSNLh5%N^4z^?Jpq~$!MC3ej(nJ5lcFaCyeD|ehDD1aapTW0X zHThwe9mV4dW4YuK2WQu4S29NcNHGBDKWWKW;;l+{C4dk&;?DAy6#U5Ks7A_n_@pn2 zx$LaZ|8M)VBxz>loyrvjx>o1^T46BQ66D>%T42dfsSm_?2D6=s-QY$ z*2n)gLvY@GCGdi-BEvz#at`J0(Zw!&uR* z82Y`WIrQ?iTonhW9f8dMLhSoLw$1D+A3Rm;_p+b~AcJ$|{@AO}t$+8x0$$xgy32Jn zK$~4q)Ir4ZANG}m8S4-m9TTV4Lt@|OI*`z=Ge@5R5MXXt7ypZg1~VhF0)m8Vq#_dh z4+-b~&;K*T@#eO7-V0~TZPY7hHeN}Q(QK+>B4ZbJCtc3V7g31aegn3r^TTUZPVd(J z;`;f>P*%f!6Q^^MH`9vXOOdapOF+{>KkUj+_8?Rtkfu!mk2k#S@G=^|BsM9)NV4P< znW2ErLElxl80&`h0t8WofHxrv=M0vez-#{~4)`QRsnW|r%oSRjl$s*xA@ZWKpzKBN z_B{wJuV=ev#u9lFBv43nX1oEYM)pKiF+AQyi{b|*ed;DNO5B?yy!0b7X*v*s?C`0Y znrUf+{-}YlObC1WO<;yDgSMG-%M%w9~wLtC@Oj0 z5sBZCK8Npx)6Wnic$P0$t=6Qx8C5NE!duQl*+^56#FoSH0!Z(&BE8`sFbGyS=Ni+M zAx=!E1(`gx|2S-iBo=HGEdTMEa3+&bTTE?}9M<#K0t96nh+dTt1Guw0P+@wM#w&JP=FCY+it#iew7%RW9x@OrA z+CuI_Vwfl>kXi#{{&MZ`CF7R=gn!dPp9}Q85VI`T+R0fV`Z`4Z!!BW;5^VQU(@*E; zdlL=d!71Hn+Wy2|W-OP!NUz3cB7F*1Ildg!B`MkqvfYQ9ylz4$pU{C>^0MFWcNbP1 z!aEfsS{jH}8ba|@^?No{ydGYfYX%3z9bd`sQX&+^Cug;7?T5Am%>eO(bk=NliWXRy zaD*Zt7%unGGSq?$glp!%2@3}$kdB#C_JCiz-OIhpk$Y#~Wb?oZGQ~(MZEi*E9z9NH z>=HOdS@O^ZnF&AljpxqxWz7fESpb^z9r~EWxbx}|>hp*HGggn^lP?rN<;ok;5;OOg z6hYcVAM#S);l9%H{iW-w%SP;K$-4G6es886Qx}74@S2a|C=ZPI84Ryij#K?MR?FF3 zDSkEgq4VX(GXy%jbtoNHw0BQjJ=mPF4-(tYcc;7w;VD@B!{}#qFXpI!vg)Oi@8OaK z!HF%c7tHR)cMxhi5;HoDt!mE!Yt!mkmi%-c8Us-@-IcUVwmZU*fE9RdB#;C^qwxQL zg~$I*foOJqaQxb&$R+x1Zb1^;{HQdZS`@vW-iYPdq`~W}!B`gQ>f@p->**^$EA5QN zP>R5ooekn>SvX&az5T)Ry%IjKpS@kUHAp<@^mpd=$v7~=chxv1>ga_k;zt2Z#-#wf z9ruQ4hgDwa%<-_Axu0k`U8zIGAJI`Csy(kXZkr^CUQelgTM1{+{atE=@w-*s(EhE6 zdi!sVM^cM*6+T)8JQf4KUH%1REo%Tgg_q^eRn@b-QXs6xp;wd-znXl+jSfFvJJl&L z%4jp&nhr+LDDn#nw4GY>W}-rP*FxG;=``(g#u(a?AuZqizD#K-eykhnxpaET;8H~k zCL|GXFh74#=(yDjVaUROwzKx+gS?Cr(519{5*YC8&Oqn2*K2vA0h8b_=vkZdOW`t8Fq1`SCDv8-fH zyFH$=({Kj~aub(P>aVG=(YwZ_KOx$%tE4~a&wuUXayoe_RoJSnT8Vd^S4!YePuSgs z*^hvY(fs4_1O)ysoS?casxK(Q? z6{>Jc-;XmZO4?ZSZQ+qqE|VX=2ke>C#3q(CR5rtSQWbY|3bS-@+)L2>g4&y2tHz^~ zB*b375A5-<8F|A{QP$u88n_t8N>k6@c-GiMP{qO(Fcq5N`f1CMUP^U6d&5@=UW_s~9Q-VA@x94Bvedohq4bQH-v?JLH=ZW^ze3|*B z>P%3upp0g5Q3NOp6pVEJ?;%KPB*V9)wkE1pnv5pS{PeNdvF5^<#JAw2mq6~-3PKw} zIjZ6k5;i=QL7cX6b3RJ{Of?OoXAGQ*L}Ro(bUkd@&UtWXYrXzb_Un@?ostF9*^PxM zJC37Vy6L7Zst+YA2Hr!;NO?CtL@AB7j5E6cJCAoooL$RC#46d#f9b}4*ABXfOF+)Q zGJ@4~(0=umQAYM(Gl+_G2C?nwKHc1l200m>*?xog)q2|#gasTSinoEq8R!3N?5g9U zh`RRDA`*gxbgIAt3X;--beBskNOvwQu%wjK(v2Y9AT6y(hrrUH^h$U4yQuH?{`<{u z{+zjU=G^B#&pBuIo{ffPm3`qejKnC5!Wi5zJ`HSHbbdQ|2i4>=bF_QsB}7MFqlau+{^&)qZSFUQ zNJWDo5&qS!eA+MO@W>*?NDft@{F&`{nfwIeJ{+H~UMG+&6>cc|^FG7L00}O})?UpsE~j;md*N)(7X}RU&3jSnWeArL3@Dp0AGP)5Zp)AjV_=k9d$@zcnHZYt4&uRl6cG+ykuCNtGC zMZB?XGYjOc5a8weX0Ef!VIWabr7dy6JMwrqXH#Yp->3r9BtJcwx*!kdFmYtC8Kq3A z=jc`|R@B1+!}ND02m0`_BETmo@7D>Y=(S%FnRPWfMfb^l;cQJJZsD;b?R6A5jL#NI zv=^9u+kB>1P!KF;8qg;BE{yBj->nRfut7ldGX`oe8*SaAs$K(>4TI%f{jn^jLxwAF zuI;r#7in1pVOC<%Yh_(xBvO+`=hl282U2|pN>ZG8J199mwJsUGRwqlyzX(jriSVJs>uvBB=At^1XX z^EA|FFO1s2ImG|BV}jv=J8>5leI(`UYWJyn7mO3t+m+C{|_{O8C|bcnZk*(~ou511E3KK3)^BzLVHXOs`b6_<^y|NVb2Si=;C} zKko?~b1vwnkdKc*E=s`o` z=IP~CL_AYl$B}gBnFgBzweq_**+GB&^ooIjN5gjsDn^OkrRi+r>nmU>AY-mFl`c zw*2l#IgNL|cI4SzQ&@3Y)O0?LQW32ny?D2CTMy;jQ`3Udwyi2N7M}f1!Q_t1UFa^? zd&rTPl9pUXrrCoRNHk|~U08x;NH-b9(Zo6Ni?Gbnu4q$6jIF06mze?c?YzIo@K|1y z>t=b`6P`e3zdCCUOD!MDP;iRr?#EN+w#)r*10t)px0$200C-TUtSst+^qL9F(Kd8#(QP88VmW`&#+iZZllgVqh zyaT1C&&mx0!yk#GjpT)jD6L6zEg~)peud|S99@ zL*p1cXl~^kEo*Y~O3&n1Pzi31-@^U2Z5`Gjo!Fl66gM*gpud@}gxn>u7bA{({FFuz zEe$Zm+c{T6D?Iz{NX)wZ-kR z(yuYHO#QyrDnL0ujlQ~7WZZ3h@Fhfn2!KW;n+%Wwx8IAi_>|ju{s8xZ+4<4a9o<=7 zk;H=f+9acmJ)ECNP48AG-@ij6Jc)4V0N$K z$cQ)FldunnT4B4I^G>m9LxpukpS&@RleTlZkH#uideD|5=o|0*Q7)B^jjusfG!wAr89x$!es@teRo#e8B%v=NL5B< zjR1@_7yU`~xdC1o&QY+%^zs$H(y&2JJmKybL!#sCEAmc;x*j*DoJ}g`YB(mMI`E{; zDugE3pdI=C@C@NB=aAOkPrB_qkbfOUbA!}%Ce9F&wlR@v!dgTVa+G))bu5BaF~c7V z>I>~oRVj@p+b9WArw5?lozX2QOhZnT*!syQ6rs(> z%zS*Ro^g{udF%9!v^ABpsuT`x0|F-_D-%^-8%J&L>Y5sSnzV;BwCBUKg)>jT@OhK7 z2Za{jUTHA}k=tyPN8Jk5W`Bfx#H%S5`<_8O6)PDt^=w_(>cz@Caz?0<88>?WJDA5Z7)7m$!KbU!G<|RCv2c ze8o=jvlwL75LXlVOvZC#E^UxY&N5M+e9^sTyj)u~sU*oK!a;Hib_=-aM@SpkM!#?D zhuilCAqDCOW@ag%hrf)!^yCihs{Tkm_GV{$z~Y`!Gwwu^_rxpC_=<@|B_mqaYs3|= zp0;>J|EHu~=5Ir=_9;m*axSaw#n0*M?T%RKr##>(^uO;ShMgaM^`xVNGT~ z_PzN}Q=4@BzXy=JgCAi}K;PScE@|$>e^jcL-xPM$CQk0ktTLbE_o`B5<59ldQy!9> zZF&<6BwQUqtW8Ok`97!Lt&zEYIY4YbN3m;`G1E5`S#R!$FTFKV&yxaFCd0bHGFkG4 z@=wi0b@LgdO#tyq6E7gky`-d3T_#N0A6YAeDojM@eX+44g{;S@WZOID9us@|;$`Hu;yKJ|oZ1Zg7>humWklH`Yq4M!>Zz!YqUE^OrK?Z(zfo zKWk$bZ5m=pI*r)-e1yAJcB5MjD9`!4d|W$|=eD4$wI;@$((}}oW!{QAjPB7e_pdnE)rsJi>tFSEsA18+C0afqv0)qYhWCb| zX^R6eQRf?ev=7Y`*M>8mxr$`xAu$MhfvnZr(CfOUK2MFRV2%CO?__bd8|qsghY@sN zW#3TeDRJtiCS1eNt+b0r*{fhbUF-OB>{Mp$>QklmwSOSnvSK0Kz+HwHYX_-6W`F7N zr*$~HiAv8B8Y)#s?5aV0vp~(^ab$VUsdai+$!{JQbiSEJ<5l8sp4!~fGXN*nP#E32 zGfdoQjKr@eoaF!vRa(JlXT2RVKc<%`oQgKC7&I^oPactnK-7d1pjK6@p7)o)@{Ls&d=Qrsxf!(zRrk)&T&<57oEhy1Exm>iUw=t+;@s(GI z?MLp{Z;f}eNPcL%Z{oYA#u?hMx;%acpFb`y|02{B${=*dLI8h#MnDX|ltaQ6l&#v5 zjKeCduAMJ_ip1=O%U~R_+`HxFQsP328^BSM{v||eZo9WeB)-hJ5l=^Rw>^*B*Noc` z;o4LqM$(V$F!+)|P&#yl$aE8vn$X~{c|VCaneGQe1c)H5K_5$hfK z;0iA@5B7iAN^VWf#+0yPTGV5tXtOs>=yd$oI+m|&Q) z<&IZ3Z6iC4MYt5$k#V8NDZe$M#)>8SO9pQnvFYf%EYVI zqSelZEjT7`ydmXDz)l_sp$+Q)aXI8pN2V;a*_a2bB^9B zMX%*gnh1uaz+=b8*5y1%oR{R_JxvgkPxMn40j1g9XAiw24$3MCMaC!Adu!~h+}O}h zJSZ_)!VG58PH3QIb}roBra2tQqobtEs8z;QlnbM zc6BN})v++V`Krn7Z}%4jM?`)-qu-{h=Fd+V$#>`oDG)<;``h|2?bjne#&^)sh}7*#T=Of%8~zbq>y$zNf|-mJ(u)oB1)5WxJv*^)YX_%g7aMx~w!xZiuFkWx9?>S8Q2v?M6c z+47;gd9pTNHr zN$NfejI8*65C32Zjp#!hN+!}5xYf#X$Iq!^k&~9tEJSHWY;b^i{#1B(aoLTq4VIk@ zmq3(n1E{y8AaVBT{Z^D#u)!@De=D8?Eg_0IV>~L{T?uKDU6@P5u{zaB@JR)zDI4Dz z)}MF-;159oRsjcz>HVzTF2b=qfOr#omk)p72bO6#+sPcU;QSjNty}rs`|F!L+$0k; zB~>l)pDxo&z+;xTLUEv1l?~#!K6~^!Un{$o74ldFCj~Gc{F^}`T=A84%!FQJ$Y!Uu z5NQ)P^V{}v4GrI-`z_+{4f+;kQoz_>z+n zaai1bYHGcr<9Ybh>;1ntxfnHQUaI5!6S;*!`tcKq9>q`nW3Tw%8{#B63eNs6ue`uo zwDcNiqT$kLnhYFtM3fQ7KnX(dFHpZm7xn#FvD0X8^s-7aP2#V#@u8#*xNGQ=whWR# zujKpk5y-qbX4dl=B!Z+NuXEF$)wb}cRwted>^gBGzvm$zyKz>YpMw099IjI-4RDK%q?hBR{7n(x+ljeNyaG0Su2x0%U6 zm`_S&4w#2!3(s{2wO$>z2^@@+)n_Q^0akjdX0k%Zae>GX0o(4s-2nj(ny5D@0T(U)$(BIg(24@h>*E8@U}TWqFjr8pI_jC=pkL*=7gfV~MWS_o zF7e99IZT5bp?@)pDGkb3rJJ!mezGgP2!44=qBaPwZ!i(eV291dN+?wev6Z}IGQgs~ z(|XT0=G{&E_I6xgXSjC}yr{IOrDwB0T$YrC;UwGqI6f*HSh@*jyA~L){Pvfu0QYj! zP*`sar8=?@T`10{W`!}8;4__2%&vfqyr(`4VK`_=@(!Ovf))V^tK z(oVIQlA|Uah=k{8+G)FwM-SuOGn1xfuVh1o9{r;iiSTmj_fAIAq<`s|ni&w2gz-@u z@%zFL(iH%er(T707WO;aUJgqR;Z%1q3{j;3}+PwvapTCsW# z9E4XmWx8_|Eh#+9-4P~nahm{%DYF9w{zd@+{|6{$=?5IeEFcg|UI9|S2X*y~@tLJp zS$SX^EGxZvC;JyD20YHC)BDBXW!3Y0x@Vhh|1GTiBKgAI-{#$t{ zj8U)vVl3h*`u-vAu3|uN=U#th$q)a#( Date: Mon, 1 Apr 2024 09:22:35 +0200 Subject: [PATCH 088/120] Bugfix/fix duplicated tags in position detail dialog (#3224) * Fix duplicated tags * Update changelog --- CHANGELOG.md | 4 ++++ apps/api/src/app/portfolio/portfolio.service.ts | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a0b55060..7da7e2f93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improved the usability of the date range support by specific years (`2023`, `2022`, `2021`, etc.) in the assistant (experimental) - Introduced a factory for the portfolio calculations to support different algorithms in future +### Fixed + +- Fixed the duplicated tags in the position detail dialog + ## 2.69.0 - 2024-03-30 ### Added diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 566ad4049..0bdafec48 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -719,8 +719,6 @@ export class PortfolioService { { dataSource: aDataSource, symbol: aSymbol } ]); - tags = uniqBy(tags, 'id'); - const portfolioCalculator = this.calculatorFactory.createCalculator({ activities: orders.filter((order) => { tags = tags.concat(order.tags); @@ -731,6 +729,8 @@ export class PortfolioService { currency: userCurrency }); + tags = uniqBy(tags, 'id'); + const portfolioStart = portfolioCalculator.getStartDate(); const transactionPoints = portfolioCalculator.getTransactionPoints(); From d7b579e3e86bf9bf7973b60322c821a69bd15559 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 1 Apr 2024 10:42:15 +0200 Subject: [PATCH 089/120] Feature/refactor getAnnualizedPerformancePercent to portfolio calculator (#3226) * Move getAnnualizedPerformancePercent() to portfolio calculator --- .../calculator/portfolio-calculator.ts | 19 +---- .../twr/portfolio-calculator.spec.ts | 68 +--------------- .../app/portfolio/portfolio.service.spec.ts | 78 +++++++++++++++++++ .../src/app/portfolio/portfolio.service.ts | 56 ++++++------- 4 files changed, 111 insertions(+), 110 deletions(-) create mode 100644 apps/api/src/app/portfolio/portfolio.service.spec.ts diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index c3edc86d9..48fcaf343 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -29,7 +29,7 @@ import { max, subDays } from 'date-fns'; -import { isNumber, last, uniq } from 'lodash'; +import { last, uniq } from 'lodash'; export abstract class PortfolioCalculator { protected static readonly ENABLE_LOGGING = false; @@ -80,23 +80,6 @@ export abstract class PortfolioCalculator { positions: TimelinePosition[] ): CurrentPositions; - public getAnnualizedPerformancePercent({ - daysInMarket, - netPerformancePercent - }: { - daysInMarket: number; - netPerformancePercent: Big; - }): Big { - if (isNumber(daysInMarket) && daysInMarket > 0) { - const exponent = new Big(365).div(daysInMarket).toNumber(); - return new Big( - Math.pow(netPerformancePercent.plus(1).toNumber(), exponent) - ).minus(1); - } - - return new Big(0); - } - public async getChartData({ end = new Date(Date.now()), start, diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.spec.ts index b68f4358d..365593846 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.spec.ts @@ -1,12 +1,7 @@ -import { - PerformanceCalculationType, - PortfolioCalculatorFactory -} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; +import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; -import { Big } from 'big.js'; - describe('PortfolioCalculator', () => { let currentRateService: CurrentRateService; let exchangeRateDataService: ExchangeRateDataService; @@ -28,64 +23,5 @@ describe('PortfolioCalculator', () => { ); }); - describe('annualized performance percentage', () => { - it('Get annualized performance', async () => { - const portfolioCalculator = factory.createCalculator({ - activities: [], - calculationType: PerformanceCalculationType.TWR, - currency: 'CHF' - }); - - expect( - portfolioCalculator - .getAnnualizedPerformancePercent({ - daysInMarket: NaN, // differenceInDays of date-fns returns NaN for the same day - netPerformancePercent: new Big(0) - }) - .toNumber() - ).toEqual(0); - - expect( - portfolioCalculator - .getAnnualizedPerformancePercent({ - daysInMarket: 0, - netPerformancePercent: new Big(0) - }) - .toNumber() - ).toEqual(0); - - /** - * Source: https://www.readyratios.com/reference/analysis/annualized_rate.html - */ - expect( - portfolioCalculator - .getAnnualizedPerformancePercent({ - daysInMarket: 65, // < 1 year - netPerformancePercent: new Big(0.1025) - }) - .toNumber() - ).toBeCloseTo(0.729705); - - expect( - portfolioCalculator - .getAnnualizedPerformancePercent({ - daysInMarket: 365, // 1 year - netPerformancePercent: new Big(0.05) - }) - .toNumber() - ).toBeCloseTo(0.05); - - /** - * Source: https://www.investopedia.com/terms/a/annualized-total-return.asp#annualized-return-formula-and-calculation - */ - expect( - portfolioCalculator - .getAnnualizedPerformancePercent({ - daysInMarket: 575, // > 1 year - netPerformancePercent: new Big(0.2374) - }) - .toNumber() - ).toBeCloseTo(0.145); - }); - }); + test.skip('Skip empty test', () => 1); }); diff --git a/apps/api/src/app/portfolio/portfolio.service.spec.ts b/apps/api/src/app/portfolio/portfolio.service.spec.ts new file mode 100644 index 000000000..7654b7df3 --- /dev/null +++ b/apps/api/src/app/portfolio/portfolio.service.spec.ts @@ -0,0 +1,78 @@ +import { Big } from 'big.js'; + +import { PortfolioService } from './portfolio.service'; + +describe('PortfolioService', () => { + let portfolioService: PortfolioService; + + beforeAll(async () => { + portfolioService = new PortfolioService( + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ); + }); + + describe('annualized performance percentage', () => { + it('Get annualized performance', async () => { + expect( + portfolioService + .getAnnualizedPerformancePercent({ + daysInMarket: NaN, // differenceInDays of date-fns returns NaN for the same day + netPerformancePercent: new Big(0) + }) + .toNumber() + ).toEqual(0); + + expect( + portfolioService + .getAnnualizedPerformancePercent({ + daysInMarket: 0, + netPerformancePercent: new Big(0) + }) + .toNumber() + ).toEqual(0); + + /** + * Source: https://www.readyratios.com/reference/analysis/annualized_rate.html + */ + expect( + portfolioService + .getAnnualizedPerformancePercent({ + daysInMarket: 65, // < 1 year + netPerformancePercent: new Big(0.1025) + }) + .toNumber() + ).toBeCloseTo(0.729705); + + expect( + portfolioService + .getAnnualizedPerformancePercent({ + daysInMarket: 365, // 1 year + netPerformancePercent: new Big(0.05) + }) + .toNumber() + ).toBeCloseTo(0.05); + + /** + * Source: https://www.investopedia.com/terms/a/annualized-total-return.asp#annualized-return-formula-and-calculation + */ + expect( + portfolioService + .getAnnualizedPerformancePercent({ + daysInMarket: 575, // > 1 year + netPerformancePercent: new Big(0.2374) + }) + .toNumber() + ).toBeCloseTo(0.145); + }); + }); +}); diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 0bdafec48..17a1ea4a0 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -78,7 +78,7 @@ import { parseISO, set } from 'date-fns'; -import { isEmpty, last, uniq, uniqBy } from 'lodash'; +import { isEmpty, isNumber, last, uniq, uniqBy } from 'lodash'; import { PortfolioCalculator } from './calculator/portfolio-calculator'; import { @@ -217,6 +217,24 @@ export class PortfolioService { }; } + public getAnnualizedPerformancePercent({ + daysInMarket, + netPerformancePercent + }: { + daysInMarket: number; + netPerformancePercent: Big; + }): Big { + if (isNumber(daysInMarket) && daysInMarket > 0) { + const exponent = new Big(365).div(daysInMarket).toNumber(); + + return new Big( + Math.pow(netPerformancePercent.plus(1).toNumber(), exponent) + ).minus(1); + } + + return new Big(0); + } + public async getDividends({ activities, groupBy @@ -1769,34 +1787,20 @@ export class PortfolioService { const daysInMarket = differenceInDays(new Date(), firstOrderDate); - const annualizedPerformancePercent = this.calculatorFactory - .createCalculator({ - activities: [], - calculationType: PerformanceCalculationType.TWR, - currency: userCurrency - }) - .getAnnualizedPerformancePercent({ + const annualizedPerformancePercent = this.getAnnualizedPerformancePercent({ + daysInMarket, + netPerformancePercent: new Big( + performanceInformation.performance.currentNetPerformancePercent + ) + })?.toNumber(); + + const annualizedPerformancePercentWithCurrencyEffect = + this.getAnnualizedPerformancePercent({ daysInMarket, netPerformancePercent: new Big( - performanceInformation.performance.currentNetPerformancePercent + performanceInformation.performance.currentNetPerformancePercentWithCurrencyEffect ) - }) - ?.toNumber(); - - const annualizedPerformancePercentWithCurrencyEffect = - this.calculatorFactory - .createCalculator({ - activities: [], - calculationType: PerformanceCalculationType.TWR, - currency: userCurrency - }) - .getAnnualizedPerformancePercent({ - daysInMarket, - netPerformancePercent: new Big( - performanceInformation.performance.currentNetPerformancePercentWithCurrencyEffect - ) - }) - ?.toNumber(); + })?.toNumber(); return { ...performanceInformation.performance, From efdc9b387fa89310e135a315654a7ec8fabb0d2b Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 1 Apr 2024 13:24:00 +0200 Subject: [PATCH 090/120] Eliminate ghostfolio-style.scss (#3228) --- apps/client/src/app/app.component.scss | 2 -- .../app/components/access-table/access-table.component.scss | 2 -- .../components/accounts-table/accounts-table.component.scss | 2 -- apps/client/src/app/components/admin-jobs/admin-jobs.scss | 2 -- .../admin-market-data-detail.component.scss | 2 -- .../app/components/admin-market-data/admin-market-data.scss | 2 -- .../src/app/components/admin-overview/admin-overview.scss | 2 -- .../components/admin-platform/admin-platform.component.scss | 2 -- .../components/admin-settings/admin-settings.component.scss | 2 -- .../src/app/components/admin-tag/admin-tag.component.scss | 2 -- apps/client/src/app/components/admin-users/admin-users.scss | 2 -- apps/client/src/app/components/header/header.component.scss | 2 -- .../src/app/components/home-holdings/home-holdings.scss | 2 -- apps/client/src/app/components/home-market/home-market.scss | 2 -- .../src/app/components/home-overview/home-overview.scss | 2 -- .../src/app/components/home-summary/home-summary.scss | 2 -- .../src/app/components/positions/positions.component.scss | 6 ++---- apps/client/src/app/pages/about/about-page.scss | 2 -- apps/client/src/app/pages/admin/admin-page.scss | 2 -- apps/client/src/app/pages/home/home-page.scss | 2 -- apps/client/src/app/pages/landing/landing-page.scss | 2 -- apps/client/src/app/pages/portfolio/portfolio-page.scss | 2 -- .../src/app/pages/user-account/user-account-page.scss | 2 -- apps/client/src/app/pages/zen/zen-page.scss | 2 -- apps/client/src/styles/ghostfolio-style.scss | 4 ---- apps/client/src/styles/theme.scss | 3 +++ .../lib/account-balances/account-balances.component.scss | 2 -- .../lib/activities-filter/activities-filter.component.scss | 2 -- .../lib/activities-table/activities-table.component.scss | 2 -- .../ui/src/lib/holdings-table/holdings-table.component.scss | 2 -- 30 files changed, 5 insertions(+), 62 deletions(-) delete mode 100644 apps/client/src/styles/ghostfolio-style.scss diff --git a/apps/client/src/app/app.component.scss b/apps/client/src/app/app.component.scss index 21d33e3c9..a23e94fbb 100644 --- a/apps/client/src/app/app.component.scss +++ b/apps/client/src/app/app.component.scss @@ -1,5 +1,3 @@ -@import 'apps/client/src/styles/ghostfolio-style'; - :host { display: block; min-height: 100svh; diff --git a/apps/client/src/app/components/access-table/access-table.component.scss b/apps/client/src/app/components/access-table/access-table.component.scss index f506edfc6..22a5d6732 100644 --- a/apps/client/src/app/components/access-table/access-table.component.scss +++ b/apps/client/src/app/components/access-table/access-table.component.scss @@ -1,5 +1,3 @@ -@import 'apps/client/src/styles/ghostfolio-style'; - :host { display: block; diff --git a/apps/client/src/app/components/accounts-table/accounts-table.component.scss b/apps/client/src/app/components/accounts-table/accounts-table.component.scss index 39e455dca..e934483db 100644 --- a/apps/client/src/app/components/accounts-table/accounts-table.component.scss +++ b/apps/client/src/app/components/accounts-table/accounts-table.component.scss @@ -1,5 +1,3 @@ -@import 'apps/client/src/styles/ghostfolio-style'; - :host { display: block; diff --git a/apps/client/src/app/components/admin-jobs/admin-jobs.scss b/apps/client/src/app/components/admin-jobs/admin-jobs.scss index b5b58f67e..5d4e87f30 100644 --- a/apps/client/src/app/components/admin-jobs/admin-jobs.scss +++ b/apps/client/src/app/components/admin-jobs/admin-jobs.scss @@ -1,5 +1,3 @@ -@import 'apps/client/src/styles/ghostfolio-style'; - :host { display: block; } diff --git a/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.scss b/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.scss index 8121fc119..a03533589 100644 --- a/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.scss +++ b/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.scss @@ -1,5 +1,3 @@ -@import 'apps/client/src/styles/ghostfolio-style'; - :host { display: block; font-size: 0.9rem; diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.scss b/apps/client/src/app/components/admin-market-data/admin-market-data.scss index b5b58f67e..5d4e87f30 100644 --- a/apps/client/src/app/components/admin-market-data/admin-market-data.scss +++ b/apps/client/src/app/components/admin-market-data/admin-market-data.scss @@ -1,5 +1,3 @@ -@import 'apps/client/src/styles/ghostfolio-style'; - :host { display: block; } diff --git a/apps/client/src/app/components/admin-overview/admin-overview.scss b/apps/client/src/app/components/admin-overview/admin-overview.scss index 25209ac97..a4ae1edd2 100644 --- a/apps/client/src/app/components/admin-overview/admin-overview.scss +++ b/apps/client/src/app/components/admin-overview/admin-overview.scss @@ -1,5 +1,3 @@ -@import 'apps/client/src/styles/ghostfolio-style'; - :host { display: block; diff --git a/apps/client/src/app/components/admin-platform/admin-platform.component.scss b/apps/client/src/app/components/admin-platform/admin-platform.component.scss index b5b58f67e..5d4e87f30 100644 --- a/apps/client/src/app/components/admin-platform/admin-platform.component.scss +++ b/apps/client/src/app/components/admin-platform/admin-platform.component.scss @@ -1,5 +1,3 @@ -@import 'apps/client/src/styles/ghostfolio-style'; - :host { display: block; } diff --git a/apps/client/src/app/components/admin-settings/admin-settings.component.scss b/apps/client/src/app/components/admin-settings/admin-settings.component.scss index b5b58f67e..5d4e87f30 100644 --- a/apps/client/src/app/components/admin-settings/admin-settings.component.scss +++ b/apps/client/src/app/components/admin-settings/admin-settings.component.scss @@ -1,5 +1,3 @@ -@import 'apps/client/src/styles/ghostfolio-style'; - :host { display: block; } diff --git a/apps/client/src/app/components/admin-tag/admin-tag.component.scss b/apps/client/src/app/components/admin-tag/admin-tag.component.scss index b5b58f67e..5d4e87f30 100644 --- a/apps/client/src/app/components/admin-tag/admin-tag.component.scss +++ b/apps/client/src/app/components/admin-tag/admin-tag.component.scss @@ -1,5 +1,3 @@ -@import 'apps/client/src/styles/ghostfolio-style'; - :host { display: block; } diff --git a/apps/client/src/app/components/admin-users/admin-users.scss b/apps/client/src/app/components/admin-users/admin-users.scss index f06a9d825..e4990bf59 100644 --- a/apps/client/src/app/components/admin-users/admin-users.scss +++ b/apps/client/src/app/components/admin-users/admin-users.scss @@ -1,5 +1,3 @@ -@import 'apps/client/src/styles/ghostfolio-style'; - :host { display: block; diff --git a/apps/client/src/app/components/header/header.component.scss b/apps/client/src/app/components/header/header.component.scss index 0c6557ddd..04d634d3b 100644 --- a/apps/client/src/app/components/header/header.component.scss +++ b/apps/client/src/app/components/header/header.component.scss @@ -1,5 +1,3 @@ -@import 'apps/client/src/styles/ghostfolio-style'; - :host { display: block; z-index: 999; diff --git a/apps/client/src/app/components/home-holdings/home-holdings.scss b/apps/client/src/app/components/home-holdings/home-holdings.scss index b5b58f67e..5d4e87f30 100644 --- a/apps/client/src/app/components/home-holdings/home-holdings.scss +++ b/apps/client/src/app/components/home-holdings/home-holdings.scss @@ -1,5 +1,3 @@ -@import 'apps/client/src/styles/ghostfolio-style'; - :host { display: block; } diff --git a/apps/client/src/app/components/home-market/home-market.scss b/apps/client/src/app/components/home-market/home-market.scss index f9e5e6275..5b523160d 100644 --- a/apps/client/src/app/components/home-market/home-market.scss +++ b/apps/client/src/app/components/home-market/home-market.scss @@ -1,5 +1,3 @@ -@import 'apps/client/src/styles/ghostfolio-style'; - :host { display: block; diff --git a/apps/client/src/app/components/home-overview/home-overview.scss b/apps/client/src/app/components/home-overview/home-overview.scss index 9f8a1ce49..3a692b28d 100644 --- a/apps/client/src/app/components/home-overview/home-overview.scss +++ b/apps/client/src/app/components/home-overview/home-overview.scss @@ -1,5 +1,3 @@ -@import 'apps/client/src/styles/ghostfolio-style'; - :host { display: block; height: 100%; diff --git a/apps/client/src/app/components/home-summary/home-summary.scss b/apps/client/src/app/components/home-summary/home-summary.scss index b5b58f67e..5d4e87f30 100644 --- a/apps/client/src/app/components/home-summary/home-summary.scss +++ b/apps/client/src/app/components/home-summary/home-summary.scss @@ -1,5 +1,3 @@ -@import 'apps/client/src/styles/ghostfolio-style'; - :host { display: block; } diff --git a/apps/client/src/app/components/positions/positions.component.scss b/apps/client/src/app/components/positions/positions.component.scss index 90eff65ea..d3e8995a1 100644 --- a/apps/client/src/app/components/positions/positions.component.scss +++ b/apps/client/src/app/components/positions/positions.component.scss @@ -1,11 +1,9 @@ -@import 'apps/client/src/styles/ghostfolio-style'; - :host { display: block; gf-position { &:nth-child(even) { - background-color: rgba(0, 0, 0, $alpha-hover); + background-color: rgba(0, 0, 0, var(--gf-theme-alpha-hover)); } } } @@ -13,7 +11,7 @@ :host-context(.is-dark-theme) { gf-position { &:nth-child(even) { - background-color: rgba(255, 255, 255, $alpha-hover); + background-color: rgba(255, 255, 255, var(--gf-theme-alpha-hover)); } } } diff --git a/apps/client/src/app/pages/about/about-page.scss b/apps/client/src/app/pages/about/about-page.scss index 6a0b74854..e87d9a05b 100644 --- a/apps/client/src/app/pages/about/about-page.scss +++ b/apps/client/src/app/pages/about/about-page.scss @@ -1,5 +1,3 @@ -@import 'apps/client/src/styles/ghostfolio-style'; - :host { color: rgb(var(--dark-primary-text)); } diff --git a/apps/client/src/app/pages/admin/admin-page.scss b/apps/client/src/app/pages/admin/admin-page.scss index 6a0b74854..e87d9a05b 100644 --- a/apps/client/src/app/pages/admin/admin-page.scss +++ b/apps/client/src/app/pages/admin/admin-page.scss @@ -1,5 +1,3 @@ -@import 'apps/client/src/styles/ghostfolio-style'; - :host { color: rgb(var(--dark-primary-text)); } diff --git a/apps/client/src/app/pages/home/home-page.scss b/apps/client/src/app/pages/home/home-page.scss index 6a0b74854..e87d9a05b 100644 --- a/apps/client/src/app/pages/home/home-page.scss +++ b/apps/client/src/app/pages/home/home-page.scss @@ -1,5 +1,3 @@ -@import 'apps/client/src/styles/ghostfolio-style'; - :host { color: rgb(var(--dark-primary-text)); } diff --git a/apps/client/src/app/pages/landing/landing-page.scss b/apps/client/src/app/pages/landing/landing-page.scss index 6a8dd8ec5..0b8736819 100644 --- a/apps/client/src/app/pages/landing/landing-page.scss +++ b/apps/client/src/app/pages/landing/landing-page.scss @@ -1,5 +1,3 @@ -@import 'apps/client/src/styles/ghostfolio-style'; - :host { display: block; diff --git a/apps/client/src/app/pages/portfolio/portfolio-page.scss b/apps/client/src/app/pages/portfolio/portfolio-page.scss index 6a0b74854..e87d9a05b 100644 --- a/apps/client/src/app/pages/portfolio/portfolio-page.scss +++ b/apps/client/src/app/pages/portfolio/portfolio-page.scss @@ -1,5 +1,3 @@ -@import 'apps/client/src/styles/ghostfolio-style'; - :host { color: rgb(var(--dark-primary-text)); } diff --git a/apps/client/src/app/pages/user-account/user-account-page.scss b/apps/client/src/app/pages/user-account/user-account-page.scss index 6a0b74854..e87d9a05b 100644 --- a/apps/client/src/app/pages/user-account/user-account-page.scss +++ b/apps/client/src/app/pages/user-account/user-account-page.scss @@ -1,5 +1,3 @@ -@import 'apps/client/src/styles/ghostfolio-style'; - :host { color: rgb(var(--dark-primary-text)); } diff --git a/apps/client/src/app/pages/zen/zen-page.scss b/apps/client/src/app/pages/zen/zen-page.scss index 6a0b74854..e87d9a05b 100644 --- a/apps/client/src/app/pages/zen/zen-page.scss +++ b/apps/client/src/app/pages/zen/zen-page.scss @@ -1,5 +1,3 @@ -@import 'apps/client/src/styles/ghostfolio-style'; - :host { color: rgb(var(--dark-primary-text)); } diff --git a/apps/client/src/styles/ghostfolio-style.scss b/apps/client/src/styles/ghostfolio-style.scss deleted file mode 100644 index 103f4cf14..000000000 --- a/apps/client/src/styles/ghostfolio-style.scss +++ /dev/null @@ -1,4 +0,0 @@ -$mat-css-dark-theme-selector: '.is-dark-theme'; - -$alpha-disabled-text: 0.38; -$alpha-hover: 0.04; diff --git a/apps/client/src/styles/theme.scss b/apps/client/src/styles/theme.scss index 8114402d5..cc9b164e6 100644 --- a/apps/client/src/styles/theme.scss +++ b/apps/client/src/styles/theme.scss @@ -3,6 +3,8 @@ $dark-primary-text: rgba(black, 0.87); $light-primary-text: white; +$mat-css-dark-theme-selector: '.is-dark-theme'; + $gf-primary: ( 50: var(--gf-theme-primary-50), 100: var(--gf-theme-primary-100), @@ -106,6 +108,7 @@ $gf-theme-dark: mat.define-dark-theme( } :root { + --gf-theme-alpha-hover: 0.04; --gf-theme-primary-500: #36cfcc; --gf-theme-primary-500-rgb: 0, 187, 255; --gf-theme-secondary-500: #3686cf; diff --git a/libs/ui/src/lib/account-balances/account-balances.component.scss b/libs/ui/src/lib/account-balances/account-balances.component.scss index b5b58f67e..5d4e87f30 100644 --- a/libs/ui/src/lib/account-balances/account-balances.component.scss +++ b/libs/ui/src/lib/account-balances/account-balances.component.scss @@ -1,5 +1,3 @@ -@import 'apps/client/src/styles/ghostfolio-style'; - :host { display: block; } diff --git a/libs/ui/src/lib/activities-filter/activities-filter.component.scss b/libs/ui/src/lib/activities-filter/activities-filter.component.scss index 7d0649bfc..07964fdfa 100644 --- a/libs/ui/src/lib/activities-filter/activities-filter.component.scss +++ b/libs/ui/src/lib/activities-filter/activities-filter.component.scss @@ -1,5 +1,3 @@ -@import 'apps/client/src/styles/ghostfolio-style'; - :host { display: block; diff --git a/libs/ui/src/lib/activities-table/activities-table.component.scss b/libs/ui/src/lib/activities-table/activities-table.component.scss index 003303f95..bb5e11691 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.scss +++ b/libs/ui/src/lib/activities-table/activities-table.component.scss @@ -1,5 +1,3 @@ -@import 'apps/client/src/styles/ghostfolio-style'; - :host { display: block; diff --git a/libs/ui/src/lib/holdings-table/holdings-table.component.scss b/libs/ui/src/lib/holdings-table/holdings-table.component.scss index a33b78aff..8e321bcf1 100644 --- a/libs/ui/src/lib/holdings-table/holdings-table.component.scss +++ b/libs/ui/src/lib/holdings-table/holdings-table.component.scss @@ -1,5 +1,3 @@ -@import 'apps/client/src/styles/ghostfolio-style'; - :host { display: block; From 6f3cce1c5f2cb5bb59d2051f912db8e7d5bc95bb Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 2 Apr 2024 20:17:16 +0200 Subject: [PATCH 091/120] Feature/disable option to update cash balance if date is not today (#3229) * Disable option to update cash balance if date is not today * Update changelog --- CHANGELOG.md | 1 + .../create-or-update-activity-dialog.component.ts | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7da7e2f93..0e0559738 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Disabled the option to update the cash balance of an account if date is not today - Improved the usability of the date range support by specific years (`2023`, `2022`, `2021`, etc.) in the assistant (experimental) - Introduced a factory for the portfolio calculations to support different algorithms in future diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts index b628aba46..4fb8e9d81 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts @@ -275,6 +275,17 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { } ); + this.activityForm.controls['date'].valueChanges.subscribe(() => { + if (isToday(this.activityForm.controls['date'].value)) { + this.activityForm.controls['updateAccountBalance'].enable(); + } else { + this.activityForm.controls['updateAccountBalance'].disable(); + this.activityForm.controls['updateAccountBalance'].setValue(false); + } + + this.changeDetectorRef.markForCheck(); + }); + this.activityForm.controls['searchSymbol'].valueChanges.subscribe(() => { if (this.activityForm.controls['searchSymbol'].invalid) { this.data.activity.SymbolProfile = null; From ca7717f9c56a70dc5f6fb2ab1ddaa8f1707d36d5 Mon Sep 17 00:00:00 2001 From: Bastien Jeannelle <48835068+Sonlis@users.noreply.github.com> Date: Tue, 2 Apr 2024 21:26:14 +0300 Subject: [PATCH 092/120] Bugfix/Enable tini in docker compose files instead of adding it to the Dockerfile (#3232) * Enable tini in docker compose files instead of adding it to the Dockerfile * Update changelog --- CHANGELOG.md | 2 ++ Dockerfile | 7 ------- docker/docker-compose.build.yml | 1 + docker/docker-compose.yml | 1 + 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e0559738..c0446ee8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Set up the language localization for Chinese (`zh`) +- Added `init: true` to the `docker-compose` files (`docker-compose.yml` and `docker-compose.build.yml`) to avoid zombie processes - Set up _Webpack Bundle Analyzer_ ### Changed @@ -21,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed the duplicated tags in the position detail dialog +- Removed `Tini` from the docker image ## 2.69.0 - 2024-03-30 diff --git a/Dockerfile b/Dockerfile index aa578e235..f9396d0e7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -56,13 +56,6 @@ RUN apt update && apt install -y \ openssl \ && rm -rf /var/lib/apt/lists/* -# Add tini, which is an init process that handles signaling within the container -# and with the host. See https://github.com/krallin/tini -ENV TINI_VERSION v0.19.0 -ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini -RUN chmod +x /tini -ENTRYPOINT ["/tini", "--"] - COPY --from=builder /ghostfolio/dist/apps /ghostfolio/apps COPY ./docker/entrypoint.sh /ghostfolio/entrypoint.sh WORKDIR /ghostfolio/apps/api diff --git a/docker/docker-compose.build.yml b/docker/docker-compose.build.yml index 2ac90b7c1..7ad52be7d 100644 --- a/docker/docker-compose.build.yml +++ b/docker/docker-compose.build.yml @@ -2,6 +2,7 @@ version: '3.9' services: ghostfolio: build: ../ + init: true env_file: - ../.env environment: diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 007a46883..d2dbb8112 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -2,6 +2,7 @@ version: '3.9' services: ghostfolio: image: ghostfolio/ghostfolio:latest + init: true env_file: - ../.env environment: From 26b9660e110f67bbdb49975eadf4e5fdf94d56d6 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 2 Apr 2024 20:29:47 +0200 Subject: [PATCH 093/120] Release 2.70.0 (#3234) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0446ee8f..2c36c692d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## 2.70.0 - 2024-04-02 ### Added diff --git a/package.json b/package.json index 05ffd3e49..5db51af5d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "2.69.0", + "version": "2.70.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio", From 371c999fbc8a94ac6ee2b610da148d011bec457f Mon Sep 17 00:00:00 2001 From: Arshad Jamal Date: Wed, 3 Apr 2024 23:17:53 +0530 Subject: [PATCH 094/120] Feature/Add dividend yield to position detail dialog (#2636) * Add dividend yield to position detail dialog * Update changelog --------- Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> --- CHANGELOG.md | 6 +++ .../portfolio-position-detail.interface.ts | 2 + .../src/app/portfolio/portfolio.service.ts | 24 ++++++++++++ .../position-detail-dialog.component.ts | 4 ++ .../position-detail-dialog.html | 39 +++++++++++++------ 5 files changed, 63 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c36c692d..a6513785b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added + +- Added the dividend yield to the position detail dialog (experimental) + ## 2.70.0 - 2024-04-02 ### Added diff --git a/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts b/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts index 2925ca9bc..c058a0249 100644 --- a/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts @@ -12,6 +12,8 @@ export interface PortfolioPositionDetail { averagePrice: number; dataProviderInfo: DataProviderInfo; dividendInBaseCurrency: number; + dividendYieldPercent: number; + dividendYieldPercentWithCurrencyEffect: number; feeInBaseCurrency: number; firstBuyDate: string; grossPerformance: number; diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 17a1ea4a0..3b8a42d89 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -710,6 +710,8 @@ export class PortfolioService { averagePrice: undefined, dataProviderInfo: undefined, dividendInBaseCurrency: undefined, + dividendYieldPercent: undefined, + dividendYieldPercentWithCurrencyEffect: undefined, feeInBaseCurrency: undefined, firstBuyDate: undefined, grossPerformance: undefined, @@ -769,6 +771,8 @@ export class PortfolioService { firstBuyDate, marketPrice, quantity, + timeWeightedInvestment, + timeWeightedInvestmentWithCurrencyEffect, transactionCount } = position; @@ -781,6 +785,21 @@ export class PortfolioService { return Account; }); + const dividendYieldPercent = this.getAnnualizedPerformancePercent({ + daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)), + netPerformancePercent: dividendInBaseCurrency.div( + timeWeightedInvestment + ) + }); + + const dividendYieldPercentWithCurrencyEffect = + this.getAnnualizedPerformancePercent({ + daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)), + netPerformancePercent: dividendInBaseCurrency.div( + timeWeightedInvestmentWithCurrencyEffect + ) + }); + const historicalData = await this.dataProviderService.getHistorical( [{ dataSource, symbol: aSymbol }], 'day', @@ -854,6 +873,9 @@ export class PortfolioService { averagePrice: averagePrice.toNumber(), dataProviderInfo: portfolioCalculator.getDataProviderInfos()?.[0], dividendInBaseCurrency: dividendInBaseCurrency.toNumber(), + dividendYieldPercent: dividendYieldPercent.toNumber(), + dividendYieldPercentWithCurrencyEffect: + dividendYieldPercentWithCurrencyEffect.toNumber(), feeInBaseCurrency: this.exchangeRateDataService.toCurrency( fee.toNumber(), SymbolProfile.currency, @@ -930,6 +952,8 @@ export class PortfolioService { averagePrice: 0, dataProviderInfo: undefined, dividendInBaseCurrency: 0, + dividendYieldPercent: 0, + dividendYieldPercentWithCurrencyEffect: 0, feeInBaseCurrency: 0, firstBuyDate: undefined, grossPerformance: undefined, diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts index 6ada2eeb1..bb37b9ed5 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts @@ -48,6 +48,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit { public dataProviderInfo: DataProviderInfo; public dataSource: MatTableDataSource; public dividendInBaseCurrency: number; + public dividendYieldPercentWithCurrencyEffect: number; public feeInBaseCurrency: number; public firstBuyDate: string; public historicalDataItems: LineChartItem[]; @@ -95,6 +96,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit { averagePrice, dataProviderInfo, dividendInBaseCurrency, + dividendYieldPercentWithCurrencyEffect, feeInBaseCurrency, firstBuyDate, historicalData, @@ -119,6 +121,8 @@ export class PositionDetailDialog implements OnDestroy, OnInit { this.dataProviderInfo = dataProviderInfo; this.dataSource = new MatTableDataSource(orders.reverse()); this.dividendInBaseCurrency = dividendInBaseCurrency; + this.dividendYieldPercentWithCurrencyEffect = + dividendYieldPercentWithCurrencyEffect; this.feeInBaseCurrency = feeInBaseCurrency; this.firstBuyDate = firstBuyDate; this.historicalDataItems = historicalData.map( diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html index 3a8694c83..3680f5701 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -134,17 +134,29 @@ >Investment
-
- Dividend -
+ @if (dividendInBaseCurrency && user?.settings?.isExperimentalFeatures) { +
+ Dividend +
+
+ Dividend Yield +
+ }
+ @if (user?.settings?.isExperimentalFeatures) { +
+ }
Asset Class Date: Wed, 3 Apr 2024 19:24:38 +0100 Subject: [PATCH 095/120] Feature/add support to override asset (sub) class and url in admin control panel (#3218) * Add support to override asset (sub) class and url in admin control panel * Update changelog --- CHANGELOG.md | 3 +++ apps/api/src/app/admin/admin.service.ts | 23 +++++++++++-------- .../src/app/admin/update-asset-profile.dto.ts | 10 +++++++- .../symbol-profile/symbol-profile.service.ts | 6 +++-- .../asset-profile-dialog.component.ts | 9 +++++--- .../asset-profile-dialog.html | 10 ++++++-- apps/client/src/app/services/admin.service.ts | 6 +++-- 7 files changed, 47 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6513785b..f2b8c35b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added the dividend yield to the position detail dialog (experimental) +- Added support to override the asset class of an asset profile in the asset profile details dialog of the admin control +- Added support to override the asset sub class of an asset profile in the asset profile details dialog of the admin control +- Added support to override the url of an asset profile in the asset profile details dialog of the admin control ## 2.70.0 - 2024-04-02 diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index e8a2432e8..2aac43a18 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -25,6 +25,7 @@ import { MarketDataPreset } from '@ghostfolio/common/types'; import { BadRequestException, Injectable } from '@nestjs/common'; import { + AssetClass, AssetSubClass, DataSource, Prisma, @@ -332,12 +333,18 @@ export class AdminService { scraperConfiguration, sectors, symbol, - symbolMapping + symbolMapping, + url }: Prisma.SymbolProfileUpdateInput & UniqueAsset) { + const symbolProfileOverrides = { + assetClass: assetClass as AssetClass, + assetSubClass: assetSubClass as AssetSubClass, + name: name as string, + url: url as string + }; + const updatedSymbolProfile: Prisma.SymbolProfileUpdateInput & UniqueAsset = { - assetClass, - assetSubClass, comment, countries, currency, @@ -347,16 +354,12 @@ export class AdminService { symbol, symbolMapping, ...(dataSource === 'MANUAL' - ? { name } + ? { assetClass, assetSubClass, name, url } : { SymbolProfileOverrides: { upsert: { - create: { - name: name as string - }, - update: { - name: name as string - } + create: symbolProfileOverrides, + update: symbolProfileOverrides } } }) diff --git a/apps/api/src/app/admin/update-asset-profile.dto.ts b/apps/api/src/app/admin/update-asset-profile.dto.ts index 4a0457194..e3de3cab1 100644 --- a/apps/api/src/app/admin/update-asset-profile.dto.ts +++ b/apps/api/src/app/admin/update-asset-profile.dto.ts @@ -5,7 +5,8 @@ import { IsISO4217CurrencyCode, IsObject, IsOptional, - IsString + IsString, + IsUrl } from 'class-validator'; export class UpdateAssetProfileDto { @@ -46,4 +47,11 @@ export class UpdateAssetProfileDto { symbolMapping?: { [dataProvider: string]: string; }; + + @IsOptional() + @IsUrl({ + protocols: ['https'], + require_protocol: true + }) + url?: string; } diff --git a/apps/api/src/services/symbol-profile/symbol-profile.service.ts b/apps/api/src/services/symbol-profile/symbol-profile.service.ts index 656b7b7e4..915b2f716 100644 --- a/apps/api/src/services/symbol-profile/symbol-profile.service.ts +++ b/apps/api/src/services/symbol-profile/symbol-profile.service.ts @@ -98,7 +98,8 @@ export class SymbolProfileService { sectors, symbol, symbolMapping, - SymbolProfileOverrides + SymbolProfileOverrides, + url }: Prisma.SymbolProfileUpdateInput & UniqueAsset) { return this.prismaService.symbolProfile.update({ data: { @@ -111,7 +112,8 @@ export class SymbolProfileService { scraperConfiguration, sectors, symbolMapping, - SymbolProfileOverrides + SymbolProfileOverrides, + url }, where: { dataSource_symbol: { dataSource, symbol } } }); diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts index 352e709bd..2f0c2546b 100644 --- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts +++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts @@ -64,7 +64,8 @@ export class AssetProfileDialog implements OnDestroy, OnInit { name: ['', Validators.required], scraperConfiguration: '', sectors: '', - symbolMapping: '' + symbolMapping: '', + url: '' }); public assetProfileSubClass: string; public benchmarks: Partial[]; @@ -163,7 +164,8 @@ export class AssetProfileDialog implements OnDestroy, OnInit { this.assetProfile?.scraperConfiguration ?? {} ), sectors: JSON.stringify(this.assetProfile?.sectors ?? []), - symbolMapping: JSON.stringify(this.assetProfile?.symbolMapping ?? {}) + symbolMapping: JSON.stringify(this.assetProfile?.symbolMapping ?? {}), + url: this.assetProfile?.url ?? '' }); this.assetProfileForm.markAsPristine(); @@ -293,7 +295,8 @@ export class AssetProfileDialog implements OnDestroy, OnInit { currency: (( (this.assetProfileForm.controls['currency'].value) ))?.value, - name: this.assetProfileForm.controls['name'].value + name: this.assetProfileForm.controls['name'].value, + url: this.assetProfileForm.controls['url'].value }; this.adminService diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html index 93240ba3a..bc6327f0e 100644 --- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html +++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -224,7 +224,7 @@ />
-
+
Asset Class @@ -237,7 +237,7 @@
-
+
Asset Sub Class @@ -330,6 +330,12 @@
+ + Url + + +
+
Note
-
- - Scraper Configuration -
+ @if (assetProfile?.dataSource === 'MANUAL') { +
+ + Scraper Configuration +
+ + +
+
+
+
+ + Sectors - -
- -
-
- - Sectors - - -
-
- - Countries - - -
+
+
+
+ + Countries + + +
+ }
Url + @if (assetProfileForm.controls['url'].value) { + + }
diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.module.ts b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.module.ts index 846b5e599..76266205f 100644 --- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.module.ts +++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.module.ts @@ -1,5 +1,6 @@ import { GfAdminMarketDataDetailModule } from '@ghostfolio/client/components/admin-market-data-detail/admin-market-data-detail.module'; import { AdminMarketDataService } from '@ghostfolio/client/components/admin-market-data/admin-market-data.service'; +import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module'; import { GfCurrencySelectorModule } from '@ghostfolio/ui/currency-selector/currency-selector.module'; import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module'; import { GfValueModule } from '@ghostfolio/ui/value'; @@ -26,6 +27,7 @@ import { AssetProfileDialog } from './asset-profile-dialog.component'; GfAdminMarketDataDetailModule, GfCurrencySelectorModule, GfPortfolioProportionChartModule, + GfSymbolIconModule, GfValueModule, MatButtonModule, MatCheckboxModule, diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/interfaces/interfaces.ts index c230a39ee..6a966b427 100644 --- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/interfaces/interfaces.ts +++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/interfaces/interfaces.ts @@ -1,6 +1,9 @@ +import { ColorScheme } from '@ghostfolio/common/types'; + import { DataSource } from '@prisma/client'; export interface AssetProfileDialogParams { + colorScheme: ColorScheme; dataSource: DataSource; deviceType: string; locale: string; From c6641fde36bc9f7809bbaff8059e1496fd229aae Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 6 Apr 2024 09:11:15 +0200 Subject: [PATCH 100/120] Feature/add icon to create or update platform dialog (#3241) * Add platform icon * Update changelog --- CHANGELOG.md | 1 + .../create-or-update-platform-dialog.html | 3 +++ .../create-or-update-platform-dialog.module.ts | 3 +++ 3 files changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c536d4106..c20da8462 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added support to override the asset sub class of an asset profile in the asset profile details dialog of the admin control - Added support to override the url of an asset profile in the asset profile details dialog of the admin control - Added the asset profile icon to the asset profile details dialog of the admin control +- Added the platform icon to the create or update platform dialog of the admin control - Extended the content of the _Self-Hosting_ section by the data providers on the Frequently Asked Questions (FAQ) page ### Changed diff --git a/apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html b/apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html index 06f6ab72e..780308130 100644 --- a/apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html +++ b/apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html @@ -12,6 +12,9 @@ Url + @if (data.platform.url) { + + }
diff --git a/apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.module.ts b/apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.module.ts index bf576480a..1b7c0bd8f 100644 --- a/apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.module.ts +++ b/apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.module.ts @@ -1,3 +1,5 @@ +import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module'; + import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; @@ -12,6 +14,7 @@ import { CreateOrUpdatePlatformDialog } from './create-or-update-platform-dialog declarations: [CreateOrUpdatePlatformDialog], imports: [ CommonModule, + GfSymbolIconModule, FormsModule, MatButtonModule, MatDialogModule, From 6152ff4b4474f26bb7f4c2b032a17989ff6414c6 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 6 Apr 2024 12:50:35 +0200 Subject: [PATCH 101/120] Remove condition (#3242) --- .../position-detail-dialog/position-detail-dialog.html | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html index 3680f5701..537fe104d 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -191,9 +191,7 @@ }
- @if (user?.settings?.isExperimentalFeatures) { -
- } +
Asset Class Date: Sat, 6 Apr 2024 20:00:56 +0200 Subject: [PATCH 102/120] Feature/add quotes in README.md for API auth documentation (#3246) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 36a4eb818..234e2c941 100644 --- a/README.md +++ b/README.md @@ -206,7 +206,7 @@ Set the header for each request as follows: "Authorization": "Bearer eyJh..." ``` -You can get the _Bearer Token_ via `POST http://localhost:3333/api/v1/auth/anonymous` (Body: `{ accessToken: }`) +You can get the _Bearer Token_ via `POST http://localhost:3333/api/v1/auth/anonymous` (Body: `{ "accessToken": "" }`) Deprecated: `GET http://localhost:3333/api/v1/auth/anonymous/` or `curl -s http://localhost:3333/api/v1/auth/anonymous/`. From ca2e748c56581f4d2bd3fb0588b0d5554506c50e Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 6 Apr 2024 20:03:16 +0200 Subject: [PATCH 103/120] Bugfix/add missing tags in portfolio calculator (#3243) * Add missing tags * Update changelog --- CHANGELOG.md | 4 ++++ .../portfolio/calculator/portfolio-calculator.ts | 14 ++++++++++---- ...tor-baln-buy-and-sell-in-two-activities.spec.ts | 1 + .../portfolio-calculator-baln-buy-and-sell.spec.ts | 1 + .../twr/portfolio-calculator-baln-buy.spec.ts | 1 + ...alculator-btcusd-buy-and-sell-partially.spec.ts | 2 +- .../twr/portfolio-calculator-googl-buy.spec.ts | 2 +- ...folio-calculator-msft-buy-with-dividend.spec.ts | 2 +- ...-calculator-novn-buy-and-sell-partially.spec.ts | 1 + .../portfolio-calculator-novn-buy-and-sell.spec.ts | 1 + apps/api/src/app/portfolio/portfolio.service.ts | 14 ++++---------- 11 files changed, 26 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c20da8462..bf0eeb0fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improved the url validation in the create and update platform endpoint - Improved the language localization for German (`de`) +### Fixed + +- Fixed the missing tags in the portfolio calculations + ## 2.70.0 - 2024-04-02 ### Added diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index 48fcaf343..488f9ce99 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -29,7 +29,7 @@ import { max, subDays } from 'date-fns'; -import { last, uniq } from 'lodash'; +import { last, uniq, uniqBy } from 'lodash'; export abstract class PortfolioCalculator { protected static readonly ENABLE_LOGGING = false; @@ -57,9 +57,10 @@ export abstract class PortfolioCalculator { this.currentRateService = currentRateService; this.exchangeRateDataService = exchangeRateDataService; this.orders = activities.map( - ({ date, fee, quantity, SymbolProfile, type, unitPrice }) => { + ({ date, fee, quantity, SymbolProfile, tags = [], type, unitPrice }) => { return { SymbolProfile, + tags, type, date: format(date, DATE_FORMAT), fee: new Big(fee), @@ -711,17 +712,17 @@ export abstract class PortfolioCalculator { currentTransactionPointItem = { investment, - tags, averagePrice: newQuantity.gt(0) ? investment.div(newQuantity) : new Big(0), currency: SymbolProfile.currency, dataSource: SymbolProfile.dataSource, dividend: new Big(0), - fee: fee.plus(oldAccumulatedSymbol.fee), + fee: oldAccumulatedSymbol.fee.plus(fee), firstBuyDate: oldAccumulatedSymbol.firstBuyDate, quantity: newQuantity, symbol: SymbolProfile.symbol, + tags: oldAccumulatedSymbol.tags.concat(tags), transactionCount: oldAccumulatedSymbol.transactionCount + 1 }; } else { @@ -740,6 +741,11 @@ export abstract class PortfolioCalculator { }; } + currentTransactionPointItem.tags = uniqBy( + currentTransactionPointItem.tags, + 'id' + ); + symbols[SymbolProfile.symbol] = currentTransactionPointItem; const items = lastTransactionPoint?.items ?? []; diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts index ee71a1fb3..b936d21a9 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts @@ -164,6 +164,7 @@ describe('PortfolioCalculator', () => { marketPriceInBaseCurrency: 148.9, quantity: new Big('0'), symbol: 'BALN.SW', + tags: [], timeWeightedInvestment: new Big('285.80000000000000396627'), timeWeightedInvestmentWithCurrencyEffect: new Big( '285.80000000000000396627' diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts index 69078be7c..d1557bc12 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts @@ -149,6 +149,7 @@ describe('PortfolioCalculator', () => { marketPriceInBaseCurrency: 148.9, quantity: new Big('0'), symbol: 'BALN.SW', + tags: [], timeWeightedInvestment: new Big('285.8'), timeWeightedInvestmentWithCurrencyEffect: new Big('285.8'), transactionCount: 2, diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts index b52aac68d..593503493 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts @@ -134,6 +134,7 @@ describe('PortfolioCalculator', () => { marketPriceInBaseCurrency: 148.9, quantity: new Big('2'), symbol: 'BALN.SW', + tags: [], timeWeightedInvestment: new Big('273.2'), timeWeightedInvestmentWithCurrencyEffect: new Big('273.2'), transactionCount: 1, diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts index 420ba48f1..e3f351b28 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts @@ -166,7 +166,7 @@ describe('PortfolioCalculator', () => { ), quantity: new Big('1'), symbol: 'BTCUSD', - tags: undefined, + tags: [], timeWeightedInvestment: new Big('640.56763686131386861314'), timeWeightedInvestmentWithCurrencyEffect: new Big( '636.79469348020066587024' diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts index 5f33d771b..e7796b4d3 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts @@ -147,7 +147,7 @@ describe('PortfolioCalculator', () => { marketPriceInBaseCurrency: 103.10483, quantity: new Big('1'), symbol: 'GOOGL', - tags: undefined, + tags: [], timeWeightedInvestment: new Big('89.12'), timeWeightedInvestmentWithCurrencyEffect: new Big('82.329056'), transactionCount: 1, diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts index a2c106784..49a07e73f 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts @@ -126,7 +126,7 @@ describe('PortfolioCalculator', () => { marketPriceInBaseCurrency: 331.83, quantity: new Big('1'), symbol: 'MSFT', - tags: undefined, + tags: [], transactionCount: 2 } ], diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts index 21e0bb499..2bfd6d865 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts @@ -148,6 +148,7 @@ describe('PortfolioCalculator', () => { marketPriceInBaseCurrency: 87.8, quantity: new Big('1'), symbol: 'NOVN.SW', + tags: [], timeWeightedInvestment: new Big('145.10285714285714285714'), timeWeightedInvestmentWithCurrencyEffect: new Big( '145.10285714285714285714' diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts index 28920ece7..be3f75dc2 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts @@ -175,6 +175,7 @@ describe('PortfolioCalculator', () => { marketPriceInBaseCurrency: 87.8, quantity: new Big('0'), symbol: 'NOVN.SW', + tags: [], timeWeightedInvestment: new Big('151.6'), timeWeightedInvestmentWithCurrencyEffect: new Big('151.6'), transactionCount: 2, diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 3b8a42d89..fc13ea8e6 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -63,8 +63,7 @@ import { DataSource, Order, Platform, - Prisma, - Tag + Prisma } from '@prisma/client'; import { Big } from 'big.js'; import { isUUID } from 'class-validator'; @@ -701,11 +700,8 @@ export class PortfolioService { ); }); - let tags: Tag[] = []; - if (orders.length <= 0) { return { - tags, accounts: [], averagePrice: undefined, dataProviderInfo: undefined, @@ -730,6 +726,7 @@ export class PortfolioService { orders: [], quantity: undefined, SymbolProfile: undefined, + tags: [], transactionCount: undefined, value: undefined }; @@ -741,16 +738,12 @@ export class PortfolioService { const portfolioCalculator = this.calculatorFactory.createCalculator({ activities: orders.filter((order) => { - tags = tags.concat(order.tags); - return ['BUY', 'DIVIDEND', 'ITEM', 'SELL'].includes(order.type); }), calculationType: PerformanceCalculationType.TWR, currency: userCurrency }); - tags = uniqBy(tags, 'id'); - const portfolioStart = portfolioCalculator.getStartDate(); const transactionPoints = portfolioCalculator.getTransactionPoints(); @@ -771,6 +764,7 @@ export class PortfolioService { firstBuyDate, marketPrice, quantity, + tags, timeWeightedInvestment, timeWeightedInvestmentWithCurrencyEffect, transactionCount @@ -947,7 +941,6 @@ export class PortfolioService { minPrice, orders, SymbolProfile, - tags, accounts: [], averagePrice: 0, dataProviderInfo: undefined, @@ -967,6 +960,7 @@ export class PortfolioService { netPerformancePercentWithCurrencyEffect: undefined, netPerformanceWithCurrencyEffect: undefined, quantity: 0, + tags: [], transactionCount: undefined, value: 0 }; From 50dbbf056953222c10c9f733b3e2ee746ade51bb Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 6 Apr 2024 21:15:19 +0200 Subject: [PATCH 104/120] Feature/refactor symbol icon module to asset profile icon component (#3245) * Refactor symbol icon module to asset profile icon component (standalone) --- .../accounts-table/accounts-table.component.html | 4 ++-- .../accounts-table/accounts-table.module.ts | 4 ++-- .../asset-profile-dialog/asset-profile-dialog.html | 2 +- .../asset-profile-dialog.module.ts | 4 ++-- .../admin-platform/admin-platform.component.html | 2 +- .../admin-platform/admin-platform.module.ts | 4 ++-- .../create-or-update-platform-dialog.html | 6 +++++- .../create-or-update-platform-dialog.module.ts | 4 ++-- .../asset-profile-icon.component.html | 8 ++++++++ .../asset-profile-icon.component.scss} | 0 .../asset-profile-icon.component.ts} | 13 +++++++++---- .../symbol-icon/symbol-icon.component.html | 7 ------- .../components/symbol-icon/symbol-icon.module.ts | 12 ------------ .../create-or-update-account-dialog.html | 2 +- .../create-or-update-account-dialog.module.ts | 4 ++-- .../transfer-balance/transfer-balance-dialog.html | 4 ++-- .../transfer-balance-dialog.module.ts | 4 ++-- .../create-or-update-activity-dialog.html | 2 +- .../create-or-update-activity-dialog.module.ts | 4 ++-- .../activities-table.component.html | 4 ++-- .../lib/activities-table/activities-table.module.ts | 4 ++-- libs/ui/src/lib/assistant/assistant.html | 2 +- libs/ui/src/lib/assistant/assistant.module.ts | 4 ++-- .../holdings-table/holdings-table.component.html | 2 +- .../src/lib/holdings-table/holdings-table.module.ts | 4 ++-- 25 files changed, 54 insertions(+), 56 deletions(-) create mode 100644 apps/client/src/app/components/asset-profile-icon/asset-profile-icon.component.html rename apps/client/src/app/components/{symbol-icon/symbol-icon.component.scss => asset-profile-icon/asset-profile-icon.component.scss} (100%) rename apps/client/src/app/components/{symbol-icon/symbol-icon.component.ts => asset-profile-icon/asset-profile-icon.component.ts} (63%) delete mode 100644 apps/client/src/app/components/symbol-icon/symbol-icon.component.html delete mode 100644 apps/client/src/app/components/symbol-icon/symbol-icon.module.ts diff --git a/apps/client/src/app/components/accounts-table/accounts-table.component.html b/apps/client/src/app/components/accounts-table/accounts-table.component.html index cd4f139c4..241b5d90a 100644 --- a/apps/client/src/app/components/accounts-table/accounts-table.component.html +++ b/apps/client/src/app/components/accounts-table/accounts-table.component.html @@ -34,7 +34,7 @@ Name -
- Url @if (assetProfileForm.controls['url'].value) { - Name - Url @if (data.platform.url) { - + }
diff --git a/apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.module.ts b/apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.module.ts index 1b7c0bd8f..ac97e57cf 100644 --- a/apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.module.ts +++ b/apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.module.ts @@ -1,4 +1,4 @@ -import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module'; +import { GfAssetProfileIconComponent } from '@ghostfolio/client/components/asset-profile-icon/asset-profile-icon.component'; import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; @@ -14,7 +14,7 @@ import { CreateOrUpdatePlatformDialog } from './create-or-update-platform-dialog declarations: [CreateOrUpdatePlatformDialog], imports: [ CommonModule, - GfSymbolIconModule, + GfAssetProfileIconComponent, FormsModule, MatButtonModule, MatDialogModule, diff --git a/apps/client/src/app/components/asset-profile-icon/asset-profile-icon.component.html b/apps/client/src/app/components/asset-profile-icon/asset-profile-icon.component.html new file mode 100644 index 000000000..f0abad285 --- /dev/null +++ b/apps/client/src/app/components/asset-profile-icon/asset-profile-icon.component.html @@ -0,0 +1,8 @@ +@if (src) { + +} diff --git a/apps/client/src/app/components/symbol-icon/symbol-icon.component.scss b/apps/client/src/app/components/asset-profile-icon/asset-profile-icon.component.scss similarity index 100% rename from apps/client/src/app/components/symbol-icon/symbol-icon.component.scss rename to apps/client/src/app/components/asset-profile-icon/asset-profile-icon.component.scss diff --git a/apps/client/src/app/components/symbol-icon/symbol-icon.component.ts b/apps/client/src/app/components/asset-profile-icon/asset-profile-icon.component.ts similarity index 63% rename from apps/client/src/app/components/symbol-icon/symbol-icon.component.ts rename to apps/client/src/app/components/asset-profile-icon/asset-profile-icon.component.ts index a6fa0901a..4d96ef83f 100644 --- a/apps/client/src/app/components/symbol-icon/symbol-icon.component.ts +++ b/apps/client/src/app/components/asset-profile-icon/asset-profile-icon.component.ts @@ -1,4 +1,6 @@ +import { CommonModule } from '@angular/common'; import { + CUSTOM_ELEMENTS_SCHEMA, ChangeDetectionStrategy, Component, Input, @@ -7,12 +9,15 @@ import { import { DataSource } from '@prisma/client'; @Component({ - selector: 'gf-symbol-icon', changeDetection: ChangeDetectionStrategy.OnPush, - templateUrl: './symbol-icon.component.html', - styleUrls: ['./symbol-icon.component.scss'] + imports: [CommonModule], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + selector: 'gf-asset-profile-icon', + standalone: true, + styleUrls: ['./asset-profile-icon.component.scss'], + templateUrl: './asset-profile-icon.component.html' }) -export class SymbolIconComponent implements OnChanges { +export class GfAssetProfileIconComponent implements OnChanges { @Input() dataSource: DataSource; @Input() size: 'large'; @Input() symbol: string; diff --git a/apps/client/src/app/components/symbol-icon/symbol-icon.component.html b/apps/client/src/app/components/symbol-icon/symbol-icon.component.html deleted file mode 100644 index 0aebd0e5b..000000000 --- a/apps/client/src/app/components/symbol-icon/symbol-icon.component.html +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/apps/client/src/app/components/symbol-icon/symbol-icon.module.ts b/apps/client/src/app/components/symbol-icon/symbol-icon.module.ts deleted file mode 100644 index 8eee9ef0c..000000000 --- a/apps/client/src/app/components/symbol-icon/symbol-icon.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; - -import { SymbolIconComponent } from './symbol-icon.component'; - -@NgModule({ - declarations: [SymbolIconComponent], - exports: [SymbolIconComponent], - imports: [CommonModule], - schemas: [CUSTOM_ELEMENTS_SCHEMA] -}) -export class GfSymbolIconModule {} diff --git a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html index 9ff61d0c2..e2981462f 100644 --- a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html +++ b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -61,7 +61,7 @@ ) { -
-
-
- -
@if (element.Account?.Platform?.url) { -
- - Date: Sun, 7 Apr 2024 09:25:14 +0200 Subject: [PATCH 105/120] Feature/add key to x ray rule (#3248) * Add key * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/portfolio/rules.service.ts | 12 ++++++++++-- apps/api/src/models/rule.ts | 8 ++++++++ .../rules/account-cluster-risk/current-investment.ts | 1 + .../rules/account-cluster-risk/single-account.ts | 1 + .../base-currency-current-investment.ts | 1 + .../currency-cluster-risk/current-investment.ts | 1 + .../rules/emergency-fund/emergency-fund-setup.ts | 1 + .../rules/fees/fee-ratio-initial-investment.ts | 1 + 9 files changed, 25 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf0eeb0fd..932206f90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added support to override the url of an asset profile in the asset profile details dialog of the admin control - Added the asset profile icon to the asset profile details dialog of the admin control - Added the platform icon to the create or update platform dialog of the admin control +- Extended the rules in the _X-ray_ section by a `key` - Extended the content of the _Self-Hosting_ section by the data providers on the Frequently Asked Questions (FAQ) page ### Changed diff --git a/apps/api/src/app/portfolio/rules.service.ts b/apps/api/src/app/portfolio/rules.service.ts index 7dfcee56a..85f7ed55b 100644 --- a/apps/api/src/app/portfolio/rules.service.ts +++ b/apps/api/src/app/portfolio/rules.service.ts @@ -17,8 +17,16 @@ export class RulesService { return rule.getSettings(aUserSettings)?.isActive; }) .map((rule) => { - const evaluationResult = rule.evaluate(rule.getSettings(aUserSettings)); - return { ...evaluationResult, name: rule.getName() }; + const { evaluation, value } = rule.evaluate( + rule.getSettings(aUserSettings) + ); + + return { + evaluation, + value, + key: rule.getKey(), + name: rule.getName() + }; }); } } diff --git a/apps/api/src/models/rule.ts b/apps/api/src/models/rule.ts index 171da810d..ba37f4e94 100644 --- a/apps/api/src/models/rule.ts +++ b/apps/api/src/models/rule.ts @@ -7,19 +7,27 @@ import { EvaluationResult } from './interfaces/evaluation-result.interface'; import { RuleInterface } from './interfaces/rule.interface'; export abstract class Rule implements RuleInterface { + private key: string; private name: string; public constructor( protected exchangeRateDataService: ExchangeRateDataService, { + key, name }: { + key: string; name: string; } ) { + this.key = key; this.name = name; } + public getKey() { + return this.key; + } + public getName() { return this.name; } diff --git a/apps/api/src/models/rules/account-cluster-risk/current-investment.ts b/apps/api/src/models/rules/account-cluster-risk/current-investment.ts index 52f20a218..a9a60f912 100644 --- a/apps/api/src/models/rules/account-cluster-risk/current-investment.ts +++ b/apps/api/src/models/rules/account-cluster-risk/current-investment.ts @@ -15,6 +15,7 @@ export class AccountClusterRiskCurrentInvestment extends Rule { accounts: PortfolioDetails['accounts'] ) { super(exchangeRateDataService, { + key: AccountClusterRiskCurrentInvestment.name, name: 'Investment' }); diff --git a/apps/api/src/models/rules/account-cluster-risk/single-account.ts b/apps/api/src/models/rules/account-cluster-risk/single-account.ts index b5028228a..a47895c13 100644 --- a/apps/api/src/models/rules/account-cluster-risk/single-account.ts +++ b/apps/api/src/models/rules/account-cluster-risk/single-account.ts @@ -11,6 +11,7 @@ export class AccountClusterRiskSingleAccount extends Rule { accounts: PortfolioDetails['accounts'] ) { super(exchangeRateDataService, { + key: AccountClusterRiskSingleAccount.name, name: 'Single Account' }); diff --git a/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts b/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts index a2bd44f44..39406e6c2 100644 --- a/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts +++ b/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts @@ -11,6 +11,7 @@ export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule { positions: TimelinePosition[] ) { super(exchangeRateDataService, { + key: CurrencyClusterRiskCurrentInvestment.name, name: 'Investment' }); diff --git a/apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts b/apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts index b6248ab51..20e9502bf 100644 --- a/apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts +++ b/apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts @@ -11,6 +11,7 @@ export class EmergencyFundSetup extends Rule { emergencyFund: number ) { super(exchangeRateDataService, { + key: EmergencyFundSetup.name, name: 'Emergency Fund: Set up' }); diff --git a/apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts b/apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts index 0ba70d23c..69db9634d 100644 --- a/apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts +++ b/apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts @@ -13,6 +13,7 @@ export class FeeRatioInitialInvestment extends Rule { fees: number ) { super(exchangeRateDataService, { + key: FeeRatioInitialInvestment.name, name: 'Fee Ratio' }); From 719bbe156e081b04d12cc85fce47152d13ca52b2 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 7 Apr 2024 09:26:30 +0200 Subject: [PATCH 106/120] Feature/optimize calculation of allocations by market (#3249) * Optimize calculation of allocations by market * Update changelog --- CHANGELOG.md | 1 + .../src/app/portfolio/portfolio.controller.ts | 5 +- .../src/app/portfolio/portfolio.service.ts | 175 ++++++++++-------- .../allocations/allocations-page.component.ts | 3 +- apps/client/src/app/services/data.service.ts | 8 +- 5 files changed, 113 insertions(+), 79 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 932206f90..19140a7cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Optimized the calculation of allocations by market - Improved the url validation in the create and update platform endpoint - Improved the language localization for German (`de`) diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 6047b7abd..0ae596d84 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -78,9 +78,11 @@ export class PortfolioController { @Query('assetClasses') filterByAssetClasses?: string, @Query('range') dateRange: DateRange = 'max', @Query('tags') filterByTags?: string, - @Query('withLiabilities') withLiabilitiesParam = 'false' + @Query('withLiabilities') withLiabilitiesParam = 'false', + @Query('withMarkets') withMarketsParam = 'false' ): Promise { const withLiabilities = withLiabilitiesParam === 'true'; + const withMarkets = withMarketsParam === 'true'; let hasDetails = true; let hasError = false; @@ -106,6 +108,7 @@ export class PortfolioController { filters, impersonationId, withLiabilities, + withMarkets, userId: this.request.user.id, withSummary: true }); diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index fc13ea8e6..99fb47e2c 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -63,7 +63,8 @@ import { DataSource, Order, Platform, - Prisma + Prisma, + SymbolProfile } from '@prisma/client'; import { Big } from 'big.js'; import { isUUID } from 'class-validator'; @@ -337,6 +338,7 @@ export class PortfolioService { userId, withExcludedAccounts = false, withLiabilities = false, + withMarkets = false, withSummary = false }: { dateRange?: DateRange; @@ -345,6 +347,7 @@ export class PortfolioService { userId: string; withExcludedAccounts?: boolean; withLiabilities?: boolean; + withMarkets?: boolean; withSummary?: boolean; }): Promise { userId = await this.getUserId(impersonationId, userId); @@ -484,77 +487,17 @@ export class PortfolioService { } } - const symbolProfile = symbolProfileMap[symbol]; + const assetProfile = symbolProfileMap[symbol]; const dataProviderResponse = dataProviderResponses[symbol]; - const markets: PortfolioPosition['markets'] = { - [UNKNOWN_KEY]: 0, - developedMarkets: 0, - emergingMarkets: 0, - otherMarkets: 0 - }; - const marketsAdvanced: PortfolioPosition['marketsAdvanced'] = { - [UNKNOWN_KEY]: 0, - asiaPacific: 0, - emergingMarkets: 0, - europe: 0, - japan: 0, - northAmerica: 0, - otherMarkets: 0 - }; - - if (symbolProfile.countries.length > 0) { - for (const country of symbolProfile.countries) { - if (developedMarkets.includes(country.code)) { - markets.developedMarkets = new Big(markets.developedMarkets) - .plus(country.weight) - .toNumber(); - } else if (emergingMarkets.includes(country.code)) { - markets.emergingMarkets = new Big(markets.emergingMarkets) - .plus(country.weight) - .toNumber(); - } else { - markets.otherMarkets = new Big(markets.otherMarkets) - .plus(country.weight) - .toNumber(); - } + let markets: PortfolioPosition['markets']; + let marketsAdvanced: PortfolioPosition['marketsAdvanced']; - if (country.code === 'JP') { - marketsAdvanced.japan = new Big(marketsAdvanced.japan) - .plus(country.weight) - .toNumber(); - } else if (country.code === 'CA' || country.code === 'US') { - marketsAdvanced.northAmerica = new Big(marketsAdvanced.northAmerica) - .plus(country.weight) - .toNumber(); - } else if (asiaPacificMarkets.includes(country.code)) { - marketsAdvanced.asiaPacific = new Big(marketsAdvanced.asiaPacific) - .plus(country.weight) - .toNumber(); - } else if (emergingMarkets.includes(country.code)) { - marketsAdvanced.emergingMarkets = new Big( - marketsAdvanced.emergingMarkets - ) - .plus(country.weight) - .toNumber(); - } else if (europeMarkets.includes(country.code)) { - marketsAdvanced.europe = new Big(marketsAdvanced.europe) - .plus(country.weight) - .toNumber(); - } else { - marketsAdvanced.otherMarkets = new Big(marketsAdvanced.otherMarkets) - .plus(country.weight) - .toNumber(); - } - } - } else { - markets[UNKNOWN_KEY] = new Big(markets[UNKNOWN_KEY]) - .plus(valueInBaseCurrency) - .toNumber(); - - marketsAdvanced[UNKNOWN_KEY] = new Big(marketsAdvanced[UNKNOWN_KEY]) - .plus(valueInBaseCurrency) - .toNumber(); + if (withMarkets) { + ({ markets, marketsAdvanced } = this.getMarkets({ + assetProfile, + valueInBaseCurrency + })); } holdings[symbol] = { @@ -568,10 +511,10 @@ export class PortfolioService { allocationInPercentage: filteredValueInBaseCurrency.eq(0) ? 0 : valueInBaseCurrency.div(filteredValueInBaseCurrency).toNumber(), - assetClass: symbolProfile.assetClass, - assetSubClass: symbolProfile.assetSubClass, - countries: symbolProfile.countries, - dataSource: symbolProfile.dataSource, + assetClass: assetProfile.assetClass, + assetSubClass: assetProfile.assetSubClass, + countries: assetProfile.countries, + dataSource: assetProfile.dataSource, dateOfFirstActivity: parseDate(firstBuyDate), dividend: dividend?.toNumber() ?? 0, grossPerformance: grossPerformance?.toNumber() ?? 0, @@ -582,7 +525,7 @@ export class PortfolioService { grossPerformanceWithCurrencyEffect?.toNumber() ?? 0, investment: investment.toNumber(), marketState: dataProviderResponse?.marketState ?? 'delayed', - name: symbolProfile.name, + name: assetProfile.name, netPerformance: netPerformance?.toNumber() ?? 0, netPerformancePercent: netPerformancePercentage?.toNumber() ?? 0, netPerformancePercentWithCurrencyEffect: @@ -590,8 +533,8 @@ export class PortfolioService { netPerformanceWithCurrencyEffect: netPerformanceWithCurrencyEffect?.toNumber() ?? 0, quantity: quantity.toNumber(), - sectors: symbolProfile.sectors, - url: symbolProfile.url, + sectors: assetProfile.sectors, + url: assetProfile.url, valueInBaseCurrency: valueInBaseCurrency.toNumber() }; } @@ -1630,6 +1573,86 @@ export class PortfolioService { }; } + private getMarkets({ + assetProfile, + valueInBaseCurrency + }: { + assetProfile: EnhancedSymbolProfile; + valueInBaseCurrency: Big; + }) { + const markets = { + [UNKNOWN_KEY]: 0, + developedMarkets: 0, + emergingMarkets: 0, + otherMarkets: 0 + }; + const marketsAdvanced = { + [UNKNOWN_KEY]: 0, + asiaPacific: 0, + emergingMarkets: 0, + europe: 0, + japan: 0, + northAmerica: 0, + otherMarkets: 0 + }; + + if (assetProfile.countries.length > 0) { + for (const country of assetProfile.countries) { + if (developedMarkets.includes(country.code)) { + markets.developedMarkets = new Big(markets.developedMarkets) + .plus(country.weight) + .toNumber(); + } else if (emergingMarkets.includes(country.code)) { + markets.emergingMarkets = new Big(markets.emergingMarkets) + .plus(country.weight) + .toNumber(); + } else { + markets.otherMarkets = new Big(markets.otherMarkets) + .plus(country.weight) + .toNumber(); + } + + if (country.code === 'JP') { + marketsAdvanced.japan = new Big(marketsAdvanced.japan) + .plus(country.weight) + .toNumber(); + } else if (country.code === 'CA' || country.code === 'US') { + marketsAdvanced.northAmerica = new Big(marketsAdvanced.northAmerica) + .plus(country.weight) + .toNumber(); + } else if (asiaPacificMarkets.includes(country.code)) { + marketsAdvanced.asiaPacific = new Big(marketsAdvanced.asiaPacific) + .plus(country.weight) + .toNumber(); + } else if (emergingMarkets.includes(country.code)) { + marketsAdvanced.emergingMarkets = new Big( + marketsAdvanced.emergingMarkets + ) + .plus(country.weight) + .toNumber(); + } else if (europeMarkets.includes(country.code)) { + marketsAdvanced.europe = new Big(marketsAdvanced.europe) + .plus(country.weight) + .toNumber(); + } else { + marketsAdvanced.otherMarkets = new Big(marketsAdvanced.otherMarkets) + .plus(country.weight) + .toNumber(); + } + } + } else { + markets[UNKNOWN_KEY] = new Big(markets[UNKNOWN_KEY]) + .plus(valueInBaseCurrency) + .toNumber(); + + marketsAdvanced[UNKNOWN_KEY] = new Big(marketsAdvanced[UNKNOWN_KEY]) + .plus(valueInBaseCurrency) + .toNumber(); + } + + return { markets, marketsAdvanced }; + } + private getStreaks({ investments, savingsRate diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts index 67ad82316..0dba81d1e 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts @@ -205,7 +205,8 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { private fetchPortfolioDetails() { return this.dataService.fetchPortfolioDetails({ - filters: this.userService.getFilters() + filters: this.userService.getFilters(), + withMarkets: true }); } diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 088512cec..aeeb2f07b 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -411,10 +411,12 @@ export class DataService { public fetchPortfolioDetails({ filters, - withLiabilities = false + withLiabilities = false, + withMarkets = false }: { filters?: Filter[]; withLiabilities?: boolean; + withMarkets?: boolean; } = {}): Observable { let params = this.buildFiltersAsQueryParams({ filters }); @@ -422,6 +424,10 @@ export class DataService { params = params.append('withLiabilities', withLiabilities); } + if (withMarkets) { + params = params.append('withMarkets', withMarkets); + } + return this.http .get('/api/v1/portfolio/details', { params From 07c0e5a612fe569ab0124880eb51e21219f9156e Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 7 Apr 2024 11:30:32 +0200 Subject: [PATCH 107/120] Feature/add currency to order database schema (#3251) * Add currency to Order database schema * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/import/import.service.ts | 14 +++--- apps/api/src/app/order/create-order.dto.ts | 4 ++ apps/api/src/app/order/order.controller.ts | 19 +++++++- apps/api/src/app/order/order.service.ts | 8 +--- apps/api/src/app/order/update-order.dto.ts | 4 ++ .../portfolio-calculator-test-utils.ts | 1 + ...ate-or-update-activity-dialog.component.ts | 46 ++++--------------- .../create-or-update-activity-dialog.html | 6 +-- .../migration.sql | 2 + prisma/schema.prisma | 1 + 11 files changed, 51 insertions(+), 55 deletions(-) create mode 100644 prisma/migrations/20240407073037_added_currency_to_order/migration.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 19140a7cc..7b2f531a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added the asset profile icon to the asset profile details dialog of the admin control - Added the platform icon to the create or update platform dialog of the admin control - Extended the rules in the _X-ray_ section by a `key` +- Added `currency` to the `Order` database schema as a preparation to set a custom currency - Extended the content of the _Self-Hosting_ section by the data providers on the Frequently Asked Questions (FAQ) page ### Changed diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index f45512318..cbdff87c0 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -112,6 +112,7 @@ export class ImportService { accountId: Account?.id, accountUserId: undefined, comment: undefined, + currency: undefined, createdAt: undefined, fee: 0, feeInBaseCurrency: 0, @@ -261,6 +262,7 @@ export class ImportService { { accountId, comment, + currency, date, error, fee, @@ -285,7 +287,6 @@ export class ImportService { assetSubClass, countries, createdAt, - currency, dataSource, figi, figiComposite, @@ -342,6 +343,7 @@ export class ImportService { if (isDryRun) { order = { comment, + currency, date, fee, quantity, @@ -357,7 +359,6 @@ export class ImportService { assetSubClass, countries, createdAt, - currency, dataSource, figi, figiComposite, @@ -371,6 +372,7 @@ export class ImportService { symbolMapping, updatedAt, url, + currency: assetProfile.currency, comment: assetProfile.comment }, Account: validatedAccount, @@ -394,9 +396,9 @@ export class ImportService { SymbolProfile: { connectOrCreate: { create: { - currency, dataSource, - symbol + symbol, + currency: assetProfile.currency }, where: { dataSource_symbol: { @@ -420,14 +422,14 @@ export class ImportService { value, feeInBaseCurrency: this.exchangeRateDataService.toCurrency( fee, - currency, + assetProfile.currency, userCurrency ), // @ts-ignore SymbolProfile: assetProfile, valueInBaseCurrency: this.exchangeRateDataService.toCurrency( value, - currency, + assetProfile.currency, userCurrency ) }); diff --git a/apps/api/src/app/order/create-order.dto.ts b/apps/api/src/app/order/create-order.dto.ts index aecec842a..6d36f036a 100644 --- a/apps/api/src/app/order/create-order.dto.ts +++ b/apps/api/src/app/order/create-order.dto.ts @@ -42,6 +42,10 @@ export class CreateOrderDto { @IsISO4217CurrencyCode() currency: string; + @IsISO4217CurrencyCode() + @IsOptional() + customCurrency?: string; + @IsOptional() @IsEnum(DataSource, { each: true }) dataSource?: DataSource; diff --git a/apps/api/src/app/order/order.controller.ts b/apps/api/src/app/order/order.controller.ts index 2f9825d6b..c7fec0dac 100644 --- a/apps/api/src/app/order/order.controller.ts +++ b/apps/api/src/app/order/order.controller.ts @@ -126,13 +126,22 @@ export class OrderController { @UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseInterceptors(TransformDataSourceInRequestInterceptor) public async createOrder(@Body() data: CreateOrderDto): Promise { + const currency = data.currency; + const customCurrency = data.customCurrency; + + if (customCurrency) { + data.currency = customCurrency; + + delete data.customCurrency; + } + const order = await this.orderService.createOrder({ ...data, date: parseISO(data.date), SymbolProfile: { connectOrCreate: { create: { - currency: data.currency, + currency, dataSource: data.dataSource, symbol: data.symbol }, @@ -182,8 +191,16 @@ export class OrderController { const date = parseISO(data.date); const accountId = data.accountId; + const customCurrency = data.customCurrency; + delete data.accountId; + if (customCurrency) { + data.currency = customCurrency; + + delete data.customCurrency; + } + return this.orderService.updateOrder({ data: { ...data, diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index 126b04a07..20b2d5f15 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -26,6 +26,7 @@ import { endOfToday, isAfter } from 'date-fns'; import { groupBy, uniqBy } from 'lodash'; import { v4 as uuidv4 } from 'uuid'; +import { CreateOrderDto } from './create-order.dto'; import { Activities } from './interfaces/activities.interface'; @Injectable() @@ -65,7 +66,6 @@ export class OrderService { } const accountId = data.accountId; - let currency = data.currency; const tags = data.tags ?? []; const updateAccountBalance = data.updateAccountBalance ?? false; const userId = data.userId; @@ -73,7 +73,6 @@ export class OrderService { if (['FEE', 'INTEREST', 'ITEM', 'LIABILITY'].includes(data.type)) { const assetClass = data.assetClass; const assetSubClass = data.assetSubClass; - currency = data.SymbolProfile.connectOrCreate.create.currency; const dataSource: DataSource = 'MANUAL'; const id = uuidv4(); const name = data.SymbolProfile.connectOrCreate.create.symbol; @@ -81,7 +80,6 @@ export class OrderService { data.id = id; data.SymbolProfile.connectOrCreate.create.assetClass = assetClass; data.SymbolProfile.connectOrCreate.create.assetSubClass = assetSubClass; - data.SymbolProfile.connectOrCreate.create.currency = currency; data.SymbolProfile.connectOrCreate.create.dataSource = dataSource; data.SymbolProfile.connectOrCreate.create.name = name; data.SymbolProfile.connectOrCreate.create.symbol = id; @@ -116,7 +114,6 @@ export class OrderService { delete data.comment; } - delete data.currency; delete data.dataSource; delete data.symbol; delete data.tags; @@ -155,8 +152,8 @@ export class OrderService { await this.accountService.updateAccountBalance({ accountId, amount, - currency, userId, + currency: data.SymbolProfile.connectOrCreate.create.currency, date: data.date as Date }); } @@ -442,7 +439,6 @@ export class OrderService { delete data.assetClass; delete data.assetSubClass; - delete data.currency; delete data.dataSource; delete data.symbol; delete data.tags; diff --git a/apps/api/src/app/order/update-order.dto.ts b/apps/api/src/app/order/update-order.dto.ts index c0a400c57..be3c2b6e5 100644 --- a/apps/api/src/app/order/update-order.dto.ts +++ b/apps/api/src/app/order/update-order.dto.ts @@ -41,6 +41,10 @@ export class UpdateOrderDto { @IsISO4217CurrencyCode() currency: string; + @IsISO4217CurrencyCode() + @IsOptional() + customCurrency?: string; + @IsString() dataSource: DataSource; diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts index 6d1939fcd..504b5b171 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts @@ -3,6 +3,7 @@ export const activityDummyData = { accountUserId: undefined, comment: undefined, createdAt: new Date(), + currency: undefined, feeInBaseCurrency: undefined, id: undefined, isDraft: false, diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts index 4fb8e9d81..21a2ca920 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts @@ -98,10 +98,6 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { this.data.activity?.SymbolProfile?.currency, Validators.required ], - currencyOfFee: [ - this.data.activity?.SymbolProfile?.currency, - Validators.required - ], currencyOfUnitPrice: [ this.data.activity?.SymbolProfile?.currency, Validators.required @@ -149,45 +145,16 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { takeUntil(this.unsubscribeSubject) ) .subscribe(async () => { - let exchangeRateOfFee = 1; let exchangeRateOfUnitPrice = 1; this.activityForm.controls['feeInCustomCurrency'].setErrors(null); this.activityForm.controls['unitPriceInCustomCurrency'].setErrors(null); const currency = this.activityForm.controls['currency'].value; - const currencyOfFee = this.activityForm.controls['currencyOfFee'].value; const currencyOfUnitPrice = this.activityForm.controls['currencyOfUnitPrice'].value; const date = this.activityForm.controls['date'].value; - if (currency && currencyOfFee && currency !== currencyOfFee && date) { - try { - const { marketPrice } = await lastValueFrom( - this.dataService - .fetchExchangeRateForDate({ - date, - symbol: `${currencyOfFee}-${currency}` - }) - .pipe(takeUntil(this.unsubscribeSubject)) - ); - - exchangeRateOfFee = marketPrice; - } catch { - this.activityForm.controls['feeInCustomCurrency'].setErrors({ - invalid: true - }); - } - } - - const feeInCustomCurrency = - this.activityForm.controls['feeInCustomCurrency'].value * - exchangeRateOfFee; - - this.activityForm.controls['fee'].setValue(feeInCustomCurrency, { - emitEvent: false - }); - if ( currency && currencyOfUnitPrice && @@ -212,10 +179,18 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { } } + const feeInCustomCurrency = + this.activityForm.controls['feeInCustomCurrency'].value * + exchangeRateOfUnitPrice; + const unitPriceInCustomCurrency = this.activityForm.controls['unitPriceInCustomCurrency'].value * exchangeRateOfUnitPrice; + this.activityForm.controls['fee'].setValue(feeInCustomCurrency, { + emitEvent: false + }); + this.activityForm.controls['unitPrice'].setValue( unitPriceInCustomCurrency, { @@ -258,7 +233,6 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { })?.currency ?? this.data.user.settings.baseCurrency; this.activityForm.controls['currency'].setValue(currency); - this.activityForm.controls['currencyOfFee'].setValue(currency); this.activityForm.controls['currencyOfUnitPrice'].setValue(currency); if (['FEE', 'INTEREST'].includes(type)) { @@ -328,7 +302,6 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { })?.currency ?? this.data.user.settings.baseCurrency; this.activityForm.controls['currency'].setValue(currency); - this.activityForm.controls['currencyOfFee'].setValue(currency); this.activityForm.controls['currencyOfUnitPrice'].setValue(currency); this.activityForm.controls['dataSource'].removeValidators( @@ -361,7 +334,6 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { })?.currency ?? this.data.user.settings.baseCurrency; this.activityForm.controls['currency'].setValue(currency); - this.activityForm.controls['currencyOfFee'].setValue(currency); this.activityForm.controls['currencyOfUnitPrice'].setValue(currency); this.activityForm.controls['dataSource'].removeValidators( @@ -486,6 +458,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { assetSubClass: this.activityForm.controls['assetSubClass'].value, comment: this.activityForm.controls['comment'].value, currency: this.activityForm.controls['currency'].value, + customCurrency: this.activityForm.controls['currencyOfUnitPrice'].value, date: this.activityForm.controls['date'].value, dataSource: this.activityForm.controls['dataSource'].value, fee: this.activityForm.controls['fee'].value, @@ -549,7 +522,6 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { ) .subscribe(({ currency, dataSource, marketPrice }) => { this.activityForm.controls['currency'].setValue(currency); - this.activityForm.controls['currencyOfFee'].setValue(currency); this.activityForm.controls['currencyOfUnitPrice'].setValue(currency); this.activityForm.controls['dataSource'].setValue(dataSource); diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html index 9491a440e..79ea7647a 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html +++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html @@ -290,11 +290,7 @@ matTextSuffix [ngClass]="{ 'd-none': !activityForm.controls['currency']?.value }" > - - - {{ currency }} - - + {{ activityForm.controls['currencyOfUnitPrice'].value }}
Date: Sun, 7 Apr 2024 11:32:55 +0200 Subject: [PATCH 108/120] Release 2.71.0 (#3252) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b2f531a3..2e1ab7f1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## 2.71.0 - 2024-04-07 ### Added diff --git a/package.json b/package.json index 5db51af5d..fbb811928 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "2.70.0", + "version": "2.71.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio", From 71892e67b2aea4db49f69b49aeef186f337c109c Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 7 Apr 2024 20:27:38 +0200 Subject: [PATCH 109/120] Feature/upgrade prisma to version 5.12.1 (#3253) * Upgrade prisma to version 5.12.1 * Update changelog --- CHANGELOG.md | 6 ++++ package.json | 4 +-- yarn.lock | 90 ++++++++++++++++++++++++++-------------------------- 3 files changed, 53 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e1ab7f1d..59c3fe4d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Changed + +- Upgraded `prisma` from version `5.11.0` to `5.12.1` + ## 2.71.0 - 2024-04-07 ### Added diff --git a/package.json b/package.json index fbb811928..752218436 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "@nestjs/platform-express": "10.1.3", "@nestjs/schedule": "3.0.2", "@nestjs/serve-static": "4.0.0", - "@prisma/client": "5.11.0", + "@prisma/client": "5.12.1", "@simplewebauthn/browser": "9.0.1", "@simplewebauthn/server": "9.0.3", "@stripe/stripe-js": "1.47.0", @@ -125,7 +125,7 @@ "passport": "0.6.0", "passport-google-oauth20": "2.0.0", "passport-jwt": "4.0.0", - "prisma": "5.11.0", + "prisma": "5.12.1", "reflect-metadata": "0.1.13", "rxjs": "7.5.6", "stripe": "11.12.0", diff --git a/yarn.lock b/yarn.lock index ee83c3606..d505b65d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5357,46 +5357,46 @@ resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.25.tgz#f077fdc0b5d0078d30893396ff4827a13f99e817" integrity sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ== -"@prisma/client@5.11.0": - version "5.11.0" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.11.0.tgz#d8e55fab85163415b2245fb408b9106f83c8106d" - integrity sha512-SWshvS5FDXvgJKM/a0y9nDC1rqd7KG0Q6ZVzd+U7ZXK5soe73DJxJJgbNBt2GNXOa+ysWB4suTpdK5zfFPhwiw== - -"@prisma/debug@5.11.0": - version "5.11.0" - resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-5.11.0.tgz#80e3f9d5a8f678c67a8783f7fcdda3cbbb8dd091" - integrity sha512-N6yYr3AbQqaiUg+OgjkdPp3KPW1vMTAgtKX6+BiB/qB2i1TjLYCrweKcUjzOoRM5BriA4idrkTej9A9QqTfl3A== - -"@prisma/engines-version@5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102": - version "5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102" - resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102.tgz#a7aa218b1ebf1077798c931632461aae8ce6a8f7" - integrity sha512-WXCuyoymvrS4zLz4wQagSsc3/nE6CHy8znyiMv8RKazKymOMd5o9FP5RGwGHAtgoxd+aB/BWqxuP/Ckfu7/3MA== - -"@prisma/engines@5.11.0": - version "5.11.0" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.11.0.tgz#96e941c5c81ce68f3a8b4c481007d397564c5d4b" - integrity sha512-gbrpQoBTYWXDRqD+iTYMirDlF9MMlQdxskQXbhARhG6A/uFQjB7DZMYocMQLoiZXO/IskfDOZpPoZE8TBQKtEw== - dependencies: - "@prisma/debug" "5.11.0" - "@prisma/engines-version" "5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102" - "@prisma/fetch-engine" "5.11.0" - "@prisma/get-platform" "5.11.0" - -"@prisma/fetch-engine@5.11.0": - version "5.11.0" - resolved "https://registry.yarnpkg.com/@prisma/fetch-engine/-/fetch-engine-5.11.0.tgz#cd7a2fa5b5d89f1da0689e329c56fa69223fba7d" - integrity sha512-994viazmHTJ1ymzvWugXod7dZ42T2ROeFuH6zHPcUfp/69+6cl5r9u3NFb6bW8lLdNjwLYEVPeu3hWzxpZeC0w== - dependencies: - "@prisma/debug" "5.11.0" - "@prisma/engines-version" "5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102" - "@prisma/get-platform" "5.11.0" - -"@prisma/get-platform@5.11.0": - version "5.11.0" - resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-5.11.0.tgz#19a768127b1712c27f5dec8a0a79a4c9675829eb" - integrity sha512-rxtHpMLxNTHxqWuGOLzR2QOyQi79rK1u1XYAVLZxDGTLz/A+uoDnjz9veBFlicrpWjwuieM4N6jcnjj/DDoidw== - dependencies: - "@prisma/debug" "5.11.0" +"@prisma/client@5.12.1": + version "5.12.1" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.12.1.tgz#c26a674fea76754b3a9e8b90a11e617f90212f76" + integrity sha512-6/JnizEdlSBxDIdiLbrBdMW5NqDxOmhXAJaNXiPpgzAPr/nLZResT6MMpbOHLo5yAbQ1Vv5UU8PTPRzb0WIxdA== + +"@prisma/debug@5.12.1": + version "5.12.1" + resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-5.12.1.tgz#007c8ad2e466d565bcd0671b8846c27f8700c722" + integrity sha512-kd/wNsR0klrv79o1ITsbWxYyh4QWuBidvxsXSParPsYSu0ircUmNk3q4ojsgNc3/81b0ozg76iastOG43tbf8A== + +"@prisma/engines-version@5.12.0-21.473ed3124229e22d881cb7addf559799debae1ab": + version "5.12.0-21.473ed3124229e22d881cb7addf559799debae1ab" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.12.0-21.473ed3124229e22d881cb7addf559799debae1ab.tgz#c78d099a3fe86d446db7442e64e56987e39e7f32" + integrity sha512-6yvO8s80Tym61aB4QNtYZfWVmE3pwqe807jEtzm8C5VDe7nw8O1FGX3TXUaXmWV0fQTIAfRbeL2Gwrndabp/0g== + +"@prisma/engines@5.12.1": + version "5.12.1" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.12.1.tgz#a50649427d627a9af962a188a84c65d61c6e2b3f" + integrity sha512-HQDdglLw2bZR/TXD2Y+YfDMvi5Q8H+acbswqOsWyq9pPjBLYJ6gzM+ptlTU/AV6tl0XSZLU1/7F4qaWa8bqpJA== + dependencies: + "@prisma/debug" "5.12.1" + "@prisma/engines-version" "5.12.0-21.473ed3124229e22d881cb7addf559799debae1ab" + "@prisma/fetch-engine" "5.12.1" + "@prisma/get-platform" "5.12.1" + +"@prisma/fetch-engine@5.12.1": + version "5.12.1" + resolved "https://registry.yarnpkg.com/@prisma/fetch-engine/-/fetch-engine-5.12.1.tgz#c38e9fa17fdc535b4c83cbb7645569ad0a511fa9" + integrity sha512-qSs3KcX1HKcea1A+hlJVK/ljj0PNIUHDxAayGMvgJBqmaN32P9tCidlKz1EGv6WoRFICYnk3Dd/YFLBwnFIozA== + dependencies: + "@prisma/debug" "5.12.1" + "@prisma/engines-version" "5.12.0-21.473ed3124229e22d881cb7addf559799debae1ab" + "@prisma/get-platform" "5.12.1" + +"@prisma/get-platform@5.12.1": + version "5.12.1" + resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-5.12.1.tgz#33f427f6d744dee62a9e06858889691d78b50804" + integrity sha512-pgIR+pSvhYHiUcqXVEZS31NrFOTENC9yFUdEAcx7cdQBoZPmHVjtjN4Ss6NzVDMYPrKJJ51U14EhEoeuBlMioQ== + dependencies: + "@prisma/debug" "5.12.1" "@radix-ui/number@1.0.1": version "1.0.1" @@ -16574,12 +16574,12 @@ pretty-hrtime@^1.0.3: resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" integrity sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A== -prisma@5.11.0: - version "5.11.0" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.11.0.tgz#ef3891f79921a2deec6f540eba13a3cc8525f6d2" - integrity sha512-KCLiug2cs0Je7kGkQBN9jDWoZ90ogE/kvZTUTgz2h94FEo8pczCkPH7fPNXkD1sGU7Yh65risGGD1HQ5DF3r3g== +prisma@5.12.1: + version "5.12.1" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.12.1.tgz#db4596253bb066afc9f08744642f200a398d8d51" + integrity sha512-SkMnb6wyIxTv9ACqiHBI2u9gD6y98qXRoCoLEnZsF6yee5Qg828G+ARrESN+lQHdw4maSZFFSBPPDpvSiVTo0Q== dependencies: - "@prisma/engines" "5.11.0" + "@prisma/engines" "5.12.1" prismjs@^1.28.0: version "1.29.0" From 3bf7ac76a0dd6589d80859aa4ce7a6669bcea966 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 8 Apr 2024 08:07:00 +0200 Subject: [PATCH 110/120] Feature/upgrade nx to version 18.2.3 (#3256) * Upgrade Nx to version 17.3.3 * Update changelog --- CHANGELOG.md | 2 + package.json | 70 +-- yarn.lock | 1547 +++++++++++++++++++++++++++----------------------- 3 files changed, 865 insertions(+), 754 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59c3fe4d1..9aacb00de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Upgraded `angular` from version `17.2.4` to `17.3.3` +- Upgraded `Nx` from version `18.1.2` to `18.2.3` - Upgraded `prisma` from version `5.11.0` to `5.12.1` ## 2.71.0 - 2024-04-07 diff --git a/package.json b/package.json index 752218436..1f2e7520a 100644 --- a/package.json +++ b/package.json @@ -54,17 +54,17 @@ "workspace-generator": "nx workspace-generator" }, "dependencies": { - "@angular/animations": "17.2.4", - "@angular/cdk": "17.2.2", - "@angular/common": "17.2.4", - "@angular/compiler": "17.2.4", - "@angular/core": "17.2.4", - "@angular/forms": "17.2.4", - "@angular/material": "17.2.2", - "@angular/platform-browser": "17.2.4", - "@angular/platform-browser-dynamic": "17.2.4", - "@angular/router": "17.2.4", - "@angular/service-worker": "17.2.4", + "@angular/animations": "17.3.3", + "@angular/cdk": "17.3.3", + "@angular/common": "17.3.3", + "@angular/compiler": "17.3.3", + "@angular/core": "17.3.3", + "@angular/forms": "17.3.3", + "@angular/material": "17.3.3", + "@angular/platform-browser": "17.3.3", + "@angular/platform-browser-dynamic": "17.3.3", + "@angular/router": "17.3.3", + "@angular/service-worker": "17.3.3", "@codewithdan/observable-store": "2.2.15", "@dfinity/agent": "0.15.7", "@dfinity/auth-client": "0.15.7", @@ -136,30 +136,30 @@ "zone.js": "0.14.4" }, "devDependencies": { - "@angular-devkit/build-angular": "17.2.3", - "@angular-devkit/core": "17.2.3", - "@angular-devkit/schematics": "17.2.3", - "@angular-eslint/eslint-plugin": "17.2.1", - "@angular-eslint/eslint-plugin-template": "17.2.1", - "@angular-eslint/template-parser": "17.2.1", - "@angular/cli": "17.2.3", - "@angular/compiler-cli": "17.2.4", - "@angular/language-service": "17.2.4", - "@angular/localize": "17.2.4", - "@angular/pwa": "17.2.3", + "@angular-devkit/build-angular": "17.3.3", + "@angular-devkit/core": "17.3.3", + "@angular-devkit/schematics": "17.3.3", + "@angular-eslint/eslint-plugin": "17.3.0", + "@angular-eslint/eslint-plugin-template": "17.3.0", + "@angular-eslint/template-parser": "17.3.0", + "@angular/cli": "17.3.3", + "@angular/compiler-cli": "17.3.3", + "@angular/language-service": "17.3.3", + "@angular/localize": "17.3.3", + "@angular/pwa": "17.3.3", "@nestjs/schematics": "10.0.1", "@nestjs/testing": "10.1.3", - "@nx/angular": "18.1.2", - "@nx/cypress": "18.1.2", - "@nx/eslint-plugin": "18.1.2", - "@nx/jest": "18.1.2", - "@nx/js": "18.1.2", - "@nx/nest": "18.1.2", - "@nx/node": "18.1.2", - "@nx/storybook": "18.1.2", - "@nx/web": "18.1.2", - "@nx/workspace": "18.1.2", - "@schematics/angular": "17.2.3", + "@nx/angular": "18.2.3", + "@nx/cypress": "18.2.3", + "@nx/eslint-plugin": "18.2.3", + "@nx/jest": "18.2.3", + "@nx/js": "18.2.3", + "@nx/nest": "18.2.3", + "@nx/node": "18.2.3", + "@nx/storybook": "18.2.3", + "@nx/web": "18.2.3", + "@nx/workspace": "18.2.3", + "@schematics/angular": "17.3.3", "@simplewebauthn/types": "9.0.1", "@storybook/addon-essentials": "7.6.5", "@storybook/angular": "7.6.5", @@ -187,7 +187,7 @@ "jest": "29.4.3", "jest-environment-jsdom": "29.4.3", "jest-preset-angular": "14.0.3", - "nx": "18.1.2", + "nx": "18.2.3", "prettier": "3.2.5", "prettier-plugin-organize-attributes": "1.0.0", "react": "18.2.0", @@ -198,7 +198,7 @@ "ts-jest": "29.1.0", "ts-node": "10.9.1", "tslib": "2.6.0", - "typescript": "5.3.3", + "typescript": "5.4.4", "webpack-bundle-analyzer": "4.10.1" }, "engines": { diff --git a/yarn.lock b/yarn.lock index d505b65d0..9c77b9917 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,7 +12,15 @@ resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.1.tgz#abfccb8ca78075a2b6187345c26243c1a0842f28" integrity sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg== -"@ampproject/remapping@2.2.1", "@ampproject/remapping@^2.2.0": +"@ampproject/remapping@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" + integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + +"@ampproject/remapping@^2.2.0": version "2.2.1" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== @@ -20,12 +28,12 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" -"@angular-devkit/architect@0.1702.3": - version "0.1702.3" - resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1702.3.tgz#4bdd844f55c94dc6b3153bb693b6b1c5621d3ae2" - integrity sha512-4jeBgtBIZxAeJyiwSdbRE4+rWu34j0UMCKia8s7473rKj0Tn4+dXlHmA/kuFYIp6K/9pE/hBoeUFxLNA/DZuRQ== +"@angular-devkit/architect@0.1703.3": + version "0.1703.3" + resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1703.3.tgz#d60cdc2d2ad3b204d8b353124a8defa92c40db69" + integrity sha512-BKbdigCjmspqxOxSIQuWgPZzpyuKqZoTBDh0jDeLcAmvPsuxCgIWbsExI4OQ0CyusnQ+XT0IT39q8B9rvF56cg== dependencies: - "@angular-devkit/core" "17.2.3" + "@angular-devkit/core" "17.3.3" rxjs "7.8.1" "@angular-devkit/architect@^0.1301.0 || ^0.1401.0 || ^0.1501.0 || ^0.1601.0 || ^0.1700.0": @@ -36,83 +44,83 @@ "@angular-devkit/core" "17.0.0" rxjs "7.8.1" -"@angular-devkit/build-angular@17.2.3": - version "17.2.3" - resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-17.2.3.tgz#0ff93a434fc1f31d065943688b5c9559bdfc1593" - integrity sha512-AZsEHZj+k2Lxb7uQUwfEpSE6TvQhCoIgP6XLKgKxZHUOiTUVXDj84WhNcbup5SsSG1cafmoVN7APxxuSwHcoeg== +"@angular-devkit/build-angular@17.3.3": + version "17.3.3" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-17.3.3.tgz#94b610596300a8acba22f5c30dcb03220cbd96da" + integrity sha512-E/6Z1MIMhEB1I2sN+Pw4/zinwAFj4vLDh6dEuj856WWEPndgPiUB6fGX4EbCTsyIUzboXI5ysdNyt2Eq56bllA== dependencies: - "@ampproject/remapping" "2.2.1" - "@angular-devkit/architect" "0.1702.3" - "@angular-devkit/build-webpack" "0.1702.3" - "@angular-devkit/core" "17.2.3" - "@babel/core" "7.23.9" + "@ampproject/remapping" "2.3.0" + "@angular-devkit/architect" "0.1703.3" + "@angular-devkit/build-webpack" "0.1703.3" + "@angular-devkit/core" "17.3.3" + "@babel/core" "7.24.0" "@babel/generator" "7.23.6" "@babel/helper-annotate-as-pure" "7.22.5" "@babel/helper-split-export-declaration" "7.22.6" "@babel/plugin-transform-async-generator-functions" "7.23.9" "@babel/plugin-transform-async-to-generator" "7.23.3" - "@babel/plugin-transform-runtime" "7.23.9" - "@babel/preset-env" "7.23.9" - "@babel/runtime" "7.23.9" + "@babel/plugin-transform-runtime" "7.24.0" + "@babel/preset-env" "7.24.0" + "@babel/runtime" "7.24.0" "@discoveryjs/json-ext" "0.5.7" - "@ngtools/webpack" "17.2.3" + "@ngtools/webpack" "17.3.3" "@vitejs/plugin-basic-ssl" "1.1.0" ansi-colors "4.1.3" - autoprefixer "10.4.17" + autoprefixer "10.4.18" babel-loader "9.1.3" babel-plugin-istanbul "6.1.1" browserslist "^4.21.5" copy-webpack-plugin "11.0.0" - critters "0.0.20" + critters "0.0.22" css-loader "6.10.0" - esbuild-wasm "0.20.0" + esbuild-wasm "0.20.1" fast-glob "3.3.2" http-proxy-middleware "2.0.6" - https-proxy-agent "7.0.2" - inquirer "9.2.14" + https-proxy-agent "7.0.4" + inquirer "9.2.15" jsonc-parser "3.2.1" karma-source-map-support "1.4.0" less "4.2.0" less-loader "11.1.0" license-webpack-plugin "4.0.2" loader-utils "3.2.1" - magic-string "0.30.7" - mini-css-extract-plugin "2.8.0" + magic-string "0.30.8" + mini-css-extract-plugin "2.8.1" mrmime "2.0.0" open "8.4.2" ora "5.4.1" parse5-html-rewriting-stream "7.0.0" picomatch "4.0.1" - piscina "4.3.1" + piscina "4.4.0" postcss "8.4.35" - postcss-loader "8.1.0" + postcss-loader "8.1.1" resolve-url-loader "5.0.0" rxjs "7.8.1" - sass "1.70.0" - sass-loader "14.1.0" + sass "1.71.1" + sass-loader "14.1.1" semver "7.6.0" source-map-loader "5.0.0" source-map-support "0.5.21" - terser "5.27.0" + terser "5.29.1" tree-kill "1.2.2" tslib "2.6.2" - undici "6.6.2" - vite "5.0.12" + undici "6.7.1" + vite "5.1.5" watchpack "2.4.0" - webpack "5.90.1" - webpack-dev-middleware "6.1.1" + webpack "5.90.3" + webpack-dev-middleware "6.1.2" webpack-dev-server "4.15.1" webpack-merge "5.10.0" webpack-subresource-integrity "5.1.0" optionalDependencies: - esbuild "0.20.0" + esbuild "0.20.1" -"@angular-devkit/build-webpack@0.1702.3": - version "0.1702.3" - resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1702.3.tgz#9054793a57e494f4f80d541611a7c1c63377a24c" - integrity sha512-G9F2Ori8WxJtMvOQGxTdg7d+5aAO1IPeEtMiZwFPrw65Ey6Gvfm0h2+3FnQdzeKrZmGaTk5E6gffHXJJQfCnmQ== +"@angular-devkit/build-webpack@0.1703.3": + version "0.1703.3" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1703.3.tgz#b7fcc2fa2c0c6ba4cc1dcdd8d108c8f536d03a60" + integrity sha512-d0JjE8MaGVNphlJfeP1OZKhNT4wCXkEZKdSdwE0+W+vDHNUuZiUBB1czO48sb7T4xBrdjRWlV/9CzMNJ7n3ydA== dependencies: - "@angular-devkit/architect" "0.1702.3" + "@angular-devkit/architect" "0.1703.3" rxjs "7.8.1" "@angular-devkit/core@16.0.1": @@ -149,10 +157,10 @@ rxjs "7.8.1" source-map "0.7.4" -"@angular-devkit/core@17.2.3": - version "17.2.3" - resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-17.2.3.tgz#9e38dc4242212a6b00bf03e518add0f9b75b6e7f" - integrity sha512-A7WWl1/VsZw6utFFPBib1wSbAB5OeBgAgQmVpVe9wW8u9UZa6CLc7b3InWtRRyBXTo9Sa5GNZDFfwlXhy3iW3w== +"@angular-devkit/core@17.3.3": + version "17.3.3" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-17.3.3.tgz#dce2f615355b2ef59c19927d90620670a6c890d0" + integrity sha512-J22Sh3M7rj8Ar3iEs20ko5wgC3DE7vWfYZNdimt2IJiS4J7BEX8R3Awf+TRt+6AN3NFm3/xe1Sz4yvDh3FvNFg== dependencies: ajv "8.12.0" ajv-formats "2.1.1" @@ -194,87 +202,87 @@ ora "5.4.1" rxjs "7.8.1" -"@angular-devkit/schematics@17.2.3": - version "17.2.3" - resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-17.2.3.tgz#1e9f803deccdfdd1eeeff36c702d6dadae91227f" - integrity sha512-JZCzHHheotv+iJ4p6qLc3pEi2M8NO12Slo6uiCg2T9B01glAcJB7DA1nwqjwD1cElf24Pt0C+HI0r+Lng48IsQ== +"@angular-devkit/schematics@17.3.3": + version "17.3.3" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-17.3.3.tgz#223d8ffd27e6daaf63a3161dbe8c849860541bf1" + integrity sha512-SABqTtj2im4PJhQjNaAsSypbNkpZFW8YozJ3P748tlh5a9XoHpgiqXv5JhRbyKElLDAyk5i9fe2++JmSudPG/Q== dependencies: - "@angular-devkit/core" "17.2.3" + "@angular-devkit/core" "17.3.3" jsonc-parser "3.2.1" - magic-string "0.30.7" + magic-string "0.30.8" ora "5.4.1" rxjs "7.8.1" -"@angular-eslint/bundled-angular-compiler@17.2.1": - version "17.2.1" - resolved "https://registry.yarnpkg.com/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-17.2.1.tgz#d849b0845371b41856b9f598af81ce5bf799bca0" - integrity sha512-puC0itsZv2QlrDOCcWtq1KZH+DvfrpV+mV78HHhi6+h25R5iIhr8ARKcl3EQxFjvrFq34jhG8pSupxKvFbHVfA== +"@angular-eslint/bundled-angular-compiler@17.3.0": + version "17.3.0" + resolved "https://registry.yarnpkg.com/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-17.3.0.tgz#08b8b1bebbb677a1f208b56516fc9177a289d212" + integrity sha512-ejfNzRuBeHUV8m2fkgs+M809rj5STuCuQo4fdfc6ccQpzXDI6Ha7BKpTznWfg5g529q/wrkoGSGgFxU9Yc2/dQ== -"@angular-eslint/eslint-plugin-template@17.2.1": - version "17.2.1" - resolved "https://registry.yarnpkg.com/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-17.2.1.tgz#226a623219375a2344112c1c896fefef0dae4df6" - integrity sha512-hl1hcHtcm90wyVL1OQGTz16oA0KHon+FFb3Qg0fLXObaXxA495Ecefd9ub5Xxg4JEOPRDi29bF1Y3YKpwflgeg== +"@angular-eslint/eslint-plugin-template@17.3.0": + version "17.3.0" + resolved "https://registry.yarnpkg.com/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-17.3.0.tgz#712a99503b4ef12e9f37979375539c3ace44375b" + integrity sha512-9l/aRfpE9MCRVDWRb+rSB9Zei0paep1vqV6M/87VUnzBnzqeMRnVuPvQowilh2zweVSGKBF25Vp4HkwOL6ExDQ== dependencies: - "@angular-eslint/bundled-angular-compiler" "17.2.1" - "@angular-eslint/utils" "17.2.1" - "@typescript-eslint/type-utils" "6.19.0" - "@typescript-eslint/utils" "6.19.0" + "@angular-eslint/bundled-angular-compiler" "17.3.0" + "@angular-eslint/utils" "17.3.0" + "@typescript-eslint/type-utils" "7.2.0" + "@typescript-eslint/utils" "7.2.0" aria-query "5.3.0" axobject-query "4.0.0" -"@angular-eslint/eslint-plugin@17.2.1": - version "17.2.1" - resolved "https://registry.yarnpkg.com/@angular-eslint/eslint-plugin/-/eslint-plugin-17.2.1.tgz#2be51ead1785950feb8351001e0683eae42f4c29" - integrity sha512-9yA81BHpsaCUKRBtHGN3ieAy8HpIoffzPQMu34lYqZFT4yGHGhYmhQjNSQGBRbV2LD9dVv2U35rMHNmUcozXpw== +"@angular-eslint/eslint-plugin@17.3.0": + version "17.3.0" + resolved "https://registry.yarnpkg.com/@angular-eslint/eslint-plugin/-/eslint-plugin-17.3.0.tgz#b5037877cdc64d407649247e5ca09851c8674b4e" + integrity sha512-81cQbOEPoQupFX8WmpqZn+y8VA7JdVRGBtt+uJNKBXcJknTpPWdLBZRFlgVakmC24iEZ0Fint/N3NBBQI3mz2A== dependencies: - "@angular-eslint/utils" "17.2.1" - "@typescript-eslint/utils" "6.19.0" + "@angular-eslint/utils" "17.3.0" + "@typescript-eslint/utils" "7.2.0" -"@angular-eslint/template-parser@17.2.1": - version "17.2.1" - resolved "https://registry.yarnpkg.com/@angular-eslint/template-parser/-/template-parser-17.2.1.tgz#005f997346eb17c6dbca5fffc41da51b7e755013" - integrity sha512-WPQYFvRju0tCDXQ/pwrzC911pE07JvpeDgcN2elhzV6lxDHJEZpA5O9pnW9qgNA6J6XM9Q7dBkJ22ztAzC4WFw== +"@angular-eslint/template-parser@17.3.0": + version "17.3.0" + resolved "https://registry.yarnpkg.com/@angular-eslint/template-parser/-/template-parser-17.3.0.tgz#580a703cbaa4967d36a953a00f5c347987c14171" + integrity sha512-m+UzAnWgtjeS0x6skSmR0eXltD/p7HZA+c8pPyAkiHQzkxE7ohhfyZc03yWGuYJvWQUqQAKKdO/nQop14TP0bg== dependencies: - "@angular-eslint/bundled-angular-compiler" "17.2.1" + "@angular-eslint/bundled-angular-compiler" "17.3.0" eslint-scope "^8.0.0" -"@angular-eslint/utils@17.2.1": - version "17.2.1" - resolved "https://registry.yarnpkg.com/@angular-eslint/utils/-/utils-17.2.1.tgz#3d4217775d86479348fdd0e1ad83014c9d8339f2" - integrity sha512-qQYTBXy90dWM7fhhpa5i9lTtqqhJisvRa+naCrQx9kBgR458JScLdkVIdcZ9D/rPiDCmKiVUfgcDISnjUeqTqg== +"@angular-eslint/utils@17.3.0": + version "17.3.0" + resolved "https://registry.yarnpkg.com/@angular-eslint/utils/-/utils-17.3.0.tgz#85915e864c7b7f33df1fdf15f74cc99fd5895e1e" + integrity sha512-PJT9pxWqpvI9OXO+7L5SIVhvMW+RFjeafC7PYjtvSbNFpz+kF644BiAcfMJ0YqBnkrw3JXt+RAX25CT4mXIoXw== dependencies: - "@angular-eslint/bundled-angular-compiler" "17.2.1" - "@typescript-eslint/utils" "6.19.0" + "@angular-eslint/bundled-angular-compiler" "17.3.0" + "@typescript-eslint/utils" "7.2.0" -"@angular/animations@17.2.4": - version "17.2.4" - resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-17.2.4.tgz#d1c7d74b1bc360ba853c299a8db70d1a9517bf1c" - integrity sha512-eTjD8XeioL1Xj+W6iQayOh2JBCfjkg+MG3wzyEW0jhetE/N+wm2xbI1aub2pYplKsu96hOih3lfowYt7qIKGfw== +"@angular/animations@17.3.3": + version "17.3.3" + resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-17.3.3.tgz#b6487fbaa970cfd1f998d72a61e74c7e3deb14be" + integrity sha512-poLW3FHe5wkxmTIsQ3em2vq4obgQHyZJz6biF+4hCqQSNMbMBS0e5ZycAiJLkUD/WLc88lQZ20muRO7qjVuMLA== dependencies: tslib "^2.3.0" -"@angular/cdk@17.2.2": - version "17.2.2" - resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-17.2.2.tgz#c3f3335209a2099cbc124c2b1e9b1b8d20488647" - integrity sha512-no3FownDI+05SvCGOxduramTJw+V5p/rKebz4msZbsAXXLnOScZPN2rDgMKShl2dQokc6gjsKXsy8fAYpx7NSQ== +"@angular/cdk@17.3.3": + version "17.3.3" + resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-17.3.3.tgz#a266eb76a91ee2ce8f1e2df4bdc9a40b8dab29eb" + integrity sha512-hfS9pwaNE6CTZqP3FBh9tZPbuf//bDqZ5IpMzscfDFrwX8ycxBiI3znH/rFSf9l1rL0OQGoqWWNVfJCT+RrukA== dependencies: tslib "^2.3.0" optionalDependencies: parse5 "^7.1.2" -"@angular/cli@17.2.3": - version "17.2.3" - resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-17.2.3.tgz#c5e70fcb8f1e4cc15ef5fba04533fbe414e875c1" - integrity sha512-GIF9NF4t8PiHS4wt6baw1hECfmMOmNHvDAuT12/xoAueOairxIQ+AX13WaEHMJriWujm31TjqbwXmhPxMSEQpw== +"@angular/cli@17.3.3": + version "17.3.3" + resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-17.3.3.tgz#66880fb12b0d4e536222ec7a256431795fe344c9" + integrity sha512-veIGK2sRm0SfiLHeftx0W0xC3N8uxoqxXiSG57V6W2wIFN/fKm3aRq3sa8phz7vxUzoKGqyZh6hsT7ybkjgkGA== dependencies: - "@angular-devkit/architect" "0.1702.3" - "@angular-devkit/core" "17.2.3" - "@angular-devkit/schematics" "17.2.3" - "@schematics/angular" "17.2.3" + "@angular-devkit/architect" "0.1703.3" + "@angular-devkit/core" "17.3.3" + "@angular-devkit/schematics" "17.3.3" + "@schematics/angular" "17.3.3" "@yarnpkg/lockfile" "1.1.0" ansi-colors "4.1.3" - ini "4.1.1" - inquirer "9.2.14" + ini "4.1.2" + inquirer "9.2.15" jsonc-parser "3.2.1" npm-package-arg "11.0.1" npm-pick-manifest "9.0.0" @@ -286,17 +294,17 @@ symbol-observable "4.0.0" yargs "17.7.2" -"@angular/common@17.2.4": - version "17.2.4" - resolved "https://registry.yarnpkg.com/@angular/common/-/common-17.2.4.tgz#d56054b499fc9bbc9350df668111b1680c09b1fa" - integrity sha512-ymzDHZPQWpBKVQ7lPZucU+vBSb70Re6y5TKzkOX7oYE8Z1+tiNGLvfmzGsO2/N0lvwyZWXjkdXYEDON2hIlZ1Q== +"@angular/common@17.3.3": + version "17.3.3" + resolved "https://registry.yarnpkg.com/@angular/common/-/common-17.3.3.tgz#6bbd0c033446010ada04511b6955d048259cf9d7" + integrity sha512-GwlKetNpfWKiG2j4S6bYTi6PA2iT4+eln7o8owo44xZWdQnWQjfxnH39vQuCyhi6OOQL1dozmae+fVXgQsV6jQ== dependencies: tslib "^2.3.0" -"@angular/compiler-cli@17.2.4": - version "17.2.4" - resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-17.2.4.tgz#2aa9366974723a428530fdfd29ecfa5cdfad1839" - integrity sha512-VGQx1YoYuifQZNj2/nGMEyYVYvXSWrt1ZXK43dgxPDH3jCWNncOBUYtmyCmYvxKvDz0aDO3KL8cro8c4+N0pPw== +"@angular/compiler-cli@17.3.3": + version "17.3.3" + resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-17.3.3.tgz#e2505b95b0d56118ea0950eae18bb0fa2c2e7515" + integrity sha512-vM0lqwuXQZ912HbLnIuvUblvIz2WEUsU7a5Z2ieNey6famH4zxPH12vCbVwXgicB6GLHorhOfcWC5443wD2mJw== dependencies: "@babel/core" "7.23.9" "@jridgewell/sourcemap-codec" "^1.4.14" @@ -307,10 +315,10 @@ tslib "^2.3.0" yargs "^17.2.1" -"@angular/compiler@17.2.4": - version "17.2.4" - resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-17.2.4.tgz#82fb29e80096b1fb3f6fd4d4f2c22fdbc0bb9a9b" - integrity sha512-McSsBcoHhMkaQpHM5/wTosAKTzJY5uE6ji3z+ec5GrIJhV7jrVfa67+RUoUzHe+rlD/7oQbX1L/OaHKDP8+/mA== +"@angular/compiler@17.3.3": + version "17.3.3" + resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-17.3.3.tgz#ac6aefbb01f031b5834477aff46aa267719f7156" + integrity sha512-ZNMRfagMxMjk1KW5H3ssCg5QL0J6ZW1JAZ1mrTXixqS7gbdwl60bTGE+EfuEwbjvovEYaj4l9cga47eMaxZTbQ== dependencies: tslib "^2.3.0" @@ -319,10 +327,10 @@ resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-9.0.0.tgz#87e0bef4c369b6cadae07e3a4295778fc93799d5" integrity sha512-ctjwuntPfZZT2mNj2NDIVu51t9cvbhl/16epc5xEwyzyDt76pX9UgwvY+MbXrf/C/FWwdtmNtfP698BKI+9leQ== -"@angular/core@17.2.4": - version "17.2.4" - resolved "https://registry.yarnpkg.com/@angular/core/-/core-17.2.4.tgz#46600cbc399e150ed6332db48d8c4b644789169f" - integrity sha512-5Bko+vk7H1Ce57MHuRcpZtq2Srq5euufSvwg0piPozp0yYmCqNoYN7c128kgi6PbiPQeAnKRzRbEuYd1YCU4Tw== +"@angular/core@17.3.3": + version "17.3.3" + resolved "https://registry.yarnpkg.com/@angular/core/-/core-17.3.3.tgz#e0fd86eccd0106a5b8602c56eb4449cbb4538219" + integrity sha512-O/jr3aFJMCxF6Jmymjx4jIigRHJfqM/ALIi60y2LVznBVFkk9xyMTsAjgWQIEHX+2muEIzgfKuXzpL0y30y+wA== dependencies: tslib "^2.3.0" @@ -331,32 +339,32 @@ resolved "https://registry.yarnpkg.com/@angular/core/-/core-9.0.0.tgz#227dc53e1ac81824f998c6e76000b7efc522641e" integrity sha512-6Pxgsrf0qF9iFFqmIcWmjJGkkCaCm6V5QNnxMy2KloO3SDq6QuMVRbN9RtC8Urmo25LP+eZ6ZgYqFYpdD8Hd9w== -"@angular/forms@17.2.4": - version "17.2.4" - resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-17.2.4.tgz#0ec0d0e5e53659162397b631240786c681438234" - integrity sha512-flubCxK6Rc1YmAu23+o+NwqaIWbJ4MIYij05b1GlpRKB5GRX6M0fOl7uRHZmA6dC4xZGt/MUklRqb71T7dJ5JQ== +"@angular/forms@17.3.3": + version "17.3.3" + resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-17.3.3.tgz#ff00da4f7ab1f6fefda7b3c323ddb07c2a4b23ac" + integrity sha512-wqn+eAggbOZY91hr7oDjv5qdflszVOC9SZMcWJUoZTGn+8eoV6v6728GDFuDDwYkKQ9G9eQbX4IZmYoVw3TVjQ== dependencies: tslib "^2.3.0" -"@angular/language-service@17.2.4": - version "17.2.4" - resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-17.2.4.tgz#1c216681c6ea2a246ff6c09f0f9d05be4889b8ab" - integrity sha512-4F32tJtl9Z8QKe1djkPRj/WY45NKv1bn9aL9Bi9z3T5ZkBCVsdnnXcm4hXnD9gXgWL5RozV2NTbuhGlGx5R0Pg== +"@angular/language-service@17.3.3": + version "17.3.3" + resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-17.3.3.tgz#99b26aabc10b210e1cedf4b8cca1cac64ccf4183" + integrity sha512-OtdWNY0Syg4UvA8j2IhQJeq/UjWHYbRiyUcZjGKPRzuqIPjUhsmMyuW3zpi7Pwx2CpBzZXcik1Ra2WZ0gbwigg== -"@angular/localize@17.2.4": - version "17.2.4" - resolved "https://registry.yarnpkg.com/@angular/localize/-/localize-17.2.4.tgz#adb03882d1dc0d1bf54c3c04d5be9bc4ddd511a3" - integrity sha512-l6qZzP7f0fH6bCufyrhlUD6n7ggfTEaerIZW/jw0mnXFqVsHVfXX2jWHKljaZJWT3vhDp312i8xAukoAPM0uSQ== +"@angular/localize@17.3.3": + version "17.3.3" + resolved "https://registry.yarnpkg.com/@angular/localize/-/localize-17.3.3.tgz#3f9c3c66eb02648edc9c8d348124d7170bab1946" + integrity sha512-gahGKy0VBZ+KP6MUULGQMoi5SN3REwslaPvtomizzz9fdmqHfR8PPd1vOJSNm2IEVlvm1hv1dDRjPcR4DJwvaQ== dependencies: "@babel/core" "7.23.9" "@types/babel__core" "7.20.5" fast-glob "3.3.2" yargs "^17.2.1" -"@angular/material@17.2.2": - version "17.2.2" - resolved "https://registry.yarnpkg.com/@angular/material/-/material-17.2.2.tgz#33e3e11183030d9f4c713b56be3cf49fd1df1f0a" - integrity sha512-ToUp8gARTvdze9L7jhEuKqdos221jUCMRD6qzhl07XZRlxVbf/5VXUq2Nn7ei9uN11Ii1UY5pC0GS2XtlyHp4A== +"@angular/material@17.3.3": + version "17.3.3" + resolved "https://registry.yarnpkg.com/@angular/material/-/material-17.3.3.tgz#33f7ca38d3c0ff909bde4e8ae490ea2da49ecd3f" + integrity sha512-cb3PYY+Lf3FvXxXIRmOBcTn5QS9Ghr5Eq0aiJiiYV6YVohr0YGWsndMCZ/5a2j8fxpboDo9THeTnOuuAOJv7AA== dependencies: "@material/animation" "15.0.0-canary.7f224ddd4.0" "@material/auto-init" "15.0.0-canary.7f224ddd4.0" @@ -407,40 +415,40 @@ "@material/typography" "15.0.0-canary.7f224ddd4.0" tslib "^2.3.0" -"@angular/platform-browser-dynamic@17.2.4": - version "17.2.4" - resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.2.4.tgz#c28d405c812d39ce885a4cd65909457741ac062d" - integrity sha512-tNS6WexBbdks4uiB0JfPjUG2/rJ/5wuWr9C11CIgsMo+Onbw49imwDQQTxsx1A3misVb72mUufRza9DcxfSBxg== +"@angular/platform-browser-dynamic@17.3.3": + version "17.3.3" + resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.3.3.tgz#0e747cecb51ebaec53c11ebfef289972b793484d" + integrity sha512-jSgSNHRTXCIat20I+4tLm/e8qOvrIE3Zv7S/DtYZEiAth84uoznvo1kXnN+KREse2vP/WoNgSDKQ2JLzkwYXSQ== dependencies: tslib "^2.3.0" -"@angular/platform-browser@17.2.4": - version "17.2.4" - resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-17.2.4.tgz#06733dbbd49c3b28f77be142dc6b0dbb7be07638" - integrity sha512-A1jkx4ApIx76VDxm8UZLKEq+gwpKZb4qjzCTBDfjOpXB0MJQ5IaYdCrV0E/vPCKZhIfjbEHK+9H1vHRYDCcXtA== +"@angular/platform-browser@17.3.3": + version "17.3.3" + resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-17.3.3.tgz#b00a68526e2f39e9797ad4696f9dd8b42451f268" + integrity sha512-XFWjquD+Pr9VszRzrDlT6uaf57TsY9XhL9iHCNok6Op5DpVQpIAuw1vFt2t5ZoQ0gv+lY8mVWnxgqe3CgTdYxw== dependencies: tslib "^2.3.0" -"@angular/pwa@17.2.3": - version "17.2.3" - resolved "https://registry.yarnpkg.com/@angular/pwa/-/pwa-17.2.3.tgz#008059b28ae6a7849ad2236259a513ccca155904" - integrity sha512-4fcJgE3Y3g/FQ1czT+HTMYNjXuzTJYGJ1tGP0+++aWdk/IKj+RSGhc31ZjVgjJGk8Rk6inAthNOI8rvByepR5Q== +"@angular/pwa@17.3.3": + version "17.3.3" + resolved "https://registry.yarnpkg.com/@angular/pwa/-/pwa-17.3.3.tgz#28851976b5763b2e6608dafc6fdd86af1416d42a" + integrity sha512-my2EHZ+ld9L+r2BUfL1Mq9ozUvE+1BY3bav5o4ZkwwtQOZ3XTqYIK2v3Z5O/Mot3XAIAeUR9rksoHGtCZcagMg== dependencies: - "@angular-devkit/schematics" "17.2.3" - "@schematics/angular" "17.2.3" + "@angular-devkit/schematics" "17.3.3" + "@schematics/angular" "17.3.3" parse5-html-rewriting-stream "7.0.0" -"@angular/router@17.2.4": - version "17.2.4" - resolved "https://registry.yarnpkg.com/@angular/router/-/router-17.2.4.tgz#b376d14d2022cdea86172f2013884af5ee7a8f54" - integrity sha512-HnEq6OtyXVJx24Vps0N2GsdvynQ8Mv6twjGmhBlo3x/19ay0WEHdHdsayOSKFvxXg9LCLPnSDYlmpk074IsgqA== +"@angular/router@17.3.3": + version "17.3.3" + resolved "https://registry.yarnpkg.com/@angular/router/-/router-17.3.3.tgz#29859efaeaf9e70ff098011679d1407b68de5997" + integrity sha512-kj42+TtwvET7MFqxB3pkKyob0VNmspASlv8Y29vSpzzaOHn8J1fDf6H+8opoIC+Gmvo5NqXUDwq7nxI5aQ0mUQ== dependencies: tslib "^2.3.0" -"@angular/service-worker@17.2.4": - version "17.2.4" - resolved "https://registry.yarnpkg.com/@angular/service-worker/-/service-worker-17.2.4.tgz#4a247029a5c5390da69d26e421814f83546553aa" - integrity sha512-D9itcdOz8Y4r/WncfmPZlaHHe2kPjMEuSkky36OSvFFP8RXhvuu4iCPYwYvhJYILxKZq++usKZSkPtuETlyKhQ== +"@angular/service-worker@17.3.3": + version "17.3.3" + resolved "https://registry.yarnpkg.com/@angular/service-worker/-/service-worker-17.3.3.tgz#cd8c83793771e93e0a98192428d956d92e9cb487" + integrity sha512-ZS7MPNPdvIoNKuPfK2pukKiyn+OXVMUALBjSH6k2ayiKxhMiNBCybA4KkQf6DZ9NIlKiGoBc46wf7FZybWAYbQ== dependencies: tslib "^2.3.0" @@ -513,6 +521,27 @@ json5 "^2.2.3" semver "^6.3.1" +"@babel/core@7.24.0": + version "7.24.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.0.tgz#56cbda6b185ae9d9bed369816a8f4423c5f2ff1b" + integrity sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.23.5" + "@babel/generator" "^7.23.6" + "@babel/helper-compilation-targets" "^7.23.6" + "@babel/helper-module-transforms" "^7.23.3" + "@babel/helpers" "^7.24.0" + "@babel/parser" "^7.24.0" + "@babel/template" "^7.24.0" + "@babel/traverse" "^7.24.0" + "@babel/types" "^7.24.0" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.13.16", "@babel/core@^7.20.2": version "7.23.2" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.2.tgz#ed10df0d580fff67c5f3ee70fd22e2e4c90a9f94" @@ -916,6 +945,15 @@ "@babel/traverse" "^7.24.1" "@babel/types" "^7.24.0" +"@babel/helpers@^7.24.0": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.24.4.tgz#dc00907fd0d95da74563c142ef4cd21f2cb856b6" + integrity sha512-FewdlZbSiwaVGlgT1DPANDuCHaDMiOo+D/IDYRFYjHOuv66xMSJ7fQwwODwRNAPkADIO/z1EoF/l2BCWlWABDw== + dependencies: + "@babel/template" "^7.24.0" + "@babel/traverse" "^7.24.1" + "@babel/types" "^7.24.0" + "@babel/highlight@^7.22.13": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54" @@ -1910,6 +1948,16 @@ "@babel/plugin-syntax-object-rest-spread" "^7.8.3" "@babel/plugin-transform-parameters" "^7.23.3" +"@babel/plugin-transform-object-rest-spread@^7.24.0": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.1.tgz#5a3ce73caf0e7871a02e1c31e8b473093af241ff" + integrity sha512-XjD5f0YqOtebto4HGISLNfiNMTTs6tbkFf2TOqJlYKYmbo+mN9Dnpl4SRoofiziuOWMIyq3sZEUqLo3hLITFEA== + dependencies: + "@babel/helper-compilation-targets" "^7.23.6" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-transform-parameters" "^7.24.1" + "@babel/plugin-transform-object-super@^7.18.6", "@babel/plugin-transform-object-super@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz#794a8d2fcb5d0835af722173c1a9d704f44e218c" @@ -1974,6 +2022,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-transform-parameters@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.1.tgz#983c15d114da190506c75b616ceb0f817afcc510" + integrity sha512-8Jl6V24g+Uw5OGPeWNKrKqXPDw2YDjLc53ojwfMcKwlEoETKU9rU0mHUtcg9JntWI/QYzGAXNWEcVHZ+fR+XXg== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/plugin-transform-private-methods@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz#21c8af791f76674420a147ae62e9935d790f8722" @@ -2054,7 +2109,19 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-runtime@7.23.9", "@babel/plugin-transform-runtime@^7.23.2": +"@babel/plugin-transform-runtime@7.24.0": + version "7.24.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.0.tgz#e308fe27d08b74027d42547081eefaf4f2ffbcc9" + integrity sha512-zc0GA5IitLKJrSfXlXmp8KDqLrnGECK7YRfQBmEKg1NmBOQ7e+KuclBEKJgzifQeUYLdNiAw4B4bjyvzWVLiSA== + dependencies: + "@babel/helper-module-imports" "^7.22.15" + "@babel/helper-plugin-utils" "^7.24.0" + babel-plugin-polyfill-corejs2 "^0.4.8" + babel-plugin-polyfill-corejs3 "^0.9.0" + babel-plugin-polyfill-regenerator "^0.5.5" + semver "^6.3.1" + +"@babel/plugin-transform-runtime@^7.23.2": version "7.23.9" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.9.tgz#2c64d0680fc8e09e1dfe8fd5c646fe72abd82004" integrity sha512-A7clW3a0aSjm3ONU9o2HAILSegJCYlEZmOhmBRReVtIpY/Z/p7yIZ+wR41Z+UipwdGuqwtID/V/dOdZXjwi9gQ== @@ -2220,14 +2287,14 @@ "@babel/helper-create-regexp-features-plugin" "^7.22.15" "@babel/helper-plugin-utils" "^7.22.5" -"@babel/preset-env@7.23.9": - version "7.23.9" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.23.9.tgz#beace3b7994560ed6bf78e4ae2073dff45387669" - integrity sha512-3kBGTNBBk9DQiPoXYS0g0BYlwTQYUTifqgKTjxUwEUkduRT2QOa0FPGBJ+NROQhGyYO5BuTJwGvBnqKDykac6A== +"@babel/preset-env@7.24.0": + version "7.24.0" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.24.0.tgz#11536a7f4b977294f0bdfad780f01a8ac8e183fc" + integrity sha512-ZxPEzV9IgvGn73iK0E6VB9/95Nd7aMFpbE0l8KQFDG70cOV9IxRP7Y2FUPmlK0v6ImlLqYX50iuZ3ZTVhOF2lA== dependencies: "@babel/compat-data" "^7.23.5" "@babel/helper-compilation-targets" "^7.23.6" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/helper-validator-option" "^7.23.5" "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.23.3" "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.23.3" @@ -2280,7 +2347,7 @@ "@babel/plugin-transform-new-target" "^7.23.3" "@babel/plugin-transform-nullish-coalescing-operator" "^7.23.4" "@babel/plugin-transform-numeric-separator" "^7.23.4" - "@babel/plugin-transform-object-rest-spread" "^7.23.4" + "@babel/plugin-transform-object-rest-spread" "^7.24.0" "@babel/plugin-transform-object-super" "^7.23.3" "@babel/plugin-transform-optional-catch-binding" "^7.23.4" "@babel/plugin-transform-optional-chaining" "^7.23.4" @@ -2636,10 +2703,10 @@ resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== -"@babel/runtime@7.23.9": - version "7.23.9" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.9.tgz#47791a15e4603bb5f905bc0753801cf21d6345f7" - integrity sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw== +"@babel/runtime@7.24.0": + version "7.24.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.0.tgz#584c450063ffda59697021430cb47101b085951e" + integrity sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw== dependencies: regenerator-runtime "^0.14.0" @@ -2725,7 +2792,7 @@ debug "^4.3.1" globals "^11.1.0" -"@babel/traverse@^7.24.1": +"@babel/traverse@^7.24.0", "@babel/traverse@^7.24.1": version "7.24.1" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.1.tgz#d65c36ac9dd17282175d1e4a3c49d5b7988f530c" integrity sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ== @@ -2942,10 +3009,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz#d1bc06aedb6936b3b6d313bf809a5a40387d2b7f" integrity sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA== -"@esbuild/aix-ppc64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.0.tgz#509621cca4e67caf0d18561a0c56f8b70237472f" - integrity sha512-fGFDEctNh0CcSwsiRPxiaqX0P5rq+AqE0SRhYGZ4PX46Lg1FNR6oCxJghf8YgY0WQEgQuh3lErUFE4KxLeRmmw== +"@esbuild/aix-ppc64@0.20.1": + version "0.20.1" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.1.tgz#eafa8775019b3650a77e8310ba4dbd17ca7af6d5" + integrity sha512-m55cpeupQ2DbuRGQMMZDzbv9J9PgVelPjlcmM5kxHnrBdBx6REaEd7LamYV7Dm8N7rCyR/XwU6rVP8ploKtIkA== "@esbuild/aix-ppc64@0.20.2": version "0.20.2" @@ -2967,10 +3034,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz#7ad65a36cfdb7e0d429c353e00f680d737c2aed4" integrity sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA== -"@esbuild/android-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.0.tgz#109a6fdc4a2783fc26193d2687827045d8fef5ab" - integrity sha512-aVpnM4lURNkp0D3qPoAzSG92VXStYmoVPOgXveAUoQBWRSuQzt51yvSju29J6AHPmwY1BjH49uR29oyfH1ra8Q== +"@esbuild/android-arm64@0.20.1": + version "0.20.1" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.1.tgz#68791afa389550736f682c15b963a4f37ec2f5f6" + integrity sha512-hCnXNF0HM6AjowP+Zou0ZJMWWa1VkD77BXe959zERgGJBBxB+sV+J9f/rcjeg2c5bsukD/n17RKWXGFCO5dD5A== "@esbuild/android-arm64@0.20.2": version "0.20.2" @@ -2992,10 +3059,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.12.tgz#b0c26536f37776162ca8bde25e42040c203f2824" integrity sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w== -"@esbuild/android-arm@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.0.tgz#1397a2c54c476c4799f9b9073550ede496c94ba5" - integrity sha512-3bMAfInvByLHfJwYPJRlpTeaQA75n8C/QKpEaiS4HrFWFiJlNI0vzq/zCjBrhAYcPyVPG7Eo9dMrcQXuqmNk5g== +"@esbuild/android-arm@0.20.1": + version "0.20.1" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.1.tgz#38c91d8ee8d5196f7fbbdf4f0061415dde3a473a" + integrity sha512-4j0+G27/2ZXGWR5okcJi7pQYhmkVgb4D7UKwxcqrjhvp5TKWx3cUjgB1CGj1mfdmJBQ9VnUGgUhign+FPF2Zgw== "@esbuild/android-arm@0.20.2": version "0.20.2" @@ -3017,10 +3084,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.12.tgz#cb13e2211282012194d89bf3bfe7721273473b3d" integrity sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew== -"@esbuild/android-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.0.tgz#2b615abefb50dc0a70ac313971102f4ce2fdb3ca" - integrity sha512-uK7wAnlRvjkCPzh8jJ+QejFyrP8ObKuR5cBIsQZ+qbMunwR8sbd8krmMbxTLSrDhiPZaJYKQAU5Y3iMDcZPhyQ== +"@esbuild/android-x64@0.20.1": + version "0.20.1" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.1.tgz#93f6190ce997b313669c20edbf3645fc6c8d8f22" + integrity sha512-MSfZMBoAsnhpS+2yMFYIQUPs8Z19ajwfuaSZx+tSl09xrHZCjbeXXMsUF/0oq7ojxYEpsSo4c0SfjxOYXRbpaA== "@esbuild/android-x64@0.20.2": version "0.20.2" @@ -3042,10 +3109,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz#cbee41e988020d4b516e9d9e44dd29200996275e" integrity sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g== -"@esbuild/darwin-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.0.tgz#5c122ed799eb0c35b9d571097f77254964c276a2" - integrity sha512-AjEcivGAlPs3UAcJedMa9qYg9eSfU6FnGHJjT8s346HSKkrcWlYezGE8VaO2xKfvvlZkgAhyvl06OJOxiMgOYQ== +"@esbuild/darwin-arm64@0.20.1": + version "0.20.1" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.1.tgz#0d391f2e81fda833fe609182cc2fbb65e03a3c46" + integrity sha512-Ylk6rzgMD8klUklGPzS414UQLa5NPXZD5tf8JmQU8GQrj6BrFA/Ic9tb2zRe1kOZyCbGl+e8VMbDRazCEBqPvA== "@esbuild/darwin-arm64@0.20.2": version "0.20.2" @@ -3067,10 +3134,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz#e37d9633246d52aecf491ee916ece709f9d5f4cd" integrity sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A== -"@esbuild/darwin-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.0.tgz#9561d277002ba8caf1524f209de2b22e93d170c1" - integrity sha512-bsgTPoyYDnPv8ER0HqnJggXK6RyFy4PH4rtsId0V7Efa90u2+EifxytE9pZnsDgExgkARy24WUQGv9irVbTvIw== +"@esbuild/darwin-x64@0.20.1": + version "0.20.1" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.1.tgz#92504077424584684862f483a2242cfde4055ba2" + integrity sha512-pFIfj7U2w5sMp52wTY1XVOdoxw+GDwy9FsK3OFz4BpMAjvZVs0dT1VXs8aQm22nhwoIWUmIRaE+4xow8xfIDZA== "@esbuild/darwin-x64@0.20.2": version "0.20.2" @@ -3092,10 +3159,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz#1ee4d8b682ed363b08af74d1ea2b2b4dbba76487" integrity sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA== -"@esbuild/freebsd-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.0.tgz#84178986a3138e8500d17cc380044868176dd821" - integrity sha512-kQ7jYdlKS335mpGbMW5tEe3IrQFIok9r84EM3PXB8qBFJPSc6dpWfrtsC/y1pyrz82xfUIn5ZrnSHQQsd6jebQ== +"@esbuild/freebsd-arm64@0.20.1": + version "0.20.1" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.1.tgz#a1646fa6ba87029c67ac8a102bb34384b9290774" + integrity sha512-UyW1WZvHDuM4xDz0jWun4qtQFauNdXjXOtIy7SYdf7pbxSWWVlqhnR/T2TpX6LX5NI62spt0a3ldIIEkPM6RHw== "@esbuild/freebsd-arm64@0.20.2": version "0.20.2" @@ -3117,10 +3184,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz#37a693553d42ff77cd7126764b535fb6cc28a11c" integrity sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg== -"@esbuild/freebsd-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.0.tgz#3f9ce53344af2f08d178551cd475629147324a83" - integrity sha512-uG8B0WSepMRsBNVXAQcHf9+Ko/Tr+XqmK7Ptel9HVmnykupXdS4J7ovSQUIi0tQGIndhbqWLaIL/qO/cWhXKyQ== +"@esbuild/freebsd-x64@0.20.1": + version "0.20.1" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.1.tgz#41c9243ab2b3254ea7fb512f71ffdb341562e951" + integrity sha512-itPwCw5C+Jh/c624vcDd9kRCCZVpzpQn8dtwoYIt2TJF3S9xJLiRohnnNrKwREvcZYx0n8sCSbvGH349XkcQeg== "@esbuild/freebsd-x64@0.20.2": version "0.20.2" @@ -3142,10 +3209,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz#be9b145985ec6c57470e0e051d887b09dddb2d4b" integrity sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA== -"@esbuild/linux-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.0.tgz#24efa685515689df4ecbc13031fa0a9dda910a11" - integrity sha512-uTtyYAP5veqi2z9b6Gr0NUoNv9F/rOzI8tOD5jKcCvRUn7T60Bb+42NDBCWNhMjkQzI0qqwXkQGo1SY41G52nw== +"@esbuild/linux-arm64@0.20.1": + version "0.20.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.1.tgz#f3c1e1269fbc9eedd9591a5bdd32bf707a883156" + integrity sha512-cX8WdlF6Cnvw/DO9/X7XLH2J6CkBnz7Twjpk56cshk9sjYVcuh4sXQBy5bmTwzBjNVZze2yaV1vtcJS04LbN8w== "@esbuild/linux-arm64@0.20.2": version "0.20.2" @@ -3167,10 +3234,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz#207ecd982a8db95f7b5279207d0ff2331acf5eef" integrity sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w== -"@esbuild/linux-arm@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.0.tgz#6b586a488e02e9b073a75a957f2952b3b6e87b4c" - integrity sha512-2ezuhdiZw8vuHf1HKSf4TIk80naTbP9At7sOqZmdVwvvMyuoDiZB49YZKLsLOfKIr77+I40dWpHVeY5JHpIEIg== +"@esbuild/linux-arm@0.20.1": + version "0.20.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.1.tgz#4503ca7001a8ee99589c072801ce9d7540717a21" + integrity sha512-LojC28v3+IhIbfQ+Vu4Ut5n3wKcgTu6POKIHN9Wpt0HnfgUGlBuyDDQR4jWZUZFyYLiz4RBBBmfU6sNfn6RhLw== "@esbuild/linux-arm@0.20.2": version "0.20.2" @@ -3192,10 +3259,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz#d0d86b5ca1562523dc284a6723293a52d5860601" integrity sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA== -"@esbuild/linux-ia32@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.0.tgz#84ce7864f762708dcebc1b123898a397dea13624" - integrity sha512-c88wwtfs8tTffPaoJ+SQn3y+lKtgTzyjkD8NgsyCtCmtoIC8RDL7PrJU05an/e9VuAke6eJqGkoMhJK1RY6z4w== +"@esbuild/linux-ia32@0.20.1": + version "0.20.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.1.tgz#98c474e3e0cbb5bcbdd8561a6e65d18f5767ce48" + integrity sha512-4H/sQCy1mnnGkUt/xszaLlYJVTz3W9ep52xEefGtd6yXDQbz/5fZE5dFLUgsPdbUOQANcVUa5iO6g3nyy5BJiw== "@esbuild/linux-ia32@0.20.2": version "0.20.2" @@ -3217,10 +3284,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz#9a37f87fec4b8408e682b528391fa22afd952299" integrity sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA== -"@esbuild/linux-loong64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.0.tgz#1922f571f4cae1958e3ad29439c563f7d4fd9037" - integrity sha512-lR2rr/128/6svngnVta6JN4gxSXle/yZEZL3o4XZ6esOqhyR4wsKyfu6qXAL04S4S5CgGfG+GYZnjFd4YiG3Aw== +"@esbuild/linux-loong64@0.20.1": + version "0.20.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.1.tgz#a8097d28d14b9165c725fe58fc438f80decd2f33" + integrity sha512-c0jgtB+sRHCciVXlyjDcWb2FUuzlGVRwGXgI+3WqKOIuoo8AmZAddzeOHeYLtD+dmtHw3B4Xo9wAUdjlfW5yYA== "@esbuild/linux-loong64@0.20.2": version "0.20.2" @@ -3242,10 +3309,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz#4ddebd4e6eeba20b509d8e74c8e30d8ace0b89ec" integrity sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w== -"@esbuild/linux-mips64el@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.0.tgz#7ca1bd9df3f874d18dbf46af009aebdb881188fe" - integrity sha512-9Sycc+1uUsDnJCelDf6ZNqgZQoK1mJvFtqf2MUz4ujTxGhvCWw+4chYfDLPepMEvVL9PDwn6HrXad5yOrNzIsQ== +"@esbuild/linux-mips64el@0.20.1": + version "0.20.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.1.tgz#c44f6f0d7d017c41ad3bb15bfdb69b690656b5ea" + integrity sha512-TgFyCfIxSujyuqdZKDZ3yTwWiGv+KnlOeXXitCQ+trDODJ+ZtGOzLkSWngynP0HZnTsDyBbPy7GWVXWaEl6lhA== "@esbuild/linux-mips64el@0.20.2": version "0.20.2" @@ -3267,10 +3334,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz#adb67dadb73656849f63cd522f5ecb351dd8dee8" integrity sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg== -"@esbuild/linux-ppc64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.0.tgz#8f95baf05f9486343bceeb683703875d698708a4" - integrity sha512-CoWSaaAXOZd+CjbUTdXIJE/t7Oz+4g90A3VBCHLbfuc5yUQU/nFDLOzQsN0cdxgXd97lYW/psIIBdjzQIwTBGw== +"@esbuild/linux-ppc64@0.20.1": + version "0.20.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.1.tgz#0765a55389a99237b3c84227948c6e47eba96f0d" + integrity sha512-b+yuD1IUeL+Y93PmFZDZFIElwbmFfIKLKlYI8M6tRyzE6u7oEP7onGk0vZRh8wfVGC2dZoy0EqX1V8qok4qHaw== "@esbuild/linux-ppc64@0.20.2": version "0.20.2" @@ -3292,10 +3359,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz#11bc0698bf0a2abf8727f1c7ace2112612c15adf" integrity sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg== -"@esbuild/linux-riscv64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.0.tgz#ca63b921d5fe315e28610deb0c195e79b1a262ca" - integrity sha512-mlb1hg/eYRJUpv8h/x+4ShgoNLL8wgZ64SUr26KwglTYnwAWjkhR2GpoKftDbPOCnodA9t4Y/b68H4J9XmmPzA== +"@esbuild/linux-riscv64@0.20.1": + version "0.20.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.1.tgz#e4153b032288e3095ddf4c8be07893781b309a7e" + integrity sha512-wpDlpE0oRKZwX+GfomcALcouqjjV8MIX8DyTrxfyCfXxoKQSDm45CZr9fanJ4F6ckD4yDEPT98SrjvLwIqUCgg== "@esbuild/linux-riscv64@0.20.2": version "0.20.2" @@ -3317,10 +3384,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz#e86fb8ffba7c5c92ba91fc3b27ed5a70196c3cc8" integrity sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg== -"@esbuild/linux-s390x@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.0.tgz#cb3d069f47dc202f785c997175f2307531371ef8" - integrity sha512-fgf9ubb53xSnOBqyvWEY6ukBNRl1mVX1srPNu06B6mNsNK20JfH6xV6jECzrQ69/VMiTLvHMicQR/PgTOgqJUQ== +"@esbuild/linux-s390x@0.20.1": + version "0.20.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.1.tgz#b9ab8af6e4b73b26d63c1c426d7669a5d53eb5a7" + integrity sha512-5BepC2Au80EohQ2dBpyTquqGCES7++p7G+7lXe1bAIvMdXm4YYcEfZtQrP4gaoZ96Wv1Ute61CEHFU7h4FMueQ== "@esbuild/linux-s390x@0.20.2": version "0.20.2" @@ -3342,10 +3409,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz#5f37cfdc705aea687dfe5dfbec086a05acfe9c78" integrity sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg== -"@esbuild/linux-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.0.tgz#ac617e0dc14e9758d3d7efd70288c14122557dc7" - integrity sha512-H9Eu6MGse++204XZcYsse1yFHmRXEWgadk2N58O/xd50P9EvFMLJTQLg+lB4E1cF2xhLZU5luSWtGTb0l9UeSg== +"@esbuild/linux-x64@0.20.1": + version "0.20.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.1.tgz#0b25da17ac38c3e11cdd06ca3691d4d6bef2755f" + integrity sha512-5gRPk7pKuaIB+tmH+yKd2aQTRpqlf1E4f/mC+tawIm/CGJemZcHZpp2ic8oD83nKgUPMEd0fNanrnFljiruuyA== "@esbuild/linux-x64@0.20.2": version "0.20.2" @@ -3367,10 +3434,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz#29da566a75324e0d0dd7e47519ba2f7ef168657b" integrity sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA== -"@esbuild/netbsd-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.0.tgz#6cc778567f1513da6e08060e0aeb41f82eb0f53c" - integrity sha512-lCT675rTN1v8Fo+RGrE5KjSnfY0x9Og4RN7t7lVrN3vMSjy34/+3na0q7RIfWDAj0e0rCh0OL+P88lu3Rt21MQ== +"@esbuild/netbsd-x64@0.20.1": + version "0.20.1" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.1.tgz#3148e48406cd0d4f7ba1e0bf3f4d77d548c98407" + integrity sha512-4fL68JdrLV2nVW2AaWZBv3XEm3Ae3NZn/7qy2KGAt3dexAgSVT+Hc97JKSZnqezgMlv9x6KV0ZkZY7UO5cNLCg== "@esbuild/netbsd-x64@0.20.2": version "0.20.2" @@ -3392,10 +3459,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz#306c0acbdb5a99c95be98bdd1d47c916e7dc3ff0" integrity sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw== -"@esbuild/openbsd-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.0.tgz#76848bcf76b4372574fb4d06cd0ed1fb29ec0fbe" - integrity sha512-HKoUGXz/TOVXKQ+67NhxyHv+aDSZf44QpWLa3I1lLvAwGq8x1k0T+e2HHSRvxWhfJrFxaaqre1+YyzQ99KixoA== +"@esbuild/openbsd-x64@0.20.1": + version "0.20.1" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.1.tgz#7b73e852986a9750192626d377ac96ac2b749b76" + integrity sha512-GhRuXlvRE+twf2ES+8REbeCb/zeikNqwD3+6S5y5/x+DYbAQUNl0HNBs4RQJqrechS4v4MruEr8ZtAin/hK5iw== "@esbuild/openbsd-x64@0.20.2": version "0.20.2" @@ -3417,10 +3484,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz#0933eaab9af8b9b2c930236f62aae3fc593faf30" integrity sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA== -"@esbuild/sunos-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.0.tgz#ea4cd0639bf294ad51bc08ffbb2dac297e9b4706" - integrity sha512-GDwAqgHQm1mVoPppGsoq4WJwT3vhnz/2N62CzhvApFD1eJyTroob30FPpOZabN+FgCjhG+AgcZyOPIkR8dfD7g== +"@esbuild/sunos-x64@0.20.1": + version "0.20.1" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.1.tgz#402a441cdac2eee98d8be378c7bc23e00c1861c5" + integrity sha512-ZnWEyCM0G1Ex6JtsygvC3KUUrlDXqOihw8RicRuQAzw+c4f1D66YlPNNV3rkjVW90zXVsHwZYWbJh3v+oQFM9Q== "@esbuild/sunos-x64@0.20.2": version "0.20.2" @@ -3442,10 +3509,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz#773bdbaa1971b36db2f6560088639ccd1e6773ae" integrity sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A== -"@esbuild/win32-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.0.tgz#a5c171e4a7f7e4e8be0e9947a65812c1535a7cf0" - integrity sha512-0vYsP8aC4TvMlOQYozoksiaxjlvUcQrac+muDqj1Fxy6jh9l9CZJzj7zmh8JGfiV49cYLTorFLxg7593pGldwQ== +"@esbuild/win32-arm64@0.20.1": + version "0.20.1" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.1.tgz#36c4e311085806a6a0c5fc54d1ac4d7b27e94d7b" + integrity sha512-QZ6gXue0vVQY2Oon9WyLFCdSuYbXSoxaZrPuJ4c20j6ICedfsDilNPYfHLlMH7vGfU5DQR0czHLmJvH4Nzis/A== "@esbuild/win32-arm64@0.20.2": version "0.20.2" @@ -3467,10 +3534,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz#000516cad06354cc84a73f0943a4aa690ef6fd67" integrity sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ== -"@esbuild/win32-ia32@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.0.tgz#f8ac5650c412d33ea62d7551e0caf82da52b7f85" - integrity sha512-p98u4rIgfh4gdpV00IqknBD5pC84LCub+4a3MO+zjqvU5MVXOc3hqR2UgT2jI2nh3h8s9EQxmOsVI3tyzv1iFg== +"@esbuild/win32-ia32@0.20.1": + version "0.20.1" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.1.tgz#0cf933be3fb9dc58b45d149559fe03e9e22b54fe" + integrity sha512-HzcJa1NcSWTAU0MJIxOho8JftNp9YALui3o+Ny7hCh0v5f90nprly1U3Sj1Ldj/CvKKdvvFsCRvDkpsEMp4DNw== "@esbuild/win32-ia32@0.20.2": version "0.20.2" @@ -3492,10 +3559,10 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz#c57c8afbb4054a3ab8317591a0b7320360b444ae" integrity sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA== -"@esbuild/win32-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.0.tgz#2efddf82828aac85e64cef62482af61c29561bee" - integrity sha512-NgJnesu1RtWihtTtXGFMU5YSE6JyyHPMxCwBZK7a6/8d31GuSo9l0Ss7w1Jw5QnKUawG6UEehs883kcXf5fYwg== +"@esbuild/win32-x64@0.20.1": + version "0.20.1" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.1.tgz#77583b6ea54cee7c1410ebbd54051b6a3fcbd8ba" + integrity sha512-0MBh53o6XtI6ctDnRMeQ+xoCN8kD2qI1rY1KgF/xdWQwoFeKou7puvDfV8/Wv4Ctx2rRpET/gGdz3YlNtNACSA== "@esbuild/win32-x64@0.20.2": version "0.20.2" @@ -3539,11 +3606,6 @@ resolved "https://registry.yarnpkg.com/@fal-works/esbuild-plugin-global-externals/-/esbuild-plugin-global-externals-2.1.2.tgz#c05ed35ad82df8e6ac616c68b92c2282bd083ba4" integrity sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ== -"@fastify/busboy@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.0.0.tgz#f22824caff3ae506b18207bad4126dbc6ccdb6b8" - integrity sha512-JUFJad5lv7jxj926GPgymrWQxxjPYuJNiNjNMzqT+HiuP6Vl3dk5xzG+8sTX96np0ZAluvaMzPsjhHZ5rNuNQQ== - "@floating-ui/core@^1.4.2": version "1.5.0" resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.5.0.tgz#5c05c60d5ae2d05101c3021c1a2a350ddc027f8c" @@ -4796,10 +4858,10 @@ dependencies: tslib "2.6.1" -"@ngtools/webpack@17.2.3": - version "17.2.3" - resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-17.2.3.tgz#d1e06039407487dfcca1f136ca036c1f4c6eba40" - integrity sha512-+d5Q7/ctDHePYZXcg0GFwL/AbyEkPMHoCiT7pmLI0B0n87D/mYKK/qmVN1VANBrFLTuIe8RtcL0aJ9pw8HAxWA== +"@ngtools/webpack@17.3.3": + version "17.3.3" + resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-17.3.3.tgz#da62af790e2d7280fe8b03f5dbff343580ffc0f0" + integrity sha512-053KMbg1Tb+Mmg4Htsv8yTpI7ABghguoxhwosQXKB0CjO6M0oexuvdaxbRDQ1vd5xYNOW9LcOfxOMPIwyU4BBA== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -4885,98 +4947,98 @@ read-package-json-fast "^3.0.0" which "^4.0.0" -"@nrwl/angular@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nrwl/angular/-/angular-18.1.2.tgz#8c46b9040e62505d939e57cbd378f060a53fe1f1" - integrity sha512-qgzWh4ku3bV+v2ZVFbNmGpFe9xGCxYPKDPE7YKpGKjBYb+5rrFjITkbR4TD0B3mgI7iOYX6ZdkYrPjpmCsI1kA== +"@nrwl/angular@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nrwl/angular/-/angular-18.2.3.tgz#eb65c229726c50f00d78e679d5696b0494548a24" + integrity sha512-F0RtvSIH/Qs0ju7VcdfBpDeBZvwio2g4KdNpRog9ZBlgJjEmRWu7aA733A2xng96IkJkIrO0DiJaFbNdm8Yelw== dependencies: - "@nx/angular" "18.1.2" + "@nx/angular" "18.2.3" tslib "^2.3.0" -"@nrwl/cypress@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nrwl/cypress/-/cypress-18.1.2.tgz#04be1190e423ded1c9d5175632e83bbf1171d1f1" - integrity sha512-RphBWkdY32h97oQ/9FPw4vbLOld9MY65osrb47OKnpBGiqPKSWZOkhxLlgWN0FZV2hGRfvHUKHCC1ZmKHi6fCA== +"@nrwl/cypress@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nrwl/cypress/-/cypress-18.2.3.tgz#934f7615f347ece85025a925de2e132a9a924164" + integrity sha512-P44e3hmhXWK6jALoHK8bAVUvIqGP8SCX/GM/e6jVAZIjvAC+L69WTkAXVLgZkiD8WpZegSho47AtaL1D256a/g== dependencies: - "@nx/cypress" "18.1.2" + "@nx/cypress" "18.2.3" -"@nrwl/devkit@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nrwl/devkit/-/devkit-18.1.2.tgz#6ab2b3a85f8af181db48ef1351298633da2b0126" - integrity sha512-x+6UJNeWoDtke1FhEAP6ptDLUPJC/xOJ+Wri6RFTi+/ekw7qD3Bj73XHU9C47HBxMxN2voUVMfIX3mC65/CXiQ== +"@nrwl/devkit@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nrwl/devkit/-/devkit-18.2.3.tgz#4de5589b99c6e5a15da334630079d027a59fdd4c" + integrity sha512-BJQdPmXFze7g4zsHhwSTssAcm/hvl0rXbIzZYQxncsVU4d+Fx0GS3JYBZ+9EcfnCeAEb10jGvn7Rfk+0okMmOw== dependencies: - "@nx/devkit" "18.1.2" + "@nx/devkit" "18.2.3" -"@nrwl/eslint-plugin-nx@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nrwl/eslint-plugin-nx/-/eslint-plugin-nx-18.1.2.tgz#d1cd83547cb6d825a6661ded008f0bef0f09bdd1" - integrity sha512-cmTMpG09avCHbbrpHE2rld4o+GEUX6Q7URh51QvKbeIeBvT77uEbin7EzptWJNjN4Ht9hKEAiQkZPideualWeA== +"@nrwl/eslint-plugin-nx@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nrwl/eslint-plugin-nx/-/eslint-plugin-nx-18.2.3.tgz#f66513f8a2bbb63f3555129f8d8236a0936d7573" + integrity sha512-hwIqxp2A0G250tFzEDmVbhwhtldoB7858848AME99Nt9Ij27ThzYaLIG+TYicmb+Rq2FqG9G/6wS89eg1aAg2Q== dependencies: - "@nx/eslint-plugin" "18.1.2" + "@nx/eslint-plugin" "18.2.3" -"@nrwl/jest@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nrwl/jest/-/jest-18.1.2.tgz#f2e0a4ac17cc042075986aa9fe92ea4bab7ef864" - integrity sha512-G+Zr/MDS3k1Bg0Pmv2YWlqBhpaZq38W7GdSci4DEkdQMBZtHhoObTrAfnRXwwP5Zsh5FAXltfTdAkWn8e6lQtg== +"@nrwl/jest@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nrwl/jest/-/jest-18.2.3.tgz#f603d310729b216788d00d70ad0c00193f8bc430" + integrity sha512-64Ebl6GYOQ8AWp/MpPx8qXwKla3x0wnVGjJK4gUGOJ9ShMQnzd4hgYLpVuB4Des7QSxXoict3YVOVocY0joNow== dependencies: - "@nx/jest" "18.1.2" + "@nx/jest" "18.2.3" -"@nrwl/js@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nrwl/js/-/js-18.1.2.tgz#3b4d0db7d964c2bf76569e6521bb313bace58bbd" - integrity sha512-BTxmaF73TB9Ym8MyXUfFjeS3kyw/elORrSrEu6b4ei1Q/DszEpZHhvavN1nUebqJe3RifW9IKL2TFblSoIWCTg== +"@nrwl/js@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nrwl/js/-/js-18.2.3.tgz#f7aa58ae957403215ca2507c6740245796a27d3e" + integrity sha512-fOpKQg7CvzOmcow9fbBc5l96Pbv8gTe9qba4jiw3Z+EH776qraNBL9pRpff653V+obVh//gkq84BUeoJgk8vzQ== dependencies: - "@nx/js" "18.1.2" + "@nx/js" "18.2.3" -"@nrwl/nest@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nrwl/nest/-/nest-18.1.2.tgz#1bd2a8637f3b56b2074b54246669817c7512b310" - integrity sha512-UD1j1r2hpH3O78Yad1dAcInYpvX7NPGnrwXvJqojyOqT5A/GqXxKKnpTsE5IZsNwdFfIQXRGY4QjacEsBuZf5Q== +"@nrwl/nest@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nrwl/nest/-/nest-18.2.3.tgz#c4544725ab375f1189e719b50410d38c36bcec95" + integrity sha512-hQqh3kGZ6Ky7zHzyRDNnzZ57Egn7w96IEfABsT2nnpej0LPWebryPefGSAI4Afgy2q8jSUFj9Bgjf6j93Ogiqg== dependencies: - "@nx/nest" "18.1.2" + "@nx/nest" "18.2.3" -"@nrwl/node@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nrwl/node/-/node-18.1.2.tgz#112b9653447d69eef041a79f6302e869a2b3c320" - integrity sha512-1Z9ZMOdzw8v7sOaOUaQnJr3Aclqchpg1Thj9mqslu/FZeXlp7pngeT4hPvt3FqFbLvxZIH5u3uxQjFNW4QKNxQ== +"@nrwl/node@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nrwl/node/-/node-18.2.3.tgz#fff8c16005c41c8c7d3fe6b7ee4a1b4446e44461" + integrity sha512-cQWAAtG+AexiWsU+5DRfEvbHhU0vkcvhdD3yUG8HjcMjc7eHS82PPxWUk/EzEI/3+3fYcHhSw5F/WyqaVkYJaw== dependencies: - "@nx/node" "18.1.2" + "@nx/node" "18.2.3" -"@nrwl/storybook@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nrwl/storybook/-/storybook-18.1.2.tgz#f9cc07ca9ec2a89cb1729f8b2bf50fbc01f18882" - integrity sha512-inItJAeJUw0DnOkH/cp7JuASp0ndO+4xvJVAD26fEWzYB3pVdM77FIow4BSYXV0P10h7c9K7R9X8KdGXdEbSkg== +"@nrwl/storybook@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nrwl/storybook/-/storybook-18.2.3.tgz#340b10b845dfa373c0513fba40213631e6d0bf51" + integrity sha512-+kZYFSKtZp4+qDav2Z9+xGtRoCiYFE7toKSWNO+7qBmuEm+noCppgxyJ4dv92gc2DR8FSWPrHKp5x+Km0dwJYQ== dependencies: - "@nx/storybook" "18.1.2" + "@nx/storybook" "18.2.3" -"@nrwl/tao@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nrwl/tao/-/tao-18.1.2.tgz#5a05befc9260cae8c7f38ec83669f4ffbfd83f3b" - integrity sha512-IA+osZ5TlKMwJmcP7TECW7TO0JdNNQud9Dgkh1ZfJ4GWnT7WEkE9b2Yf1IFeeB81kCTXXq8jfISa8ZY21MjRaQ== +"@nrwl/tao@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nrwl/tao/-/tao-18.2.3.tgz#c29abf614c23b404d30340326fb52d33822815c0" + integrity sha512-vmteqzGcKPbexaAVPb/7VfXI5dXxzZwSm3rem3z20QlDOmNh1545VLO9YEfT5xzmZT2CC7F0etR4KcrJLtoT5g== dependencies: - nx "18.1.2" + nx "18.2.3" tslib "^2.3.0" -"@nrwl/web@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nrwl/web/-/web-18.1.2.tgz#58cefe472c638f368207c29cf57e6e5db6cb1ad5" - integrity sha512-mu8OW+wFMYgLB2R4X8gzdxLhREXJ5CIRvkS1xFmf91/pMru2kiBpiGrX3+6lvM4RcMFaY4YZiKpVp09JOeCQwQ== +"@nrwl/web@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nrwl/web/-/web-18.2.3.tgz#15e43dd727ba06df111edf16c7f74f915600e41d" + integrity sha512-nlucIDaMUOAG5xOJTCzvKHhCAxergYUrfi2XgPYjg4mu+ApqJxI7HO6FVXZazdivEwNy++BLErjmOjMb52iaVw== dependencies: - "@nx/web" "18.1.2" + "@nx/web" "18.2.3" -"@nrwl/webpack@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nrwl/webpack/-/webpack-18.1.2.tgz#f5152df6b9311afb90cc559414f15d5f363c096a" - integrity sha512-ivFdNTnvLAHwWvF/83hPlBhuSvVXEblS4l4OkPRVw8kG7DjEAfvY5Zb1GsqsThTMAgtXPEVdqzofAskFr+xKPw== +"@nrwl/webpack@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nrwl/webpack/-/webpack-18.2.3.tgz#d3443ed4478e87defb5be209a335d71b91ccce27" + integrity sha512-ygkYzE+ihhFBuMmOuOkGse0Aas0CSbJ8uLth8UumnapU1euBTP5blhp+y6HRCRT8SzfGzc0qJ+M8YLb6E/DvMQ== dependencies: - "@nx/webpack" "18.1.2" + "@nx/webpack" "18.2.3" -"@nrwl/workspace@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nrwl/workspace/-/workspace-18.1.2.tgz#bae0bc66b4f1a071e77d158e5fded72da41a67ec" - integrity sha512-8nI5KxGAr30QBwXlQpMiIr+MdmGNdYxBU0HikqQP3RPk97+y6g/O6He2cOGZFFN5hDbeuQ/R15hyGtPLYS9jLg== +"@nrwl/workspace@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nrwl/workspace/-/workspace-18.2.3.tgz#811b3778b3222be835248eb366de0b9a98f3843e" + integrity sha512-5tVtui/iy+VZTk3x/eFj21Zm0ICPUre9CfB5jlJ2MwH8w+96+186Yt2XGJATkFfnVnjqnszOcjk5BLlra8fdLA== dependencies: - "@nx/workspace" "18.1.2" + "@nx/workspace" "18.2.3" "@nuxtjs/opencollective@0.3.2": version "0.3.2" @@ -4987,51 +5049,51 @@ consola "^2.15.0" node-fetch "^2.6.1" -"@nx/angular@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nx/angular/-/angular-18.1.2.tgz#75e7c3c24510b0f7727a137bb0bc4d8e572dc7d9" - integrity sha512-ES9GtbIPmnz5W6Q9fZ+CkwyIRVjGkRd8Ee5eSwi0nRZDeCPSar+k9+52bpzW8VV5LbCQZKEGS8NjRhV0l+/9jw== - dependencies: - "@nrwl/angular" "18.1.2" - "@nx/devkit" "18.1.2" - "@nx/eslint" "18.1.2" - "@nx/js" "18.1.2" - "@nx/web" "18.1.2" - "@nx/webpack" "18.1.2" - "@nx/workspace" "18.1.2" +"@nx/angular@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nx/angular/-/angular-18.2.3.tgz#564c304c253a268c87d2cb751655691f471e98f7" + integrity sha512-oSp4cNlyCUd03SNoa+x5zjrnqBjxANInB0+vsGD5VlOewpfkVNWPb7TIRa+JyOFXWSWblF0LNEiLxWnxY+27gw== + dependencies: + "@nrwl/angular" "18.2.3" + "@nx/devkit" "18.2.3" + "@nx/eslint" "18.2.3" + "@nx/js" "18.2.3" + "@nx/web" "18.2.3" + "@nx/webpack" "18.2.3" + "@nx/workspace" "18.2.3" "@phenomnomnominal/tsquery" "~5.0.1" - "@typescript-eslint/type-utils" "^6.9.1" + "@typescript-eslint/type-utils" "^7.3.0" chalk "^4.1.0" find-cache-dir "^3.3.2" ignore "^5.0.4" magic-string "~0.30.2" minimatch "9.0.3" - piscina "^4.2.1" + piscina "^4.4.0" semver "^7.5.3" tslib "^2.3.0" webpack "^5.80.0" webpack-merge "^5.8.0" -"@nx/cypress@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nx/cypress/-/cypress-18.1.2.tgz#9cef040c511eeb16fa8109bcc4e6c38bc8492e5d" - integrity sha512-JMdpGHWhL0Iv+Eod1tIT1LVSTZfQmp40sWpH7Jri7WCoEw3vh4F4//1Az84Irs+QKDXUPX/eV1avbJY+2HIZqQ== +"@nx/cypress@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nx/cypress/-/cypress-18.2.3.tgz#fe32cbb21d592fbe0c62f1ee47abc5ffc232bd23" + integrity sha512-TY5LC4cXFAMq3hrIQDTKYwGgNVDWCTF6i22gaaMlTayowfSWcEug5FHfBGXzpvYR4Q5Snci988krI1yN/6Fbfw== dependencies: - "@nrwl/cypress" "18.1.2" - "@nx/devkit" "18.1.2" - "@nx/eslint" "18.1.2" - "@nx/js" "18.1.2" + "@nrwl/cypress" "18.2.3" + "@nx/devkit" "18.2.3" + "@nx/eslint" "18.2.3" + "@nx/js" "18.2.3" "@phenomnomnominal/tsquery" "~5.0.1" detect-port "^1.5.1" semver "^7.5.3" tslib "^2.3.0" -"@nx/devkit@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nx/devkit/-/devkit-18.1.2.tgz#5d1d83bd10caedb0797ee940d61e03d546221405" - integrity sha512-xgiPqKdJ6GVrqXsAyHD/yxqCDW1LekkWgazkuBI8MKA5J2IwZ4Ex5pMsOVMuWz2sTRejuPRqajBclFRMbhfCig== +"@nx/devkit@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nx/devkit/-/devkit-18.2.3.tgz#81788e9d018772414ddad0f1aba7ce007da570a3" + integrity sha512-dugw9Jm3Og28uwGee94P3KYkqiUV7J8RgibOQjQG4J2Vt3DPBNEGSgBD72qKkzpioEo+XSVUkn9h3GrdmnRU+Q== dependencies: - "@nrwl/devkit" "18.1.2" + "@nrwl/devkit" "18.2.3" ejs "^3.1.7" enquirer "~2.3.6" ignore "^5.0.4" @@ -5040,44 +5102,44 @@ tslib "^2.3.0" yargs-parser "21.1.1" -"@nx/eslint-plugin@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nx/eslint-plugin/-/eslint-plugin-18.1.2.tgz#4fec9bcd36900007048dacecb33ae418a0ee7e8e" - integrity sha512-enlPiKl/TdW/YTxNmlBvMt4Z6hm/Ozp5R+G9d7w+e82ZwBBaJnsTZYlNGuhFmWP9ZVMCVjivJHe9da0Ea4e7yQ== +"@nx/eslint-plugin@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nx/eslint-plugin/-/eslint-plugin-18.2.3.tgz#99e7e46d8bc32966bdb9c0be7783b7e7bc84a795" + integrity sha512-vOHkzHNpDLLd5RMrL/8/sAdqBqMcf2FrSJWug6W4cC0x8hzUpNwnfEn+i4ZCV/QxduQH4/UP96AbOm7rzwoAdg== dependencies: - "@nrwl/eslint-plugin-nx" "18.1.2" - "@nx/devkit" "18.1.2" - "@nx/js" "18.1.2" - "@typescript-eslint/type-utils" "^6.13.2" - "@typescript-eslint/utils" "^6.13.2" + "@nrwl/eslint-plugin-nx" "18.2.3" + "@nx/devkit" "18.2.3" + "@nx/js" "18.2.3" + "@typescript-eslint/type-utils" "^7.3.0" + "@typescript-eslint/utils" "^7.3.0" chalk "^4.1.0" confusing-browser-globals "^1.0.9" jsonc-eslint-parser "^2.1.0" semver "^7.5.3" tslib "^2.3.0" -"@nx/eslint@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nx/eslint/-/eslint-18.1.2.tgz#f4f0830c890c71f098938129a3f35c215c205166" - integrity sha512-cNCbCg5/qYCXrcBuJaJjy6+aLTDcU9LfxEvuBrA3RdAVqpSj0EjxocrmXwbSZTQt6JDhgraoZqtFRxGZ+44Oww== +"@nx/eslint@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nx/eslint/-/eslint-18.2.3.tgz#714106ffb0e9a7dd35e3248640a073e811eb9c66" + integrity sha512-qr1A3on5tPR3Rxsrg1wlPLVB/L6iFDp+II1xBb/3PBAsddKvPCzPASsogAm0Q3RdqK2JkJrwo/rX3YxwrjZ5cQ== dependencies: - "@nx/devkit" "18.1.2" - "@nx/js" "18.1.2" - "@nx/linter" "18.1.2" + "@nx/devkit" "18.2.3" + "@nx/js" "18.2.3" + "@nx/linter" "18.2.3" eslint "^8.0.0" tslib "^2.3.0" - typescript "~5.3.2" + typescript "~5.4.2" -"@nx/jest@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nx/jest/-/jest-18.1.2.tgz#4ff73bee42c459f22a262355bc249ff93f9a8ab8" - integrity sha512-lCWVAzeN+U5xppqU6kuJaHCdudiSxnVdoYcaW0yf5bx3XYaKIIN+2NTPxRGy/QKibnQwqv3Y5NdIFDt67OYQ4Q== +"@nx/jest@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nx/jest/-/jest-18.2.3.tgz#f36a0671bb64e45961dad4a97c835873a5e297cf" + integrity sha512-AMI/RuTlNz5d0JiBdraCkZ1ABfEyuJkdCvVjo/RDu1NAw1Xv4hI4+5UG7sb/bJoX6n+lK1SS51z+Zb/ab8lQoQ== dependencies: "@jest/reporters" "^29.4.1" "@jest/test-result" "^29.4.1" - "@nrwl/jest" "18.1.2" - "@nx/devkit" "18.1.2" - "@nx/js" "18.1.2" + "@nrwl/jest" "18.2.3" + "@nx/devkit" "18.2.3" + "@nx/js" "18.2.3" "@phenomnomnominal/tsquery" "~5.0.1" chalk "^4.1.0" identity-obj-proxy "3.0.0" @@ -5089,10 +5151,10 @@ tslib "^2.3.0" yargs-parser "21.1.1" -"@nx/js@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nx/js/-/js-18.1.2.tgz#e42c0801ac9edf755dde8c5bbf1501d985250af8" - integrity sha512-bq3goTS6zM6Eg3DNLAz51gB34zHqhYf7LzTFJOGDRogzmEGsttSbX46eiaD4oSpq/s4ybwRe2cHV3ZGodaZL1A== +"@nx/js@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nx/js/-/js-18.2.3.tgz#fd9a1be56fd674f80563a20d90feffc6ab9b830a" + integrity sha512-hFSmgyaMVIlN/SyFwOwn/IveHsGxxJOv7qhewACg9NlKOa6+eEJYlEbOik9LjvcosDOh5icrngjsFgFJoC1sWA== dependencies: "@babel/core" "^7.23.2" "@babel/plugin-proposal-decorators" "^7.22.7" @@ -5101,9 +5163,9 @@ "@babel/preset-env" "^7.23.2" "@babel/preset-typescript" "^7.22.5" "@babel/runtime" "^7.22.6" - "@nrwl/js" "18.1.2" - "@nx/devkit" "18.1.2" - "@nx/workspace" "18.1.2" + "@nrwl/js" "18.2.3" + "@nx/devkit" "18.2.3" + "@nx/workspace" "18.2.3" "@phenomnomnominal/tsquery" "~5.0.1" babel-plugin-const-enum "^1.0.1" babel-plugin-macros "^2.8.0" @@ -5125,125 +5187,125 @@ tsconfig-paths "^4.1.2" tslib "^2.3.0" -"@nx/linter@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nx/linter/-/linter-18.1.2.tgz#e511c7fb059edce57a54666e5066008106999d76" - integrity sha512-bIGFQwHixXv6BuVLVIc5HYwzlodGxK9HsTp+5RtokCXZ0LIv4jkm0FsNExkBBqlaf4oLLqu0F/IZYfzhTwcCsA== +"@nx/linter@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nx/linter/-/linter-18.2.3.tgz#a2b9bd388955f7872c42b5dfcabb7f308344039b" + integrity sha512-buxqe0N/d5iVWA4zE/jX8xrkCJLyGG2h1bSTrz1oyPvM3SdcWr69JpL8j1wtBvnKo/brDzLbNWsnrUwO9cgSAQ== dependencies: - "@nx/eslint" "18.1.2" + "@nx/eslint" "18.2.3" -"@nx/nest@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nx/nest/-/nest-18.1.2.tgz#63a473739ee90206660d315577f3abc9ec701ff7" - integrity sha512-lhnhTxv2pcKSQYrnpbTW50Xm6O7yPbH4d4VqKDj8p863lU623Wh6SW5scl3YCxwr354i4xITo3Ui212oGDKFog== +"@nx/nest@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nx/nest/-/nest-18.2.3.tgz#24a01ba16207768e80ff42004fa6552b2605f609" + integrity sha512-kBIKF08iB8V4k9RaPsSzawaVIZo/czx+SEaQmS7+fSsVerLji5ZJoFKqSxnDz6ksW1lfkmRpymSdWWHRc/UDKA== dependencies: "@nestjs/schematics" "^9.1.0" - "@nrwl/nest" "18.1.2" - "@nx/devkit" "18.1.2" - "@nx/eslint" "18.1.2" - "@nx/js" "18.1.2" - "@nx/node" "18.1.2" + "@nrwl/nest" "18.2.3" + "@nx/devkit" "18.2.3" + "@nx/eslint" "18.2.3" + "@nx/js" "18.2.3" + "@nx/node" "18.2.3" "@phenomnomnominal/tsquery" "~5.0.1" tslib "^2.3.0" -"@nx/node@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nx/node/-/node-18.1.2.tgz#bf432f38c2c695ff26f42b7191c79665b3b58738" - integrity sha512-xXiA0yNIIXIAC6sD/j1kW4SXFbTsYneyKwl0az0TwQkHAwYSPnUASLbj1tfE/DCpH5s9S0x/A35IqtHNhIMZtw== +"@nx/node@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nx/node/-/node-18.2.3.tgz#5ba965f8163dadb802bd43c9706807efaea750be" + integrity sha512-ryI+xzFLBNlRqLNH5UkHO2ZYIs1u1dUbkf7HTyJ4AOrzH2kuBd7tKfFI/IcDzmhjMOfhDYPC0RV7osOSS5Vdiw== dependencies: - "@nrwl/node" "18.1.2" - "@nx/devkit" "18.1.2" - "@nx/eslint" "18.1.2" - "@nx/jest" "18.1.2" - "@nx/js" "18.1.2" + "@nrwl/node" "18.2.3" + "@nx/devkit" "18.2.3" + "@nx/eslint" "18.2.3" + "@nx/jest" "18.2.3" + "@nx/js" "18.2.3" tslib "^2.3.0" -"@nx/nx-darwin-arm64@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nx/nx-darwin-arm64/-/nx-darwin-arm64-18.1.2.tgz#8c6d020d744e52900b215d180ad29c2873dc4acd" - integrity sha512-KduC9WBmeTLP8HyTg4NOgQGLk9LEd5qd9dGuYKPU0jA4b+eJIa0rRHEjFdc5WulQrcUAvTIKfmScRCgzR96ogg== - -"@nx/nx-darwin-x64@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nx/nx-darwin-x64/-/nx-darwin-x64-18.1.2.tgz#7247cbea93ea2b8c9ad7d22b1f25374454543589" - integrity sha512-mBf3X8m4P4QHoW8g/L/YoK8zkndDyIw4bojLg8Q3xc47s5JZFCqSSMeOXZ9NicM2DpPiDWSQALtQX7A8lIsoAA== - -"@nx/nx-freebsd-x64@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nx/nx-freebsd-x64/-/nx-freebsd-x64-18.1.2.tgz#cddc5e345f100a8fa88ff4e39e0e253f843ef3f7" - integrity sha512-ZqzT2BTsOHhWip1PvNm7AZ4Pzn4I+IZNRvtRgpETYvIH+nqoCmi5rrEi1avnhnr6P5hyzh2mISRSyk186SbZew== - -"@nx/nx-linux-arm-gnueabihf@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-18.1.2.tgz#4a1a5de3bc3ca2d8525259ea5c15ff1ca779cdcb" - integrity sha512-V9Dp9uuuce+/f50dXxaYz1C9ULo5+5VS35yc6gN7b6SchCWjNK+xg1YcHBTRNc2ChBtayO2z+mBQ1s6wMDNs/Q== - -"@nx/nx-linux-arm64-gnu@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-18.1.2.tgz#cbb2624d0d245242a09f90715eaabcfb4c8be804" - integrity sha512-aM860T4Hy2JCLcU56mtARIp1MdT1Ms7cGUQzE+a5irM8ZdaHsPdRnYqIgEKd3hoF6PQ6/piHFXWa4xm7pe/2KA== - -"@nx/nx-linux-arm64-musl@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-18.1.2.tgz#a99fd2898030bca1be14077e2e23ec3122c369c1" - integrity sha512-BgBoOeIgCQ56xii7fKNWiE7UIP/0G+OQhdWJQmh+q6NN0kk78WsdCSq+f7f7LQIji5HiNqUUVx9fd1s6xRSb/w== - -"@nx/nx-linux-x64-gnu@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-18.1.2.tgz#e80bfef4f0cf8243f18dd1aa91b4b47a915e3497" - integrity sha512-WDOjtk+K2Tc9SNjGe+zmyy05VUerZpEQ5kvB6Ude0v/W2bMnmpVrLZwwTF5Yrq0ebbUlXM/9wtc1Zjjc75MU2g== - -"@nx/nx-linux-x64-musl@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-18.1.2.tgz#afd9a93b306c5b8f52385a0b54de2c3573e61834" - integrity sha512-I7jTmbfR5CHC3KVlU3SkqYKJnn25MbH8pdRZJY4gaHnqL9JzbHw9rxddhKBj41lez7jQZTGLnPFUV7JPLXTzKg== - -"@nx/nx-win32-arm64-msvc@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-18.1.2.tgz#490f04af286894a3e1b33fb83cd9ce1466166280" - integrity sha512-KQobKvkrdkmaJmx0Pyt2lzHkNugO0gE7q9F4h22KIECyGW1tC3nSPAB4F3mmdE2KuWKgYG5WLafvzusysLsR7g== - -"@nx/nx-win32-x64-msvc@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-18.1.2.tgz#e6587d71e529cc30376d250da0af3095300c259f" - integrity sha512-uvJvROSwHBwkTOoOPkb56jEsKJjr4LnZ3fCHmEbrtGhAUC0gAUL+dWJUDHoatrGzN+bM2VqrvgNCGkityK96hw== - -"@nx/storybook@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nx/storybook/-/storybook-18.1.2.tgz#dab2c43d2b197cf46fc255301fce5f35774f19d4" - integrity sha512-gRCTvhWdtZK8RiggHoHN3LYI+Go4sfgkawAWBmrGIqBBtuitBvOm/hKJeUM0OdXKq/g6QsK4p4/okDqWOEuYaw== - dependencies: - "@nrwl/storybook" "18.1.2" - "@nx/cypress" "18.1.2" - "@nx/devkit" "18.1.2" - "@nx/eslint" "18.1.2" - "@nx/js" "18.1.2" +"@nx/nx-darwin-arm64@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nx/nx-darwin-arm64/-/nx-darwin-arm64-18.2.3.tgz#4b3f14437e6f5a7233594f63a8d36097e8872af1" + integrity sha512-TEks/vXHE87rNvVqhcIzQOM/+aZvNCf/70PhGG4RBEb+qV0C1kw7nygzdoLI4inFC76Qxhyya/K3J2OnU5ATiw== + +"@nx/nx-darwin-x64@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nx/nx-darwin-x64/-/nx-darwin-x64-18.2.3.tgz#bc546836bcce2293181a315666d4a0ea7b42642a" + integrity sha512-UsBbNbNXj+L2OzPyQYotyzmZF4h+ryaZ8quYDfdnlYwvFeqkdb2QJ3vJRd6in0kMWGrdk/ria/wZMCxR7U1ggg== + +"@nx/nx-freebsd-x64@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nx/nx-freebsd-x64/-/nx-freebsd-x64-18.2.3.tgz#afe920b26385fffb77cbad5435ff97607481c01c" + integrity sha512-f9BXGOeRPhrsNm99TCnOqZZeZUqN1BUOEzWa12eo3u+vQG6Qba3qKn7T92SeEzxOx/mUP/Csv3pFYoY6TE26jA== + +"@nx/nx-linux-arm-gnueabihf@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-18.2.3.tgz#d438dddcfb0fd255b8903214118c304eb89f33ef" + integrity sha512-ekqr5jZhD6PxGM5IbI/RtlERDJ+8HR04OIdfo6HkbwxwCHxZlzZq+ApEZYum4AbjP6cuc3Zd/us1uuDqfQbeHw== + +"@nx/nx-linux-arm64-gnu@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-18.2.3.tgz#a496666a70499d22589dfe0df3d46e738921250b" + integrity sha512-iAW2J8NBFU4zDn5nqRgUq4t7gYC8ALyALzznr97ZvMTQorWfmHYgPUAj/opNqUcr10fjxcmXT0Ux2SX3DgUDmw== + +"@nx/nx-linux-arm64-musl@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-18.2.3.tgz#90744f03377959bdc90cf5a8a2f00afb5b084b70" + integrity sha512-AJjGVHGGew0QVKUL30mjFjafowrSDYSQ1GgkJCLuWef5jl4rFvm9ruZswVja1KfZTFaImTCU01tZjPBr3zhmAA== + +"@nx/nx-linux-x64-gnu@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-18.2.3.tgz#0c0fac195dcf5eaf58a89bbd4764e06c5e338f14" + integrity sha512-nk5Xg8vmbBRoL0fOgZNBl1paC7hmjACLaSBmU7U2X+Y+QPGQzSw2b+Zn1MKVUWDmc4E6VnQfZ8n0L27+r9NgRw== + +"@nx/nx-linux-x64-musl@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-18.2.3.tgz#ba2e290ae3e7e0b43669546591e576c3f7ff2c50" + integrity sha512-bOlhul/eov58k9fX8lltopUDOIBEohZq2qc4ag91W2r4jdp6suAiqfXRxQwNZ2iHd8nAXuCDIHCbUuojs6OZnA== + +"@nx/nx-win32-arm64-msvc@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-18.2.3.tgz#5fc56f77ae98e273b1abb9eaf91492f1a40f2dc6" + integrity sha512-olXer0LnCvJrdV5ynd19fZHvvarRK/p1JnkoOUZDPVV+A3jGQQ8+paz+/5iLQBKA+5VcgWyqAaGFJnpyEFmnoQ== + +"@nx/nx-win32-x64-msvc@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-18.2.3.tgz#91083c549d4cdbaa0098cf1158edce98f1b440d6" + integrity sha512-BgzPjF/wqi7zIFcspcKzN37BX1wgGo0OTLncK2PN5nyzSQ+XeNbR5laDswxzOGdB4CRLPqak2+YMhYnoiXeRCg== + +"@nx/storybook@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nx/storybook/-/storybook-18.2.3.tgz#38a9d6a541113304abf5d918411bf8696ccdb51e" + integrity sha512-6XvgLD2L4+cbEwPneey+mxB7nGUi4l0K+R1AWjijGg14X2IzyCTgs5up56vIAKVuwuXxGWUTuXgSXwfKXINLIg== + dependencies: + "@nrwl/storybook" "18.2.3" + "@nx/cypress" "18.2.3" + "@nx/devkit" "18.2.3" + "@nx/eslint" "18.2.3" + "@nx/js" "18.2.3" "@phenomnomnominal/tsquery" "~5.0.1" semver "^7.5.3" tslib "^2.3.0" -"@nx/web@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nx/web/-/web-18.1.2.tgz#a5e066e3edd651862cef9b3fac923625d8d20d71" - integrity sha512-SUqwXwwFMjtMakCWHb/pMxIRYiRg9GZnvaIF3/iiaRcXSldReIXbP6+weW36SLv9fjxtObV/CiW59AILwkPzHA== +"@nx/web@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nx/web/-/web-18.2.3.tgz#c1d1bf7a77d16fd9281453c1c9e68dea853f0fc0" + integrity sha512-nDiCHinZGfkGWuTL2HLk6tUcKILMYtrtd6c/cE8T+5TnTXNaMrOQFt1GvDa60HGh8xgRgpjzTsGfJFrf90GJcA== dependencies: - "@nrwl/web" "18.1.2" - "@nx/devkit" "18.1.2" - "@nx/js" "18.1.2" + "@nrwl/web" "18.2.3" + "@nx/devkit" "18.2.3" + "@nx/js" "18.2.3" chalk "^4.1.0" detect-port "^1.5.1" http-server "^14.1.0" tslib "^2.3.0" -"@nx/webpack@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nx/webpack/-/webpack-18.1.2.tgz#bb9a65c30644ee017ea6818bcc66edfa67e095d8" - integrity sha512-Mv24F7IlYRqSqsn97+WlpylCpC79v4aQubZRPE/dxx7eRqBMykg1BSy8uDI926ggkr096EUejQkN44pzu7o/gQ== +"@nx/webpack@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nx/webpack/-/webpack-18.2.3.tgz#15b3c4be376f2313abdd64c3b67e04f11fbde1fc" + integrity sha512-0X7HXrF/VP9BrbbYD4Ewr4KnMT85eSaIBr/TvTrUM4v2BXqPgP4NVQhkd6jLlEkWuDlYtAgwFjcx4NGWRqnwbw== dependencies: "@babel/core" "^7.23.2" - "@nrwl/webpack" "18.1.2" - "@nx/devkit" "18.1.2" - "@nx/js" "18.1.2" + "@nrwl/webpack" "18.2.3" + "@nx/devkit" "18.2.3" + "@nx/js" "18.2.3" ajv "^8.12.0" autoprefixer "^10.4.9" babel-loader "^9.1.2" @@ -5278,16 +5340,16 @@ webpack-node-externals "^3.0.0" webpack-subresource-integrity "^5.1.0" -"@nx/workspace@18.1.2": - version "18.1.2" - resolved "https://registry.yarnpkg.com/@nx/workspace/-/workspace-18.1.2.tgz#8bc8f5e31007535e11e6c64c97e68ef31d179903" - integrity sha512-/b7qJqnxdWYfBb0UDgVJLmPv5qN50LbarzGLwJxSIVnlRWH94UOO4HW+W0tcEDNnf0RnFP1zDIysz+qu5CmV0g== +"@nx/workspace@18.2.3": + version "18.2.3" + resolved "https://registry.yarnpkg.com/@nx/workspace/-/workspace-18.2.3.tgz#797b2aef3613ddb30ea4dbef0bd1bde37b3c60e2" + integrity sha512-en3lSArMrHZ75SqMHnnZjXiMunc6QFDMcglNPQwIE8TuXnV8UWQ1e4hkzRo6hY/YOoY7HcFvMEJ5KyP8OWCmQg== dependencies: - "@nrwl/workspace" "18.1.2" - "@nx/devkit" "18.1.2" + "@nrwl/workspace" "18.2.3" + "@nx/devkit" "18.2.3" chalk "^4.1.0" enquirer "~2.3.6" - nx "18.1.2" + nx "18.2.3" tslib "^2.3.0" yargs-parser "21.1.1" @@ -5760,13 +5822,13 @@ dependencies: any-observable "^0.3.0" -"@schematics/angular@17.2.3": - version "17.2.3" - resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-17.2.3.tgz#0e618017da1139ec921071019ba0c0e13446a996" - integrity sha512-rXsYmWC1a8uvGTC6RwICwg1GLLQlTw8jOSqHf6T2AFMzP4p1FV3/GFSGyPIMl9yPwn6JqbmfQy3Bvj0stQNM0Q== +"@schematics/angular@17.3.3": + version "17.3.3" + resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-17.3.3.tgz#d6fe530dd478fe2449d0d0990d083a14e2d6a18e" + integrity sha512-kNlyjIKTBhfi8Jab3MCkxNRbbpErbzdu0lZNSL8Nidmqs6Tk23Dc1bZe4t/gPNOCkCvQlwYa6X88SjC/ntyVng== dependencies: - "@angular-devkit/core" "17.2.3" - "@angular-devkit/schematics" "17.2.3" + "@angular-devkit/core" "17.3.3" + "@angular-devkit/schematics" "17.3.3" jsonc-parser "3.2.1" "@schematics/angular@^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0": @@ -7604,22 +7666,6 @@ "@typescript-eslint/types" "5.62.0" "@typescript-eslint/visitor-keys" "5.62.0" -"@typescript-eslint/scope-manager@6.14.0": - version "6.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.14.0.tgz#53d24363fdb5ee0d1d8cda4ed5e5321272ab3d48" - integrity sha512-VT7CFWHbZipPncAZtuALr9y3EuzY1b1t1AEkIq2bTXUPKw+pHoXflGNG5L+Gv6nKul1cz1VH8fz16IThIU0tdg== - dependencies: - "@typescript-eslint/types" "6.14.0" - "@typescript-eslint/visitor-keys" "6.14.0" - -"@typescript-eslint/scope-manager@6.19.0": - version "6.19.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.19.0.tgz#b6d2abb825b29ab70cb542d220e40c61c1678116" - integrity sha512-dO1XMhV2ehBI6QN8Ufi7I10wmUovmLU0Oru3n5LVlM2JuzB4M+dVphCPLkVpKvGij2j/pHBWuJ9piuXx+BhzxQ== - dependencies: - "@typescript-eslint/types" "6.19.0" - "@typescript-eslint/visitor-keys" "6.19.0" - "@typescript-eslint/scope-manager@6.21.0": version "6.21.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz#ea8a9bfc8f1504a6ac5d59a6df308d3a0630a2b1" @@ -7628,17 +7674,23 @@ "@typescript-eslint/types" "6.21.0" "@typescript-eslint/visitor-keys" "6.21.0" -"@typescript-eslint/type-utils@6.19.0": - version "6.19.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.19.0.tgz#522a494ef0d3e9fdc5e23a7c22c9331bbade0101" - integrity sha512-mcvS6WSWbjiSxKCwBcXtOM5pRkPQ6kcDds/juxcy/727IQr3xMEcwr/YLHW2A2+Fp5ql6khjbKBzOyjuPqGi/w== +"@typescript-eslint/scope-manager@7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.2.0.tgz#cfb437b09a84f95a0930a76b066e89e35d94e3da" + integrity sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg== + dependencies: + "@typescript-eslint/types" "7.2.0" + "@typescript-eslint/visitor-keys" "7.2.0" + +"@typescript-eslint/scope-manager@7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.5.0.tgz#70f0a7361430ab1043a5f97386da2a0d8b2f4d56" + integrity sha512-Z1r7uJY0MDeUlql9XJ6kRVgk/sP11sr3HKXn268HZyqL7i4cEfrdFuSSY/0tUqT37l5zT0tJOsuDP16kio85iA== dependencies: - "@typescript-eslint/typescript-estree" "6.19.0" - "@typescript-eslint/utils" "6.19.0" - debug "^4.3.4" - ts-api-utils "^1.0.1" + "@typescript-eslint/types" "7.5.0" + "@typescript-eslint/visitor-keys" "7.5.0" -"@typescript-eslint/type-utils@6.21.0", "@typescript-eslint/type-utils@^6.13.2": +"@typescript-eslint/type-utils@6.21.0": version "6.21.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz#6473281cfed4dacabe8004e8521cee0bd9d4c01e" integrity sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag== @@ -7648,13 +7700,23 @@ debug "^4.3.4" ts-api-utils "^1.0.1" -"@typescript-eslint/type-utils@^6.9.1": - version "6.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.14.0.tgz#ac9cb5ba0615c837f1a6b172feeb273d36e4f8af" - integrity sha512-x6OC9Q7HfYKqjnuNu5a7kffIYs3No30isapRBJl1iCHLitD8O0lFbRcVGiOcuyN837fqXzPZ1NS10maQzZMKqw== +"@typescript-eslint/type-utils@7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.2.0.tgz#7be5c30e9b4d49971b79095a1181324ef6089a19" + integrity sha512-xHi51adBHo9O9330J8GQYQwrKBqbIPJGZZVQTHHmy200hvkLZFWJIFtAG/7IYTWUyun6DE6w5InDReePJYJlJA== + dependencies: + "@typescript-eslint/typescript-estree" "7.2.0" + "@typescript-eslint/utils" "7.2.0" + debug "^4.3.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/type-utils@^7.3.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.5.0.tgz#a8faa403232da3a3901655387c7082111f692cf9" + integrity sha512-A021Rj33+G8mx2Dqh0nMO9GyjjIBK3MqgVgZ2qlKf6CJy51wY/lkkFqq3TqqnH34XyAHUkq27IjlUkWlQRpLHw== dependencies: - "@typescript-eslint/typescript-estree" "6.14.0" - "@typescript-eslint/utils" "6.14.0" + "@typescript-eslint/typescript-estree" "7.5.0" + "@typescript-eslint/utils" "7.5.0" debug "^4.3.4" ts-api-utils "^1.0.1" @@ -7663,21 +7725,21 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== -"@typescript-eslint/types@6.14.0": - version "6.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.14.0.tgz#935307f7a931016b7a5eb25d494ea3e1f613e929" - integrity sha512-uty9H2K4Xs8E47z3SnXEPRNDfsis8JO27amp2GNCnzGETEW3yTqEIVg5+AI7U276oGF/tw6ZA+UesxeQ104ceA== - -"@typescript-eslint/types@6.19.0": - version "6.19.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.19.0.tgz#689b0498c436272a6a2059b09f44bcbd90de294a" - integrity sha512-lFviGV/vYhOy3m8BJ/nAKoAyNhInTdXpftonhWle66XHAtT1ouBlkjL496b5H5hb8dWXHwtypTqgtb/DEa+j5A== - "@typescript-eslint/types@6.21.0": version "6.21.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d" integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== +"@typescript-eslint/types@7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.2.0.tgz#0feb685f16de320e8520f13cca30779c8b7c403f" + integrity sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA== + +"@typescript-eslint/types@7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.5.0.tgz#0a284bcdef3cb850ec9fd57992df9f29d6bde1bc" + integrity sha512-tv5B4IHeAdhR7uS4+bf8Ov3k793VEVHd45viRRkehIUZxm0WF82VPiLgHzA/Xl4TGPg1ZD49vfxBKFPecD5/mg== + "@typescript-eslint/typescript-estree@5.62.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b" @@ -7691,26 +7753,27 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@6.14.0": - version "6.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.14.0.tgz#90c7ddd45cd22139adf3d4577580d04c9189ac13" - integrity sha512-yPkaLwK0yH2mZKFE/bXkPAkkFgOv15GJAUzgUVonAbv0Hr4PK/N2yaA/4XQbTZQdygiDkpt5DkxPELqHguNvyw== +"@typescript-eslint/typescript-estree@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz#c47ae7901db3b8bddc3ecd73daff2d0895688c46" + integrity sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ== dependencies: - "@typescript-eslint/types" "6.14.0" - "@typescript-eslint/visitor-keys" "6.14.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" + minimatch "9.0.3" semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/typescript-estree@6.19.0": - version "6.19.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.19.0.tgz#0813ba364a409afb4d62348aec0202600cb468fa" - integrity sha512-o/zefXIbbLBZ8YJ51NlkSAt2BamrK6XOmuxSR3hynMIzzyMY33KuJ9vuMdFSXW+H0tVvdF9qBPTHA91HDb4BIQ== +"@typescript-eslint/typescript-estree@7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.2.0.tgz#5beda2876c4137f8440c5a84b4f0370828682556" + integrity sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA== dependencies: - "@typescript-eslint/types" "6.19.0" - "@typescript-eslint/visitor-keys" "6.19.0" + "@typescript-eslint/types" "7.2.0" + "@typescript-eslint/visitor-keys" "7.2.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" @@ -7718,13 +7781,13 @@ semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/typescript-estree@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz#c47ae7901db3b8bddc3ecd73daff2d0895688c46" - integrity sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ== +"@typescript-eslint/typescript-estree@7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.5.0.tgz#aa5031c511874420f6b5edd90f8e4021525ee776" + integrity sha512-YklQQfe0Rv2PZEueLTUffiQGKQneiIEKKnfIqPIOxgM9lKSZFCjT5Ad4VqRKj/U4+kQE3fa8YQpskViL7WjdPQ== dependencies: - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" + "@typescript-eslint/types" "7.5.0" + "@typescript-eslint/visitor-keys" "7.5.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" @@ -7732,43 +7795,43 @@ semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/utils@6.14.0": - version "6.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.14.0.tgz#856a9e274367d99ffbd39c48128b93a86c4261e3" - integrity sha512-XwRTnbvRr7Ey9a1NT6jqdKX8y/atWG+8fAIu3z73HSP8h06i3r/ClMhmaF/RGWGW1tHJEwij1uEg2GbEmPYvYg== +"@typescript-eslint/utils@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.21.0.tgz#4714e7a6b39e773c1c8e97ec587f520840cd8134" + integrity sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ== dependencies: "@eslint-community/eslint-utils" "^4.4.0" "@types/json-schema" "^7.0.12" "@types/semver" "^7.5.0" - "@typescript-eslint/scope-manager" "6.14.0" - "@typescript-eslint/types" "6.14.0" - "@typescript-eslint/typescript-estree" "6.14.0" + "@typescript-eslint/scope-manager" "6.21.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/typescript-estree" "6.21.0" semver "^7.5.4" -"@typescript-eslint/utils@6.19.0": - version "6.19.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.19.0.tgz#557b72c3eeb4f73bef8037c85dae57b21beb1a4b" - integrity sha512-QR41YXySiuN++/dC9UArYOg4X86OAYP83OWTewpVx5ct1IZhjjgTLocj7QNxGhWoTqknsgpl7L+hGygCO+sdYw== +"@typescript-eslint/utils@7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.2.0.tgz#fc8164be2f2a7068debb4556881acddbf0b7ce2a" + integrity sha512-YfHpnMAGb1Eekpm3XRK8hcMwGLGsnT6L+7b2XyRv6ouDuJU1tZir1GS2i0+VXRatMwSI1/UfcyPe53ADkU+IuA== dependencies: "@eslint-community/eslint-utils" "^4.4.0" "@types/json-schema" "^7.0.12" "@types/semver" "^7.5.0" - "@typescript-eslint/scope-manager" "6.19.0" - "@typescript-eslint/types" "6.19.0" - "@typescript-eslint/typescript-estree" "6.19.0" + "@typescript-eslint/scope-manager" "7.2.0" + "@typescript-eslint/types" "7.2.0" + "@typescript-eslint/typescript-estree" "7.2.0" semver "^7.5.4" -"@typescript-eslint/utils@6.21.0", "@typescript-eslint/utils@^6.13.2": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.21.0.tgz#4714e7a6b39e773c1c8e97ec587f520840cd8134" - integrity sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ== +"@typescript-eslint/utils@7.5.0", "@typescript-eslint/utils@^7.3.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.5.0.tgz#bbd963647fbbe9ffea033f42c0fb7e89bb19c858" + integrity sha512-3vZl9u0R+/FLQcpy2EHyRGNqAS/ofJ3Ji8aebilfJe+fobK8+LbIFmrHciLVDxjDoONmufDcnVSF38KwMEOjzw== dependencies: "@eslint-community/eslint-utils" "^4.4.0" "@types/json-schema" "^7.0.12" "@types/semver" "^7.5.0" - "@typescript-eslint/scope-manager" "6.21.0" - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/typescript-estree" "6.21.0" + "@typescript-eslint/scope-manager" "7.5.0" + "@typescript-eslint/types" "7.5.0" + "@typescript-eslint/typescript-estree" "7.5.0" semver "^7.5.4" "@typescript-eslint/utils@^5.45.0": @@ -7793,28 +7856,28 @@ "@typescript-eslint/types" "5.62.0" eslint-visitor-keys "^3.3.0" -"@typescript-eslint/visitor-keys@6.14.0": - version "6.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.14.0.tgz#1d1d486581819287de824a56c22f32543561138e" - integrity sha512-fB5cw6GRhJUz03MrROVuj5Zm/Q+XWlVdIsFj+Zb1Hvqouc8t+XP2H5y53QYU/MGtd2dPg6/vJJlhoX3xc2ehfw== +"@typescript-eslint/visitor-keys@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz#87a99d077aa507e20e238b11d56cc26ade45fe47" + integrity sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A== dependencies: - "@typescript-eslint/types" "6.14.0" + "@typescript-eslint/types" "6.21.0" eslint-visitor-keys "^3.4.1" -"@typescript-eslint/visitor-keys@6.19.0": - version "6.19.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.19.0.tgz#4565e0ecd63ca1f81b96f1dd76e49f746c6b2b49" - integrity sha512-hZaUCORLgubBvtGpp1JEFEazcuEdfxta9j4iUwdSAr7mEsYYAp3EAUyCZk3VEEqGj6W+AV4uWyrDGtrlawAsgQ== +"@typescript-eslint/visitor-keys@7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.2.0.tgz#5035f177752538a5750cca1af6044b633610bf9e" + integrity sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A== dependencies: - "@typescript-eslint/types" "6.19.0" + "@typescript-eslint/types" "7.2.0" eslint-visitor-keys "^3.4.1" -"@typescript-eslint/visitor-keys@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz#87a99d077aa507e20e238b11d56cc26ade45fe47" - integrity sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A== +"@typescript-eslint/visitor-keys@7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.5.0.tgz#8abcac66f93ef20b093e87a400c2d21e3a6d55ee" + integrity sha512-mcuHM/QircmA6O7fy6nn2w/3ditQkj+SgtOc8DW3uQ10Yfj42amm2i+6F2K4YAOPNNTmE6iM1ynM6lrSwdendA== dependencies: - "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/types" "7.5.0" eslint-visitor-keys "^3.4.1" "@ungap/structured-clone@^1.2.0": @@ -8510,13 +8573,13 @@ at-least-node@^1.0.0: resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== -autoprefixer@10.4.17: - version "10.4.17" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.17.tgz#35cd5695cbbe82f536a50fa025d561b01fdec8be" - integrity sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg== +autoprefixer@10.4.18: + version "10.4.18" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.18.tgz#fcb171a3b017be7cb5d8b7a825f5aacbf2045163" + integrity sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g== dependencies: - browserslist "^4.22.2" - caniuse-lite "^1.0.30001578" + browserslist "^4.23.0" + caniuse-lite "^1.0.30001591" fraction.js "^4.3.7" normalize-range "^0.1.2" picocolors "^1.0.0" @@ -8990,6 +9053,16 @@ browserslist@^4.22.2: node-releases "^2.0.14" update-browserslist-db "^1.0.13" +browserslist@^4.23.0: + version "4.23.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.0.tgz#8f3acc2bbe73af7213399430890f86c63a5674ab" + integrity sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ== + dependencies: + caniuse-lite "^1.0.30001587" + electron-to-chromium "^1.4.668" + node-releases "^2.0.14" + update-browserslist-db "^1.0.13" + bs-logger@0.x, bs-logger@^0.2.6: version "0.2.6" resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" @@ -9185,10 +9258,10 @@ caniuse-lite@^1.0.30001565: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001570.tgz#b4e5c1fa786f733ab78fc70f592df6b3f23244ca" integrity sha512-+3e0ASu4sw1SWaoCtvPeyXp+5PsjigkSt8OXZbF9StH5pQWbxEjLAZE3n8Aup5udop1uRiKA7a4utUk/uoSpUw== -caniuse-lite@^1.0.30001578: - version "1.0.30001599" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001599.tgz#571cf4f3f1506df9bf41fcbb6d10d5d017817bce" - integrity sha512-LRAQHZ4yT1+f9LemSMeqdMpMxZcc4RMWdj4tiFe3G8tNkWK+E58g+/tzotb5cU6TbcVJLr4fySiAW7XmxQvZQA== +caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001591: + version "1.0.30001606" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001606.tgz#b4d5f67ab0746a3b8b5b6d1f06e39c51beb39a9e" + integrity sha512-LPbwnW4vfpJId225pwjZJOgX1m9sGfbw/RKJvw/t0QhYOOaTXHvkjVGFGPpvwEzufrjvTlsULnVTxdy4/6cqkg== case-sensitive-paths-webpack-plugin@^2.4.0: version "2.4.0" @@ -9868,10 +9941,10 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -critters@0.0.20: - version "0.0.20" - resolved "https://registry.yarnpkg.com/critters/-/critters-0.0.20.tgz#08ddb961550ab7b3a59370537e4f01df208f7646" - integrity sha512-CImNRorKOl5d8TWcnAz5n5izQ6HFsvz29k327/ELy6UFcmbiZNOsinaKvzv16WZR0P6etfSWYzE47C4/56B3Uw== +critters@0.0.22: + version "0.0.22" + resolved "https://registry.yarnpkg.com/critters/-/critters-0.0.22.tgz#ce76b1cbc70078c89d23725646357e3850236dae" + integrity sha512-NU7DEcQZM2Dy8XTKFHxtdnIM/drE312j2T4PCVaSUcS0oBeyT/NImpRw/Ap0zOr/1SE7SgPK9tGPg1WK/sVakw== dependencies: chalk "^4.1.0" css-select "^5.1.0" @@ -9879,7 +9952,7 @@ critters@0.0.20: domhandler "^5.0.2" htmlparser2 "^8.0.2" postcss "^8.4.23" - pretty-bytes "^5.3.0" + postcss-media-query-parser "^0.2.3" cron-parser@^4.2.1: version "4.9.0" @@ -10926,6 +10999,11 @@ electron-to-chromium@^1.4.601: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.614.tgz#2fe789d61fa09cb875569f37c309d0c2701f91c0" integrity sha512-X4ze/9Sc3QWs6h92yerwqv7aB/uU8vCjZcrMjA8N9R1pjMFRe44dLsck5FzLilOYvcXuDn93B+bpGYyufc70gQ== +electron-to-chromium@^1.4.668: + version "1.4.729" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.729.tgz#8477d21e2a50993781950885b2731d92ad532c00" + integrity sha512-bx7+5Saea/qu14kmPTDHQxkp2UnziG3iajUQu3BxFvCOnpAJdDbMV4rSl+EqFDkkpNNVUFlR1kDfpL59xfy1HA== + elegant-spinner@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" @@ -11150,44 +11228,44 @@ esbuild-register@^3.4.0, esbuild-register@^3.5.0: dependencies: debug "^4.3.4" -esbuild-wasm@0.20.0: - version "0.20.0" - resolved "https://registry.yarnpkg.com/esbuild-wasm/-/esbuild-wasm-0.20.0.tgz#79b46ee616d4ca7d207ccd2a80c41de62a9ebfd2" - integrity sha512-Lc9KeQCg1Zf8kCtfDXgy29rx0x8dOuhDWbkP76Wc64q7ctOOc1Zv1C39AxiE+y4N6ONyXtJk4HKpM7jlU7/jSA== +esbuild-wasm@0.20.1: + version "0.20.1" + resolved "https://registry.yarnpkg.com/esbuild-wasm/-/esbuild-wasm-0.20.1.tgz#fdc14b95e3e16ec8e082dd641edb96140c1723f7" + integrity sha512-6v/WJubRsjxBbQdz6izgvx7LsVFvVaGmSdwrFHmEzoVgfXL89hkKPoQHsnVI2ngOkcBUQT9kmAM1hVL1k/Av4A== esbuild-wasm@>=0.15.13: version "0.20.2" resolved "https://registry.yarnpkg.com/esbuild-wasm/-/esbuild-wasm-0.20.2.tgz#bbee2a729776b0b88b765c014f161b627435c5b6" integrity sha512-7o6nmsEqlcXJXMNqnx5K+M4w4OPx7yTFXQHcJyeP3SkXb8p2T8N9E1ayK4vd/qDBepH6fuPoZwiFvZm8x5qv+w== -esbuild@0.20.0: - version "0.20.0" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.0.tgz#a7170b63447286cd2ff1f01579f09970e6965da4" - integrity sha512-6iwE3Y2RVYCME1jLpBqq7LQWK3MW6vjV2bZy6gt/WrqkY+WE74Spyc0ThAOYpMtITvnjX09CrC6ym7A/m9mebA== +esbuild@0.20.1: + version "0.20.1" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.1.tgz#1e4cbb380ad1959db7609cb9573ee77257724a3e" + integrity sha512-OJwEgrpWm/PCMsLVWXKqvcjme3bHNpOgN7Tb6cQnR5n0TPbQx1/Xrn7rqM+wn17bYeT6MGB5sn1Bh5YiGi70nA== optionalDependencies: - "@esbuild/aix-ppc64" "0.20.0" - "@esbuild/android-arm" "0.20.0" - "@esbuild/android-arm64" "0.20.0" - "@esbuild/android-x64" "0.20.0" - "@esbuild/darwin-arm64" "0.20.0" - "@esbuild/darwin-x64" "0.20.0" - "@esbuild/freebsd-arm64" "0.20.0" - "@esbuild/freebsd-x64" "0.20.0" - "@esbuild/linux-arm" "0.20.0" - "@esbuild/linux-arm64" "0.20.0" - "@esbuild/linux-ia32" "0.20.0" - "@esbuild/linux-loong64" "0.20.0" - "@esbuild/linux-mips64el" "0.20.0" - "@esbuild/linux-ppc64" "0.20.0" - "@esbuild/linux-riscv64" "0.20.0" - "@esbuild/linux-s390x" "0.20.0" - "@esbuild/linux-x64" "0.20.0" - "@esbuild/netbsd-x64" "0.20.0" - "@esbuild/openbsd-x64" "0.20.0" - "@esbuild/sunos-x64" "0.20.0" - "@esbuild/win32-arm64" "0.20.0" - "@esbuild/win32-ia32" "0.20.0" - "@esbuild/win32-x64" "0.20.0" + "@esbuild/aix-ppc64" "0.20.1" + "@esbuild/android-arm" "0.20.1" + "@esbuild/android-arm64" "0.20.1" + "@esbuild/android-x64" "0.20.1" + "@esbuild/darwin-arm64" "0.20.1" + "@esbuild/darwin-x64" "0.20.1" + "@esbuild/freebsd-arm64" "0.20.1" + "@esbuild/freebsd-x64" "0.20.1" + "@esbuild/linux-arm" "0.20.1" + "@esbuild/linux-arm64" "0.20.1" + "@esbuild/linux-ia32" "0.20.1" + "@esbuild/linux-loong64" "0.20.1" + "@esbuild/linux-mips64el" "0.20.1" + "@esbuild/linux-ppc64" "0.20.1" + "@esbuild/linux-riscv64" "0.20.1" + "@esbuild/linux-s390x" "0.20.1" + "@esbuild/linux-x64" "0.20.1" + "@esbuild/netbsd-x64" "0.20.1" + "@esbuild/openbsd-x64" "0.20.1" + "@esbuild/sunos-x64" "0.20.1" + "@esbuild/win32-arm64" "0.20.1" + "@esbuild/win32-ia32" "0.20.1" + "@esbuild/win32-x64" "0.20.1" esbuild@>=0.15.13: version "0.20.2" @@ -12786,10 +12864,10 @@ http2-wrapper@^1.0.0-beta.5.2: quick-lru "^5.1.1" resolve-alpn "^1.0.0" -https-proxy-agent@7.0.2, https-proxy-agent@^7.0.1, https-proxy-agent@^7.0.2: - version "7.0.2" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz#e2645b846b90e96c6e6f347fb5b2e41f1590b09b" - integrity sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA== +https-proxy-agent@7.0.4: + version "7.0.4" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz#8e97b841a029ad8ddc8731f26595bad868cb4168" + integrity sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg== dependencies: agent-base "^7.0.2" debug "4" @@ -12810,6 +12888,14 @@ https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: agent-base "6" debug "4" +https-proxy-agent@^7.0.1, https-proxy-agent@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz#e2645b846b90e96c6e6f347fb5b2e41f1590b09b" + integrity sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA== + dependencies: + agent-base "^7.0.2" + debug "4" + human-signals@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" @@ -12932,15 +13018,15 @@ ini@1.3.7: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84" integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ== -ini@4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/ini/-/ini-4.1.1.tgz#d95b3d843b1e906e56d6747d5447904ff50ce7a1" - integrity sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g== +ini@4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/ini/-/ini-4.1.2.tgz#7f646dbd9caea595e61f88ef60bfff8b01f8130a" + integrity sha512-AMB1mvwR1pyBFY/nSevUX6y8nJWS63/SzUKD3JyQn97s4xgIdgQPT75IRouIiBAN4yLQBUShNYVW0+UG25daCw== -inquirer@9.2.14: - version "9.2.14" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-9.2.14.tgz#b55474f1e4f5f0eb28b2f75f09c082209f0cc2ca" - integrity sha512-4ByIMt677Iz5AvjyKrDpzaepIyMewNvDcvwpVVRZNmy9dLakVoVgdCHZXbK1SlVJra1db0JZ6XkJyHsanpdrdQ== +inquirer@9.2.15: + version "9.2.15" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-9.2.15.tgz#2135a36190a6e5c92f5d205e0af1fea36b9d3492" + integrity sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg== dependencies: "@ljharb/through" "^2.3.12" ansi-escapes "^4.3.2" @@ -14580,10 +14666,10 @@ magic-string@0.30.5, magic-string@^0.30.5, magic-string@~0.30.2: dependencies: "@jridgewell/sourcemap-codec" "^1.4.15" -magic-string@0.30.7: - version "0.30.7" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.7.tgz#0cecd0527d473298679da95a2d7aeb8c64048505" - integrity sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA== +magic-string@0.30.8: + version "0.30.8" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.8.tgz#14e8624246d2bedba70d5462aa99ac9681844613" + integrity sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ== dependencies: "@jridgewell/sourcemap-codec" "^1.4.15" @@ -15009,10 +15095,10 @@ mimic-response@^3.1.0: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== -mini-css-extract-plugin@2.8.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.8.0.tgz#1aeae2a90a954b6426c9e8311eab36b450f553a0" - integrity sha512-CxmUYPFcTgET1zImteG/LZOy/4T5rTojesQXkSNBiquhydn78tfbCE9sjIjnJ/UcjNjOC1bphTCCW5rrS7cXAg== +mini-css-extract-plugin@2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.8.1.tgz#75245f3f30ce3a56dbdd478084df6fe475f02dc7" + integrity sha512-/1HDlyFRxWIZPI1ZpgqlZ8jMw/1Dp/dl3P0L1jtZ+zVcHqwPhGwaJwKL00WVgfnBy6PWCde9W65or7IIETImuA== dependencies: schema-utils "^4.0.0" tapable "^2.2.1" @@ -15547,12 +15633,12 @@ nwsapi@^2.2.2: resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.7.tgz#738e0707d3128cb750dddcfe90e4610482df0f30" integrity sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ== -nx@18.1.2: - version "18.1.2" - resolved "https://registry.yarnpkg.com/nx/-/nx-18.1.2.tgz#e193111cbc6162e1e0afad07b24b01a960649423" - integrity sha512-E414xp6lVtiTGdDUMVo72G96G66t7oJMqmcHRMEZ/mVq5ZpNWUhfMuRq5Fh8orXPtrM3xk5SHokmmFvo5PKC+g== +nx@18.2.3: + version "18.2.3" + resolved "https://registry.yarnpkg.com/nx/-/nx-18.2.3.tgz#40d4c18a3b7143e10b3645b5bd47a65c5b9d5e6f" + integrity sha512-4XGvvIzXeeeSj1hObiBL7E7aXX6rbiB1F856AqUdGoysYfkhcxOFyeAv5XsXeukl9gYwh/LH84paXjEOkGaJlA== dependencies: - "@nrwl/tao" "18.1.2" + "@nrwl/tao" "18.2.3" "@yarnpkg/lockfile" "^1.1.0" "@yarnpkg/parsers" "3.0.0-rc.46" "@zkochan/js-yaml" "0.0.6" @@ -15587,16 +15673,16 @@ nx@18.1.2: yargs "^17.6.2" yargs-parser "21.1.1" optionalDependencies: - "@nx/nx-darwin-arm64" "18.1.2" - "@nx/nx-darwin-x64" "18.1.2" - "@nx/nx-freebsd-x64" "18.1.2" - "@nx/nx-linux-arm-gnueabihf" "18.1.2" - "@nx/nx-linux-arm64-gnu" "18.1.2" - "@nx/nx-linux-arm64-musl" "18.1.2" - "@nx/nx-linux-x64-gnu" "18.1.2" - "@nx/nx-linux-x64-musl" "18.1.2" - "@nx/nx-win32-arm64-msvc" "18.1.2" - "@nx/nx-win32-x64-msvc" "18.1.2" + "@nx/nx-darwin-arm64" "18.2.3" + "@nx/nx-darwin-x64" "18.2.3" + "@nx/nx-freebsd-x64" "18.2.3" + "@nx/nx-linux-arm-gnueabihf" "18.2.3" + "@nx/nx-linux-arm64-gnu" "18.2.3" + "@nx/nx-linux-arm64-musl" "18.2.3" + "@nx/nx-linux-x64-gnu" "18.2.3" + "@nx/nx-linux-x64-musl" "18.2.3" + "@nx/nx-win32-arm64-msvc" "18.2.3" + "@nx/nx-win32-x64-msvc" "18.2.3" oauth@0.9.x: version "0.9.15" @@ -16159,10 +16245,10 @@ pirates@^4.0.4, pirates@^4.0.5: resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== -piscina@4.3.1, piscina@^4.2.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/piscina/-/piscina-4.3.1.tgz#eaa59461caa27f07c637e667b14c36a0bd7e7daf" - integrity sha512-MBj0QYm3hJQ/C/wIXTN1OCYC8uQ4BBJ4LVele2P4ZwVQAH04vkk8E1SpDbuemLAL1dZorbuOob9rYqJeWCcCRg== +piscina@4.4.0, piscina@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/piscina/-/piscina-4.4.0.tgz#e3af8e5721d8fad08c6ccaf8a64f9f42279efbb5" + integrity sha512-+AQduEJefrOApE4bV7KRmp3N2JnnyErlVqq4P/jmko4FPz9Z877BCccl/iB3FdrWSUkvbGV9Kan/KllJgat3Vg== optionalDependencies: nice-napi "^1.0.2" @@ -16270,10 +16356,10 @@ postcss-import@~14.1.0: read-cache "^1.0.0" resolve "^1.1.7" -postcss-loader@8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-8.1.0.tgz#590e8bd872d7cdf53c486cbcd40c4c94789f1216" - integrity sha512-AbperNcX3rlob7Ay7A/HQcrofug1caABBkopoFeOQMspZBqcqj6giYn1Bwey/0uiOPAcR+NQD0I2HC7rXzk91w== +postcss-loader@8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-8.1.1.tgz#2822589e7522927344954acb55bbf26e8b195dfe" + integrity sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ== dependencies: cosmiconfig "^9.0.0" jiti "^1.20.0" @@ -16288,6 +16374,11 @@ postcss-loader@^6.1.1: klona "^2.0.5" semver "^7.3.5" +postcss-media-query-parser@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz#27b39c6f4d94f81b1a73b8f76351c609e5cef244" + integrity sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig== + postcss-merge-longhand@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-6.0.0.tgz#6f627b27db939bce316eaa97e22400267e798d69" @@ -16495,7 +16586,7 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0, postcss-value-parser@^ resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@8.4.35, postcss@^8.4.32: +postcss@8.4.35: version "8.4.35" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.35.tgz#60997775689ce09011edf083a549cea44aabe2f7" integrity sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA== @@ -16522,6 +16613,15 @@ postcss@^8.4.33: picocolors "^1.0.0" source-map-js "^1.2.0" +postcss@^8.4.35: + version "8.4.38" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" + integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.0" + source-map-js "^1.2.0" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -16547,7 +16647,7 @@ prettier@^2.8.0: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== -pretty-bytes@^5.3.0, pretty-bytes@^5.4.1: +pretty-bytes@^5.4.1: version "5.6.0" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== @@ -17342,10 +17442,10 @@ safevalues@^0.3.4: resolved "https://registry.yarnpkg.com/safevalues/-/safevalues-0.3.4.tgz#82e846a02b6956d7d40bf9f41e92e13fce0186db" integrity sha512-LRneZZRXNgjzwG4bDQdOTSbze3fHm1EAKN/8bePxnlEZiBmkYEDggaHbuvHI9/hoqHbGfsEA7tWS9GhYHZBBsw== -sass-loader@14.1.0: - version "14.1.0" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-14.1.0.tgz#43ba90e0cd8a15a1e932e818c525b0115a0ce8a3" - integrity sha512-LS2mLeFWA+orYxHNu+O18Xe4jR0kyamNOOUsE3NyBP4DvIL+8stHpNX0arYTItdPe80kluIiJ7Wfe/9iHSRO0Q== +sass-loader@14.1.1: + version "14.1.1" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-14.1.1.tgz#2c9d2277c5b1c5fe789cd0570c046d8ad23cb7ca" + integrity sha512-QX8AasDg75monlybel38BZ49JP5Z+uSKfKwF2rO7S74BywaRmGQMUBw9dtkS+ekyM/QnP+NOrRYq8ABMZ9G8jw== dependencies: neo-async "^2.6.2" @@ -17357,10 +17457,10 @@ sass-loader@^12.2.0: klona "^2.0.4" neo-async "^2.6.2" -sass@1.70.0: - version "1.70.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.70.0.tgz#761197419d97b5358cb25f9dd38c176a8a270a75" - integrity sha512-uUxNQ3zAHeAx5nRFskBnrWzDUJrrvpCPD5FNAoRvTi0WwremlheES3tg+56PaVtCs5QDRX5CBLxxKMDJMEa1WQ== +sass@1.71.1: + version "1.71.1" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.71.1.tgz#dfb09c63ce63f89353777bbd4a88c0a38386ee54" + integrity sha512-wovtnV2PxzteLlfNzbgm1tFXPLoZILYAMJtvoXXkD7/+1uP41eKkIt1ypWq5/q2uT94qHjXehEYfmjKOvjL9sg== dependencies: chokidar ">=3.0.0 <4.0.0" immutable "^4.0.0" @@ -18300,10 +18400,10 @@ terser-webpack-plugin@^5.3.10: serialize-javascript "^6.0.1" terser "^5.26.0" -terser@5.27.0: - version "5.27.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.27.0.tgz#70108689d9ab25fef61c4e93e808e9fd092bf20c" - integrity sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A== +terser@5.29.1: + version "5.29.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.29.1.tgz#44e58045b70c09792ba14bfb7b4e14ca8755b9fa" + integrity sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ== dependencies: "@jridgewell/source-map" "^0.3.3" acorn "^8.8.2" @@ -18743,10 +18843,10 @@ types-ramda@^0.29.4: dependencies: ts-toolbelt "^9.6.0" -typescript@5.3.3, typescript@~5.3.2: - version "5.3.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" - integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== +typescript@5.4.4, typescript@~5.4.2: + version "5.4.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.4.tgz#eb2471e7b0a5f1377523700a21669dce30c2d952" + integrity sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw== uglify-js@^3.1.4: version "3.17.4" @@ -18785,12 +18885,10 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -undici@6.6.2: - version "6.6.2" - resolved "https://registry.yarnpkg.com/undici/-/undici-6.6.2.tgz#8dce5ae54e8a3bc7140c2b2a0972b5fde9a88efb" - integrity sha512-vSqvUE5skSxQJ5sztTZ/CdeJb1Wq0Hf44hlYMciqHghvz+K88U0l7D6u1VsndoFgskDcnU+nG3gYmMzJVzd9Qg== - dependencies: - "@fastify/busboy" "^2.0.0" +undici@6.7.1: + version "6.7.1" + resolved "https://registry.yarnpkg.com/undici/-/undici-6.7.1.tgz#3cb27222fd5d72c1b2058f4e18bf9b53dd933af8" + integrity sha512-+Wtb9bAQw6HYWzCnxrPTMVEV3Q1QjYanI0E4q02ehReMuquQdLTEFEYbfs7hcImVYKcQkWSwT6buEmSVIiDDtQ== unfetch@^4.2.0: version "4.2.0" @@ -19064,13 +19162,13 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" -vite@5.0.12: - version "5.0.12" - resolved "https://registry.yarnpkg.com/vite/-/vite-5.0.12.tgz#8a2ffd4da36c132aec4adafe05d7adde38333c47" - integrity sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w== +vite@5.1.5: + version "5.1.5" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.1.5.tgz#bdbc2b15e8000d9cc5172f059201178f9c9de5fb" + integrity sha512-BdN1xh0Of/oQafhU+FvopafUp6WaYenLU/NFoL5WyJL++GxkNfieKzBhM24H3HVsPQrlAqB7iJYTHabzaRed5Q== dependencies: esbuild "^0.19.3" - postcss "^8.4.32" + postcss "^8.4.35" rollup "^4.2.0" optionalDependencies: fsevents "~2.3.3" @@ -19145,10 +19243,10 @@ webpack-bundle-analyzer@4.10.1: sirv "^2.0.3" ws "^7.3.1" -webpack-dev-middleware@6.1.1, webpack-dev-middleware@^6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-6.1.1.tgz#6bbc257ec83ae15522de7a62f995630efde7cc3d" - integrity sha512-y51HrHaFeeWir0YO4f0g+9GwZawuigzcAdRNon6jErXy/SqV/+O6eaVAzDqE6t3e3NpGeR5CS+cCDaTC+V3yEQ== +webpack-dev-middleware@6.1.2: + version "6.1.2" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-6.1.2.tgz#0463232e59b7d7330fa154121528d484d36eb973" + integrity sha512-Wu+EHmX326YPYUpQLKmKbTyZZJIB8/n6R09pTmB03kJmnMsVPTo9COzHZFr01txwaCAuZvfBJE4ZCHRcKs5JaQ== dependencies: colorette "^2.0.10" memfs "^3.4.12" @@ -19167,6 +19265,17 @@ webpack-dev-middleware@^5.3.1: range-parser "^1.2.1" schema-utils "^4.0.0" +webpack-dev-middleware@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-6.1.1.tgz#6bbc257ec83ae15522de7a62f995630efde7cc3d" + integrity sha512-y51HrHaFeeWir0YO4f0g+9GwZawuigzcAdRNon6jErXy/SqV/+O6eaVAzDqE6t3e3NpGeR5CS+cCDaTC+V3yEQ== + dependencies: + colorette "^2.0.10" + memfs "^3.4.12" + mime-types "^2.1.31" + range-parser "^1.2.1" + schema-utils "^4.0.0" + webpack-dev-server@4.15.1, webpack-dev-server@^4.9.3: version "4.15.1" resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz#8944b29c12760b3a45bdaa70799b17cb91b03df7" @@ -19273,10 +19382,10 @@ webpack@5, webpack@^5.80.0: watchpack "^2.4.0" webpack-sources "^3.2.3" -webpack@5.90.1: - version "5.90.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.90.1.tgz#62ab0c097d7cbe83d32523dbfbb645cdb7c3c01c" - integrity sha512-SstPdlAC5IvgFnhiRok8hqJo/+ArAbNv7rhU4fnWGHNVfN59HSQFaxZDSAL3IFG2YmqxuRs+IU33milSxbPlog== +webpack@5.90.3: + version "5.90.3" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.90.3.tgz#37b8f74d3ded061ba789bb22b31e82eed75bd9ac" + integrity sha512-h6uDYlWCctQRuXBs1oYpVe6sFcWedl0dpcVaTf/YF67J9bKvwJajFulMVSYKHrksMB3I/pIagRzDxwxkebuzKA== dependencies: "@types/eslint-scope" "^3.7.3" "@types/estree" "^1.0.5" From 2b97bbd05daabad45afe0fd11ca05a4298b323e0 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 9 Apr 2024 13:44:23 +0200 Subject: [PATCH 111/120] Move getChart() to portfolio calculator (#3255) --- .../calculator/portfolio-calculator.ts | 34 +++++++++- .../portfolio-position-detail.interface.ts | 6 -- .../src/app/portfolio/portfolio.service.ts | 68 ++----------------- 3 files changed, 37 insertions(+), 71 deletions(-) diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index 488f9ce99..a9dbff442 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -4,9 +4,13 @@ import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/curre import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface'; import { TransactionPointSymbol } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point-symbol.interface'; import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface'; -import { getFactor } from '@ghostfolio/api/helper/portfolio.helper'; +import { + getFactor, + getInterval +} from '@ghostfolio/api/helper/portfolio.helper'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; +import { MAX_CHART_ITEMS } from '@ghostfolio/common/config'; import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; import { DataProviderInfo, @@ -17,10 +21,11 @@ import { TimelinePosition, UniqueAsset } from '@ghostfolio/common/interfaces'; -import { GroupBy } from '@ghostfolio/common/types'; +import { DateRange, GroupBy } from '@ghostfolio/common/types'; import { Big } from 'big.js'; import { + differenceInDays, eachDayOfInterval, endOfDay, format, @@ -81,6 +86,31 @@ export abstract class PortfolioCalculator { positions: TimelinePosition[] ): CurrentPositions; + public async getChart({ + dateRange = 'max', + withDataDecimation = true + }: { + dateRange?: DateRange; + withDataDecimation?: boolean; + }): Promise { + if (this.getTransactionPoints().length === 0) { + return []; + } + + const { endDate, startDate } = getInterval(dateRange, this.getStartDate()); + + const daysInMarket = differenceInDays(endDate, startDate) + 1; + const step = withDataDecimation + ? Math.round(daysInMarket / Math.min(daysInMarket, MAX_CHART_ITEMS)) + : 1; + + return this.getChartData({ + step, + end: endDate, + start: startDate + }); + } + public async getChartData({ end = new Date(Date.now()), start, diff --git a/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts b/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts index c058a0249..a32d47e21 100644 --- a/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts @@ -36,9 +36,3 @@ export interface PortfolioPositionDetail { transactionCount: number; value: number; } - -export interface HistoricalDataContainer { - isAllTimeHigh: boolean; - isAllTimeLow: boolean; - items: HistoricalDataItem[]; -} diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 99fb47e2c..198395e51 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -21,7 +21,6 @@ import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/sy import { DEFAULT_CURRENCY, EMERGENCY_FUND_TAG_ID, - MAX_CHART_ITEMS, UNKNOWN_KEY } from '@ghostfolio/common/config'; import { @@ -63,8 +62,7 @@ import { DataSource, Order, Platform, - Prisma, - SymbolProfile + Prisma } from '@prisma/client'; import { Big } from 'big.js'; import { isUUID } from 'class-validator'; @@ -80,15 +78,11 @@ import { } from 'date-fns'; import { isEmpty, isNumber, last, uniq, uniqBy } from 'lodash'; -import { PortfolioCalculator } from './calculator/portfolio-calculator'; import { PerformanceCalculationType, PortfolioCalculatorFactory } from './calculator/portfolio-calculator.factory'; -import { - HistoricalDataContainer, - PortfolioPositionDetail -} from './interfaces/portfolio-position-detail.interface'; +import { PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface'; import { RulesService } from './rules.service'; const asiaPacificMarkets = require('../../assets/countries/asia-pacific-markets.json'); @@ -292,11 +286,8 @@ export class PortfolioService { currency: this.request.user.Settings.settings.baseCurrency }); - const { items } = await this.getChart({ + const items = await portfolioCalculator.getChart({ dateRange, - impersonationId, - portfolioCalculator, - userId, withDataDecimation: false }); @@ -1161,11 +1152,8 @@ export class PortfolioService { let currentNetPerformanceWithCurrencyEffect = netPerformanceWithCurrencyEffect; - const { items } = await this.getChart({ - dateRange, - impersonationId, - portfolioCalculator, - userId + const items = await portfolioCalculator.getChart({ + dateRange }); const itemOfToday = items.find(({ date }) => { @@ -1381,52 +1369,6 @@ export class PortfolioService { return cashPositions; } - private async getChart({ - dateRange = 'max', - impersonationId, - portfolioCalculator, - userId, - withDataDecimation = true - }: { - dateRange?: DateRange; - impersonationId: string; - portfolioCalculator: PortfolioCalculator; - userId: string; - withDataDecimation?: boolean; - }): Promise { - if (portfolioCalculator.getTransactionPoints().length === 0) { - return { - isAllTimeHigh: false, - isAllTimeLow: false, - items: [] - }; - } - - userId = await this.getUserId(impersonationId, userId); - - const { endDate, startDate } = getInterval( - dateRange, - portfolioCalculator.getStartDate() - ); - - const daysInMarket = differenceInDays(endDate, startDate) + 1; - const step = withDataDecimation - ? Math.round(daysInMarket / Math.min(daysInMarket, MAX_CHART_ITEMS)) - : 1; - - const items = await portfolioCalculator.getChartData({ - step, - end: endDate, - start: startDate - }); - - return { - items, - isAllTimeHigh: false, - isAllTimeLow: false - }; - } - private getDividendsByGroup({ dividends, groupBy From 34d9ceb009cd4240e3ff9cd8f9970a6be8ecb7a3 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 11 Apr 2024 19:14:03 +0200 Subject: [PATCH 112/120] Feature/add support to immediately execute queue job (#3259) * Add support to immediately execute queue job * Update changelog --- CHANGELOG.md | 4 + .../src/app/admin/queue/queue.controller.ts | 7 ++ apps/api/src/app/admin/queue/queue.service.ts | 4 + .../admin-jobs/admin-jobs.component.ts | 9 ++ .../app/components/admin-jobs/admin-jobs.html | 3 + apps/client/src/app/services/admin.service.ts | 4 + apps/client/src/locales/messages.de.xlf | 86 ++++++++++--------- apps/client/src/locales/messages.es.xlf | 86 ++++++++++--------- apps/client/src/locales/messages.fr.xlf | 86 ++++++++++--------- apps/client/src/locales/messages.it.xlf | 86 ++++++++++--------- apps/client/src/locales/messages.nl.xlf | 86 ++++++++++--------- apps/client/src/locales/messages.pl.xlf | 86 ++++++++++--------- apps/client/src/locales/messages.pt.xlf | 86 ++++++++++--------- apps/client/src/locales/messages.tr.xlf | 86 ++++++++++--------- apps/client/src/locales/messages.xlf | 85 +++++++++--------- apps/client/src/locales/messages.zh.xlf | 86 ++++++++++--------- 16 files changed, 500 insertions(+), 390 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9aacb00de..cbb92106f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added support to immediately execute a queue job from the admin control panel + ### Changed - Upgraded `angular` from version `17.2.4` to `17.3.3` diff --git a/apps/api/src/app/admin/queue/queue.controller.ts b/apps/api/src/app/admin/queue/queue.controller.ts index 89bd851bc..978cb9721 100644 --- a/apps/api/src/app/admin/queue/queue.controller.ts +++ b/apps/api/src/app/admin/queue/queue.controller.ts @@ -46,4 +46,11 @@ export class QueueController { public async deleteJob(@Param('id') id: string): Promise { return this.queueService.deleteJob(id); } + + @Get('job/:id/execute') + @HasPermission(permissions.accessAdminControl) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) + public async executeJob(@Param('id') id: string): Promise { + return this.queueService.executeJob(id); + } } diff --git a/apps/api/src/app/admin/queue/queue.service.ts b/apps/api/src/app/admin/queue/queue.service.ts index c5143e870..e4f29bc60 100644 --- a/apps/api/src/app/admin/queue/queue.service.ts +++ b/apps/api/src/app/admin/queue/queue.service.ts @@ -32,6 +32,10 @@ export class QueueService { } } + public async executeJob(aId: string) { + return (await this.dataGatheringQueue.getJob(aId))?.promote(); + } + public async getJobs({ limit = 1000, status = QUEUE_JOB_STATUS_LIST diff --git a/apps/client/src/app/components/admin-jobs/admin-jobs.component.ts b/apps/client/src/app/components/admin-jobs/admin-jobs.component.ts index 5eff103df..99d67dea2 100644 --- a/apps/client/src/app/components/admin-jobs/admin-jobs.component.ts +++ b/apps/client/src/app/components/admin-jobs/admin-jobs.component.ts @@ -98,6 +98,15 @@ export class AdminJobsComponent implements OnDestroy, OnInit { }); } + public onExecuteJob(aId: string) { + this.adminService + .executeJob(aId) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + this.fetchJobs(); + }); + } + public onViewData(aData: AdminJobs['jobs'][0]['data']) { alert(JSON.stringify(aData, null, ' ')); } diff --git a/apps/client/src/app/components/admin-jobs/admin-jobs.html b/apps/client/src/app/components/admin-jobs/admin-jobs.html index 12b31dfc8..31dad0ca6 100644 --- a/apps/client/src/app/components/admin-jobs/admin-jobs.html +++ b/apps/client/src/app/components/admin-jobs/admin-jobs.html @@ -147,6 +147,9 @@ > View Stacktrace + diff --git a/apps/client/src/app/services/admin.service.ts b/apps/client/src/app/services/admin.service.ts index df2c0b603..5bc281900 100644 --- a/apps/client/src/app/services/admin.service.ts +++ b/apps/client/src/app/services/admin.service.ts @@ -72,6 +72,10 @@ export class AdminService { return this.http.delete(`/api/v1/tag/${aId}`); } + public executeJob(aId: string) { + return this.http.get(`/api/v1/admin/queue/job/${aId}/execute`); + } + public fetchAdminData() { return this.http.get('/api/v1/admin'); } diff --git a/apps/client/src/locales/messages.de.xlf b/apps/client/src/locales/messages.de.xlf index f5c2bb6e8..033f5c58d 100644 --- a/apps/client/src/locales/messages.de.xlf +++ b/apps/client/src/locales/messages.de.xlf @@ -110,7 +110,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 302 + 300 apps/client/src/app/pages/portfolio/activities/activities-page.html @@ -130,7 +130,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 214 + 215 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -338,7 +338,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 272 + 270 @@ -426,7 +426,7 @@ Job löschen apps/client/src/app/components/admin-jobs/admin-jobs.html - 151 + 154 @@ -478,7 +478,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 353 + 365 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -486,7 +486,7 @@ apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html - 19 + 26 apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.html @@ -506,7 +506,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 412 + 408 apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html @@ -522,7 +522,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 360 + 372 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -530,7 +530,7 @@ apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html - 26 + 33 apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.html @@ -546,7 +546,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 419 + 415 @@ -838,7 +838,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 327 + 325 apps/client/src/app/pages/accounts/accounts-page.html @@ -1496,15 +1496,15 @@ Sektoren apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 190 + 191 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 312 + 316 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 241 + 239 apps/client/src/app/pages/public/public-page.html @@ -1516,15 +1516,15 @@ Länder apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 200 + 201 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 323 + 327 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 253 + 251 @@ -1536,11 +1536,11 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 347 + 345 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 367 + 363 libs/ui/src/lib/assistant/assistant.html @@ -1552,7 +1552,7 @@ Datenfehler melden apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 365 + 363 @@ -2064,7 +2064,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 220 + 222 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -2488,7 +2488,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 314 + 310 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2500,7 +2500,7 @@ Kommentar apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 340 + 352 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -2508,7 +2508,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 323 + 319 @@ -2524,15 +2524,15 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 229 + 232 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 199 + 197 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 338 + 334 @@ -2888,15 +2888,15 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 242 + 245 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 208 + 206 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 354 + 350 @@ -2904,11 +2904,11 @@ Sektor apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 176 + 174 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 225 + 223 @@ -2924,7 +2924,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 235 + 233 @@ -3472,7 +3472,7 @@ Symbol Zuordnung apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 276 + 279 @@ -4024,7 +4024,7 @@ Ups! Der historische Wechselkurs konnte nicht abgerufen werden vom apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 304 + 300 @@ -4148,7 +4148,7 @@ Url apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 334 + 339 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -4536,7 +4536,7 @@ Scraper Konfiguration apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 287 + 291 @@ -13356,7 +13356,7 @@ Benchmark apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 270 + 273 @@ -14668,7 +14668,7 @@ Test apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 305 + 309 @@ -15015,6 +15015,14 @@ 156 + + Execute Job + Job ausführen + + apps/client/src/app/components/admin-jobs/admin-jobs.html + 151 + + diff --git a/apps/client/src/locales/messages.es.xlf b/apps/client/src/locales/messages.es.xlf index 9afa12b84..c770a2dc2 100644 --- a/apps/client/src/locales/messages.es.xlf +++ b/apps/client/src/locales/messages.es.xlf @@ -111,7 +111,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 302 + 300 apps/client/src/app/pages/portfolio/activities/activities-page.html @@ -131,7 +131,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 214 + 215 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -339,7 +339,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 272 + 270 @@ -427,7 +427,7 @@ Elimina el trabajo apps/client/src/app/components/admin-jobs/admin-jobs.html - 151 + 154 @@ -479,7 +479,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 353 + 365 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -487,7 +487,7 @@ apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html - 19 + 26 apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.html @@ -507,7 +507,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 412 + 408 apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html @@ -523,7 +523,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 360 + 372 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -531,7 +531,7 @@ apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html - 26 + 33 apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.html @@ -547,7 +547,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 419 + 415 @@ -839,7 +839,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 327 + 325 apps/client/src/app/pages/accounts/accounts-page.html @@ -1494,15 +1494,15 @@ Sectores apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 190 + 191 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 312 + 316 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 241 + 239 apps/client/src/app/pages/public/public-page.html @@ -1514,15 +1514,15 @@ Países apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 200 + 201 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 323 + 327 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 253 + 251 @@ -1534,11 +1534,11 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 347 + 345 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 367 + 363 libs/ui/src/lib/assistant/assistant.html @@ -1550,7 +1550,7 @@ Reporta un anomalía de los datos apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 365 + 363 @@ -2062,7 +2062,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 220 + 222 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -2486,7 +2486,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 314 + 310 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2498,7 +2498,7 @@ Nota apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 340 + 352 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -2506,7 +2506,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 323 + 319 @@ -2522,15 +2522,15 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 229 + 232 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 199 + 197 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 338 + 334 @@ -2874,15 +2874,15 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 242 + 245 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 208 + 206 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 354 + 350 @@ -2930,11 +2930,11 @@ Sector apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 176 + 174 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 225 + 223 @@ -2950,7 +2950,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 235 + 233 @@ -3470,7 +3470,7 @@ Mapeo de símbolos apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 276 + 279 @@ -4022,7 +4022,7 @@ Oops! Could not get the historical exchange rate from apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 304 + 300 @@ -4146,7 +4146,7 @@ Url apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 334 + 339 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -4534,7 +4534,7 @@ Scraper Configuration apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 287 + 291 @@ -13354,7 +13354,7 @@ Benchmark apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 270 + 273 @@ -14666,7 +14666,7 @@ Test apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 305 + 309 @@ -15013,6 +15013,14 @@ 156 + + Execute Job + Execute Job + + apps/client/src/app/components/admin-jobs/admin-jobs.html + 151 + + diff --git a/apps/client/src/locales/messages.fr.xlf b/apps/client/src/locales/messages.fr.xlf index c315dda63..3a241feda 100644 --- a/apps/client/src/locales/messages.fr.xlf +++ b/apps/client/src/locales/messages.fr.xlf @@ -122,7 +122,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 302 + 300 apps/client/src/app/pages/portfolio/activities/activities-page.html @@ -142,7 +142,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 214 + 215 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -198,7 +198,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 220 + 222 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -386,7 +386,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 272 + 270 @@ -482,7 +482,7 @@ Supprimer Tâche apps/client/src/app/components/admin-jobs/admin-jobs.html - 151 + 154 @@ -534,7 +534,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 353 + 365 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -542,7 +542,7 @@ apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html - 19 + 26 apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.html @@ -562,7 +562,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 412 + 408 apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html @@ -578,7 +578,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 360 + 372 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -586,7 +586,7 @@ apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html - 26 + 33 apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.html @@ -602,7 +602,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 419 + 415 @@ -626,15 +626,15 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 229 + 232 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 199 + 197 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 338 + 334 @@ -650,15 +650,15 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 242 + 245 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 208 + 206 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 354 + 350 @@ -758,11 +758,11 @@ Secteur apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 176 + 174 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 225 + 223 @@ -778,7 +778,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 235 + 233 @@ -786,15 +786,15 @@ Secteurs apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 190 + 191 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 312 + 316 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 241 + 239 apps/client/src/app/pages/public/public-page.html @@ -806,15 +806,15 @@ Pays apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 200 + 201 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 323 + 327 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 253 + 251 @@ -822,7 +822,7 @@ Équivalence de Symboles apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 276 + 279 @@ -830,7 +830,7 @@ Note apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 340 + 352 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -838,7 +838,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 323 + 319 @@ -930,11 +930,11 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 347 + 345 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 367 + 363 libs/ui/src/lib/assistant/assistant.html @@ -1050,7 +1050,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 327 + 325 apps/client/src/app/pages/accounts/accounts-page.html @@ -1913,7 +1913,7 @@ Signaler une Erreur de Données apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 365 + 363 @@ -2697,7 +2697,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 314 + 310 libs/ui/src/lib/activities-table/activities-table.component.html @@ -4021,7 +4021,7 @@ Oups ! Nous n'avons pas pu obtenir le taux de change historique à partir de apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 304 + 300 @@ -4145,7 +4145,7 @@ Lien apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 334 + 339 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -4533,7 +4533,7 @@ Scraper Configuration apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 287 + 291 @@ -13353,7 +13353,7 @@ Benchmark apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 270 + 273 @@ -14665,7 +14665,7 @@ Test apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 305 + 309 @@ -15012,6 +15012,14 @@ 156 + + Execute Job + Execute Job + + apps/client/src/app/components/admin-jobs/admin-jobs.html + 151 + + diff --git a/apps/client/src/locales/messages.it.xlf b/apps/client/src/locales/messages.it.xlf index 631ad824c..99f669151 100644 --- a/apps/client/src/locales/messages.it.xlf +++ b/apps/client/src/locales/messages.it.xlf @@ -111,7 +111,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 302 + 300 apps/client/src/app/pages/portfolio/activities/activities-page.html @@ -131,7 +131,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 214 + 215 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -339,7 +339,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 272 + 270 @@ -427,7 +427,7 @@ Elimina il lavoro apps/client/src/app/components/admin-jobs/admin-jobs.html - 151 + 154 @@ -479,7 +479,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 353 + 365 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -487,7 +487,7 @@ apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html - 19 + 26 apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.html @@ -507,7 +507,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 412 + 408 apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html @@ -523,7 +523,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 360 + 372 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -531,7 +531,7 @@ apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html - 26 + 33 apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.html @@ -547,7 +547,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 419 + 415 @@ -839,7 +839,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 327 + 325 apps/client/src/app/pages/accounts/accounts-page.html @@ -1494,15 +1494,15 @@ Settori apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 190 + 191 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 312 + 316 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 241 + 239 apps/client/src/app/pages/public/public-page.html @@ -1514,15 +1514,15 @@ Paesi apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 200 + 201 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 323 + 327 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 253 + 251 @@ -1534,11 +1534,11 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 347 + 345 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 367 + 363 libs/ui/src/lib/assistant/assistant.html @@ -1550,7 +1550,7 @@ Segnala un'anomalia dei dati apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 365 + 363 @@ -2062,7 +2062,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 220 + 222 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -2486,7 +2486,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 314 + 310 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2498,7 +2498,7 @@ Nota apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 340 + 352 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -2506,7 +2506,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 323 + 319 @@ -2522,15 +2522,15 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 229 + 232 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 199 + 197 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 338 + 334 @@ -2874,15 +2874,15 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 242 + 245 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 208 + 206 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 354 + 350 @@ -2930,11 +2930,11 @@ Settore apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 176 + 174 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 225 + 223 @@ -2950,7 +2950,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 235 + 233 @@ -3470,7 +3470,7 @@ Mappatura dei simboli apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 276 + 279 @@ -4022,7 +4022,7 @@ Ops! Impossibile ottenere il tasso di cambio storico da apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 304 + 300 @@ -4146,7 +4146,7 @@ Url apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 334 + 339 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -4534,7 +4534,7 @@ Configurazione dello scraper apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 287 + 291 @@ -13354,7 +13354,7 @@ Benchmark apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 270 + 273 @@ -14666,7 +14666,7 @@ Test apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 305 + 309 @@ -15013,6 +15013,14 @@ 156 + + Execute Job + Execute Job + + apps/client/src/app/components/admin-jobs/admin-jobs.html + 151 + + diff --git a/apps/client/src/locales/messages.nl.xlf b/apps/client/src/locales/messages.nl.xlf index 6464af55e..9116a411a 100644 --- a/apps/client/src/locales/messages.nl.xlf +++ b/apps/client/src/locales/messages.nl.xlf @@ -110,7 +110,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 302 + 300 apps/client/src/app/pages/portfolio/activities/activities-page.html @@ -130,7 +130,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 214 + 215 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -338,7 +338,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 272 + 270 @@ -426,7 +426,7 @@ Taak verwijderen apps/client/src/app/components/admin-jobs/admin-jobs.html - 151 + 154 @@ -478,7 +478,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 353 + 365 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -486,7 +486,7 @@ apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html - 19 + 26 apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.html @@ -506,7 +506,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 412 + 408 apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html @@ -522,7 +522,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 360 + 372 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -530,7 +530,7 @@ apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html - 26 + 33 apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.html @@ -546,7 +546,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 419 + 415 @@ -838,7 +838,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 327 + 325 apps/client/src/app/pages/accounts/accounts-page.html @@ -1493,15 +1493,15 @@ Sectoren apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 190 + 191 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 312 + 316 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 241 + 239 apps/client/src/app/pages/public/public-page.html @@ -1513,15 +1513,15 @@ Landen apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 200 + 201 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 323 + 327 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 253 + 251 @@ -1533,11 +1533,11 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 347 + 345 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 367 + 363 libs/ui/src/lib/assistant/assistant.html @@ -1549,7 +1549,7 @@ Gegevensstoring melden apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 365 + 363 @@ -2061,7 +2061,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 220 + 222 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -2485,7 +2485,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 314 + 310 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2497,7 +2497,7 @@ Opmerking apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 340 + 352 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -2505,7 +2505,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 323 + 319 @@ -2521,15 +2521,15 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 229 + 232 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 199 + 197 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 338 + 334 @@ -2873,15 +2873,15 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 242 + 245 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 208 + 206 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 354 + 350 @@ -2929,11 +2929,11 @@ Sector apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 176 + 174 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 225 + 223 @@ -2949,7 +2949,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 235 + 233 @@ -3469,7 +3469,7 @@ Symbool toewijzen apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 276 + 279 @@ -4021,7 +4021,7 @@ Oeps! Kon de historische wisselkoers niet krijgen van apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 304 + 300 @@ -4145,7 +4145,7 @@ Url apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 334 + 339 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -4533,7 +4533,7 @@ Scraper instellingen apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 287 + 291 @@ -13353,7 +13353,7 @@ Benchmark apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 270 + 273 @@ -14665,7 +14665,7 @@ Test apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 305 + 309 @@ -15012,6 +15012,14 @@ 156 + + Execute Job + Execute Job + + apps/client/src/app/components/admin-jobs/admin-jobs.html + 151 + + diff --git a/apps/client/src/locales/messages.pl.xlf b/apps/client/src/locales/messages.pl.xlf index 5d685039c..c02f78aee 100644 --- a/apps/client/src/locales/messages.pl.xlf +++ b/apps/client/src/locales/messages.pl.xlf @@ -1626,7 +1626,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 302 + 300 apps/client/src/app/pages/portfolio/activities/activities-page.html @@ -1674,7 +1674,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 214 + 215 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -1730,7 +1730,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 220 + 222 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -1918,7 +1918,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 272 + 270 @@ -1998,7 +1998,7 @@ Delete Job apps/client/src/app/components/admin-jobs/admin-jobs.html - 151 + 154 @@ -2050,7 +2050,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 353 + 365 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -2058,7 +2058,7 @@ apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html - 19 + 26 apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.html @@ -2078,7 +2078,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 412 + 408 apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html @@ -2094,7 +2094,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 360 + 372 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -2102,7 +2102,7 @@ apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html - 26 + 33 apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.html @@ -2118,7 +2118,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 419 + 415 @@ -2174,15 +2174,15 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 229 + 232 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 199 + 197 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 338 + 334 @@ -2198,15 +2198,15 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 242 + 245 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 208 + 206 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 354 + 350 @@ -2338,11 +2338,11 @@ Sector apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 176 + 174 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 225 + 223 @@ -2358,7 +2358,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 235 + 233 @@ -2366,15 +2366,15 @@ Sectors apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 190 + 191 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 312 + 316 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 241 + 239 apps/client/src/app/pages/public/public-page.html @@ -2386,15 +2386,15 @@ Countries apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 200 + 201 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 323 + 327 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 253 + 251 @@ -2402,7 +2402,7 @@ Benchmark apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 270 + 273 @@ -2410,7 +2410,7 @@ Symbol Mapping apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 276 + 279 @@ -2418,7 +2418,7 @@ Scraper Configuration apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 287 + 291 @@ -2426,7 +2426,7 @@ Note apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 340 + 352 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -2434,7 +2434,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 323 + 319 @@ -2646,7 +2646,7 @@ Url apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 334 + 339 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -2678,7 +2678,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 327 + 325 apps/client/src/app/pages/accounts/accounts-page.html @@ -2730,11 +2730,11 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 347 + 345 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 367 + 363 libs/ui/src/lib/assistant/assistant.html @@ -3528,7 +3528,7 @@ Report Data Glitch apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 365 + 363 @@ -5068,7 +5068,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 314 + 310 libs/ui/src/lib/activities-table/activities-table.component.html @@ -5080,7 +5080,7 @@ Oops! Could not get the historical exchange rate from apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 304 + 300 @@ -14668,7 +14668,7 @@ Test apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 305 + 309 @@ -15015,6 +15015,14 @@ 156 + + Execute Job + Execute Job + + apps/client/src/app/components/admin-jobs/admin-jobs.html + 151 + + diff --git a/apps/client/src/locales/messages.pt.xlf b/apps/client/src/locales/messages.pt.xlf index 11794a252..6ebacade4 100644 --- a/apps/client/src/locales/messages.pt.xlf +++ b/apps/client/src/locales/messages.pt.xlf @@ -122,7 +122,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 302 + 300 apps/client/src/app/pages/portfolio/activities/activities-page.html @@ -142,7 +142,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 214 + 215 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -198,7 +198,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 220 + 222 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -386,7 +386,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 272 + 270 @@ -482,7 +482,7 @@ Apagar Tarefa apps/client/src/app/components/admin-jobs/admin-jobs.html - 151 + 154 @@ -534,7 +534,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 353 + 365 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -542,7 +542,7 @@ apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html - 19 + 26 apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.html @@ -562,7 +562,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 412 + 408 apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html @@ -578,7 +578,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 360 + 372 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -586,7 +586,7 @@ apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html - 26 + 33 apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.html @@ -602,7 +602,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 419 + 415 @@ -626,15 +626,15 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 229 + 232 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 199 + 197 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 338 + 334 @@ -650,15 +650,15 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 242 + 245 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 208 + 206 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 354 + 350 @@ -918,7 +918,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 327 + 325 apps/client/src/app/pages/accounts/accounts-page.html @@ -1789,11 +1789,11 @@ Setor apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 176 + 174 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 225 + 223 @@ -1809,7 +1809,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 235 + 233 @@ -1817,15 +1817,15 @@ Setores apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 190 + 191 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 312 + 316 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 241 + 239 apps/client/src/app/pages/public/public-page.html @@ -1837,15 +1837,15 @@ Países apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 200 + 201 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 323 + 327 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 253 + 251 @@ -1857,11 +1857,11 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 347 + 345 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 367 + 363 libs/ui/src/lib/assistant/assistant.html @@ -1873,7 +1873,7 @@ Dados do Relatório com Problema apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 365 + 363 @@ -2601,7 +2601,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 314 + 310 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2613,7 +2613,7 @@ Nota apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 340 + 352 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -2621,7 +2621,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 323 + 319 @@ -3437,7 +3437,7 @@ Mapeamento de Símbolo apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 276 + 279 @@ -4021,7 +4021,7 @@ Oops! Não foi possível obter a taxa de câmbio histórica de apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 304 + 300 @@ -4145,7 +4145,7 @@ Url apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 334 + 339 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -4533,7 +4533,7 @@ Scraper Configuration apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 287 + 291 @@ -13353,7 +13353,7 @@ Benchmark apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 270 + 273 @@ -14665,7 +14665,7 @@ Test apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 305 + 309 @@ -15012,6 +15012,14 @@ 156 + + Execute Job + Execute Job + + apps/client/src/app/components/admin-jobs/admin-jobs.html + 151 + + diff --git a/apps/client/src/locales/messages.tr.xlf b/apps/client/src/locales/messages.tr.xlf index 7bd98ba40..39f5817ce 100644 --- a/apps/client/src/locales/messages.tr.xlf +++ b/apps/client/src/locales/messages.tr.xlf @@ -1618,7 +1618,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 302 + 300 apps/client/src/app/pages/portfolio/activities/activities-page.html @@ -1638,7 +1638,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 214 + 215 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -1694,7 +1694,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 220 + 222 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -1866,7 +1866,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 272 + 270 @@ -1962,7 +1962,7 @@ İşleri Sil apps/client/src/app/components/admin-jobs/admin-jobs.html - 151 + 154 @@ -2014,7 +2014,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 353 + 365 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -2022,7 +2022,7 @@ apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html - 19 + 26 apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.html @@ -2042,7 +2042,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 412 + 408 apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html @@ -2058,7 +2058,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 360 + 372 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -2066,7 +2066,7 @@ apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html - 26 + 33 apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.html @@ -2082,7 +2082,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 419 + 415 @@ -2130,15 +2130,15 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 229 + 232 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 199 + 197 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 338 + 334 @@ -2154,15 +2154,15 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 242 + 245 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 208 + 206 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 354 + 350 @@ -2270,11 +2270,11 @@ Sektör apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 176 + 174 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 225 + 223 @@ -2290,7 +2290,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 235 + 233 @@ -2298,15 +2298,15 @@ Sektörler apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 190 + 191 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 312 + 316 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 241 + 239 apps/client/src/app/pages/public/public-page.html @@ -2318,15 +2318,15 @@ Ülkeler apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 200 + 201 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 323 + 327 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 253 + 251 @@ -2334,7 +2334,7 @@ Sembol Eşleştirme apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 276 + 279 @@ -2342,7 +2342,7 @@ Veri Toplayıcı Yapılandırması apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 287 + 291 @@ -2350,7 +2350,7 @@ Not apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 340 + 352 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -2358,7 +2358,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 323 + 319 @@ -2470,11 +2470,11 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 347 + 345 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 367 + 363 libs/ui/src/lib/assistant/assistant.html @@ -2558,7 +2558,7 @@ Url apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 334 + 339 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -2590,7 +2590,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 327 + 325 apps/client/src/app/pages/accounts/accounts-page.html @@ -3369,7 +3369,7 @@ Rapor Veri Sorunu apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 365 + 363 @@ -4533,7 +4533,7 @@ Hay Allah! Geçmiş döviz kuru alınamadı: apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 304 + 300 @@ -4545,7 +4545,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 314 + 310 libs/ui/src/lib/activities-table/activities-table.component.html @@ -13353,7 +13353,7 @@ Benchmark apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 270 + 273 @@ -14665,7 +14665,7 @@ Test apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 305 + 309 @@ -15012,6 +15012,14 @@ 156 + + Execute Job + Execute Job + + apps/client/src/app/components/admin-jobs/admin-jobs.html + 151 + + diff --git a/apps/client/src/locales/messages.xlf b/apps/client/src/locales/messages.xlf index f11713076..53ff1578b 100644 --- a/apps/client/src/locales/messages.xlf +++ b/apps/client/src/locales/messages.xlf @@ -1594,7 +1594,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 302 + 300 apps/client/src/app/pages/portfolio/activities/activities-page.html @@ -1646,7 +1646,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 214 + 215 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -1700,7 +1700,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 220 + 222 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -1881,7 +1881,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 272 + 270 @@ -1952,7 +1952,7 @@ Delete Job apps/client/src/app/components/admin-jobs/admin-jobs.html - 151 + 154 @@ -2000,7 +2000,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 353 + 365 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -2008,7 +2008,7 @@ apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html - 19 + 26 apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.html @@ -2028,7 +2028,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 412 + 408 apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html @@ -2043,7 +2043,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 360 + 372 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -2051,7 +2051,7 @@ apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html - 26 + 33 apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.html @@ -2067,7 +2067,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 419 + 415 @@ -2117,15 +2117,15 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 229 + 232 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 199 + 197 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 338 + 334 @@ -2140,15 +2140,15 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 242 + 245 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 208 + 206 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 354 + 350 @@ -2267,11 +2267,11 @@ Sector apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 176 + 174 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 225 + 223 @@ -2286,22 +2286,22 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 235 + 233 Sectors apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 190 + 191 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 312 + 316 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 241 + 239 apps/client/src/app/pages/public/public-page.html @@ -2312,43 +2312,43 @@ Countries apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 200 + 201 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 323 + 327 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 253 + 251 Benchmark apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 270 + 273 Symbol Mapping apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 276 + 279 Scraper Configuration apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 287 + 291 Note apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 340 + 352 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -2356,7 +2356,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 323 + 319 @@ -2549,7 +2549,7 @@ Url apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 334 + 339 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -2580,7 +2580,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 327 + 325 apps/client/src/app/pages/accounts/accounts-page.html @@ -2627,11 +2627,11 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 347 + 345 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 367 + 363 libs/ui/src/lib/assistant/assistant.html @@ -3340,7 +3340,7 @@ Report Data Glitch apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 365 + 363 @@ -4719,7 +4719,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 314 + 310 libs/ui/src/lib/activities-table/activities-table.component.html @@ -4730,7 +4730,7 @@ Oops! Could not get the historical exchange rate from apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 304 + 300 @@ -14086,7 +14086,7 @@ Test apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 305 + 309 @@ -14387,6 +14387,13 @@ 156 + + Execute Job + + apps/client/src/app/components/admin-jobs/admin-jobs.html + 151 + + diff --git a/apps/client/src/locales/messages.zh.xlf b/apps/client/src/locales/messages.zh.xlf index af033158b..fa47da7e0 100644 --- a/apps/client/src/locales/messages.zh.xlf +++ b/apps/client/src/locales/messages.zh.xlf @@ -1627,7 +1627,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 302 + 300 apps/client/src/app/pages/portfolio/activities/activities-page.html @@ -1683,7 +1683,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 214 + 215 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -1739,7 +1739,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 220 + 222 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -1927,7 +1927,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 272 + 270 @@ -2007,7 +2007,7 @@ 删除作业 apps/client/src/app/components/admin-jobs/admin-jobs.html - 151 + 154 @@ -2059,7 +2059,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 353 + 365 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -2067,7 +2067,7 @@ apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html - 19 + 26 apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.html @@ -2087,7 +2087,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 412 + 408 apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html @@ -2103,7 +2103,7 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 360 + 372 apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -2111,7 +2111,7 @@ apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html - 26 + 33 apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.html @@ -2127,7 +2127,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 419 + 415 @@ -2183,15 +2183,15 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 229 + 232 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 199 + 197 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 338 + 334 @@ -2207,15 +2207,15 @@ apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 242 + 245 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 208 + 206 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 354 + 350 @@ -2347,11 +2347,11 @@ 行业 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 176 + 174 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 225 + 223 @@ -2367,7 +2367,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 235 + 233 @@ -2375,15 +2375,15 @@ 行业 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 190 + 191 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 312 + 316 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 241 + 239 apps/client/src/app/pages/public/public-page.html @@ -2395,15 +2395,15 @@ 国家 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 200 + 201 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 323 + 327 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 253 + 251 @@ -2411,7 +2411,7 @@ 基准 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 270 + 273 @@ -2419,7 +2419,7 @@ 符号映射 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 276 + 279 @@ -2427,7 +2427,7 @@ 刮削配置 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 287 + 291 @@ -2435,7 +2435,7 @@ 笔记 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 340 + 352 apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -2443,7 +2443,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 323 + 319 @@ -2663,7 +2663,7 @@ 网址 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 334 + 339 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -2695,7 +2695,7 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 327 + 325 apps/client/src/app/pages/accounts/accounts-page.html @@ -2747,11 +2747,11 @@ apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 347 + 345 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 367 + 363 libs/ui/src/lib/assistant/assistant.html @@ -3545,7 +3545,7 @@ 报告数据故障 apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html - 365 + 363 @@ -5085,7 +5085,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 314 + 310 libs/ui/src/lib/activities-table/activities-table.component.html @@ -5097,7 +5097,7 @@ 哎呀!无法获取历史汇率 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 304 + 300 @@ -14677,7 +14677,7 @@ 测试 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html - 305 + 309 @@ -15016,6 +15016,14 @@ 156 + + Execute Job + Execute Job + + apps/client/src/app/components/admin-jobs/admin-jobs.html + 151 + + From 6f8fe45fc277aeea5332674f82f86e201dbe6be1 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 11 Apr 2024 19:14:56 +0200 Subject: [PATCH 113/120] Update OSS Friends (#3258) --- apps/client/src/assets/oss-friends.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/client/src/assets/oss-friends.json b/apps/client/src/assets/oss-friends.json index cbbbd5987..88037573f 100644 --- a/apps/client/src/assets/oss-friends.json +++ b/apps/client/src/assets/oss-friends.json @@ -1,5 +1,5 @@ { - "createdAt": "2024-03-11T00:00:00.000Z", + "createdAt": "2024-04-09T00:00:00.000Z", "data": [ { "name": "Aptabase", @@ -86,6 +86,11 @@ "description": "Open source, end-to-end encrypted platform that lets you securely manage secrets and configs across your team, devices, and infrastructure.", "href": "https://infisical.com" }, + { + "name": "Keep", + "description": "Open source alert management and AIOps platform.", + "href": "https://keephq.dev" + }, { "name": "Langfuse", "description": "Open source LLM engineering platform. Debug, analyze and iterate together.", @@ -119,7 +124,7 @@ { "name": "Requestly", "description": "Makes frontend development cycle 10x faster with API Client, Mock Server, Intercept & Modify HTTP Requests and Session Replays.", - "href": "https://requestly.io" + "href": "https://requestly.com" }, { "name": "Revert", From 45340b581f4b39f775b7d17645fb70b368852b47 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 12 Apr 2024 15:13:34 +0200 Subject: [PATCH 114/120] Bugfix/fix public page by including markets data (#3263) * Include markets data * Update changelog --- CHANGELOG.md | 4 ++++ apps/api/src/app/portfolio/portfolio.controller.ts | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbb92106f..68a85fd95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Upgraded `Nx` from version `18.1.2` to `18.2.3` - Upgraded `prisma` from version `5.11.0` to `5.12.1` +### Fixed + +- Fixed an issue in the public page + ## 2.71.0 - 2024-04-07 ### Added diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 0ae596d84..efb9318d7 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -539,7 +539,8 @@ export class PortfolioController { const { holdings } = await this.portfolioService.getDetails({ filters: [{ id: 'EQUITY', type: 'ASSET_CLASS' }], impersonationId: access.userId, - userId: user.id + userId: user.id, + withMarkets: true }); const portfolioPublicDetails: PortfolioPublicDetails = { From 4e7d93db13c6af29bd222c1f93876544d69fdc65 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 13 Apr 2024 09:28:14 +0200 Subject: [PATCH 115/120] Feature/adapt priorities of data gathering jobs (#3262) * Adapt priorities of data gathering jobs * Update changelog --- CHANGELOG.md | 2 + apps/api/src/app/admin/admin.controller.ts | 16 +++---- apps/api/src/app/admin/queue/queue.service.ts | 1 + apps/api/src/app/import/import.service.ts | 13 ++++-- apps/api/src/app/order/order.controller.ts | 22 ++++++---- apps/api/src/app/order/order.service.ts | 22 ++++++---- apps/api/src/services/cron.service.ts | 4 +- .../data-gathering/data-gathering.service.ts | 40 +++++++++++++----- .../admin-jobs/admin-jobs.component.ts | 13 +++++- .../app/components/admin-jobs/admin-jobs.html | 42 ++++++++++++++++--- libs/common/src/lib/config.ts | 7 ++-- .../lib/interfaces/admin-jobs.interface.ts | 1 + 12 files changed, 136 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68a85fd95..73999efb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,9 +10,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added support to immediately execute a queue job from the admin control panel +- Added a priority column to the queue jobs view in the admin control panel ### Changed +- Adapted the priorities of queue jobs - Upgraded `angular` from version `17.2.4` to `17.3.3` - Upgraded `Nx` from version `18.1.2` to `18.2.3` - Upgraded `prisma` from version `5.11.0` to `5.12.1` diff --git a/apps/api/src/app/admin/admin.controller.ts b/apps/api/src/app/admin/admin.controller.ts index dab8fb8b2..298a471c3 100644 --- a/apps/api/src/app/admin/admin.controller.ts +++ b/apps/api/src/app/admin/admin.controller.ts @@ -7,13 +7,12 @@ import { ManualService } from '@ghostfolio/api/services/data-provider/manual/man import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { PropertyDto } from '@ghostfolio/api/services/property/property.dto'; import { + DATA_GATHERING_QUEUE_PRIORITY_HIGH, + DATA_GATHERING_QUEUE_PRIORITY_MEDIUM, GATHER_ASSET_PROFILE_PROCESS, GATHER_ASSET_PROFILE_PROCESS_OPTIONS } from '@ghostfolio/common/config'; -import { - getAssetProfileIdentifier, - resetHours -} from '@ghostfolio/common/helper'; +import { getAssetProfileIdentifier } from '@ghostfolio/common/helper'; import { AdminData, AdminMarketData, @@ -94,7 +93,8 @@ export class AdminController { name: GATHER_ASSET_PROFILE_PROCESS, opts: { ...GATHER_ASSET_PROFILE_PROCESS_OPTIONS, - jobId: getAssetProfileIdentifier({ dataSource, symbol }) + jobId: getAssetProfileIdentifier({ dataSource, symbol }), + priority: DATA_GATHERING_QUEUE_PRIORITY_MEDIUM } }; }) @@ -119,7 +119,8 @@ export class AdminController { name: GATHER_ASSET_PROFILE_PROCESS, opts: { ...GATHER_ASSET_PROFILE_PROCESS_OPTIONS, - jobId: getAssetProfileIdentifier({ dataSource, symbol }) + jobId: getAssetProfileIdentifier({ dataSource, symbol }), + priority: DATA_GATHERING_QUEUE_PRIORITY_MEDIUM } }; }) @@ -141,7 +142,8 @@ export class AdminController { name: GATHER_ASSET_PROFILE_PROCESS, opts: { ...GATHER_ASSET_PROFILE_PROCESS_OPTIONS, - jobId: getAssetProfileIdentifier({ dataSource, symbol }) + jobId: getAssetProfileIdentifier({ dataSource, symbol }), + priority: DATA_GATHERING_QUEUE_PRIORITY_HIGH } }); } diff --git a/apps/api/src/app/admin/queue/queue.service.ts b/apps/api/src/app/admin/queue/queue.service.ts index e4f29bc60..abae3cad1 100644 --- a/apps/api/src/app/admin/queue/queue.service.ts +++ b/apps/api/src/app/admin/queue/queue.service.ts @@ -58,6 +58,7 @@ export class QueueService { finishedOn: job.finishedOn, id: job.id, name: job.name, + opts: job.opts, stacktrace: job.stacktrace, state: await job.getState(), timestamp: job.timestamp diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index cbdff87c0..26df9d069 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -13,6 +13,10 @@ import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/da import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; +import { + DATA_GATHERING_QUEUE_PRIORITY_HIGH, + DATA_GATHERING_QUEUE_PRIORITY_MEDIUM +} from '@ghostfolio/common/config'; import { DATE_FORMAT, getAssetProfileIdentifier, @@ -448,15 +452,16 @@ export class ImportService { }); }); - this.dataGatheringService.gatherSymbols( - uniqueActivities.map(({ date, SymbolProfile }) => { + this.dataGatheringService.gatherSymbols({ + dataGatheringItems: uniqueActivities.map(({ date, SymbolProfile }) => { return { date, dataSource: SymbolProfile.dataSource, symbol: SymbolProfile.symbol }; - }) - ); + }), + priority: DATA_GATHERING_QUEUE_PRIORITY_HIGH + }); } return activities; diff --git a/apps/api/src/app/order/order.controller.ts b/apps/api/src/app/order/order.controller.ts index c7fec0dac..3dadedcaf 100644 --- a/apps/api/src/app/order/order.controller.ts +++ b/apps/api/src/app/order/order.controller.ts @@ -7,7 +7,10 @@ import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interc import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service'; -import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; +import { + DATA_GATHERING_QUEUE_PRIORITY_HIGH, + HEADER_KEY_IMPERSONATION +} from '@ghostfolio/common/config'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import type { DateRange, RequestWithUser } from '@ghostfolio/common/types'; @@ -160,13 +163,16 @@ export class OrderController { if (data.dataSource && !order.isDraft) { // Gather symbol data in the background, if data source is set // (not MANUAL) and not draft - this.dataGatheringService.gatherSymbols([ - { - dataSource: data.dataSource, - date: order.date, - symbol: data.symbol - } - ]); + this.dataGatheringService.gatherSymbols({ + dataGatheringItems: [ + { + dataSource: data.dataSource, + date: order.date, + symbol: data.symbol + } + ], + priority: DATA_GATHERING_QUEUE_PRIORITY_HIGH + }); } return order; diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index 20b2d5f15..35bfa1bcf 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -4,6 +4,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate- import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { + DATA_GATHERING_QUEUE_PRIORITY_HIGH, GATHER_ASSET_PROFILE_PROCESS, GATHER_ASSET_PROFILE_PROCESS_OPTIONS } from '@ghostfolio/common/config'; @@ -101,7 +102,8 @@ export class OrderService { jobId: getAssetProfileIdentifier({ dataSource: data.SymbolProfile.connectOrCreate.create.dataSource, symbol: data.SymbolProfile.connectOrCreate.create.symbol - }) + }), + priority: DATA_GATHERING_QUEUE_PRIORITY_HIGH } }); } @@ -427,13 +429,17 @@ export class OrderService { if (!isDraft) { // Gather symbol data of order in the background, if not draft - this.dataGatheringService.gatherSymbols([ - { - dataSource: data.SymbolProfile.connect.dataSource_symbol.dataSource, - date: data.date, - symbol: data.SymbolProfile.connect.dataSource_symbol.symbol - } - ]); + this.dataGatheringService.gatherSymbols({ + dataGatheringItems: [ + { + dataSource: + data.SymbolProfile.connect.dataSource_symbol.dataSource, + date: data.date, + symbol: data.SymbolProfile.connect.dataSource_symbol.symbol + } + ], + priority: DATA_GATHERING_QUEUE_PRIORITY_HIGH + }); } } diff --git a/apps/api/src/services/cron.service.ts b/apps/api/src/services/cron.service.ts index d74ad6a94..fc5d613a2 100644 --- a/apps/api/src/services/cron.service.ts +++ b/apps/api/src/services/cron.service.ts @@ -1,4 +1,5 @@ import { + DATA_GATHERING_QUEUE_PRIORITY_LOW, GATHER_ASSET_PROFILE_PROCESS, GATHER_ASSET_PROFILE_PROCESS_OPTIONS, PROPERTY_IS_DATA_GATHERING_ENABLED @@ -56,7 +57,8 @@ export class CronService { name: GATHER_ASSET_PROFILE_PROCESS, opts: { ...GATHER_ASSET_PROFILE_PROCESS_OPTIONS, - jobId: getAssetProfileIdentifier({ dataSource, symbol }) + jobId: getAssetProfileIdentifier({ dataSource, symbol }), + priority: DATA_GATHERING_QUEUE_PRIORITY_LOW } }; }) diff --git a/apps/api/src/services/data-gathering/data-gathering.service.ts b/apps/api/src/services/data-gathering/data-gathering.service.ts index 6dccd645e..b2b0c371c 100644 --- a/apps/api/src/services/data-gathering/data-gathering.service.ts +++ b/apps/api/src/services/data-gathering/data-gathering.service.ts @@ -8,6 +8,8 @@ import { PropertyService } from '@ghostfolio/api/services/property/property.serv import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { DATA_GATHERING_QUEUE, + DATA_GATHERING_QUEUE_PRIORITY_HIGH, + DATA_GATHERING_QUEUE_PRIORITY_LOW, GATHER_HISTORICAL_MARKET_DATA_PROCESS, GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS, PROPERTY_BENCHMARKS @@ -61,24 +63,35 @@ export class DataGatheringService { public async gather7Days() { const dataGatheringItems = await this.getSymbols7D(); - await this.gatherSymbols(dataGatheringItems); + await this.gatherSymbols({ + dataGatheringItems, + priority: DATA_GATHERING_QUEUE_PRIORITY_LOW + }); } public async gatherMax() { const dataGatheringItems = await this.getSymbolsMax(); - await this.gatherSymbols(dataGatheringItems); + await this.gatherSymbols({ + dataGatheringItems, + priority: DATA_GATHERING_QUEUE_PRIORITY_LOW + }); } public async gatherSymbol({ dataSource, symbol }: UniqueAsset) { await this.marketDataService.deleteMany({ dataSource, symbol }); - const symbols = (await this.getSymbolsMax()).filter((dataGatheringItem) => { - return ( - dataGatheringItem.dataSource === dataSource && - dataGatheringItem.symbol === symbol - ); + const dataGatheringItems = (await this.getSymbolsMax()).filter( + (dataGatheringItem) => { + return ( + dataGatheringItem.dataSource === dataSource && + dataGatheringItem.symbol === symbol + ); + } + ); + await this.gatherSymbols({ + dataGatheringItems, + priority: DATA_GATHERING_QUEUE_PRIORITY_HIGH }); - await this.gatherSymbols(symbols); } public async gatherSymbolForDate({ @@ -230,9 +243,15 @@ export class DataGatheringService { ); } - public async gatherSymbols(aSymbolsWithStartDate: IDataGatheringItem[]) { + public async gatherSymbols({ + dataGatheringItems, + priority + }: { + dataGatheringItems: IDataGatheringItem[]; + priority: number; + }) { await this.addJobsToQueue( - aSymbolsWithStartDate.map(({ dataSource, date, symbol }) => { + dataGatheringItems.map(({ dataSource, date, symbol }) => { return { data: { dataSource, @@ -242,6 +261,7 @@ export class DataGatheringService { name: GATHER_HISTORICAL_MARKET_DATA_PROCESS, opts: { ...GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS, + priority, jobId: `${getAssetProfileIdentifier({ dataSource, symbol diff --git a/apps/client/src/app/components/admin-jobs/admin-jobs.component.ts b/apps/client/src/app/components/admin-jobs/admin-jobs.component.ts index 99d67dea2..23730f3aa 100644 --- a/apps/client/src/app/components/admin-jobs/admin-jobs.component.ts +++ b/apps/client/src/app/components/admin-jobs/admin-jobs.component.ts @@ -1,6 +1,11 @@ import { AdminService } from '@ghostfolio/client/services/admin.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; -import { QUEUE_JOB_STATUS_LIST } from '@ghostfolio/common/config'; +import { + DATA_GATHERING_QUEUE_PRIORITY_HIGH, + DATA_GATHERING_QUEUE_PRIORITY_LOW, + DATA_GATHERING_QUEUE_PRIORITY_MEDIUM, + QUEUE_JOB_STATUS_LIST +} from '@ghostfolio/common/config'; import { getDateWithTimeFormatString } from '@ghostfolio/common/helper'; import { AdminJobs, User } from '@ghostfolio/common/interfaces'; @@ -24,6 +29,11 @@ import { takeUntil } from 'rxjs/operators'; templateUrl: './admin-jobs.html' }) export class AdminJobsComponent implements OnDestroy, OnInit { + public DATA_GATHERING_QUEUE_PRIORITY_LOW = DATA_GATHERING_QUEUE_PRIORITY_LOW; + public DATA_GATHERING_QUEUE_PRIORITY_HIGH = + DATA_GATHERING_QUEUE_PRIORITY_HIGH; + public DATA_GATHERING_QUEUE_PRIORITY_MEDIUM = + DATA_GATHERING_QUEUE_PRIORITY_MEDIUM; public defaultDateTimeFormat: string; public filterForm: FormGroup; public dataSource: MatTableDataSource = @@ -33,6 +43,7 @@ export class AdminJobsComponent implements OnDestroy, OnInit { 'type', 'symbol', 'dataSource', + 'priority', 'attempts', 'created', 'finished', diff --git a/apps/client/src/app/components/admin-jobs/admin-jobs.html b/apps/client/src/app/components/admin-jobs/admin-jobs.html index 31dad0ca6..5da929fe1 100644 --- a/apps/client/src/app/components/admin-jobs/admin-jobs.html +++ b/apps/client/src/app/components/admin-jobs/admin-jobs.html @@ -58,6 +58,25 @@ + + + Priority + + + @if (element.opts.priority === DATA_GATHERING_QUEUE_PRIORITY_LOW) { + + } @else if ( + element.opts.priority === DATA_GATHERING_QUEUE_PRIORITY_MEDIUM + ) { + + } @else if ( + element.opts.priority === DATA_GATHERING_QUEUE_PRIORITY_HIGH + ) { + + } + + + Attempts @@ -90,24 +109,37 @@ Status - + - - + + diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index 293f77488..5e1366ce2 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -32,8 +32,11 @@ export const warnColorRgb = { }; export const DATA_GATHERING_QUEUE = 'DATA_GATHERING_QUEUE'; -export const DATA_GATHERING_QUEUE_PRIORITY_LOW = Number.MAX_SAFE_INTEGER; export const DATA_GATHERING_QUEUE_PRIORITY_HIGH = 1; +export const DATA_GATHERING_QUEUE_PRIORITY_LOW = Number.MAX_SAFE_INTEGER; +export const DATA_GATHERING_QUEUE_PRIORITY_MEDIUM = Math.round( + DATA_GATHERING_QUEUE_PRIORITY_LOW / 2 +); export const DEFAULT_CURRENCY = 'USD'; export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy'; @@ -69,7 +72,6 @@ export const GATHER_ASSET_PROFILE_PROCESS_OPTIONS: JobOptions = { delay: ms('1 minute'), type: 'exponential' }, - priority: DATA_GATHERING_QUEUE_PRIORITY_HIGH, removeOnComplete: true }; export const GATHER_HISTORICAL_MARKET_DATA_PROCESS = @@ -80,7 +82,6 @@ export const GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS: JobOptions = { delay: ms('1 minute'), type: 'exponential' }, - priority: DATA_GATHERING_QUEUE_PRIORITY_LOW, removeOnComplete: true }; diff --git a/libs/common/src/lib/interfaces/admin-jobs.interface.ts b/libs/common/src/lib/interfaces/admin-jobs.interface.ts index 25e937626..b4c91ebc0 100644 --- a/libs/common/src/lib/interfaces/admin-jobs.interface.ts +++ b/libs/common/src/lib/interfaces/admin-jobs.interface.ts @@ -8,6 +8,7 @@ export interface AdminJobs { | 'finishedOn' | 'id' | 'name' + | 'opts' | 'stacktrace' | 'timestamp' > & { From 7d308917ddba1b6f7d1fa40b9005d73b022ec0fb Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 13 Apr 2024 09:28:38 +0200 Subject: [PATCH 116/120] Feature/upgrade yahoo finance2 to version 2.11.1 (#3254) * Upgrade yahoo-finance2 to version 2.11.1 * Update changelog --- CHANGELOG.md | 1 + package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73999efb7..e48f26c0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Upgraded `angular` from version `17.2.4` to `17.3.3` - Upgraded `Nx` from version `18.1.2` to `18.2.3` - Upgraded `prisma` from version `5.11.0` to `5.12.1` +- Upgraded `yahoo-finance2` from version `2.11.0` to `2.11.1` ### Fixed diff --git a/package.json b/package.json index 1f2e7520a..252fb045f 100644 --- a/package.json +++ b/package.json @@ -132,7 +132,7 @@ "svgmap": "2.6.0", "twitter-api-v2": "1.14.2", "uuid": "9.0.1", - "yahoo-finance2": "2.11.0", + "yahoo-finance2": "2.11.1", "zone.js": "0.14.4" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 9c77b9917..8cbfbccd0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19620,10 +19620,10 @@ y18n@^5.0.5: resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== -yahoo-finance2@2.11.0: - version "2.11.0" - resolved "https://registry.yarnpkg.com/yahoo-finance2/-/yahoo-finance2-2.11.0.tgz#293ade36d2e969218ad38fc308abc11ca02c1fab" - integrity sha512-lQWjnf9cnfAEmpL9ymwabxjQxu1xT5jg03NYOYcpn0ObOx9oGk9hes6JDaCeIq/xT+xgMdPMupsgYxnAxfKyhw== +yahoo-finance2@2.11.1: + version "2.11.1" + resolved "https://registry.yarnpkg.com/yahoo-finance2/-/yahoo-finance2-2.11.1.tgz#97758d4784ef0b4efe4b370a72063929cc4c6342" + integrity sha512-YglgpjIDithq1PG8Je/gy8nzJFqkH214x2ZGfr6Y+HV4ymTDFLluq2W9Hsvvyydv1zTv9/Ykedf0J4YIpmO2Zg== dependencies: "@types/tough-cookie" "^4.0.2" ajv "8.10.0" From b31bbbe2d14eee09b78699f80d8d37c1a03613de Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 13 Apr 2024 09:31:07 +0200 Subject: [PATCH 117/120] Release 2.72.0 (#3270) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e48f26c0a..344ad73ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## 2.72.0 - 2024-04-13 ### Added diff --git a/package.json b/package.json index 252fb045f..35ee3b1a4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "2.71.0", + "version": "2.72.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio", From 6c57609db853f17a95ae761ca6c620eaa33ce5cd Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 13 Apr 2024 11:07:18 +0200 Subject: [PATCH 118/120] Feature/move dividend fee and interest calculation to portfolio calculator (#3267) * Move dividend, feee and interest calculation to portfolio calculator * Update changelog --- CHANGELOG.md | 8 + .../calculator/mwr/portfolio-calculator.ts | 4 +- .../portfolio-calculator.factory.ts | 7 +- .../calculator/portfolio-calculator.ts | 569 ++++++++++-------- ...aln-buy-and-sell-in-two-activities.spec.ts | 14 +- ...folio-calculator-baln-buy-and-sell.spec.ts | 14 +- .../twr/portfolio-calculator-baln-buy.spec.ts | 14 +- ...ator-btcusd-buy-and-sell-partially.spec.ts | 14 +- .../twr/portfolio-calculator-fee.spec.ts | 132 ++++ .../portfolio-calculator-googl-buy.spec.ts | 14 +- ...-calculator-msft-buy-with-dividend.spec.ts | 14 +- .../portfolio-calculator-no-orders.spec.ts | 16 +- ...ulator-novn-buy-and-sell-partially.spec.ts | 13 +- ...folio-calculator-novn-buy-and-sell.spec.ts | 14 +- .../calculator/twr/portfolio-calculator.ts | 32 +- ...ace.ts => portfolio-snapshot.interface.ts} | 4 +- .../interfaces/transaction-point.interface.ts | 4 + .../src/app/portfolio/portfolio.controller.ts | 8 +- .../src/app/portfolio/portfolio.service.ts | 160 ++--- apps/api/src/helper/portfolio.helper.ts | 26 +- .../interfaces/symbol-metrics.interface.ts | 3 + 21 files changed, 655 insertions(+), 429 deletions(-) create mode 100644 apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-fee.spec.ts rename apps/api/src/app/portfolio/interfaces/{current-positions.interface.ts => portfolio-snapshot.interface.ts} (82%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 344ad73ed..7da5c3ddd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Changed + +- Moved the dividend calculations into the portfolio calculator +- Moved the fee calculations into the portfolio calculator +- Moved the interest calculations into the portfolio calculator + ## 2.72.0 - 2024-04-13 ### Added diff --git a/apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts index ec744f624..978f1f3aa 100644 --- a/apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts @@ -1,5 +1,5 @@ import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; -import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface'; +import { PortfolioSnapshot } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-snapshot.interface'; import { SymbolMetrics, TimelinePosition, @@ -9,7 +9,7 @@ import { export class MWRPortfolioCalculator extends PortfolioCalculator { protected calculateOverallPerformance( positions: TimelinePosition[] - ): CurrentPositions { + ): PortfolioSnapshot { throw new Error('Method not implemented.'); } diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts index cf1fe9324..e64c23942 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts @@ -1,6 +1,7 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; +import { DateRange } from '@ghostfolio/common/types'; import { Injectable } from '@nestjs/common'; @@ -23,17 +24,20 @@ export class PortfolioCalculatorFactory { public createCalculator({ activities, calculationType, - currency + currency, + dateRange = 'max' }: { activities: Activity[]; calculationType: PerformanceCalculationType; currency: string; + dateRange?: DateRange; }): PortfolioCalculator { switch (calculationType) { case PerformanceCalculationType.MWR: return new MWRPortfolioCalculator({ activities, currency, + dateRange, currentRateService: this.currentRateService, exchangeRateDataService: this.exchangeRateDataService }); @@ -42,6 +46,7 @@ export class PortfolioCalculatorFactory { activities, currency, currentRateService: this.currentRateService, + dateRange, exchangeRateDataService: this.exchangeRateDataService }); default: diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index a9dbff442..712c0ac04 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -1,7 +1,7 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; -import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface'; import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface'; +import { PortfolioSnapshot } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-snapshot.interface'; import { TransactionPointSymbol } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point-symbol.interface'; import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface'; import { @@ -11,7 +11,12 @@ import { import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { MAX_CHART_ITEMS } from '@ghostfolio/common/config'; -import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; +import { + DATE_FORMAT, + getSum, + parseDate, + resetHours +} from '@ghostfolio/common/helper'; import { DataProviderInfo, HistoricalDataItem, @@ -44,18 +49,24 @@ export abstract class PortfolioCalculator { private currency: string; private currentRateService: CurrentRateService; private dataProviderInfos: DataProviderInfo[]; + private endDate: Date; private exchangeRateDataService: ExchangeRateDataService; + private snapshot: PortfolioSnapshot; + private snapshotPromise: Promise; + private startDate: Date; private transactionPoints: TransactionPoint[]; public constructor({ activities, currency, currentRateService, + dateRange, exchangeRateDataService }: { activities: Activity[]; currency: string; currentRateService: CurrentRateService; + dateRange: DateRange; exchangeRateDataService: ExchangeRateDataService; }) { this.currency = currency; @@ -79,12 +90,270 @@ export abstract class PortfolioCalculator { return a.date?.localeCompare(b.date); }); + const { endDate, startDate } = getInterval(dateRange); + + this.endDate = endDate; + this.startDate = startDate; + this.computeTransactionPoints(); + + this.snapshotPromise = this.initialize(); } protected abstract calculateOverallPerformance( positions: TimelinePosition[] - ): CurrentPositions; + ): PortfolioSnapshot; + + public async computeSnapshot( + start: Date, + end?: Date + ): Promise { + const lastTransactionPoint = last(this.transactionPoints); + + let endDate = end; + + if (!endDate) { + endDate = new Date(Date.now()); + + if (lastTransactionPoint) { + endDate = max([endDate, parseDate(lastTransactionPoint.date)]); + } + } + + const transactionPoints = this.transactionPoints?.filter(({ date }) => { + return isBefore(parseDate(date), endDate); + }); + + if (!transactionPoints.length) { + return { + currentValueInBaseCurrency: new Big(0), + grossPerformance: new Big(0), + grossPerformancePercentage: new Big(0), + grossPerformancePercentageWithCurrencyEffect: new Big(0), + grossPerformanceWithCurrencyEffect: new Big(0), + hasErrors: false, + netPerformance: new Big(0), + netPerformancePercentage: new Big(0), + netPerformancePercentageWithCurrencyEffect: new Big(0), + netPerformanceWithCurrencyEffect: new Big(0), + positions: [], + totalFeesWithCurrencyEffect: new Big(0), + totalInterestWithCurrencyEffect: new Big(0), + totalInvestment: new Big(0), + totalInvestmentWithCurrencyEffect: new Big(0) + }; + } + + const currencies: { [symbol: string]: string } = {}; + const dataGatheringItems: IDataGatheringItem[] = []; + let dates: Date[] = []; + let firstIndex = transactionPoints.length; + let firstTransactionPoint: TransactionPoint = null; + + dates.push(resetHours(start)); + + for (const { currency, dataSource, symbol } of transactionPoints[ + firstIndex - 1 + ].items) { + dataGatheringItems.push({ + dataSource, + symbol + }); + + currencies[symbol] = currency; + } + + for (let i = 0; i < transactionPoints.length; i++) { + if ( + !isBefore(parseDate(transactionPoints[i].date), start) && + firstTransactionPoint === null + ) { + firstTransactionPoint = transactionPoints[i]; + firstIndex = i; + } + + if (firstTransactionPoint !== null) { + dates.push(resetHours(parseDate(transactionPoints[i].date))); + } + } + + dates.push(resetHours(endDate)); + + // Add dates of last week for fallback + dates.push(subDays(resetHours(new Date()), 7)); + dates.push(subDays(resetHours(new Date()), 6)); + dates.push(subDays(resetHours(new Date()), 5)); + dates.push(subDays(resetHours(new Date()), 4)); + dates.push(subDays(resetHours(new Date()), 3)); + dates.push(subDays(resetHours(new Date()), 2)); + dates.push(subDays(resetHours(new Date()), 1)); + dates.push(resetHours(new Date())); + + dates = uniq( + dates.map((date) => { + return date.getTime(); + }) + ) + .map((timestamp) => { + return new Date(timestamp); + }) + .sort((a, b) => { + return a.getTime() - b.getTime(); + }); + + let exchangeRatesByCurrency = + await this.exchangeRateDataService.getExchangeRatesByCurrency({ + currencies: uniq(Object.values(currencies)), + endDate: endOfDay(endDate), + startDate: this.getStartDate(), + targetCurrency: this.currency + }); + + const { + dataProviderInfos, + errors: currentRateErrors, + values: marketSymbols + } = await this.currentRateService.getValues({ + dataGatheringItems, + dateQuery: { + in: dates + } + }); + + this.dataProviderInfos = dataProviderInfos; + + const marketSymbolMap: { + [date: string]: { [symbol: string]: Big }; + } = {}; + + for (const marketSymbol of marketSymbols) { + const date = format(marketSymbol.date, DATE_FORMAT); + + if (!marketSymbolMap[date]) { + marketSymbolMap[date] = {}; + } + + if (marketSymbol.marketPrice) { + marketSymbolMap[date][marketSymbol.symbol] = new Big( + marketSymbol.marketPrice + ); + } + } + + const endDateString = format(endDate, DATE_FORMAT); + + if (firstIndex > 0) { + firstIndex--; + } + + const positions: TimelinePosition[] = []; + let hasAnySymbolMetricsErrors = false; + + const errors: ResponseError['errors'] = []; + + for (const item of lastTransactionPoint.items) { + const marketPriceInBaseCurrency = ( + marketSymbolMap[endDateString]?.[item.symbol] ?? item.averagePrice + ).mul( + exchangeRatesByCurrency[`${item.currency}${this.currency}`]?.[ + endDateString + ] + ); + + const { + grossPerformance, + grossPerformancePercentage, + grossPerformancePercentageWithCurrencyEffect, + grossPerformanceWithCurrencyEffect, + hasErrors, + netPerformance, + netPerformancePercentage, + netPerformancePercentageWithCurrencyEffect, + netPerformanceWithCurrencyEffect, + timeWeightedInvestment, + timeWeightedInvestmentWithCurrencyEffect, + totalDividend, + totalDividendInBaseCurrency, + totalInvestment, + totalInvestmentWithCurrencyEffect + } = this.getSymbolMetrics({ + marketSymbolMap, + start, + dataSource: item.dataSource, + end: endDate, + exchangeRates: + exchangeRatesByCurrency[`${item.currency}${this.currency}`], + symbol: item.symbol + }); + + hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors; + + positions.push({ + dividend: totalDividend, + dividendInBaseCurrency: totalDividendInBaseCurrency, + timeWeightedInvestment, + timeWeightedInvestmentWithCurrencyEffect, + averagePrice: item.averagePrice, + currency: item.currency, + dataSource: item.dataSource, + fee: item.fee, + firstBuyDate: item.firstBuyDate, + grossPerformance: !hasErrors ? grossPerformance ?? null : null, + grossPerformancePercentage: !hasErrors + ? grossPerformancePercentage ?? null + : null, + grossPerformancePercentageWithCurrencyEffect: !hasErrors + ? grossPerformancePercentageWithCurrencyEffect ?? null + : null, + grossPerformanceWithCurrencyEffect: !hasErrors + ? grossPerformanceWithCurrencyEffect ?? null + : null, + investment: totalInvestment, + investmentWithCurrencyEffect: totalInvestmentWithCurrencyEffect, + marketPrice: + marketSymbolMap[endDateString]?.[item.symbol]?.toNumber() ?? null, + marketPriceInBaseCurrency: + marketPriceInBaseCurrency?.toNumber() ?? null, + netPerformance: !hasErrors ? netPerformance ?? null : null, + netPerformancePercentage: !hasErrors + ? netPerformancePercentage ?? null + : null, + netPerformancePercentageWithCurrencyEffect: !hasErrors + ? netPerformancePercentageWithCurrencyEffect ?? null + : null, + netPerformanceWithCurrencyEffect: !hasErrors + ? netPerformanceWithCurrencyEffect ?? null + : null, + quantity: item.quantity, + symbol: item.symbol, + tags: item.tags, + transactionCount: item.transactionCount, + valueInBaseCurrency: new Big(marketPriceInBaseCurrency).mul( + item.quantity + ) + }); + + if ( + (hasErrors || + currentRateErrors.find(({ dataSource, symbol }) => { + return dataSource === item.dataSource && symbol === item.symbol; + })) && + item.investment.gt(0) + ) { + errors.push({ dataSource: item.dataSource, symbol: item.symbol }); + } + } + + const overall = this.calculateOverallPerformance(positions); + + return { + ...overall, + errors, + positions, + hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors, + totalInterestWithCurrencyEffect: lastTransactionPoint.interest + }; + } public async getChart({ dateRange = 'max', @@ -380,256 +649,30 @@ export abstract class PortfolioCalculator { }); } - public async getCurrentPositions( - start: Date, - end?: Date - ): Promise { - const lastTransactionPoint = last(this.transactionPoints); - - let endDate = end; - - if (!endDate) { - endDate = new Date(Date.now()); - - if (lastTransactionPoint) { - endDate = max([endDate, parseDate(lastTransactionPoint.date)]); - } - } - - const transactionPoints = this.transactionPoints?.filter(({ date }) => { - return isBefore(parseDate(date), endDate); - }); - - if (!transactionPoints.length) { - return { - currentValueInBaseCurrency: new Big(0), - grossPerformance: new Big(0), - grossPerformancePercentage: new Big(0), - grossPerformancePercentageWithCurrencyEffect: new Big(0), - grossPerformanceWithCurrencyEffect: new Big(0), - hasErrors: false, - netPerformance: new Big(0), - netPerformancePercentage: new Big(0), - netPerformancePercentageWithCurrencyEffect: new Big(0), - netPerformanceWithCurrencyEffect: new Big(0), - positions: [], - totalInvestment: new Big(0), - totalInvestmentWithCurrencyEffect: new Big(0) - }; - } - - const currencies: { [symbol: string]: string } = {}; - const dataGatheringItems: IDataGatheringItem[] = []; - let dates: Date[] = []; - let firstIndex = transactionPoints.length; - let firstTransactionPoint: TransactionPoint = null; - - dates.push(resetHours(start)); - - for (const { currency, dataSource, symbol } of transactionPoints[ - firstIndex - 1 - ].items) { - dataGatheringItems.push({ - dataSource, - symbol - }); - - currencies[symbol] = currency; - } - - for (let i = 0; i < transactionPoints.length; i++) { - if ( - !isBefore(parseDate(transactionPoints[i].date), start) && - firstTransactionPoint === null - ) { - firstTransactionPoint = transactionPoints[i]; - firstIndex = i; - } - - if (firstTransactionPoint !== null) { - dates.push(resetHours(parseDate(transactionPoints[i].date))); - } - } - - dates.push(resetHours(endDate)); + public getDataProviderInfos() { + return this.dataProviderInfos; + } - // Add dates of last week for fallback - dates.push(subDays(resetHours(new Date()), 7)); - dates.push(subDays(resetHours(new Date()), 6)); - dates.push(subDays(resetHours(new Date()), 5)); - dates.push(subDays(resetHours(new Date()), 4)); - dates.push(subDays(resetHours(new Date()), 3)); - dates.push(subDays(resetHours(new Date()), 2)); - dates.push(subDays(resetHours(new Date()), 1)); - dates.push(resetHours(new Date())); + public async getDividendInBaseCurrency() { + await this.snapshotPromise; - dates = uniq( - dates.map((date) => { - return date.getTime(); - }) - ) - .map((timestamp) => { - return new Date(timestamp); + return getSum( + this.snapshot.positions.map(({ dividendInBaseCurrency }) => { + return dividendInBaseCurrency; }) - .sort((a, b) => { - return a.getTime() - b.getTime(); - }); - - let exchangeRatesByCurrency = - await this.exchangeRateDataService.getExchangeRatesByCurrency({ - currencies: uniq(Object.values(currencies)), - endDate: endOfDay(endDate), - startDate: this.getStartDate(), - targetCurrency: this.currency - }); - - const { - dataProviderInfos, - errors: currentRateErrors, - values: marketSymbols - } = await this.currentRateService.getValues({ - dataGatheringItems, - dateQuery: { - in: dates - } - }); - - this.dataProviderInfos = dataProviderInfos; - - const marketSymbolMap: { - [date: string]: { [symbol: string]: Big }; - } = {}; - - for (const marketSymbol of marketSymbols) { - const date = format(marketSymbol.date, DATE_FORMAT); - - if (!marketSymbolMap[date]) { - marketSymbolMap[date] = {}; - } - - if (marketSymbol.marketPrice) { - marketSymbolMap[date][marketSymbol.symbol] = new Big( - marketSymbol.marketPrice - ); - } - } - - const endDateString = format(endDate, DATE_FORMAT); - - if (firstIndex > 0) { - firstIndex--; - } - - const positions: TimelinePosition[] = []; - let hasAnySymbolMetricsErrors = false; - - const errors: ResponseError['errors'] = []; - - for (const item of lastTransactionPoint.items) { - const marketPriceInBaseCurrency = ( - marketSymbolMap[endDateString]?.[item.symbol] ?? item.averagePrice - ).mul( - exchangeRatesByCurrency[`${item.currency}${this.currency}`]?.[ - endDateString - ] - ); - - const { - grossPerformance, - grossPerformancePercentage, - grossPerformancePercentageWithCurrencyEffect, - grossPerformanceWithCurrencyEffect, - hasErrors, - netPerformance, - netPerformancePercentage, - netPerformancePercentageWithCurrencyEffect, - netPerformanceWithCurrencyEffect, - timeWeightedInvestment, - timeWeightedInvestmentWithCurrencyEffect, - totalDividend, - totalDividendInBaseCurrency, - totalInvestment, - totalInvestmentWithCurrencyEffect - } = this.getSymbolMetrics({ - marketSymbolMap, - start, - dataSource: item.dataSource, - end: endDate, - exchangeRates: - exchangeRatesByCurrency[`${item.currency}${this.currency}`], - symbol: item.symbol - }); - - hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors; - - positions.push({ - dividend: totalDividend, - dividendInBaseCurrency: totalDividendInBaseCurrency, - timeWeightedInvestment, - timeWeightedInvestmentWithCurrencyEffect, - averagePrice: item.averagePrice, - currency: item.currency, - dataSource: item.dataSource, - fee: item.fee, - firstBuyDate: item.firstBuyDate, - grossPerformance: !hasErrors ? grossPerformance ?? null : null, - grossPerformancePercentage: !hasErrors - ? grossPerformancePercentage ?? null - : null, - grossPerformancePercentageWithCurrencyEffect: !hasErrors - ? grossPerformancePercentageWithCurrencyEffect ?? null - : null, - grossPerformanceWithCurrencyEffect: !hasErrors - ? grossPerformanceWithCurrencyEffect ?? null - : null, - investment: totalInvestment, - investmentWithCurrencyEffect: totalInvestmentWithCurrencyEffect, - marketPrice: - marketSymbolMap[endDateString]?.[item.symbol]?.toNumber() ?? null, - marketPriceInBaseCurrency: - marketPriceInBaseCurrency?.toNumber() ?? null, - netPerformance: !hasErrors ? netPerformance ?? null : null, - netPerformancePercentage: !hasErrors - ? netPerformancePercentage ?? null - : null, - netPerformancePercentageWithCurrencyEffect: !hasErrors - ? netPerformancePercentageWithCurrencyEffect ?? null - : null, - netPerformanceWithCurrencyEffect: !hasErrors - ? netPerformanceWithCurrencyEffect ?? null - : null, - quantity: item.quantity, - symbol: item.symbol, - tags: item.tags, - transactionCount: item.transactionCount, - valueInBaseCurrency: new Big(marketPriceInBaseCurrency).mul( - item.quantity - ) - }); - - if ( - (hasErrors || - currentRateErrors.find(({ dataSource, symbol }) => { - return dataSource === item.dataSource && symbol === item.symbol; - })) && - item.investment.gt(0) - ) { - errors.push({ dataSource: item.dataSource, symbol: item.symbol }); - } - } + ); + } - const overall = this.calculateOverallPerformance(positions); + public async getFeesInBaseCurrency() { + await this.snapshotPromise; - return { - ...overall, - errors, - positions, - hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors - }; + return this.snapshot.totalFeesWithCurrencyEffect; } - public getDataProviderInfos() { - return this.dataProviderInfos; + public async getInterestInBaseCurrency() { + await this.snapshotPromise; + + return this.snapshot.totalInterestWithCurrencyEffect; } public getInvestments(): { date: string; investment: Big }[] { @@ -672,6 +715,12 @@ export abstract class PortfolioCalculator { })); } + public async getSnapshot() { + await this.snapshotPromise; + + return this.snapshot; + } + public getStartDate() { return this.transactionPoints.length > 0 ? parseDate(this.transactionPoints[0].date) @@ -718,6 +767,13 @@ export abstract class PortfolioCalculator { type, unitPrice } of this.orders) { + if ( + // TODO + ['ITEM', 'LIABILITY'].includes(type) + ) { + continue; + } + let currentTransactionPointItem: TransactionPointSymbol; const oldAccumulatedSymbol = symbols[SymbolProfile.symbol]; @@ -790,18 +846,39 @@ export abstract class PortfolioCalculator { return a.symbol?.localeCompare(b.symbol); }); + let fees = new Big(0); + + if (type === 'FEE') { + fees = fee; + } + + let interest = new Big(0); + + if (type === 'INTEREST') { + interest = quantity.mul(unitPrice); + } + if (lastDate !== date || lastTransactionPoint === null) { lastTransactionPoint = { date, + fees, + interest, items: newItems }; this.transactionPoints.push(lastTransactionPoint); } else { + lastTransactionPoint.fees = lastTransactionPoint.fees.plus(fees); + lastTransactionPoint.interest = + lastTransactionPoint.interest.plus(interest); lastTransactionPoint.items = newItems; } lastDate = date; } } + + private async initialize() { + this.snapshot = await this.computeSnapshot(this.startDate, this.endDate); + } } diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts index b936d21a9..8ddae9df6 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts @@ -46,6 +46,10 @@ describe('PortfolioCalculator', () => { describe('get current positions', () => { it.only('with BALN.SW buy and sell in two activities', async () => { + const spy = jest + .spyOn(Date, 'now') + .mockImplementation(() => parseDate('2021-12-18').getTime()); + const activities: Activity[] = [ { ...activityDummyData, @@ -100,15 +104,11 @@ describe('PortfolioCalculator', () => { currency: 'CHF' }); - const spy = jest - .spyOn(Date, 'now') - .mockImplementation(() => parseDate('2021-12-18').getTime()); - const chartData = await portfolioCalculator.getChartData({ start: parseDate('2021-11-22') }); - const currentPositions = await portfolioCalculator.getCurrentPositions( + const portfolioSnapshot = await portfolioCalculator.computeSnapshot( parseDate('2021-11-22') ); @@ -121,7 +121,7 @@ describe('PortfolioCalculator', () => { spy.mockRestore(); - expect(currentPositions).toEqual({ + expect(portfolioSnapshot).toEqual({ currentValueInBaseCurrency: new Big('0'), errors: [], grossPerformance: new Big('-12.6'), @@ -173,6 +173,8 @@ describe('PortfolioCalculator', () => { valueInBaseCurrency: new Big('0') } ], + totalFeesWithCurrencyEffect: new Big('3.2'), + totalInterestWithCurrencyEffect: new Big('0'), totalInvestment: new Big('0'), totalInvestmentWithCurrencyEffect: new Big('0') }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts index d1557bc12..febd1769d 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts @@ -46,6 +46,10 @@ describe('PortfolioCalculator', () => { describe('get current positions', () => { it.only('with BALN.SW buy and sell', async () => { + const spy = jest + .spyOn(Date, 'now') + .mockImplementation(() => parseDate('2021-12-18').getTime()); + const activities: Activity[] = [ { ...activityDummyData, @@ -85,15 +89,11 @@ describe('PortfolioCalculator', () => { currency: 'CHF' }); - const spy = jest - .spyOn(Date, 'now') - .mockImplementation(() => parseDate('2021-12-18').getTime()); - const chartData = await portfolioCalculator.getChartData({ start: parseDate('2021-11-22') }); - const currentPositions = await portfolioCalculator.getCurrentPositions( + const portfolioSnapshot = await portfolioCalculator.computeSnapshot( parseDate('2021-11-22') ); @@ -106,7 +106,7 @@ describe('PortfolioCalculator', () => { spy.mockRestore(); - expect(currentPositions).toEqual({ + expect(portfolioSnapshot).toEqual({ currentValueInBaseCurrency: new Big('0'), errors: [], grossPerformance: new Big('-12.6'), @@ -156,6 +156,8 @@ describe('PortfolioCalculator', () => { valueInBaseCurrency: new Big('0') } ], + totalFeesWithCurrencyEffect: new Big('3.2'), + totalInterestWithCurrencyEffect: new Big('0'), totalInvestment: new Big('0'), totalInvestmentWithCurrencyEffect: new Big('0') }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts index 593503493..2b9fd06f0 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts @@ -46,6 +46,10 @@ describe('PortfolioCalculator', () => { describe('get current positions', () => { it.only('with BALN.SW buy', async () => { + const spy = jest + .spyOn(Date, 'now') + .mockImplementation(() => parseDate('2021-12-18').getTime()); + const activities: Activity[] = [ { ...activityDummyData, @@ -70,15 +74,11 @@ describe('PortfolioCalculator', () => { currency: 'CHF' }); - const spy = jest - .spyOn(Date, 'now') - .mockImplementation(() => parseDate('2021-12-18').getTime()); - const chartData = await portfolioCalculator.getChartData({ start: parseDate('2021-11-30') }); - const currentPositions = await portfolioCalculator.getCurrentPositions( + const portfolioSnapshot = await portfolioCalculator.computeSnapshot( parseDate('2021-11-30') ); @@ -91,7 +91,7 @@ describe('PortfolioCalculator', () => { spy.mockRestore(); - expect(currentPositions).toEqual({ + expect(portfolioSnapshot).toEqual({ currentValueInBaseCurrency: new Big('297.8'), errors: [], grossPerformance: new Big('24.6'), @@ -141,6 +141,8 @@ describe('PortfolioCalculator', () => { valueInBaseCurrency: new Big('297.8') } ], + totalFeesWithCurrencyEffect: new Big('1.55'), + totalInterestWithCurrencyEffect: new Big('0'), totalInvestment: new Big('273.2'), totalInvestmentWithCurrencyEffect: new Big('273.2') }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts index e3f351b28..ceff92449 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts @@ -59,6 +59,10 @@ describe('PortfolioCalculator', () => { describe('get current positions', () => { it.only('with BTCUSD buy and sell partially', async () => { + const spy = jest + .spyOn(Date, 'now') + .mockImplementation(() => parseDate('2018-01-01').getTime()); + const activities: Activity[] = [ { ...activityDummyData, @@ -98,15 +102,11 @@ describe('PortfolioCalculator', () => { currency: 'CHF' }); - const spy = jest - .spyOn(Date, 'now') - .mockImplementation(() => parseDate('2018-01-01').getTime()); - const chartData = await portfolioCalculator.getChartData({ start: parseDate('2015-01-01') }); - const currentPositions = await portfolioCalculator.getCurrentPositions( + const portfolioSnapshot = await portfolioCalculator.computeSnapshot( parseDate('2015-01-01') ); @@ -119,7 +119,7 @@ describe('PortfolioCalculator', () => { spy.mockRestore(); - expect(currentPositions).toEqual({ + expect(portfolioSnapshot).toEqual({ currentValueInBaseCurrency: new Big('13298.425356'), errors: [], grossPerformance: new Big('27172.74'), @@ -175,6 +175,8 @@ describe('PortfolioCalculator', () => { valueInBaseCurrency: new Big('13298.425356') } ], + totalFeesWithCurrencyEffect: new Big('0'), + totalInterestWithCurrencyEffect: new Big('0'), totalInvestment: new Big('320.43'), totalInvestmentWithCurrencyEffect: new Big('318.542667299999967957') }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-fee.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-fee.spec.ts new file mode 100644 index 000000000..b689a0c30 --- /dev/null +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-fee.spec.ts @@ -0,0 +1,132 @@ +import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; +import { + activityDummyData, + symbolProfileDummyData +} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; +import { + PortfolioCalculatorFactory, + PerformanceCalculationType +} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; +import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; +import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; +import { parseDate } from '@ghostfolio/common/helper'; + +import { Big } from 'big.js'; + +jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + CurrentRateService: jest.fn().mockImplementation(() => { + return CurrentRateServiceMock; + }) + }; +}); + +describe('PortfolioCalculator', () => { + let currentRateService: CurrentRateService; + let exchangeRateDataService: ExchangeRateDataService; + let factory: PortfolioCalculatorFactory; + + beforeEach(() => { + currentRateService = new CurrentRateService(null, null, null, null); + + exchangeRateDataService = new ExchangeRateDataService( + null, + null, + null, + null + ); + + factory = new PortfolioCalculatorFactory( + currentRateService, + exchangeRateDataService + ); + }); + + describe('compute portfolio snapshot', () => { + it.only('with fee activity', async () => { + const spy = jest + .spyOn(Date, 'now') + .mockImplementation(() => parseDate('2021-12-18').getTime()); + + const activities: Activity[] = [ + { + ...activityDummyData, + date: new Date('2021-09-01'), + fee: 49, + quantity: 0, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'MANUAL', + name: 'Account Opening Fee', + symbol: '2c463fb3-af07-486e-adb0-8301b3d72141' + }, + type: 'FEE', + unitPrice: 0 + } + ]; + + const portfolioCalculator = factory.createCalculator({ + activities, + calculationType: PerformanceCalculationType.TWR, + currency: 'USD' + }); + + const portfolioSnapshot = await portfolioCalculator.computeSnapshot( + parseDate('2021-11-30') + ); + + spy.mockRestore(); + + expect(portfolioSnapshot).toEqual({ + currentValueInBaseCurrency: new Big('0'), + errors: [], + grossPerformance: new Big('0'), + grossPerformancePercentage: new Big('0'), + grossPerformancePercentageWithCurrencyEffect: new Big('0'), + grossPerformanceWithCurrencyEffect: new Big('0'), + hasErrors: true, + netPerformance: new Big('0'), + netPerformancePercentage: new Big('0'), + netPerformancePercentageWithCurrencyEffect: new Big('0'), + netPerformanceWithCurrencyEffect: new Big('0'), + positions: [ + { + averagePrice: new Big('0'), + currency: 'USD', + dataSource: 'MANUAL', + dividend: new Big('0'), + dividendInBaseCurrency: new Big('0'), + fee: new Big('49'), + firstBuyDate: '2021-09-01', + grossPerformance: null, + grossPerformancePercentage: null, + grossPerformancePercentageWithCurrencyEffect: null, + grossPerformanceWithCurrencyEffect: null, + investment: new Big('0'), + investmentWithCurrencyEffect: new Big('0'), + marketPrice: null, + marketPriceInBaseCurrency: 0, + netPerformance: null, + netPerformancePercentage: null, + netPerformancePercentageWithCurrencyEffect: null, + netPerformanceWithCurrencyEffect: null, + quantity: new Big('0'), + symbol: '2c463fb3-af07-486e-adb0-8301b3d72141', + tags: [], + timeWeightedInvestment: new Big('0'), + timeWeightedInvestmentWithCurrencyEffect: new Big('0'), + transactionCount: 1, + valueInBaseCurrency: new Big('0') + } + ], + totalFeesWithCurrencyEffect: new Big('49'), + totalInterestWithCurrencyEffect: new Big('0'), + totalInvestment: new Big('0'), + totalInvestmentWithCurrencyEffect: new Big('0') + }); + }); + }); +}); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts index e7796b4d3..911167f7a 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts @@ -59,6 +59,10 @@ describe('PortfolioCalculator', () => { describe('get current positions', () => { it.only('with GOOGL buy', async () => { + const spy = jest + .spyOn(Date, 'now') + .mockImplementation(() => parseDate('2023-07-10').getTime()); + const activities: Activity[] = [ { ...activityDummyData, @@ -83,15 +87,11 @@ describe('PortfolioCalculator', () => { currency: 'CHF' }); - const spy = jest - .spyOn(Date, 'now') - .mockImplementation(() => parseDate('2023-07-10').getTime()); - const chartData = await portfolioCalculator.getChartData({ start: parseDate('2023-01-03') }); - const currentPositions = await portfolioCalculator.getCurrentPositions( + const portfolioSnapshot = await portfolioCalculator.computeSnapshot( parseDate('2023-01-03') ); @@ -104,7 +104,7 @@ describe('PortfolioCalculator', () => { spy.mockRestore(); - expect(currentPositions).toEqual({ + expect(portfolioSnapshot).toEqual({ currentValueInBaseCurrency: new Big('103.10483'), errors: [], grossPerformance: new Big('27.33'), @@ -154,6 +154,8 @@ describe('PortfolioCalculator', () => { valueInBaseCurrency: new Big('103.10483') } ], + totalFeesWithCurrencyEffect: new Big('1'), + totalInterestWithCurrencyEffect: new Big('0'), totalInvestment: new Big('89.12'), totalInvestmentWithCurrencyEffect: new Big('82.329056') }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts index 49a07e73f..6dc489c5d 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts @@ -59,6 +59,10 @@ describe('PortfolioCalculator', () => { describe('get current positions', () => { it.only('with MSFT buy', async () => { + const spy = jest + .spyOn(Date, 'now') + .mockImplementation(() => parseDate('2023-07-10').getTime()); + const activities: Activity[] = [ { ...activityDummyData, @@ -98,17 +102,13 @@ describe('PortfolioCalculator', () => { currency: 'USD' }); - const spy = jest - .spyOn(Date, 'now') - .mockImplementation(() => parseDate('2023-07-10').getTime()); - - const currentPositions = await portfolioCalculator.getCurrentPositions( + const portfolioSnapshot = await portfolioCalculator.computeSnapshot( parseDate('2023-07-10') ); spy.mockRestore(); - expect(currentPositions).toMatchObject({ + expect(portfolioSnapshot).toMatchObject({ errors: [], hasErrors: false, positions: [ @@ -130,6 +130,8 @@ describe('PortfolioCalculator', () => { transactionCount: 2 } ], + totalFeesWithCurrencyEffect: new Big('19'), + totalInterestWithCurrencyEffect: new Big('0'), totalInvestment: new Big('298.58'), totalInvestmentWithCurrencyEffect: new Big('298.58') }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-no-orders.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-no-orders.spec.ts index 905747519..ece39c87b 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-no-orders.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-no-orders.spec.ts @@ -42,22 +42,22 @@ describe('PortfolioCalculator', () => { describe('get current positions', () => { it('with no orders', async () => { + const spy = jest + .spyOn(Date, 'now') + .mockImplementation(() => parseDate('2021-12-18').getTime()); + const portfolioCalculator = factory.createCalculator({ activities: [], calculationType: PerformanceCalculationType.TWR, currency: 'CHF' }); - const spy = jest - .spyOn(Date, 'now') - .mockImplementation(() => parseDate('2021-12-18').getTime()); - const start = subDays(new Date(Date.now()), 10); const chartData = await portfolioCalculator.getChartData({ start }); - const currentPositions = - await portfolioCalculator.getCurrentPositions(start); + const portfolioSnapshot = + await portfolioCalculator.computeSnapshot(start); const investments = portfolioCalculator.getInvestments(); @@ -68,7 +68,7 @@ describe('PortfolioCalculator', () => { spy.mockRestore(); - expect(currentPositions).toEqual({ + expect(portfolioSnapshot).toEqual({ currentValueInBaseCurrency: new Big(0), grossPerformance: new Big(0), grossPerformancePercentage: new Big(0), @@ -80,6 +80,8 @@ describe('PortfolioCalculator', () => { netPerformancePercentageWithCurrencyEffect: new Big(0), netPerformanceWithCurrencyEffect: new Big(0), positions: [], + totalFeesWithCurrencyEffect: new Big('0'), + totalInterestWithCurrencyEffect: new Big('0'), totalInvestment: new Big(0), totalInvestmentWithCurrencyEffect: new Big(0) }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts index 2bfd6d865..a3c12829a 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts @@ -46,6 +46,10 @@ describe('PortfolioCalculator', () => { describe('get current positions', () => { it.only('with NOVN.SW buy and sell partially', async () => { + const spy = jest + .spyOn(Date, 'now') + .mockImplementation(() => parseDate('2022-04-11').getTime()); + const activities: Activity[] = [ { ...activityDummyData, @@ -84,15 +88,12 @@ describe('PortfolioCalculator', () => { calculationType: PerformanceCalculationType.TWR, currency: 'CHF' }); - const spy = jest - .spyOn(Date, 'now') - .mockImplementation(() => parseDate('2022-04-11').getTime()); const chartData = await portfolioCalculator.getChartData({ start: parseDate('2022-03-07') }); - const currentPositions = await portfolioCalculator.getCurrentPositions( + const portfolioSnapshot = await portfolioCalculator.computeSnapshot( parseDate('2022-03-07') ); @@ -105,7 +106,7 @@ describe('PortfolioCalculator', () => { spy.mockRestore(); - expect(currentPositions).toEqual({ + expect(portfolioSnapshot).toEqual({ currentValueInBaseCurrency: new Big('87.8'), errors: [], grossPerformance: new Big('21.93'), @@ -157,6 +158,8 @@ describe('PortfolioCalculator', () => { valueInBaseCurrency: new Big('87.8') } ], + totalFeesWithCurrencyEffect: new Big('4.25'), + totalInterestWithCurrencyEffect: new Big('0'), totalInvestment: new Big('75.80'), totalInvestmentWithCurrencyEffect: new Big('75.80') }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts index be3f75dc2..f1bf56f11 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts @@ -46,6 +46,10 @@ describe('PortfolioCalculator', () => { describe('get current positions', () => { it.only('with NOVN.SW buy and sell', async () => { + const spy = jest + .spyOn(Date, 'now') + .mockImplementation(() => parseDate('2022-04-11').getTime()); + const activities: Activity[] = [ { ...activityDummyData, @@ -85,15 +89,11 @@ describe('PortfolioCalculator', () => { currency: 'CHF' }); - const spy = jest - .spyOn(Date, 'now') - .mockImplementation(() => parseDate('2022-04-11').getTime()); - const chartData = await portfolioCalculator.getChartData({ start: parseDate('2022-03-07') }); - const currentPositions = await portfolioCalculator.getCurrentPositions( + const portfolioSnapshot = await portfolioCalculator.computeSnapshot( parseDate('2022-03-07') ); @@ -132,7 +132,7 @@ describe('PortfolioCalculator', () => { valueWithCurrencyEffect: 0 }); - expect(currentPositions).toEqual({ + expect(portfolioSnapshot).toEqual({ currentValueInBaseCurrency: new Big('0'), errors: [], grossPerformance: new Big('19.86'), @@ -182,6 +182,8 @@ describe('PortfolioCalculator', () => { valueInBaseCurrency: new Big('0') } ], + totalFeesWithCurrencyEffect: new Big('0'), + totalInterestWithCurrencyEffect: new Big('0'), totalInvestment: new Big('0'), totalInvestmentWithCurrencyEffect: new Big('0') }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts index 0fee9c5c7..b9b7fd900 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts @@ -1,6 +1,6 @@ import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; -import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface'; import { PortfolioOrderItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order-item.interface'; +import { PortfolioSnapshot } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-snapshot.interface'; import { getFactor } from '@ghostfolio/api/helper/portfolio.helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { @@ -23,19 +23,27 @@ import { cloneDeep, first, last, sortBy } from 'lodash'; export class TWRPortfolioCalculator extends PortfolioCalculator { protected calculateOverallPerformance( positions: TimelinePosition[] - ): CurrentPositions { + ): PortfolioSnapshot { let currentValueInBaseCurrency = new Big(0); let grossPerformance = new Big(0); let grossPerformanceWithCurrencyEffect = new Big(0); let hasErrors = false; let netPerformance = new Big(0); let netPerformanceWithCurrencyEffect = new Big(0); + let totalFeesWithCurrencyEffect = new Big(0); + let totalInterestWithCurrencyEffect = new Big(0); let totalInvestment = new Big(0); let totalInvestmentWithCurrencyEffect = new Big(0); let totalTimeWeightedInvestment = new Big(0); let totalTimeWeightedInvestmentWithCurrencyEffect = new Big(0); for (const currentPosition of positions) { + if (currentPosition.fee) { + totalFeesWithCurrencyEffect = totalFeesWithCurrencyEffect.plus( + currentPosition.fee + ); + } + if (currentPosition.valueInBaseCurrency) { currentValueInBaseCurrency = currentValueInBaseCurrency.plus( currentPosition.valueInBaseCurrency @@ -101,6 +109,8 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { hasErrors, netPerformance, netPerformanceWithCurrencyEffect, + totalFeesWithCurrencyEffect, + totalInterestWithCurrencyEffect, totalInvestment, totalInvestmentWithCurrencyEffect, netPerformancePercentage: totalTimeWeightedInvestment.eq(0) @@ -178,6 +188,8 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { let totalDividend = new Big(0); let totalDividendInBaseCurrency = new Big(0); + let totalInterest = new Big(0); + let totalInterestInBaseCurrency = new Big(0); let totalInvestment = new Big(0); let totalInvestmentFromBuyTransactions = new Big(0); let totalInvestmentFromBuyTransactionsWithCurrencyEffect = new Big(0); @@ -198,6 +210,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { return { currentValues: {}, currentValuesWithCurrencyEffect: {}, + feesWithCurrencyEffect: new Big(0), grossPerformance: new Big(0), grossPerformancePercentage: new Big(0), grossPerformancePercentageWithCurrencyEffect: new Big(0), @@ -220,6 +233,8 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { timeWeightedInvestmentWithCurrencyEffect: new Big(0), totalDividend: new Big(0), totalDividendInBaseCurrency: new Big(0), + totalInterest: new Big(0), + totalInterestInBaseCurrency: new Big(0), totalInvestment: new Big(0), totalInvestmentWithCurrencyEffect: new Big(0) }; @@ -240,6 +255,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { return { currentValues: {}, currentValuesWithCurrencyEffect: {}, + feesWithCurrencyEffect: new Big(0), grossPerformance: new Big(0), grossPerformancePercentage: new Big(0), grossPerformancePercentageWithCurrencyEffect: new Big(0), @@ -262,6 +278,8 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { timeWeightedInvestmentWithCurrencyEffect: new Big(0), totalDividend: new Big(0), totalDividendInBaseCurrency: new Big(0), + totalInterest: new Big(0), + totalInterestInBaseCurrency: new Big(0), totalInvestment: new Big(0), totalInvestmentWithCurrencyEffect: new Big(0) }; @@ -511,6 +529,13 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { totalDividendInBaseCurrency = totalDividendInBaseCurrency.plus( dividend.mul(exchangeRateAtOrderDate ?? 1) ); + } else if (order.type === 'INTEREST') { + const interest = order.quantity.mul(order.unitPrice); + + totalInterest = totalInterest.plus(interest); + totalInterestInBaseCurrency = totalInterestInBaseCurrency.plus( + interest.mul(exchangeRateAtOrderDate ?? 1) + ); } const valueOfInvestment = totalUnits.mul(order.unitPriceInBaseCurrency); @@ -808,6 +833,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { return { currentValues, currentValuesWithCurrencyEffect, + feesWithCurrencyEffect, grossPerformancePercentage, grossPerformancePercentageWithCurrencyEffect, initialValue, @@ -823,6 +849,8 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { timeWeightedInvestmentValuesWithCurrencyEffect, totalDividend, totalDividendInBaseCurrency, + totalInterest, + totalInterestInBaseCurrency, totalInvestment, totalInvestmentWithCurrencyEffect, grossPerformance: totalGrossPerformance, diff --git a/apps/api/src/app/portfolio/interfaces/current-positions.interface.ts b/apps/api/src/app/portfolio/interfaces/portfolio-snapshot.interface.ts similarity index 82% rename from apps/api/src/app/portfolio/interfaces/current-positions.interface.ts rename to apps/api/src/app/portfolio/interfaces/portfolio-snapshot.interface.ts index 308cc4037..b8cc904fa 100644 --- a/apps/api/src/app/portfolio/interfaces/current-positions.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/portfolio-snapshot.interface.ts @@ -2,7 +2,7 @@ import { ResponseError, TimelinePosition } from '@ghostfolio/common/interfaces'; import { Big } from 'big.js'; -export interface CurrentPositions extends ResponseError { +export interface PortfolioSnapshot extends ResponseError { currentValueInBaseCurrency: Big; grossPerformance: Big; grossPerformanceWithCurrencyEffect: Big; @@ -15,6 +15,8 @@ export interface CurrentPositions extends ResponseError { netPerformancePercentage: Big; netPerformancePercentageWithCurrencyEffect: Big; positions: TimelinePosition[]; + totalFeesWithCurrencyEffect: Big; + totalInterestWithCurrencyEffect: Big; totalInvestment: Big; totalInvestmentWithCurrencyEffect: Big; } diff --git a/apps/api/src/app/portfolio/interfaces/transaction-point.interface.ts b/apps/api/src/app/portfolio/interfaces/transaction-point.interface.ts index 178df3456..2f5218405 100644 --- a/apps/api/src/app/portfolio/interfaces/transaction-point.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/transaction-point.interface.ts @@ -1,6 +1,10 @@ +import { Big } from 'big.js'; + import { TransactionPointSymbol } from './transaction-point-symbol.interface'; export interface TransactionPoint { date: string; + fees: Big; + interest: Big; items: TransactionPointSymbol[]; } diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index efb9318d7..7ee92e91c 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -107,7 +107,8 @@ export class PortfolioController { dateRange, filters, impersonationId, - withLiabilities, + // TODO + // withLiabilities, withMarkets, userId: this.request.user.id, withSummary: true @@ -389,11 +390,9 @@ export class PortfolioController { @Query('assetClasses') filterByAssetClasses?: string, @Query('range') dateRange: DateRange = 'max', @Query('tags') filterByTags?: string, - @Query('withExcludedAccounts') withExcludedAccountsParam = 'false', - @Query('withItems') withItemsParam = 'false' + @Query('withExcludedAccounts') withExcludedAccountsParam = 'false' ): Promise { const withExcludedAccounts = withExcludedAccountsParam === 'true'; - const withItems = withItemsParam === 'true'; const hasReadRestrictedAccessPermission = this.userService.hasReadRestrictedAccessPermission({ @@ -412,7 +411,6 @@ export class PortfolioController { filters, impersonationId, withExcludedAccounts, - withItems, userId: this.request.user.id }); diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 198395e51..8714a15ec 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -23,12 +23,7 @@ import { EMERGENCY_FUND_TAG_ID, UNKNOWN_KEY } from '@ghostfolio/common/config'; -import { - DATE_FORMAT, - getAllActivityTypes, - getSum, - parseDate -} from '@ghostfolio/common/helper'; +import { DATE_FORMAT, getSum, parseDate } from '@ghostfolio/common/helper'; import { Accounts, EnhancedSymbolProfile, @@ -78,6 +73,7 @@ import { } from 'date-fns'; import { isEmpty, isNumber, last, uniq, uniqBy } from 'lodash'; +import { PortfolioCalculator } from './calculator/portfolio-calculator'; import { PerformanceCalculationType, PortfolioCalculatorFactory @@ -349,19 +345,8 @@ export class PortfolioService { (user.Settings?.settings as UserSettings)?.emergencyFund ?? 0 ); - let types = getAllActivityTypes().filter((activityType) => { - return activityType !== 'FEE'; - }); - - if (withLiabilities === false) { - types = types.filter((activityType) => { - return activityType !== 'LIABILITY'; - }); - } - const { activities } = await this.orderService.getOrders({ filters, - types, userCurrency, userId, withExcludedAccounts @@ -369,16 +354,13 @@ export class PortfolioService { const portfolioCalculator = this.calculatorFactory.createCalculator({ activities, + dateRange, calculationType: PerformanceCalculationType.TWR, currency: userCurrency }); - const { startDate } = getInterval( - dateRange, - portfolioCalculator.getStartDate() - ); - const currentPositions = - await portfolioCalculator.getCurrentPositions(startDate); + const { currentValueInBaseCurrency, hasErrors, positions } = + await portfolioCalculator.getSnapshot(); const cashDetails = await this.accountService.getCashDetails({ filters, @@ -388,10 +370,9 @@ export class PortfolioService { const holdings: PortfolioDetails['holdings'] = {}; - const totalValueInBaseCurrency = - currentPositions.currentValueInBaseCurrency.plus( - cashDetails.balanceInBaseCurrency - ); + const totalValueInBaseCurrency = currentValueInBaseCurrency.plus( + cashDetails.balanceInBaseCurrency + ); const isFilteredByAccount = filters?.some(({ type }) => { @@ -409,7 +390,7 @@ export class PortfolioService { let filteredValueInBaseCurrency = isFilteredByAccount ? totalValueInBaseCurrency - : currentPositions.currentValueInBaseCurrency; + : currentValueInBaseCurrency; if ( filters?.length === 0 || @@ -422,14 +403,12 @@ export class PortfolioService { ); } - const dataGatheringItems = currentPositions.positions.map( - ({ dataSource, symbol }) => { - return { - dataSource, - symbol - }; - } - ); + const dataGatheringItems = positions.map(({ dataSource, symbol }) => { + return { + dataSource, + symbol + }; + }); const [dataProviderResponses, symbolProfiles] = await Promise.all([ this.dataProviderService.getQuotes({ user, items: dataGatheringItems }), @@ -442,7 +421,7 @@ export class PortfolioService { } const portfolioItemsNow: { [symbol: string]: TimelinePosition } = {}; - for (const position of currentPositions.positions) { + for (const position of positions) { portfolioItemsNow[position.symbol] = position; } @@ -465,7 +444,7 @@ export class PortfolioService { tags, transactionCount, valueInBaseCurrency - } of currentPositions.positions) { + } of positions) { if (isFilteredByClosedHoldings === true) { if (!quantity.eq(0)) { // Ignore positions with a quantity @@ -593,6 +572,7 @@ export class PortfolioService { filteredValueInBaseCurrency, holdings, impersonationId, + portfolioCalculator, userCurrency, userId, balanceInBaseCurrency: cashDetails.balanceInBaseCurrency, @@ -605,10 +585,10 @@ export class PortfolioService { return { accounts, + hasErrors, holdings, platforms, - summary, - hasErrors: currentPositions.hasErrors + summary }; } @@ -681,10 +661,9 @@ export class PortfolioService { const portfolioStart = portfolioCalculator.getStartDate(); const transactionPoints = portfolioCalculator.getTransactionPoints(); - const currentPositions = - await portfolioCalculator.getCurrentPositions(portfolioStart); + const { positions } = await portfolioCalculator.getSnapshot(); - const position = currentPositions.positions.find(({ symbol }) => { + const position = positions.find(({ symbol }) => { return symbol === aSymbol; }); @@ -916,13 +895,12 @@ export class PortfolioService { const userId = await this.getUserId(impersonationId, this.request.user.id); const user = await this.userService.user({ id: userId }); - const { endDate, startDate } = getInterval(dateRange); + const { endDate } = getInterval(dateRange); const { activities } = await this.orderService.getOrders({ endDate, filters, userId, - types: ['BUY', 'SELL'], userCurrency: this.getUserCurrency() }); @@ -935,16 +913,14 @@ export class PortfolioService { const portfolioCalculator = this.calculatorFactory.createCalculator({ activities, + dateRange, calculationType: PerformanceCalculationType.TWR, currency: this.request.user.Settings.settings.baseCurrency }); - const currentPositions = await portfolioCalculator.getCurrentPositions( - startDate, - endDate - ); + let { hasErrors, positions } = await portfolioCalculator.getSnapshot(); - let positions = currentPositions.positions.filter(({ quantity }) => { + positions = positions.filter(({ quantity }) => { return !quantity.eq(0); }); @@ -983,7 +959,7 @@ export class PortfolioService { } return { - hasErrors: currentPositions.hasErrors, + hasErrors, positions: positions.map( ({ averagePrice, @@ -1050,15 +1026,13 @@ export class PortfolioService { filters, impersonationId, userId, - withExcludedAccounts = false, - withItems = false + withExcludedAccounts = false }: { dateRange?: DateRange; filters?: Filter[]; impersonationId: string; userId: string; withExcludedAccounts?: boolean; - withItems?: boolean; }): Promise { userId = await this.getUserId(impersonationId, userId); const user = await this.userService.user({ id: userId }); @@ -1096,8 +1070,7 @@ export class PortfolioService { filters, userCurrency, userId, - withExcludedAccounts, - types: withItems ? ['BUY', 'ITEM', 'SELL'] : ['BUY', 'SELL'] + withExcludedAccounts }); if (accountBalanceItems?.length <= 0 && activities?.length <= 0) { @@ -1123,6 +1096,7 @@ export class PortfolioService { const portfolioCalculator = this.calculatorFactory.createCalculator({ activities, + dateRange, calculationType: PerformanceCalculationType.TWR, currency: userCurrency }); @@ -1140,7 +1114,7 @@ export class PortfolioService { netPerformancePercentageWithCurrencyEffect, netPerformanceWithCurrencyEffect, totalInvestment - } = await portfolioCalculator.getCurrentPositions(startDate, endDate); + } = await portfolioCalculator.getSnapshot(); let currentNetPerformance = netPerformance; @@ -1231,8 +1205,7 @@ export class PortfolioService { const { activities } = await this.orderService.getOrders({ userCurrency, - userId, - types: ['BUY', 'SELL'] + userId }); const portfolioCalculator = this.calculatorFactory.createCalculator({ @@ -1241,13 +1214,10 @@ export class PortfolioService { currency: this.request.user.Settings.settings.baseCurrency }); - const currentPositions = await portfolioCalculator.getCurrentPositions( - portfolioCalculator.getStartDate() - ); + let { totalFeesWithCurrencyEffect, positions, totalInvestment } = + await portfolioCalculator.getSnapshot(); - const positions = currentPositions.positions.filter( - (item) => !item.quantity.eq(0) - ); + positions = positions.filter((item) => !item.quantity.eq(0)); const portfolioItemsNow: { [symbol: string]: TimelinePosition } = {}; @@ -1309,8 +1279,8 @@ export class PortfolioService { [ new FeeRatioInitialInvestment( this.exchangeRateDataService, - currentPositions.totalInvestment.toNumber(), - this.getFees({ activities, userCurrency }).toNumber() + totalInvestment.toNumber(), + totalFeesWithCurrencyEffect.toNumber() ) ], userSettings @@ -1454,30 +1424,6 @@ export class PortfolioService { return valueInBaseCurrencyOfEmergencyFundPositions.toNumber(); } - private getFees({ - activities, - userCurrency - }: { - activities: Activity[]; - userCurrency: string; - }) { - return getSum( - activities - .filter(({ isDraft }) => { - return isDraft === false; - }) - .map(({ fee, SymbolProfile }) => { - return new Big( - this.exchangeRateDataService.toCurrency( - fee, - SymbolProfile.currency, - userCurrency - ) - ); - }) - ); - } - private getInitialCashPosition({ balance, currency @@ -1623,6 +1569,7 @@ export class PortfolioService { filteredValueInBaseCurrency, holdings, impersonationId, + portfolioCalculator, userCurrency, userId }: { @@ -1631,6 +1578,7 @@ export class PortfolioService { filteredValueInBaseCurrency: Big; holdings: PortfolioDetails['holdings']; impersonationId: string; + portfolioCalculator: PortfolioCalculator; userCurrency: string; userId: string; }): Promise { @@ -1659,17 +1607,8 @@ export class PortfolioService { } } - const dividendInBaseCurrency = getSum( - ( - await this.getDividends({ - activities: activities.filter(({ type }) => { - return type === 'DIVIDEND'; - }) - }) - ).map(({ investment }) => { - return new Big(investment); - }) - ); + const dividendInBaseCurrency = + await portfolioCalculator.getDividendInBaseCurrency(); const emergencyFund = new Big( Math.max( @@ -1678,15 +1617,13 @@ export class PortfolioService { ) ); - const fees = this.getFees({ activities, userCurrency }).toNumber(); - const firstOrderDate = activities[0]?.date; + const fees = await portfolioCalculator.getFeesInBaseCurrency(); - const interest = this.getSumOfActivityType({ - activities, - userCurrency, - activityType: 'INTEREST' - }).toNumber(); + const firstOrderDate = portfolioCalculator.getStartDate(); + + const interest = await portfolioCalculator.getInterestInBaseCurrency(); + // TODO: Move to portfolio calculator const items = getSum( Object.keys(holdings) .filter((symbol) => { @@ -1701,6 +1638,7 @@ export class PortfolioService { }) ).toNumber(); + // TODO: Move to portfolio calculator const liabilities = getSum( Object.keys(holdings) .filter((symbol) => { @@ -1791,9 +1729,7 @@ export class PortfolioService { annualizedPerformancePercentWithCurrencyEffect, cash, excludedAccountsAndActivities, - fees, firstOrderDate, - interest, items, liabilities, totalBuy, @@ -1807,6 +1743,7 @@ export class PortfolioService { .toNumber(), total: emergencyFund.toNumber() }, + fees: fees.toNumber(), filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(), filteredValueInPercentage: netWorth ? filteredValueInBaseCurrency.div(netWorth).toNumber() @@ -1814,6 +1751,7 @@ export class PortfolioService { fireWealth: new Big(performanceInformation.performance.currentValue) .minus(emergencyFundPositionsValueInBaseCurrency) .toNumber(), + interest: interest.toNumber(), ordersCount: activities.filter(({ type }) => { return type === 'BUY' || type === 'SELL'; }).length, diff --git a/apps/api/src/helper/portfolio.helper.ts b/apps/api/src/helper/portfolio.helper.ts index 730f34bde..f762b2ad5 100644 --- a/apps/api/src/helper/portfolio.helper.ts +++ b/apps/api/src/helper/portfolio.helper.ts @@ -37,36 +37,48 @@ export function getInterval( aDateRange: DateRange, portfolioStart = new Date(0) ) { - let endDate = endOfDay(new Date()); + let endDate = endOfDay(new Date(Date.now())); let startDate = portfolioStart; switch (aDateRange) { case '1d': - startDate = max([startDate, subDays(resetHours(new Date()), 1)]); + startDate = max([ + startDate, + subDays(resetHours(new Date(Date.now())), 1) + ]); break; case 'mtd': startDate = max([ startDate, - subDays(startOfMonth(resetHours(new Date())), 1) + subDays(startOfMonth(resetHours(new Date(Date.now()))), 1) ]); break; case 'wtd': startDate = max([ startDate, - subDays(startOfWeek(resetHours(new Date()), { weekStartsOn: 1 }), 1) + subDays( + startOfWeek(resetHours(new Date(Date.now())), { weekStartsOn: 1 }), + 1 + ) ]); break; case 'ytd': startDate = max([ startDate, - subDays(startOfYear(resetHours(new Date())), 1) + subDays(startOfYear(resetHours(new Date(Date.now()))), 1) ]); break; case '1y': - startDate = max([startDate, subYears(resetHours(new Date()), 1)]); + startDate = max([ + startDate, + subYears(resetHours(new Date(Date.now())), 1) + ]); break; case '5y': - startDate = max([startDate, subYears(resetHours(new Date()), 5)]); + startDate = max([ + startDate, + subYears(resetHours(new Date(Date.now())), 5) + ]); break; case 'max': break; diff --git a/libs/common/src/lib/interfaces/symbol-metrics.interface.ts b/libs/common/src/lib/interfaces/symbol-metrics.interface.ts index ecb80ef92..99a1b3467 100644 --- a/libs/common/src/lib/interfaces/symbol-metrics.interface.ts +++ b/libs/common/src/lib/interfaces/symbol-metrics.interface.ts @@ -7,6 +7,7 @@ export interface SymbolMetrics { currentValuesWithCurrencyEffect: { [date: string]: Big; }; + feesWithCurrencyEffect: Big; grossPerformance: Big; grossPerformancePercentage: Big; grossPerformancePercentageWithCurrencyEffect: Big; @@ -41,6 +42,8 @@ export interface SymbolMetrics { timeWeightedInvestmentWithCurrencyEffect: Big; totalDividend: Big; totalDividendInBaseCurrency: Big; + totalInterest: Big; + totalInterestInBaseCurrency: Big; totalInvestment: Big; totalInvestmentWithCurrencyEffect: Big; } From 5d4e2fba8c8e160df07a735c1e73578a834f7333 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 14 Apr 2024 08:12:32 +0200 Subject: [PATCH 119/120] Feature/move wealth item and liability calculations to portfolio calculator (#3272) * Move (wealth) item calculations to portfolio calculator * Move liability calculations to portfolio calculator * Update changelog --- CHANGELOG.md | 2 + .../calculator/portfolio-calculator.ts | 66 +++++++-- ...aln-buy-and-sell-in-two-activities.spec.ts | 4 +- ...folio-calculator-baln-buy-and-sell.spec.ts | 4 +- .../twr/portfolio-calculator-baln-buy.spec.ts | 4 +- ...ator-btcusd-buy-and-sell-partially.spec.ts | 4 +- .../twr/portfolio-calculator-fee.spec.ts | 4 +- .../portfolio-calculator-googl-buy.spec.ts | 4 +- .../twr/portfolio-calculator-item.spec.ts | 134 ++++++++++++++++++ .../portfolio-calculator-liability.spec.ts | 134 ++++++++++++++++++ ...-calculator-msft-buy-with-dividend.spec.ts | 4 +- .../portfolio-calculator-no-orders.spec.ts | 4 +- ...ulator-novn-buy-and-sell-partially.spec.ts | 4 +- ...folio-calculator-novn-buy-and-sell.spec.ts | 4 +- .../calculator/twr/portfolio-calculator.ts | 38 ++++- .../portfolio-snapshot.interface.ts | 2 + .../interfaces/transaction-point.interface.ts | 2 + .../src/app/portfolio/portfolio.controller.ts | 4 - .../src/app/portfolio/portfolio.service.ts | 42 +----- apps/api/src/helper/portfolio.helper.ts | 2 - .../home-summary/home-summary.component.ts | 2 +- apps/client/src/app/services/data.service.ts | 6 - .../interfaces/symbol-metrics.interface.ts | 4 + 23 files changed, 406 insertions(+), 72 deletions(-) create mode 100644 apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-item.spec.ts create mode 100644 apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-liability.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 7da5c3ddd..4fec3111c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Moved the dividend calculations into the portfolio calculator - Moved the fee calculations into the portfolio calculator - Moved the interest calculations into the portfolio calculator +- Moved the liability calculations into the portfolio calculator +- Moved the (wealth) item calculations into the portfolio calculator ## 2.72.0 - 2024-04-13 diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index 712c0ac04..1d2eadfbf 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -140,7 +140,9 @@ export abstract class PortfolioCalculator { totalFeesWithCurrencyEffect: new Big(0), totalInterestWithCurrencyEffect: new Big(0), totalInvestment: new Big(0), - totalInvestmentWithCurrencyEffect: new Big(0) + totalInvestmentWithCurrencyEffect: new Big(0), + totalLiabilitiesWithCurrencyEffect: new Big(0), + totalValuablesWithCurrencyEffect: new Big(0) }; } @@ -149,6 +151,9 @@ export abstract class PortfolioCalculator { let dates: Date[] = []; let firstIndex = transactionPoints.length; let firstTransactionPoint: TransactionPoint = null; + let totalInterestWithCurrencyEffect = new Big(0); + let totalLiabilitiesWithCurrencyEffect = new Big(0); + let totalValuablesWithCurrencyEffect = new Big(0); dates.push(resetHours(start)); @@ -274,8 +279,11 @@ export abstract class PortfolioCalculator { timeWeightedInvestmentWithCurrencyEffect, totalDividend, totalDividendInBaseCurrency, + totalInterestInBaseCurrency, totalInvestment, - totalInvestmentWithCurrencyEffect + totalInvestmentWithCurrencyEffect, + totalLiabilitiesInBaseCurrency, + totalValuablesInBaseCurrency } = this.getSymbolMetrics({ marketSymbolMap, start, @@ -333,6 +341,17 @@ export abstract class PortfolioCalculator { ) }); + totalInterestWithCurrencyEffect = totalInterestWithCurrencyEffect.plus( + totalInterestInBaseCurrency + ); + + totalLiabilitiesWithCurrencyEffect = + totalLiabilitiesWithCurrencyEffect.plus(totalLiabilitiesInBaseCurrency); + + totalValuablesWithCurrencyEffect = totalValuablesWithCurrencyEffect.plus( + totalValuablesInBaseCurrency + ); + if ( (hasErrors || currentRateErrors.find(({ dataSource, symbol }) => { @@ -350,8 +369,10 @@ export abstract class PortfolioCalculator { ...overall, errors, positions, - hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors, - totalInterestWithCurrencyEffect: lastTransactionPoint.interest + totalInterestWithCurrencyEffect, + totalLiabilitiesWithCurrencyEffect, + totalValuablesWithCurrencyEffect, + hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors }; } @@ -715,6 +736,12 @@ export abstract class PortfolioCalculator { })); } + public async getLiabilitiesInBaseCurrency() { + await this.snapshotPromise; + + return this.snapshot.totalLiabilitiesWithCurrencyEffect; + } + public async getSnapshot() { await this.snapshotPromise; @@ -751,6 +778,12 @@ export abstract class PortfolioCalculator { return this.transactionPoints; } + public async getValuablesInBaseCurrency() { + await this.snapshotPromise; + + return this.snapshot.totalValuablesWithCurrencyEffect; + } + private computeTransactionPoints() { this.transactionPoints = []; const symbols: { [symbol: string]: TransactionPointSymbol } = {}; @@ -767,13 +800,6 @@ export abstract class PortfolioCalculator { type, unitPrice } of this.orders) { - if ( - // TODO - ['ITEM', 'LIABILITY'].includes(type) - ) { - continue; - } - let currentTransactionPointItem: TransactionPointSymbol; const oldAccumulatedSymbol = symbols[SymbolProfile.symbol]; @@ -858,11 +884,25 @@ export abstract class PortfolioCalculator { interest = quantity.mul(unitPrice); } + let liabilities = new Big(0); + + if (type === 'LIABILITY') { + liabilities = quantity.mul(unitPrice); + } + + let valuables = new Big(0); + + if (type === 'ITEM') { + valuables = quantity.mul(unitPrice); + } + if (lastDate !== date || lastTransactionPoint === null) { lastTransactionPoint = { date, fees, interest, + liabilities, + valuables, items: newItems }; @@ -872,6 +912,10 @@ export abstract class PortfolioCalculator { lastTransactionPoint.interest = lastTransactionPoint.interest.plus(interest); lastTransactionPoint.items = newItems; + lastTransactionPoint.liabilities = + lastTransactionPoint.liabilities.plus(liabilities); + lastTransactionPoint.valuables = + lastTransactionPoint.valuables.plus(valuables); } lastDate = date; diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts index 8ddae9df6..a11ae8896 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts @@ -176,7 +176,9 @@ describe('PortfolioCalculator', () => { totalFeesWithCurrencyEffect: new Big('3.2'), totalInterestWithCurrencyEffect: new Big('0'), totalInvestment: new Big('0'), - totalInvestmentWithCurrencyEffect: new Big('0') + totalInvestmentWithCurrencyEffect: new Big('0'), + totalLiabilitiesWithCurrencyEffect: new Big('0'), + totalValuablesWithCurrencyEffect: new Big('0') }); expect(investments).toEqual([ diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts index febd1769d..8d93d8b97 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts @@ -159,7 +159,9 @@ describe('PortfolioCalculator', () => { totalFeesWithCurrencyEffect: new Big('3.2'), totalInterestWithCurrencyEffect: new Big('0'), totalInvestment: new Big('0'), - totalInvestmentWithCurrencyEffect: new Big('0') + totalInvestmentWithCurrencyEffect: new Big('0'), + totalLiabilitiesWithCurrencyEffect: new Big('0'), + totalValuablesWithCurrencyEffect: new Big('0') }); expect(investments).toEqual([ diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts index 2b9fd06f0..f26331134 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts @@ -144,7 +144,9 @@ describe('PortfolioCalculator', () => { totalFeesWithCurrencyEffect: new Big('1.55'), totalInterestWithCurrencyEffect: new Big('0'), totalInvestment: new Big('273.2'), - totalInvestmentWithCurrencyEffect: new Big('273.2') + totalInvestmentWithCurrencyEffect: new Big('273.2'), + totalLiabilitiesWithCurrencyEffect: new Big('0'), + totalValuablesWithCurrencyEffect: new Big('0') }); expect(investments).toEqual([ diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts index ceff92449..2a9ba0916 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts @@ -178,7 +178,9 @@ describe('PortfolioCalculator', () => { totalFeesWithCurrencyEffect: new Big('0'), totalInterestWithCurrencyEffect: new Big('0'), totalInvestment: new Big('320.43'), - totalInvestmentWithCurrencyEffect: new Big('318.542667299999967957') + totalInvestmentWithCurrencyEffect: new Big('318.542667299999967957'), + totalLiabilitiesWithCurrencyEffect: new Big('0'), + totalValuablesWithCurrencyEffect: new Big('0') }); expect(investments).toEqual([ diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-fee.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-fee.spec.ts index b689a0c30..83f99e3cb 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-fee.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-fee.spec.ts @@ -125,7 +125,9 @@ describe('PortfolioCalculator', () => { totalFeesWithCurrencyEffect: new Big('49'), totalInterestWithCurrencyEffect: new Big('0'), totalInvestment: new Big('0'), - totalInvestmentWithCurrencyEffect: new Big('0') + totalInvestmentWithCurrencyEffect: new Big('0'), + totalLiabilitiesWithCurrencyEffect: new Big('0'), + totalValuablesWithCurrencyEffect: new Big('0') }); }); }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts index 911167f7a..0642b28ed 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts @@ -157,7 +157,9 @@ describe('PortfolioCalculator', () => { totalFeesWithCurrencyEffect: new Big('1'), totalInterestWithCurrencyEffect: new Big('0'), totalInvestment: new Big('89.12'), - totalInvestmentWithCurrencyEffect: new Big('82.329056') + totalInvestmentWithCurrencyEffect: new Big('82.329056'), + totalLiabilitiesWithCurrencyEffect: new Big('0'), + totalValuablesWithCurrencyEffect: new Big('0') }); expect(investments).toEqual([ diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-item.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-item.spec.ts new file mode 100644 index 000000000..b8ef6954e --- /dev/null +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-item.spec.ts @@ -0,0 +1,134 @@ +import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; +import { + activityDummyData, + symbolProfileDummyData +} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; +import { + PortfolioCalculatorFactory, + PerformanceCalculationType +} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; +import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; +import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; +import { parseDate } from '@ghostfolio/common/helper'; + +import { Big } from 'big.js'; + +jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + CurrentRateService: jest.fn().mockImplementation(() => { + return CurrentRateServiceMock; + }) + }; +}); + +describe('PortfolioCalculator', () => { + let currentRateService: CurrentRateService; + let exchangeRateDataService: ExchangeRateDataService; + let factory: PortfolioCalculatorFactory; + + beforeEach(() => { + currentRateService = new CurrentRateService(null, null, null, null); + + exchangeRateDataService = new ExchangeRateDataService( + null, + null, + null, + null + ); + + factory = new PortfolioCalculatorFactory( + currentRateService, + exchangeRateDataService + ); + }); + + describe('compute portfolio snapshot', () => { + it.only('with item activity', async () => { + const spy = jest + .spyOn(Date, 'now') + .mockImplementation(() => parseDate('2022-01-31').getTime()); + + const activities: Activity[] = [ + { + ...activityDummyData, + date: new Date('2022-01-01'), + fee: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'MANUAL', + name: 'Penthouse Apartment', + symbol: 'dac95060-d4f2-4653-a253-2c45e6fb5cde' + }, + type: 'ITEM', + unitPrice: 500000 + } + ]; + + const portfolioCalculator = factory.createCalculator({ + activities, + calculationType: PerformanceCalculationType.TWR, + currency: 'USD' + }); + + const portfolioSnapshot = await portfolioCalculator.computeSnapshot( + parseDate('2022-01-01') + ); + + spy.mockRestore(); + + expect(portfolioSnapshot).toEqual({ + currentValueInBaseCurrency: new Big('0'), + errors: [], + grossPerformance: new Big('0'), + grossPerformancePercentage: new Big('0'), + grossPerformancePercentageWithCurrencyEffect: new Big('0'), + grossPerformanceWithCurrencyEffect: new Big('0'), + hasErrors: true, + netPerformance: new Big('0'), + netPerformancePercentage: new Big('0'), + netPerformancePercentageWithCurrencyEffect: new Big('0'), + netPerformanceWithCurrencyEffect: new Big('0'), + positions: [ + { + averagePrice: new Big('500000'), + currency: 'USD', + dataSource: 'MANUAL', + dividend: new Big('0'), + dividendInBaseCurrency: new Big('0'), + fee: new Big('0'), + firstBuyDate: '2022-01-01', + grossPerformance: null, + grossPerformancePercentage: null, + grossPerformancePercentageWithCurrencyEffect: null, + grossPerformanceWithCurrencyEffect: null, + investment: new Big('0'), + investmentWithCurrencyEffect: new Big('0'), + marketPrice: null, + marketPriceInBaseCurrency: 500000, + netPerformance: null, + netPerformancePercentage: null, + netPerformancePercentageWithCurrencyEffect: null, + netPerformanceWithCurrencyEffect: null, + quantity: new Big('0'), + symbol: 'dac95060-d4f2-4653-a253-2c45e6fb5cde', + tags: [], + timeWeightedInvestment: new Big('0'), + timeWeightedInvestmentWithCurrencyEffect: new Big('0'), + transactionCount: 1, + valueInBaseCurrency: new Big('0') + } + ], + totalFeesWithCurrencyEffect: new Big('0'), + totalInterestWithCurrencyEffect: new Big('0'), + totalInvestment: new Big('0'), + totalInvestmentWithCurrencyEffect: new Big('0'), + totalLiabilitiesWithCurrencyEffect: new Big('0'), + totalValuablesWithCurrencyEffect: new Big('0') + }); + }); + }); +}); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-liability.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-liability.spec.ts new file mode 100644 index 000000000..9ef369c8f --- /dev/null +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-liability.spec.ts @@ -0,0 +1,134 @@ +import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; +import { + activityDummyData, + symbolProfileDummyData +} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; +import { + PortfolioCalculatorFactory, + PerformanceCalculationType +} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; +import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; +import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; +import { parseDate } from '@ghostfolio/common/helper'; + +import { Big } from 'big.js'; + +jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + CurrentRateService: jest.fn().mockImplementation(() => { + return CurrentRateServiceMock; + }) + }; +}); + +describe('PortfolioCalculator', () => { + let currentRateService: CurrentRateService; + let exchangeRateDataService: ExchangeRateDataService; + let factory: PortfolioCalculatorFactory; + + beforeEach(() => { + currentRateService = new CurrentRateService(null, null, null, null); + + exchangeRateDataService = new ExchangeRateDataService( + null, + null, + null, + null + ); + + factory = new PortfolioCalculatorFactory( + currentRateService, + exchangeRateDataService + ); + }); + + describe('compute portfolio snapshot', () => { + it.only('with liability activity', async () => { + const spy = jest + .spyOn(Date, 'now') + .mockImplementation(() => parseDate('2022-01-31').getTime()); + + const activities: Activity[] = [ + { + ...activityDummyData, + date: new Date('2022-01-01'), + fee: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'MANUAL', + name: 'Loan', + symbol: '55196015-1365-4560-aa60-8751ae6d18f8' + }, + type: 'LIABILITY', + unitPrice: 3000 + } + ]; + + const portfolioCalculator = factory.createCalculator({ + activities, + calculationType: PerformanceCalculationType.TWR, + currency: 'USD' + }); + + const portfolioSnapshot = await portfolioCalculator.computeSnapshot( + parseDate('2022-01-01') + ); + + spy.mockRestore(); + + expect(portfolioSnapshot).toEqual({ + currentValueInBaseCurrency: new Big('0'), + errors: [], + grossPerformance: new Big('0'), + grossPerformancePercentage: new Big('0'), + grossPerformancePercentageWithCurrencyEffect: new Big('0'), + grossPerformanceWithCurrencyEffect: new Big('0'), + hasErrors: true, + netPerformance: new Big('0'), + netPerformancePercentage: new Big('0'), + netPerformancePercentageWithCurrencyEffect: new Big('0'), + netPerformanceWithCurrencyEffect: new Big('0'), + positions: [ + { + averagePrice: new Big('3000'), + currency: 'USD', + dataSource: 'MANUAL', + dividend: new Big('0'), + dividendInBaseCurrency: new Big('0'), + fee: new Big('0'), + firstBuyDate: '2022-01-01', + grossPerformance: null, + grossPerformancePercentage: null, + grossPerformancePercentageWithCurrencyEffect: null, + grossPerformanceWithCurrencyEffect: null, + investment: new Big('0'), + investmentWithCurrencyEffect: new Big('0'), + marketPrice: null, + marketPriceInBaseCurrency: 3000, + netPerformance: null, + netPerformancePercentage: null, + netPerformancePercentageWithCurrencyEffect: null, + netPerformanceWithCurrencyEffect: null, + quantity: new Big('0'), + symbol: '55196015-1365-4560-aa60-8751ae6d18f8', + tags: [], + timeWeightedInvestment: new Big('0'), + timeWeightedInvestmentWithCurrencyEffect: new Big('0'), + transactionCount: 1, + valueInBaseCurrency: new Big('0') + } + ], + totalFeesWithCurrencyEffect: new Big('0'), + totalInterestWithCurrencyEffect: new Big('0'), + totalInvestment: new Big('0'), + totalInvestmentWithCurrencyEffect: new Big('0'), + totalLiabilitiesWithCurrencyEffect: new Big('0'), + totalValuablesWithCurrencyEffect: new Big('0') + }); + }); + }); +}); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts index 6dc489c5d..e50ce4194 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts @@ -133,7 +133,9 @@ describe('PortfolioCalculator', () => { totalFeesWithCurrencyEffect: new Big('19'), totalInterestWithCurrencyEffect: new Big('0'), totalInvestment: new Big('298.58'), - totalInvestmentWithCurrencyEffect: new Big('298.58') + totalInvestmentWithCurrencyEffect: new Big('298.58'), + totalLiabilitiesWithCurrencyEffect: new Big('0'), + totalValuablesWithCurrencyEffect: new Big('0') }); }); }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-no-orders.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-no-orders.spec.ts index ece39c87b..1d69abfbf 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-no-orders.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-no-orders.spec.ts @@ -83,7 +83,9 @@ describe('PortfolioCalculator', () => { totalFeesWithCurrencyEffect: new Big('0'), totalInterestWithCurrencyEffect: new Big('0'), totalInvestment: new Big(0), - totalInvestmentWithCurrencyEffect: new Big(0) + totalInvestmentWithCurrencyEffect: new Big(0), + totalLiabilitiesWithCurrencyEffect: new Big('0'), + totalValuablesWithCurrencyEffect: new Big('0') }); expect(investments).toEqual([]); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts index a3c12829a..3d63f1a5d 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts @@ -161,7 +161,9 @@ describe('PortfolioCalculator', () => { totalFeesWithCurrencyEffect: new Big('4.25'), totalInterestWithCurrencyEffect: new Big('0'), totalInvestment: new Big('75.80'), - totalInvestmentWithCurrencyEffect: new Big('75.80') + totalInvestmentWithCurrencyEffect: new Big('75.80'), + totalLiabilitiesWithCurrencyEffect: new Big('0'), + totalValuablesWithCurrencyEffect: new Big('0') }); expect(investments).toEqual([ diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts index f1bf56f11..6f0b03800 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts @@ -185,7 +185,9 @@ describe('PortfolioCalculator', () => { totalFeesWithCurrencyEffect: new Big('0'), totalInterestWithCurrencyEffect: new Big('0'), totalInvestment: new Big('0'), - totalInvestmentWithCurrencyEffect: new Big('0') + totalInvestmentWithCurrencyEffect: new Big('0'), + totalLiabilitiesWithCurrencyEffect: new Big('0'), + totalValuablesWithCurrencyEffect: new Big('0') }); expect(investments).toEqual([ diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts index b9b7fd900..7dcef89cb 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts @@ -109,6 +109,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { hasErrors, netPerformance, netPerformanceWithCurrencyEffect, + positions, totalFeesWithCurrencyEffect, totalInterestWithCurrencyEffect, totalInvestment, @@ -131,7 +132,8 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { : grossPerformanceWithCurrencyEffect.div( totalTimeWeightedInvestmentWithCurrencyEffect ), - positions + totalLiabilitiesWithCurrencyEffect: new Big(0), + totalValuablesWithCurrencyEffect: new Big(0) }; } @@ -194,8 +196,12 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { let totalInvestmentFromBuyTransactions = new Big(0); let totalInvestmentFromBuyTransactionsWithCurrencyEffect = new Big(0); let totalInvestmentWithCurrencyEffect = new Big(0); + let totalLiabilities = new Big(0); + let totalLiabilitiesInBaseCurrency = new Big(0); let totalQuantityFromBuyTransactions = new Big(0); let totalUnits = new Big(0); + let totalValuables = new Big(0); + let totalValuablesInBaseCurrency = new Big(0); let valueAtStartDate: Big; let valueAtStartDateWithCurrencyEffect: Big; @@ -236,7 +242,11 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { totalInterest: new Big(0), totalInterestInBaseCurrency: new Big(0), totalInvestment: new Big(0), - totalInvestmentWithCurrencyEffect: new Big(0) + totalInvestmentWithCurrencyEffect: new Big(0), + totalLiabilities: new Big(0), + totalLiabilitiesInBaseCurrency: new Big(0), + totalValuables: new Big(0), + totalValuablesInBaseCurrency: new Big(0) }; } @@ -281,7 +291,11 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { totalInterest: new Big(0), totalInterestInBaseCurrency: new Big(0), totalInvestment: new Big(0), - totalInvestmentWithCurrencyEffect: new Big(0) + totalInvestmentWithCurrencyEffect: new Big(0), + totalLiabilities: new Big(0), + totalLiabilitiesInBaseCurrency: new Big(0), + totalValuables: new Big(0), + totalValuablesInBaseCurrency: new Big(0) }; } @@ -536,6 +550,20 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { totalInterestInBaseCurrency = totalInterestInBaseCurrency.plus( interest.mul(exchangeRateAtOrderDate ?? 1) ); + } else if (order.type === 'ITEM') { + const valuables = order.quantity.mul(order.unitPrice); + + totalValuables = totalValuables.plus(valuables); + totalValuablesInBaseCurrency = totalValuablesInBaseCurrency.plus( + valuables.mul(exchangeRateAtOrderDate ?? 1) + ); + } else if (order.type === 'LIABILITY') { + const liabilities = order.quantity.mul(order.unitPrice); + + totalLiabilities = totalLiabilities.plus(liabilities); + totalLiabilitiesInBaseCurrency = totalLiabilitiesInBaseCurrency.plus( + liabilities.mul(exchangeRateAtOrderDate ?? 1) + ); } const valueOfInvestment = totalUnits.mul(order.unitPriceInBaseCurrency); @@ -853,6 +881,10 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { totalInterestInBaseCurrency, totalInvestment, totalInvestmentWithCurrencyEffect, + totalLiabilities, + totalLiabilitiesInBaseCurrency, + totalValuables, + totalValuablesInBaseCurrency, grossPerformance: totalGrossPerformance, grossPerformanceWithCurrencyEffect: totalGrossPerformanceWithCurrencyEffect, diff --git a/apps/api/src/app/portfolio/interfaces/portfolio-snapshot.interface.ts b/apps/api/src/app/portfolio/interfaces/portfolio-snapshot.interface.ts index b8cc904fa..d89734987 100644 --- a/apps/api/src/app/portfolio/interfaces/portfolio-snapshot.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/portfolio-snapshot.interface.ts @@ -19,4 +19,6 @@ export interface PortfolioSnapshot extends ResponseError { totalInterestWithCurrencyEffect: Big; totalInvestment: Big; totalInvestmentWithCurrencyEffect: Big; + totalLiabilitiesWithCurrencyEffect: Big; + totalValuablesWithCurrencyEffect: Big; } diff --git a/apps/api/src/app/portfolio/interfaces/transaction-point.interface.ts b/apps/api/src/app/portfolio/interfaces/transaction-point.interface.ts index 2f5218405..fcbea81ca 100644 --- a/apps/api/src/app/portfolio/interfaces/transaction-point.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/transaction-point.interface.ts @@ -7,4 +7,6 @@ export interface TransactionPoint { fees: Big; interest: Big; items: TransactionPointSymbol[]; + liabilities: Big; + valuables: Big; } diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 7ee92e91c..56c0a231c 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -78,10 +78,8 @@ export class PortfolioController { @Query('assetClasses') filterByAssetClasses?: string, @Query('range') dateRange: DateRange = 'max', @Query('tags') filterByTags?: string, - @Query('withLiabilities') withLiabilitiesParam = 'false', @Query('withMarkets') withMarketsParam = 'false' ): Promise { - const withLiabilities = withLiabilitiesParam === 'true'; const withMarkets = withMarketsParam === 'true'; let hasDetails = true; @@ -107,8 +105,6 @@ export class PortfolioController { dateRange, filters, impersonationId, - // TODO - // withLiabilities, withMarkets, userId: this.request.user.id, withSummary: true diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 8714a15ec..95a68eaae 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -60,7 +60,6 @@ import { Prisma } from '@prisma/client'; import { Big } from 'big.js'; -import { isUUID } from 'class-validator'; import { differenceInDays, format, @@ -324,7 +323,6 @@ export class PortfolioService { impersonationId, userId, withExcludedAccounts = false, - withLiabilities = false, withMarkets = false, withSummary = false }: { @@ -333,7 +331,6 @@ export class PortfolioService { impersonationId: string; userId: string; withExcludedAccounts?: boolean; - withLiabilities?: boolean; withMarkets?: boolean; withSummary?: boolean; }): Promise { @@ -1623,35 +1620,10 @@ export class PortfolioService { const interest = await portfolioCalculator.getInterestInBaseCurrency(); - // TODO: Move to portfolio calculator - const items = getSum( - Object.keys(holdings) - .filter((symbol) => { - return ( - isUUID(symbol) && - holdings[symbol].dataSource === 'MANUAL' && - holdings[symbol].valueInBaseCurrency > 0 - ); - }) - .map((symbol) => { - return new Big(holdings[symbol].valueInBaseCurrency).abs(); - }) - ).toNumber(); - - // TODO: Move to portfolio calculator - const liabilities = getSum( - Object.keys(holdings) - .filter((symbol) => { - return ( - isUUID(symbol) && - holdings[symbol].dataSource === 'MANUAL' && - holdings[symbol].valueInBaseCurrency < 0 - ); - }) - .map((symbol) => { - return new Big(holdings[symbol].valueInBaseCurrency).abs(); - }) - ).toNumber(); + const liabilities = + await portfolioCalculator.getLiabilitiesInBaseCurrency(); + + const valuables = await portfolioCalculator.getValuablesInBaseCurrency(); const totalBuy = this.getSumOfActivityType({ userCurrency, @@ -1701,7 +1673,7 @@ export class PortfolioService { const netWorth = new Big(balanceInBaseCurrency) .plus(performanceInformation.performance.currentValue) - .plus(items) + .plus(valuables) .plus(excludedAccountsAndActivities) .minus(liabilities) .toNumber(); @@ -1730,8 +1702,6 @@ export class PortfolioService { cash, excludedAccountsAndActivities, firstOrderDate, - items, - liabilities, totalBuy, totalSell, committedFunds: committedFunds.toNumber(), @@ -1752,6 +1722,8 @@ export class PortfolioService { .minus(emergencyFundPositionsValueInBaseCurrency) .toNumber(), interest: interest.toNumber(), + items: valuables.toNumber(), + liabilities: liabilities.toNumber(), ordersCount: activities.filter(({ type }) => { return type === 'BUY' || type === 'SELL'; }).length, diff --git a/apps/api/src/helper/portfolio.helper.ts b/apps/api/src/helper/portfolio.helper.ts index f762b2ad5..21b111395 100644 --- a/apps/api/src/helper/portfolio.helper.ts +++ b/apps/api/src/helper/portfolio.helper.ts @@ -18,10 +18,8 @@ export function getFactor(activityType: ActivityType) { switch (activityType) { case 'BUY': - case 'ITEM': factor = 1; break; - case 'LIABILITY': case 'SELL': factor = -1; break; diff --git a/apps/client/src/app/components/home-summary/home-summary.component.ts b/apps/client/src/app/components/home-summary/home-summary.component.ts index bb68a4627..b67b67ce5 100644 --- a/apps/client/src/app/components/home-summary/home-summary.component.ts +++ b/apps/client/src/app/components/home-summary/home-summary.component.ts @@ -102,7 +102,7 @@ export class HomeSummaryComponent implements OnDestroy, OnInit { this.isLoading = true; this.dataService - .fetchPortfolioDetails({ withLiabilities: true }) + .fetchPortfolioDetails() .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(({ summary }) => { this.summary = summary; diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index aeeb2f07b..8a3f7d293 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -411,19 +411,13 @@ export class DataService { public fetchPortfolioDetails({ filters, - withLiabilities = false, withMarkets = false }: { filters?: Filter[]; - withLiabilities?: boolean; withMarkets?: boolean; } = {}): Observable { let params = this.buildFiltersAsQueryParams({ filters }); - if (withLiabilities) { - params = params.append('withLiabilities', withLiabilities); - } - if (withMarkets) { params = params.append('withMarkets', withMarkets); } diff --git a/libs/common/src/lib/interfaces/symbol-metrics.interface.ts b/libs/common/src/lib/interfaces/symbol-metrics.interface.ts index 99a1b3467..57eed9212 100644 --- a/libs/common/src/lib/interfaces/symbol-metrics.interface.ts +++ b/libs/common/src/lib/interfaces/symbol-metrics.interface.ts @@ -46,4 +46,8 @@ export interface SymbolMetrics { totalInterestInBaseCurrency: Big; totalInvestment: Big; totalInvestmentWithCurrencyEffect: Big; + totalLiabilities: Big; + totalLiabilitiesInBaseCurrency: Big; + totalValuables: Big; + totalValuablesInBaseCurrency: Big; } From 9241c04d5a2099665b925b735d3319307b429c52 Mon Sep 17 00:00:00 2001 From: Fedron <40535546+Fedron@users.noreply.github.com> Date: Sun, 14 Apr 2024 18:52:41 +0100 Subject: [PATCH 120/120] Feature/add form validation against DTO for activity and account (#3230) * Add form validation against DTO for activity and account * Update changelog --- CHANGELOG.md | 5 +++ ...eate-or-update-account-dialog.component.ts | 31 +++++++++++---- ...ate-or-update-activity-dialog.component.ts | 35 +++++++++++++---- apps/client/src/app/util/form.util.ts | 38 +++++++++++++++++++ 4 files changed, 94 insertions(+), 15 deletions(-) create mode 100644 apps/client/src/app/util/form.util.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fec3111c..8e26c1369 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added a form validation against the DTO in the create or update account dialog +- Added a form validation against the DTO in the create or update activity dialog + ### Changed - Moved the dividend calculations into the portfolio calculator diff --git a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts index c97bbf113..4e3ef335e 100644 --- a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts +++ b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts @@ -1,6 +1,7 @@ import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto'; import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto'; import { DataService } from '@ghostfolio/client/services/data.service'; +import { validateObjectForForm } from '@ghostfolio/client/util/form.util'; import { Currency } from '@ghostfolio/common/interfaces'; import { @@ -102,7 +103,7 @@ export class CreateOrUpdateAccountDialog implements OnDestroy { this.dialogRef.close(); } - public onSubmit() { + public async onSubmit() { const account: CreateAccountDto | UpdateAccountDto = { balance: this.accountForm.controls['balance'].value, comment: this.accountForm.controls['comment'].value, @@ -113,13 +114,29 @@ export class CreateOrUpdateAccountDialog implements OnDestroy { platformId: this.accountForm.controls['platformId'].value?.id ?? null }; - if (this.data.account.id) { - (account as UpdateAccountDto).id = this.data.account.id; - } else { - delete (account as CreateAccountDto).id; - } + try { + if (this.data.account.id) { + (account as UpdateAccountDto).id = this.data.account.id; + + await validateObjectForForm({ + classDto: UpdateAccountDto, + form: this.accountForm, + object: account + }); + } else { + delete (account as CreateAccountDto).id; + + await validateObjectForForm({ + classDto: CreateAccountDto, + form: this.accountForm, + object: account + }); + } - this.dialogRef.close({ account }); + this.dialogRef.close({ account }); + } catch (error) { + console.error(error); + } } public ngOnDestroy() { diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts index 21a2ca920..1196de58c 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts @@ -1,6 +1,7 @@ import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto'; import { DataService } from '@ghostfolio/client/services/data.service'; +import { validateObjectForForm } from '@ghostfolio/client/util/form.util'; import { getDateFormatString } from '@ghostfolio/common/helper'; import { translate } from '@ghostfolio/ui/i18n'; @@ -451,7 +452,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { ); } - public onSubmit() { + public async onSubmit() { const activity: CreateOrderDto | UpdateOrderDto = { accountId: this.activityForm.controls['accountId'].value, assetClass: this.activityForm.controls['assetClass'].value, @@ -474,14 +475,32 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { unitPrice: this.activityForm.controls['unitPrice'].value }; - if (this.data.activity.id) { - (activity as UpdateOrderDto).id = this.data.activity.id; - } else { - (activity as CreateOrderDto).updateAccountBalance = - this.activityForm.controls['updateAccountBalance'].value; - } + try { + if (this.data.activity.id) { + (activity as UpdateOrderDto).id = this.data.activity.id; - this.dialogRef.close({ activity }); + await validateObjectForForm({ + classDto: UpdateOrderDto, + form: this.activityForm, + ignoreFields: ['dataSource', 'date'], + object: activity as UpdateOrderDto + }); + } else { + (activity as CreateOrderDto).updateAccountBalance = + this.activityForm.controls['updateAccountBalance'].value; + + await validateObjectForForm({ + classDto: CreateOrderDto, + form: this.activityForm, + ignoreFields: ['dataSource', 'date'], + object: activity + }); + } + + this.dialogRef.close({ activity }); + } catch (error) { + console.error(error); + } } public ngOnDestroy() { diff --git a/apps/client/src/app/util/form.util.ts b/apps/client/src/app/util/form.util.ts new file mode 100644 index 000000000..d11490c7e --- /dev/null +++ b/apps/client/src/app/util/form.util.ts @@ -0,0 +1,38 @@ +import { FormGroup } from '@angular/forms'; +import { plainToInstance } from 'class-transformer'; +import { validate } from 'class-validator'; + +export async function validateObjectForForm({ + classDto, + form, + ignoreFields = [], + object +}: { + classDto: { new (): T }; + form: FormGroup; + ignoreFields?: string[]; + object: T; +}): Promise { + const objectInstance = plainToInstance(classDto, object); + const errors = await validate(objectInstance as object); + + const nonIgnoredErrors = errors.filter(({ property }) => { + return !ignoreFields.includes(property); + }); + + if (nonIgnoredErrors.length === 0) { + return Promise.resolve(); + } + + for (const { constraints, property } of nonIgnoredErrors) { + const formControl = form.get(property); + + if (formControl) { + formControl.setErrors({ + validationError: Object.values(constraints)[0] + }); + } + } + + return Promise.reject(nonIgnoredErrors); +}