diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fd872a1c..e314c023e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,61 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Upgraded `Node.js` from version `16` to `18` (`Dockerfile`) +## 1.229.0 - 2023-01-21 + +### Added + +- Added a blog post: _Ghostfolio auf Sackgeld.com vorgestellt_ +- Added _Sackgeld.com_ to the _As seen in_ section on the landing page + +### Changed + +- Removed the toggle _Original Shares_ vs. _Current Shares_ on the allocations page +- Hid error messages related to no current investment in the client +- Refactored the value redaction interceptor for the impersonation mode + +### Fixed + +- Fixed the value of the active (emergency fund) filter in percentage on the allocations page + +## 1.228.1 - 2023-01-18 + +### Added + +- Extended the hints in user settings + +### Changed + +- Improved the date formatting in the tooltip of the dividend timeline grouped by month / year +- Improved the date formatting in the tooltip of the investment timeline grouped by month / year +- Reduced the execution interval of the data gathering to every 4 hours +- Removed emergency fund as an asset class + +## 1.227.1 - 2023-01-14 + +### Changed + +- Improved the language localization for German (`de`) + +### Fixed + +- Fixed the create or edit activity dialog + +## 1.227.0 - 2023-01-14 + +### Added + +- Added support for assets other than cash in emergency fund (affecting buying power) +- Added support for translated tags + +### Changed + +- Improved the logo alignment + +### Fixed + +- Fixed the grouping by month / year of the dividend and investment timeline + ## 1.226.0 - 2023-01-11 ### Added diff --git a/README.md b/README.md index f5ad6d4db..08c758a5d 100644 --- a/README.md +++ b/README.md @@ -269,7 +269,7 @@ You can get the _Bearer Token_ via `GET http://localhost:3333/api/v1/auth/anonym Ghostfolio is **100% free** and **open source**. We encourage and support an active and healthy community that accepts contributions from the public - including you. -Not sure what to work on? We have got some ideas. Please join the Ghostfolio [Slack channel](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg), tweet to [@ghostfolio\_](https://twitter.com/ghostfolio_) or send an e-mail to hi@ghostfol.io. We would love to hear from you. +Not sure what to work on? We have got some ideas. Please join the Ghostfolio [Slack channel](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg) or tweet to [@ghostfolio\_](https://twitter.com/ghostfolio_). We would love to hear from you. If you like to support this project, get [**Ghostfolio Premium**](https://ghostfol.io/en/pricing) or [**Buy me a coffee**](https://www.buymeacoffee.com/ghostfolio). diff --git a/apps/api/src/app/account/account.controller.ts b/apps/api/src/app/account/account.controller.ts index 9e170b7fd..2a71042c8 100644 --- a/apps/api/src/app/account/account.controller.ts +++ b/apps/api/src/app/account/account.controller.ts @@ -1,9 +1,6 @@ import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; import { UserService } from '@ghostfolio/api/app/user/user.service'; -import { - nullifyValuesInObject, - nullifyValuesInObjects -} from '@ghostfolio/api/helper/object.helper'; +import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service'; import { Accounts } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; @@ -22,7 +19,8 @@ import { Param, Post, Put, - UseGuards + UseGuards, + UseInterceptors } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; @@ -85,6 +83,7 @@ export class AccountController { @Get() @UseGuards(AuthGuard('jwt')) + @UseInterceptors(RedactValuesInResponseInterceptor) public async getAllAccounts( @Headers('impersonation-id') impersonationId ): Promise { @@ -94,39 +93,15 @@ export class AccountController { this.request.user.id ); - let accountsWithAggregations = - await this.portfolioService.getAccountsWithAggregations({ - userId: impersonationUserId || this.request.user.id, - withExcludedAccounts: true - }); - - if ( - impersonationUserId || - this.userService.isRestrictedView(this.request.user) - ) { - accountsWithAggregations = { - ...nullifyValuesInObject(accountsWithAggregations, [ - 'totalBalanceInBaseCurrency', - 'totalValueInBaseCurrency' - ]), - accounts: nullifyValuesInObjects(accountsWithAggregations.accounts, [ - 'balance', - 'balanceInBaseCurrency', - 'convertedBalance', - 'fee', - 'quantity', - 'unitPrice', - 'value', - 'valueInBaseCurrency' - ]) - }; - } - - return accountsWithAggregations; + return this.portfolioService.getAccountsWithAggregations({ + userId: impersonationUserId || this.request.user.id, + withExcludedAccounts: true + }); } @Get(':id') @UseGuards(AuthGuard('jwt')) + @UseInterceptors(RedactValuesInResponseInterceptor) public async getAccountById( @Headers('impersonation-id') impersonationId, @Param('id') id: string @@ -137,35 +112,13 @@ export class AccountController { this.request.user.id ); - let accountsWithAggregations = + const accountsWithAggregations = await this.portfolioService.getAccountsWithAggregations({ filters: [{ id, type: 'ACCOUNT' }], userId: impersonationUserId || this.request.user.id, withExcludedAccounts: true }); - if ( - impersonationUserId || - this.userService.isRestrictedView(this.request.user) - ) { - accountsWithAggregations = { - ...nullifyValuesInObject(accountsWithAggregations, [ - 'totalBalanceInBaseCurrency', - 'totalValueInBaseCurrency' - ]), - accounts: nullifyValuesInObjects(accountsWithAggregations.accounts, [ - 'balance', - 'balanceInBaseCurrency', - 'convertedBalance', - 'fee', - 'quantity', - 'unitPrice', - 'value', - 'valueInBaseCurrency' - ]) - }; - } - return accountsWithAggregations.accounts[0]; } diff --git a/apps/api/src/app/frontend.middleware.ts b/apps/api/src/app/frontend.middleware.ts index eb9e5561c..8165af216 100644 --- a/apps/api/src/app/frontend.middleware.ts +++ b/apps/api/src/app/frontend.middleware.ts @@ -83,6 +83,13 @@ export class FrontendMiddleware implements NestMiddleware { ) { featureGraphicPath = 'assets/images/blog/20221226.jpg'; title = `The importance of tracking your personal finances - ${title}`; + } else if ( + request.path.startsWith( + '/de/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt' + ) + ) { + featureGraphicPath = 'assets/images/blog/ghostfolio-x-sackgeld.png'; + title = `Ghostfolio auf Sackgeld.com vorgestellt - ${title}`; } if ( diff --git a/apps/api/src/app/portfolio/portfolio-calculator.ts b/apps/api/src/app/portfolio/portfolio-calculator.ts index 48f1e7507..9c659c167 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.ts @@ -447,7 +447,7 @@ export class PortfolioCalculator { transactionCount: item.transactionCount }); - if (hasErrors) { + if (hasErrors && item.investment.gt(0)) { errors.push({ dataSource: item.dataSource, symbol: item.symbol }); } } diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 3a1fa9898..e607a5f96 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -356,6 +356,7 @@ export class PortfolioController { @Get('positions') @UseGuards(AuthGuard('jwt')) + @UseInterceptors(RedactValuesInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor) public async getPositions( @Headers('impersonation-id') impersonationId: string, @@ -370,27 +371,11 @@ export class PortfolioController { filterByTags }); - const result = await this.portfolioService.getPositions({ + return this.portfolioService.getPositions({ dateRange, filters, impersonationId }); - - if ( - impersonationId || - this.userService.isRestrictedView(this.request.user) - ) { - result.positions = result.positions.map((position) => { - return nullifyValuesInObject(position, [ - 'grossPerformance', - 'investment', - 'netPerformance', - 'quantity' - ]); - }); - } - - return result; } @Get('public/:accessId') @@ -441,7 +426,7 @@ export class PortfolioController { for (const [symbol, portfolioPosition] of Object.entries(holdings)) { portfolioPublicDetails.holdings[symbol] = { - allocationCurrent: portfolioPosition.value / totalValue, + allocationInPercentage: portfolioPosition.value / totalValue, countries: hasDetails ? portfolioPosition.countries : [], currency: hasDetails ? portfolioPosition.currency : undefined, dataSource: portfolioPosition.dataSource, @@ -460,6 +445,7 @@ export class PortfolioController { } @Get('position/:dataSource/:symbol') + @UseInterceptors(RedactValuesInResponseInterceptor) @UseInterceptors(TransformDataSourceInRequestInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor) @UseGuards(AuthGuard('jwt')) @@ -468,27 +454,13 @@ export class PortfolioController { @Param('dataSource') dataSource, @Param('symbol') symbol ): Promise { - let position = await this.portfolioService.getPosition( + const position = await this.portfolioService.getPosition( dataSource, impersonationId, symbol ); if (position) { - if ( - impersonationId || - this.userService.isRestrictedView(this.request.user) - ) { - position = nullifyValuesInObject(position, [ - 'grossPerformance', - 'investment', - 'netPerformance', - 'orders', - 'quantity', - 'value' - ]); - } - return position; } diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index f752010b3..3410be220 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -1,5 +1,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 { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface'; @@ -19,7 +20,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate- import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; import { - ASSET_SUB_CLASS_EMERGENCY_FUND, + EMERGENCY_FUND_TAG_ID, MAX_CHART_ITEMS, UNKNOWN_KEY } from '@ghostfolio/common/config'; @@ -536,12 +537,9 @@ export class PortfolioService { holdings[item.symbol] = { markets, - allocationCurrent: filteredValueInBaseCurrency.eq(0) + allocationInPercentage: filteredValueInBaseCurrency.eq(0) ? 0 : value.div(filteredValueInBaseCurrency).toNumber(), - allocationInvestment: item.investment - .div(totalInvestmentInBaseCurrency) - .toNumber(), assetClass: symbolProfile.assetClass, assetSubClass: symbolProfile.assetSubClass, countries: symbolProfile.countries, @@ -574,7 +572,6 @@ export class PortfolioService { ) { const cashPositions = await this.getCashPositions({ cashDetails, - emergencyFund, userCurrency, investment: totalInvestmentInBaseCurrency, value: filteredValueInBaseCurrency @@ -594,10 +591,52 @@ export class PortfolioService { withExcludedAccounts }); + if ( + filters?.length === 1 && + filters[0].id === EMERGENCY_FUND_TAG_ID && + filters[0].type === 'TAG' + ) { + const cashPositions = await this.getCashPositions({ + cashDetails, + userCurrency, + investment: totalInvestmentInBaseCurrency, + value: filteredValueInBaseCurrency + }); + + const emergencyFundInCash = emergencyFund + .minus( + this.getEmergencyFundPositionsValueInBaseCurrency({ + activities: orders + }) + ) + .toNumber(); + + filteredValueInBaseCurrency = emergencyFund; + + accounts[UNKNOWN_KEY] = { + balance: 0, + currency: userCurrency, + current: emergencyFundInCash, + name: UNKNOWN_KEY, + original: emergencyFundInCash + }; + + holdings[userCurrency] = { + ...cashPositions[userCurrency], + investment: emergencyFundInCash, + value: emergencyFundInCash + }; + } + const summary = await this.getSummary({ impersonationId, userCurrency, - userId + userId, + balanceInBaseCurrency: cashDetails.balanceInBaseCurrency, + emergencyFundPositionsValueInBaseCurrency: + this.getEmergencyFundPositionsValueInBaseCurrency({ + activities: orders + }) }); return { @@ -1000,29 +1039,21 @@ export class PortfolioService { const portfolioStart = parseDate(transactionPoints[0].date); const startDate = this.getStartDate(dateRange, portfolioStart); - const currentPositions = await portfolioCalculator.getCurrentPositions( - startDate - ); - - const hasErrors = currentPositions.hasErrors; - const currentValue = currentPositions.currentValue.toNumber(); - const currentGrossPerformance = currentPositions.grossPerformance; - const currentGrossPerformancePercent = - currentPositions.grossPerformancePercentage; - let currentNetPerformance = currentPositions.netPerformance; - let currentNetPerformancePercent = - currentPositions.netPerformancePercentage; - const totalInvestment = currentPositions.totalInvestment; - - // if (currentGrossPerformance.mul(currentGrossPerformancePercent).lt(0)) { - // // If algebraic sign is different, harmonize it - // currentGrossPerformancePercent = currentGrossPerformancePercent.mul(-1); - // } - - // if (currentNetPerformance.mul(currentNetPerformancePercent).lt(0)) { - // // If algebraic sign is different, harmonize it - // currentNetPerformancePercent = currentNetPerformancePercent.mul(-1); - // } + const { + currentValue, + errors, + grossPerformance, + grossPerformancePercentage, + hasErrors, + netPerformance, + netPerformancePercentage, + totalInvestment + } = await portfolioCalculator.getCurrentPositions(startDate); + + const currentGrossPerformance = grossPerformance; + const currentGrossPerformancePercent = grossPerformancePercentage; + let currentNetPerformance = netPerformance; + let currentNetPerformancePercent = netPerformancePercentage; const historicalDataContainer = await this.getChart({ dateRange, @@ -1044,28 +1075,28 @@ export class PortfolioService { } return { + errors, + hasErrors, chart: historicalDataContainer.items.map( ({ date, - netPerformance, + netPerformance: netPerformanceOfItem, netPerformanceInPercentage, - totalInvestment, + totalInvestment: totalInvestmentOfItem, value }) => { return { date, - netPerformance, netPerformanceInPercentage, - totalInvestment, - value + value, + netPerformance: netPerformanceOfItem, + totalInvestment: totalInvestmentOfItem }; } ), - errors: currentPositions.errors, firstOrderDate: parseDate(historicalDataContainer.items[0]?.date), - hasErrors: currentPositions.hasErrors || hasErrors, performance: { - currentValue, + currentValue: currentValue.toNumber(), currentGrossPerformance: currentGrossPerformance.toNumber(), currentGrossPerformancePercent: currentGrossPerformancePercent.toNumber(), @@ -1167,7 +1198,7 @@ export class PortfolioService { new FeeRatioInitialInvestment( this.exchangeRateDataService, currentPositions.totalInvestment.toNumber(), - this.getFees({ orders, userCurrency }).toNumber() + this.getFees({ userCurrency, activities: orders }).toNumber() ) ], this.request.user.Settings.settings @@ -1178,16 +1209,14 @@ export class PortfolioService { private async getCashPositions({ cashDetails, - emergencyFund, investment, userCurrency, value }: { cashDetails: CashDetails; - emergencyFund: Big; investment: Big; - value: Big; userCurrency: string; + value: Big; }) { const cashPositions: PortfolioDetails['holdings'] = { [userCurrency]: this.getInitialCashPosition({ @@ -1218,62 +1247,38 @@ export class PortfolioService { } } - if (emergencyFund.gt(0)) { - cashPositions[ASSET_SUB_CLASS_EMERGENCY_FUND] = { - ...cashPositions[userCurrency], - assetSubClass: ASSET_SUB_CLASS_EMERGENCY_FUND, - investment: emergencyFund.toNumber(), - name: ASSET_SUB_CLASS_EMERGENCY_FUND, - symbol: ASSET_SUB_CLASS_EMERGENCY_FUND, - value: emergencyFund.toNumber() - }; - - cashPositions[userCurrency].investment = new Big( - cashPositions[userCurrency].investment - ) - .minus(emergencyFund) - .toNumber(); - cashPositions[userCurrency].value = new Big( - cashPositions[userCurrency].value - ) - .minus(emergencyFund) - .toNumber(); - } - for (const symbol of Object.keys(cashPositions)) { // Calculate allocations for each currency - cashPositions[symbol].allocationCurrent = value.gt(0) + cashPositions[symbol].allocationInPercentage = value.gt(0) ? new Big(cashPositions[symbol].value).div(value).toNumber() : 0; - cashPositions[symbol].allocationInvestment = investment.gt(0) - ? new Big(cashPositions[symbol].investment).div(investment).toNumber() - : 0; } return cashPositions; } private getDividend({ + activities, date = new Date(0), - orders, userCurrency }: { + activities: OrderWithAccount[]; date?: Date; - orders: OrderWithAccount[]; + userCurrency: string; }) { - return orders - .filter((order) => { - // Filter out all orders before given date and type dividend + return activities + .filter((activity) => { + // Filter out all activities before given date and type dividend return ( - isBefore(date, new Date(order.date)) && - order.type === TypeOfOrder.DIVIDEND + isBefore(date, new Date(activity.date)) && + activity.type === TypeOfOrder.DIVIDEND ); }) - .map((order) => { + .map(({ quantity, SymbolProfile, unitPrice }) => { return this.exchangeRateDataService.toCurrency( - new Big(order.quantity).mul(order.unitPrice).toNumber(), - order.SymbolProfile.currency, + new Big(quantity).mul(unitPrice).toNumber(), + SymbolProfile.currency, userCurrency ); }) @@ -1345,24 +1350,56 @@ export class PortfolioService { return dividendsByGroup; } + private getEmergencyFundPositionsValueInBaseCurrency({ + activities + }: { + activities: Activity[]; + }) { + const emergencyFundOrders = activities.filter((activity) => { + return ( + activity.tags?.some(({ id }) => { + return id === EMERGENCY_FUND_TAG_ID; + }) ?? false + ); + }); + + let valueInBaseCurrencyOfEmergencyFundPositions = new Big(0); + + for (const order of emergencyFundOrders) { + if (order.type === 'BUY') { + valueInBaseCurrencyOfEmergencyFundPositions = + valueInBaseCurrencyOfEmergencyFundPositions.plus( + order.valueInBaseCurrency + ); + } else if (order.type === 'SELL') { + valueInBaseCurrencyOfEmergencyFundPositions = + valueInBaseCurrencyOfEmergencyFundPositions.minus( + order.valueInBaseCurrency + ); + } + } + + return valueInBaseCurrencyOfEmergencyFundPositions.toNumber(); + } + private getFees({ + activities, date = new Date(0), - orders, userCurrency }: { + activities: OrderWithAccount[]; date?: Date; - orders: OrderWithAccount[]; userCurrency: string; }) { - return orders - .filter((order) => { - // Filter out all orders before given date - return isBefore(date, new Date(order.date)); + return activities + .filter((activity) => { + // Filter out all activities before given date + return isBefore(date, new Date(activity.date)); }) - .map((order) => { + .map(({ fee, SymbolProfile }) => { return this.exchangeRateDataService.toCurrency( - order.fee, - order.SymbolProfile.currency, + fee, + SymbolProfile.currency, userCurrency ); }) @@ -1381,8 +1418,7 @@ export class PortfolioService { }): PortfolioPosition { return { currency, - allocationCurrent: 0, - allocationInvestment: 0, + allocationInPercentage: 0, assetClass: AssetClass.CASH, assetSubClass: AssetClass.CASH, countries: [], @@ -1429,26 +1465,42 @@ export class PortfolioService { private getStartDate(aDateRange: DateRange, portfolioStart: Date) { switch (aDateRange) { case '1d': - portfolioStart = max([portfolioStart, subDays(new Date(), 1)]); + portfolioStart = max([ + portfolioStart, + subDays(new Date().setHours(0, 0, 0, 0), 1) + ]); break; case 'ytd': - portfolioStart = max([portfolioStart, setDayOfYear(new Date(), 1)]); + portfolioStart = max([ + portfolioStart, + setDayOfYear(new Date().setHours(0, 0, 0, 0), 1) + ]); break; case '1y': - portfolioStart = max([portfolioStart, subYears(new Date(), 1)]); + portfolioStart = max([ + portfolioStart, + subYears(new Date().setHours(0, 0, 0, 0), 1) + ]); break; case '5y': - portfolioStart = max([portfolioStart, subYears(new Date(), 5)]); + portfolioStart = max([ + portfolioStart, + subYears(new Date().setHours(0, 0, 0, 0), 5) + ]); break; } return portfolioStart; } private async getSummary({ + balanceInBaseCurrency, + emergencyFundPositionsValueInBaseCurrency, impersonationId, userCurrency, userId }: { + balanceInBaseCurrency: number; + emergencyFundPositionsValueInBaseCurrency: number; impersonationId: string; userCurrency: string; userId: string; @@ -1461,11 +1513,7 @@ export class PortfolioService { userId }); - const { balanceInBaseCurrency } = await this.accountService.getCashDetails({ - userId, - currency: userCurrency - }); - const orders = await this.orderService.getOrders({ + const activities = await this.orderService.getOrders({ userCurrency, userId }); @@ -1480,18 +1528,24 @@ export class PortfolioService { return account?.isExcluded ?? false; }); - const dividend = this.getDividend({ orders, userCurrency }).toNumber(); + const dividend = this.getDividend({ + activities, + userCurrency + }).toNumber(); const emergencyFund = new Big( (user.Settings?.settings as UserSettings)?.emergencyFund ?? 0 ); - const fees = this.getFees({ orders, userCurrency }).toNumber(); - const firstOrderDate = orders[0]?.date; - const items = this.getItems(orders).toNumber(); + const fees = this.getFees({ activities, userCurrency }).toNumber(); + const firstOrderDate = activities[0]?.date; + const items = this.getItems(activities).toNumber(); - const totalBuy = this.getTotalByType(orders, userCurrency, 'BUY'); - const totalSell = this.getTotalByType(orders, userCurrency, 'SELL'); + const totalBuy = this.getTotalByType(activities, userCurrency, 'BUY'); + const totalSell = this.getTotalByType(activities, userCurrency, 'SELL'); - const cash = new Big(balanceInBaseCurrency).minus(emergencyFund).toNumber(); + const cash = new Big(balanceInBaseCurrency) + .minus(emergencyFund) + .plus(emergencyFundPositionsValueInBaseCurrency) + .toNumber(); const committedFunds = new Big(totalBuy).minus(totalSell); const totalOfExcludedActivities = new Big( this.getTotalByType(excludedActivities, userCurrency, 'BUY') @@ -1547,8 +1601,8 @@ export class PortfolioService { totalSell, committedFunds: committedFunds.toNumber(), emergencyFund: emergencyFund.toNumber(), - ordersCount: orders.filter((order) => { - return order.type === 'BUY' || order.type === 'SELL'; + ordersCount: activities.filter(({ type }) => { + return type === 'BUY' || type === 'SELL'; }).length }; } @@ -1565,7 +1619,7 @@ export class PortfolioService { withExcludedAccounts?: boolean; }): Promise<{ transactionPoints: TransactionPoint[]; - orders: OrderWithAccount[]; + orders: Activity[]; portfolioOrders: PortfolioOrder[]; }> { const userCurrency = diff --git a/apps/api/src/helper/object.helper.ts b/apps/api/src/helper/object.helper.ts index b24ee42ee..7ee07b468 100644 --- a/apps/api/src/helper/object.helper.ts +++ b/apps/api/src/helper/object.helper.ts @@ -1,4 +1,4 @@ -import { cloneDeep, isObject } from 'lodash'; +import { cloneDeep, isArray, isObject } from 'lodash'; export function hasNotDefinedValuesInObject(aObject: Object): boolean { for (const key in aObject) { @@ -43,15 +43,23 @@ export function redactAttributes({ for (const option of options) { if (redactedObject.hasOwnProperty(option.attribute)) { - redactedObject[option.attribute] = - option.valueMap[redactedObject[option.attribute]] ?? - option.valueMap['*'] ?? - redactedObject[option.attribute]; + if (option.valueMap['*'] || option.valueMap['*'] === null) { + redactedObject[option.attribute] = option.valueMap['*']; + } else if (option.valueMap[redactedObject[option.attribute]]) { + redactedObject[option.attribute] = + option.valueMap[redactedObject[option.attribute]]; + } } else { // If the attribute is not present on the current object, // check if it exists on any nested objects for (const property in redactedObject) { - if (typeof redactedObject[property] === 'object') { + if (isArray(redactedObject[property])) { + redactedObject[property] = redactedObject[property].map( + (currentObject) => { + return redactAttributes({ options, object: currentObject }); + } + ); + } else if (isObject(redactedObject[property])) { // Recursively call the function on the nested object redactedObject[property] = redactAttributes({ options, 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 fa1b7f7f7..724cdc450 100644 --- a/apps/api/src/interceptors/redact-values-in-response.interceptor.ts +++ b/apps/api/src/interceptors/redact-values-in-response.interceptor.ts @@ -1,5 +1,5 @@ -import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { UserService } from '@ghostfolio/api/app/user/user.service'; +import { redactAttributes } from '@ghostfolio/api/helper/object.helper'; import { CallHandler, ExecutionContext, @@ -28,59 +28,35 @@ export class RedactValuesInResponseInterceptor hasImpersonationId || this.userService.isRestrictedView(request.user) ) { - if (data.accounts) { - for (const accountId of Object.keys(data.accounts)) { - if (data.accounts[accountId]?.balance !== undefined) { - data.accounts[accountId].balance = null; - } - } - } - - if (data.activities) { - data.activities = data.activities.map((activity: Activity) => { - if (activity.Account?.balance !== undefined) { - activity.Account.balance = null; - } - - if (activity.comment !== undefined) { - activity.comment = null; - } - - if (activity.fee !== undefined) { - activity.fee = null; - } - - if (activity.feeInBaseCurrency !== undefined) { - activity.feeInBaseCurrency = null; - } - - if (activity.quantity !== undefined) { - activity.quantity = null; - } - - if (activity.unitPrice !== undefined) { - activity.unitPrice = null; - } - - if (activity.value !== undefined) { - activity.value = null; - } - - if (activity.valueInBaseCurrency !== undefined) { - activity.valueInBaseCurrency = null; - } - - return activity; - }); - } - - if (data.filteredValueInBaseCurrency) { - data.filteredValueInBaseCurrency = null; - } - - if (data.totalValueInBaseCurrency) { - data.totalValueInBaseCurrency = null; - } + data = redactAttributes({ + object: data, + options: [ + 'balance', + 'balanceInBaseCurrency', + 'comment', + 'convertedBalance', + 'fee', + 'feeInBaseCurrency', + 'filteredValueInBaseCurrency', + 'grossPerformance', + 'investment', + 'netPerformance', + 'quantity', + 'symbolMapping', + 'totalBalanceInBaseCurrency', + 'totalValueInBaseCurrency', + 'unitPrice', + 'value', + 'valueInBaseCurrency' + ].map((attribute) => { + return { + attribute, + valueMap: { + '*': null + } + }; + }) + }); } return data; diff --git a/apps/api/src/services/cron.service.ts b/apps/api/src/services/cron.service.ts index e3141ebe7..8b036c35e 100644 --- a/apps/api/src/services/cron.service.ts +++ b/apps/api/src/services/cron.service.ts @@ -19,8 +19,8 @@ export class CronService { private readonly twitterBotService: TwitterBotService ) {} - @Cron(CronExpression.EVERY_HOUR) - public async runEveryHour() { + @Cron(CronExpression.EVERY_4_HOURS) + public async runEveryFourHours() { await this.dataGatheringService.gather7Days(); } diff --git a/apps/client/src/app/app-routing.module.ts b/apps/client/src/app/app-routing.module.ts index b90c19364..740d919c4 100644 --- a/apps/client/src/app/app-routing.module.ts +++ b/apps/client/src/app/app-routing.module.ts @@ -116,6 +116,13 @@ const routes: Routes = [ './pages/blog/2022/12/the-importance-of-tracking-your-personal-finances/the-importance-of-tracking-your-personal-finances-page.module' ).then((m) => m.TheImportanceOfTrackingYourPersonalFinancesPageModule) }, + { + path: 'blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt', + loadChildren: () => + import( + './pages/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt/ghostfolio-auf-sackgeld-vorgestellt-page.module' + ).then((m) => m.GhostfolioAufSackgeldVorgestelltPageModule) + }, { path: 'demo', loadChildren: () => diff --git a/apps/client/src/app/components/admin-overview/admin-overview.component.ts b/apps/client/src/app/components/admin-overview/admin-overview.component.ts index e528e748b..f25de8426 100644 --- a/apps/client/src/app/components/admin-overview/admin-overview.component.ts +++ b/apps/client/src/app/components/admin-overview/admin-overview.component.ts @@ -4,12 +4,12 @@ import { CacheService } from '@ghostfolio/client/services/cache.service'; import { DataService } from '@ghostfolio/client/services/data.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { - ghostfolioPrefix, PROPERTY_COUPONS, PROPERTY_CURRENCIES, PROPERTY_IS_READ_ONLY_MODE, PROPERTY_IS_USER_SIGNUP_ENABLED, - PROPERTY_SYSTEM_MESSAGE + PROPERTY_SYSTEM_MESSAGE, + ghostfolioPrefix } from '@ghostfolio/common/config'; import { Coupon, InfoItem, User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; diff --git a/apps/client/src/app/components/home-overview/home-overview.component.ts b/apps/client/src/app/components/home-overview/home-overview.component.ts index eb8f0c81c..be1191b17 100644 --- a/apps/client/src/app/components/home-overview/home-overview.component.ts +++ b/apps/client/src/app/components/home-overview/home-overview.component.ts @@ -110,13 +110,12 @@ export class HomeOverviewComponent implements OnDestroy, OnInit { range: this.user?.settings?.dateRange }) .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((response) => { - this.errors = response.errors; - this.hasError = response.hasErrors; - this.performance = response.performance; + .subscribe(({ chart, errors, performance }) => { + this.errors = errors; + this.performance = performance; this.isLoadingPerformance = false; - this.historicalDataItems = response.chart.map( + this.historicalDataItems = chart.map( ({ date, netPerformanceInPercentage }) => { return { date, diff --git a/apps/client/src/app/components/home-overview/home-overview.html b/apps/client/src/app/components/home-overview/home-overview.html index 9a0cbb54c..6c0644021 100644 --- a/apps/client/src/app/components/home-overview/home-overview.html +++ b/apps/client/src/app/components/home-overview/home-overview.html @@ -37,7 +37,6 @@ [baseCurrency]="user?.settings?.baseCurrency" [deviceType]="deviceType" [errors]="errors" - [hasError]="hasError" [isAllTimeHigh]="isAllTimeHigh" [isAllTimeLow]="isAllTimeLow" [isLoading]="isLoadingPerformance" diff --git a/apps/client/src/app/components/investment-chart/investment-chart.component.ts b/apps/client/src/app/components/investment-chart/investment-chart.component.ts index bfce434e8..65ecbabb8 100644 --- a/apps/client/src/app/components/investment-chart/investment-chart.component.ts +++ b/apps/client/src/app/components/investment-chart/investment-chart.component.ts @@ -11,7 +11,8 @@ import { import { getTooltipOptions, getTooltipPositionerMapTop, - getVerticalHoverLinePlugin + getVerticalHoverLinePlugin, + transformTickToAbbreviation } from '@ghostfolio/common/chart-helper'; import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config'; import { @@ -19,8 +20,7 @@ import { getBackgroundColor, getDateFormatString, getTextColor, - parseDate, - transformTickToAbbreviation + parseDate } from '@ghostfolio/common/helper'; import { LineChartItem } from '@ghostfolio/common/interfaces'; import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface'; @@ -136,10 +136,13 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy { date, investment: last(this.investments).investment }); - this.values.push({ date, value: last(this.values).value }); + this.values.push({ + date, + value: last(this.values).value + }); } - const data = { + const chartData = { labels: this.historicalDataItems.map(({ date }) => { return parseDate(date); }), @@ -191,12 +194,15 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy { if (this.chartCanvas) { if (this.chart) { - this.chart.data = data; + this.chart.data = chartData; this.chart.options.plugins.tooltip = ( this.getTooltipPluginConfiguration() ); this.chart.options.scales.x.min = this.daysInMarket - ? subDays(new Date(), this.daysInMarket).toISOString() + ? subDays( + new Date().setHours(0, 0, 0, 0), + this.daysInMarket + ).toISOString() : undefined; if ( @@ -210,7 +216,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy { this.chart.update(); } else { this.chart = new Chart(this.chartCanvas.nativeElement, { - data, + data: chartData, options: { animation: false, elements: { @@ -325,6 +331,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy { ...getTooltipOptions({ colorScheme: this.colorScheme, currency: this.isInPercent ? undefined : this.currency, + groupBy: this.groupBy, locale: this.isInPercent ? undefined : this.locale, unit: this.isInPercent ? '%' : undefined }), diff --git a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html index 5601e42cc..025259c23 100644 --- a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html +++ b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html @@ -3,14 +3,14 @@
diff --git a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts index 1d2323676..586d16ebf 100644 --- a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts +++ b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts @@ -28,7 +28,6 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit { @Input() baseCurrency: string; @Input() deviceType: string; @Input() errors: ResponseError['errors']; - @Input() hasError: boolean; @Input() isAllTimeHigh: boolean; @Input() isAllTimeLow: boolean; @Input() isLoading: boolean; 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 8d19b846e..870971cbc 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 @@ -125,7 +125,12 @@ export class PositionDetailDialog implements OnDestroy, OnInit { this.quantity = quantity; this.sectors = {}; this.SymbolProfile = SymbolProfile; - this.tags = tags; + this.tags = tags.map(({ id, name }) => { + return { + id, + name: translate(name) + }; + }); this.transactionCount = transactionCount; this.value = value; diff --git a/apps/client/src/app/pages/about/about-page.html b/apps/client/src/app/pages/about/about-page.html index 20843c292..a6b688944 100644 --- a/apps/client/src/app/pages/about/about-page.html +++ b/apps/client/src/app/pages/about/about-page.html @@ -42,9 +42,11 @@ href="https://twitter.com/ghostfolio_" title="Tweet to Ghostfolio on Twitter" >@ghostfolio_, send an e-mail to - hi@ghostfol.io, send an e-mail to + hi@ghostfol.io or open an issue at
- Valid until {{ user?.subscription?.expiresAt | date: - defaultDateFormat }} + Valid until {{ + user?.subscription?.expiresAt | date: defaultDateFormat }}
@@ -74,8 +74,8 @@
Presenter View
- Hides sensitive values such as absolute performances and - quantities. + Protection for sensitive information like absolute performances + and quantity values
@@ -210,8 +210,11 @@
-
- Zen Mode +
+
Zen Mode
+
+ Distraction-free experience for turbulent times +
Experimental Features
+
+ Sneak peek at upcoming functionality +
2022-07-23
Ghostfolio meets Internet Identity Teaser diff --git a/apps/client/src/app/pages/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt/ghostfolio-auf-sackgeld-vorgestellt-page-routing.module.ts b/apps/client/src/app/pages/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt/ghostfolio-auf-sackgeld-vorgestellt-page-routing.module.ts new file mode 100644 index 000000000..fa3ad2df1 --- /dev/null +++ b/apps/client/src/app/pages/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt/ghostfolio-auf-sackgeld-vorgestellt-page-routing.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { AuthGuard } from '@ghostfolio/client/core/auth.guard'; + +import { GhostfolioAufSackgeldVorgestelltPageComponent } from './ghostfolio-auf-sackgeld-vorgestellt-page.component'; + +const routes: Routes = [ + { + canActivate: [AuthGuard], + component: GhostfolioAufSackgeldVorgestelltPageComponent, + path: '', + title: 'Ghostfolio auf Sackgeld.com vorgestellt' + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class GhostfolioAufSackgeldVorgestelltPageRoutingModule {} diff --git a/apps/client/src/app/pages/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt/ghostfolio-auf-sackgeld-vorgestellt-page.component.ts b/apps/client/src/app/pages/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt/ghostfolio-auf-sackgeld-vorgestellt-page.component.ts new file mode 100644 index 000000000..0b0443d22 --- /dev/null +++ b/apps/client/src/app/pages/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt/ghostfolio-auf-sackgeld-vorgestellt-page.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + host: { class: 'page' }, + selector: 'gf-ghostfolio-auf-sackgeld-vorgestellt-page', + styleUrls: ['./ghostfolio-auf-sackgeld-vorgestellt-page.scss'], + templateUrl: './ghostfolio-auf-sackgeld-vorgestellt-page.html' +}) +export class GhostfolioAufSackgeldVorgestelltPageComponent {} diff --git a/apps/client/src/app/pages/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt/ghostfolio-auf-sackgeld-vorgestellt-page.html b/apps/client/src/app/pages/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt/ghostfolio-auf-sackgeld-vorgestellt-page.html new file mode 100644 index 000000000..1a149277b --- /dev/null +++ b/apps/client/src/app/pages/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt/ghostfolio-auf-sackgeld-vorgestellt-page.html @@ -0,0 +1,150 @@ +
+
+
+
+
+

Ghostfolio auf Sackgeld.com vorgestellt

+
2023-01-21
+ Ghostfolio auf Sackgeld.com vorgestellt Teaser +
+
+

+ Wir freuen uns darüber, dass unsere Open Source Portfolio Tracking + Software Ghostfolio auf dem + Fintech News Portal Sackgeld.com vorgestellt wurde. +

+
+
+

+ Ghostfolio – Open Source Wealth Management Software +

+

+ Ghostfolio ermöglicht es dir, deine Portfolio-Performance einfach zu + verfolgen und zu analysieren. Es bietet dir detaillierte + Informationen über deine Positionen, historische Entwicklung und die + Zusammenstellung deines Portfolios. Durch die Open Source-Lizenz (GNU Affero General Public License v3.0) wird die Software ständig weiterentwickelt und verbessert und du + hast sogar die Möglichkeit, dich selbst daran zu beteiligen. Wir + sind davon überzeugt, mit dem Open-Source-Ansatz von Ghostfolio das + Finanzwissen und Investieren für alle zugänglicher zu machen. +

+
+
+

Sackgeld.com – App für ein höheres Sackgeld

+

+ Das Schweizer Fintech News Portal + Sackgeld.com + informiert über die neuesten Entwicklungen und Innovationen im + Bereich FinTech. Dazu gehören News, Artikel und persönliche + Erfahrungen aus der Welt der digitalen Finanz Apps, Säule 3a, P2P + und Immobilien. +

+
+
+

+ Wenn du mehr über Ghostfolio erfahren möchtest, kannst du hier den + ganzen Artikel "Was taugt Ghostfolio als Portfolio Performance Tracking-Tool?" nachlesen. +

+

+ Wir freuen uns auf dein Feedback.
+ Thomas von Ghostfolio +

+
+
+
    +
  • + AGPL-3.0 +
  • +
  • + Aktie +
  • +
  • + Altersvorsorge +
  • +
  • + Anlage +
  • +
  • + App +
  • +
  • + Feedback +
  • +
  • + Finanzwissen +
  • +
  • + Fintech +
  • +
  • + Ghostfolio +
  • +
  • + Immobilien +
  • +
  • + Innovation +
  • +
  • + Investieren +
  • +
  • + Lizenz +
  • +
  • + Open Source +
  • +
  • + OSS +
  • +
  • + P2P +
  • +
  • + Performance +
  • +
  • + Portfolio +
  • +
  • + Sackgeld +
  • +
  • + Säule 3a +
  • +
  • + Schweiz +
  • +
  • + Software +
  • +
  • + Taschengeld +
  • +
  • + Tool +
  • +
  • + Vermögen +
  • +
  • + Wealth Management +
  • +
+
+
+
+
+
diff --git a/apps/client/src/app/pages/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt/ghostfolio-auf-sackgeld-vorgestellt-page.module.ts b/apps/client/src/app/pages/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt/ghostfolio-auf-sackgeld-vorgestellt-page.module.ts new file mode 100644 index 000000000..8cc63ce83 --- /dev/null +++ b/apps/client/src/app/pages/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt/ghostfolio-auf-sackgeld-vorgestellt-page.module.ts @@ -0,0 +1,17 @@ +import { CommonModule } from '@angular/common'; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { GhostfolioAufSackgeldVorgestelltPageRoutingModule } from './ghostfolio-auf-sackgeld-vorgestellt-page-routing.module'; +import { GhostfolioAufSackgeldVorgestelltPageComponent } from './ghostfolio-auf-sackgeld-vorgestellt-page.component'; + +@NgModule({ + declarations: [GhostfolioAufSackgeldVorgestelltPageComponent], + imports: [ + CommonModule, + GhostfolioAufSackgeldVorgestelltPageRoutingModule, + RouterModule + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class GhostfolioAufSackgeldVorgestelltPageModule {} diff --git a/apps/client/src/app/pages/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt/ghostfolio-auf-sackgeld-vorgestellt-page.scss b/apps/client/src/app/pages/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt/ghostfolio-auf-sackgeld-vorgestellt-page.scss new file mode 100644 index 000000000..5d4e87f30 --- /dev/null +++ b/apps/client/src/app/pages/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt/ghostfolio-auf-sackgeld-vorgestellt-page.scss @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/apps/client/src/app/pages/blog/blog-page.html b/apps/client/src/app/pages/blog/blog-page.html index 2e8379955..e2ebfa891 100644 --- a/apps/client/src/app/pages/blog/blog-page.html +++ b/apps/client/src/app/pages/blog/blog-page.html @@ -2,6 +2,32 @@

Blog

+ + + + +
diff --git a/apps/client/src/app/pages/features/features-page.html b/apps/client/src/app/pages/features/features-page.html index 05cdd8454..aaac67a0c 100644 --- a/apps/client/src/app/pages/features/features-page.html +++ b/apps/client/src/app/pages/features/features-page.html @@ -197,7 +197,7 @@

Multi-Language

- Use Ghostfolio in multiple languages: English, Dutch, Français, + Use Ghostfolio in multiple languages: English, Dutch, French, German, Italian, Portuguese diff --git a/apps/client/src/app/pages/landing/landing-page.html b/apps/client/src/app/pages/landing/landing-page.html index 64f943f3c..2b3024a45 100644 --- a/apps/client/src/app/pages/landing/landing-page.html +++ b/apps/client/src/app/pages/landing/landing-page.html @@ -106,7 +106,7 @@

As seen in
-
+
-
+
-
+
-
+
-
+
-
+
+ +
+