Compare commits

..

13 Commits

Author SHA1 Message Date
333b63bfe2 Release 1.198.0 (#1294) 2022-09-25 21:46:19 +02:00
3006c21b12 Add dutch translation (#1291)
* Add dutch translation
2022-09-25 18:12:33 +02:00
f01a3f893d Exclude accounts (#1289)
* Exclude accounts

* Update changelog
2022-09-25 18:02:46 +02:00
72974e888f Clean up spaces (#1288) 2022-09-25 15:14:51 +02:00
0cee7a0b35 Release 1.197.0 (#1287) 2022-09-24 13:16:47 +02:00
f3d337b044 Feature/add value of active filters on allocations page (#1286)
* Add value

* Update changelog
2022-09-24 13:15:16 +02:00
7667af059c Feature/combine performance and chart calculation (#1285)
* Combine performance and chart calculation endpoints

* Update changelog
2022-09-24 13:12:40 +02:00
1095b47f45 Feature/add multi language support to feature overview (#1284)
* Add multi-language support

* Update changelog
2022-09-24 12:29:36 +02:00
dacd7271eb Feature/improve density of various selectors (#1283)
* Improve density

* Update changelog
2022-09-24 09:58:09 +02:00
e093041184 Release 1.196.0 (#1281) 2022-09-22 21:05:05 +02:00
8f2caa508a Feature/extend landing page (#1279)
* Extend landing page

* Update changelog
2022-09-22 20:52:46 +02:00
862f670ccf Feature/setup italiano (#1276)
* Setup italiano

* Update changelog
2022-09-22 20:52:03 +02:00
54bf4c7a43 Update messages.it.xlf (#1280) 2022-09-22 20:51:31 +02:00
59 changed files with 3735 additions and 301 deletions

View File

@ -5,6 +5,32 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 1.198.0 - 25.09.2022
### Added
- Added support to exclude an account from analysis
- Set up the language localization for Nederlands (`nl`)
## 1.197.0 - 24.09.2022
### Added
- Added the value of the active filter in percentage on the allocations page
- Extended the feature overview page by multi-language support (English, German, Italian)
### Changed
- Combined the performance and chart calculation
- Improved the style of various selectors (density)
## 1.196.0 - 22.09.2022
### Added
- Set up the language localization for Italiano (`it`)
- Extended the landing page
## 1.195.0 - 20.09.2022
### Changed
@ -195,7 +221,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Set up `ng-extract-i18n-merge` to improve the i18n extraction and merge workflow
- Set up language localization for German (`de`)
- Set up the language localization for German (`de`)
- Resolved the feature graphic of the blog post
### Changed

View File

@ -140,6 +140,10 @@
"baseHref": "/it/",
"localize": ["it"]
},
"development-nl": {
"baseHref": "/nl/",
"localize": ["nl"]
},
"production": {
"fileReplacements": [
{
@ -187,6 +191,9 @@
"development-it": {
"browserTarget": "client:build:development-it"
},
"development-nl": {
"browserTarget": "client:build:development-nl"
},
"production": {
"browserTarget": "client:build:production"
}
@ -198,7 +205,11 @@
"browserTarget": "client:build",
"includeContext": true,
"outputPath": "src/locales",
"targetFiles": ["messages.de.xlf", "messages.it.xlf"]
"targetFiles": [
"messages.de.xlf",
"messages.it.xlf",
"messages.nl.xlf"
]
}
},
"lint": {
@ -225,6 +236,10 @@
"it": {
"baseHref": "/it/",
"translation": "apps/client/src/locales/messages.it.xlf"
},
"nl": {
"baseHref": "/nl/",
"translation": "apps/client/src/locales/messages.nl.xlf"
}
},
"sourceLocale": "en"

View File

@ -96,7 +96,9 @@ export class AccountController {
let accountsWithAggregations =
await this.portfolioService.getAccountsWithAggregations(
impersonationUserId || this.request.user.id
impersonationUserId || this.request.user.id,
undefined,
true
);
if (
@ -139,7 +141,8 @@ export class AccountController {
let accountsWithAggregations =
await this.portfolioService.getAccountsWithAggregations(
impersonationUserId || this.request.user.id,
[{ id, type: 'ACCOUNT' }]
[{ id, type: 'ACCOUNT' }],
true
);
if (

View File

@ -107,15 +107,23 @@ export class AccountService {
public async getCashDetails({
currency,
filters = [],
userId
userId,
withExcludedAccounts = false
}: {
currency: string;
filters?: Filter[];
userId: string;
withExcludedAccounts?: boolean;
}): Promise<CashDetails> {
let totalCashBalanceInBaseCurrency = new Big(0);
const where: Prisma.AccountWhereInput = { userId };
const where: Prisma.AccountWhereInput = {
userId
};
if (withExcludedAccounts === false) {
where.isExcluded = false;
}
const {
ACCOUNT: filtersByAccount,

View File

@ -1,5 +1,11 @@
import { AccountType } from '@prisma/client';
import { IsNumber, IsString, ValidateIf } from 'class-validator';
import {
IsBoolean,
IsNumber,
IsOptional,
IsString,
ValidateIf
} from 'class-validator';
export class CreateAccountDto {
@IsString()
@ -11,6 +17,10 @@ export class CreateAccountDto {
@IsString()
currency: string;
@IsBoolean()
@IsOptional()
isExcluded?: boolean;
@IsString()
name: string;

View File

@ -1,5 +1,11 @@
import { AccountType } from '@prisma/client';
import { IsNumber, IsString, ValidateIf } from 'class-validator';
import {
IsBoolean,
IsNumber,
IsOptional,
IsString,
ValidateIf
} from 'class-validator';
export class UpdateAccountDto {
@IsString()
@ -14,6 +20,10 @@ export class UpdateAccountDto {
@IsString()
id: string;
@IsBoolean()
@IsOptional()
isExcluded?: boolean;
@IsString()
name: string;

View File

@ -164,7 +164,7 @@ export class BenchmarkService {
);
const marketPriceAtStartDate = marketDataItems?.[0]?.marketPrice ?? 0;
return {
const response = {
marketData: [
...marketDataItems
.filter((marketDataItem, index) => {
@ -181,17 +181,22 @@ export class BenchmarkService {
marketDataItem.marketPrice
) * 100
};
}),
{
date: format(new Date(), DATE_FORMAT),
value:
this.calculateChangeInPercentage(
marketPriceAtStartDate,
currentSymbolItem.marketPrice
) * 100
}
})
]
};
if (currentSymbolItem?.marketPrice) {
response.marketData.push({
date: format(new Date(), DATE_FORMAT),
value:
this.calculateChangeInPercentage(
marketPriceAtStartDate,
currentSymbolItem.marketPrice
) * 100
});
}
return response;
}
private getMarketCondition(aPerformanceInPercent: number) {

View File

@ -11,6 +11,8 @@ import { NextFunction, Request, Response } from 'express';
export class FrontendMiddleware implements NestMiddleware {
public indexHtmlDe = '';
public indexHtmlEn = '';
public indexHtmlIt = '';
public indexHtmlNl = '';
public isProduction: boolean;
public constructor(
@ -32,6 +34,14 @@ export class FrontendMiddleware implements NestMiddleware {
this.getPathOfIndexHtmlFile(DEFAULT_LANGUAGE_CODE),
'utf8'
);
this.indexHtmlIt = fs.readFileSync(
this.getPathOfIndexHtmlFile('it'),
'utf8'
);
this.indexHtmlNl = fs.readFileSync(
this.getPathOfIndexHtmlFile('nl'),
'utf8'
);
} catch {}
}
@ -61,6 +71,24 @@ export class FrontendMiddleware implements NestMiddleware {
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else if (req.path === '/it' || req.path.startsWith('/it/')) {
res.send(
this.interpolate(this.indexHtmlIt, {
featureGraphicPath,
languageCode: 'it',
path: req.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else if (req.path === '/nl' || req.path.startsWith('/nl/')) {
res.send(
this.interpolate(this.indexHtmlNl, {
featureGraphicPath,
languageCode: 'nl',
path: req.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else {
res.send(
this.interpolate(this.indexHtmlEn, {

View File

@ -109,7 +109,8 @@ export class OrderController {
filters,
userCurrency,
includeDrafts: true,
userId: impersonationUserId || this.request.user.id
userId: impersonationUserId || this.request.user.id,
withExcludedAccounts: true
});
if (

View File

@ -189,13 +189,15 @@ export class OrderService {
includeDrafts = false,
types,
userCurrency,
userId
userId,
withExcludedAccounts = false
}: {
filters?: Filter[];
includeDrafts?: boolean;
types?: TypeOfOrder[];
userCurrency: string;
userId: string;
withExcludedAccounts?: boolean;
}): Promise<Activity[]> {
const where: Prisma.OrderWhereInput = { userId };
@ -284,24 +286,28 @@ export class OrderService {
},
orderBy: { date: 'asc' }
})
).map((order) => {
const value = new Big(order.quantity).mul(order.unitPrice).toNumber();
)
.filter((order) => {
return withExcludedAccounts || order.Account?.isExcluded === false;
})
.map((order) => {
const value = new Big(order.quantity).mul(order.unitPrice).toNumber();
return {
...order,
value,
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
order.fee,
order.SymbolProfile.currency,
userCurrency
),
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
return {
...order,
value,
order.SymbolProfile.currency,
userCurrency
)
};
});
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
order.fee,
order.SymbolProfile.currency,
userCurrency
),
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
value,
order.SymbolProfile.currency,
userCurrency
)
};
});
}
public async updateOrder({

View File

@ -272,23 +272,20 @@ export class PortfolioCalculator {
}
}
const isInPercentage = true;
return Object.keys(totalNetPerformanceValues).map((date) => {
return isInPercentage
? {
date,
value: totalInvestmentValues[date].eq(0)
? 0
: totalNetPerformanceValues[date]
.div(totalInvestmentValues[date])
.mul(100)
.toNumber()
}
: {
date,
value: totalNetPerformanceValues[date].toNumber()
};
const netPerformanceInPercentage = totalInvestmentValues[date].eq(0)
? 0
: totalNetPerformanceValues[date]
.div(totalInvestmentValues[date])
.mul(100)
.toNumber();
return {
date,
netPerformanceInPercentage,
netPerformance: totalNetPerformanceValues[date].toNumber(),
value: netPerformanceInPercentage
};
});
}

View File

@ -110,26 +110,6 @@ export class PortfolioController {
};
}
@Get('chart')
@UseGuards(AuthGuard('jwt'))
@Version('2')
public async getChartV2(
@Headers('impersonation-id') impersonationId: string,
@Query('range') range
): Promise<PortfolioChart> {
const historicalDataContainer = await this.portfolioService.getChartV2(
impersonationId,
range
);
return {
chart: historicalDataContainer.items,
hasError: false,
isAllTimeHigh: false,
isAllTimeLow: false
};
}
@Get('details')
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(RedactValuesInResponseInterceptor)
@ -168,12 +148,15 @@ export class PortfolioController {
})
];
let portfolioSummary: PortfolioSummary;
const {
accounts,
filteredValueInBaseCurrency,
filteredValueInPercentage,
hasErrors,
holdings,
summary,
totalValueInBaseCurrency
} = await this.portfolioService.getDetails(
impersonationId,
@ -186,6 +169,8 @@ export class PortfolioController {
hasError = true;
}
portfolioSummary = summary;
if (
impersonationId ||
this.userService.isRestrictedView(this.request.user)
@ -219,6 +204,22 @@ export class PortfolioController {
accounts[name].current = current / totalValue;
accounts[name].original = original / totalInvestment;
}
portfolioSummary = nullifyValuesInObject(summary, [
'cash',
'committedFunds',
'currentGrossPerformance',
'currentNetPerformance',
'currentValue',
'dividend',
'emergencyFund',
'excludedAccountsAndActivities',
'fees',
'items',
'netWorth',
'totalBuy',
'totalSell'
]);
}
let hasDetails = true;
@ -244,7 +245,8 @@ export class PortfolioController {
filteredValueInPercentage,
hasError,
holdings,
totalValueInBaseCurrency
totalValueInBaseCurrency,
summary: hasDetails ? portfolioSummary : undefined
};
}
@ -319,6 +321,35 @@ export class PortfolioController {
return performanceInformation;
}
@Get('performance')
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInResponseInterceptor)
@Version('2')
public async getPerformanceV2(
@Headers('impersonation-id') impersonationId: string,
@Query('range') dateRange
): Promise<PortfolioPerformanceResponse> {
const performanceInformation = await this.portfolioService.getPerformanceV2(
{
dateRange,
impersonationId
}
);
if (
impersonationId ||
this.request.user.Settings.settings.viewMode === 'ZEN' ||
this.userService.isRestrictedView(this.request.user)
) {
performanceInformation.performance = nullifyValuesInObject(
performanceInformation.performance,
['currentGrossPerformance', 'currentNetPerformance', 'currentValue']
);
}
return performanceInformation;
}
@Get('positions')
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInResponseInterceptor)
@ -411,46 +442,6 @@ export class PortfolioController {
return portfolioPublicDetails;
}
@Get('summary')
@UseGuards(AuthGuard('jwt'))
public async getSummary(
@Headers('impersonation-id') impersonationId
): Promise<PortfolioSummary> {
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === 'Basic'
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
let summary = await this.portfolioService.getSummary(impersonationId);
if (
impersonationId ||
this.userService.isRestrictedView(this.request.user)
) {
summary = nullifyValuesInObject(summary, [
'cash',
'committedFunds',
'currentGrossPerformance',
'currentNetPerformance',
'currentValue',
'dividend',
'emergencyFund',
'fees',
'items',
'netWorth',
'totalBuy',
'totalSell'
]);
}
return summary;
}
@Get('position/:dataSource/:symbol')
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)

View File

@ -50,8 +50,11 @@ import type {
import { Inject, Injectable } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import {
Account,
AssetClass,
DataSource,
Order,
Platform,
Prisma,
Tag,
Type as TypeOfOrder
@ -106,7 +109,8 @@ export class PortfolioService {
public async getAccounts(
aUserId: string,
aFilters?: Filter[]
aFilters?: Filter[],
withExcludedAccounts = false
): Promise<AccountWithValue[]> {
const where: Prisma.AccountWhereInput = { userId: aUserId };
@ -120,7 +124,13 @@ export class PortfolioService {
include: { Order: true, Platform: true },
orderBy: { name: 'asc' }
}),
this.getDetails(aUserId, aUserId, undefined, aFilters)
this.getDetails(
aUserId,
aUserId,
undefined,
aFilters,
withExcludedAccounts
)
]);
const userCurrency = this.request.user.Settings.settings.baseCurrency;
@ -160,9 +170,14 @@ export class PortfolioService {
public async getAccountsWithAggregations(
aUserId: string,
aFilters?: Filter[]
aFilters?: Filter[],
withExcludedAccounts = false
): Promise<Accounts> {
const accounts = await this.getAccounts(aUserId, aFilters);
const accounts = await this.getAccounts(
aUserId,
aFilters,
withExcludedAccounts
);
let totalBalanceInBaseCurrency = new Big(0);
let totalValueInBaseCurrency = new Big(0);
let transactionCount = 0;
@ -355,11 +370,14 @@ export class PortfolioService {
};
}
public async getChartV2(
aImpersonationId: string,
aDateRange: DateRange = 'max'
): Promise<HistoricalDataContainer> {
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
public async getChartV2({
dateRange = 'max',
impersonationId
}: {
dateRange?: DateRange;
impersonationId: string;
}): Promise<HistoricalDataContainer> {
const userId = await this.getUserId(impersonationId, this.request.user.id);
const { portfolioOrders, transactionPoints } =
await this.getTransactionPoints({
@ -383,7 +401,7 @@ export class PortfolioService {
const endDate = new Date();
const portfolioStart = parseDate(transactionPoints[0].date);
const startDate = this.getStartDate(aDateRange, portfolioStart);
const startDate = this.getStartDate(dateRange, portfolioStart);
const daysInMarket = differenceInDays(new Date(), startDate);
const step = Math.round(
@ -407,7 +425,8 @@ export class PortfolioService {
aImpersonationId: string,
aUserId: string,
aDateRange: DateRange = 'max',
aFilters?: Filter[]
aFilters?: Filter[],
withExcludedAccounts = false
): Promise<PortfolioDetails & { hasErrors: boolean }> {
const userId = await this.getUserId(aImpersonationId, aUserId);
const user = await this.userService.user({ id: userId });
@ -423,6 +442,7 @@ export class PortfolioService {
const { orders, portfolioOrders, transactionPoints } =
await this.getTransactionPoints({
userId,
withExcludedAccounts,
filters: aFilters
});
@ -577,6 +597,7 @@ export class PortfolioService {
portfolioItemsNow,
userCurrency,
userId,
withExcludedAccounts,
filters: aFilters
});
@ -585,6 +606,7 @@ export class PortfolioService {
return {
accounts,
holdings,
summary,
filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(),
filteredValueInPercentage: summary.netWorth
? filteredValueInBaseCurrency.div(summary.netWorth).toNumber()
@ -603,7 +625,11 @@ export class PortfolioService {
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
const orders = (
await this.orderService.getOrders({ userCurrency, userId })
await this.orderService.getOrders({
userCurrency,
userId,
withExcludedAccounts: true
})
).filter(({ SymbolProfile }) => {
return (
SymbolProfile.dataSource === aDataSource &&
@ -987,6 +1013,105 @@ export class PortfolioService {
};
}
public async getPerformanceV2({
dateRange = 'max',
impersonationId
}: {
dateRange?: DateRange;
impersonationId: string;
}): Promise<PortfolioPerformanceResponse> {
const userId = await this.getUserId(impersonationId, this.request.user.id);
const { portfolioOrders, transactionPoints } =
await this.getTransactionPoints({
userId
});
const portfolioCalculator = new PortfolioCalculator({
currency: this.request.user.Settings.settings.baseCurrency,
currentRateService: this.currentRateService,
orders: portfolioOrders
});
if (transactionPoints?.length <= 0) {
return {
chart: [],
hasErrors: false,
performance: {
currentGrossPerformance: 0,
currentGrossPerformancePercent: 0,
currentNetPerformance: 0,
currentNetPerformancePercent: 0,
currentValue: 0
}
};
}
portfolioCalculator.setTransactionPoints(transactionPoints);
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;
// 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 historicalDataContainer = await this.getChartV2({
dateRange,
impersonationId
});
const itemOfToday = historicalDataContainer.items.find((item) => {
return item.date === format(new Date(), DATE_FORMAT);
});
if (itemOfToday) {
currentNetPerformance = new Big(itemOfToday.netPerformance);
currentNetPerformancePercent = new Big(
itemOfToday.netPerformanceInPercentage
).div(100);
}
return {
chart: historicalDataContainer.items.map(
({ date, netPerformanceInPercentage }) => {
return {
date,
value: netPerformanceInPercentage
};
}
),
errors: currentPositions.errors,
hasErrors: currentPositions.hasErrors || hasErrors,
performance: {
currentValue,
currentGrossPerformance: currentGrossPerformance.toNumber(),
currentGrossPerformancePercent:
currentGrossPerformancePercent.toNumber(),
currentNetPerformance: currentNetPerformance.toNumber(),
currentNetPerformancePercent: currentNetPerformancePercent.toNumber()
}
};
}
public async getReport(impersonationId: string): Promise<PortfolioReport> {
const currency = this.request.user.Settings.settings.baseCurrency;
const userId = await this.getUserId(impersonationId, this.request.user.id);
@ -1079,74 +1204,6 @@ export class PortfolioService {
};
}
public async getSummary(aImpersonationId: string): Promise<PortfolioSummary> {
const userCurrency = this.request.user.Settings.settings.baseCurrency;
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
const user = await this.userService.user({ id: userId });
const performanceInformation = await this.getPerformance(aImpersonationId);
const { balanceInBaseCurrency } = await this.accountService.getCashDetails({
userId,
currency: userCurrency
});
const orders = await this.orderService.getOrders({
userCurrency,
userId
});
const dividend = this.getDividend(orders).toNumber();
const emergencyFund = new Big(
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
);
const fees = this.getFees(orders).toNumber();
const firstOrderDate = orders[0]?.date;
const items = this.getItems(orders).toNumber();
const totalBuy = this.getTotalByType(orders, userCurrency, 'BUY');
const totalSell = this.getTotalByType(orders, userCurrency, 'SELL');
const cash = new Big(balanceInBaseCurrency).minus(emergencyFund).toNumber();
const committedFunds = new Big(totalBuy).minus(totalSell);
const netWorth = new Big(balanceInBaseCurrency)
.plus(performanceInformation.performance.currentValue)
.plus(items)
.toNumber();
const daysInMarket = differenceInDays(new Date(), firstOrderDate);
const annualizedPerformancePercent = new PortfolioCalculator({
currency: userCurrency,
currentRateService: this.currentRateService,
orders: []
})
.getAnnualizedPerformancePercent({
daysInMarket,
netPerformancePercent: new Big(
performanceInformation.performance.currentNetPerformancePercent
)
})
?.toNumber();
return {
...performanceInformation.performance,
annualizedPerformancePercent,
cash,
dividend,
fees,
firstOrderDate,
items,
netWorth,
totalBuy,
totalSell,
committedFunds: committedFunds.toNumber(),
emergencyFund: emergencyFund.toNumber(),
ordersCount: orders.filter((order) => {
return order.type === 'BUY' || order.type === 'SELL';
}).length
};
}
private async getCashPositions({
cashDetails,
emergencyFund,
@ -1322,14 +1379,117 @@ export class PortfolioService {
return portfolioStart;
}
private async getSummary(
aImpersonationId: string
): Promise<PortfolioSummary> {
const userCurrency = this.request.user.Settings.settings.baseCurrency;
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
const user = await this.userService.user({ id: userId });
const performanceInformation = await this.getPerformance(aImpersonationId);
const { balanceInBaseCurrency } = await this.accountService.getCashDetails({
userId,
currency: userCurrency
});
const orders = await this.orderService.getOrders({
userCurrency,
userId
});
const excludedActivities = (
await this.orderService.getOrders({
userCurrency,
userId,
withExcludedAccounts: true
})
).filter(({ Account: account }) => {
return account?.isExcluded ?? false;
});
const dividend = this.getDividend(orders).toNumber();
const emergencyFund = new Big(
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
);
const fees = this.getFees(orders).toNumber();
const firstOrderDate = orders[0]?.date;
const items = this.getItems(orders).toNumber();
const totalBuy = this.getTotalByType(orders, userCurrency, 'BUY');
const totalSell = this.getTotalByType(orders, userCurrency, 'SELL');
const cash = new Big(balanceInBaseCurrency).minus(emergencyFund).toNumber();
const committedFunds = new Big(totalBuy).minus(totalSell);
const totalOfExcludedActivities = new Big(
this.getTotalByType(excludedActivities, userCurrency, 'BUY')
).minus(this.getTotalByType(excludedActivities, userCurrency, 'SELL'));
const cashDetailsWithExcludedAccounts =
await this.accountService.getCashDetails({
userId,
currency: userCurrency,
withExcludedAccounts: true
});
const excludedBalanceInBaseCurrency = new Big(
cashDetailsWithExcludedAccounts.balanceInBaseCurrency
).minus(balanceInBaseCurrency);
const excludedAccountsAndActivities = excludedBalanceInBaseCurrency
.plus(totalOfExcludedActivities)
.toNumber();
const netWorth = new Big(balanceInBaseCurrency)
.plus(performanceInformation.performance.currentValue)
.plus(items)
.plus(excludedAccountsAndActivities)
.toNumber();
const daysInMarket = differenceInDays(new Date(), firstOrderDate);
const annualizedPerformancePercent = new PortfolioCalculator({
currency: userCurrency,
currentRateService: this.currentRateService,
orders: []
})
.getAnnualizedPerformancePercent({
daysInMarket,
netPerformancePercent: new Big(
performanceInformation.performance.currentNetPerformancePercent
)
})
?.toNumber();
return {
...performanceInformation.performance,
annualizedPerformancePercent,
cash,
dividend,
excludedAccountsAndActivities,
fees,
firstOrderDate,
items,
netWorth,
totalBuy,
totalSell,
committedFunds: committedFunds.toNumber(),
emergencyFund: emergencyFund.toNumber(),
ordersCount: orders.filter((order) => {
return order.type === 'BUY' || order.type === 'SELL';
}).length
};
}
private async getTransactionPoints({
filters,
includeDrafts = false,
userId
userId,
withExcludedAccounts
}: {
filters?: Filter[];
includeDrafts?: boolean;
userId: string;
withExcludedAccounts?: boolean;
}): Promise<{
transactionPoints: TransactionPoint[];
orders: OrderWithAccount[];
@ -1343,6 +1503,7 @@ export class PortfolioService {
includeDrafts,
userCurrency,
userId,
withExcludedAccounts,
types: ['BUY', 'SELL']
});
@ -1394,17 +1555,22 @@ export class PortfolioService {
orders,
portfolioItemsNow,
userCurrency,
userId
userId,
withExcludedAccounts
}: {
filters?: Filter[];
orders: OrderWithAccount[];
portfolioItemsNow: { [p: string]: TimelinePosition };
userCurrency: string;
userId: string;
withExcludedAccounts?: boolean;
}) {
const accounts: PortfolioDetails['accounts'] = {};
let currentAccounts = [];
let currentAccounts: (Account & {
Order?: Order[];
Platform?: Platform;
})[] = [];
if (filters.length === 0) {
currentAccounts = await this.accountService.getAccounts(userId);
@ -1424,6 +1590,10 @@ export class PortfolioService {
});
}
currentAccounts = currentAccounts.filter((account) => {
return withExcludedAccounts || account.isExcluded === false;
});
for (const account of currentAccounts) {
const ordersByAccount = orders.filter(({ accountId }) => {
return accountId === account.id;

View File

@ -61,7 +61,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
.subscribe(({ accountType, name, Platform, valueInBaseCurrency }) => {
this.accountType = accountType;
this.name = name;
this.platformName = Platform?.name;
this.platformName = Platform?.name ?? '-';
this.valueInBaseCurrency = valueInBaseCurrency;
this.changeDetectorRef.markForCheck();

View File

@ -21,10 +21,12 @@
<div class="row">
<div class="col-6 mb-3">
<gf-value size="medium" [value]="accountType">Account Type</gf-value>
<gf-value i18n size="medium" [value]="accountType"
>Account Type</gf-value
>
</div>
<div class="col-6 mb-3">
<gf-value size="medium" [value]="platformName">Platform</gf-value>
<gf-value i18n size="medium" [value]="platformName">Platform</gf-value>
</div>
</div>

View File

@ -2,7 +2,10 @@
<div class="row">
<div class="col">
<form class="align-items-center d-flex" [formGroup]="filterForm">
<mat-form-field appearance="outline" class="flex-grow-1">
<mat-form-field
appearance="outline"
class="compact-with-outline flex-grow-1 mr-2 without-hint"
>
<mat-select formControlName="status">
<mat-option></mat-option>
<mat-option
@ -13,7 +16,7 @@
</mat-select>
</mat-form-field>
<button
class="ml-1"
class="mt-1"
color="warn"
mat-flat-button
(click)="onDeleteJobs()"

View File

@ -162,8 +162,11 @@
</button>
</div>
<div class="mt-2">
<form #couponForm="ngForm">
<mat-form-field appearance="outline" class="mr-2">
<form #couponForm="ngForm" class="align-items-center d-flex">
<mat-form-field
appearance="outline"
class="compact-with-outline mr-2 without-hint"
>
<mat-select
name="duration"
[value]="couponDuration"
@ -176,6 +179,7 @@
</mat-select>
</mat-form-field>
<button
class="mt-1"
color="primary"
mat-flat-button
(click)="onAddCoupon()"

View File

@ -10,7 +10,11 @@
</div>
</div>
<div class="col-md-6 col-xs-12 d-flex justify-content-end">
<mat-form-field appearance="outline" class="w-100" color="accent">
<mat-form-field
appearance="outline"
class="w-100 without-hint"
color="accent"
>
<mat-label i18n>Compare with...</mat-label>
<mat-select
name="benchmark"
@ -26,7 +30,7 @@
</mat-form-field>
</div>
</div>
<div *ngIf="user.settings.viewMode !== 'ZEN'" class="mb-3 text-center">
<div *ngIf="user.settings.viewMode !== 'ZEN'" class="my-2 text-center">
<gf-toggle
[defaultValue]="user?.settings?.dateRange"
[isLoading]="isLoading"

View File

@ -76,8 +76,6 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
!this.hasImpersonationId &&
!this.user.settings.isRestrictedView &&
this.user.settings.viewMode !== 'ZEN';
this.update();
}
public onChangeDateRange(dateRange: DateRange) {
@ -104,36 +102,51 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
}
private update() {
this.historicalDataItems = null;
this.isLoadingPerformance = true;
this.dataService
.fetchChart({
.fetchPortfolioPerformance({
range: this.user?.settings?.dateRange,
version: this.user?.settings?.isExperimentalFeatures ? 2 : 1
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((chartData) => {
this.historicalDataItems = chartData.chart.map((chartDataItem) => {
return {
date: chartDataItem.date,
value: chartDataItem.value
};
});
this.isAllTimeHigh = chartData.isAllTimeHigh;
this.isAllTimeLow = chartData.isAllTimeLow;
this.changeDetectorRef.markForCheck();
});
this.dataService
.fetchPortfolioPerformance({ range: this.user?.settings?.dateRange })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => {
this.errors = response.errors;
this.hasError = response.hasErrors;
this.performance = response.performance;
this.isLoadingPerformance = false;
if (this.user?.settings?.isExperimentalFeatures) {
this.historicalDataItems = response.chart.map(({ date, value }) => {
return {
date,
value
};
});
} else {
this.dataService
.fetchChart({
range: this.user?.settings?.dateRange,
version: 1
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((chartData) => {
this.historicalDataItems = chartData.chart.map(
({ date, value }) => {
return {
date,
value
};
}
);
this.isAllTimeHigh = chartData.isAllTimeHigh;
this.isAllTimeLow = chartData.isAllTimeLow;
this.changeDetectorRef.markForCheck();
});
}
this.changeDetectorRef.markForCheck();
});

View File

@ -1,8 +1,18 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import {
MatSnackBar,
MatSnackBarRef,
TextOnlySnackBar
} from '@angular/material/snack-bar';
import { Router } from '@angular/router';
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 { PortfolioSummary, User } from '@ghostfolio/common/interfaces';
import {
InfoItem,
PortfolioSummary,
User
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@ -14,8 +24,11 @@ import { takeUntil } from 'rxjs/operators';
})
export class HomeSummaryComponent implements OnDestroy, OnInit {
public hasImpersonationId: boolean;
public hasPermissionForSubscription: boolean;
public hasPermissionToUpdateUserSettings: boolean;
public info: InfoItem;
public isLoading = true;
public snackBarRef: MatSnackBarRef<TextOnlySnackBar>;
public summary: PortfolioSummary;
public user: User;
@ -25,8 +38,17 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private impersonationStorageService: ImpersonationStorageService,
private router: Router,
private snackBar: MatSnackBar,
private userService: UserService
) {
this.info = this.dataService.fetchInfo();
this.hasPermissionForSubscription = hasPermission(
this.info?.globalPermissions,
permissions.enableSubscription
);
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
@ -50,8 +72,6 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
.subscribe((aId) => {
this.hasImpersonationId = !!aId;
});
this.update();
}
public onChangeEmergencyFund(emergencyFund: number) {
@ -81,12 +101,30 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
this.isLoading = true;
this.dataService
.fetchPortfolioSummary()
.fetchPortfolioDetails({})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => {
this.summary = response;
.subscribe(({ summary }) => {
this.summary = summary;
this.isLoading = false;
if (!this.summary) {
this.snackBarRef = this.snackBar.open(
$localize`This feature requires a subscription.`,
this.hasPermissionForSubscription
? $localize`Upgrade Plan`
: undefined,
{ duration: 6000 }
);
this.snackBarRef.afterDismissed().subscribe(() => {
this.snackBarRef = undefined;
});
this.snackBarRef.onAction().subscribe(() => {
this.router.navigate(['/pricing']);
});
}
this.changeDetectorRef.markForCheck();
});

View File

@ -172,6 +172,17 @@
></gf-value>
</div>
</div>
<div class="row px-3 py-1">
<div class="d-flex flex-grow-1" i18n>Excluded from Analysis</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
[currency]="baseCurrency"
[locale]="locale"
[value]="isLoading ? undefined : summary?.excludedAccountsAndActivities"
></gf-value>
</div>
</div>
<div class="row">
<div class="col"><hr /></div>
</div>

View File

@ -54,7 +54,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
public hasPermissionToUpdateViewMode: boolean;
public hasPermissionToUpdateUserSettings: boolean;
public language = document.documentElement.lang;
public locales = ['de', 'de-CH', 'en-GB', 'en-US'];
public locales = ['de', 'de-CH', 'en-GB', 'en-US', 'it', 'nl'];
public price: number;
public priceId: string;
public snackBarRef: MatSnackBarRef<TextOnlySnackBar>;

View File

@ -94,7 +94,10 @@
<ng-container i18n>Base Currency</ng-container>
</div>
<div class="pl-1 w-50">
<mat-form-field appearance="outline" class="w-100">
<mat-form-field
appearance="outline"
class="compact-with-outline w-100 without-hint"
>
<mat-select
name="baseCurrency"
[disabled]="!hasPermissionToUpdateUserSettings"
@ -116,7 +119,10 @@
<div class="hint-text text-muted" i18n>Beta</div>
</div>
<div class="pl-1 w-50">
<mat-form-field appearance="outline" class="w-100">
<mat-form-field
appearance="outline"
class="compact-with-outline w-100 without-hint"
>
<mat-select
name="language"
[disabled]="!hasPermissionToUpdateUserSettings"
@ -126,6 +132,8 @@
<mat-option [value]="null"></mat-option>
<mat-option value="de">Deutsch</mat-option>
<mat-option value="en">English</mat-option>
<mat-option value="it">Italiano</mat-option>
<mat-option value="nl">Nederlands</mat-option>
</mat-select>
</mat-form-field>
</div>
@ -138,7 +146,10 @@
</div>
</div>
<div class="pl-1 w-50">
<mat-form-field appearance="outline" class="w-100">
<mat-form-field
appearance="outline"
class="compact-with-outline w-100 without-hint"
>
<mat-select
name="locale"
[disabled]="!hasPermissionToUpdateUserSettings"
@ -161,7 +172,10 @@
</div>
<div class="pl-1 w-50">
<div class="align-items-center d-flex overflow-hidden">
<mat-form-field appearance="outline" class="w-100">
<mat-form-field
appearance="outline"
class="compact-with-outline w-100 without-hint"
>
<mat-select
name="viewMode"
[disabled]="!hasPermissionToUpdateViewMode"

View File

@ -59,8 +59,8 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
this.openCreateAccountDialog();
} else if (params['editDialog']) {
if (this.accounts) {
const account = this.accounts.find((account) => {
return account.id === params['accountId'];
const account = this.accounts.find(({ id }) => {
return id === params['accountId'];
});
this.openUpdateAccountDialog(account);
@ -155,6 +155,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
balance,
currency,
id,
isExcluded,
name,
platformId
}: AccountModel): void {
@ -165,6 +166,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
balance,
currency,
id,
isExcluded,
name,
platformId
}
@ -231,6 +233,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
accountType: AccountType.SECURITIES,
balance: 0,
currency: this.user?.settings?.baseCurrency,
isExcluded: false,
name: null,
platformId: null
}

View File

@ -50,6 +50,14 @@
</mat-select>
</mat-form-field>
</div>
<div class="mb-3 px-2">
<mat-checkbox
color="primary"
name="isExcluded"
[(ngModel)]="data.account.isExcluded"
>Exclude from Analysis</mat-checkbox
>
</div>
<div *ngIf="data.account.id">
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Account ID</mat-label>

View File

@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
@ -15,6 +16,7 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.c
CommonModule,
FormsModule,
MatButtonModule,
MatCheckboxModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule,

View File

@ -192,6 +192,17 @@
</div>
</mat-card>
</div>
<div class="col-xs-12 col-md-4 mb-3">
<mat-card class="d-flex flex-column h-100">
<div class="flex-grow-1">
<h4>Multi-Language</h4>
<p class="m-0">
Use Ghostfolio in multiple languages: English, German and
Italian are currently supported.
</p>
</div>
</mat-card>
</div>
<div class="col-xs-12 col-md-4 mb-3">
<mat-card class="d-flex flex-column h-100">
<div class="flex-grow-1">

View File

@ -55,6 +55,28 @@
</div>
</div>
<div class="row my-3">
<div class="col-md-4 my-2">
<mat-card>
<mat-card-title class="text-center">360° View</mat-card-title>
Get the full picture of your personal finances across multiple
platforms.
</mat-card>
</div>
<div class="col-md-4 my-2">
<mat-card>
<mat-card-title class="text-center">Web3 Ready</mat-card-title>
Use Ghostfolio anonymously and own your financial data.
</mat-card>
</div>
<div class="col-md-4 my-2">
<mat-card>
<mat-card-title class="text-center">Open Source</mat-card-title>
Benefit from continuous improvements through a strong community.
</mat-card>
</div>
</div>
<div class="row my-5">
<div class="col-md-6 offset-md-3">
<h2 class="h4 mb-1 text-center">Why <strong>Ghostfolio</strong>?</h2>
@ -133,19 +155,43 @@
</div>
</div>
<div class="row my-5">
<div class="col-md-6 offset-md-3">
<div class="row my-3">
<div class="col-12">
<h2 class="h4 mb-1 text-center">
How does <strong>Ghostfolio</strong> work?
</h2>
<p class="lead mb-3 text-center">Get started in only 3 steps</p>
<ol class="m-0 pl-3">
<li class="mb-2">
Sign up anonymously<br />(no e-mail address nor credit card required)
</li>
<li class="mb-2">Add any of your historical transactions</li>
<li>Get valuable insights of your portfolio composition</li>
</ol>
</div>
<div class="col-md-4 my-2">
<mat-card class="d-flex flex-row h-100">
<div class="flex-grow-1">
<div class="font-weight-bold">Sign up anonymously*</div>
<div class="text-muted">
<small>* no e-mail address nor credit card required</small>
</div>
</div>
<div class="pl-2 text-muted text-right">1</div>
</mat-card>
</div>
<div class="col-md-4 my-2">
<mat-card class="d-flex flex-row h-100">
<div class="flex-grow-1">
<div class="font-weight-bold">
Add any of your historical transactions
</div>
</div>
<div class="pl-2 text-muted text-right">2</div>
</mat-card>
</div>
<div class="col-md-4 my-2">
<mat-card class="d-flex flex-row h-100">
<div class="flex-grow-1">
<div class="font-weight-bold">
Get valuable insights of your portfolio composition
</div>
</div>
<div class="pl-2 text-muted text-right">3</div>
</mat-card>
</div>
</div>

View File

@ -1,6 +1,7 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { RouterModule } from '@angular/router';
import { GfLogoModule } from '@ghostfolio/ui/logo';
@ -14,6 +15,7 @@ import { LandingPageComponent } from './landing-page.component';
GfLogoModule,
LandingPageRoutingModule,
MatButtonModule,
MatCardModule,
RouterModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]

View File

@ -13,8 +13,16 @@
<div class="row">
<div class="col">
<mat-card class="mb-3">
<mat-card-header>
<mat-card-title i18n>Proportion of Net Worth</mat-card-title>
<mat-card-header class="overflow-hidden w-100">
<mat-card-title class="text-truncate" i18n
>Proportion of Net Worth</mat-card-title
>
<gf-value
class="align-items-end flex-grow-1 ml-2"
size="medium"
[isPercent]="true"
[value]="isLoading ? undefined : portfolioDetails?.filteredValueInPercentage"
></gf-value>
</mat-card-header>
<mat-card-content>
<mat-progress-bar

View File

@ -20,6 +20,7 @@
::ng-deep {
.mat-card-header-text {
flex: 1 1 auto;
overflow: hidden;
}
}

View File

@ -126,7 +126,10 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
this.isLoadingBenchmarkComparator = true;
this.dataService
.fetchChart({ range: this.user?.settings?.dateRange, version: 2 })
.fetchPortfolioPerformance({
range: this.user?.settings?.dateRange,
version: 2
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ chart }) => {
this.firstOrderDate = new Date(chart?.[0]?.date ?? new Date());

View File

@ -37,14 +37,14 @@ export class FirePageComponent implements OnDestroy, OnInit {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
this.dataService
.fetchPortfolioSummary()
.fetchPortfolioDetails({})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ cash, currentValue }) => {
if (cash === null || currentValue === null) {
.subscribe(({ summary }) => {
if (summary.cash === null || summary.currentValue === null) {
return;
}
this.fireWealth = new Big(currentValue);
this.fireWealth = new Big(summary.currentValue);
this.withdrawalRatePerYear = this.fireWealth.mul(4).div(100);
this.withdrawalRatePerMonth = this.withdrawalRatePerYear.div(12);

View File

@ -31,7 +31,6 @@ import {
PortfolioPerformanceResponse,
PortfolioPublicDetails,
PortfolioReport,
PortfolioSummary,
UniqueAsset,
User
} from '@ghostfolio/common/interfaces';
@ -302,7 +301,11 @@ export class DataService {
);
}
public fetchPortfolioDetails({ filters }: { filters?: Filter[] }) {
public fetchPortfolioDetails({
filters
}: {
filters?: Filter[];
}): Observable<PortfolioDetails> {
let params = new HttpParams();
if (filters?.length > 0) {
@ -348,17 +351,32 @@ export class DataService {
}
}
return this.http.get<PortfolioDetails>('/api/v1/portfolio/details', {
params
});
return this.http
.get<any>('/api/v1/portfolio/details', {
params
})
.pipe(
map((response) => {
if (response.summary?.firstOrderDate) {
response.summary.firstOrderDate = parseISO(
response.summary.firstOrderDate
);
}
return response;
})
);
}
public fetchPortfolioPerformance(params: { [param: string]: any }) {
public fetchPortfolioPerformance({
range,
version
}: {
range: DateRange;
version: number;
}) {
return this.http.get<PortfolioPerformanceResponse>(
'/api/v1/portfolio/performance',
{
params
}
`/api/v${version}/portfolio/performance`,
{ params: { range } }
);
}
@ -372,18 +390,6 @@ export class DataService {
return this.http.get<PortfolioReport>('/api/v1/portfolio/report');
}
public fetchPortfolioSummary(): Observable<PortfolioSummary> {
return this.http.get<any>('/api/v1/portfolio/summary').pipe(
map((summary) => {
if (summary.firstOrderDate) {
summary.firstOrderDate = parseISO(summary.firstOrderDate);
}
return summary;
})
);
}
public fetchPositionDetail({
dataSource,
symbol

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -146,13 +146,6 @@ ngx-skeleton-loader {
@include gf-table;
}
.mat-fab,
.mat-flat-button {
&.mat-primary {
color: rgba(var(--light-primary-text)) !important;
}
}
.mat-card {
&:not([class*='mat-elevation-z']) {
border: 1px solid rgba(var(--dark-dividers));
@ -164,6 +157,49 @@ ngx-skeleton-loader {
margin: 0 !important;
}
.mat-fab,
.mat-flat-button {
&.mat-primary {
color: rgba(var(--light-primary-text)) !important;
}
}
.mat-form-field {
&.compact-with-outline {
.mat-form-field-wrapper {
margin: 0.5rem 0 0.25rem;
padding-bottom: 1rem;
.mat-form-field-infix {
border-top-width: 0;
padding: 1rem 0 0.75rem;
.mat-form-field-label {
margin-top: 0.1rem;
}
.mat-select-arrow-wrapper {
transform: none;
}
}
.mat-form-field-prefix {
top: 0;
}
.mat-form-field-suffix {
top: 0;
}
}
}
&.without-hint {
.mat-form-field-wrapper {
padding-bottom: 0;
}
}
}
.no-min-width {
min-width: unset !important;
}

View File

@ -1,7 +1,7 @@
import * as currencies from '@dinero.js/currencies';
import { DataSource } from '@prisma/client';
import { getDate, getMonth, getYear, parse, subDays } from 'date-fns';
import { de } from 'date-fns/locale';
import { de, it, nl } from 'date-fns/locale';
import { ghostfolioScraperApiSymbolPrefix, locale } from './config';
import { Benchmark } from './interfaces';
@ -75,6 +75,10 @@ export function getCssVariable(aCssVariable: string) {
export function getDateFnsLocale(aLanguageCode: string) {
if (aLanguageCode === 'de') {
return de;
} else if (aLanguageCode === 'it') {
return it;
} else if (aLanguageCode === 'nl') {
return nl;
}
return undefined;

View File

@ -2,5 +2,7 @@ export interface HistoricalDataItem {
averagePrice?: number;
date: string;
grossPerformancePercent?: number;
netPerformance?: number;
netPerformanceInPercentage?: number;
value: number;
}

View File

@ -1,4 +1,7 @@
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
import {
PortfolioPosition,
PortfolioSummary
} from '@ghostfolio/common/interfaces';
export interface PortfolioDetails {
accounts: {
@ -13,5 +16,6 @@ export interface PortfolioDetails {
filteredValueInBaseCurrency?: number;
filteredValueInPercentage: number;
holdings: { [symbol: string]: PortfolioPosition };
summary: PortfolioSummary;
totalValueInBaseCurrency?: number;
}

View File

@ -3,9 +3,10 @@ import { PortfolioPerformance } from './portfolio-performance.interface';
export interface PortfolioSummary extends PortfolioPerformance {
annualizedPerformancePercent: number;
cash: number;
dividend: number;
committedFunds: number;
dividend: number;
emergencyFund: number;
excludedAccountsAndActivities: number;
fees: number;
firstOrderDate: Date;
items: number;

View File

@ -1,6 +1,8 @@
import { HistoricalDataItem } from '../historical-data-item.interface';
import { PortfolioPerformance } from '../portfolio-performance.interface';
import { ResponseError } from './errors.interface';
export interface PortfolioPerformanceResponse extends ResponseError {
chart?: HistoricalDataItem[];
performance: PortfolioPerformance;
}

View File

@ -93,7 +93,7 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy {
}
public ngOnChanges() {
if (this.historicalDataItems) {
if (this.historicalDataItems || this.historicalDataItems === null) {
setTimeout(() => {
// Wait for the chartCanvas
this.initialize();

View File

@ -1,6 +1,6 @@
{
"name": "ghostfolio",
"version": "1.195.0",
"version": "1.198.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"scripts": {

View File

@ -1,5 +1,5 @@
-- AlterTable
ALTER TABLE "Order" ADD COLUMN "symbolProfileId" TEXT;
ALTER TABLE "Order" ADD COLUMN "symbolProfileId" TEXT;
-- CreateTable
CREATE TABLE "SymbolProfile" (

View File

@ -1,5 +1,5 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "authChallenge" TEXT;
ALTER TABLE "User" ADD COLUMN "authChallenge" TEXT;
-- CreateTable
CREATE TABLE "AuthDevice" (

View File

@ -1,2 +1,2 @@
-- AlterTable
ALTER TABLE "SymbolProfile" ADD COLUMN "sectors" JSONB;
ALTER TABLE "SymbolProfile" ADD COLUMN "sectors" JSONB;

View File

@ -2,5 +2,5 @@
ALTER TYPE "AccountType" ADD VALUE 'CASH';
-- AlterTable
ALTER TABLE "Account" ADD COLUMN "balance" DOUBLE PRECISION NOT NULL DEFAULT 0,
ADD COLUMN "currency" "Currency" NOT NULL DEFAULT E'USD';
ALTER TABLE "Account" ADD COLUMN "balance" DOUBLE PRECISION NOT NULL DEFAULT 0,
ADD COLUMN "currency" "Currency" NOT NULL DEFAULT E'USD';

View File

@ -1,2 +1,2 @@
-- AlterTable
ALTER TABLE "SymbolProfile" ADD COLUMN "currency" "Currency";
ALTER TABLE "SymbolProfile" ADD COLUMN "currency" "Currency";

View File

@ -1,2 +1,2 @@
-- AlterTable
ALTER TABLE "Order" ADD COLUMN "isDraft" BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE "Order" ADD COLUMN "isDraft" BOOLEAN NOT NULL DEFAULT false;

View File

@ -2,4 +2,4 @@
CREATE TYPE "AssetClass" AS ENUM ('CASH', 'COMMODITY', 'EQUITY');
-- AlterTable
ALTER TABLE "SymbolProfile" ADD COLUMN "assetClass" "AssetClass";
ALTER TABLE "SymbolProfile" ADD COLUMN "assetClass" "AssetClass";

View File

@ -1,2 +1,2 @@
-- AlterTable
ALTER TABLE "Settings" ADD COLUMN "settings" JSONB;
ALTER TABLE "Settings" ADD COLUMN "settings" JSONB;

View File

@ -2,4 +2,4 @@
CREATE TYPE "AssetSubClass" AS ENUM ('CRYPTOCURRENCY', 'ETF', 'STOCK');
-- AlterTable
ALTER TABLE "SymbolProfile" ADD COLUMN "assetSubClass" "AssetSubClass";
ALTER TABLE "SymbolProfile" ADD COLUMN "assetSubClass" "AssetSubClass";

View File

@ -1,2 +1,2 @@
-- AlterTable
ALTER TABLE "MarketData" ADD COLUMN "dataSource" "DataSource" NOT NULL DEFAULT E'YAHOO';
ALTER TABLE "MarketData" ADD COLUMN "dataSource" "DataSource" NOT NULL DEFAULT E'YAHOO';

View File

@ -1,2 +1,2 @@
-- AlterTable
ALTER TABLE "SymbolProfile" ADD COLUMN "symbolMapping" JSONB;
ALTER TABLE "SymbolProfile" ADD COLUMN "symbolMapping" JSONB;

View File

@ -1,2 +1,2 @@
-- AlterTable
ALTER TABLE "SymbolProfile" ADD COLUMN "scraperConfiguration" JSONB;
ALTER TABLE "SymbolProfile" ADD COLUMN "scraperConfiguration" JSONB;

View File

@ -1,2 +1,2 @@
-- AlterTable
ALTER TABLE "SymbolProfile" ADD COLUMN "url" TEXT;
ALTER TABLE "SymbolProfile" ADD COLUMN "url" TEXT;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Account" ADD COLUMN "isExcluded" BOOLEAN NOT NULL DEFAULT false;

View File

@ -27,6 +27,7 @@ model Account {
currency String?
id String @default(uuid())
isDefault Boolean @default(false)
isExcluded Boolean @default(false)
name String?
platformId String?
updatedAt DateTime @updatedAt